"use client"; 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 { useTranslations } from "next-intl"; import type { RecipeResult as ExploreRecipeResult } from "@/app/(app)/explore/page"; 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; }; function RecipeCard({ recipe }: { recipe: ExploreRecipeResult | SearchRecipeResult }) { const totalMins = (recipe.prepMins ?? 0) + (recipe.cookMins ?? 0); const description = "description" in recipe ? recipe.description : null; return (

{recipe.title}

{recipe.difficulty && ( {recipe.difficulty} )}
{description && (

{description}

)}
{totalMins > 0 && ( {totalMins}m )} {recipe.authorName && ( {recipe.authorName} )}
); } function HorizontalScroll({ children }: { children: React.ReactNode }) { return (
{children}
); } type Props = { trending: ExploreRecipeResult[]; recent: ExploreRecipeResult[]; initialQuery: string; }; export function ExplorePageContent({ trending, recent, initialQuery }: Props) { const router = useRouter(); const searchParams = useSearchParams(); const t = useTranslations("explore"); const tCommon = useTranslations("common"); 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 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 (
{/* Search bar */}

Explore

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 && ( {total} {total === 1 ? "recipe" : "recipes"} found )}
)}
{/* Search results */} {hasQuery && loading && (
{Array.from({ length: 8 }).map((_, i) => (
))}
)} {hasQuery && !loading && results.length === 0 && (

No recipes found for “{query}”

Try different keywords or remove filters

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

Recipe ideas

Tell the AI what you're in the mood for, or let it surprise you.

{ 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}

{idea.difficulty}

{idea.description}

{idea.tags.map((tag) => ( {tag} ))} {idea.totalMins && ( {idea.totalMins}m )}
))}
)}

Trending this week

{trending.length === 0 ? (

No trending recipes yet. Be the first to share one!

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

Recently added

{recent.length === 0 ? (

No public recipes yet.

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