import { useState, useCallback } from 'react' import { useParams, useNavigate, useSearchParams, Link } from 'react-router' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { motion } from 'framer-motion' import toast from 'react-hot-toast' import { ArrowLeft, Heart, Clock, Users, ChefHat, ShoppingCart, Pencil, Minus, Plus, Send, Trash2 } from 'lucide-react' import { Dices } from 'lucide-react' import { fetchRecipe, toggleFavorite, fetchRandomRecipe } from '../api/recipes' import { addFromRecipe } from '../api/shopping' import { createNote, deleteNote } from '../api/notes' import { Badge } from '../components/ui/Badge' import { Skeleton } from '../components/ui/Skeleton' const gradients = ['from-primary/40 to-secondary/40', 'from-secondary/40 to-sage/40'] export function RecipePage() { const { slug } = useParams<{ slug: string }>() const navigate = useNavigate() const [searchParams] = useSearchParams() const fromRandom = searchParams.get('from') === 'random' const qc = useQueryClient() const [servingScale, setServingScale] = useState(null) const [noteText, setNoteText] = useState('') const [rerolling, setRerolling] = useState(false) const handleReroll = useCallback(async () => { setRerolling(true) try { const r = await fetchRandomRecipe() if (r?.slug) navigate(`/recipe/${r.slug}?from=random`, { replace: true }) } catch { /* ignore */ } setRerolling(false) }, [navigate]) const { data: recipe, isLoading } = useQuery({ queryKey: ['recipe', slug], queryFn: () => fetchRecipe(slug!), enabled: !!slug, }) const favMutation = useMutation({ mutationFn: () => toggleFavorite(recipe!.id), onSuccess: () => { qc.invalidateQueries({ queryKey: ['recipe', slug] }) qc.invalidateQueries({ queryKey: ['recipes'] }) }, onError: () => toast.error('Fehler beim Ändern des Favoriten-Status'), }) const shoppingMutation = useMutation({ mutationFn: () => addFromRecipe(recipe!.id), onSuccess: (data) => { qc.invalidateQueries({ queryKey: ['shopping'] }) toast.success(`${data.added} Zutaten zur Einkaufsliste hinzugefügt!`) }, onError: () => toast.error('Fehler beim Hinzufügen zur Einkaufsliste'), }) const noteMutation = useMutation({ mutationFn: (content: string) => createNote(recipe!.id, content), onSuccess: () => { qc.invalidateQueries({ queryKey: ['recipe', slug] }) setNoteText('') toast.success('Notiz gespeichert! 📝') }, onError: () => toast.error('Fehler beim Speichern der Notiz'), }) const deleteNoteMutation = useMutation({ mutationFn: (noteId: string) => deleteNote(noteId), onSuccess: () => { qc.invalidateQueries({ queryKey: ['recipe', slug] }) toast.success('Notiz gelöscht') }, }) if (isLoading) { return (
) } if (!recipe) return
Rezept nicht gefunden.
const originalServings = recipe.servings || 4 const currentServings = servingScale ?? originalServings const scaleFactor = currentServings / originalServings const totalTime = recipe.total_time_min || ((recipe.prep_time_min || 0) + (recipe.cook_time_min || 0)) const gradient = gradients[recipe.title.length % gradients.length] const scaleAmount = (amount?: number) => { if (!amount) return '' const scaled = amount * scaleFactor // Nice formatting if (scaled === Math.round(scaled)) return String(Math.round(scaled)) return scaled.toFixed(1).replace(/\.0$/, '') } // Group ingredients const ingredientGroups = (recipe.ingredients || []).reduce>((acc, ing) => { const group = ing.group_name || 'Zutaten' if (!acc[group]) acc[group] = [] acc[group]!.push(ing) return acc }, {}) return ( {/* Hero */}
{recipe.image_url ? ( {recipe.title} ) : (
🍰
)}
{/* Title & Meta */}

{recipe.title}

{recipe.category_name ? ( {recipe.category_name} ) : ( 🍽️ Allgemein )} {totalTime > 0 && ( {totalTime} min )} {recipe.difficulty && ( {recipe.difficulty} )}
{recipe.description &&

{recipe.description}

}
{/* Tags */} {recipe.tags && recipe.tags.length > 0 && (
{recipe.tags.map((tag) => ( #{tag} ))}
)} {/* Serving Calculator */}
Portionen
{currentServings}
{servingScale !== null && servingScale !== originalServings && ( )}
{/* Ingredients */} {Object.keys(ingredientGroups).length > 0 && (

Zutaten

{Object.entries(ingredientGroups).map(([group, items]) => (
{Object.keys(ingredientGroups).length > 1 && (

{group}

)}
    {items!.map((ing, i) => ( {ing.amount ? `${scaleAmount(ing.amount)} ${ing.unit || ''}`.trim() : ''} {ing.name} ))}
))}
)} {/* Add to shopping list */} {recipe.ingredients && recipe.ingredients.length > 0 && ( )} {/* Steps */} {recipe.steps && recipe.steps.length > 0 && (

Zubereitung

    {recipe.steps.map((step, i) => ( {step.step_number || i + 1}

    {step.instruction}

    {step.timer_minutes && ( {step.timer_label || 'Timer'}: {step.timer_minutes} min )}
    ))}
)} {/* Notes */}

Notizen

{/* Existing notes */} {recipe.notes && recipe.notes.length > 0 && (
{recipe.notes.map((note) => ( 📝 {note.content} ))}
)} {/* Add note */}
setNoteText(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && noteText.trim()) noteMutation.mutate(noteText.trim()) }} placeholder="Notiz hinzufügen..." className="flex-1 bg-surface border border-sand rounded-xl px-4 py-3 text-sm text-espresso placeholder:text-warm-grey/50 focus:outline-none focus:ring-2 focus:ring-primary/30 min-h-[44px]" />
{/* Floating Re-Roll Button */} {fromRandom && ( )}
) }