From e93e9903dc6f8d131bba1c71362bb19fc43be620 Mon Sep 17 00:00:00 2001 From: Tomasz Brauza Date: Tue, 16 Dec 2025 19:41:23 +0100 Subject: [PATCH 01/11] packages audit --- package-lock.json | 90 +++++++++++++++++++++-------------------------- package.json | 2 +- 2 files changed, 41 insertions(+), 51 deletions(-) diff --git a/package-lock.json b/package-lock.json index c4eb209..183f332 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "bcrypt": "^6.0.0", "dotenv": "^17.2.3", "lucide-react": "^0.556.0", - "next": "16.0.1", + "next": "^16.0.10", "next-auth": "^4.24.13", "react": "19.2.0", "react-dom": "19.2.0", @@ -1762,10 +1762,9 @@ } }, "node_modules/@next/env": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.1.tgz", - "integrity": "sha512-LFvlK0TG2L3fEOX77OC35KowL8D7DlFF45C0OvKMC4hy8c/md1RC4UMNDlUGJqfCoCS2VWrZ4dSE6OjaX5+8mw==", - "license": "MIT" + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz", + "integrity": "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==" }, "node_modules/@next/eslint-plugin-next": { "version": "16.0.1", @@ -1778,13 +1777,12 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.1.tgz", - "integrity": "sha512-R0YxRp6/4W7yG1nKbfu41bp3d96a0EalonQXiMe+1H9GTHfKxGNCGFNWUho18avRBPsO8T3RmdWuzmfurlQPbg==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.10.tgz", + "integrity": "sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -1794,13 +1792,12 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.1.tgz", - "integrity": "sha512-kETZBocRux3xITiZtOtVoVvXyQLB7VBxN7L6EPqgI5paZiUlnsgYv4q8diTNYeHmF9EiehydOBo20lTttCbHAg==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.10.tgz", + "integrity": "sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -1810,13 +1807,12 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.1.tgz", - "integrity": "sha512-hWg3BtsxQuSKhfe0LunJoqxjO4NEpBmKkE+P2Sroos7yB//OOX3jD5ISP2wv8QdUwtRehMdwYz6VB50mY6hqAg==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.10.tgz", + "integrity": "sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1826,13 +1822,12 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.1.tgz", - "integrity": "sha512-UPnOvYg+fjAhP3b1iQStcYPWeBFRLrugEyK/lDKGk7kLNua8t5/DvDbAEFotfV1YfcOY6bru76qN9qnjLoyHCQ==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.10.tgz", + "integrity": "sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1842,13 +1837,12 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.1.tgz", - "integrity": "sha512-Et81SdWkcRqAJziIgFtsFyJizHoWne4fzJkvjd6V4wEkWTB4MX6J0uByUb0peiJQ4WeAt6GGmMszE5KrXK6WKg==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.10.tgz", + "integrity": "sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1858,13 +1852,12 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.1.tgz", - "integrity": "sha512-qBbgYEBRrC1egcG03FZaVfVxrJm8wBl7vr8UFKplnxNRprctdP26xEv9nJ07Ggq4y1adwa0nz2mz83CELY7N6Q==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.10.tgz", + "integrity": "sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1874,13 +1867,12 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.1.tgz", - "integrity": "sha512-cPuBjYP6I699/RdbHJonb3BiRNEDm5CKEBuJ6SD8k3oLam2fDRMKAvmrli4QMDgT2ixyRJ0+DTkiODbIQhRkeQ==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.10.tgz", + "integrity": "sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -1890,13 +1882,12 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.1.tgz", - "integrity": "sha512-XeEUJsE4JYtfrXe/LaJn3z1pD19fK0Q6Er8Qoufi+HqvdO4LEPyCxLUt4rxA+4RfYo6S9gMlmzCMU2F+AatFqQ==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.10.tgz", + "integrity": "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -7059,12 +7050,11 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/next/-/next-16.0.1.tgz", - "integrity": "sha512-e9RLSssZwd35p7/vOa+hoDFggUZIUbZhIUSLZuETCwrCVvxOs87NamoUzT+vbcNAL8Ld9GobBnWOA6SbV/arOw==", - "license": "MIT", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz", + "integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==", "dependencies": { - "@next/env": "16.0.1", + "@next/env": "16.0.10", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -7077,14 +7067,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.0.1", - "@next/swc-darwin-x64": "16.0.1", - "@next/swc-linux-arm64-gnu": "16.0.1", - "@next/swc-linux-arm64-musl": "16.0.1", - "@next/swc-linux-x64-gnu": "16.0.1", - "@next/swc-linux-x64-musl": "16.0.1", - "@next/swc-win32-arm64-msvc": "16.0.1", - "@next/swc-win32-x64-msvc": "16.0.1", + "@next/swc-darwin-arm64": "16.0.10", + "@next/swc-darwin-x64": "16.0.10", + "@next/swc-linux-arm64-gnu": "16.0.10", + "@next/swc-linux-arm64-musl": "16.0.10", + "@next/swc-linux-x64-gnu": "16.0.10", + "@next/swc-linux-x64-musl": "16.0.10", + "@next/swc-win32-arm64-msvc": "16.0.10", + "@next/swc-win32-x64-msvc": "16.0.10", "sharp": "^0.34.4" }, "peerDependencies": { diff --git a/package.json b/package.json index 642bacb..2d67ded 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "bcrypt": "^6.0.0", "dotenv": "^17.2.3", "lucide-react": "^0.556.0", - "next": "16.0.1", + "next": "^16.0.10", "next-auth": "^4.24.13", "react": "19.2.0", "react-dom": "19.2.0", From 823c6426294f5fe47d8ddae1765d5c20a1d7890f Mon Sep 17 00:00:00 2001 From: Tomasz Brauza Date: Tue, 16 Dec 2025 19:41:49 +0100 Subject: [PATCH 02/11] schema update for user-added meals --- prisma/schema.prisma | 3 +++ 1 file changed, 3 insertions(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f5c1005..9e08cb6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,6 +17,8 @@ model Meal { mealPlans MealPlan[] suggestedMealType MealType[] tags Tag[] @relation("MealTags") + userId String? // null means it's a system meal, otherwise it's user-specific + user User? @relation(fields: [userId], references: [id]) } enum DayOfWeek { @@ -50,6 +52,7 @@ model User { createdAt DateTime @default(now()) mealPlans MealPlan[] //one to many preferences UserPreference[] + meals Meal[] // user-created meals } enum MealType { From a8d2fac8353a173409d39d85df4f830f1ae4c35e Mon Sep 17 00:00:00 2001 From: Tomasz Brauza Date: Tue, 16 Dec 2025 19:42:32 +0100 Subject: [PATCH 03/11] added user meals to mealpage and delete user meal button --- app/meals/page.tsx | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/app/meals/page.tsx b/app/meals/page.tsx index cd75545..b5335f8 100644 --- a/app/meals/page.tsx +++ b/app/meals/page.tsx @@ -1,6 +1,8 @@ import prisma from "@/lib/prisma"; import Link from "next/link"; import { MealType } from "@/app/generated/prisma/enums"; +import { getSession } from "@/lib/auth"; +import DeleteMealButton from "./DeleteMealButton"; const mealTypeLabels: Record = { BREAKFAST: "Śniadanie", @@ -10,7 +12,17 @@ const mealTypeLabels: Record = { }; export default async function MealsPage() { + const session = await getSession(); + const userId = session?.user?.id; + + const meals = await prisma.meal.findMany({ + where: { + OR: [ + { userId: null }, + { userId: userId || "" }, + ], + }, orderBy: { id: "asc" }, include: { tags: true }, }); @@ -44,9 +56,19 @@ export default async function MealsPage() { className="group rounded-xl border border-zinc-800 bg-zinc-900/60 shadow-sm transition-colors hover:bg-zinc-900" >
-

- {meal.name} -

+
+

+ {meal.name} +

+ {meal.userId && ( +
+ + Twój + + +
+ )} +
{meal.suggestedMealType && meal.suggestedMealType.length > 0 && (
From 80f7fc1161a8d5de3ff69969856b79dcdb1e0eae Mon Sep 17 00:00:00 2001 From: Tomasz Brauza Date: Tue, 16 Dec 2025 19:42:49 +0100 Subject: [PATCH 04/11] meal adding page --- app/meals/new/page.tsx | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/app/meals/new/page.tsx b/app/meals/new/page.tsx index b2ad536..9b2f193 100644 --- a/app/meals/new/page.tsx +++ b/app/meals/new/page.tsx @@ -1,12 +1,33 @@ -export default function NewMealPage() { +import { getSession } from "@/lib/auth"; +import prisma from "@/lib/prisma"; +import NewMealForm from "./NewMealForm"; + +export default async function NewMealPage() { + const session = await getSession(); + + if (!session?.user?.id) { + return ( +
+

+ Musisz być zalogowany, aby dodać posiłek. +

+
+ ); + } + + const tags = await prisma.tag.findMany({ + orderBy: { name: "asc" }, + }); + return (
-

+

Dodaj posiłek

-

- Tu będzie formularz dodawania posiłku. +

+ Wypełnij formularz, aby dodać własny posiłek do bazy.

+
); } From 6219e4a8f989afe6e2d0dc53f29dee778ffd7a18 Mon Sep 17 00:00:00 2001 From: Tomasz Brauza Date: Tue, 16 Dec 2025 19:43:12 +0100 Subject: [PATCH 05/11] user meal delete button --- app/meals/DeleteMealButton.tsx | 71 ++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 app/meals/DeleteMealButton.tsx diff --git a/app/meals/DeleteMealButton.tsx b/app/meals/DeleteMealButton.tsx new file mode 100644 index 0000000..0d460fb --- /dev/null +++ b/app/meals/DeleteMealButton.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +interface Props { + mealId: number; + mealName: string; +} + +export default function DeleteMealButton({ mealId, mealName }: Props) { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); + + const handleDelete = async () => { + setLoading(true); + try { + const res = await fetch(`/api/meals/${mealId}`, { + method: "DELETE", + }); + + if (res.ok) { + router.refresh(); + } else { + const data = await res.json(); + alert(data.error || "Nie udało się usunąć posiłku"); + } + } catch (err) { + console.error("Error deleting meal:", err); + alert("Wystąpił błąd podczas usuwania posiłku"); + } finally { + setLoading(false); + setShowConfirm(false); + } + }; + + if (!showConfirm) { + return ( + + ); + } + + return ( +
+ + +
+ ); +} From 90aea7d7e04a34167aeda36b83b628ec871c73e7 Mon Sep 17 00:00:00 2001 From: Tomasz Brauza Date: Tue, 16 Dec 2025 19:43:22 +0100 Subject: [PATCH 06/11] user meal adding form --- app/meals/new/NewMealForm.tsx | 328 ++++++++++++++++++++++++++++++++++ 1 file changed, 328 insertions(+) create mode 100644 app/meals/new/NewMealForm.tsx diff --git a/app/meals/new/NewMealForm.tsx b/app/meals/new/NewMealForm.tsx new file mode 100644 index 0000000..1e9dab8 --- /dev/null +++ b/app/meals/new/NewMealForm.tsx @@ -0,0 +1,328 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { MealType } from "@/app/generated/prisma/enums"; + +interface Tag { + id: number; + name: string; +} + +interface Props { + tags: Tag[]; +} + +const mealTypeLabels: Record = { + BREAKFAST: "Śniadanie", + LUNCH: "Obiad", + DINNER: "Kolacja", + DESSERT: "Deser", +}; + +export default function NewMealForm({ tags }: Props) { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [ingredients, setIngredients] = useState<{ name: string; amount: string }[]>([ + { name: "", amount: "" }, + ]); + const [recipeSteps, setRecipeSteps] = useState([""]); + const [selectedMealTypes, setSelectedMealTypes] = useState([]); + const [selectedTagIds, setSelectedTagIds] = useState([]); + + const addIngredient = () => { + setIngredients([...ingredients, { name: "", amount: "" }]); + }; + + const removeIngredient = (index: number) => { + setIngredients(ingredients.filter((_, i) => i !== index)); + }; + + const updateIngredient = (index: number, field: "name" | "amount", value: string) => { + const updated = [...ingredients]; + updated[index][field] = value; + setIngredients(updated); + }; + + const addRecipeStep = () => { + setRecipeSteps([...recipeSteps, ""]); + }; + + const removeRecipeStep = (index: number) => { + setRecipeSteps(recipeSteps.filter((_, i) => i !== index)); + }; + + const updateRecipeStep = (index: number, value: string) => { + const updated = [...recipeSteps]; + updated[index] = value; + setRecipeSteps(updated); + }; + + const toggleMealType = (mealType: MealType) => { + if (selectedMealTypes.includes(mealType)) { + setSelectedMealTypes(selectedMealTypes.filter((t) => t !== mealType)); + } else { + setSelectedMealTypes([...selectedMealTypes, mealType]); + } + }; + + const toggleTag = (tagId: number) => { + if (selectedTagIds.includes(tagId)) { + setSelectedTagIds(selectedTagIds.filter((id) => id !== tagId)); + } else { + setSelectedTagIds([...selectedTagIds, tagId]); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (!name.trim()) { + setError("Nazwa posiłku jest wymagana"); + return; + } + + if (selectedMealTypes.length === 0) { + setError("Wybierz przynajmniej jeden typ posiłku"); + return; + } + + const validIngredients = ingredients.filter( + (ing) => ing.name.trim() && ing.amount.trim() + ); + + if (validIngredients.length === 0) { + setError("Dodaj przynajmniej jeden składnik"); + return; + } + + const ingredientsObj: Record = {}; + for (const ing of validIngredients) { + const amount = parseFloat(ing.amount); + if (isNaN(amount) || amount <= 0) { + setError(`Nieprawidłowa ilość dla składnika: ${ing.name}`); + return; + } + ingredientsObj[ing.name.trim()] = amount; + } + + const validRecipeSteps = recipeSteps.filter((step) => step.trim()); + + setLoading(true); + + try { + const res = await fetch("/api/meals", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: name.trim(), + description: description.trim() || undefined, + ingredients: ingredientsObj, + recipe: validRecipeSteps.length > 0 ? validRecipeSteps : undefined, + suggestedMealType: selectedMealTypes, + tagIds: selectedTagIds, + }), + }); + + if (res.ok) { + router.push("/meals"); + } else { + const data = await res.json(); + setError(data.error || "Nie udało się dodać posiłku"); + } + } catch (err) { + console.error("Error submitting meal:", err); + setError("Wystąpił błąd podczas dodawania posiłku"); + } finally { + setLoading(false); + } + }; + + return ( +
+ {error && ( +
+ {error} +
+ )} + + {/* Name */} +
+ + setName(e.target.value)} + className="w-full rounded-lg border border-zinc-700 bg-zinc-900/60 px-4 py-2 text-zinc-100 focus:border-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500/50" + placeholder="np. Spaghetti Carbonara" + /> +
+ + {/* Description */} +
+ +