"use client"; import { useState, useEffect, useRef, useCallback } from "react"; import { useRouter, useSearchParams } from "next/navigation"; 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 { 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 = { easy: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200", medium: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200", hard: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200", }; type SearchRecipeResult = { id: string; title: string; description: string | null; difficulty: string | null; prepMins: number | null; cookMins: number | null; authorName: string | null; }; type SearchResponse = { data: SearchRecipeResult[]; total: number; limit: number; offset: number; }; type RecipeIdea = { title: string; description: string; tags: string[]; difficulty: "easy" | "medium" | "hard"; 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 (
{children}
); } type Props = { trending: ExploreRecipeResult[]; recent: ExploreRecipeResult[]; initialQuery: string; initialTab: "recipes" | "people"; }; 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"); const [maxMins, setMaxMins] = useState(""); const [results, setResults] = useState([]); const [total, setTotal] = useState(0); const [offset, setOffset] = useState(0); const [loading, setLoading] = useState(false); const [loadingMore, setLoadingMore] = useState(false); const [ideasPrompt, setIdeasPrompt] = useState(""); const [ideas, setIdeas] = useState([]); const [ideasLoading, setIdeasLoading] = useState(false); const [generatingId, setGeneratingId] = useState(null); const inputRef = useRef(null); const debounceRef = useRef | null>(null); const fetchResults = useCallback( async (q: string, diff: string, maxM: string, off: number, append = false) => { if (!q.trim()) { setResults([]); setTotal(0); setOffset(0); return; } if (off === 0) setLoading(true); else setLoadingMore(true); try { const params = new URLSearchParams({ q, limit: "20", offset: String(off) }); if (diff && diff !== "any") params.set("difficulty", diff); if (maxM) params.set("maxMins", maxM); const res = await fetch(`/api/v1/search?${params}`); if (!res.ok) return; const json = await res.json() as SearchResponse; if (append) setResults((prev) => [...prev, ...json.data]); else setResults(json.data); setTotal(json.total); setOffset(off + json.data.length); } finally { setLoading(false); setLoadingMore(false); } }, [] ); useEffect(() => { if (initialQuery) fetchResults(initialQuery, "any", "", 0); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const updateUrl = useCallback( (q: string) => { const params = new URLSearchParams(searchParams.toString()); if (q) params.set("q", q); else params.delete("q"); router.replace(`/explore?${params}`, { scroll: false }); }, [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([]); try { const res = await fetch("/api/v1/ai/recipe-ideas", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt }), }); if (!res.ok) return; const data = await res.json() as RecipeIdea[]; setIdeas(data); } finally { setIdeasLoading(false); } }, []); const generateFromIdea = useCallback(async (idea: RecipeIdea) => { const key = idea.title; setGeneratingId(key); try { const res = await fetch("/api/v1/ai/generate-from-idea", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: idea.title }), }); if (!res.ok) return; const { id } = await res.json() as { id: string }; router.push(`/recipes/${id}`); } finally { setGeneratingId(null); } }, [router]); const handleInputChange = (value: string) => { setInputValue(value); if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { setQuery(value); updateUrl(value); fetchResults(value, difficulty, maxMins, 0); }, 300); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (debounceRef.current) clearTimeout(debounceRef.current); setQuery(inputValue); updateUrl(inputValue); fetchResults(inputValue, difficulty, maxMins, 0); }; const handleDifficultyChange = (value: string | null) => { const v = value ?? "any"; setDifficulty(v); fetchResults(query, v, maxMins, 0); }; const handleMaxMinsChange = (value: string) => { setMaxMins(value); if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { fetchResults(query, difficulty, value, 0); }, 300); }; const hasQuery = query.trim().length > 0; const hasMore = results.length < total; return (

{t("title")}

{t("tabRecipes")} {t("tabPeople")}
{/* Search bar */}
handleInputChange(e.target.value)} placeholder={t("searchPlaceholder")} className="pl-10 h-12 text-base" autoFocus={!!initialQuery} /> {hasQuery && (
handleMaxMinsChange(e.target.value)} className="w-36" /> {!loading && ( {t(total === 1 ? "resultsFoundSingular" : "resultsFoundPlural", { count: total })} )}
)}
{/* Search results */} {hasQuery && loading && (
{Array.from({ length: 8 }).map((_, i) => (
))}
)} {hasQuery && !loading && results.length === 0 && (

{t("noResultsTitle", { query })}

{t("noResultsHint")}

)} {hasQuery && !loading && results.length > 0 && ( <>
{results.map((recipe) => ( ))}
{hasMore && (
)} )} {/* Explore sections — shown when no query */} {!hasQuery && ( <> {/* AI Recipe Ideas */}

{t("recipeIdeas")}

{t("recipeIdeasHint")}

{ e.preventDefault(); fetchIdeas(ideasPrompt); }} className="flex gap-2" > setIdeasPrompt(e.target.value)} placeholder={t("aiSearchPlaceholder")} className="bg-background" />
{ideasLoading && (
{Array.from({ length: 6 }).map((_, i) => (
))}
)} {!ideasLoading && ideas.length > 0 && (
{ideas.map((idea) => (

{idea.title}

{tRecipe(`difficulty.${idea.difficulty}`)}

{idea.description}

{idea.tags.map((tag) => ( {tag} ))} {idea.totalMins && ( {tRecipe("total", { mins: idea.totalMins })} )}
))}
)}

{t("trendingThisWeek")}

{trending.length === 0 ? (

{t("noTrending")}

) : ( {trending.map((recipe) => (
))}
)}

{t("recentlyAdded")}

{recent.length === 0 ? (

{t("noRecent")}

) : (
{recent.map((recipe) => ( ))}
)}
)}
); }