182 lines
6.2 KiB
TypeScript
182 lines
6.2 KiB
TypeScript
'use client';
|
||
|
||
import { useState } from 'react';
|
||
import Link from 'next/link';
|
||
import type { Recipe } from '../../features/inventory/types';
|
||
|
||
function RecipePlaceholder({ name }: { name: string }) {
|
||
const initial = name.trim().charAt(0).toUpperCase() || '?';
|
||
return (
|
||
<div
|
||
style={{
|
||
width: '100%',
|
||
height: '160px',
|
||
background: '#e9ecef',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
fontSize: '2.5rem',
|
||
fontWeight: 700,
|
||
color: '#868e96',
|
||
borderRadius: '8px 8px 0 0',
|
||
userSelect: 'none',
|
||
}}
|
||
>
|
||
{initial}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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()))
|
||
.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={{ 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={{
|
||
flex: '1 1 200px',
|
||
padding: '0.6rem 1rem',
|
||
fontSize: '1rem',
|
||
border: '1px solid #ced4da',
|
||
borderRadius: '24px',
|
||
outline: 'none',
|
||
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 || onlyWithImage ? 'Inga recept matchar filtren.' : 'Inga recept tillagda ännu.'}
|
||
</p>
|
||
)}
|
||
|
||
<div
|
||
style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))',
|
||
gap: '1rem',
|
||
}}
|
||
>
|
||
{filtered.map((recipe) => (
|
||
<Link
|
||
key={recipe.id}
|
||
href={`/recipes/${recipe.id}`}
|
||
style={{ textDecoration: 'none', color: 'inherit' }}
|
||
>
|
||
<div
|
||
style={{
|
||
border: '1px solid #dee2e6',
|
||
borderRadius: '8px',
|
||
overflow: 'hidden',
|
||
transition: 'box-shadow 0.15s',
|
||
background: '#fff',
|
||
cursor: 'pointer',
|
||
}}
|
||
onMouseEnter={(e) =>
|
||
((e.currentTarget as HTMLDivElement).style.boxShadow = '0 4px 12px rgba(0,0,0,0.12)')
|
||
}
|
||
onMouseLeave={(e) =>
|
||
((e.currentTarget as HTMLDivElement).style.boxShadow = 'none')
|
||
}
|
||
>
|
||
{recipe.imageUrl ? (
|
||
<img
|
||
src={recipe.imageUrl}
|
||
alt={recipe.name}
|
||
style={{
|
||
width: '100%',
|
||
height: '160px',
|
||
objectFit: 'cover',
|
||
display: 'block',
|
||
}}
|
||
/>
|
||
) : (
|
||
<RecipePlaceholder name={recipe.name} />
|
||
)}
|
||
<div style={{ padding: '0.75rem 1rem 0.85rem' }}>
|
||
<h3
|
||
style={{
|
||
margin: 0,
|
||
fontSize: '1rem',
|
||
fontWeight: 600,
|
||
whiteSpace: 'nowrap',
|
||
overflow: 'hidden',
|
||
textOverflow: 'ellipsis',
|
||
}}
|
||
>
|
||
{recipe.name}
|
||
</h3>
|
||
{recipe.description && (
|
||
<p
|
||
style={{
|
||
margin: '0.25rem 0 0.5rem',
|
||
fontSize: '0.85rem',
|
||
color: '#868e96',
|
||
overflow: 'hidden',
|
||
display: '-webkit-box',
|
||
WebkitLineClamp: 2,
|
||
WebkitBoxOrient: 'vertical',
|
||
} as React.CSSProperties}
|
||
>
|
||
{recipe.description}
|
||
</p>
|
||
)}
|
||
<div style={{ display: 'flex', gap: '0.75rem', marginTop: recipe.description ? 0 : '0.4rem', fontSize: '0.78rem', color: '#adb5bd' }}>
|
||
{recipe.ingredients?.length > 0 && (
|
||
<span>{recipe.ingredients.length} ingredienser</span>
|
||
)}
|
||
<span>{new Date(recipe.createdAt).toLocaleDateString('sv-SE')}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|