chat.sad.ovh/src/routes/app/ServerDashboard.svelte
2026-01-17 20:58:32 +02:00

318 lines
12 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import { PERMISSION_LABELS, type OverviewServer, type Permission } 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();
let newRoleName = $state('');
let selectedRoleId = $state<string | null>(null);
const {
server,
psd,
currentPage = $bindable()
}: {
server: OverviewServer;
psd: PageServerData;
currentPage: string;
} = $props();
let selectedRole = $derived(server.roles.find((r) => r.id === selectedRoleId));
</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="space-y-4">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-lg font-medium">Roles</h2>
<form
method="POST"
action="?/createRole"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success') {
toast.success('Role created successfully');
newRoleName = '';
await invalidateAll();
await fill_overview_data(psd);
} else if (result.type == 'error' || result.type == 'failure') {
toast.error(
'Failed to create role: ' +
(result.type === 'error' ? result.error.message : result.data?.error)
);
}
};
}}
class="flex gap-2"
>
<input type="hidden" name="serverId" value={currentPage} />
<Input
placeholder="New role name"
name="name"
class="h-8 w-40"
bind:value={newRoleName}
/>
<Button size="sm" variant="outline" type="submit" disabled={!newRoleName}>
Create Role
</Button>
</form>
</div>
<div class="flex flex-wrap gap-2">
{#each server.roles 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.canBeDeleted}
<form
method="POST"
action="?/deleteRole"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success') {
toast.success('Role deleted successfully');
if (selectedRoleId === role.id) selectedRoleId = null;
await invalidateAll();
await fill_overview_data(psd);
} else if (result.type == 'error' || result.type == 'failure') {
toast.error(
'Failed to delete role: ' +
(result.type === 'error' ? result.error.message : result.data?.error)
);
}
};
}}
class="m-0"
>
<input type="hidden" name="serverId" value={currentPage} />
<input type="hidden" name="roleId" value={role.id} />
<button type="submit" class="text-destructive hover:text-destructive/80">
×
</button>
</form>
{/if}
</Button>
{/each}
</div>
{#if selectedRoleId}
<Card.Root>
<Card.Header>
<Card.Title>{selectedRole?.name} permissions</Card.Title>
</Card.Header>
<Card.Content class="space-y-4">
<form
method="POST"
action="?/changeRole"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success') {
toast.success('Role updated successfully');
await invalidateAll();
await fill_overview_data(psd);
} else if (result.type == 'error' || result.type == 'failure') {
toast.error(
'Failed to change role: ' +
(result.type === 'error' ? result.error.message : result.data?.error)
);
}
};
}}
class="space-y-4"
>
<input type="hidden" name="serverId" value={currentPage} />
<input type="hidden" name="roleId" value={selectedRoleId} />
<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>
{PERMISSION_LABELS[perm as Permission]}
</Label>
<Switch name={perm} checked={enabled} />
</div>
{/each}
</div>
<div class="flex items-center justify-between">
<p class="text-sm text-muted-foreground">Changes will be saved to the server</p>
<Button type="submit" variant="outline">Save Changes</Button>
</div>
</form>
</Card.Content>
<Card.Content class="space-y-4 pt-0">
<div>
<h3 class="mb-2 font-medium">Role Information</h3>
<div class="space-y-1 text-sm text-muted-foreground">
<p>
Created by: {selectedRole?.createdBy.username}
</p>
<p>
Created at: {formatTimestamp(selectedRole?.createdAt!)}
</p>
<p>
Members: {selectedRole?.users.length || 0}
</p>
</div>
</div>
</Card.Content>
</Card.Root>
{/if}
</div>
</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>
<form
method="POST"
action="?/changeServerName"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success') {
toast.success('Server name updated successfully');
await invalidateAll();
await fill_overview_data(psd);
} else if (result.type == 'error' || result.type == 'failure') {
toast.error(
'Failed to update server name: ' +
(result.type === 'error' ? result.error.message : result.data?.error)
);
}
};
}}
class="flex items-center gap-4"
>
<input type="hidden" name="serverId" value={currentPage} />
<Input name="name" placeholder="New server name" class="max-w-xs" value={server?.name} />
<Button type="submit" variant="outline">Save Name</Button>
</form>
</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 == 'error' || result.type == 'failure') {
toast.error(
'Failed to create invite: ' +
(result.type === 'error' ? result.error.message : 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 == 'error' || result.type == 'failure') {
toast.error(
'Failed to delete invite: ' +
(result.type === 'error' ? result.error.message : 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>