Rework BlueMap claiming and expand ranks guilds chat and warps

This commit is contained in:
scoped committed 2026-09-30 18:09:18 -04:00
1 parent 434ed5338a
commit 6a2944a88e
68 files changed
+1453 -2500

No files matched your search

+108 -2
View File
@@ -17,7 +17,16 @@
let allGuilds = $state<Guild[]>([]);
let members = $state<GuildMember[]>([]);
let posts = $state<GuildPost[]>([]);
let activeTab = $state<'territory' | 'members' | 'feed' | 'wallet'>('territory');
let activeTab = $state<'territory' | 'members' | 'feed' | 'wallet' | 'settings'>('territory');
type GuildRole = { id: number; name: string; can_invite: boolean; can_kick: boolean; can_claim: boolean; can_post: boolean; can_manage: boolean };
type JoinRequest = { uuid: string; name: string; message: string; created_at: string };
let roles = $state<GuildRole[]>([]);
let joinRequests = $state<JoinRequest[]>([]);
let newRoleName = $state('');
let roleFlags = $state({ can_invite: false, can_kick: false, can_claim: false, can_post: true, can_manage: false });
let guildEdit = $state({ description: '', motd: '', icon_url: '', banner_url: '' });
const myRole = $derived(members.find((m) => m.uuid === activeAccount()?.uuid)?.role ?? '');
const canManageDetails = $derived(myRole === 'leader' || myRole === 'officer' || !!roles.find((r) => r.name === myRole)?.can_manage);
let loading = $state(false);
let walletServers = $state<Array<{ id: number; name: string; instance_id: string }>>([]);
@@ -58,9 +67,14 @@
]);
members = mList;
posts = pList;
roles = await invoke<GuildRole[]>('get_guild_roles', { guildId: mine.id }).catch(() => []);
joinRequests = await invoke<JoinRequest[]>('get_guild_join_requests', { guildId: mine.id }).catch(() => []);
guildEdit = { description: mine.description ?? '', motd: mine.motd ?? '', icon_url: mine.icon_url ?? '', banner_url: mine.banner_url ?? '' };
} else {
members = [];
posts = [];
roles = [];
joinRequests = [];
}
} catch (e: any) {
toast(e?.message ?? 'Failed to load guild data', 'error');
@@ -85,6 +99,59 @@
}
}
async function requestJoin(guild: Guild) {
try {
await invoke('request_guild_join', { guildId: guild.id, message: '' });
toast(`Request sent to ${guild.name}`, 'ok');
} catch (e: any) { toast(String(e), 'error'); }
}
async function answerJoin(req: JoinRequest, accept: boolean) {
if (!myGuild) return;
try {
await invoke('respond_guild_join_request', { guildId: myGuild.id, uuid: req.uuid, accept });
toast(accept ? `${req.name} joined the guild` : 'Request declined', 'ok');
await loadInstanceGuildData();
} catch (e: any) { toast(String(e), 'error'); }
}
async function createRole() {
if (!myGuild || !newRoleName.trim()) return;
try {
await invoke('create_guild_role', { guildId: myGuild.id, role: { name: newRoleName.trim(), ...roleFlags } });
newRoleName = '';
roles = await invoke<GuildRole[]>('get_guild_roles', { guildId: myGuild.id });
toast('Guild role created', 'ok');
} catch (e: any) { toast(String(e), 'error'); }
}
async function assignRole(member: GuildMember, role: string) {
if (!myGuild) return;
try {
await invoke('assign_guild_role', { guildId: myGuild.id, uuid: member.uuid, role });
member.role = role as GuildMember['role'];
toast('Guild role assigned', 'ok');
} catch (e: any) { toast(String(e), 'error'); }
}
async function deleteRole(role: GuildRole) {
if (!myGuild) return;
try {
await invoke('delete_guild_role', { guildId: myGuild.id, roleId: role.id });
await loadInstanceGuildData();
toast('Guild role deleted', 'ok');
} catch (e: any) { toast(String(e), 'error'); }
}
async function saveGuild() {
if (!myGuild) return;
try {
await invoke('update_guild', { guildId: myGuild.id, description: guildEdit.description, motd: guildEdit.motd, iconUrl: guildEdit.icon_url, bannerUrl: guildEdit.banner_url });
await loadInstanceGuildData();
toast('Guild details saved', 'ok');
} catch (e: any) { toast(String(e), 'error'); }
}
onMount(async () => {
loadInvites();
try { walletServers = (await invoke<{ servers: typeof walletServers }>('get_public_servers')).servers; }
@@ -242,6 +309,7 @@
<button class:active={activeTab === 'wallet'} onclick={() => (activeTab = 'wallet')}>
<Landmark size={15} /> Treasury
</button>
{#if canManageDetails}<button class:active={activeTab === 'settings'} onclick={() => (activeTab = 'settings')}>Guild settings</button>{/if}
</div>
</div>
@@ -270,6 +338,13 @@
<GuildWallet guild={myGuild} {members} servers={walletServers} instanceId={selectedInstId} />
{:else if activeTab === 'members'}
<div class="members-tab">
{#if joinRequests.length}
<div class="card"><h3>Join requests</h3>
{#each joinRequests as req}
<div class="row"><strong>{req.name}</strong><span>{req.message}</span><span class="spacer"></span><button class="sm" onclick={() => answerJoin(req, false)}>Decline</button><button class="sm primary" onclick={() => answerJoin(req, true)}>Accept</button></div>
{/each}
</div>
{/if}
<div class="members-grid">
{#each members as m (m.uuid)}
<div
@@ -292,16 +367,42 @@
<span class="badge leader-badge"><Crown size={11} /> Leader</span>
{:else if m.role === 'officer'}
<span class="badge officer-badge"><UserCheck size={11} /> Officer</span>
{:else}
{:else if m.role === 'member'}
<span class="badge member-role-badge">Member</span>
{:else}
<span class="badge member-role-badge">{m.role}</span>
{/if}
</div>
<span class="tiny muted">Joined {new Date(m.joined_at).toLocaleDateString()}</span>
{#if myGuild.leader_uuid === activeAccount()?.uuid && m.uuid !== activeAccount()?.uuid}
<select value={m.role} aria-label="Role for {m.name}" onchange={(e) => assignRole(m, e.currentTarget.value)}>
<option value="member">Member</option><option value="officer">Officer</option>
{#each roles as role}<option value={role.name}>{role.name}</option>{/each}
</select>
{/if}
</div>
</div>
{/each}
</div>
</div>
{:else if activeTab === 'settings'}
<div class="card col">
<h3>Guild appearance</h3>
<label>Description<textarea bind:value={guildEdit.description} maxlength="500"></textarea></label>
<label>Message of the day<input bind:value={guildEdit.motd} maxlength="200" /></label>
<label>Icon URL<input bind:value={guildEdit.icon_url} type="url" /></label>
<label>Banner URL<input bind:value={guildEdit.banner_url} type="url" /></label>
<button class="primary" onclick={saveGuild}>Save guild details</button>
</div>
{#if myRole === 'leader'}<div class="card col">
<h3>Guild roles</h3>
{#each roles as role}
<div class="row"><strong>{role.name}</strong><span class="tiny muted">{[role.can_invite && 'Invite', role.can_kick && 'Kick', role.can_claim && 'Claim', role.can_post && 'Post', role.can_manage && 'Manage'].filter(Boolean).join(', ') || 'No extra permissions'}</span><span class="spacer"></span><button class="sm danger" onclick={() => deleteRole(role)}>Delete</button></div>
{/each}
<label>New role name<input bind:value={newRoleName} maxlength="24" /></label>
<div class="row"><label><input type="checkbox" bind:checked={roleFlags.can_invite} /> Invite</label><label><input type="checkbox" bind:checked={roleFlags.can_kick} /> Kick</label><label><input type="checkbox" bind:checked={roleFlags.can_claim} /> Claim</label><label><input type="checkbox" bind:checked={roleFlags.can_post} /> Post</label><label><input type="checkbox" bind:checked={roleFlags.can_manage} /> Manage</label></div>
<button class="primary" onclick={createRole}>Create role</button>
</div>{/if}
{:else if activeTab === 'feed'}
<div class="feed-tab">
<!-- Post Announcement Form -->
@@ -407,6 +508,7 @@
{#if g.description}
<p class="dir-desc">{g.description}</p>
{/if}
<button class="sm primary" onclick={() => requestJoin(g)}>Request to join</button>
</div>
{/each}
</div>
@@ -436,6 +538,10 @@
<span class="field-label">Banner Image URL (Optional)</span>
<input type="url" placeholder="https://example.com/banner.png" bind:value={newGuildBanner} />
</label>
<label>
<span class="field-label">Icon Image URL (Optional)</span>
<input type="url" placeholder="https://example.com/guild-icon.png" bind:value={newGuildIcon} />
</label>
</div>
{#snippet footer()}
<button class="ghost" onclick={() => (createModalOpen = false)}>Cancel</button>