'use client'; import { useState, useEffect, useTransition } from 'react'; import { useRouter } from 'next/navigation'; import type { Recipe, Product, RecipeInventoryPreview, } from '../../../features/inventory/types'; import { fetchJson } from '../../../lib/api'; import { parseErrorResponse } from '../../../lib/error-handler'; // ────────────────────────────────────────────── // Hjälpfunktioner // ────────────────────────────────────────────── function SimpleMarkdownPreview({ text }: { text: string }) { return (
{text.split('\n').map((line, i) => { if (line.startsWith('# ')) return

{line.slice(2)}

; if (line.startsWith('## ')) return

{line.slice(3)}

; if (line.startsWith('- ') || line.startsWith('* ')) return
• {line.slice(2)}
; if (line.trim() === '') return
; return
{line}
; })}
); } function StatusBadge({ status }: { status: 'enough' | 'missing' | 'unit_mismatch' }) { const styles = { enough: { label: 'Räcker', color: '#1f5f2c', background: '#ecf8ee', border: '#b9e0bf' }, missing: { label: 'Saknas', color: '#8b0000', background: '#ffeaea', border: '#f1b5b5' }, unit_mismatch: { label: 'Enhetskonflikt', color: '#8a4b00', background: '#fff4e5', border: '#f0cf9b' }, }[status]; return ( {styles.label} ); } const UNIT_OPTIONS = [ { value: '', label: 'Välj enhet' }, { value: 'g', label: 'g (gram)' }, { value: 'kg', label: 'kg (kilogram)' }, { value: 'hg', label: 'hg (hektogram)' }, { value: 'ml', label: 'ml (milliliter)' }, { value: 'dl', label: 'dl (deciliter)' }, { value: 'l', label: 'l (liter)' }, { value: 'st', label: 'st (styck)' }, { value: 'tsk', label: 'tsk (tesked)' }, { value: 'msk', label: 'msk (matsked)' }, { value: 'port', label: 'port (portioner)' }, { value: 'efter smak', label: 'Efter smak' }, { value: 'förp', label: 'förp (förpackning)' }, { value: 'klyfta', label: 'klyfta' }, ]; // ────────────────────────────────────────────── // Huvud-komponent // ────────────────────────────────────────────── export default function RecipeDetailClient({ recipe: initialRecipe }: { recipe: Recipe }) { const router = useRouter(); const [recipe, setRecipe] = useState(initialRecipe); const [isEditing, setIsEditing] = useState(false); const [isLiked, setIsLiked] = useState(false); const [isSaving, setIsSaving] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [error, setError] = useState(null); // Redigeringsformulär-state const [form, setForm] = useState({ name: initialRecipe.name, description: initialRecipe.description || '', instructions: initialRecipe.instructions || '', imageUrl: initialRecipe.imageUrl || '', ingredients: initialRecipe.ingredients.map((ing) => ({ productId: ing.productId, quantity: String(ing.quantity), unit: ing.unit, note: ing.note || '', })), }); // Produktlista för ingrediens-väljare const [products, setProducts] = useState([]); // Inventarieförhandsgranskning const [preview, setPreview] = useState(null); const [previewError, setPreviewError] = useState(null); const [isPreviewing, startPreviewTransition] = useTransition(); // Bilduppdatering const [imageUrlInput, setImageUrlInput] = useState(''); const [imageError, setImageError] = useState(null); const [isUploadingImage, setIsUploadingImage] = useState(false); // localStorage: gilla useEffect(() => { const liked = localStorage.getItem(`recipe-liked-${recipe.id}`) === 'true'; setIsLiked(liked); }, [recipe.id]); // Ladda produkter för redigera-läge useEffect(() => { if (isEditing && products.length === 0) { fetchJson('/api/products').then(setProducts).catch(console.error); } }, [isEditing, products.length]); // ── Gilla ── const toggleLike = () => { const next = !isLiked; setIsLiked(next); localStorage.setItem(`recipe-liked-${recipe.id}`, String(next)); }; // ── Inventarieförhandsgranskning ── const loadPreview = () => { setPreviewError(null); startPreviewTransition(async () => { try { const res = await fetch(`/api/recipe-preview-proxy?id=${recipe.id}`, { cache: 'no-store' }); if (!res.ok) throw new Error(await parseErrorResponse(res)); setPreview(await res.json()); } catch (err) { setPreviewError(err instanceof Error ? err.message : 'Fel vid hämtning av inventariedata'); } }); }; // ── Ta bort recept ── const handleDelete = async () => { if (!confirm(`Ta bort receptet "${recipe.name}"? Det går inte att ångra.`)) return; setIsDeleting(true); try { const res = await fetch(`/api/recipes/${recipe.id}`, { method: 'DELETE' }); if (!res.ok) throw new Error(await parseErrorResponse(res)); router.push('/recipes'); } catch (err) { setError(err instanceof Error ? err.message : 'Kunde inte ta bort receptet.'); setIsDeleting(false); } }; // ── Spara redigering ── const handleSave = async (e: React.FormEvent) => { e.preventDefault(); setIsSaving(true); setError(null); try { const body = { ...form, ingredients: form.ingredients.map((ing) => ({ ...ing, quantity: Number(ing.quantity), })), }; const res = await fetch(`/api/recipes/${recipe.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(await parseErrorResponse(res)); const updated: Recipe = await res.json(); setRecipe(updated); setIsEditing(false); } catch (err) { setError(err instanceof Error ? err.message : 'Kunde inte spara receptet.'); } finally { setIsSaving(false); } }; // ── Uppdatera bild ── const handleImageUpdate = async () => { if (!imageUrlInput.trim()) return; setIsUploadingImage(true); setImageError(null); try { const res = await fetch(`/api/recipes/${recipe.id}/image`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sourceUrl: imageUrlInput.trim() }), }); if (!res.ok) throw new Error(await parseErrorResponse(res)); const updated: Recipe = await res.json(); setRecipe(updated); setForm((f) => ({ ...f, imageUrl: updated.imageUrl || '' })); setImageUrlInput(''); } catch (err) { setImageError(err instanceof Error ? err.message : 'Kunde inte hämta bilden.'); } finally { setIsUploadingImage(false); } }; // ── Ingrediens-hjälpfunktioner (redigera-läge) ── const setIngredientField = (idx: number, field: string, value: string | number) => { setForm((f) => { const ings = [...f.ingredients]; ings[idx] = { ...ings[idx], [field]: value }; return { ...f, ingredients: ings }; }); }; const addIngredient = () => setForm((f) => ({ ...f, ingredients: [...f.ingredients, { productId: 0, quantity: '', unit: '', note: '' }], })); const removeIngredient = (idx: number) => setForm((f) => { const ings = [...f.ingredients]; ings.splice(idx, 1); return { ...f, ingredients: ings }; }); // ────────────────────────────────────────────── // VY-LÄGE // ────────────────────────────────────────────── if (!isEditing) { return (
{/* Bild */} {recipe.imageUrl ? ( {recipe.name} ) : (
{recipe.name.charAt(0).toUpperCase()}
)} {/* Titel + knappar */}

