Files
publisher-dashboard/apps/publisher-dashboard/src/routes/admin/orgs/+page.svelte
RevIQ f9f26bb590 Add shadcn UI components and replace raw HTML form elements
- Install 8 shadcn components: select, skeleton, checkbox, switch, avatar,
  dropdown-menu, tooltip, textarea
- Replace raw <select> elements with Select component in members page
- Replace raw checkbox with Checkbox component in admin user details
- Add Skeleton loading states to admin pages (users list, orgs list,
  user details) for better UX

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 15:26:49 +08:00

245 lines
8.0 KiB
Svelte

<script lang="ts">
import { AlertCircle, Building, Eye, Plus, Trash2 } from "@lucide/svelte";
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
import { toast } from "svelte-sonner";
import { api } from "$lib/api/client.js";
import DashboardLayout from "$lib/components/layout/dashboard-layout.svelte";
import ConfirmDialog from "$lib/components/org/confirm-dialog.svelte";
import { Button } from "$lib/components/ui/button/index.js";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "$lib/components/ui/card/index.js";
import { Skeleton } from "$lib/components/ui/skeleton/index.js";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "$lib/components/ui/table/index.js";
import { formatDate } from "$lib/utils/format-date.js";
/**
* Admin Organizations list page
*/
const queryClient = useQueryClient();
// Fetch all orgs
const orgsQuery = createQuery(() => ({
queryKey: ["admin", "orgs"],
queryFn: () => api.admin.orgs.list(),
}));
// Confirmation dialog state
let confirmDialogOpen = $state(false);
let confirmDialogTitle = $state("");
let confirmDialogDescription = $state("");
let confirmAction = $state<() => Promise<void>>(() => Promise.resolve());
let isConfirmLoading = $state(false);
/**
* Handle delete org action
*/
function handleDelete(slug: string, displayName: string) {
confirmDialogTitle = "Delete Organization";
confirmDialogDescription = `Are you sure you want to delete "${displayName}" (${slug})? This action cannot be undone. All members, invitations, and sites will be permanently deleted.`;
confirmAction = async () => {
try {
await api.admin.orgs.delete({ slug });
toast.success("Organization deleted");
await queryClient.invalidateQueries({ queryKey: ["admin", "orgs"] });
} catch (e) {
toast.error(
e instanceof Error ? e.message : "Failed to delete organization",
);
}
};
confirmDialogOpen = true;
}
/**
* Execute confirm action
*/
async function executeConfirmAction() {
isConfirmLoading = true;
try {
await confirmAction();
confirmDialogOpen = false;
} finally {
isConfirmLoading = false;
}
}
</script>
<svelte:head>
<title>Organizations | Admin | Publisher Dashboard</title>
</svelte:head>
<DashboardLayout title="Organizations">
<div class="space-y-6">
{#if orgsQuery.isPending}
<!-- Loading skeleton -->
<div class="flex items-center justify-between">
<Skeleton class="h-7 w-40" />
<Skeleton class="h-9 w-40" />
</div>
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2 text-base">
<Building class="h-4 w-4" />
<Skeleton class="h-5 w-32" />
</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Slug</TableHead>
<TableHead>Display Name</TableHead>
<TableHead>Created At</TableHead>
<TableHead class="w-[120px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{#each Array(5) as _}
<TableRow>
<TableCell><Skeleton class="h-4 w-24" /></TableCell>
<TableCell><Skeleton class="h-4 w-32" /></TableCell>
<TableCell><Skeleton class="h-4 w-24" /></TableCell>
<TableCell>
<div class="flex items-center gap-1">
<Skeleton class="h-8 w-8" />
<Skeleton class="h-8 w-8" />
</div>
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
</CardContent>
</Card>
{:else if orgsQuery.error}
<!-- Error state -->
<div class="flex flex-col items-center justify-center py-16">
<AlertCircle class="h-8 w-8 text-destructive" />
<p class="mt-4 text-sm text-destructive">
{orgsQuery.error instanceof Error ? orgsQuery.error.message : "Failed to load organizations"}
</p>
</div>
{:else if orgsQuery.data}
<!-- Header -->
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold">
Organizations ({orgsQuery.data.length})
</h2>
<Button href="/admin/orgs/new">
<Plus class="mr-2 h-4 w-4" />
New Organization
</Button>
</div>
{#if orgsQuery.data.length === 0}
<!-- Empty state -->
<Card class="border-dashed">
<CardContent class="flex flex-col items-center justify-center py-16">
<div class="flex h-16 w-16 items-center justify-center rounded-full bg-muted">
<Building class="h-8 w-8 text-muted-foreground" />
</div>
<h3 class="mt-4 text-lg font-semibold">No organizations yet</h3>
<p class="mt-2 text-center text-sm text-muted-foreground">
Create your first organization to get started.
</p>
<Button href="/admin/orgs/new" class="mt-4">
<Plus class="mr-2 h-4 w-4" />
New Organization
</Button>
</CardContent>
</Card>
{:else}
<!-- Organizations table -->
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2 text-base">
<Building class="h-4 w-4" />
All Organizations
</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Slug</TableHead>
<TableHead>Display Name</TableHead>
<TableHead>Created At</TableHead>
<TableHead class="w-[120px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{#each orgsQuery.data as org (org.id)}
<TableRow>
<TableCell class="font-mono text-sm">{org.slug}</TableCell>
<TableCell class="font-medium">{org.displayName}</TableCell>
<TableCell class="text-muted-foreground">
{formatDate(org.createdAt)}
</TableCell>
<TableCell>
<div class="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
href="/dashboard/{org.slug}"
title="View organization"
>
<Eye class="h-4 w-4" />
<span class="sr-only">View</span>
</Button>
<Button
variant="ghost"
size="icon"
class="text-destructive hover:text-destructive"
onclick={() => handleDelete(org.slug, org.displayName)}
title="Delete organization"
>
<Trash2 class="h-4 w-4" />
<span class="sr-only">Delete</span>
</Button>
</div>
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
</CardContent>
</Card>
{/if}
<!-- Back link -->
<div class="pt-4">
<a
href="/admin"
class="text-sm text-muted-foreground hover:text-foreground"
>
&larr; Back to admin dashboard
</a>
</div>
{/if}
</div>
</DashboardLayout>
<!-- Confirmation dialog -->
<ConfirmDialog
bind:open={confirmDialogOpen}
title={confirmDialogTitle}
description={confirmDialogDescription}
variant="destructive"
confirmLabel="Delete"
loading={isConfirmLoading}
onconfirm={executeConfirmAction}
oncancel={() => confirmDialogOpen = false}
/>