feat: add error boundaries, loading skeletons, page transitions, and empty states
- ErrorBoundary: global error boundary with ShieldAI branding, retry/report - Skeleton: SkeletonText, SkeletonCard, SkeletonAvatar, SkeletonTable - PageTransition: fade-in + translate-y on route change, respects reduced motion - EmptyState: reusable component with icon, title, description, action - Button: add ariaLabel prop support - Toast: add aria-live=polite region - Dashboard: replace pulse divs with SkeletonCard fallbacks - Service pages: add skeleton layouts, empty states, mutation loading states - 404 page: polished with ShieldAI branding and home navigation - app.tsx: add ErrorBoundary, PageTransition, skip-to-content link - app.css: add page-enter animation with prefers-reduced-motion support
This commit is contained in:
@@ -1,9 +1,19 @@
|
||||
import { createSignal, createResource, For, Show } from "solid-js";
|
||||
import { createSignal, createResource, For, Show, Suspense } from "solid-js";
|
||||
import { Title } from "@solidjs/meta";
|
||||
import { Sidebar, TopBar } from "~/components/dashboard";
|
||||
import { Button, Card } from "~/components/ui";
|
||||
import { Button, Card, EmptyState, SkeletonCard, SkeletonTable } from "~/components/ui";
|
||||
import { api } from "~/lib/api";
|
||||
|
||||
function BrokerIcon() {
|
||||
return (
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M8 12h8" />
|
||||
<path d="M12 8v8" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RemoveBrokersPage() {
|
||||
const [sidebarOpen, setSidebarOpen] = createSignal(false);
|
||||
const [brokers] = createResource(
|
||||
@@ -31,70 +41,86 @@ export default function RemoveBrokersPage() {
|
||||
<Sidebar open={sidebarOpen()} onClose={() => setSidebarOpen(false)} />
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<TopBar onMenuToggle={() => setSidebarOpen(v => !v)} />
|
||||
<main class="flex-1 overflow-y-auto p-6">
|
||||
<main id="main-content" class="flex-1 overflow-y-auto p-6">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h1 class="text-2xl font-bold text-[var(--color-text-primary)] mb-6">RemoveBrokers</h1>
|
||||
|
||||
<Show when={stats()}>
|
||||
<div class="grid grid-cols-3 gap-4 mb-6">
|
||||
<Card class="p-4 text-center">
|
||||
<p class="text-2xl font-bold text-[var(--color-brand-primary)]">
|
||||
{String((stats() as Record<string, unknown>)?.totalRequests ?? 0)}
|
||||
</p>
|
||||
<p class="text-xs text-[var(--color-text-tertiary)]">Total Requests</p>
|
||||
</Card>
|
||||
<Card class="p-4 text-center">
|
||||
<p class="text-2xl font-bold text-[var(--color-success)]">
|
||||
{String((stats() as Record<string, unknown>)?.completedRequests ?? 0)}
|
||||
</p>
|
||||
<p class="text-xs text-[var(--color-text-tertiary)]">Completed</p>
|
||||
</Card>
|
||||
<Card class="p-4 text-center">
|
||||
<p class="text-2xl font-bold text-[var(--color-warning)]">
|
||||
{String((stats() as Record<string, unknown>)?.pendingRequests ?? 0)}
|
||||
</p>
|
||||
<p class="text-xs text-[var(--color-text-tertiary)]">Pending</p>
|
||||
</Card>
|
||||
</div>
|
||||
</Show>
|
||||
<Suspense fallback={<div class="grid grid-cols-3 gap-4 mb-6"><SkeletonCard class="h-24" /><SkeletonCard class="h-24" /><SkeletonCard class="h-24" /></div>}>
|
||||
<Show when={stats()}>
|
||||
<div class="grid grid-cols-3 gap-4 mb-6">
|
||||
<Card class="p-4 text-center">
|
||||
<p class="text-2xl font-bold text-[var(--color-brand-primary)]">
|
||||
{String((stats() as Record<string, unknown>)?.totalRequests ?? 0)}
|
||||
</p>
|
||||
<p class="text-xs text-[var(--color-text-tertiary)]">Total Requests</p>
|
||||
</Card>
|
||||
<Card class="p-4 text-center">
|
||||
<p class="text-2xl font-bold text-[var(--color-success)]">
|
||||
{String((stats() as Record<string, unknown>)?.completedRequests ?? 0)}
|
||||
</p>
|
||||
<p class="text-xs text-[var(--color-text-tertiary)]">Completed</p>
|
||||
</Card>
|
||||
<Card class="p-4 text-center">
|
||||
<p class="text-2xl font-bold text-[var(--color-warning)]">
|
||||
{String((stats() as Record<string, unknown>)?.pendingRequests ?? 0)}
|
||||
</p>
|
||||
<p class="text-xs text-[var(--color-text-tertiary)]">Pending</p>
|
||||
</Card>
|
||||
</div>
|
||||
</Show>
|
||||
</Suspense>
|
||||
|
||||
<Card class="mb-6">
|
||||
<div class="px-4 py-3 border-b border-[var(--color-border)]/50">
|
||||
<h2 class="text-sm font-semibold text-[var(--color-text-primary)]">Data Brokers</h2>
|
||||
</div>
|
||||
<div class="divide-y divide-[var(--color-border)]/50">
|
||||
<For each={brokers()}>
|
||||
{(broker: Record<string, unknown>) => (
|
||||
<div class="px-4 py-3 flex items-center justify-between">
|
||||
<p class="text-sm text-[var(--color-text-primary)]">{String(broker.name ?? "")}</p>
|
||||
<Button size="sm" onClick={() => createRequest(String(broker.id))}>
|
||||
Opt Out
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Suspense fallback={<div class="p-4"><SkeletonTable rows={4} columns={2} /></div>}>
|
||||
<Show when={brokers().length > 0} fallback={
|
||||
<EmptyState
|
||||
icon={<BrokerIcon />}
|
||||
title="No data brokers found"
|
||||
description="Your broker registry is empty. We'll scan for brokers automatically."
|
||||
/>
|
||||
}>
|
||||
<div class="divide-y divide-[var(--color-border)]/50">
|
||||
<For each={brokers()}>
|
||||
{(broker: Record<string, unknown>) => (
|
||||
<div class="px-4 py-3 flex items-center justify-between">
|
||||
<p class="text-sm text-[var(--color-text-primary)]">{String(broker.name ?? "")}</p>
|
||||
<Button size="sm" onClick={() => createRequest(String(broker.id))}>
|
||||
Opt Out
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Suspense>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div class="px-4 py-3 border-b border-[var(--color-border)]/50">
|
||||
<h2 class="text-sm font-semibold text-[var(--color-text-primary)]">Removal Requests</h2>
|
||||
</div>
|
||||
<div class="divide-y divide-[var(--color-border)]/50">
|
||||
<For each={removalRequests()?.items ?? []}>
|
||||
{(req: Record<string, unknown>) => (
|
||||
<div class="px-4 py-3">
|
||||
<p class="text-sm text-[var(--color-text-primary)]">{String(req.brokerName ?? "")}</p>
|
||||
<p class="text-xs text-[var(--color-text-tertiary)]">Status: {String(req.status ?? "")}</p>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
<Show when={!(removalRequests()?.items?.length)}>
|
||||
<div class="px-4 py-8 text-center text-sm text-[var(--color-text-tertiary)]">
|
||||
No removal requests yet
|
||||
<Suspense fallback={<div class="p-4"><SkeletonTable rows={3} columns={2} /></div>}>
|
||||
<Show when={(removalRequests()?.items ?? []).length > 0} fallback={
|
||||
<EmptyState
|
||||
title="No removal requests yet"
|
||||
description="Opt out of data brokers above to generate removal requests."
|
||||
/>
|
||||
}>
|
||||
<div class="divide-y divide-[var(--color-border)]/50">
|
||||
<For each={removalRequests()?.items ?? []}>
|
||||
{(req: Record<string, unknown>) => (
|
||||
<div class="px-4 py-3">
|
||||
<p class="text-sm text-[var(--color-text-primary)]">{String(req.brokerName ?? "")}</p>
|
||||
<p class="text-xs text-[var(--color-text-tertiary)]">Status: {String(req.status ?? "")}</p>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Suspense>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user