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:
Nils-Johan Gynther
2026-04-15 19:46:50 +02:00
parent a2038ffbec
commit 73bf5193c4
20 changed files with 933 additions and 49 deletions
+31
View File
@@ -3,6 +3,9 @@ import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { CreateRecipeDto } from './dto/create-recipe.dto';
import { ParseMarkdownDto } from './dto/parse-markdown.dto';
import { downloadAndOptimizeImage } from '../common/utils/download-image';
const IMAGE_DEST_DIR = process.env.IMAGE_DEST_DIR || '/app/recipe-images';
// Lokala typdefiniitioner (tidigare från recipe-document-converter)
interface ParsedIngredient {
@@ -340,6 +343,7 @@ export class RecipesService {
name: updateRecipeDto.name,
description: updateRecipeDto.description || null,
instructions: updateRecipeDto.instructions || null,
...(updateRecipeDto.imageUrl !== undefined && { imageUrl: updateRecipeDto.imageUrl || null }),
ingredients: {
create: updateRecipeDto.ingredients.map((ingredient) => ({
productId: ingredient.productId,
@@ -374,12 +378,39 @@ export class RecipesService {
await this.prisma.recipe.delete({ where: { id } });
}
async updateImage(id: number, sourceUrl: string) {
const existingRecipe = await this.prisma.recipe.findUnique({ where: { id } });
if (!existingRecipe) {
throw new NotFoundException(`Recipe with id ${id} not found`);
}
const imageUrl = await downloadAndOptimizeImage(sourceUrl, IMAGE_DEST_DIR);
return this.prisma.recipe.update({
where: { id },
data: { imageUrl },
include: { ingredients: { include: { product: true } } },
});
}
async create(createRecipeDto: CreateRecipeDto) {
// Om imageUrl är en extern URL — ladda ner och optimera
let imageUrl: string | null = createRecipeDto.imageUrl || null;
if (imageUrl && imageUrl.startsWith('http')) {
try {
imageUrl = await downloadAndOptimizeImage(imageUrl, IMAGE_DEST_DIR);
} catch (err) {
console.warn('[RecipesService] Kunde inte ladda ner receptbild:', err);
imageUrl = null;
}
}
const recipe = await this.prisma.recipe.create({
data: {
name: createRecipeDto.name,
description: createRecipeDto.description || null,
instructions: createRecipeDto.instructions || null,
imageUrl,
ingredients: {
create: createRecipeDto.ingredients.map((ingredient) => ({
productId: ingredient.productId,