deleting of servers, invites, make a different file for dashboard, make

invites a new citizen, partly fix PSD type, switch to glass and 9x
dicebear
This commit is contained in:
Soph :3 2026-01-16 21:24:35 +02:00
parent 578edf32d4
commit f1dcd0cfc5
10 changed files with 529 additions and 300 deletions

View file

@ -0,0 +1,331 @@
<script lang="ts">
import type { OverviewServer } from '$lib';
import { enhance } from '$app/forms';
import * as Tabs from '$lib/components/ui/tabs/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { Switch } from '$lib/components/ui/switch/index.js';
import { Button } from '$lib/components/ui/button';
import Input from '$lib/components/ui/input/input.svelte';
import { toast } from 'svelte-sonner';
import { invalidateAll } from '$app/navigation';
import { fill_overview_data } from '$lib/state.svelte';
import { formatTimestamp } from '$lib/utils';
import type { PageServerData } from './$types';
let inviteCode = $state();
type FakePermission =
| 'changeChannelName'
| 'changeServerName'
| 'createInvite'
| 'createChannels'
| 'deleteChannels'
| 'deleteInvites'
| 'addRoles'
| 'deleteRoles'
| 'kickPeople'
| 'banPeople';
type FakeRole = {
id: string;
name: string;
permissions: Record<FakePermission, boolean>;
};
let newRoleName = $state('');
let fakeRoles = $state<FakeRole[]>([
{
id: 'admin',
name: 'Admin',
permissions: {
changeChannelName: true,
changeServerName: true,
createInvite: true,
createChannels: true,
deleteChannels: true,
deleteInvites: true,
addRoles: true,
deleteRoles: true,
kickPeople: true,
banPeople: true
}
},
{
id: 'moderator',
name: 'Moderator',
permissions: {
changeChannelName: true,
changeServerName: false,
createInvite: true,
createChannels: false,
deleteChannels: true,
deleteInvites: true,
addRoles: true,
deleteRoles: true,
kickPeople: true,
banPeople: false
}
},
{
id: 'member',
name: 'Member',
permissions: {
changeChannelName: false,
changeServerName: false,
createInvite: true,
createChannels: false,
deleteChannels: false,
deleteInvites: false,
addRoles: false,
deleteRoles: false,
kickPeople: false,
banPeople: false
}
}
]);
let selectedRoleId = $state<string | null>(null);
let selectedRole = $derived(fakeRoles.find((r) => r.id === selectedRoleId));
const {
server,
psd,
currentPage = $bindable()
}: {
server: OverviewServer;
psd: PageServerData;
currentPage: string;
} = $props();
</script>
<Tabs.Root value="roles" class="max-w-200 p-2">
<Tabs.List>
<Tabs.Trigger value="roles">Roles</Tabs.Trigger>
<Tabs.Trigger value="general">General</Tabs.Trigger>
<Tabs.Trigger value="dangerous">Dangerous</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="dangerous" class="space-y-6 p-4">
<div class="space-y-2">
<h2 class="text-lg font-medium text-destructive">Danger Zone</h2>
<p class="text-sm text-muted-foreground">
These actions are irreversible and will affect all members of the server.
</p>
<form
method="POST"
action="?/deleteServer"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success') {
toast.success('Server deleted successfully');
await invalidateAll();
await fill_overview_data(psd);
} else if (result.type === 'failure') {
toast.error('Failed to delete server: ' + result.data?.error);
}
};
}}
class="flex items-center gap-2 pt-2"
>
<input type="hidden" name="serverId" value={currentPage} />
<Button type="submit" variant="destructive">Delete Server</Button>
<p class="text-sm text-destructive">
This will permanently delete the server and all its data.
</p>
</form>
</div>
</Tabs.Content>
<Tabs.Content value="roles" class="space-y-6 p-4">
<div class="space-y-6">
<div class="flex items-center justify-between">
<h2 class="text-lg font-medium">Roles (fake)</h2>
<div class="flex gap-2">
<Input placeholder="New role name" class="h-8 w-40" bind:value={newRoleName} />
<Button
size="sm"
variant="outline"
onclick={() => {
if (!newRoleName) return;
const newRole: FakeRole = {
id: crypto.randomUUID(),
name: newRoleName,
permissions: {
changeChannelName: false,
changeServerName: false,
createInvite: false,
createChannels: false,
deleteChannels: false,
deleteInvites: false,
addRoles: false,
deleteRoles: false,
kickPeople: false,
banPeople: false
}
};
fakeRoles = [...fakeRoles, newRole];
selectedRoleId = newRole.id;
newRoleName = '';
}}
>
Create Role
</Button>
</div>
</div>
<div class="flex flex-wrap gap-2">
{#each fakeRoles as role (role.id)}
<Button
type="button"
variant={selectedRoleId === role.id ? 'secondary' : 'ghost'}
size="sm"
class="gap-1"
onclick={() => (selectedRoleId = role.id)}
>
{role.name}
{#if role.id !== 'member'}
<button
type="button"
class="text-destructive hover:text-destructive/80"
onclick={(e) => {
e.stopPropagation();
fakeRoles = fakeRoles.filter((r) => r.id !== role.id);
if (selectedRoleId === role.id) selectedRoleId = null;
}}
>
×
</button>
{/if}
</Button>
{/each}
</div>
{#if selectedRole}
<Card.Root>
<Card.Header>
<Card.Title>{selectedRole.name} permissions</Card.Title>
</Card.Header>
<Card.Content class="space-y-4">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
{#each Object.entries(selectedRole.permissions) as [perm, enabled] (perm)}
<div class="flex items-center justify-between">
<Label>
{#if perm === 'changeChannelName'}Change channel name{/if}
{#if perm === 'changeServerName'}Change server name{/if}
{#if perm === 'createInvite'}Create invite{/if}
{#if perm === 'createChannels'}Create channels{/if}
{#if perm === 'deleteChannels'}Delete channels{/if}
{#if perm === 'deleteInvites'}Delete invites{/if}
{#if perm === 'addRoles'}Add roles{/if}
{#if perm === 'deleteRoles'}Delete roles{/if}
{#if perm === 'kickPeople'}Kick members{/if}
{#if perm === 'banPeople'}Ban members{/if}
</Label>
<Switch
checked={enabled}
onchange={(e) => {
selectedRole.permissions[perm as FakePermission] = e.detail;
}}
/>
</div>
{/each}
</div>
<p class="text-sm text-muted-foreground">Changes are local only (no backend yet)</p>
</Card.Content>
</Card.Root>
{/if}
</div>
</Tabs.Content>
<Tabs.Content value="general" class="space-y-6 p-4">
<div class="space-y-2">
<h2 class="text-lg font-medium">Server Settings</h2>
<div class="flex items-center gap-4">
<Input
placeholder="New server name"
class="max-w-xs"
value={server?.name}
oninput={(e) => {
server.name = e.currentTarget.value;
}}
/>
<Button
variant="outline"
onclick={() => toast.info('Server name change would be saved (no backend yet)')}
>
Save Name
</Button>
</div>
<p class="text-xs text-muted-foreground">Changes are local only (no backend yet)</p>
</div>
<div class="space-y-2">
<h2 class="text-lg font-medium">Invites</h2>
<form
method="POST"
action="?/createInvite"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success') {
toast.success('Invite created successfully');
inviteCode = location.origin + '/invite/' + result.data!.code;
await invalidateAll();
await fill_overview_data(psd);
} else if (result.type === 'failure') {
toast.error('Failed to create invite: ' + result.data?.error);
}
};
}}
class="flex gap-2"
>
<input type="hidden" name="serverId" value={currentPage} />
<div class="flex flex-1 gap-2">
<Input placeholder="Create new invite" class="flex-1" readonly value={inviteCode} />
<div class="flex items-center gap-2">
<Label class="text-sm">Max uses:</Label>
<Input type="number" name="maxUses" min="0" class="w-20" placeholder="10" />
</div>
</div>
<Button type="submit">Create Invite</Button>
</form>
<div class="space-y-2 pt-4">
<h3 class="font-medium">Active Invites</h3>
<div class="space-y-2 rounded border p-3">
{#each server.invites || [] as invite (invite.code)}
<div class="flex items-center justify-between rounded p-2 hover:bg-muted/50">
<div class="flex items-center gap-2">
<div class="font-mono">{invite.code}</div>
<div class="text-sm text-muted-foreground">
{invite.uses}/{invite.maxUses || '∞'} uses • Created by {invite.createdBy
.username} on {formatTimestamp(invite.createdAt)}
</div>
</div>
<form
method="POST"
action="?/deleteInvite"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success') {
toast.success('Invite deleted successfully');
await invalidateAll();
await fill_overview_data(psd);
} else if (result.type === 'failure') {
toast.error('Failed to delete invite: ' + result.data?.error);
}
};
}}
class="m-0"
>
<input type="hidden" name="inviteCode" value={invite.code} />
<input type="hidden" name="serverId" value={currentPage} />
<Button variant="destructive" size="sm" type="submit">Delete</Button>
</form>
</div>
{/each}
</div>
</div>
</div>
</Tabs.Content>
</Tabs.Root>