Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"react-day-picker": "9.7.0",
"react-dom": "^19.1.0",
"react-dropzone": "^14.3.8",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.58.1",
"react-hotkeys-hook": "^4.6.2",
"react-i18next": "^15.5.3",
Expand Down
13 changes: 13 additions & 0 deletions client/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Button } from "@/core/components/ui/button";
import { AlertCircle, RefreshCw } from "lucide-react";

interface ComponentErrorFallbackProps {
error: Error;
resetErrorBoundary: () => void;
componentName?: string;
}

export function ComponentErrorFallback({
error,
resetErrorBoundary,
componentName = "Component"
}: ComponentErrorFallbackProps) {
return (
<div className="flex items-center justify-center p-6 border border-destructive/20 rounded-lg bg-destructive/5">
<div className="text-center max-w-sm">
<div className="flex justify-center mb-4">
<AlertCircle className="h-6 w-6 text-destructive" />
</div>

<h3 className="font-semibold text-foreground mb-2">
{componentName} Error
</h3>

<p className="text-sm text-muted-foreground mb-4">
This component failed to load. Try refreshing or contact support if the issue persists.
</p>

<Button
onClick={resetErrorBoundary}
variant="outline"
size="sm"
>
<RefreshCw className="mr-2 h-3 w-3" />
Retry
</Button>

{process.env.NODE_ENV === 'development' && (
<details className="mt-3 text-left">
<summary className="text-xs text-muted-foreground hover:text-foreground cursor-pointer">
Error details
</summary>
<pre className="mt-1 p-2 bg-muted rounded text-xs overflow-auto max-h-24">
{error.message}
</pre>
</details>
)}
</div>
</div>
);
}
48 changes: 48 additions & 0 deletions client/src/core/components/error-boundary/error-fallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Button } from "@/core/components/ui/button";
import { AlertTriangle, RefreshCw } from "lucide-react";

interface ErrorFallbackProps {
error: Error;
resetErrorBoundary: () => void;
}

export function ErrorFallback({ error, resetErrorBoundary }: ErrorFallbackProps) {
return (
<div className="flex items-center justify-center min-h-[400px] p-8">
<div className="text-center max-w-md">
<div className="flex justify-center mb-6">
<div className="p-3 bg-destructive/10 rounded-full">
<AlertTriangle className="h-8 w-8 text-destructive" />
</div>
</div>

<h2 className="text-2xl font-semibold text-foreground mb-2">
Something went wrong
</h2>

<p className="text-muted-foreground mb-6">
We encountered an unexpected error. Please try refreshing the page or contact support if the problem persists.
</p>

<div className="space-y-3">
<Button
onClick={resetErrorBoundary}
className="w-full"
>
<RefreshCw className="mr-2 h-4 w-4" />
Try again
</Button>

<details className="text-left">
<summary className="text-sm text-muted-foreground hover:text-foreground cursor-pointer">
Show error details
</summary>
<pre className="mt-2 p-3 bg-muted rounded-md text-sm overflow-auto max-h-32">
{error.message}
</pre>
</details>
</div>
</div>
</div>
);
}
24 changes: 24 additions & 0 deletions client/src/core/components/error-boundary/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
import { ErrorInfo } from 'react';
import { ErrorFallback } from './error-fallback';
import { RouteErrorFallback } from './route-error-fallback';
import { ComponentErrorFallback } from './component-error-fallback';

interface ErrorBoundaryProps {
children: React.ReactNode;
fallback?: React.ComponentType<{ error: Error; resetErrorBoundary: () => void }>;
onError?: (error: Error, info: ErrorInfo) => void;
}

export function ErrorBoundary({ children, fallback, onError }: ErrorBoundaryProps) {
return (
<ReactErrorBoundary
FallbackComponent={fallback || ErrorFallback}
onError={onError}
>
{children}
</ReactErrorBoundary>
);
}

export { ErrorFallback, RouteErrorFallback, ComponentErrorFallback };
68 changes: 68 additions & 0 deletions client/src/core/components/error-boundary/route-error-fallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Button } from "@/core/components/ui/button";
import { AlertTriangle, Home, RefreshCw } from "lucide-react";
import { useNavigate } from "@tanstack/react-router";

