318 lines
12 KiB
Svelte
318 lines
12 KiB
Svelte
<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>
|