diff --git a/app/(dashboard)/_components/board-list.tsx b/app/(dashboard)/_components/board-list.tsx index ab25f0f..47902ff 100644 --- a/app/(dashboard)/_components/board-list.tsx +++ b/app/(dashboard)/_components/board-list.tsx @@ -19,6 +19,7 @@ export const BoardList = ({ orgId }: BoardListProps) => { const searchParams = useSearchParams(); const { organization } = useOrganization(); const [isOrgSwitching, setIsOrgSwitching] = useState(false); + const [hasConvexError, setHasConvexError] = useState(false); // Inject keyframes dynamically since no global CSS is used useEffect(() => { @@ -41,14 +42,17 @@ export const BoardList = ({ orgId }: BoardListProps) => { }; }, []); - // Handle organization switching + // Handle organization switching with improved error handling useEffect(() => { if (organization?.id !== orgId) { setIsOrgSwitching(true); const timer = setTimeout(() => { setIsOrgSwitching(false); - }, 1000); + }, 2000); // Increased timeout to handle RSC delays return () => clearTimeout(timer); + } else { + // Clear switching state when organization matches + setIsOrgSwitching(false); } }, [organization?.id, orgId]); @@ -63,9 +67,53 @@ export const BoardList = ({ orgId }: BoardListProps) => { const data = useQuery(api.boards.get, { orgId, search, favorites }); + // Handle Convex connection errors with improved timeout + useEffect(() => { + const timeout = setTimeout(() => { + if (data === undefined && !hasConvexError && !isOrgSwitching) { + setHasConvexError(true); + } + }, 8000); // Increased timeout to handle organization switching + + if (data !== undefined) { + setHasConvexError(false); + } + + return () => clearTimeout(timeout); + }, [data, hasConvexError, isOrgSwitching]); + // Debug logging // console.log("BoardList - Search:", search, "Favorites:", favorites, "Data:", data); + // Show error state when Convex backend is unavailable + if (hasConvexError) { + return ( +
+
+
🔌
+

Connection Error

+

+ Unable to connect to the backend. Please make sure the Convex development server is running. +

+
+

+ Run this command in your terminal: +

+ + npx convex dev + +
+ +
+
+ ); + } + // Show loading state when organization is switching if (isOrgSwitching || data === undefined) { return ( diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index 10a7e3a..ad6bbbe 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -7,6 +7,7 @@ import { ConvexClientProvider } from "@/providers/convex-client-provider"; import { Toaster } from "@/components/ui/sonner"; import { ModalProvider } from "@/providers/modal-provider"; import { useEffect } from "react"; +import ErrorBoundary from "@/components/error-boundary"; interface DashboardLayoutProps { children: React.ReactNode; @@ -81,7 +82,9 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
- {children} + + {children} +
diff --git a/app/(dashboard)/page.tsx b/app/(dashboard)/page.tsx index 0be87c2..06d6648 100644 --- a/app/(dashboard)/page.tsx +++ b/app/(dashboard)/page.tsx @@ -4,9 +4,56 @@ import { useOrganization } from "@clerk/nextjs"; import { EmptyOrg } from "./_components/empty-org"; import { BoardList } from "./_components/board-list"; import { MobileOptimization } from "@/components/mobile-optimization"; +import { useEffect, useState } from "react"; const DashboardPage = () => { - const { organization } = useOrganization(); + const { organization, isLoaded } = useOrganization(); + const [orgError, setOrgError] = useState(false); + + // Handle organization loading errors with improved timeout + useEffect(() => { + const timeout = setTimeout(() => { + if (!isLoaded && !orgError) { + setOrgError(true); + } + }, 5000); // Increased timeout to handle RSC delays + + if (isLoaded) { + setOrgError(false); + } + + return () => clearTimeout(timeout); + }, [isLoaded, orgError]); + + // Show error state if organization fails to load + if (orgError) { + return ( +
+
+
⚠️
+

Authentication Error

+

+ Unable to load organization information. Please check your authentication setup. +

+ +
+
+ ); + } + + // Show loading state while organization is loading + if (!isLoaded) { + return ( +
+
+
+ ); + } return ( <> diff --git a/components/error-boundary.tsx b/components/error-boundary.tsx new file mode 100644 index 0000000..1231b81 --- /dev/null +++ b/components/error-boundary.tsx @@ -0,0 +1,78 @@ +"use client"; + +import React from "react"; +import { Button } from "@/components/ui/button"; + +interface ErrorBoundaryState { + hasError: boolean; + error?: Error; +} + +interface ErrorBoundaryProps { + children: React.ReactNode; + fallback?: React.ComponentType<{ error?: Error; reset: () => void }>; +} + +export class ErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error("ErrorBoundary caught an error:", error, errorInfo); + } + + reset = () => { + this.setState({ hasError: false, error: undefined }); + }; + + render() { + if (this.state.hasError) { + const FallbackComponent = this.props.fallback || DefaultErrorFallback; + return ; + } + + return this.props.children; + } +} + +function DefaultErrorFallback({ error, reset }: { error?: Error; reset: () => void }) { + return ( +
+
+
⚠️
+

Something went wrong

+

+ {error?.message || "An unexpected error occurred while loading this page."} +

+
+ + +
+ {process.env.NODE_ENV === "development" && error && ( +
+ Error Details +
+              {error.stack}
+            
+
+ )} +
+
+ ); +} + +export default ErrorBoundary; diff --git a/middleware.ts b/middleware.ts index d2b5c0a..569477d 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,8 +1,12 @@ -import { clerkMiddleware } from "@clerk/nextjs/server"; +import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; + +const isPublicRoute = createRouteMatcher(["/", "/sign-in(.*)", "/sign-up(.*)"]); export default clerkMiddleware(async (auth, req) => { const { userId, redirectToSignIn } = await auth(); - if (!userId) { + + // Protect routes that aren't public + if (!userId && !isPublicRoute(req)) { redirectToSignIn(); } });