feat: add image handling to recipes
- Implemented image downloading and optimization in QuickImportService. - Added imageUrl field to CreateRecipeDto for recipe creation. - Created an endpoint in RecipesController to update recipe images. - Enhanced RecipesService to handle image URL updates and optimizations. - Updated Docker Compose to mount a volume for recipe images. - Refactored frontend to display images in recipe grids and detail views. - Added a new utility function for downloading and optimizing images. - Created a new API route for handling image uploads. - Introduced RecipeGrid component for better recipe display. - Updated RecipeDetailClient to manage image updates and display. - Added migration for new imageUrl column in the Recipe table.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
'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 filtered = recipes.filter((r) =>
|
||||
r.name.toLowerCase().includes(search.toLowerCase()),
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '1.25rem' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Sök efter recept..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.6rem 1rem',
|
||||
fontSize: '1rem',
|
||||
border: '1px solid #ced4da',
|
||||
borderRadius: '24px',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<p style={{ color: '#868e96', textAlign: 'center', marginTop: '2rem' }}>
|
||||
{search ? 'Inga recept matchar sökningen.' : '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' }}>
|
||||
<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',
|
||||
fontSize: '0.85rem',
|
||||
color: '#868e96',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
{recipe.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user