Routing
Learn how to use Supastart's routing system.
Routing
Supastart uses Next.js App Router with a structured routing system that includes route groups, protected routes, and authentication-based routing.
Route Structure
app/
├── (auth)/ # Authentication routes
│ ├── signin/ # Login page
│ ├── signup/ # Registration page
│ ├── change-password/ # Change password page
│ ├── reset-password/ # Reset password request page
│ └── verify-email/ # Email verification page
│
├── (protected)/ # Protected routes (require authentication)
│ ├── dashboard/ # Main dashboard
│ ├── account/ # User account settings
│ ├── logs/ # Activity logs
│ ├── settings/ # Application settings
│ └── user-management/ # User management features
│
├── api/ # API routes
│ └── ...
│
├── auth/ # Auth callback handling
│ └── callback/ # OAuth and email verification callback
│
└── components/ # Shared components
└── ...
Route Groups
Supastart uses route groups for logical organization:
-
Authentication Group
(auth)
- Contains all authentication-related pages
- Uses a centered auth layout
- Public access
- Special auth flow handling
-
Protected Group
(protected)
- Contains all authenticated user pages
- Uses DefaultLayout with header, sidebar, and footer
- Requires authentication through the AuthProvider
- Automatic redirection to login if not authenticated
Protected Routes
Protected routes are implemented through a layout wrapper that checks for authentication:
// app/(protected)/layout.tsx
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/providers';
import { DefaultLayout } from '@/app/components/layouts/default/default-layout';
import { ScreenLoader } from '@/app/components/common/screen-loader';
import { useSyncSession } from '@/hooks/use-sync-session';
export default function ProtectedLayout({
children,
}: {
children: React.ReactNode;
}) {
const { user, hasInitialized, isLoading: isAuthLoading } = useAuth();
const router = useRouter();
const [redirecting, setRedirecting] = useState(false);
// Session synchronization
const { syncing } = useSyncSession();
// Combined loading state
const isLoading = isAuthLoading || syncing || !hasInitialized;
useEffect(() => {
if (hasInitialized && !user && !redirecting) {
setRedirecting(true);
router.push('/signin');
}
}, [user, hasInitialized, router, redirecting]);
// Show loading state during authentication processing
if (isLoading) {
return <ScreenLoader />;
}
// Only render the layout if we have a user
return user ? <DefaultLayout>{children}</DefaultLayout> : null;
}
Authentication Routing
Auth Callback Route
The callback route in app/auth/callback/route.ts
handles various authentication flows:
export async function GET(request: NextRequest) {
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get('code');
const type = requestUrl.searchParams.get('type');
// Create Supabase client
const cookieStore = cookies();
const supabase = createRouteHandlerClient({ cookies: () => cookieStore });
// Handle PKCE flow with code exchange
if (code) {
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (error) {
return NextResponse.redirect(new URL('/signin?error=auth_callback_failed', request.url));
}
}
// Redirect based on the auth flow type
if (type === 'recovery') {
// Password reset flow
return NextResponse.redirect(new URL('/change-password', request.url));
} else if (type === 'signup') {
// Signup confirmation flow
return NextResponse.redirect(new URL('/verify-email', request.url));
} else {
// Default flow (sign in)
return NextResponse.redirect(new URL('/dashboard', request.url));
}
}
Navigation
Client-Side Navigation
Use the useRouter
hook for programmatic navigation:
import { useRouter } from 'next/navigation';
function NavigationExample() {
const router = useRouter();
const handleNavigate = () => {
router.push('/dashboard');
};
return <button onClick={handleNavigate}>Go to Dashboard</button>;
}
Link Component
Use the Link
component for declarative navigation:
import Link from 'next/link';
function NavigationLinks() {
return (
<nav>
<Link href="/dashboard">Dashboard</Link>
<Link href="/account">Account Settings</Link>
<Link href="/settings">App Settings</Link>
</nav>
);
}
Layouts
Auth Layout
Authentication pages use a centered layout:
// app/(auth)/layout.tsx
'use client';
import { I18nProvider } from '@/providers';
import { Container } from '@/app/components/common/container';
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<I18nProvider>
<div className="bg-background min-h-screen flex flex-col">
<Container className="flex-1 flex items-center justify-center py-12">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-card py-8 px-4 shadow-md rounded-lg sm:px-10 border border-border">
{children}
</div>
</div>
</Container>
</div>
</I18nProvider>
);
}
Default Layout
Protected routes use a layout with header, sidebar, and footer:
// app/components/layouts/default/default-layout.tsx
import { ReactNode } from 'react';
import { Footer } from './common/footer';
import { Header } from './common/header';
import { Sidebar } from './sidebar';
interface DefaultLayoutProps {
children: ReactNode;
}
export function DefaultLayout({ children }: DefaultLayoutProps) {
return (
<div className="flex flex-col min-h-screen">
<Header />
<div className="flex flex-1">
<Sidebar />
<main className="flex-1 pt-16">{children}</main>
</div>
<Footer />
</div>
);
}
Tab-Based Routing
For complex interfaces that use tabs, Supastart implements a pattern for synchronized tab navigation:
// Example from settings layout
export default function Layout({ children }: { children: React.ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const [activeTab, setActiveTab] = useState<string>('');
const navRoutes = useMemo<NavRoutes>(
() => ({
general: {
title: 'General',
path: '/settings',
},
social: {
title: 'Social',
path: '/settings/social',
},
}),
[],
);
// Keep the local state in sync with the current pathname
useEffect(() => {
const found = Object.keys(navRoutes).find(
(key) => pathname === navRoutes[key].path,
);
if (found) {
setActiveTab(found);
} else {
setActiveTab('general');
}
}, [navRoutes, pathname]);
// Handle tab click: update local state immediately and trigger navigation
const handleTabClick = (key: string, path: string) => {
setActiveTab(key);
router.push(path);
};
return (
<>
<Tabs value={activeTab}>
<TabsList>
{Object.keys(navRoutes).map((key) => (
<TabsTrigger
key={key}
value={key}
onClick={() => handleTabClick(key, navRoutes[key].path)}
>
{navRoutes[key].title}
</TabsTrigger>
))}
</TabsList>
</Tabs>
{children}
</>
);
}
Breadcrumbs
Navigation breadcrumbs help users understand their location:
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/account">Account</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Profile</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
Route Loading States
Supastart provides loading states for routes:
Screen Loader
For full-screen loading during authentication and route transitions:
// app/components/common/screen-loader.tsx
export function ScreenLoader() {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80">
<Spinner className="h-8 w-8 text-primary" />
</div>
);
}
Content Loading
For loading specific content:
// app/components/common/content-loader.tsx
export function ContentLoader({ className }: { className?: string }) {
return (
<div className={cn("flex justify-center p-8", className)}>
<Spinner className="h-6 w-6 text-primary" />
</div>
);
}
Error Handling
The application includes custom error handling components:
Not Found
// app/not-found.tsx
export default function NotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h1 className="text-4xl font-bold">404 - Page Not Found</h1>
<p className="mt-4 text-muted-foreground">
The page you're looking for doesn't exist.
</p>
<Link href="/" className="mt-6 btn btn-primary">
Go Home
</Link>
</div>
);
}
Error Component
// app/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h1 className="text-4xl font-bold">Something went wrong</h1>
<p className="mt-4 text-muted-foreground">{error.message}</p>
<button onClick={reset} className="mt-6 btn btn-primary">
Try again
</button>
</div>
);
}
Auth Provider Navigation
The AuthProvider handles authentication-related navigation:
// providers/auth-provider.tsx
const handleAuthEvent = useCallback((event: AuthChangeEvent, session: Session | null) => {
setTimeout(async () => {
try {
setIsLoading(true);
if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') {
setUser(session?.user ?? null);
setSession(session);
router.push('/dashboard');
} else if (event === 'SIGNED_OUT') {
setUser(null);
setSession(null);
if (!pathname?.startsWith('/signin') && !pathname?.startsWith('/signup')) {
router.push('/signin');
}
}
} catch (error) {
console.error('Auth state change error:', error);
} finally {
setIsLoading(false);
}
}, 0);
}, [router, pathname]);