From 4c5a3f359b13ce5387bc2e44a9ab034e13dcf659 Mon Sep 17 00:00:00 2001 From: Igor Mariak Date: Tue, 23 Dec 2025 21:56:10 +0100 Subject: [PATCH 1/8] Update profile page --- app/api/shopping-list/route.ts | 12 ++++++++++++ app/profile/page.tsx | 3 +++ 2 files changed, 15 insertions(+) create mode 100644 app/api/shopping-list/route.ts diff --git a/app/api/shopping-list/route.ts b/app/api/shopping-list/route.ts new file mode 100644 index 0000000..dae2065 --- /dev/null +++ b/app/api/shopping-list/route.ts @@ -0,0 +1,12 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getSession } from "@/lib/auth"; + +export async function GET(request: NextRequest) { + const session = await getSession(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + + +} \ No newline at end of file diff --git a/app/profile/page.tsx b/app/profile/page.tsx index 7799444..cdfd93e 100644 --- a/app/profile/page.tsx +++ b/app/profile/page.tsx @@ -47,6 +47,9 @@ export default async function ProfilePage() { return (
+

+ Witaj, {session.user.name}! +

Szablony preferencji na dni tygodnia

From aa7a019daca0052a23adbd7da8c96c3d82a9f9bb Mon Sep 17 00:00:00 2001 From: Igor Mariak Date: Tue, 23 Dec 2025 22:29:37 +0100 Subject: [PATCH 2/8] Simple shopping list api json response --- app/api/shopping-list/route.ts | 59 +++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/app/api/shopping-list/route.ts b/app/api/shopping-list/route.ts index dae2065..4eca5c8 100644 --- a/app/api/shopping-list/route.ts +++ b/app/api/shopping-list/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { getSession } from "@/lib/auth"; +import prisma from "@/lib/prisma"; export async function GET(request: NextRequest) { const session = await getSession(); @@ -7,6 +8,62 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - + const { searchParams } = new URL(request.url) + + const start = searchParams.get("start") + const end = searchParams.get("end") + + if (!start || !end) { + return NextResponse.json( + { error: "Brak daty startowej lub końcowej" }, + { status: 400 } + ) + } + + const startDate = new Date(start) + const endDate = new Date(end) + + + // getting meal plans within date range + + try { + const mealPlans = await prisma.mealPlan.findMany({ + where: { + userId: session.user.id, + date: { + gte: startDate, + lte: endDate, + }, + }, + include: { meal : true }, + }); + + + const shoppingListMap = new Map() + + for (const plan of mealPlans) { + const ingredients = plan.meal.ingredients as Record + for (const [name, amount] of Object.entries(ingredients)) { + shoppingListMap.set( + name, + (shoppingListMap.get(name) ?? 0) + amount + ) + } + } + + const shoppingList = Array.from(shoppingListMap.entries()) + .map(([name, amount]) => ({name,amount,})) + .sort((a, b) => a.name.localeCompare(b.name)) + + return NextResponse.json({ shoppingList } ) + + + } catch (error) { + return NextResponse.json( + { error: "Błąd podczas pobierania planów posiłków" }, + { status: 500 } + ); + } + } \ No newline at end of file From 7d54bc03a4e98999bd21310356bb883784791b7a Mon Sep 17 00:00:00 2001 From: Igor Mariak Date: Sun, 28 Dec 2025 23:32:31 +0100 Subject: [PATCH 3/8] Add date picker --- app/planner/PlannerClient.tsx | 20 ++++++++++++++ app/shopping-list/new/page.tsx | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 app/shopping-list/new/page.tsx diff --git a/app/planner/PlannerClient.tsx b/app/planner/PlannerClient.tsx index cfa0a13..05f5bb2 100644 --- a/app/planner/PlannerClient.tsx +++ b/app/planner/PlannerClient.tsx @@ -7,6 +7,7 @@ import type { MealPlan, Meal } from "../generated/prisma/client"; import { useFlashStore } from "@/lib/flashStore"; import FlashNotices from "../components/FlashNotices"; import { type FailedToCreate } from '../api/meal-plan-generator/services'; +import { useRouter } from "next/navigation" type MealPlanWithMeal = MealPlan & { meal: Meal }; @@ -18,6 +19,8 @@ export default function PlannerClient({ meals }: PlannerClientProps) { const [showWeeklyForm, setShowWeeklyForm] = useState(false); const { flash } = useFlashStore(); + const router = useRouter() + return (
{ @@ -42,6 +45,23 @@ export default function PlannerClient({ meals }: PlannerClientProps) { {showWeeklyForm && ( setShowWeeklyForm(false)} /> )} + + { /* Przycisk do generowania listy zakupów */ } + +
+ +
+
+ + ); } diff --git a/app/shopping-list/new/page.tsx b/app/shopping-list/new/page.tsx new file mode 100644 index 0000000..25018d6 --- /dev/null +++ b/app/shopping-list/new/page.tsx @@ -0,0 +1,48 @@ +"use client" + +import { useRouter } from "next/navigation" +import { useState } from "react" + +export default function NewShoppingListPage() { + const router = useRouter() + + const [start, setStart] = useState("") + const [end, setEnd] = useState("") + + function handleGenerate() { + router.push( + `/shopping-list?start=${start}&end=${end}` + ) + } + + return ( +
+

+ Nowa lista zakupów +

+ +
+ setStart(e.target.value)} + className="rounded border p-2" + /> + + setEnd(e.target.value)} + className="rounded border p-2" + /> + + +
+
+ ) +} From 886f7456c70243f11e885d4a4a424b98bae8f615 Mon Sep 17 00:00:00 2001 From: Igor Mariak Date: Sun, 28 Dec 2025 23:38:48 +0100 Subject: [PATCH 4/8] Add ShoppingList table --- .../migration.sql | 13 +++++++++ prisma/schema.prisma | 28 +++++++++++++------ 2 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 prisma/migrations/20251228223723_add_shopping_list/migration.sql diff --git a/prisma/migrations/20251228223723_add_shopping_list/migration.sql b/prisma/migrations/20251228223723_add_shopping_list/migration.sql new file mode 100644 index 0000000..c36dadb --- /dev/null +++ b/prisma/migrations/20251228223723_add_shopping_list/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "ShoppingList" ( + "id" SERIAL NOT NULL, + "userId" TEXT NOT NULL, + "startDate" TIMESTAMP(3) NOT NULL, + "endDate" TIMESTAMP(3) NOT NULL, + "items" JSONB NOT NULL, + + CONSTRAINT "ShoppingList_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "ShoppingList" ADD CONSTRAINT "ShoppingList_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 290f27c..f70558b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,7 +17,7 @@ model Meal { mealPlans MealPlan[] suggestedMealType MealType[] tags Tag[] @relation("MealTags") - userId String? + userId String? user User? @relation(fields: [userId], references: [id]) } @@ -45,14 +45,15 @@ model Tag { } model User { - id String @id @default(cuid()) - email String @unique - password String - name String? - createdAt DateTime @default(now()) - mealPlans MealPlan[] //one to many - preferences UserPreference[] - meals Meal[] // user-created meals + id String @id @default(cuid()) + email String @unique + password String + name String? + createdAt DateTime @default(now()) + mealPlans MealPlan[] //one to many + preferences UserPreference[] + meals Meal[] // user-created meals + shoppingLists ShoppingList[] } enum MealType { @@ -81,3 +82,12 @@ model UserPreference { @@id([userId, dayOfWeek, tagId]) } + +model ShoppingList { + id Int @id @default(autoincrement()) + userId String + user User @relation(fields: [userId], references: [id]) + startDate DateTime + endDate DateTime + items Json +} From 4334a36fecf648d3f3373418e334a6616c7c3024 Mon Sep 17 00:00:00 2001 From: Igor Mariak Date: Mon, 29 Dec 2025 00:01:08 +0100 Subject: [PATCH 5/8] Update api with database --- app/api/shopping-list/route.ts | 20 +++++++--- app/shopping-list/new/page.tsx | 39 ++++++++++++------- app/shopping-list/page.tsx | 34 ++++++++++++++-- .../migration.sql | 1 + prisma/schema.prisma | 1 + 5 files changed, 72 insertions(+), 23 deletions(-) rename prisma/migrations/{20251228223723_add_shopping_list => 20251228224915_add_shopping_list}/migration.sql (86%) diff --git a/app/api/shopping-list/route.ts b/app/api/shopping-list/route.ts index 4eca5c8..7122643 100644 --- a/app/api/shopping-list/route.ts +++ b/app/api/shopping-list/route.ts @@ -2,16 +2,13 @@ import { NextRequest, NextResponse } from "next/server"; import { getSession } from "@/lib/auth"; import prisma from "@/lib/prisma"; -export async function GET(request: NextRequest) { +export async function POST(request: NextRequest) { const session = await getSession(); if (!session?.user?.id) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const { searchParams } = new URL(request.url) - - const start = searchParams.get("start") - const end = searchParams.get("end") + const {start, end} = await request.json(); if (!start || !end) { return NextResponse.json( @@ -51,10 +48,21 @@ export async function GET(request: NextRequest) { } } - const shoppingList = Array.from(shoppingListMap.entries()) + const shoppingListItems = Array.from(shoppingListMap.entries()) .map(([name, amount]) => ({name,amount,})) .sort((a, b) => a.name.localeCompare(b.name)) + // + + const shoppingList = await prisma.shoppingList.create({ + data: { + userId: session.user.id, + startDate: startDate, + endDate: endDate, + items: shoppingListItems, + }, + }); + return NextResponse.json({ shoppingList } ) diff --git a/app/shopping-list/new/page.tsx b/app/shopping-list/new/page.tsx index 25018d6..cfda346 100644 --- a/app/shopping-list/new/page.tsx +++ b/app/shopping-list/new/page.tsx @@ -5,14 +5,32 @@ import { useState } from "react" export default function NewShoppingListPage() { const router = useRouter() - const [start, setStart] = useState("") const [end, setEnd] = useState("") - function handleGenerate() { - router.push( - `/shopping-list?start=${start}&end=${end}` - ) + async function handleGenerate() { + if (!start || !end) { + alert("Wybierz zarówno datę początkową, jak i końcową") + return + } + + try { + const res = await fetch("/api/shopping-list", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ start, end }), + cache: "no-store", + }) + + if (!res.ok) { + throw new Error("Nie udało się wygenerować listy zakupów") + } + + router.push("/shopping-list") + } catch (err) { + console.error(err) + alert("Wystąpił błąd przy tworzeniu listy zakupów") + } } return ( @@ -21,25 +39,20 @@ export default function NewShoppingListPage() { Nowa lista zakupów -
+ {/* Proste wprowadzanie dat*/} +
setStart(e.target.value)} - className="rounded border p-2" /> - setEnd(e.target.value)} - className="rounded border p-2" /> -
diff --git a/app/shopping-list/page.tsx b/app/shopping-list/page.tsx index 4f4df47..b6da424 100644 --- a/app/shopping-list/page.tsx +++ b/app/shopping-list/page.tsx @@ -1,12 +1,38 @@ -export default function ShoppingListPage() { +import prisma from "@/lib/prisma"; +import { getSession } from "@/lib/auth"; + +export default async function ShoppingListPage() { + const session = await getSession(); + if (!session?.user?.id) return

Musisz być zalogowany

; + + const lists = await prisma.shoppingList.findMany({ + where: { userId: session.user.id }, + orderBy: { createdAt: "desc" }, + }); + + return (

Lista zakupów

-

- Tu będzie strona z listą zakupów. -

+ + {lists.length === 0 ? ( +

Brak list zakupów. Wygeneruj nową listę zakupów.

+ ) : ( + + // bardzo proste przekierowanie do list + + + )}
); } diff --git a/prisma/migrations/20251228223723_add_shopping_list/migration.sql b/prisma/migrations/20251228224915_add_shopping_list/migration.sql similarity index 86% rename from prisma/migrations/20251228223723_add_shopping_list/migration.sql rename to prisma/migrations/20251228224915_add_shopping_list/migration.sql index c36dadb..7042736 100644 --- a/prisma/migrations/20251228223723_add_shopping_list/migration.sql +++ b/prisma/migrations/20251228224915_add_shopping_list/migration.sql @@ -5,6 +5,7 @@ CREATE TABLE "ShoppingList" ( "startDate" TIMESTAMP(3) NOT NULL, "endDate" TIMESTAMP(3) NOT NULL, "items" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "ShoppingList_pkey" PRIMARY KEY ("id") ); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f70558b..5029826 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -90,4 +90,5 @@ model ShoppingList { startDate DateTime endDate DateTime items Json + createdAt DateTime @default(now()) } From 8397a6edb6994558c7ca207e4fc38c27f65b503b Mon Sep 17 00:00:00 2001 From: Igor Mariak Date: Mon, 29 Dec 2025 00:28:55 +0100 Subject: [PATCH 6/8] Add detailed page --- app/shopping-list/[id]/page.tsx | 43 +++++++++++++++++++++++++++++++++ app/shopping-list/page.tsx | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 app/shopping-list/[id]/page.tsx diff --git a/app/shopping-list/[id]/page.tsx b/app/shopping-list/[id]/page.tsx new file mode 100644 index 0000000..6a216a4 --- /dev/null +++ b/app/shopping-list/[id]/page.tsx @@ -0,0 +1,43 @@ +import prisma from "@/lib/prisma" +import { getSession } from "@/lib/auth" + +type Props = { + params : Promise<{ id: string }> +} + +export default async function ShoppingListDetailPage({ params }: Props) { + const session = await getSession() + if (!session?.user?.id) return

Musisz być zalogowany

+ + const corParams = await params + const id = parseInt(corParams.id, 10) + console.log("ID listy zakupów:", id) + + const list = await prisma.shoppingList.findUnique({ + where: { id }, + }) + + if (!list || list.userId !== session.user.id) { + return

Lista zakupów nie znaleziona

+ } + + const shoppingListItems = (list.items as { name: string; amount: number }[]) + + return ( +
+
+

+ Lista zakupów: od {list.startDate.toISOString().slice(0,10)} do {list.endDate.toISOString().slice(0,10)} +

+ +
    + {shoppingListItems.map((item: any) => ( +
  • + {item.name}: {item.amount}g +
  • + ))} +
+
+
+ ) +} diff --git a/app/shopping-list/page.tsx b/app/shopping-list/page.tsx index b6da424..34a2aab 100644 --- a/app/shopping-list/page.tsx +++ b/app/shopping-list/page.tsx @@ -14,7 +14,7 @@ export default async function ShoppingListPage() { return (

- Lista zakupów + Listy zakupowe

{lists.length === 0 ? ( From 437d8baa338742da2c22cb8785524a4a474dccd0 Mon Sep 17 00:00:00 2001 From: Igor Mariak Date: Mon, 29 Dec 2025 02:02:55 +0100 Subject: [PATCH 7/8] Add test --- app/api/shopping-list/route.ts | 4 +- app/shopping-list/page.tsx | 6 +- test/api/shoppinglist.test.ts | 120 +++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 test/api/shoppinglist.test.ts diff --git a/app/api/shopping-list/route.ts b/app/api/shopping-list/route.ts index 7122643..3dae844 100644 --- a/app/api/shopping-list/route.ts +++ b/app/api/shopping-list/route.ts @@ -63,12 +63,12 @@ export async function POST(request: NextRequest) { }, }); - return NextResponse.json({ shoppingList } ) + return NextResponse.json({ shoppingList }, { status: 201 }); } catch (error) { return NextResponse.json( - { error: "Błąd podczas pobierania planów posiłków" }, + { error: "Błąd podczas tworzenia listy zakupów" }, { status: 500 } ); } diff --git a/app/shopping-list/page.tsx b/app/shopping-list/page.tsx index 34a2aab..67d26ea 100644 --- a/app/shopping-list/page.tsx +++ b/app/shopping-list/page.tsx @@ -22,11 +22,11 @@ export default async function ShoppingListPage() { ) : ( // bardzo proste przekierowanie do list -
    +
      {lists.map(list => ( -
    • +
    • - {list.startDate.toISOString().slice(0, 10)} → {list.endDate.toISOString().slice(0, 10)} + ▫︎ {list.startDate.toISOString().slice(0, 10)} → {list.endDate.toISOString().slice(0, 10)}
    • ))} diff --git a/test/api/shoppinglist.test.ts b/test/api/shoppinglist.test.ts new file mode 100644 index 0000000..24e5c8f --- /dev/null +++ b/test/api/shoppinglist.test.ts @@ -0,0 +1,120 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import prisma from "@/lib/prisma"; +import { getSession } from "@/lib/auth"; +import { NextRequest } from "next/server"; + +vi.mock("@/lib/auth", () => ({ + getSession: vi.fn(), +})); + +describe("POST /api/shopping-list", () => { + beforeEach(async () => { + vi.resetAllMocks(); + vi.resetModules(); + + await prisma.shoppingList.deleteMany(); + await prisma.mealPlan.deleteMany(); + await prisma.meal.deleteMany(); + await prisma.user.deleteMany(); + }); + + // test dla sumowania skladnikow listy + + it("tworzy liste zakupow z poprawnie zsumowanymi skladnikami", async () => { + + const userId = "user_test_shop"; + await prisma.user.create({ + data: { + id: userId, + email: "liketoshop@gmail.com", + password: "hashed-password", + name: "Shopper", + }, + }); + + const meal1 = await prisma.meal.create({ + data: { + name: "Kanapki z serem i ogórkiem", + ingredients: { "chleb": 100, "ser": 40, "ogórek": 20 }, + recipe: ["Posmaruj chleb", "Dodaj ser i ogórek"], + userId: userId, + suggestedMealType: ["BREAKFAST"], + }, + }); + + const meal2 = await prisma.meal.create({ + data: { + name: "Tosty ala pizza", + ingredients: { "chleb": 140, "ser": 60, "szynka": 50, "pieczarki": 30, "ketchup": 10 }, + recipe: ["Ułóż składniki na chlebie", "Podpiecz w tosterze"], + userId: userId, + suggestedMealType: ["DINNER"], + }, + }); + + const meal3 = await prisma.meal.create({ + data: { + name: "Makaron mac and cheese", + ingredients: { "makaron": 150, "ser": 200, "przyprawy": 10 }, + recipe: ["Ugotuj makaron", "Dodaj ser i przyprawy"], + userId: userId, + suggestedMealType: ["LUNCH"], + }, + }); + + await prisma.mealPlan.create({ + data: { date: new Date("2025-12-22"), type: "BREAKFAST", mealId: meal1.id, userId } + }); + await prisma.mealPlan.create({ + data: { date: new Date("2025-12-22"), type: "DINNER", mealId: meal2.id, userId } + }); + await prisma.mealPlan.create({ + data: { date: new Date("2025-12-22"), type: "LUNCH", mealId: meal3.id, userId } + }); + + // mokowanie zalogowanego uzytkownika + vi.mocked(getSession).mockResolvedValue({ user: { id: userId } } as any); + + // import funkcji POST z api + const { POST } = await import("@/app/api/shopping-list/route"); + + const req = new NextRequest("http://localhost/api/shopping-list", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + start: "2025-12-21", + end: "2025-12-23", + }), + }); + + const res = await POST(req); + expect(res.status).toBe(201); + + const payload = await res.json(); + expect(payload.shoppingList.userId).toBe(userId); + + const savedList = await prisma.shoppingList.findFirst({ + where: { userId }, + }); + + const expectedItems = [ + { name: "chleb", amount: 240 }, + { name: "ser", amount: 300 }, + { name: "ketchup", amount: 10 }, + { name: "makaron", amount: 150 }, + { name: "ogórek", amount: 20 }, + { name: "pieczarki", amount: 30 }, + { name: "przyprawy", amount: 10 }, + { name: "szynka", amount: 50 }, + ]; + + // sortujemy po nazwie + const sortByName = (a: any, b: any) => a.name.localeCompare(b.name); + + expect(payload.shoppingList.items.sort(sortByName)).toEqual( + expectedItems.sort(sortByName) + ); + + }); + +}); \ No newline at end of file From 17e156df4bc536cc2c181b68c0b3f575e3f63c87 Mon Sep 17 00:00:00 2001 From: Igor Mariak Date: Fri, 2 Jan 2026 16:08:05 +0100 Subject: [PATCH 8/8] Add unathorized test --- test/api/shoppinglist.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/api/shoppinglist.test.ts b/test/api/shoppinglist.test.ts index 24e5c8f..d4d78c3 100644 --- a/test/api/shoppinglist.test.ts +++ b/test/api/shoppinglist.test.ts @@ -117,4 +117,22 @@ describe("POST /api/shopping-list", () => { }); + it("zwraca 401 jeśli użytkownik niezalogowany", async () => { + vi.mocked(getSession).mockResolvedValue(null as any); + + const { POST } = await import("@/app/api/shopping-list/route"); + + const req = new NextRequest("http://localhost/api/shopping-list", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ start: "2025-12-22", end: "2025-12-22" }), + }); + + const res = await POST(req); + expect(res.status).toBe(401); + + const payload = await res.json(); + expect(payload.error).toBe("Unauthorized"); + }); + }); \ No newline at end of file