From 9d5f397148fb62c38c95d2597935d1b17326e25c Mon Sep 17 00:00:00 2001 From: Louis <12069002+13Bytes@users.noreply.github.com> Date: Thu, 7 May 2026 21:49:01 +0200 Subject: [PATCH 1/9] Refine canonical item handling in buyable query and item creation --- .../migration.sql | 47 +++++++++++++++++++ prisma/schema.prisma | 4 ++ src/pages/buy.tsx | 7 ++- src/server/api/routers/items.ts | 29 ++++++++++-- 4 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 prisma/migrations/20260507120000_add_canonical_item_id/migration.sql diff --git a/prisma/migrations/20260507120000_add_canonical_item_id/migration.sql b/prisma/migrations/20260507120000_add_canonical_item_id/migration.sql new file mode 100644 index 0000000..e3c67ab --- /dev/null +++ b/prisma/migrations/20260507120000_add_canonical_item_id/migration.sql @@ -0,0 +1,47 @@ +-- Add canonical item IDs to preserve logical item identity across immutable item copies +ALTER TABLE "Item" ADD COLUMN "canonicalItemId" TEXT; +ALTER TABLE "ItemCategoryMapping" ADD COLUMN "canonicalItemId" TEXT; + +-- Backfill existing rows +UPDATE "Item" SET "canonicalItemId" = "id" WHERE "canonicalItemId" IS NULL; + +UPDATE "ItemCategoryMapping" +SET "canonicalItemId" = ( + SELECT "Item"."canonicalItemId" + FROM "Item" + WHERE "Item"."id" = "ItemCategoryMapping"."itemId" +) +WHERE "canonicalItemId" IS NULL; + +-- Enforce not-null after backfill +CREATE TABLE "new_Item" ( + "id" TEXT NOT NULL PRIMARY KEY, + "canonicalItemId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "price" REAL NOT NULL, + "is_active" BOOLEAN NOT NULL DEFAULT true, + "for_grouporders" BOOLEAN NOT NULL DEFAULT false, + "accountId" TEXT NOT NULL, + CONSTRAINT "Item_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "ClearingAccount" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Item" ("id", "canonicalItemId", "name", "price", "is_active", "for_grouporders", "accountId") +SELECT "id", "canonicalItemId", "name", "price", "is_active", "for_grouporders", "accountId" +FROM "Item"; +DROP TABLE "Item"; +ALTER TABLE "new_Item" RENAME TO "Item"; +CREATE INDEX "Item_canonicalItemId_idx" ON "Item"("canonicalItemId"); + +CREATE TABLE "new_ItemCategoryMapping" ( + "id" TEXT NOT NULL PRIMARY KEY, + "canonicalItemId" TEXT NOT NULL, + "itemId" TEXT NOT NULL, + "transactionId" TEXT NOT NULL, + CONSTRAINT "ItemCategoryMapping_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "ItemCategoryMapping_transactionId_fkey" FOREIGN KEY ("transactionId") REFERENCES "Transaction" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_ItemCategoryMapping" ("id", "canonicalItemId", "itemId", "transactionId") +SELECT "id", "canonicalItemId", "itemId", "transactionId" +FROM "ItemCategoryMapping"; +DROP TABLE "ItemCategoryMapping"; +ALTER TABLE "new_ItemCategoryMapping" RENAME TO "ItemCategoryMapping"; +CREATE INDEX "ItemCategoryMapping_canonicalItemId_idx" ON "ItemCategoryMapping"("canonicalItemId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7b1ced8..93e15c6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -21,6 +21,7 @@ model Item { // items must be immutable and shouldn't be deleted (if transactions exists) // - create copy instead and set is_active=false id String @id @default(cuid()) + canonicalItemId String name String categories Category[] price Float @@ -29,6 +30,7 @@ model Item { is_active Boolean @default(true) for_grouporders Boolean @default(false) accountId String + @@index([canonicalItemId]) } model ProcurementItem { @@ -107,11 +109,13 @@ model ClearingAccount { model ItemCategoryMapping { id String @id @default(cuid()) item Item @relation(fields: [itemId], references: [id]) + canonicalItemId String categories Category[] Transaction Transaction @relation(fields: [transactionId], references: [id]) itemId String transactionId String + @@index([canonicalItemId]) } model ProcurementItemBilling { diff --git a/src/pages/buy.tsx b/src/pages/buy.tsx index dfb3345..fba571a 100644 --- a/src/pages/buy.tsx +++ b/src/pages/buy.tsx @@ -59,7 +59,12 @@ const BuyPage: NextPage = () => { const searchShown = item.name.toLowerCase().includes(searchString.toLowerCase()) return categoryShown && searchShown }) - .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())) + .sort((a, b) => { + if (a.userOrderCount !== b.userOrderCount) { + return b.userOrderCount - a.userOrderCount + } + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()) + }) const selectedCategoriesCount = Object.values(displayCategories).filter(Boolean).length const allCategoriesSelected = selectedCategoriesCount === allRelevantCategories?.length diff --git a/src/server/api/routers/items.ts b/src/server/api/routers/items.ts index 686aba9..97e6c98 100644 --- a/src/server/api/routers/items.ts +++ b/src/server/api/routers/items.ts @@ -1,3 +1,4 @@ +import { randomUUID } from "crypto" import { type Prisma, type PrismaClient } from "@prisma/client" import { z } from "zod" import { createItemSchema } from "~/components/Forms/ItemForm" @@ -31,11 +32,23 @@ export const itemRouter = createTRPCRouter({ }) }), - getBuyable: protectedProcedure.query(({ ctx }) => { - return ctx.prisma.item.findMany({ + getBuyable: protectedProcedure.query(async ({ ctx }) => { + const items = await ctx.prisma.item.findMany({ where: { is_active: true, for_grouporders: false }, include: { categories: true }, }) + const groupedOrders = await ctx.prisma.itemCategoryMapping.groupBy({ + by: ["canonicalItemId"], + where: { Transaction: { userId: ctx.session.user.id, canceled: false, type: 0 } }, + _count: { _all: true }, + }) + const orderCountByCanonicalItemId = new Map( + groupedOrders.map((groupedOrder) => [groupedOrder.canonicalItemId, groupedOrder._count._all]), + ) + return items.map((item) => ({ + ...item, + userOrderCount: orderCountByCanonicalItemId.get(item.canonicalItemId) ?? 0, + })) }), @@ -49,8 +62,11 @@ export const itemRouter = createTRPCRouter({ }) }), ) - const item = await prisma.item.create({ + const canonicalItemId = randomUUID() + return await ctx.prisma.item.create({ data: { + canonicalItemId, + id: canonicalItemId, name: input.name, price: input.price, account: { connect: { id: input.account } }, @@ -59,7 +75,6 @@ export const itemRouter = createTRPCRouter({ for_grouporders: input.for_grouporders, }, }) - return item }), updateItem: adminProcedure @@ -75,11 +90,16 @@ export const itemRouter = createTRPCRouter({ }), ) const { id, ...inputData } = input + const existingItem = await prisma.item.findUniqueOrThrow({ + where: { id }, + select: { canonicalItemId: true }, + }) await prisma.$transaction([ prisma.item.update({ where: { id: input.id }, data: { is_active: false } }), prisma.item.create({ data: { ...inputData, + canonicalItemId: existingItem.canonicalItemId, account: { connect: { id: input.account } }, categories: { connect: categories.map((category) => ({ id: category.id })) }, }, @@ -210,6 +230,7 @@ export const buyItem = async ( create: [ { item: { connect: { id: product.id } }, + canonicalItemId: product.canonicalItemId, categories: { connect: product.categories.map((category) => ({ id: category.id })), }, From 8a40c268290c93885bbdffa063aea4d9f85febd7 Mon Sep 17 00:00:00 2001 From: 13 Bytes Date: Thu, 7 May 2026 23:19:42 +0200 Subject: [PATCH 2/9] adapt naming for screenshot --- .github/workflows/readme-screenshots.yml | 2 +- package.json | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/readme-screenshots.yml b/.github/workflows/readme-screenshots.yml index 8820057..b5909d0 100644 --- a/.github/workflows/readme-screenshots.yml +++ b/.github/workflows/readme-screenshots.yml @@ -49,7 +49,7 @@ jobs: run: npm run build - name: Generate screenshots - run: npm run screenshots + run: npm run screenshots:ci - name: Configure GitHub Pages uses: actions/configure-pages@v5 diff --git a/package.json b/package.json index a19533a..7668d0e 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,13 @@ "lint": "eslint .", "start": "next start", "test": "vitest", - "screenshots": "npm run screenshots:seed && npm run screenshots:capture", - "screenshots:seed": "jiti scripts/screenshots/seed.ts", - "screenshots:capture": "jiti scripts/screenshots/capture.ts", + "screenshots": "jiti scripts/screenshots/capture.ts", + "screenshots:ci": "npm run db:seed && npm run screenshots:capture", "db:generate": "prisma generate", "db:migrate": "npx prisma migrate dev", "db:push": "npx prisma db push", - "db:studio": "npx prisma studio" + "db:studio": "npx prisma studio", + "db:seed": "jiti scripts/screenshots/seed.ts" }, "dependencies": { "@headlessui/react": "^2.0.3", @@ -83,4 +83,4 @@ "@types/react": "19.2.7", "@types/react-dom": "19.2.3" } -} +} \ No newline at end of file From 05df3d1b8b1ca2f2e015c19e10b1c170ac33038f Mon Sep 17 00:00:00 2001 From: 13 Bytes Date: Fri, 8 May 2026 02:00:30 +0200 Subject: [PATCH 3/9] fix migration --- .../migration.sql | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/prisma/migrations/20260507120000_add_canonical_item_id/migration.sql b/prisma/migrations/20260507120000_add_canonical_item_id/migration.sql index e3c67ab..7674d51 100644 --- a/prisma/migrations/20260507120000_add_canonical_item_id/migration.sql +++ b/prisma/migrations/20260507120000_add_canonical_item_id/migration.sql @@ -1,19 +1,12 @@ -- Add canonical item IDs to preserve logical item identity across immutable item copies -ALTER TABLE "Item" ADD COLUMN "canonicalItemId" TEXT; -ALTER TABLE "ItemCategoryMapping" ADD COLUMN "canonicalItemId" TEXT; +-- Rebuild the tables directly so the migration also recovers from a failed +-- earlier attempt that already added nullable canonicalItemId columns. +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; --- Backfill existing rows -UPDATE "Item" SET "canonicalItemId" = "id" WHERE "canonicalItemId" IS NULL; +DROP TABLE IF EXISTS "new_Item"; +DROP TABLE IF EXISTS "new_ItemCategoryMapping"; -UPDATE "ItemCategoryMapping" -SET "canonicalItemId" = ( - SELECT "Item"."canonicalItemId" - FROM "Item" - WHERE "Item"."id" = "ItemCategoryMapping"."itemId" -) -WHERE "canonicalItemId" IS NULL; - --- Enforce not-null after backfill CREATE TABLE "new_Item" ( "id" TEXT NOT NULL PRIMARY KEY, "canonicalItemId" TEXT NOT NULL, @@ -25,11 +18,11 @@ CREATE TABLE "new_Item" ( CONSTRAINT "Item_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "ClearingAccount" ("id") ON DELETE RESTRICT ON UPDATE CASCADE ); INSERT INTO "new_Item" ("id", "canonicalItemId", "name", "price", "is_active", "for_grouporders", "accountId") -SELECT "id", "canonicalItemId", "name", "price", "is_active", "for_grouporders", "accountId" +SELECT "id", "id", "name", "price", "is_active", "for_grouporders", "accountId" FROM "Item"; DROP TABLE "Item"; ALTER TABLE "new_Item" RENAME TO "Item"; -CREATE INDEX "Item_canonicalItemId_idx" ON "Item"("canonicalItemId"); +CREATE INDEX IF NOT EXISTS "Item_canonicalItemId_idx" ON "Item"("canonicalItemId"); CREATE TABLE "new_ItemCategoryMapping" ( "id" TEXT NOT NULL PRIMARY KEY, @@ -40,8 +33,16 @@ CREATE TABLE "new_ItemCategoryMapping" ( CONSTRAINT "ItemCategoryMapping_transactionId_fkey" FOREIGN KEY ("transactionId") REFERENCES "Transaction" ("id") ON DELETE RESTRICT ON UPDATE CASCADE ); INSERT INTO "new_ItemCategoryMapping" ("id", "canonicalItemId", "itemId", "transactionId") -SELECT "id", "canonicalItemId", "itemId", "transactionId" +SELECT + "ItemCategoryMapping"."id", + (SELECT "Item"."canonicalItemId" FROM "Item" WHERE "Item"."id" = "ItemCategoryMapping"."itemId"), + "ItemCategoryMapping"."itemId", + "ItemCategoryMapping"."transactionId" FROM "ItemCategoryMapping"; DROP TABLE "ItemCategoryMapping"; ALTER TABLE "new_ItemCategoryMapping" RENAME TO "ItemCategoryMapping"; -CREATE INDEX "ItemCategoryMapping_canonicalItemId_idx" ON "ItemCategoryMapping"("canonicalItemId"); +CREATE INDEX IF NOT EXISTS "ItemCategoryMapping_canonicalItemId_idx" ON "ItemCategoryMapping"("canonicalItemId"); + +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; From 0ff4706825061ac8395006773e3e269217bea181 Mon Sep 17 00:00:00 2001 From: 13 Bytes Date: Fri, 8 May 2026 02:18:56 +0200 Subject: [PATCH 4/9] Add sorting options for items --- scripts/screenshots/seed.ts | 3 +++ src/pages/buy.tsx | 42 ++++++++++++++++++++++++++++++--- src/server/api/routers/items.ts | 19 +++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/scripts/screenshots/seed.ts b/scripts/screenshots/seed.ts index 30a2fcc..ae8d58c 100644 --- a/scripts/screenshots/seed.ts +++ b/scripts/screenshots/seed.ts @@ -150,6 +150,7 @@ async function seed() { await prisma.item.create({ data: { id: item.id, + canonicalItemId: item.id, name: item.name, price: item.price, for_grouporders: item.for_grouporders ?? false, @@ -246,6 +247,7 @@ async function seed() { items: { create: { item: { connect: { id: ids.items[0] } }, + canonicalItemId: ids.items[0], categories: { connect: [{ id: ids.categories[0] }] }, }, }, @@ -282,6 +284,7 @@ async function seed() { items: { create: { item: { connect: { id: ids.items[1] } }, + canonicalItemId: ids.items[1], categories: { connect: [{ id: ids.categories[1] }] }, }, }, diff --git a/src/pages/buy.tsx b/src/pages/buy.tsx index fba571a..f6f9126 100644 --- a/src/pages/buy.tsx +++ b/src/pages/buy.tsx @@ -9,6 +9,8 @@ import BuyItemCard from "~/components/General/BuyItemCard" import RegularPage from "~/components/Layout/RegularPage" import { api } from "~/utils/api" +type SortMode = "recent" | "alphabetic" | "mostBought" + const BuyPage: NextPage = () => { const allItemsRequest = api.item.getBuyable.useQuery() const allCategoriesRequest = api.category.getAllWithItems.useQuery() @@ -16,6 +18,7 @@ const BuyPage: NextPage = () => { const trpcUtils = api.useUtils() const animationRef = useRef(null) const [searchString, setSearchString] = useState("") + const [sortMode, setSortMode] = useState("recent") const [categoryOverrides, setCategoryOverrides] = useState<{ [index: string]: boolean }>({}) const apiBuyOneItemMultiple = api.item.buyItem.useMutation() @@ -29,6 +32,7 @@ const BuyPage: NextPage = () => { : `${quantity}x erfolgreich gekauft!` animate(animationRef, "success", message) await trpcUtils.user.invalidate() + await trpcUtils.item.getBuyable.invalidate() } catch (error: any) { console.error(error) animate(animationRef, "failure", error.message) @@ -60,10 +64,25 @@ const BuyPage: NextPage = () => { return categoryShown && searchShown }) .sort((a, b) => { - if (a.userOrderCount !== b.userOrderCount) { - return b.userOrderCount - a.userOrderCount + const nameComparison = a.name.toLowerCase().localeCompare(b.name.toLowerCase()) + + if (sortMode === "alphabetic") { + return nameComparison + } + else if (sortMode === "mostBought") { + if (a.userOrderCount !== b.userOrderCount) { + return b.userOrderCount - a.userOrderCount + } + return nameComparison + } + else { + const aLastBoughtAt = a.userLastBoughtAt?.getTime() ?? 0 + const bLastBoughtAt = b.userLastBoughtAt?.getTime() ?? 0 + if (aLastBoughtAt !== bLastBoughtAt) { + return bLastBoughtAt - aLastBoughtAt + } + return nameComparison } - return a.name.toLowerCase().localeCompare(b.name.toLowerCase()) }) const selectedCategoriesCount = Object.values(displayCategories).filter(Boolean).length @@ -123,6 +142,23 @@ const BuyPage: NextPage = () => { +
+
+ + +
+
+ {/* Results Summary */}
{displayedItems ? ( diff --git a/src/server/api/routers/items.ts b/src/server/api/routers/items.ts index 97e6c98..40066ea 100644 --- a/src/server/api/routers/items.ts +++ b/src/server/api/routers/items.ts @@ -42,12 +42,31 @@ export const itemRouter = createTRPCRouter({ where: { Transaction: { userId: ctx.session.user.id, canceled: false, type: 0 } }, _count: { _all: true }, }) + const recentOrders = await ctx.prisma.itemCategoryMapping.findMany({ + where: { Transaction: { userId: ctx.session.user.id, canceled: false, type: 0 } }, + select: { + canonicalItemId: true, + Transaction: { + select: { + createdAt: true, + }, + }, + }, + orderBy: { Transaction: { createdAt: "desc" } }, + }) const orderCountByCanonicalItemId = new Map( groupedOrders.map((groupedOrder) => [groupedOrder.canonicalItemId, groupedOrder._count._all]), ) + const lastBoughtAtByCanonicalItemId = new Map() + recentOrders.forEach((order) => { + if (!lastBoughtAtByCanonicalItemId.has(order.canonicalItemId)) { + lastBoughtAtByCanonicalItemId.set(order.canonicalItemId, order.Transaction.createdAt) + } + }) return items.map((item) => ({ ...item, userOrderCount: orderCountByCanonicalItemId.get(item.canonicalItemId) ?? 0, + userLastBoughtAt: lastBoughtAtByCanonicalItemId.get(item.canonicalItemId) ?? null, })) }), From 6d3a952b9a91124b22297084da97168ed7946707 Mon Sep 17 00:00:00 2001 From: 13 Bytes Date: Fri, 8 May 2026 13:33:23 +0200 Subject: [PATCH 5/9] improve design --- src/pages/buy.tsx | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/pages/buy.tsx b/src/pages/buy.tsx index f6f9126..30d9d0e 100644 --- a/src/pages/buy.tsx +++ b/src/pages/buy.tsx @@ -26,9 +26,9 @@ const BuyPage: NextPage = () => { const buyAction = async (itemID: string, quantity: number = 1): Promise => { try { await apiBuyOneItemMultiple.mutateAsync({ productID: itemID, quantity }) - - const message = quantity === 1 - ? "Erfolgreich gekauft!" + + const message = quantity === 1 + ? "Erfolgreich gekauft!" : `${quantity}x erfolgreich gekauft!` animate(animationRef, "success", message) await trpcUtils.user.invalidate() @@ -116,12 +116,13 @@ const BuyPage: NextPage = () => {
{/* Search Bar */} -
+
+ +
{
-
+
+ +
-
-
- {/* Results Summary */} -
- {displayedItems ? ( - - {displayedItems.length} Produkt{displayedItems.length !== 1 ? "e" : ""} gefunden - - ) : ( - Lade... - )} + +
+ {displayedItems ? ( + + {displayedItems.length} Produkt{displayedItems.length !== 1 ? "e" : ""} gefunden + + ) : ( + Lade... + )} +
@@ -209,7 +210,7 @@ const BuyPage: NextPage = () => { key={category.id} className={`btn btn-sm transition-all duration-200 ${isSelected ? "btn-primary" - : "btn-outline hover:btn-primary hover:btn-outline-primary" + : "btn-outline hover:btn-primary hover:btn-outline-primary" }`} onClick={() => { const id = category.id @@ -230,7 +231,8 @@ const BuyPage: NextPage = () => { {/* Items Grid */}
- {/* No Results Message */} {displayedItems?.length === 0 && ( + {/* No Results Message */} + {displayedItems?.length === 0 && (
From abebc99d79e759dddda4e2224835623aed87741d Mon Sep 17 00:00:00 2001 From: Louis Date: Sun, 10 May 2026 01:55:30 +0200 Subject: [PATCH 7/9] make sorting persistent in browser --- src/pages/buy.tsx | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/pages/buy.tsx b/src/pages/buy.tsx index ce65be5..b1abf1f 100644 --- a/src/pages/buy.tsx +++ b/src/pages/buy.tsx @@ -1,5 +1,5 @@ import { type NextPage } from "next" -import { useRef, useState } from "react" +import { useRef, useState, useSyncExternalStore } from "react" import { Search, Package } from "lucide-react" import ActionResponsePopup, { type AnimationHandle, @@ -11,6 +11,33 @@ import { api } from "~/utils/api" type SortMode = "recent" | "alphabetic" | "mostBought" +const SORT_MODE_STORAGE_KEY = "buyPageSortMode" +const SORT_MODE_STORAGE_EVENT = "buyPageSortModeChange" +const sortModes: SortMode[] = ["recent", "alphabetic", "mostBought"] + +const isSortMode = (value: string | null): value is SortMode => { + return sortModes.includes(value as SortMode) +} + +const getStoredSortMode = (): SortMode => { + if (typeof window === "undefined") { + return "recent" + } + + const storedSortMode = window.localStorage.getItem(SORT_MODE_STORAGE_KEY) + return isSortMode(storedSortMode) ? storedSortMode : "recent" +} + +const subscribeToSortMode = (onStoreChange: () => void) => { + window.addEventListener("storage", onStoreChange) + window.addEventListener(SORT_MODE_STORAGE_EVENT, onStoreChange) + + return () => { + window.removeEventListener("storage", onStoreChange) + window.removeEventListener(SORT_MODE_STORAGE_EVENT, onStoreChange) + } +} + const BuyPage: NextPage = () => { const allItemsRequest = api.item.getBuyable.useQuery() const allCategoriesRequest = api.category.getAllWithItems.useQuery() @@ -18,11 +45,16 @@ const BuyPage: NextPage = () => { const trpcUtils = api.useUtils() const animationRef = useRef(null) const [searchString, setSearchString] = useState("") - const [sortMode, setSortMode] = useState("recent") + const sortMode = useSyncExternalStore(subscribeToSortMode, getStoredSortMode, () => "recent") const [categoryOverrides, setCategoryOverrides] = useState<{ [index: string]: boolean }>({}) const apiBuyOneItemMultiple = api.item.buyItem.useMutation() + const handleSortModeChange = (newSortMode: SortMode) => { + window.localStorage.setItem(SORT_MODE_STORAGE_KEY, newSortMode) + window.dispatchEvent(new Event(SORT_MODE_STORAGE_EVENT)) + } + const buyAction = async (itemID: string, quantity: number = 1): Promise => { try { await apiBuyOneItemMultiple.mutateAsync({ productID: itemID, quantity }) @@ -153,7 +185,7 @@ const BuyPage: NextPage = () => {