From 898ac2ef19fd1dac6ea1fc9a69f779c9da625f8f Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Thu, 9 Apr 2026 22:53:52 +0200 Subject: [PATCH] Add CreateRecipePage component for recipe creation with ingredients. Updated UX --- backend/src/recipes/dto/create-recipe.dto.ts | 20 +- backend/src/recipes/recipes.controller.ts | 6 +- frontend/app/admin/products/page.tsx | 20 +- frontend/app/inventory/page.tsx | 32 +-- frontend/app/page.tsx | 36 +++- .../app/recipes/create/CreateRecipePage.tsx | 182 ++++++++++++++++++ frontend/app/recipes/page.tsx | 20 +- 7 files changed, 293 insertions(+), 23 deletions(-) create mode 100644 frontend/app/recipes/create/CreateRecipePage.tsx diff --git a/backend/src/recipes/dto/create-recipe.dto.ts b/backend/src/recipes/dto/create-recipe.dto.ts index 3757b9eb..e81f9509 100644 --- a/backend/src/recipes/dto/create-recipe.dto.ts +++ b/backend/src/recipes/dto/create-recipe.dto.ts @@ -4,9 +4,27 @@ import { IsString, ValidateNested, ArrayMinSize, + IsInt, + IsNumber, + Min, } from 'class-validator'; import { Type } from 'class-transformer'; -import { CreateRecipeIngredientDto } from './create-recipe-ingredient.dto'; + +class CreateRecipeIngredientDto { + @IsInt() + productId!: number; + + @IsNumber() + @Min(0.01) + quantity!: number; + + @IsString() + unit!: string; + + @IsOptional() + @IsString() + note?: string; +} export class CreateRecipeDto { @IsString() diff --git a/backend/src/recipes/recipes.controller.ts b/backend/src/recipes/recipes.controller.ts index 0c3dc6e0..b951e56c 100644 --- a/backend/src/recipes/recipes.controller.ts +++ b/backend/src/recipes/recipes.controller.ts @@ -1,6 +1,6 @@ import { Body, Controller, Get, Param, ParseIntPipe, Post } from '@nestjs/common'; -import { CreateRecipeDto } from './dto/create-recipe.dto'; import { RecipesService } from './recipes.service'; +import { CreateRecipeDto } from './dto/create-recipe.dto'; @Controller('recipes') export class RecipesController { @@ -22,7 +22,7 @@ export class RecipesController { } @Post() - create(@Body() body: CreateRecipeDto) { - return this.recipesService.create(body); + async create(@Body() createRecipeDto: CreateRecipeDto) { + return this.recipesService.create(createRecipeDto); } } \ No newline at end of file diff --git a/frontend/app/admin/products/page.tsx b/frontend/app/admin/products/page.tsx index 0a1e19fa..77d1a7d1 100644 --- a/frontend/app/admin/products/page.tsx +++ b/frontend/app/admin/products/page.tsx @@ -2,13 +2,31 @@ import { fetchJson } from '../../../lib/api'; import type { Product } from '../../../features/inventory/types'; import CanonicalNameForm from './CanonicalNameForm'; import MergePreviewForm from './MergePreviewForm'; +import Link from 'next/link'; export default async function AdminProductsPage() { const products = await fetchJson('/api/products'); return (
-

Admin: Produkter

+
+

Admin: Produkter

+ + Lägg till nytt recept + +

Här kan du granska och standardisera produktnamn.

diff --git a/frontend/app/inventory/page.tsx b/frontend/app/inventory/page.tsx index fb4a9b20..55a61fc5 100644 --- a/frontend/app/inventory/page.tsx +++ b/frontend/app/inventory/page.tsx @@ -86,15 +86,8 @@ export default async function InventoryPage({ searchParams }: InventoryPageProps const inventoryPath = (() => { const params = new URLSearchParams(); - - if (location) { - params.set('location', location); - } - - if (sort) { - params.set('sort', sort); - } - + if (location) params.set('location', location); + if (sort) params.set('sort', sort); const query = params.toString(); return query ? `/api/inventory?${query}` : '/api/inventory'; })(); @@ -112,8 +105,25 @@ export default async function InventoryPage({ searchParams }: InventoryPageProps ]; return ( -
-

Hemmavaror

+
+
+

Varor hemma

+ + Lägg till nytt recept + +
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index a0f0c5b9..a823593f 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -2,12 +2,36 @@ import Link from 'next/link'; export default function HomePage() { return ( -
-

Recipe App

-

Det här är första riktiga grunden för projektet.

-

Gå till varor som finns hemma

-

Gå till produktadmin

-

Gå till recept

+
+
+

Recipe App

+ + Lägg till nytt recept + +
+
+ + Gå till varor som finns hemma + + + Gå till produktadmin + + + Gå till recept + +
); } \ No newline at end of file diff --git a/frontend/app/recipes/create/CreateRecipePage.tsx b/frontend/app/recipes/create/CreateRecipePage.tsx new file mode 100644 index 00000000..b14bb968 --- /dev/null +++ b/frontend/app/recipes/create/CreateRecipePage.tsx @@ -0,0 +1,182 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { fetchJson } from '../../../lib/api'; + +export default function CreateRecipePage() { + const router = useRouter(); + const [recipe, setRecipe] = useState({ + name: '', + description: '', + instructions: '', + ingredients: [{ productId: 0, quantity: '', unit: '', note: '' }], + }); + const [products, setProducts] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // Hämta produkter vid sidladdning + useState(() => { + fetchJson('/api/products') + .then(setProducts) + .catch(console.error); + }, []); + + const handleIngredientChange = (index: number, field: string, value: string | number) => { + const newIngredients = [...recipe.ingredients]; + newIngredients[index] = { ...newIngredients[index], [field]: value }; + setRecipe({ ...recipe, ingredients: newIngredients }); + }; + + const addIngredient = () => { + setRecipe({ + ...recipe, + ingredients: [...recipe.ingredients, { productId: 0, quantity: '', unit: '', note: '' }], + }); + }; + + const removeIngredient = (index: number) => { + const newIngredients = [...recipe.ingredients]; + newIngredients.splice(index, 1); + setRecipe({ ...recipe, ingredients: newIngredients }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(null); + + // Konvertera quantity till number för varje ingrediens + const recipeToSend = { + ...recipe, + ingredients: recipe.ingredients.map((ing) => ({ + ...ing, + quantity: Number(ing.quantity), + })), + }; + + try { + const response = await fetch('/api/recipes', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(recipeToSend), + }); + + if (!response.ok) { + throw new Error('Kunde inte spara receptet'); + } + + router.push('/recipes'); + } catch (err) { + setError((err as Error).message); + } finally { + setIsLoading(false); + } + }; + + return ( +
+

Lägg till nytt recept

+ + {error &&

{error}

} + +
+
+ + +