feat: add tests for normalizeName and RecipesService methods, including unit conversion and alias normalization

This commit is contained in:
Nils-Johan Gynther
2026-04-16 19:22:14 +02:00
parent 1b9df4d20d
commit 9292e30abc
6 changed files with 305 additions and 9 deletions
+40 -6
View File
@@ -29,21 +29,30 @@ function RecipePlaceholder({ name }: { name: string }) {
export default function RecipeGrid({ recipes }: { recipes: Recipe[] }) {
const [search, setSearch] = useState('');
const [sort, setSort] = useState<'name' | 'newest' | 'oldest' | 'ingredients'>('newest');
const [onlyWithImage, setOnlyWithImage] = useState(false);
const filtered = recipes.filter((r) =>
r.name.toLowerCase().includes(search.toLowerCase()),
);
const filtered = recipes
.filter((r) => r.name.toLowerCase().includes(search.toLowerCase()))
.filter((r) => !onlyWithImage || !!r.imageUrl)
.sort((a, b) => {
if (sort === 'name') return a.name.localeCompare(b.name, 'sv');
if (sort === 'newest') return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
if (sort === 'oldest') return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
if (sort === 'ingredients') return (b.ingredients?.length ?? 0) - (a.ingredients?.length ?? 0);
return 0;
});
return (
<div>
<div style={{ marginBottom: '1.25rem' }}>
<div style={{ display: 'flex', gap: '0.75rem', marginBottom: '1rem', flexWrap: 'wrap', alignItems: 'center' }}>
<input
type="text"
placeholder="Sök efter recept..."
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{
width: '100%',
flex: '1 1 200px',
padding: '0.6rem 1rem',
fontSize: '1rem',
border: '1px solid #ced4da',
@@ -52,11 +61,36 @@ export default function RecipeGrid({ recipes }: { recipes: Recipe[] }) {
boxSizing: 'border-box',
}}
/>
<select
value={sort}
onChange={(e) => setSort(e.target.value as typeof sort)}
style={{
padding: '0.55rem 0.75rem',
fontSize: '0.9rem',
border: '1px solid #ced4da',
borderRadius: '8px',
background: '#fff',
cursor: 'pointer',
}}
>
<option value="newest">Senast tillagda</option>
<option value="oldest">Äldst först</option>
<option value="name">Namn (AÖ)</option>
<option value="ingredients">Flest ingredienser</option>
</select>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', fontSize: '0.9rem', cursor: 'pointer', userSelect: 'none' }}>
<input
type="checkbox"
checked={onlyWithImage}
onChange={(e) => setOnlyWithImage(e.target.checked)}
/>
Endast med bild
</label>
</div>
{filtered.length === 0 && (
<p style={{ color: '#868e96', textAlign: 'center', marginTop: '2rem' }}>
{search ? 'Inga recept matchar sökningen.' : 'Inga recept tillagda ännu.'}
{search || onlyWithImage ? 'Inga recept matchar filtren.' : 'Inga recept tillagda ännu.'}
</p>
)}
@@ -22,6 +22,13 @@ function SimpleMarkdownPreview({ text }: { text: string }) {
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>;
})}