Files
pocket-id/frontend/src/lib/components/signup/signup-token-list-modal.svelte

186 lines
5.5 KiB
Svelte

<script lang="ts">
import { page } from '$app/state';
import AdvancedTable from '$lib/components/advanced-table.svelte';
import { openConfirmDialog } from '$lib/components/confirm-dialog/';
import { Badge, type BadgeVariant } from '$lib/components/ui/badge';
import { Button, buttonVariants } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Table from '$lib/components/ui/table';
import { m } from '$lib/paraglide/messages';
import UserService from '$lib/services/user-service';
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
import type { SignupTokenDto } from '$lib/types/signup-token.type';
import { axiosErrorToast } from '$lib/utils/error-util';
import { Copy, Ellipsis, Trash2 } from '@lucide/svelte';
import { toast } from 'svelte-sonner';
let {
open = $bindable(),
signupTokens = $bindable(),
signupTokensRequestOptions,
onTokenDeleted
}: {
open: boolean;
signupTokens: Paginated<SignupTokenDto>;
signupTokensRequestOptions: SearchPaginationSortRequest;
onTokenDeleted?: () => Promise<void>;
} = $props();
const userService = new UserService();
function formatDate(dateStr: string | undefined) {
if (!dateStr) return m.never();
return new Date(dateStr).toLocaleString();
}
async function deleteToken(token: SignupTokenDto) {
openConfirmDialog({
title: m.delete_signup_token(),
message: m.are_you_sure_you_want_to_delete_this_signup_token(),
confirm: {
label: m.delete(),
destructive: true,
action: async () => {
try {
await userService.deleteSignupToken(token.id);
toast.success(m.signup_token_deleted_successfully());
// Refresh the tokens
if (onTokenDeleted) {
await onTokenDeleted();
}
} catch (e) {
axiosErrorToast(e);
}
}
}
});
}
function onOpenChange(isOpen: boolean) {
open = isOpen;
}
function isTokenExpired(expiresAt: string) {
return new Date(expiresAt) < new Date();
}
function isTokenUsedUp(token: SignupTokenDto) {
return token.usageCount >= token.usageLimit;
}
function getTokenStatus(token: SignupTokenDto) {
if (isTokenExpired(token.expiresAt)) return 'expired';
if (isTokenUsedUp(token)) return 'used-up';
return 'active';
}
function getStatusBadge(status: string): { variant: BadgeVariant; text: string } {
switch (status) {
case 'expired':
return { variant: 'destructive', text: m.expired() };
case 'used-up':
return { variant: 'secondary', text: m.used_up() };
default:
return { variant: 'default', text: m.active() };
}
}
function copySignupLink(token: SignupTokenDto) {
const signupLink = `${page.url.origin}/st/${token.token}`;
navigator.clipboard
.writeText(signupLink)
.then(() => {
toast.success(m.copied());
})
.catch((err) => {
axiosErrorToast(err);
});
}
</script>
<Dialog.Root {open} {onOpenChange}>
<Dialog.Content class="sm-min-w[500px] max-h-[90vh] min-w-[90vw] overflow-auto lg:min-w-[1000px]">
<Dialog.Header>
<Dialog.Title>{m.manage_signup_tokens()}</Dialog.Title>
<Dialog.Description>
{m.view_and_manage_active_signup_tokens()}
</Dialog.Description>
</Dialog.Header>
<div class="flex-1 overflow-hidden">
<AdvancedTable
items={signupTokens}
requestOptions={signupTokensRequestOptions}
withoutSearch={true}
onRefresh={async (options) => {
const result = await userService.listSignupTokens(options);
signupTokens = result;
return result;
}}
columns={[
{ label: m.token() },
{ label: m.status() },
{ label: m.usage(), sortColumn: 'usageCount' },
{ label: m.expires(), sortColumn: 'expiresAt' },
{ label: m.created(), sortColumn: 'createdAt' },
{ label: m.actions(), hidden: true }
]}
>
{#snippet rows({ item })}
<Table.Cell class="font-mono text-xs">
{item.token.substring(0, 2)}...{item.token.substring(item.token.length - 4)}
</Table.Cell>
<Table.Cell>
{@const status = getTokenStatus(item)}
{@const statusBadge = getStatusBadge(status)}
<Badge class="rounded-full" variant={statusBadge.variant}>
{statusBadge.text}
</Badge>
</Table.Cell>
<Table.Cell>
<div class="flex items-center gap-1">
{`${item.usageCount} ${m.of()} ${item.usageLimit}`}
</div>
</Table.Cell>
<Table.Cell class="text-sm">
<div class="flex items-center gap-1">
{formatDate(item.expiresAt)}
</div>
</Table.Cell>
<Table.Cell class="text-sm">
{formatDate(item.createdAt)}
</Table.Cell>
<Table.Cell>
<DropdownMenu.Root>
<DropdownMenu.Trigger class={buttonVariants({ variant: 'ghost', size: 'icon' })}>
<Ellipsis class="size-4" />
<span class="sr-only">{m.toggle_menu()}</span>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Item onclick={() => copySignupLink(item)}>
<Copy class="mr-2 size-4" />
{m.copy()}
</DropdownMenu.Item>
<DropdownMenu.Item
class="text-red-500 focus:!text-red-700"
onclick={() => deleteToken(item)}
>
<Trash2 class="mr-2 size-4" />
{m.delete()}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Table.Cell>
{/snippet}
</AdvancedTable>
</div>
<Dialog.Footer class="mt-3">
<Button onclick={() => (open = false)}>
{m.close()}
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>