Marketplace inversée (Next.js 16 App Router + Supabase Cloud) : auth, schéma + RLS, publication de besoins avec upload d'images, feed avec filtres, offres, acceptation via RPC, messagerie temps réel, dashboards, et Dockerfile pour déploiement Coolify.
125 lines
4.1 KiB
TypeScript
125 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { Menu, X } from "lucide-react";
|
|
import { logout } from "@/lib/actions/auth";
|
|
import { Button } from "@/components/ui/button";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
type NavUser = { id: string; email?: string } | null;
|
|
|
|
const loggedInLinks = [
|
|
{ href: "/needs", label: "Besoins" },
|
|
{ href: "/dashboard/my-needs", label: "Mes besoins" },
|
|
{ href: "/dashboard/my-offers", label: "Mes offres" },
|
|
{ href: "/messages", label: "Messages" },
|
|
];
|
|
|
|
const loggedOutLinks = [{ href: "/needs", label: "Besoins" }];
|
|
|
|
export function Navbar({ user }: { user: NavUser }) {
|
|
const [open, setOpen] = useState(false);
|
|
const pathname = usePathname();
|
|
const links = user ? loggedInLinks : loggedOutLinks;
|
|
|
|
return (
|
|
<header className="sticky top-0 z-40 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-4 sm:px-6">
|
|
<Link href="/" className="font-semibold tracking-tight" onClick={() => setOpen(false)}>
|
|
WaMarket
|
|
</Link>
|
|
|
|
<nav className="hidden items-center gap-6 text-sm font-medium md:flex">
|
|
{links.map((link) => (
|
|
<Link
|
|
key={link.href}
|
|
href={link.href}
|
|
className={cn(
|
|
"text-muted-foreground transition-colors hover:text-foreground",
|
|
pathname === link.href && "text-foreground"
|
|
)}
|
|
>
|
|
{link.label}
|
|
</Link>
|
|
))}
|
|
</nav>
|
|
|
|
<div className="hidden items-center gap-2 md:flex">
|
|
{user ? (
|
|
<>
|
|
<Button asChild size="sm">
|
|
<Link href="/needs/new">Publier un besoin</Link>
|
|
</Button>
|
|
<form action={logout}>
|
|
<Button variant="ghost" size="sm" type="submit">
|
|
Se déconnecter
|
|
</Button>
|
|
</form>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Button asChild variant="ghost" size="sm">
|
|
<Link href="/auth/login">Se connecter</Link>
|
|
</Button>
|
|
<Button asChild size="sm">
|
|
<Link href="/auth/signup">S'inscrire</Link>
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
aria-label="Ouvrir le menu"
|
|
className="flex h-9 w-9 items-center justify-center rounded-md md:hidden"
|
|
onClick={() => setOpen((v) => !v)}
|
|
>
|
|
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
|
</button>
|
|
</div>
|
|
|
|
{open && (
|
|
<div className="border-t px-4 py-4 md:hidden">
|
|
<nav className="flex flex-col gap-3 text-sm font-medium">
|
|
{links.map((link) => (
|
|
<Link
|
|
key={link.href}
|
|
href={link.href}
|
|
onClick={() => setOpen(false)}
|
|
className="text-muted-foreground hover:text-foreground"
|
|
>
|
|
{link.label}
|
|
</Link>
|
|
))}
|
|
</nav>
|
|
<div className="mt-4 flex flex-col gap-2">
|
|
{user ? (
|
|
<>
|
|
<Button asChild size="sm" onClick={() => setOpen(false)}>
|
|
<Link href="/needs/new">Publier un besoin</Link>
|
|
</Button>
|
|
<form action={logout}>
|
|
<Button variant="ghost" size="sm" type="submit" className="w-full">
|
|
Se déconnecter
|
|
</Button>
|
|
</form>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Button asChild variant="ghost" size="sm" onClick={() => setOpen(false)}>
|
|
<Link href="/auth/login">Se connecter</Link>
|
|
</Button>
|
|
<Button asChild size="sm" onClick={() => setOpen(false)}>
|
|
<Link href="/auth/signup">S'inscrire</Link>
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</header>
|
|
);
|
|
}
|