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}
;
}