Files
recipe-app/_archive/frontend/app/recipes/RecipeGrid.tsx
T
Nils-Johan Gynther ffe50e5151
Test Suite / test (24.15.0) (push) Has been cancelled
feat: add TypeScript definitions for next-auth session with accessToken and user details
2026-05-04 20:09:21 +02:00

182 lines
6.2 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 } 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>
);
}