'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'; import { UNIT_OPTIONS } from '../../../lib/units'; // ────────────────────────────────────────────── // 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)}
; const numberedMatch = line.match(/^(\d+)\.\s+(.*)/); if (numberedMatch) return (
{numberedMatch[1]}. {numberedMatch[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} ); } // ────────────────────────────────────────────── // 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); const [servings, setServings] = useState(initialRecipe.servings ?? 1); // Redigeringsformulär-state const [form, setForm] = useState({ name: initialRecipe.name, description: initialRecipe.description || '', instructions: initialRecipe.instructions || '', imageUrl: initialRecipe.imageUrl || '', servings: initialRecipe.servings as number | null, isPublic: initialRecipe.isPublic, 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.servings && (
Portioner: {servings} {servings !== recipe.servings && ( )}
)}
    {recipe.ingredients.map((ing) => { const scale = recipe.servings ? servings / recipe.servings : 1; const qty = Number(ing.quantity) * scale; const displayQty = qty % 1 === 0 ? qty : parseFloat(qty.toFixed(2)); return (
  • {displayQty} {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} )}
  • ))}
)}
)} {/* Näringsvärden */} {(() => { const scale = recipe.servings ? servings / recipe.servings : 1; const totals = recipe.ingredients.reduce( (acc, ing) => { const n = ing.product.nutrition; if (!n) return acc; const qty = Number(ing.quantity) * scale; const qtyInGrams = ing.unit === 'g' ? qty : ing.unit === 'kg' ? qty * 1000 : null; if (qtyInGrams === null) return acc; const f = qtyInGrams / 100; return { calories: acc.calories + (n.calories ?? 0) * f, protein: acc.protein + (n.protein ?? 0) * f, fat: acc.fat + (n.fat ?? 0) * f, carbohydrates: acc.carbohydrates + (n.carbohydrates ?? 0) * f, }; }, { calories: 0, protein: 0, fat: 0, carbohydrates: 0 }, ); const hasAny = recipe.ingredients.some( (i) => i.product.nutrition && (i.unit === 'g' || i.unit === 'kg'), ); if (!hasAny) return null; const portionLabel = recipe.servings ? `${servings} ${servings === 1 ? 'portion' : 'portioner'}` : 'hela receptet'; return (

Näringsvärden ({portionLabel})

{[ { label: 'Energi', value: totals.calories, unit: 'kcal' }, { label: 'Protein', value: totals.protein, unit: 'g' }, { label: 'Fett', value: totals.fat, unit: 'g' }, { label: 'Kolhydrater', value: totals.carbohydrates, unit: 'g' }, ].map(({ label, value, unit }) => (
{value < 1 ? value.toFixed(1) : Math.round(value)}{unit}
{label}
))}

* Endast ingredienser med viktenhet (g/kg) och registrerade näringsvärden inkluderas.

); })()}
); } // ────────────────────────────────────────────── // 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' }} />