interface RouteErrorFallbackProps {
error: Error;
resetErrorBoundary: () => void;
}

export function RouteErrorFallback({ error, resetErrorBoundary }: RouteErrorFallbackProps) {
const navigate = useNavigate();

const handleGoHome = () => {
navigate({ to: "/dashboard/home" });
resetErrorBoundary();
};

return (
<div className="flex items-center justify-center min-h-screen p-8">
<div className="text-center max-w-lg">
<div className="flex justify-center mb-8">
<div className="p-4 bg-destructive/10 rounded-full">
<AlertTriangle className="h-12 w-12 text-destructive" />
</div>
</div>

<h1 className="text-3xl font-bold text-foreground mb-4">
Page Error
</h1>

<p className="text-lg text-muted-foreground mb-8">
This page encountered an error and couldn't be loaded. You can try refreshing the page or returning to the dashboard.
</p>

<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
onClick={resetErrorBoundary}
variant="outline"
>
<RefreshCw className="mr-2 h-4 w-4" />
Refresh Page
</Button>

<Button onClick={handleGoHome}>
<Home className="mr-2 h-4 w-4" />
Go to Dashboard
</Button>
</div>

<details className="mt-8 text-left">
<summary className="text-sm text-muted-foreground hover:text-foreground cursor-pointer">
Show technical details
</summary>
<div className="mt-3 p-4 bg-muted rounded-md text-sm">
<div className="font-mono text-destructive mb-2">
{error.name}: {error.message}
</div>
{error.stack && (
<pre className="text-xs overflow-auto max-h-48 text-muted-foreground">
{error.stack}
</pre>
)}
</div>
</details>
</div>
</div>
);
}
123 changes: 123 additions & 0 deletions client/src/core/components/loading/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Skeleton } from "@/core/components/ui/skeleton";
import { Spinner } from "@/core/components/ui/spinner";

// Page level loading component
export function PageLoader() {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<Spinner />
<p className="mt-4 text-sm text-muted-foreground">Loading...</p>
</div>
</div>
);
}

// Table loading skeleton
export function TableLoader() {
return (
<div className="space-y-3">
<div className="flex justify-between items-center">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-8 w-24" />
</div>
<div className="space-y-2">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="flex space-x-3">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-3 w-3/4" />
</div>
<Skeleton className="h-4 w-20" />
</div>
))}
</div>
</div>
);
}

// Form loading skeleton
export function FormLoader() {
return (
<div className="space-y-6">
<div className="space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-10 w-full" />
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-10 w-full" />
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-24 w-full" />
</div>
<div className="flex space-x-2">
<Skeleton className="h-10 w-20" />
<Skeleton className="h-10 w-20" />
</div>
</div>
);
}

// Card loading skeleton
export function CardLoader() {
return (
<div className="p-6 space-y-4 border rounded-lg">
<div className="space-y-2">
<Skeleton className="h-4 w-1/4" />
<Skeleton className="h-8 w-1/2" />
</div>
<Skeleton className="h-32 w-full" />
<div className="flex justify-between">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-4 w-16" />
</div>
</div>
);
}

// Dashboard grid loader
export function DashboardLoader() {
return (
<div className="space-y-8">
<div className="flex justify-between items-center">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-10 w-32" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<CardLoader key={i} />
))}
</div>
</div>
);
}

// Inline component loader
export function InlineLoader({ text = "Loading..." }: { text?: string }) {
return (
<div className="flex items-center space-x-2 p-2">
<div className="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
<span className="text-sm text-muted-foreground">{text}</span>
</div>
);
}

// Sidebar loader
export function SidebarLoader() {
return (
<div className="space-y-4 p-4">
<Skeleton className="h-8 w-32" />
<div className="space-y-2">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex items-center space-x-3">
<Skeleton className="h-4 w-4" />
<Skeleton className="h-4 flex-1" />
</div>
))}
</div>
</div>
);
}
Loading
Loading