"use client"; import { useState, useEffect, useRef, useCallback } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import Link from "next/link"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Search, Clock, ChefHat } from "lucide-react"; type RecipeResult = { id: string; title: string; description: string | null; difficulty: string | null; prepMins: number | null; cookMins: number | null; authorName: string | null; }; type SearchResponse = { data: RecipeResult[]; total: number; limit: number; offset: number; }; 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", }; function RecipeCard({ recipe }: { recipe: RecipeResult }) { const totalMins = (recipe.prepMins ?? 0) + (recipe.cookMins ?? 0); return (

{recipe.title}

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

{recipe.description}

)}
{totalMins > 0 && ( {totalMins}m )} {recipe.authorName && ( {recipe.authorName} )}
); } export function SearchPageContent({ initialQuery }: { initialQuery: string }) { const router = useRouter(); const searchParams = useSearchParams(); const [query, setQuery] = useState(initialQuery); const [inputValue, setInputValue] = 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 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: SearchResponse = await res.json(); if (append) { setResults((prev) => [...prev, ...json.data]); } else { setResults(json.data); } setTotal(json.total); setOffset(off + json.data.length); } finally { setLoading(false); setLoadingMore(false); } }, [] ); // Auto-focus input on mount useEffect(() => { inputRef.current?.focus(); }, []); // Fetch on mount if initialQuery provided useEffect(() => { if (initialQuery) { fetchResults(initialQuery, "any", "", 0); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Sync query param into URL const updateUrl = useCallback( (q: string) => { const params = new URLSearchParams(searchParams.toString()); if (q) params.set("q", q); else params.delete("q"); router.replace(`/search?${params}`, { scroll: false }); }, [router, searchParams] ); 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 handleFilterChange = (newDiff: string, newMaxMins: string) => { fetchResults(query, newDiff, newMaxMins, 0); }; const handleDifficultyChange = (value: string | null) => { const v = value ?? "any"; setDifficulty(v); handleFilterChange(v, maxMins); }; const handleMaxMinsChange = (value: string) => { setMaxMins(value); if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { handleFilterChange(difficulty, value); }, 300); }; const handleLoadMore = () => { fetchResults(query, difficulty, maxMins, offset, true); }; const hasMore = results.length < total; const hasQuery = query.trim().length > 0; return (

Search Recipes

{/* Search input */}
handleInputChange(e.target.value)} placeholder="Search for recipes..." className="pl-10 h-12 text-base" /> {/* Filter bar */}
handleMaxMinsChange(e.target.value)} className="w-36" />
{hasQuery && !loading && ( {total} {total === 1 ? "recipe" : "recipes"} found )}
{/* States */} {!hasQuery && (

Search for recipes...

Try "pasta", "vegan dessert", or "quick dinner"

)} {hasQuery && loading && (
{Array.from({ length: 8 }).map((_, i) => (
))}
)} {hasQuery && !loading && results.length === 0 && (

No recipes found for “{query}”

Try different keywords or remove filters

)} {!loading && results.length > 0 && ( <>
{results.map((recipe) => ( ))}
{hasMore && (
)} )}
); }