From 1c6f98117aa3643a567e8f3bc7b86dcff14fe6dd Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 28 Jul 2026 11:41:09 +0700 Subject: [PATCH] feat: add error boundary, glass shimmer skeleton, empty state - Create ErrorBoundary class component with GlassCard fallback UI - Update LoadingSkeleton with glass shimmer animation overlay - Update EmptyState with GlassPanel and default Inbox icon - Add ErrorBoundary to shared components index export Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/components/shared/empty-state.tsx | 29 +++++++------ .../src/components/shared/error-boundary.tsx | 35 ++++++++++++++++ .../frontend/src/components/shared/index.ts | 1 + .../components/shared/loading-skeleton.tsx | 41 +++++++++++-------- 4 files changed, 73 insertions(+), 33 deletions(-) create mode 100644 services/frontend/src/components/shared/error-boundary.tsx diff --git a/services/frontend/src/components/shared/empty-state.tsx b/services/frontend/src/components/shared/empty-state.tsx index 3f97ffa..2bb2d68 100644 --- a/services/frontend/src/components/shared/empty-state.tsx +++ b/services/frontend/src/components/shared/empty-state.tsx @@ -1,26 +1,25 @@ +"use client"; + import type { LucideIcon } from "lucide-react"; +import { Inbox } from "lucide-react"; +import { GlassPanel } from "@/components/glass/panel"; interface EmptyStateProps { - icon: LucideIcon; - title: string; + icon?: LucideIcon; + title?: string; description?: string; } -/** - * Consistent empty state for data-fetching pages. - */ export function EmptyState({ - icon: Icon, - title, - description, + icon: Icon = Inbox, + title = "No data yet", + description = "Nothing to display here yet.", }: EmptyStateProps) { return ( -
- -

{title}

- {description && ( -

{description}

- )} -
+ + +

{title}

+

{description}

+
); } diff --git a/services/frontend/src/components/shared/error-boundary.tsx b/services/frontend/src/components/shared/error-boundary.tsx new file mode 100644 index 0000000..3720914 --- /dev/null +++ b/services/frontend/src/components/shared/error-boundary.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { Component, type ReactNode } from "react"; +import { GlassCard } from "@/components/glass/card"; +import { AlertCircle, RefreshCw } from "lucide-react"; + +interface Props { children: ReactNode; fallback?: ReactNode; } +interface State { hasError: boolean; error?: Error; } + +export class ErrorBoundary extends Component { + state: State = { hasError: false }; + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + render() { + if (this.state.hasError) { + return this.props.fallback || ( + + +

{this.state.error?.message || "Something went wrong"}

+ +
+ ); + } + return this.props.children; + } +} diff --git a/services/frontend/src/components/shared/index.ts b/services/frontend/src/components/shared/index.ts index 58b3c6c..fd1a6c0 100644 --- a/services/frontend/src/components/shared/index.ts +++ b/services/frontend/src/components/shared/index.ts @@ -1,5 +1,6 @@ export { DetailStat } from "./detail-stat"; export { EmptyState } from "./empty-state"; +export { ErrorBoundary } from "./error-boundary"; export { ErrorState } from "./error-state"; export { LoadingSkeleton } from "./loading-skeleton"; export { StatCard } from "./stat-card"; diff --git a/services/frontend/src/components/shared/loading-skeleton.tsx b/services/frontend/src/components/shared/loading-skeleton.tsx index 051dab9..81019ec 100644 --- a/services/frontend/src/components/shared/loading-skeleton.tsx +++ b/services/frontend/src/components/shared/loading-skeleton.tsx @@ -1,38 +1,43 @@ -import { Skeleton } from "@/components/ui/skeleton"; +"use client"; + import { cn } from "@/lib/utils"; interface LoadingSkeletonProps { - /** Number of skeleton rows */ count?: number; - /** Height per skeleton row */ height?: string; - /** Grid layout: columns */ + width?: string; columns?: number; - /** Additional classes */ className?: string; } -/** - * Consistent loading skeleton for data-fetching pages. - * Renders a grid of skeleton placeholders. - */ export function LoadingSkeleton({ count = 4, - height = "h-28", - columns = 1, + height = "h-24", + width, + columns, className, }: LoadingSkeletonProps) { - return ( + const items = Array.from({ length: count }, (_, i) => (
1 ? "grid-cols-1 md:grid-cols-2" : "grid-cols-1", + "glass rounded-[var(--radius-card)] overflow-hidden", + height, + width, className, )} > - {Array.from({ length: count }, (_, i) => ( - - ))} +
- ); + )); + + if (columns) { + return ( +
+ {items} +
+ ); + } + + return
{items}
; }