{recipe.name}

{error &&

{error}

} {recipe.description && (

{recipe.description}

)} {/* Ingredienser */}

Ingredienser

    {recipe.ingredients.map((ing) => (
  • {Number(ing.quantity)} {ing.unit} {ing.product.canonicalName || ing.product.name} {ing.note && ({ing.note})}
  • ))}
{/* Instruktioner */} {recipe.instructions && (

Instruktioner

)} {/* Inventarieförhandsgranskning */} {(preview || previewError) && (

Inventariegranskning

{previewError &&

{previewError}

} {preview && ( <>
Totalt: {preview.summary.totalIngredients} Räcker: {preview.summary.enoughCount} Saknas: {preview.summary.missingCount} {preview.summary.unitMismatchCount > 0 && ( Enhetskonflikt: {preview.summary.unitMismatchCount} )}
    {preview.ingredients.map((ing) => (
  • {ing.productName} {' '} {ing.requiredQuantity} {ing.requiredUnit} {ing.status !== 'enough' && ing.missingQuantity > 0 && ( <> — saknar {ing.missingQuantity} {ing.requiredUnit} )}
  • ))}
)}
)}
); } // ────────────────────────────────────────────── // REDIGERA-LÄGE // ────────────────────────────────────────────── return (

Redigera recept

{error &&

{error}

}
{/* Bild-uppdatering */}

Bild

{recipe.imageUrl && ( )}
setImageUrlInput(e.target.value)} style={inputStyle} />
{imageError &&

{imageError}

}
{/* Grundinfo */}

Receptdetaljer

setForm((f) => ({ ...f, name: e.target.value }))} style={{ ...inputStyle, marginBottom: '1rem' }} />