Files
recipe-app/frontend/app/recipes/RecipePreview.tsx
T
Nils-Johan Gynther d1870decac 11D recipe preview
2026-04-09 15:12:54 +02:00

251 lines
8.2 KiB
TypeScript

'use client';
import { useState, useTransition } from 'react';
import type {
Recipe,
RecipeInventoryPreview,
} from '../../features/inventory/types';
type Props = {
recipes: Recipe[];
};
function getStatusStyle(status: 'enough' | 'missing' | 'unit_mismatch') {
if (status === 'enough') {
return {
label: 'Räcker',
color: '#1f5f2c',
background: '#ecf8ee',
border: '#b9e0bf',
};
}
if (status === 'missing') {
return {
label: 'Saknas',
color: '#8b0000',
background: '#ffeaea',
border: '#f1b5b5',
};
}
return {
label: 'Enhetskonflikt',
color: '#8a4b00',
background: '#fff4e5',
border: '#f0cf9b',
};
}
function formatDate(value: string | null) {
if (!value) return null;
return new Date(value).toLocaleDateString('sv-SE');
}
export default function RecipePreview({ recipes }: Props) {
const [selectedRecipeId, setSelectedRecipeId] = useState('');
const [preview, setPreview] = useState<RecipeInventoryPreview | null>(null);
const [error, setError] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
const loadPreview = () => {
setError(null);
setPreview(null);
if (!selectedRecipeId) {
setError('Välj ett recept.');
return;
}
startTransition(async () => {
try {
const res = await fetch(`/api/recipe-preview-proxy?id=${selectedRecipeId}`, {
method: 'GET',
cache: 'no-store',
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || 'Kunde inte hämta recept-preview.');
}
const data: RecipeInventoryPreview = await res.json();
setPreview(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Okänt fel');
}
});
};
return (
<section style={{ display: 'grid', gap: '1rem' }}>
<div
style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '1rem',
display: 'grid',
gap: '0.75rem',
}}
>
<h2 style={{ margin: 0 }}>Recept mot hemmavaror</h2>
<label>
Recept
<br />
<select
value={selectedRecipeId}
onChange={(e) => setSelectedRecipeId(e.target.value)}
style={{ width: '100%', padding: '0.5rem' }}
>
<option value="">Välj recept</option>
{recipes.map((recipe) => (
<option key={recipe.id} value={recipe.id}>
{recipe.name}
</option>
))}
</select>
</label>
<div>
<button
type="button"
onClick={loadPreview}
disabled={isPending}
style={{ padding: '0.6rem 1rem' }}
>
{isPending ? 'Hämtar preview...' : 'Visa preview'}
</button>
</div>
{error ? <p style={{ color: 'crimson', margin: 0 }}>{error}</p> : null}
</div>
{preview ? (
<section style={{ display: 'grid', gap: '1rem' }}>
<article
style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '1rem',
display: 'grid',
gap: '0.5rem',
}}
>
<h3 style={{ margin: 0 }}>{preview.recipe.name}</h3>
{preview.recipe.description ? <div>{preview.recipe.description}</div> : null}
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
<span>Ingredienser: {preview.summary.totalIngredients}</span>
<span>Räcker: {preview.summary.enoughCount}</span>
<span>Saknas: {preview.summary.missingCount}</span>
<span>Enhetskonflikter: {preview.summary.unitMismatchCount}</span>
<strong>
{preview.summary.canCookExactly
? 'Kan lagas exakt'
: 'Kan inte lagas exakt ännu'}
</strong>
</div>
</article>
<div style={{ display: 'grid', gap: '0.75rem' }}>
{preview.ingredients.map((ingredient) => {
const statusStyle = getStatusStyle(ingredient.status);
return (
<article
key={ingredient.ingredientId}
style={{
border: `1px solid ${statusStyle.border}`,
borderRadius: '8px',
padding: '1rem',
display: 'grid',
gap: '0.75rem',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: '1rem',
flexWrap: 'wrap',
}}
>
<div>
<strong>{ingredient.productName}</strong>
<div>
Krävs: {ingredient.requiredQuantity} {ingredient.requiredUnit}
</div>
{ingredient.note ? <div>Notering: {ingredient.note}</div> : null}
</div>
<div
style={{
padding: '0.3rem 0.6rem',
borderRadius: '999px',
background: statusStyle.background,
color: statusStyle.color,
border: `1px solid ${statusStyle.border}`,
fontSize: '0.85rem',
fontWeight: 600,
whiteSpace: 'nowrap',
}}
>
{statusStyle.label}
</div>
</div>
<div style={{ display: 'grid', gap: '0.35rem' }}>
<div>
Tillgängligt i jämförbar enhet: {ingredient.availableQuantity}{' '}
{ingredient.availableUnit || ''}
</div>
{ingredient.status === 'missing' ? (
<div>
Saknas: {ingredient.missingQuantity} {ingredient.requiredUnit}
</div>
) : null}
</div>
{ingredient.matchingInventoryItems.length > 0 ? (
<div style={{ display: 'grid', gap: '0.35rem' }}>
<strong>Matchande inventory</strong>
{ingredient.matchingInventoryItems.map((item) => (
<div key={item.id}>
#{item.id}: {item.quantity} {item.unit}
{item.brand ? `, ${item.brand}` : ''}
{item.location ? `, ${item.location}` : ''}
{item.bestBeforeDate
? `, bäst före ${formatDate(item.bestBeforeDate)}`
: ''}
</div>
))}
</div>
) : null}
{ingredient.otherInventoryItems.length > 0 ? (
<div style={{ display: 'grid', gap: '0.35rem' }}>
<strong>Andra inventory-poster med annan enhet</strong>
{ingredient.otherInventoryItems.map((item) => (
<div key={item.id}>
#{item.id}: {item.quantity} {item.unit}
{item.brand ? `, ${item.brand}` : ''}
{item.location ? `, ${item.location}` : ''}
{item.bestBeforeDate
? `, bäst före ${formatDate(item.bestBeforeDate)}`
: ''}
</div>
))}
</div>
) : null}
</article>
);
})}
</div>
</section>
) : null}
</section>
);
}