DS

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/sessions

Route 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 horizontaux
Bottom 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 URL
BreadcrumbsChemin hierarchique pour les pages profondes