Files
recipe-app/frontend/app/recipes/[id]/RecipeDetailClient.tsx
T

621 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 (
<div style={{ whiteSpace: 'pre-wrap', lineHeight: 1.7 }}>
{text.split('\n').map((line, i) => {
if (line.startsWith('# ')) return <h3 key={i} style={{ margin: '0.5rem 0 0.25rem', fontSize: '1.3em', fontWeight: 700 }}>{line.slice(2)}</h3>;
if (line.startsWith('## ')) return <h4 key={i} style={{ margin: '0.5rem 0 0.25rem', fontSize: '1.1em', fontWeight: 700 }}>{line.slice(3)}</h4>;
if (line.startsWith('- ') || line.startsWith('* ')) return <div key={i} style={{ marginLeft: '1.5rem' }}> {line.slice(2)}</div>;
const numberedMatch = line.match(/^(\d+)\.\s+(.*)/);
if (numberedMatch) return (
<div key={i} style={{ display: 'flex', gap: '0.6rem', marginBottom: '0.35rem' }}>
<span style={{ fontWeight: 700, minWidth: '1.5rem', textAlign: 'right', flexShrink: 0 }}>{numberedMatch[1]}.</span>
<span>{numberedMatch[2]}</span>
</div>
);
if (line.trim() === '') return <div key={i} style={{ height: '0.5rem' }} />;
return <div key={i}>{line}</div>;
})}
</div>
);
}
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 (
<span style={{
padding: '0.15rem 0.5rem',
fontSize: '0.8rem',
fontWeight: 600,
borderRadius: '4px',
color: styles.color,
background: styles.background,
border: `1px solid ${styles.border}`,
}}>
{styles.label}
</span>
);
}
// ──────────────────────────────────────────────
// 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<string | null>(null);
const [servings, setServings] = useState<number>(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<Product[]>([]);
// Inventarieförhandsgranskning
const [preview, setPreview] = useState<RecipeInventoryPreview | null>(null);
const [previewError, setPreviewError] = useState<string | null>(null);
const [isPreviewing, startPreviewTransition] = useTransition();
// Bilduppdatering
const [imageUrlInput, setImageUrlInput] = useState('');
const [imageError, setImageError] = useState<string | null>(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<Product[]>('/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 (
<div>
{/* Bild */}
{recipe.imageUrl ? (
<img
src={recipe.imageUrl}
alt={recipe.name}
style={{ width: '100%', maxHeight: '400px', objectFit: 'cover', borderRadius: '8px', marginBottom: '1.25rem', display: 'block' }}
/>
) : (
<div style={{
width: '100%', height: '200px', background: '#e9ecef', borderRadius: '8px',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: '4rem', fontWeight: 700, color: '#868e96', marginBottom: '1.25rem',
}}>
{recipe.name.charAt(0).toUpperCase()}
</div>
)}
{/* Titel + knappar */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexWrap: 'wrap', gap: '0.75rem', marginBottom: '1rem' }}>
<h1 style={{ margin: 0, fontSize: '1.75rem' }}>{recipe.name}</h1>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
<button onClick={toggleLike} style={btnStyle(isLiked ? '#e53e3e' : undefined)}>
{isLiked ? '♥ Gillad' : '♡ Gilla'}
</button>
<button onClick={loadPreview} disabled={isPreviewing} style={btnStyle()}>
{isPreviewing ? 'Hämtar...' : '🛒 Vad behöver jag köpa?'}
</button>
<button onClick={() => setIsEditing(true)} style={btnStyle()}>Redigera</button>
<button onClick={handleDelete} disabled={isDeleting} style={btnStyle('#e53e3e')}>
{isDeleting ? 'Tar bort...' : 'Ta bort'}
</button>
</div>
</div>
{error && <p style={errorStyle}>{error}</p>}
{recipe.description && (
<p style={{ color: '#555', marginBottom: '1.25rem', fontSize: '1.05rem' }}>{recipe.description}</p>
)}
{/* Ingredienser */}
<section style={sectionStyle}>
<h2 style={sectionTitle}>Ingredienser</h2>
{recipe.servings && (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '1rem', fontSize: '0.9rem', flexWrap: 'wrap' }}>
<span style={{ color: '#555' }}>Portioner:</span>
<button type="button" onClick={() => setServings((s) => Math.max(1, s - 1))} style={btnStyle()}></button>
<span style={{ fontWeight: 700, minWidth: '2rem', textAlign: 'center' }}>{servings}</span>
<button type="button" onClick={() => setServings((s) => s + 1)} style={btnStyle()}>+</button>
{servings !== recipe.servings && (
<button type="button" onClick={() => setServings(recipe.servings!)} style={{ ...btnStyle(), fontSize: '0.8rem', padding: '0.3rem 0.6rem' }}>
Återställ ({recipe.servings})
</button>
)}
</div>
)}
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'grid', gap: '0.4rem' }}>
{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 (
<li key={ing.id} style={{ display: 'flex', gap: '0.5rem', alignItems: 'baseline' }}>
<span style={{ fontWeight: 600, minWidth: '60px', textAlign: 'right' }}>
{displayQty} {ing.unit}
</span>
<span>{ing.product.canonicalName || ing.product.name}</span>
{ing.note && <span style={{ color: '#868e96', fontSize: '0.875rem' }}>({ing.note})</span>}
</li>
);
})}
</ul>
</section>
{/* Instruktioner */}
{recipe.instructions && (
<section style={sectionStyle}>
<h2 style={sectionTitle}>Instruktioner</h2>
<SimpleMarkdownPreview text={recipe.instructions} />
</section>
)}
{/* Lagergranskning */}
{(preview || previewError) && (
<section style={{ ...sectionStyle, marginTop: '1.5rem' }}>
<h2 style={sectionTitle}>🛒 Vad behöver jag köpa?</h2>
{previewError && <p style={errorStyle}>{previewError}</p>}
{preview && (
<>
{/* Summerbanner */}
{preview.summary.canCookExactly ? (
<div style={{ background: '#f0fdf4', border: '1px solid #86efac', borderRadius: '8px', padding: '0.875rem 1rem', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.6rem', color: '#166534', fontWeight: 600 }}>
<span style={{ fontSize: '1.3rem' }}></span>
<span>Du har allt hemma! Du kan laga det här receptet direkt.</span>
</div>
) : (
<div style={{ background: '#fef2f2', border: '1px solid #fca5a5', borderRadius: '8px', padding: '0.875rem 1rem', marginBottom: '1rem', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '0.75rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.6rem', color: '#991b1b', fontWeight: 600 }}>
<span style={{ fontSize: '1.3rem' }}>🛒</span>
<span>
Du saknar {preview.summary.missingCount + preview.summary.unitMismatchCount} av {preview.summary.totalIngredients} ingredienser
</span>
</div>
<button
disabled
title="Inköpslista kommer snart"
style={{ padding: '0.45rem 1rem', background: '#e5e7eb', color: '#9ca3af', border: '1px solid #d1d5db', borderRadius: '6px', cursor: 'not-allowed', fontSize: '0.9rem', fontWeight: 500 }}
>
+ Lägg till i inköpslista (kommer snart)
</button>
</div>
)}
{/* Ingredienslista med status */}
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'grid', gap: '0.4rem' }}>
{preview.ingredients.map((ing) => {
const rowBg = ing.status === 'enough' ? '#f0fdf4' : ing.status === 'missing' ? '#fef2f2' : '#fffbeb';
const rowBorder = ing.status === 'enough' ? '#bbf7d0' : ing.status === 'missing' ? '#fca5a5' : '#fde68a';
const icon = ing.status === 'enough' ? '✅' : ing.status === 'missing' ? '❌' : '⚠️';
return (
<li key={ing.ingredientId} style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '0.5rem 0.75rem', borderRadius: '6px',
border: `1px solid ${rowBorder}`, background: rowBg,
flexWrap: 'wrap', gap: '0.5rem',
}}>
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span>{icon}</span>
<strong>{ing.productName}</strong>
<span style={{ color: '#555', fontSize: '0.875rem' }}>
{ing.requiredQuantity} {ing.requiredUnit}
{ing.status !== 'enough' && ing.missingQuantity > 0 && (
<> saknar <strong>{ing.missingQuantity} {ing.requiredUnit}</strong></>
)}
</span>
</span>
<StatusBadge status={ing.status} />
</li>
);
})}
</ul>
</>
)}
</section>
)}
{/* 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 (
<section style={{ ...sectionStyle, marginTop: '1.5rem' }}>
<h2 style={sectionTitle}>Näringsvärden ({portionLabel})</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(110px, 1fr))', gap: '0.75rem' }}>
{[
{ 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 }) => (
<div key={label} style={{ padding: '0.75rem', background: '#f8f9fa', borderRadius: '6px', textAlign: 'center' }}>
<div style={{ fontSize: '1.2rem', fontWeight: 700 }}>
{value < 1 ? value.toFixed(1) : Math.round(value)}{unit}
</div>
<div style={{ fontSize: '0.8rem', color: '#666' }}>{label}</div>
</div>
))}
</div>
<p style={{ fontSize: '0.8rem', color: '#888', marginTop: '0.5rem', marginBottom: 0 }}>
* Endast ingredienser med viktenhet (g/kg) och registrerade näringsvärden inkluderas.
</p>
</section>
);
})()}
</div>
);
}
// ──────────────────────────────────────────────
// REDIGERA-LÄGE
// ──────────────────────────────────────────────
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
<h1 style={{ margin: 0 }}>Redigera recept</h1>
<button onClick={() => setIsEditing(false)} style={btnStyle()}>Avbryt</button>
</div>
{error && <p style={errorStyle}>{error}</p>}
<form onSubmit={handleSave} style={{ display: 'grid', gap: '1.25rem' }}>
{/* Bild-uppdatering */}
<section style={sectionStyle}>
<h2 style={sectionTitle}>Bild</h2>
{recipe.imageUrl && (
<img src={recipe.imageUrl} alt="" style={{ width: '100%', maxHeight: '200px', objectFit: 'cover', borderRadius: '6px', marginBottom: '0.75rem' }} />
)}
<div style={{ display: 'flex', gap: '0.5rem' }}>
<input
type="url"
placeholder="https://... (bild-URL)"
value={imageUrlInput}
onChange={(e) => setImageUrlInput(e.target.value)}
style={inputStyle}
/>
<button type="button" onClick={handleImageUpdate} disabled={isUploadingImage || !imageUrlInput.trim()} style={btnStyle()}>
{isUploadingImage ? 'Hämtar...' : 'Uppdatera bild'}
</button>
</div>
{imageError && <p style={{ ...errorStyle, marginTop: '0.5rem' }}>{imageError}</p>}
</section>
{/* Grundinfo */}
<section style={sectionStyle}>
<h2 style={sectionTitle}>Receptdetaljer</h2>
<label style={labelStyle}>Receptnamn *</label>
<input type="text" required value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} style={{ ...inputStyle, marginBottom: '1rem' }} />
<label style={labelStyle}>Beskrivning</label>
<textarea value={form.description} onChange={(e) => setForm((f) => ({ ...f, description: e.target.value }))} rows={3} style={{ ...inputStyle, fontFamily: 'inherit', resize: 'vertical', marginBottom: '1rem' }} />
<label style={labelStyle}>Instruktioner</label>
<textarea value={form.instructions} onChange={(e) => setForm((f) => ({ ...f, instructions: e.target.value }))} rows={8} style={{ ...inputStyle, fontFamily: 'inherit', resize: 'vertical' }} />
</section>
{/* Portioner */}
<section style={sectionStyle}>
<h2 style={sectionTitle}>Portioner</h2>
<input
type="number"
min={1}
step={1}
value={form.servings ?? ''}
onChange={(e) => setForm((f) => ({ ...f, servings: e.target.value ? Number(e.target.value) : null }))}
style={{ ...inputStyle, width: '120px' }}
placeholder="t.ex. 4"
/>
<p style={{ fontSize: '0.85rem', color: '#666', marginTop: '0.4rem', marginBottom: 0 }}>
Anges portioner kan mängderna skalas receptsidan.
</p>
</section>
{/* Synlighet */}
<section style={sectionStyle}>
<h2 style={sectionTitle}>Synlighet</h2>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer' }}>
<input
type="checkbox"
checked={form.isPublic}
onChange={(e) => setForm((f) => ({ ...f, isPublic: e.target.checked }))}
style={{ width: 16, height: 16 }}
/>
<span>Publikt recept (synligt för alla inloggade)</span>
</label>
<p style={{ fontSize: '0.85rem', color: '#666', marginTop: '0.4rem', marginBottom: 0 }}>
Privata recept syns bara för dig och de du delar med.
</p>
</section>
{/* Ingredienser */}
<section style={sectionStyle}>
<h2 style={sectionTitle}>Ingredienser</h2>
{form.ingredients.map((ing, idx) => (
<div key={idx} style={{ display: 'grid', gridTemplateColumns: '1fr auto auto auto auto', gap: '0.5rem', marginBottom: '0.75rem', alignItems: 'center' }}>
<select
value={ing.productId}
onChange={(e) => setIngredientField(idx, 'productId', Number(e.target.value))}
style={inputStyle}
>
<option value={0}>Välj produkt...</option>
{products.map((p) => (
<option key={p.id} value={p.id}>{p.canonicalName || p.name}</option>
))}
</select>
<input
type="number"
placeholder="Mängd"
value={ing.quantity}
min={0}
step="any"
onChange={(e) => setIngredientField(idx, 'quantity', e.target.value)}
style={{ ...inputStyle, width: '80px' }}
/>
<select
value={ing.unit}
onChange={(e) => setIngredientField(idx, 'unit', e.target.value)}
style={{ ...inputStyle, width: '120px' }}
>
{UNIT_OPTIONS.map((u) => <option key={u.value} value={u.value}>{u.label}</option>)}
</select>
<input
type="text"
placeholder="Not (valfri)"
value={ing.note}
onChange={(e) => setIngredientField(idx, 'note', e.target.value)}
style={{ ...inputStyle, width: '110px' }}
/>
<button type="button" onClick={() => removeIngredient(idx)} style={{ ...btnStyle('#e53e3e'), whiteSpace: 'nowrap' }}>Ta bort</button>
</div>
))}
<button type="button" onClick={addIngredient} style={btnStyle()}>+ Lägg till ingrediens</button>
</section>
<div style={{ display: 'flex', gap: '0.75rem' }}>
<button type="submit" disabled={isSaving} style={btnStyle('#0070f3')}>
{isSaving ? 'Sparar...' : 'Spara ändringar'}
</button>
<button type="button" onClick={() => setIsEditing(false)} style={btnStyle()}>Avbryt</button>
</div>
</form>
</div>
);
}
// ──────────────────────────────────────────────
// Stilkonstanter
// ──────────────────────────────────────────────
function btnStyle(bg?: string): React.CSSProperties {
return {
padding: '0.45rem 0.9rem',
background: bg || '#f0f0f0',
color: bg ? '#fff' : '#333',
border: '1px solid ' + (bg || '#ccc'),
borderRadius: '6px',
cursor: 'pointer',
fontSize: '0.9rem',
fontWeight: 500,
whiteSpace: 'nowrap',
};
}
const sectionStyle: React.CSSProperties = {
border: '1px solid #dee2e6',
borderRadius: '8px',
padding: '1rem',
};
const sectionTitle: React.CSSProperties = {
margin: '0 0 0.75rem',
fontSize: '1.1rem',
fontWeight: 700,
};
const inputStyle: React.CSSProperties = {
width: '100%',
padding: '0.6rem 0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '0.95rem',
boxSizing: 'border-box',
};
const labelStyle: React.CSSProperties = {
display: 'block',
fontWeight: 600,
marginBottom: '0.35rem',
};
const errorStyle: React.CSSProperties = {
color: 'crimson',
background: '#ffe5e5',
padding: '0.6rem 0.75rem',
borderRadius: '6px',
margin: 0,
};