feat: add TypeScript definitions for next-auth session with accessToken and user details
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-04 20:09:21 +02:00
parent afd2607000
commit ffe50e5151
135 changed files with 5 additions and 38 deletions
@@ -0,0 +1,181 @@
'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>
);
}