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:
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
<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">{row.code}</span>
|
||||
<span class="text-[13px] font-medium text-foreground">{row.name}</span>
|
||||
<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>
|
||||
</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>
|
||||
{/snippet}
|
||||
</MetricsTable>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
|
||||
@@ -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}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ConfirmDialog } from "./confirm-dialog.svelte";
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user