Navigation
Patterns de navigation dans Speedcube Master : routing file-based, liens actifs, navigation mobile, breadcrumbs et onglets.
Routing file-based (App Router)
Next.js App Router utilise le systeme de fichiers pour definir les routes. Chaque dossier dans app/ correspond a un segment d'URL, et un fichier page.tsx rend la route accessible.
// Structure des fichiers → Routes generees
src/app/
├── page.tsx // → /
├── (pages)/
│ ├── timer/
│ │ └── page.tsx // → /timer
│ ├── algos/
│ │ ├── page.tsx // → /algos
│ │ └── [id]/
│ │ └── page.tsx // → /algos/:id
│ ├── learning/
│ │ ├── page.tsx // → /learning
│ │ └── [method]/
│ │ └── page.tsx // → /learning/:method
│ ├── dashboard/
│ │ ├── layout.tsx // Layout partage du dashboard
│ │ ├── page.tsx // → /dashboard
│ │ ├── stats/
│ │ │ └── page.tsx // → /dashboard/stats
│ │ └── settings/
│ │ └── page.tsx // → /dashboard/settings
│ └── room/
│ └── [code]/
│ └── page.tsx // → /room/:code
├── api/
│ ├── solves/
│ │ └── route.ts // → /api/solves
│ └── sessions/
│ └── route.ts // → /api/sessionsRoute groups
Les dossiers entre parentheses (pages) sont des groupes de routes. Ils organisent le code sans affecter l'URL. Ils permettent aussi de partager un layout entre certaines pages.
// (pages)/ est un groupe de routes
// Il n'apparait PAS dans l'URL
src/app/(pages)/timer/page.tsx // → /timer (pas /pages/timer)
// Utilisation pour partager un layout
src/app/(pages)/layout.tsx // Layout commun a toutes les pages publiques
src/app/(admin)/layout.tsx // Layout specifique a l'admin
// Le layout du groupe s'applique a toutes ses pages
// (pages)/layout.tsx
export default function PagesLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<Header />
<main className="min-h-screen pt-16 pb-20 md:pb-0">
{children}
</main>
<MobileBottomNav />
<Footer />
</>
);
}Lien actif (usePathname)
Le hook usePathname() permet de determiner la route active et de styler le lien correspondant. C'est un composant client car il utilise un hook React.
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
interface NavLinkProps {
href: string;
children: React.ReactNode;
icon?: React.ComponentType<{ className?: string }>;
exact?: boolean; // true = match exact, false = match par prefix
}
export function NavLink({ href, children, icon: Icon, exact = false }: NavLinkProps) {
const pathname = usePathname();
const isActive = exact
? pathname === href
: pathname.startsWith(href);
return (
<Link
href={href}
className={cn(
"flex items-center gap-2 px-3 py-2 rounded-lg text-sm",
"transition-colors duration-150",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:text-foreground hover:bg-muted"
)}
>
{Icon && <Icon className={cn("h-4 w-4", isActive && "text-primary")} />}
{children}
</Link>
);
}
// Utilisation dans une sidebar
<nav className="space-y-1">
<NavLink href="/dashboard" icon={Home} exact>
Accueil
</NavLink>
<NavLink href="/dashboard/stats" icon={BarChart}>
Statistiques
</NavLink>
<NavLink href="/dashboard/settings" icon={Settings}>
Parametres
</NavLink>
</nav>Navigation mobile (bottom nav)
Sur mobile, la navigation principale est une barre fixe en bas de l'ecran. Elle est masquee sur desktop (md:hidden) au profit de la sidebar ou du header.
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Timer, BookOpen, GraduationCap, LayoutDashboard } from "lucide-react";
import { cn } from "@/lib/utils";
const navItems = [
{ href: "/timer", icon: Timer, label: "Timer" },
{ href: "/algos", icon: BookOpen, label: "Algos" },
{ href: "/learning", icon: GraduationCap, label: "Apprendre" },
{ href: "/dashboard", icon: LayoutDashboard, label: "Dashboard" },
];
export function MobileBottomNav() {
const pathname = usePathname();
return (
<nav
className="fixed bottom-0 left-0 right-0 z-50 md:hidden
border-t border-border bg-card/95 backdrop-blur-md
pb-[env(safe-area-inset-bottom)]"
>
<div className="flex items-center justify-around h-16">
{navItems.map(({ href, icon: Icon, label }) => {
const isActive = pathname.startsWith(href);
return (
<Link
key={href}
href={href}
className={cn(
"flex flex-col items-center gap-1 px-3 py-2 min-w-[64px]",
"text-xs transition-colors duration-150",
isActive
? "text-primary"
: "text-muted-foreground active:text-foreground"
)}
>
<Icon className="h-5 w-5" />
<span>{label}</span>
</Link>
);
})}
</div>
</nav>
);
}Breadcrumbs
Les breadcrumbs sont utilises dans les pages profondes pour aider l'utilisateur a se reperer. Ils affichent le chemin hierarchique vers la page courante.
import Link from "next/link";
import { ChevronRight } from "lucide-react";
interface BreadcrumbItem {
label: string;
href?: string; // Pas de href = page courante
}
function Breadcrumbs({ items }: { items: BreadcrumbItem[] }) {
return (
<nav aria-label="Fil d'Ariane" className="flex items-center gap-1.5 text-sm">
{items.map((item, index) => (
<div key={index} className="flex items-center gap-1.5">
{index > 0 && (
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
)}
{item.href ? (
<Link
href={item.href}
className="text-muted-foreground hover:text-foreground transition-colors"
>
{item.label}
</Link>
) : (
<span className="text-foreground font-medium">
{item.label}
</span>
)}
</div>
))}
</nav>
);
}
// Utilisation
<Breadcrumbs
items={[
{ label: "Algos", href: "/algos" },
{ label: "3x3", href: "/algos?cube=3x3" },
{ label: "OLL #21" },
]}
/>Navigation par onglets
Les onglets sont utilises pour naviguer entre des sous-sections d'une meme page. On utilise le composant Tabs de shadcn/ui pour le rendu local, ou des liens pour la navigation URL.
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
// Onglets locaux (sans changement d'URL)
function AlgorithmDetail({ algorithm }: { algorithm: Algorithm }) {
return (
<Tabs defaultValue="solution" className="w-full">
<TabsList className="w-full sm:w-auto">
<TabsTrigger value="solution">Solution</TabsTrigger>
<TabsTrigger value="triggers">Triggers</TabsTrigger>
<TabsTrigger value="variants">Variantes</TabsTrigger>
</TabsList>
<TabsContent value="solution" className="mt-4">
<AlgorithmSolution algorithm={algorithm} />
</TabsContent>
<TabsContent value="triggers" className="mt-4">
<AlgorithmTriggers algorithm={algorithm} />
</TabsContent>
<TabsContent value="variants" className="mt-4">
<AlgorithmVariants algorithm={algorithm} />
</TabsContent>
</Tabs>
);
}
// Onglets avec URL (chaque onglet = une route)
function DashboardTabs() {
const pathname = usePathname();
const tabs = [
{ label: "Vue d'ensemble", href: "/dashboard" },
{ label: "Statistiques", href: "/dashboard/stats" },
{ label: "Sessions", href: "/dashboard/sessions" },
];
return (
<div className="border-b border-border">
<nav className="flex gap-4 -mb-px overflow-x-auto">
{tabs.map((tab) => {
const isActive = pathname === tab.href;
return (
<Link
key={tab.href}
href={tab.href}
className={cn(
"px-1 py-3 text-sm whitespace-nowrap border-b-2 transition-colors",
isActive
? "border-primary text-primary font-medium"
: "border-transparent text-muted-foreground hover:text-foreground"
)}
>
{tab.label}
</Link>
);
})}
</nav>
</div>
);
}Recapitulatif
HeaderNavigation principale sur desktop avec liens horizontauxBottom NavNavigation principale sur mobile, fixee en bas (md:hidden)SidebarNavigation secondaire dans le dashboard (hidden md:flex)TabsSous-navigation dans une page, locale ou par URLBreadcrumbsChemin hierarchique pour les pages profondes