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:
@@ -2,15 +2,16 @@
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Flame, Clock, ChefHat, Search, Sparkles, Wand2, ArrowRight } from "lucide-react";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import { Flame, Clock, ChefHat, Search, Sparkles, Wand2, ArrowRight, Shuffle } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import type { RecipeResult as ExploreRecipeResult } from "@/app/(app)/explore/page";
|
||||
import { SearchResultCard } from "@/components/recipe/search-result-card";
|
||||
import { PeopleSearch } from "@/components/social/people-search";
|
||||
|
||||
const DIFFICULTY_COLORS: Record<string, string> = {
|
||||
easy: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
|
||||
@@ -43,6 +44,19 @@ type RecipeIdea = {
|
||||
totalMins?: number;
|
||||
};
|
||||
|
||||
const SURPRISE_IDEA_PROMPTS = [
|
||||
"use only 5 ingredients",
|
||||
"no oven required",
|
||||
"one-pot meal",
|
||||
"ready in 20 minutes or less",
|
||||
"budget-friendly student meal",
|
||||
"impressive but secretly easy dinner party dish",
|
||||
"leftover-friendly comfort food",
|
||||
"kid-friendly weeknight dinner",
|
||||
"high-protein post-workout meal",
|
||||
"cozy soup for a rainy day",
|
||||
];
|
||||
|
||||
|
||||
function HorizontalScroll({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
@@ -56,15 +70,18 @@ type Props = {
|
||||
trending: ExploreRecipeResult[];
|
||||
recent: ExploreRecipeResult[];
|
||||
initialQuery: string;
|
||||
initialTab: "recipes" | "people";
|
||||
};
|
||||
|
||||
export function ExplorePageContent({ trending, recent, initialQuery }: Props) {
|
||||
export function ExplorePageContent({ trending, recent, initialQuery, initialTab }: Props) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const t = useTranslations("explore");
|
||||
const tCommon = useTranslations("common");
|
||||
const tRecipe = useTranslations("recipe");
|
||||
|
||||
const [activeTab, setActiveTab] = useState<"recipes" | "people">(initialTab);
|
||||
|
||||
const [inputValue, setInputValue] = useState(initialQuery);
|
||||
const [query, setQuery] = useState(initialQuery);
|
||||
const [difficulty, setDifficulty] = useState("any");
|
||||
@@ -127,6 +144,18 @@ export function ExplorePageContent({ trending, recent, initialQuery }: Props) {
|
||||
[router, searchParams]
|
||||
);
|
||||
|
||||
const handleTabChange = useCallback(
|
||||
(value: string | null) => {
|
||||
const next = value === "people" ? "people" : "recipes";
|
||||
setActiveTab(next);
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
if (next === "people") params.set("tab", "people");
|
||||
else params.delete("tab");
|
||||
router.replace(`/explore?${params}`, { scroll: false });
|
||||
},
|
||||
[router, searchParams]
|
||||
);
|
||||
|
||||
const fetchIdeas = useCallback(async (prompt: string) => {
|
||||
setIdeasLoading(true);
|
||||
setIdeas([]);
|
||||
@@ -197,15 +226,21 @@ export function ExplorePageContent({ trending, recent, initialQuery }: Props) {
|
||||
const hasMore = results.length < total;
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto space-y-10">
|
||||
<div className="max-w-5xl mx-auto space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-3xl font-bold">{t("title")}</h1>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={handleTabChange} className="gap-6">
|
||||
<TabsList>
|
||||
<TabsTrigger value="recipes">{t("tabRecipes")}</TabsTrigger>
|
||||
<TabsTrigger value="people">{t("tabPeople")}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="recipes">
|
||||
<div className="space-y-10">
|
||||
{/* Search bar */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-3xl font-bold">{t("title")}</h1>
|
||||
<Link href="/people" className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4">
|
||||
Find people
|
||||
</Link>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground pointer-events-none" />
|
||||
<Input
|
||||
@@ -325,10 +360,15 @@ export function ExplorePageContent({ trending, recent, initialQuery }: Props) {
|
||||
type="button"
|
||||
variant="ghost"
|
||||
disabled={ideasLoading}
|
||||
onClick={() => { setIdeasPrompt(""); fetchIdeas(""); }}
|
||||
onClick={() => {
|
||||
const idx = Math.floor(Math.random() * SURPRISE_IDEA_PROMPTS.length);
|
||||
const surprisePrompt = SURPRISE_IDEA_PROMPTS[idx]!;
|
||||
setIdeasPrompt(surprisePrompt);
|
||||
fetchIdeas(surprisePrompt);
|
||||
}}
|
||||
className="shrink-0"
|
||||
>
|
||||
{t("surpriseMe")}
|
||||
<span className="flex items-center gap-2"><Shuffle className="h-4 w-4" /> {t("surpriseMe")}</span>
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -425,6 +465,13 @@ export function ExplorePageContent({ trending, recent, initialQuery }: Props) {
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="people">
|
||||
<PeopleSearch />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user