diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 20de5a4..64d2df1 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -56,9 +56,9 @@ function LoginContent() { )} {error && ( -
+
{(error as { response?: { data?: { message?: string } } }) - ?.response?.data?.message || 'Login failed. Please try again.'} + ?.response?.data?.message || 'Login failed. Please check your credentials and try again.'}
)} @@ -72,7 +72,7 @@ function LoginContent() { placeholder="your-company" /> {errors.tenantSlug && ( -

{errors.tenantSlug.message}

+

{errors.tenantSlug.message}

)}
@@ -86,7 +86,7 @@ function LoginContent() { placeholder="you@example.com" /> {errors.email && ( -

{errors.email.message}

+

{errors.email.message}

)} @@ -100,7 +100,7 @@ function LoginContent() { placeholder="••••••••" /> {errors.password && ( -

+

{errors.password.message}

)} diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx index 1060557..58cf867 100644 --- a/app/(auth)/register/page.tsx +++ b/app/(auth)/register/page.tsx @@ -54,10 +54,10 @@ export default function RegisterPage() { className="mt-8 space-y-6 rounded-lg bg-white p-8 shadow" > {error && ( -
+
{(error as { response?: { data?: { message?: string } } }) ?.response?.data?.message || - 'Registration failed. Please try again.'} + 'Registration failed. Please check your information and try again.'}
)} @@ -71,7 +71,7 @@ export default function RegisterPage() { placeholder="John Doe" /> {errors.fullName && ( -

+

{errors.fullName.message}

)} @@ -87,7 +87,7 @@ export default function RegisterPage() { placeholder="you@example.com" /> {errors.email && ( -

{errors.email.message}

+

{errors.email.message}

)}
@@ -101,7 +101,7 @@ export default function RegisterPage() { placeholder="••••••••" /> {errors.password && ( -

+

{errors.password.message}

)} @@ -120,7 +120,7 @@ export default function RegisterPage() { placeholder="Acme Inc." /> {errors.tenantName && ( -

+

{errors.tenantName.message}

)} diff --git a/app/(dashboard)/projects/page.tsx b/app/(dashboard)/projects/page.tsx index e671e3e..6145b02 100644 --- a/app/(dashboard)/projects/page.tsx +++ b/app/(dashboard)/projects/page.tsx @@ -2,9 +2,9 @@ import { useState } from 'react'; import Link from 'next/link'; -import { Plus, FolderKanban, Calendar } from 'lucide-react'; +import { Plus, FolderKanban, Calendar, AlertCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { @@ -17,6 +17,7 @@ import { import { useProjects } from '@/lib/hooks/use-projects'; import { ProjectForm } from '@/components/projects/project-form'; import { formatDistanceToNow } from 'date-fns'; +import { EmptyState } from '@/components/ui/empty-state'; export default function ProjectsPage() { const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); @@ -52,19 +53,15 @@ export default function ProjectsPage() { if (error) { return ( -
- - - Error Loading Projects - - {error instanceof Error ? error.message : 'Failed to load projects'} - - - - - - -
+ window.location.reload(), + }} + /> ); } @@ -121,17 +118,15 @@ export default function ProjectsPage() { ))} ) : ( - - - No projects yet - - Get started by creating your first project - - - + setIsCreateDialogOpen(true), + }} + /> )} {/* Create Project Dialog */} diff --git a/app/layout.tsx b/app/layout.tsx index 83f9ee0..8515fa1 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,7 @@ import "./globals.css"; import { QueryProvider } from "@/lib/providers/query-provider"; import { SignalRProvider } from "@/lib/signalr/SignalRContext"; import { Toaster } from "@/components/ui/sonner"; +import { ErrorBoundary } from "@/components/ErrorBoundary"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -30,12 +31,14 @@ export default function RootLayout({ - - - {children} - - - + + + + {children} + + + + ); diff --git a/components/ErrorBoundary.tsx b/components/ErrorBoundary.tsx new file mode 100644 index 0000000..73c70c0 --- /dev/null +++ b/components/ErrorBoundary.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary'; +import { AlertCircle } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +interface ErrorFallbackProps { + error: Error; + resetErrorBoundary: () => void; +} + +function ErrorFallback({ error, resetErrorBoundary }: ErrorFallbackProps) { + return ( +
+ +

Something went wrong

+

+ {error.message || 'An unexpected error occurred'} +

+
+ + +
+
+ ); +} + +interface ErrorBoundaryProps { + children: React.ReactNode; +} + +export function ErrorBoundary({ children }: ErrorBoundaryProps) { + return ( + { + // Optional: Reset application state here + // For now, we'll just reload the current page + window.location.reload(); + }} + onError={(error, errorInfo) => { + // Log error to console in development + console.error('Error caught by boundary:', error, errorInfo); + + // In production, you could send this to an error tracking service + // like Sentry, LogRocket, etc. + }} + > + {children} + + ); +} diff --git a/components/ui/empty-state.tsx b/components/ui/empty-state.tsx new file mode 100644 index 0000000..a1a658b --- /dev/null +++ b/components/ui/empty-state.tsx @@ -0,0 +1,44 @@ +import { LucideIcon } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + +interface EmptyStateProps { + icon: LucideIcon; + title: string; + description: string; + action?: { + label: string; + onClick: () => void; + variant?: 'default' | 'outline' | 'secondary'; + }; + className?: string; +} + +export function EmptyState({ + icon: Icon, + title, + description, + action, + className +}: EmptyStateProps) { + return ( +
+ +

{title}

+

+ {description} +

+ {action && ( + + )} +
+ ); +} diff --git a/components/ui/loading.tsx b/components/ui/loading.tsx new file mode 100644 index 0000000..73c2056 --- /dev/null +++ b/components/ui/loading.tsx @@ -0,0 +1,37 @@ +import { Loader2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface LoadingProps { + className?: string; + text?: string; + size?: 'sm' | 'md' | 'lg'; +} + +export function Loading({ className, text, size = 'md' }: LoadingProps) { + const sizeClasses = { + sm: 'h-4 w-4', + md: 'h-6 w-6', + lg: 'h-8 w-8', + }; + + return ( +
+ + {text && {text}} +
+ ); +} + +// Full page loading component +export function LoadingPage({ text = 'Loading...' }: { text?: string }) { + return ( +
+ +
+ ); +} + +// Inline loading for buttons or small areas +export function LoadingInline({ text }: { text?: string }) { + return ; +} diff --git a/package-lock.json b/package-lock.json index b4a0fa4..a03fb08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "next-themes": "^0.4.6", "react": "19.2.0", "react-dom": "19.2.0", + "react-error-boundary": "^6.0.0", "react-hook-form": "^7.66.0", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", @@ -256,6 +257,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -6837,6 +6847,18 @@ "react": "^19.2.0" } }, + "node_modules/react-error-boundary": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.0.0.tgz", + "integrity": "sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-hook-form": { "version": "7.66.0", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", diff --git a/package.json b/package.json index 80827e6..3f74d9e 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "next-themes": "^0.4.6", "react": "19.2.0", "react-dom": "19.2.0", + "react-error-boundary": "^6.0.0", "react-hook-form": "^7.66.0", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1",