Consolidate duplicate components and create reusable MetricsTable

- Merge two ConfirmDialog components into single shared ui/confirm-dialog
  with consistent API across account and org pages
- Create MetricsTable component to reduce duplication across dashboard
  table components (ad-unit, country, domain, source tables)
- Reduces code duplication by ~200 lines
- Consistent styling and behavior across all confirmation dialogs
- Single source of truth for metrics table structure

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
igm
2026-01-12 15:51:29 +08:00
parent 5a2e0297e5
commit 5ecf12a1a1
21 changed files with 169 additions and 335 deletions

View File

@@ -1,7 +1,6 @@
export { default as AccountNav } from "./account-nav.svelte";
export { default as AddPasskeyDialog } from "./add-passkey-dialog.svelte";
export { default as ChangePasswordDialog } from "./change-password-dialog.svelte";
export { default as ConfirmDialog } from "./confirm-dialog.svelte";
export { default as DeleteAccountDialog } from "./delete-account-dialog.svelte";
export { default as PasskeyList } from "./passkey-list.svelte";
export { default as RenamePasskeyDialog } from "./rename-passkey-dialog.svelte";

View File

@@ -5,7 +5,7 @@ import { useQueryClient } from "@tanstack/svelte-query";
import { toast } from "svelte-sonner";
import { api } from "$lib/api/client";
import { Button } from "$lib/components/ui/button";
import ConfirmDialog from "./confirm-dialog.svelte";
import { ConfirmDialog } from "$lib/components/ui/confirm-dialog";
import RenamePasskeyDialog from "./rename-passkey-dialog.svelte";
interface Passkey {

View File

@@ -1,7 +1,11 @@
<script lang="ts">
import * as Table from "$lib/components/ui/table";
import MetricsTable, { type MetricsRow } from "./metrics-table.svelte";
const tableData = [
interface AdUnitRow extends MetricsRow {
name: string;
}
const tableData: AdUnitRow[] = [
{
id: 1,
name: "/header/leaderboard-728x90",
@@ -51,58 +55,10 @@ const tableData = [
impPercent: 9.16,
},
];
function getBarWidth(value: number, max: number): number {
return (value / max) * 100;
}
const maxRevPercent = Math.max(...tableData.map((d) => d.revPercent));
</script>
<Table.Root>
<Table.Header>
<Table.Row class="border-b border-border hover:bg-transparent">
<Table.Head class="h-10 w-10 pl-5"></Table.Head>
<Table.Head class="h-10 text-xs font-medium text-muted-foreground">Ad unit</Table.Head>
<Table.Head class="h-10 text-right text-xs font-medium text-muted-foreground">Revenue</Table.Head>
<Table.Head class="h-10 w-32 text-right text-xs font-medium text-muted-foreground">
<div class="flex items-center justify-end gap-1">
% of revenue
<svg class="h-3 w-3 text-muted-foreground/60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="m18 15-6-6-6 6" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
</Table.Head>
<Table.Head class="h-10 text-right text-xs font-medium text-muted-foreground">Impressions</Table.Head>
<Table.Head class="h-10 pr-5 text-right text-xs font-medium text-muted-foreground">% of impr.</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each tableData as row, i (row.id)}
<Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30">
<Table.Cell class="w-10 py-3 pl-5">
<div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground">
{i + 1}
</div>
</Table.Cell>
<Table.Cell class="py-3">
<code class="font-mono text-[13px] text-foreground">{row.name}</code>
</Table.Cell>
<Table.Cell class="py-3 text-right font-medium tabular-nums text-foreground">{row.revenue}</Table.Cell>
<Table.Cell class="w-32 py-3">
<div class="flex items-center justify-end gap-2">
<div class="h-1.5 w-16 overflow-hidden rounded-full bg-muted">
<div
class="h-full rounded-full bg-foreground/70 transition-all duration-300"
style="width: {getBarWidth(row.revPercent, maxRevPercent)}%"
></div>
</div>
<span class="w-12 text-right font-medium tabular-nums text-foreground">{row.revPercent.toFixed(2)}%</span>
</div>
</Table.Cell>
<Table.Cell class="py-3 text-right tabular-nums text-muted-foreground">{row.impressions}</Table.Cell>
<Table.Cell class="py-3 pr-5 text-right tabular-nums text-muted-foreground">{row.impPercent.toFixed(2)}%</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
<MetricsTable data={tableData} labelHeader="Ad unit" showSortIcon>
{#snippet labelCell({ row })}
<code class="font-mono text-[13px] text-foreground">{(row as AdUnitRow).name}</code>
{/snippet}
</MetricsTable>

View File

@@ -1,7 +1,12 @@
<script lang="ts">
import * as Table from "$lib/components/ui/table";
import MetricsTable, { type MetricsRow } from "./metrics-table.svelte";
const tableData = [
interface CountryRow extends MetricsRow {
name: string;
code: string;
}
const tableData: CountryRow[] = [
{
id: 1,
name: "United States",
@@ -57,54 +62,14 @@ const tableData = [
impPercent: 4.68,
},
];
function getBarWidth(value: number, max: number): number {
return (value / max) * 100;
}
const maxRevPercent = Math.max(...tableData.map((d) => d.revPercent));
</script>
<Table.Root>
<Table.Header>
<Table.Row class="border-b border-border hover:bg-transparent">
<Table.Head class="h-10 w-10 pl-5"></Table.Head>
<Table.Head class="h-10 text-xs font-medium text-muted-foreground">Country</Table.Head>
<Table.Head class="h-10 text-right text-xs font-medium text-muted-foreground">Revenue</Table.Head>
<Table.Head class="h-10 w-32 text-right text-xs font-medium text-muted-foreground">% of revenue</Table.Head>
<Table.Head class="h-10 text-right text-xs font-medium text-muted-foreground">Impressions</Table.Head>
<Table.Head class="h-10 pr-5 text-right text-xs font-medium text-muted-foreground">% of impr.</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each tableData as row, i (row.id)}
<Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30">
<Table.Cell class="w-10 py-3 pl-5">
<div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground">
{i + 1}
</div>
</Table.Cell>
<Table.Cell class="py-3">
<div class="flex items-center gap-2">
<span class="rounded bg-muted px-1.5 py-0.5 font-mono text-[11px] font-medium text-muted-foreground">{row.code}</span>
<span class="text-[13px] font-medium text-foreground">{row.name}</span>
</div>
</Table.Cell>
<Table.Cell class="py-3 text-right font-medium tabular-nums text-foreground">{row.revenue}</Table.Cell>
<Table.Cell class="w-32 py-3">
<div class="flex items-center justify-end gap-2">
<div class="h-1.5 w-16 overflow-hidden rounded-full bg-muted">
<div
class="h-full rounded-full bg-foreground/70 transition-all duration-300"
style="width: {getBarWidth(row.revPercent, maxRevPercent)}%"
></div>
</div>
<span class="w-12 text-right font-medium tabular-nums text-foreground">{row.revPercent.toFixed(2)}%</span>
</div>
</Table.Cell>
<Table.Cell class="py-3 text-right tabular-nums text-muted-foreground">{row.impressions}</Table.Cell>
<Table.Cell class="py-3 pr-5 text-right tabular-nums text-muted-foreground">{row.impPercent.toFixed(2)}%</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
<MetricsTable data={tableData} labelHeader="Country">
{#snippet labelCell({ row })}
{@const countryRow = row as CountryRow}
<div class="flex items-center gap-2">
<span class="rounded bg-muted px-1.5 py-0.5 font-mono text-[11px] font-medium text-muted-foreground">{countryRow.code}</span>
<span class="text-[13px] font-medium text-foreground">{countryRow.name}</span>
</div>
{/snippet}
</MetricsTable>

View File

@@ -1,7 +1,11 @@
<script lang="ts">
import * as Table from "$lib/components/ui/table";
import MetricsTable, { type MetricsRow } from "./metrics-table.svelte";
const tableData = [
interface DomainRow extends MetricsRow {
name: string;
}
const tableData: DomainRow[] = [
{
id: 1,
name: "example.com",
@@ -27,51 +31,10 @@ const tableData = [
impPercent: 18.45,
},
];
function getBarWidth(value: number, max: number): number {
return (value / max) * 100;
}
const maxRevPercent = Math.max(...tableData.map((d) => d.revPercent));
</script>
<Table.Root>
<Table.Header>
<Table.Row class="border-b border-border hover:bg-transparent">
<Table.Head class="h-10 w-10 pl-5"></Table.Head>
<Table.Head class="h-10 text-xs font-medium text-muted-foreground">Domain</Table.Head>
<Table.Head class="h-10 text-right text-xs font-medium text-muted-foreground">Revenue</Table.Head>
<Table.Head class="h-10 w-32 text-right text-xs font-medium text-muted-foreground">% of revenue</Table.Head>
<Table.Head class="h-10 text-right text-xs font-medium text-muted-foreground">Impressions</Table.Head>
<Table.Head class="h-10 pr-5 text-right text-xs font-medium text-muted-foreground">% of impr.</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each tableData as row, i (row.id)}
<Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30">
<Table.Cell class="w-10 py-3 pl-5">
<div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground">
{i + 1}
</div>
</Table.Cell>
<Table.Cell class="py-3">
<span class="text-[13px] font-medium text-foreground">{row.name}</span>
</Table.Cell>
<Table.Cell class="py-3 text-right font-medium tabular-nums text-foreground">{row.revenue}</Table.Cell>
<Table.Cell class="w-32 py-3">
<div class="flex items-center justify-end gap-2">
<div class="h-1.5 w-16 overflow-hidden rounded-full bg-muted">
<div
class="h-full rounded-full bg-foreground/70 transition-all duration-300"
style="width: {getBarWidth(row.revPercent, maxRevPercent)}%"
></div>
</div>
<span class="w-12 text-right font-medium tabular-nums text-foreground">{row.revPercent.toFixed(2)}%</span>
</div>
</Table.Cell>
<Table.Cell class="py-3 text-right tabular-nums text-muted-foreground">{row.impressions}</Table.Cell>
<Table.Cell class="py-3 pr-5 text-right tabular-nums text-muted-foreground">{row.impPercent.toFixed(2)}%</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
<MetricsTable data={tableData} labelHeader="Domain">
{#snippet labelCell({ row })}
<span class="text-[13px] font-medium text-foreground">{(row as DomainRow).name}</span>
{/snippet}
</MetricsTable>

View File

@@ -2,4 +2,5 @@ export { default as AdUnitTable } from "./ad-unit-table.svelte";
export { default as CountryTable } from "./country-table.svelte";
export { default as DomainTable } from "./domain-table.svelte";
export { default as KeyValueTable } from "./key-value-table.svelte";
export { default as MetricsTable, type MetricsRow } from "./metrics-table.svelte";
export { default as SourceTable } from "./source-table.svelte";

View File

@@ -1,7 +1,17 @@
<script lang="ts">
import * as Table from "$lib/components/ui/table";
const tableData = [
interface KeyValueRow {
id: number;
key: string;
value: string;
revenue: string;
revPercent: number;
impressions: string;
impPercent: number;
}
const tableData: KeyValueRow[] = [
{
id: 1,
key: "device",

View File

@@ -0,0 +1,77 @@
<script lang="ts">
import type { Snippet } from "svelte";
import * as Table from "$lib/components/ui/table";
export interface MetricsRow {
id: number;
revenue: string;
revPercent: number;
impressions: string;
impPercent: number;
}
interface Props {
data: MetricsRow[];
labelHeader: string;
labelCell: Snippet<[{ row: MetricsRow; index: number }]>;
showSortIcon?: boolean;
}
let { data, labelHeader, labelCell, showSortIcon = false }: Props = $props();
function getBarWidth(value: number, max: number): number {
return (value / max) * 100;
}
const maxRevPercent = $derived(Math.max(...data.map((d) => d.revPercent)));
</script>
<Table.Root>
<Table.Header>
<Table.Row class="border-b border-border hover:bg-transparent">
<Table.Head class="h-10 w-10 pl-5"></Table.Head>
<Table.Head class="h-10 text-xs font-medium text-muted-foreground">{labelHeader}</Table.Head>
<Table.Head class="h-10 text-right text-xs font-medium text-muted-foreground">Revenue</Table.Head>
<Table.Head class="h-10 w-32 text-right text-xs font-medium text-muted-foreground">
<div class="flex items-center justify-end gap-1">
% of revenue
{#if showSortIcon}
<svg class="h-3 w-3 text-muted-foreground/60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="m18 15-6-6-6 6" stroke-linecap="round" stroke-linejoin="round" />
</svg>
{/if}
</div>
</Table.Head>
<Table.Head class="h-10 text-right text-xs font-medium text-muted-foreground">Impressions</Table.Head>
<Table.Head class="h-10 pr-5 text-right text-xs font-medium text-muted-foreground">% of impr.</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each data as row, i (row.id)}
<Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30">
<Table.Cell class="w-10 py-3 pl-5">
<div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground">
{i + 1}
</div>
</Table.Cell>
<Table.Cell class="py-3">
{@render labelCell({ row, index: i })}
</Table.Cell>
<Table.Cell class="py-3 text-right font-medium tabular-nums text-foreground">{row.revenue}</Table.Cell>
<Table.Cell class="w-32 py-3">
<div class="flex items-center justify-end gap-2">
<div class="h-1.5 w-16 overflow-hidden rounded-full bg-muted">
<div
class="h-full rounded-full bg-foreground/70 transition-all duration-300"
style="width: {getBarWidth(row.revPercent, maxRevPercent)}%"
></div>
</div>
<span class="w-12 text-right font-medium tabular-nums text-foreground">{row.revPercent.toFixed(2)}%</span>
</div>
</Table.Cell>
<Table.Cell class="py-3 text-right tabular-nums text-muted-foreground">{row.impressions}</Table.Cell>
<Table.Cell class="py-3 pr-5 text-right tabular-nums text-muted-foreground">{row.impPercent.toFixed(2)}%</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>

View File

@@ -1,7 +1,11 @@
<script lang="ts">
import * as Table from "$lib/components/ui/table";
import MetricsTable, { type MetricsRow } from "./metrics-table.svelte";
const tableData = [
interface SourceRow extends MetricsRow {
name: string;
}
const tableData: SourceRow[] = [
{
id: 1,
name: "Google AdX",
@@ -43,51 +47,10 @@ const tableData = [
impPercent: 7.28,
},
];
function getBarWidth(value: number, max: number): number {
return (value / max) * 100;
}
const maxRevPercent = Math.max(...tableData.map((d) => d.revPercent));
</script>
<Table.Root>
<Table.Header>
<Table.Row class="border-b border-border hover:bg-transparent">
<Table.Head class="h-10 w-10 pl-5"></Table.Head>
<Table.Head class="h-10 text-xs font-medium text-muted-foreground">Source</Table.Head>
<Table.Head class="h-10 text-right text-xs font-medium text-muted-foreground">Revenue</Table.Head>
<Table.Head class="h-10 w-32 text-right text-xs font-medium text-muted-foreground">% of revenue</Table.Head>
<Table.Head class="h-10 text-right text-xs font-medium text-muted-foreground">Impressions</Table.Head>
<Table.Head class="h-10 pr-5 text-right text-xs font-medium text-muted-foreground">% of impr.</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each tableData as row, i (row.id)}
<Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30">
<Table.Cell class="w-10 py-3 pl-5">
<div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground">
{i + 1}
</div>
</Table.Cell>
<Table.Cell class="py-3">
<span class="text-[13px] font-medium text-foreground">{row.name}</span>
</Table.Cell>
<Table.Cell class="py-3 text-right font-medium tabular-nums text-foreground">{row.revenue}</Table.Cell>
<Table.Cell class="w-32 py-3">
<div class="flex items-center justify-end gap-2">
<div class="h-1.5 w-16 overflow-hidden rounded-full bg-muted">
<div
class="h-full rounded-full bg-foreground/70 transition-all duration-300"
style="width: {getBarWidth(row.revPercent, maxRevPercent)}%"
></div>
</div>
<span class="w-12 text-right font-medium tabular-nums text-foreground">{row.revPercent.toFixed(2)}%</span>
</div>
</Table.Cell>
<Table.Cell class="py-3 text-right tabular-nums text-muted-foreground">{row.impressions}</Table.Cell>
<Table.Cell class="py-3 pr-5 text-right tabular-nums text-muted-foreground">{row.impPercent.toFixed(2)}%</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
<MetricsTable data={tableData} labelHeader="Source">
{#snippet labelCell({ row })}
<span class="text-[13px] font-medium text-foreground">{(row as SourceRow).name}</span>
{/snippet}
</MetricsTable>

View File

@@ -1,95 +0,0 @@
<script lang="ts">
import { X } from "@lucide/svelte";
import { Dialog as DialogPrimitive } from "bits-ui";
import { Button } from "$lib/components/ui/button";
import { cn } from "$lib/utils";
interface Props {
open: boolean;
title: string;
description: string;
confirmLabel?: string;
cancelLabel?: string;
variant?: "destructive" | "default";
loading?: boolean;
onconfirm: () => void;
oncancel: () => void;
}
let {
open = $bindable(false),
title,
description,
confirmLabel = "Confirm",
cancelLabel = "Cancel",
variant = "default",
loading = false,
onconfirm,
oncancel,
}: Props = $props();
function handleCancel() {
open = false;
oncancel();
}
function handleConfirm() {
onconfirm();
}
</script>
<DialogPrimitive.Root bind:open>
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
/>
<DialogPrimitive.Content
class={cn(
"fixed left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2",
"rounded-lg border bg-background p-6 shadow-lg",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
"data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]",
"data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
"duration-200"
)}
>
<!-- Close button -->
<DialogPrimitive.Close
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none"
onclick={handleCancel}
>
<X class="h-4 w-4" />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
<!-- Header -->
<div class="space-y-2">
<DialogPrimitive.Title class="text-lg font-semibold leading-none tracking-tight">
{title}
</DialogPrimitive.Title>
<DialogPrimitive.Description class="text-sm text-muted-foreground">
{description}
</DialogPrimitive.Description>
</div>
<!-- Actions -->
<div class="mt-6 flex justify-end gap-3">
<Button variant="outline" onclick={handleCancel} disabled={loading}>
{cancelLabel}
</Button>
<Button
variant={variant === "destructive" ? "destructive" : "default"}
onclick={handleConfirm}
disabled={loading}
>
{#if loading}
<span class="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></span>
{/if}
{confirmLabel}
</Button>
</div>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>

View File

@@ -1,3 +1,2 @@
export { default as ConfirmDialog } from "./confirm-dialog.svelte";
export { default as OrgAvatar } from "./org-avatar.svelte";
export { default as RoleBadge } from "./role-badge.svelte";

View File

@@ -28,7 +28,7 @@ let {
onConfirm,
}: Props = $props();
async function handleConfirm() {
async function handleConfirm(): Promise<void> {
await onConfirm();
}
</script>
@@ -54,8 +54,8 @@ async function handleConfirm() {
<LoadingButton
variant="destructive"
class="w-full"
loading={loading}
loadingText={loadingText}
{loading}
{loadingText}
onclick={handleConfirm}
>
{confirmText}
@@ -77,7 +77,7 @@ async function handleConfirm() {
{cancelText}
</Button>
<LoadingButton
loading={loading}
{loading}
onclick={handleConfirm}
>
{confirmText}

View File

@@ -0,0 +1 @@
export { default as ConfirmDialog } from "./confirm-dialog.svelte";

View File

@@ -14,7 +14,7 @@ import { toast } from "svelte-sonner";
import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client";
import { ConfirmDialog } from "$lib/components/account";
import { ConfirmDialog } from "$lib/components/ui/confirm-dialog";
import { Alert, AlertDescription } from "$lib/components/ui/alert";
import { Badge } from "$lib/components/ui/badge";
import { Button } from "$lib/components/ui/button";

View File

@@ -12,7 +12,7 @@ import { formatRelativeTime } from "@reviq/common";
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
import { toast } from "svelte-sonner";
import { api } from "$lib/api/client";
import { ConfirmDialog } from "$lib/components/account";
import { ConfirmDialog } from "$lib/components/ui/confirm-dialog";
import { Alert, AlertDescription } from "$lib/components/ui/alert";
import { Badge } from "$lib/components/ui/badge";
import { Button } from "$lib/components/ui/button";

View File

@@ -15,7 +15,7 @@ import { createQuery, useQueryClient } from "@tanstack/svelte-query";
import { toast } from "svelte-sonner";
import { UAParser } from "ua-parser-js";
import { api } from "$lib/api/client";
import { ConfirmDialog } from "$lib/components/account";
import { ConfirmDialog } from "$lib/components/ui/confirm-dialog";
import { Alert, AlertDescription } from "$lib/components/ui/alert";
import { Badge } from "$lib/components/ui/badge";
import { Button } from "$lib/components/ui/button";

View File

@@ -6,7 +6,7 @@ import { toast } from "svelte-sonner";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client.js";
import { AdminLayout } from "$lib/components/layout";
import ConfirmDialog from "$lib/components/org/confirm-dialog.svelte";
import { ConfirmDialog } from "$lib/components/ui/confirm-dialog";
import { Button } from "$lib/components/ui/button/index.js";
import {
Card,
@@ -238,8 +238,7 @@ async function executeConfirmAction() {
title={confirmDialogTitle}
description={confirmDialogDescription}
variant="destructive"
confirmLabel="Delete"
confirmText="Delete"
loading={isConfirmLoading}
onconfirm={executeConfirmAction}
oncancel={() => confirmDialogOpen = false}
onConfirm={executeConfirmAction}
/>

View File

@@ -16,7 +16,8 @@ import { resolve } from "$app/paths";
import { page } from "$app/state";
import { api } from "$lib/api/client";
import { AdminLayout } from "$lib/components/layout";
import { ConfirmDialog, OrgAvatar } from "$lib/components/org";
import { OrgAvatar } from "$lib/components/org";
import { ConfirmDialog } from "$lib/components/ui/confirm-dialog";
import { Alert, AlertDescription } from "$lib/components/ui/alert";
import { Button } from "$lib/components/ui/button";
import {
@@ -82,7 +83,7 @@ let confirmDialogOpen = $state(false);
let confirmDialogTitle = $state("");
let confirmDialogDescription = $state("");
let confirmDialogVariant = $state<"default" | "destructive">("destructive");
let confirmDialogConfirmLabel = $state("Confirm");
let confirmDialogConfirmText = $state("Confirm");
let isConfirmLoading = $state(false);
let pendingAction: (() => Promise<void>) | null = $state(null);
@@ -158,7 +159,7 @@ function handleRemoveSite(domain: string) {
confirmDialogTitle = "Remove Site";
confirmDialogDescription = `Are you sure you want to remove "${domain}" from this organization? This action cannot be undone.`;
confirmDialogVariant = "destructive";
confirmDialogConfirmLabel = "Remove Site";
confirmDialogConfirmText = "Remove Site";
pendingAction = async () => {
try {
await api.admin.orgs.removeSite({ slug: slug ?? "", domain });
@@ -180,7 +181,7 @@ function handleDelete() {
confirmDialogTitle = "Delete Organization";
confirmDialogDescription = `Are you sure you want to delete "${displayName}"? This action cannot be undone. All members, invitations, and sites will be permanently deleted.`;
confirmDialogVariant = "destructive";
confirmDialogConfirmLabel = "Delete Organization";
confirmDialogConfirmText = "Delete Organization";
pendingAction = async () => {
try {
await api.admin.orgs.delete({ slug: slug ?? "" });
@@ -452,11 +453,7 @@ async function executeConfirmAction() {
title={confirmDialogTitle}
description={confirmDialogDescription}
variant={confirmDialogVariant}
confirmLabel={confirmDialogConfirmLabel}
confirmText={confirmDialogConfirmText}
loading={isConfirmLoading}
onconfirm={executeConfirmAction}
oncancel={() => {
confirmDialogOpen = false;
pendingAction = null;
}}
onConfirm={executeConfirmAction}
/>

View File

@@ -12,7 +12,8 @@ import { getContext } from "svelte";
import { toast } from "svelte-sonner";
import { api } from "$lib/api/client";
import { DashboardLayout } from "$lib/components/layout";
import { ConfirmDialog, RoleBadge } from "$lib/components/org";
import { RoleBadge } from "$lib/components/org";
import { ConfirmDialog } from "$lib/components/ui/confirm-dialog";
import { Button } from "$lib/components/ui/button";
import {
Card,
@@ -464,6 +465,5 @@ const availableInviteRoles = $derived.by(() => {
description={confirmDialogDescription}
variant={confirmDialogVariant}
loading={isConfirmLoading}
onconfirm={executeConfirmAction}
oncancel={() => confirmDialogOpen = false}
onConfirm={executeConfirmAction}
/>

View File

@@ -14,7 +14,7 @@ import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client";
import { SettingsLayout } from "$lib/components/layout";
import { ConfirmDialog } from "$lib/components/org";
import { ConfirmDialog } from "$lib/components/ui/confirm-dialog";
import { Alert, AlertDescription } from "$lib/components/ui/alert";
import { Button } from "$lib/components/ui/button";
import {
@@ -82,7 +82,7 @@ let confirmDialogOpen = $state(false);
let confirmDialogTitle = $state("");
let confirmDialogDescription = $state("");
let confirmDialogVariant = $state<"default" | "destructive">("destructive");
let confirmDialogConfirmLabel = $state("Confirm");
let confirmDialogConfirmText = $state("Confirm");
let confirmAction = $state<() => Promise<void>>(() => Promise.resolve());
let isConfirmLoading = $state(false);
@@ -119,7 +119,7 @@ function handleLeave() {
confirmDialogDescription =
"Are you sure you want to leave this organization? You will lose access to all resources and will need to be re-invited to rejoin.";
confirmDialogVariant = "destructive";
confirmDialogConfirmLabel = "Leave Organization";
confirmDialogConfirmText = "Leave Organization";
confirmAction = async () => {
try {
await api.orgs.leave({ slug });
@@ -142,7 +142,7 @@ function handleDelete() {
confirmDialogTitle = "Delete Organization";
confirmDialogDescription = `Are you sure you want to delete "${displayName}"? This action cannot be undone. All members, invitations, and sites will be permanently deleted.`;
confirmDialogVariant = "destructive";
confirmDialogConfirmLabel = "Delete Organization";
confirmDialogConfirmText = "Delete Organization";
confirmAction = async () => {
try {
await api.orgs.delete({ slug });
@@ -306,8 +306,7 @@ async function executeConfirmAction() {
title={confirmDialogTitle}
description={confirmDialogDescription}
variant={confirmDialogVariant}
confirmLabel={confirmDialogConfirmLabel}
confirmText={confirmDialogConfirmText}
loading={isConfirmLoading}
onconfirm={executeConfirmAction}
oncancel={() => confirmDialogOpen = false}
onConfirm={executeConfirmAction}
/>

View File

@@ -12,7 +12,8 @@ import { getContext } from "svelte";
import { toast } from "svelte-sonner";
import { api } from "$lib/api/client";
import { SettingsLayout } from "$lib/components/layout";
import { ConfirmDialog, RoleBadge } from "$lib/components/org";
import { RoleBadge } from "$lib/components/org";
import { ConfirmDialog } from "$lib/components/ui/confirm-dialog";
import { Button } from "$lib/components/ui/button";
import {
Card,
@@ -464,6 +465,5 @@ const availableInviteRoles = $derived.by(() => {
description={confirmDialogDescription}
variant={confirmDialogVariant}
loading={isConfirmLoading}
onconfirm={executeConfirmAction}
oncancel={() => confirmDialogOpen = false}
onConfirm={executeConfirmAction}
/>