initial
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
export function EmptyState({ title, description }: { title: string; description?: string }) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-gray-300 bg-white px-6 py-12 text-center">
|
||||
<p className="text-sm font-medium text-gray-700">{title}</p>
|
||||
{description && <p className="mt-1 text-sm text-gray-500">{description}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
/** Catches render-time errors in the tree and shows a fallback UI. */
|
||||
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
state: ErrorBoundaryState = { hasError: false };
|
||||
|
||||
static getDerivedStateFromError(): ErrorBoundaryState {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo): void {
|
||||
console.error('Unhandled error in component tree', error, info.componentStack);
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
this.props.fallback ?? (
|
||||
<div className="flex min-h-screen items-center justify-center px-4 text-center">
|
||||
<p className="text-sm text-gray-600">Something went wrong. Please reload the page.</p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { ApiError } from '@/types/api';
|
||||
|
||||
interface ErrorStateProps {
|
||||
title?: string;
|
||||
error?: ApiError | Error | null;
|
||||
}
|
||||
|
||||
export function ErrorState({ title = 'Something went wrong', error }: ErrorStateProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border border-red-200 bg-red-50 px-6 py-8 text-center">
|
||||
<p className="text-sm font-medium text-red-700">{title}</p>
|
||||
{error && <p className="mt-1 text-sm text-red-600">{error.message}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Spinner } from '@/shared/components/Spinner';
|
||||
|
||||
export function FullPageSpinner({ label = 'Loading' }: { label?: string }) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<Spinner label={label} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export function Spinner({ label = 'Loading' }: { label?: string }) {
|
||||
return (
|
||||
<span role="status" className="inline-flex items-center gap-2 text-sm text-gray-500">
|
||||
<svg className="h-4 w-4 animate-spin text-indigo-600" viewBox="0 0 24 24" fill="none">
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
|
||||
</svg>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user