- 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
98 lines
2.7 KiB
TypeScript
98 lines
2.7 KiB
TypeScript
import { MetaProvider, Title } from "@solidjs/meta";
|
|
import { Router, useLocation, Navigate } from "@solidjs/router";
|
|
import { FileRoutes } from "@solidjs/start/router";
|
|
import { Show, Suspense } from "solid-js";
|
|
import { ThemeProvider } from "./lib/theme";
|
|
import { ClerkProvider } from "clerk-solidjs/start";
|
|
import { ClerkLoaded, ClerkLoading, useAuth } from "clerk-solidjs";
|
|
import { AppShell } from "./components/layout";
|
|
import { ToastProvider, ErrorBoundary, PageTransition } from "./components/ui";
|
|
|
|
import "./app.css";
|
|
|
|
const PROTECTED_PATHS = ["/dashboard", "/onboarding"];
|
|
|
|
function pathMatches(pathname: string, prefixes: string[]): boolean {
|
|
return prefixes.some(
|
|
(p) => pathname === p || pathname.startsWith(p + "/"),
|
|
);
|
|
}
|
|
|
|
function RouteGuard() {
|
|
const location = useLocation();
|
|
const auth = useAuth();
|
|
|
|
const redirect = () => {
|
|
const pathname = location.pathname;
|
|
|
|
if (
|
|
auth.isLoaded() &&
|
|
!auth.isSignedIn() &&
|
|
pathMatches(pathname, PROTECTED_PATHS)
|
|
) {
|
|
return "/login";
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
return (
|
|
<Show when={redirect()} keyed>
|
|
{(to) => <Navigate href={to} />}
|
|
</Show>
|
|
);
|
|
}
|
|
|
|
function ClerkApp(props: { children: any }) {
|
|
return (
|
|
<ClerkProvider
|
|
publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}
|
|
>
|
|
<Suspense>
|
|
<ClerkLoaded>
|
|
<RouteGuard />
|
|
</ClerkLoaded>
|
|
<ClerkLoading>
|
|
<div class="h-16 animate-pulse bg-[var(--color-bg-secondary)]" />
|
|
</ClerkLoading>
|
|
</Suspense>
|
|
<Suspense>{props.children}</Suspense>
|
|
</ClerkProvider>
|
|
);
|
|
}
|
|
|
|
export default function App() {
|
|
return (
|
|
<MetaProvider>
|
|
<Title>ShieldAI</Title>
|
|
<ThemeProvider>
|
|
<ToastProvider>
|
|
<Router
|
|
root={(props) => (
|
|
<ClerkApp>
|
|
<AppShell>
|
|
<a
|
|
href="#main-content"
|
|
class="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-[100] focus:px-4 focus:py-2 focus:bg-[var(--color-bg)] focus:text-[var(--color-text-primary)] focus:rounded-lg focus:shadow-lg focus:outline-2 focus:outline-[var(--color-focus-ring)]"
|
|
>
|
|
Skip to main content
|
|
</a>
|
|
<ErrorBoundary>
|
|
<Suspense>
|
|
<PageTransition>
|
|
{props.children}
|
|
</PageTransition>
|
|
</Suspense>
|
|
</ErrorBoundary>
|
|
</AppShell>
|
|
</ClerkApp>
|
|
)}
|
|
>
|
|
<FileRoutes />
|
|
</Router>
|
|
</ToastProvider>
|
|
</ThemeProvider>
|
|
</MetaProvider>
|
|
);
|
|
}
|