feat: private accounts, explore/people merge, meal-plan fixes, i18n and theme cleanup

- Private accounts: users.isPrivate hides a user from search and their
  recipes from search/trending/for-you discovery surfaces (follow-aware
  where the route already has session context, blanket exclusion where it
  doesn't); existing followers and direct links are unaffected, no
  follow-request approval flow was built (explicit scope limit)
- Merged /people into the Explore tab (tab=people query param); the old
  standalone route now redirects there
- "Get Ideas" vs "Surprise Me" were doing the same empty-prompt call;
  Surprise Me now injects a real random constraint (5-ingredient, one-pot,
  etc.), matching the existing pattern in the AI recipe-generate dialog
- Meal-plan day cells now link to their recipe (was dead text) and gained
  a one-click "mark as cooked" action
- Theme toggle is now a real three-way light/dark/system control instead
  of a binary flip
- Nutrition goals form had zero i18n wiring; fully localized now

New migration 0027 (users.is_private) generated, left unapplied like the
others. Verified with typecheck, lint, and a clean --no-cache docker build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-10 09:26:03 +02:00
parent 36e7698096
commit 9c545a5bb3
20 changed files with 4804 additions and 82 deletions
+32 -8
View File
@@ -3,7 +3,7 @@
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useTheme } from "next-themes";
import { BookOpen, Calendar, Package, ChefHat, User, Rss, FolderOpen, ShoppingCart, Shield, Search, Compass, Menu, Sun, Moon, Apple } from "lucide-react";
import { BookOpen, Calendar, Package, ChefHat, User, Rss, FolderOpen, ShoppingCart, Shield, Search, Compass, Menu, Sun, Moon, Monitor, Apple } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button, buttonVariants } from "@/components/ui/button";
import {
@@ -44,8 +44,13 @@ export function Nav() {
const { data: session } = authClient.useSession();
const isAdmin = (session?.user as { role?: string } | undefined)?.role === "admin";
const username = (session?.user as { username?: string } | undefined)?.username;
const { resolvedTheme, setTheme } = useTheme();
const { theme, setTheme } = useTheme();
const t = useTranslations("nav");
const THEME_OPTIONS = [
{ value: "light", icon: Sun, label: t("lightMode") },
{ value: "dark", icon: Moon, label: t("darkMode") },
{ value: "system", icon: Monitor, label: t("systemMode") },
] as const;
return (
<header className="sticky top-0 z-50 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container mx-auto flex h-14 items-center gap-6 px-4">
@@ -126,13 +131,32 @@ export function Nav() {
<DropdownMenuItem>
<Link href="/settings" className="w-full">{t("settings")}</Link>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
className="flex items-center gap-2"
<div
role="group"
aria-label={t("systemMode")}
className="mx-1.5 my-1 flex items-center gap-0.5 rounded-md border bg-muted/50 p-0.5"
>
{resolvedTheme === "dark" ? <Sun className="h-3.5 w-3.5" /> : <Moon className="h-3.5 w-3.5" />}
{resolvedTheme === "dark" ? t("lightMode") : t("darkMode")}
</DropdownMenuItem>
{THEME_OPTIONS.map(({ value, icon: Icon, label }) => (
<button
key={value}
type="button"
title={label}
aria-label={label}
aria-pressed={theme === value}
onClick={(e) => {
e.preventDefault();
setTheme(value);
}}
className={cn(
"flex flex-1 items-center justify-center rounded-sm p-1.5 text-muted-foreground transition-colors hover:text-foreground",
theme === value && "bg-background text-foreground shadow-sm"
)}
>
<Icon className="h-3.5 w-3.5" />
<span className="sr-only">{label}</span>
</button>
))}
</div>
{isAdmin && (
<>
<DropdownMenuSeparator />