From 4005a5bf4d411a3b23b19bea4f26a7522ac57b46 Mon Sep 17 00:00:00 2001 From: Chan Wen Xu Date: Mon, 13 Mar 2023 23:06:08 +0800 Subject: [PATCH 01/60] feat: Implement orders and products endpoint [SCSE-236] [SCSE-237] --- apps/merch/.env.example | 3 + apps/merch/package.json | 2 + apps/merch/src/db/dynamodb.ts | 43 ++ apps/merch/src/db/index.ts | 2 + apps/merch/src/db/orders.ts | 59 +++ apps/merch/src/db/products.ts | 45 +++ apps/merch/src/index.ts | 22 +- apps/merch/src/routes/orders.ts | 36 ++ apps/merch/src/routes/products.ts | 28 +- packages/types/lib/cms.ts | 14 +- packages/types/lib/merch.ts | 36 +- turbo.json | 16 +- yarn.lock | 635 ++++++++++++++++++++++++++++++ 13 files changed, 910 insertions(+), 31 deletions(-) create mode 100644 apps/merch/.env.example create mode 100644 apps/merch/src/db/dynamodb.ts create mode 100644 apps/merch/src/db/index.ts create mode 100644 apps/merch/src/db/orders.ts create mode 100644 apps/merch/src/db/products.ts create mode 100644 apps/merch/src/routes/orders.ts diff --git a/apps/merch/.env.example b/apps/merch/.env.example new file mode 100644 index 00000000..0ea4aba9 --- /dev/null +++ b/apps/merch/.env.example @@ -0,0 +1,3 @@ +AWS_REGION= +PRODUCT_TABLE_NAME= +ORDER_TABLE_NAME= diff --git a/apps/merch/package.json b/apps/merch/package.json index afe34cf1..f7dd8f6b 100644 --- a/apps/merch/package.json +++ b/apps/merch/package.json @@ -13,6 +13,8 @@ "lint:fix": "TIMING=1 eslint --fix \"**/*.ts*\"" }, "dependencies": { + "@aws-sdk/client-dynamodb": "^3.289.0", + "@aws-sdk/util-dynamodb": "^3.289.0", "express": "^4.17.1", "nodelogger": "*" }, diff --git a/apps/merch/src/db/dynamodb.ts b/apps/merch/src/db/dynamodb.ts new file mode 100644 index 00000000..832d6a80 --- /dev/null +++ b/apps/merch/src/db/dynamodb.ts @@ -0,0 +1,43 @@ +import { + DynamoDB, + GetItemCommand, + ScanCommand, +} from "@aws-sdk/client-dynamodb"; +import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; +import { Logger } from "nodelogger"; +import { Order } from "types"; + +const ORDER_TABLE_NAME = process.env.ORDER_TABLE_NAME; + +export const getOrders = () => readTable(ORDER_TABLE_NAME); +export const getOrder = (id: string) => + readItem(ORDER_TABLE_NAME, id, "orderID"); + +const client = new DynamoDB({ region: process.env.AWS_REGION }); + +// readTable scans the table for all entries, refusing to read more when the +// queried data exceeds 1MB. +export const readTable = async (tableName: string): Promise => { + const command = new ScanCommand({ TableName: tableName }); + const response = await client.send(command); + if (response.LastEvaluatedKey) { + Logger.warn( + `LastEvaluatedKey was not undefined when querying ${tableName}, dropping additional items` + ); + } + return response.Items.map((item) => unmarshall(item)); +}; + +// readItem retrieves the specified item from the table. +export const readItem = async ( + tableName: string, + key: string, + keyID = "id" +): Promise => { + const command = new GetItemCommand({ + TableName: tableName, + Key: marshall({ [keyID]: key }), + }); + const response = await client.send(command); + return unmarshall(response.Item); +}; diff --git a/apps/merch/src/db/index.ts b/apps/merch/src/db/index.ts new file mode 100644 index 00000000..c2df39f8 --- /dev/null +++ b/apps/merch/src/db/index.ts @@ -0,0 +1,2 @@ +export * from "./orders"; +export * from "./products"; diff --git a/apps/merch/src/db/orders.ts b/apps/merch/src/db/orders.ts new file mode 100644 index 00000000..18488984 --- /dev/null +++ b/apps/merch/src/db/orders.ts @@ -0,0 +1,59 @@ +import { readItem } from "./dynamodb"; +import { Order, OrderStatus } from "types"; + +const ORDER_TABLE_NAME = process.env.ORDER_TABLE_NAME; + +interface DynamoOrder { + orderID: string; + paymentGateway: string; + orderItems: { + image: string; + quantity: number; + size: string; + price: number; + name: string; + colorway: string; + id: string; + product_category: string; + }[]; + status: OrderStatus; + customerEmail: string; + transactionID: string; + orderDateTime: string; +} + +export const getOrder = async (id: string) => { + const dynamoOrder = await readItem( + ORDER_TABLE_NAME, + id, + "orderID" + ); + return decodeOrder(dynamoOrder); +}; + +const decodeOrder = (order: DynamoOrder): Order => { + let date: ?string; + try { + date = new Date(order.orderDateTime).toISOString(); + } catch (e) { + date = null; + } + return { + id: order.orderID || "", + payment_method: order.paymentGateway || "", + items: order.orderItems.map((item) => ({ + id: item.id || "", + name: item.name || "", + category: item.product_category || "", + image: item.image || null, + color: item.colorway || "", + size: item.size || "", + price: item.price || 0, + quantity: item.quantity || 1, + })), + status: order.status || PENDING_PAYMENT, + customer_email: order.customerEmail || "", + transaction_id: order.transactionID || "", + transaction_time: date, + }; +}; diff --git a/apps/merch/src/db/products.ts b/apps/merch/src/db/products.ts new file mode 100644 index 00000000..b392817e --- /dev/null +++ b/apps/merch/src/db/products.ts @@ -0,0 +1,45 @@ +import { readItem, readTable } from "./dynamodb"; +import { Product } from "types"; + +const PRODUCT_TABLE_NAME = process.env.PRODUCT_TABLE_NAME; + +export const getProducts = async () => { + const dynamoProducts = await readTable(PRODUCT_TABLE_NAME); + return dynamoProducts.map(decodeProduct); +}; +export const getProduct = async (id: string) => { + const dynamoProduct = await readItem(PRODUCT_TABLE_NAME, id); + return decodeProduct(dynamoProduct); +}; + +interface DynamoProduct { + id: string; + name: string; + price: number; + product_category: string; + size_chart?: string; + images: string[]; + colorways: string[]; + is_available: boolean; + sizes: string[]; + stock: { + [color: string]: { + [size: string]: number; + }; + }; +} + +const decodeProduct = (product: DynamoProduct): Product => { + return { + id: product.id || "", + name: product.name || "", + price: product.price || 0, + category: product.product_category || "", + size_chart: product.size_chart || null, + images: product.images || [], + colors: product.colorways || {}, + is_available: product.is_available || false, + sizes: product.sizes || [], + stock: product.stock || {}, + }; +}; diff --git a/apps/merch/src/index.ts b/apps/merch/src/index.ts index 4d237145..908a3d57 100644 --- a/apps/merch/src/index.ts +++ b/apps/merch/src/index.ts @@ -1,11 +1,12 @@ -import express from "express" -import path from 'path' -import cookieParser from 'cookie-parser' +import express from "express"; +import path from "path"; +import cookieParser from "cookie-parser"; import { nodeloggerMiddleware, Logger } from "nodelogger"; // import routers -import indexRouter from './routes/index' -// import usersRouter from './routes/users' +import indexRouter from "./routes/index"; +import ordersRouter from "./routes/orders"; +import productsRouter from "./routes/products"; const app = express(); @@ -14,11 +15,12 @@ app.use(nodeloggerMiddleware); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); -app.use(express.static(path.join(__dirname, 'public'))); +app.use(express.static(path.join(__dirname, "public"))); -app.use('/', indexRouter); -// app.use('/users', usersRouter); +app.get("/", indexRouter); +app.use("/orders", ordersRouter); +app.use("/products", productsRouter); -app.listen("3000", ()=> Logger.info("server started on port 3000")) +app.listen("3000", () => Logger.info("server started on port 3000")); -export default app +export default app; diff --git a/apps/merch/src/routes/orders.ts b/apps/merch/src/routes/orders.ts new file mode 100644 index 00000000..b1819766 --- /dev/null +++ b/apps/merch/src/routes/orders.ts @@ -0,0 +1,36 @@ +import { Router } from "express"; +import { getOrder } from "../db"; +import { Order } from "types"; + +const router = Router(); + +router.get("/:id", (req, res) => { + getOrder(req.params.id) + .then((order: Order) => { + res.json(censorDetails(order)); + }) + .catch((e) => { + console.warn(e); + res.status(500).json({ error: "INTERNAL_SERVER_ERROR" }); + }); +}); + +const censorDetails = (order: Order): Order => { + const censored = { ...order }; + const customerEmail = order.customer_email.split("@"); + censored.customer_email = + starCensor(customerEmail[0]) + "@" + customerEmail.slice(1).join("@"); + if (censored.transaction_id.length > 3) { + censored.transaction_id = starCensor(censored.transaction_id); + } + return censored; +}; + +const starCensor = (text: string, lettersToKeep = 3): string => { + if (text.length < lettersToKeep) { + return text; + } + return text.substring(0, lettersToKeep) + "*".repeat(text.length - 3); +}; + +export default router; diff --git a/apps/merch/src/routes/products.ts b/apps/merch/src/routes/products.ts index 28e548db..5b064762 100644 --- a/apps/merch/src/routes/products.ts +++ b/apps/merch/src/routes/products.ts @@ -1,17 +1,29 @@ import { Router } from "express"; +import { getProduct, getProducts } from "../db"; import { Product } from "types"; const router = Router(); -const products: Product[] = [ - { - id: "1", - name: "Sweater", - }, -]; +router.get("/", (req, res) => { + getProducts() + .then((products: Product[]) => { + res.json({ products }); + }) + .catch((e) => { + console.warn(e); + res.status(500).json({ error: "INTERNAL_SERVER_ERROR" }); + }); +}); -router.get("/products", (req, res) => { - res.json({ products }); +router.get("/:id", (req, res) => { + getProduct(req.params.id) + .then((product: Product) => { + res.json(product); + }) + .catch((e) => { + console.warn(e); + res.status(500).json({ error: "INTERNAL_SERVER_ERROR" }); + }); }); export default router; diff --git a/packages/types/lib/cms.ts b/packages/types/lib/cms.ts index 697249a4..48a847d2 100644 --- a/packages/types/lib/cms.ts +++ b/packages/types/lib/cms.ts @@ -24,7 +24,7 @@ export interface Post { title?: string; category?: string | Category; tags?: string[] | Tag[]; - layout?: ( + layout: ( | { columns: { width: 'oneThird' | 'half' | 'twoThirds' | 'full'; @@ -53,9 +53,9 @@ export interface Post { status?: 'draft' | 'published'; author?: string | User; publishedDate?: string; - updatedAt: string; - createdAt: string; _status?: 'draft' | 'published'; + createdAt: string; + updatedAt: string; } export interface Tag { id: string; @@ -64,24 +64,24 @@ export interface Tag { export interface Media { id: string; alt?: string; - updatedAt: string; - createdAt: string; url?: string; filename?: string; mimeType?: string; filesize?: number; width?: number; height?: number; + createdAt: string; + updatedAt: string; } export interface User { id: string; name?: string; - updatedAt: string; - createdAt: string; email?: string; resetPasswordToken?: string; resetPasswordExpiration?: string; loginAttempts?: number; lockUntil?: string; + createdAt: string; + updatedAt: string; password?: string; } diff --git a/packages/types/lib/merch.ts b/packages/types/lib/merch.ts index 5644ab81..376cbaa0 100644 --- a/packages/types/lib/merch.ts +++ b/packages/types/lib/merch.ts @@ -1,11 +1,43 @@ export interface Product { id: string; name: string; - // todo + colors: string[]; + sizes: string[]; + images: string[]; + is_available: boolean; + price: number; + category: string; + size_chart?: string; + stock: { + [color: string]: { + [size: string]: number; + }; + }; +} + +export enum OrderStatus { + PENDING_PAYMENT = 1, + PAYMENT_COMPLETED = 2, + ORDER_COMPLETED = 3, } export interface Order { - // todo + id: string; + items: { + id: string; + name: string; + category: string; + image?: string; + color: string; + size: string; + price: string; + quantity: number; + }[]; + transaction_id: string; + transaction_time?: string; + payment_method: string; + customer_email: string; + status: OrderStatus; } export interface Promotion { diff --git a/turbo.json b/turbo.json index 19c3d510..a25ac8bd 100644 --- a/turbo.json +++ b/turbo.json @@ -14,7 +14,9 @@ "S3_SECRET_ACCESS_KEY", "AWS_REGION", "S3_BUCKET", - "FRONTEND_STAGING_DOMAIN" + "FRONTEND_STAGING_DOMAIN", + "PRODUCT_TABLE_NAME", + "ORDER_TABLE_NAME" ] }, "build": { @@ -30,7 +32,9 @@ "S3_SECRET_ACCESS_KEY", "AWS_REGION", "S3_BUCKET", - "FRONTEND_STAGING_DOMAIN" + "FRONTEND_STAGING_DOMAIN", + "PRODUCT_TABLE_NAME", + "ORDER_TABLE_NAME" ], "outputs": ["dist/**", "build/**", "out/**", ".next/**"] }, @@ -50,7 +54,9 @@ "S3_SECRET_ACCESS_KEY", "AWS_REGION", "S3_BUCKET", - "FRONTEND_STAGING_DOMAIN" + "FRONTEND_STAGING_DOMAIN", + "PRODUCT_TABLE_NAME", + "ORDER_TABLE_NAME" ] }, "serve": { @@ -65,7 +71,9 @@ "S3_SECRET_ACCESS_KEY", "AWS_REGION", "S3_BUCKET", - "FRONTEND_STAGING_DOMAIN" + "FRONTEND_STAGING_DOMAIN", + "PRODUCT_TABLE_NAME", + "ORDER_TABLE_NAME" ] }, "lint": { diff --git a/yarn.lock b/yarn.lock index e4f90aad..8c0f80b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -115,6 +115,14 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/abort-controller@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/abort-controller/-/abort-controller-3.289.0.tgz#94278f94c66ea48b0a2da70256abc036c85de6a9" + integrity sha512-Xakz8EeTl0Q3KaWRdCaRQrrYxBAkQGj6eeT+DVmMLMz4gzTcSHwvfR5tVBIPHk4+IjboJJKM5l1xAZ90AGFPAQ== + dependencies: + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/chunked-blob-reader-native@3.208.0": version "3.208.0" resolved "https://registry.yarnpkg.com/@aws-sdk/chunked-blob-reader-native/-/chunked-blob-reader-native-3.208.0.tgz#cdbd12c89a4f3ddd91bf707da8bb4af311487cc5" @@ -130,6 +138,50 @@ dependencies: tslib "^2.3.1" +"@aws-sdk/client-dynamodb@^3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-dynamodb/-/client-dynamodb-3.289.0.tgz#9e86f7bade3711456e0a3283c61afc7a4ec364ef" + integrity sha512-gsHmHotJeZJNDuhKRn1bKGYgFYqWQYPw4MNpZ136TUNwbQdAKnywVjqJaX0H0zCndJN42l5e2xOCWVSzIAM2fg== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sts" "3.289.0" + "@aws-sdk/config-resolver" "3.289.0" + "@aws-sdk/credential-provider-node" "3.289.0" + "@aws-sdk/fetch-http-handler" "3.289.0" + "@aws-sdk/hash-node" "3.289.0" + "@aws-sdk/invalid-dependency" "3.289.0" + "@aws-sdk/middleware-content-length" "3.289.0" + "@aws-sdk/middleware-endpoint" "3.289.0" + "@aws-sdk/middleware-endpoint-discovery" "3.289.0" + "@aws-sdk/middleware-host-header" "3.289.0" + "@aws-sdk/middleware-logger" "3.289.0" + "@aws-sdk/middleware-recursion-detection" "3.289.0" + "@aws-sdk/middleware-retry" "3.289.0" + "@aws-sdk/middleware-serde" "3.289.0" + "@aws-sdk/middleware-signing" "3.289.0" + "@aws-sdk/middleware-stack" "3.289.0" + "@aws-sdk/middleware-user-agent" "3.289.0" + "@aws-sdk/node-config-provider" "3.289.0" + "@aws-sdk/node-http-handler" "3.289.0" + "@aws-sdk/protocol-http" "3.289.0" + "@aws-sdk/smithy-client" "3.289.0" + "@aws-sdk/types" "3.289.0" + "@aws-sdk/url-parser" "3.289.0" + "@aws-sdk/util-base64" "3.208.0" + "@aws-sdk/util-body-length-browser" "3.188.0" + "@aws-sdk/util-body-length-node" "3.208.0" + "@aws-sdk/util-defaults-mode-browser" "3.289.0" + "@aws-sdk/util-defaults-mode-node" "3.289.0" + "@aws-sdk/util-endpoints" "3.289.0" + "@aws-sdk/util-retry" "3.289.0" + "@aws-sdk/util-user-agent-browser" "3.289.0" + "@aws-sdk/util-user-agent-node" "3.289.0" + "@aws-sdk/util-utf8" "3.254.0" + "@aws-sdk/util-waiter" "3.289.0" + tslib "^2.3.1" + uuid "^8.3.2" + "@aws-sdk/client-s3@^3.262.0": version "3.262.0" resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.262.0.tgz#3cf6d150526a37862d23d951bd564ba78ef41d92" @@ -228,6 +280,44 @@ "@aws-sdk/util-utf8" "3.254.0" tslib "^2.3.1" +"@aws-sdk/client-sso-oidc@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.289.0.tgz#dd1945682685b05c7b8700593f95c8cb9788fe08" + integrity sha512-+09EK4aWdNjF+5+nK6Dmlwx3es8NTkyABTOj9H4eKB90rXQVX8PjoaFhK/b+NcNKDxgb1E6k6evZEpAb8dYQHg== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/config-resolver" "3.289.0" + "@aws-sdk/fetch-http-handler" "3.289.0" + "@aws-sdk/hash-node" "3.289.0" + "@aws-sdk/invalid-dependency" "3.289.0" + "@aws-sdk/middleware-content-length" "3.289.0" + "@aws-sdk/middleware-endpoint" "3.289.0" + "@aws-sdk/middleware-host-header" "3.289.0" + "@aws-sdk/middleware-logger" "3.289.0" + "@aws-sdk/middleware-recursion-detection" "3.289.0" + "@aws-sdk/middleware-retry" "3.289.0" + "@aws-sdk/middleware-serde" "3.289.0" + "@aws-sdk/middleware-stack" "3.289.0" + "@aws-sdk/middleware-user-agent" "3.289.0" + "@aws-sdk/node-config-provider" "3.289.0" + "@aws-sdk/node-http-handler" "3.289.0" + "@aws-sdk/protocol-http" "3.289.0" + "@aws-sdk/smithy-client" "3.289.0" + "@aws-sdk/types" "3.289.0" + "@aws-sdk/url-parser" "3.289.0" + "@aws-sdk/util-base64" "3.208.0" + "@aws-sdk/util-body-length-browser" "3.188.0" + "@aws-sdk/util-body-length-node" "3.208.0" + "@aws-sdk/util-defaults-mode-browser" "3.289.0" + "@aws-sdk/util-defaults-mode-node" "3.289.0" + "@aws-sdk/util-endpoints" "3.289.0" + "@aws-sdk/util-retry" "3.289.0" + "@aws-sdk/util-user-agent-browser" "3.289.0" + "@aws-sdk/util-user-agent-node" "3.289.0" + "@aws-sdk/util-utf8" "3.254.0" + tslib "^2.3.1" + "@aws-sdk/client-sso@3.261.0": version "3.261.0" resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.261.0.tgz#9ab7dfed385d9a18e68dc16e7dedbd9619db4f8e" @@ -266,6 +356,44 @@ "@aws-sdk/util-utf8" "3.254.0" tslib "^2.3.1" +"@aws-sdk/client-sso@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.289.0.tgz#a77f13b1de5923c0a3048e0e1548ceef09d49cab" + integrity sha512-GIpxPaEwqXC+P8wH+G4mIDnxYFJ+2SyYTrnoxb4OUH+gAkU6tybgvsv0fy+jsVD6GAWPdfU1AYk2ZjofdFiHeA== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/config-resolver" "3.289.0" + "@aws-sdk/fetch-http-handler" "3.289.0" + "@aws-sdk/hash-node" "3.289.0" + "@aws-sdk/invalid-dependency" "3.289.0" + "@aws-sdk/middleware-content-length" "3.289.0" + "@aws-sdk/middleware-endpoint" "3.289.0" + "@aws-sdk/middleware-host-header" "3.289.0" + "@aws-sdk/middleware-logger" "3.289.0" + "@aws-sdk/middleware-recursion-detection" "3.289.0" + "@aws-sdk/middleware-retry" "3.289.0" + "@aws-sdk/middleware-serde" "3.289.0" + "@aws-sdk/middleware-stack" "3.289.0" + "@aws-sdk/middleware-user-agent" "3.289.0" + "@aws-sdk/node-config-provider" "3.289.0" + "@aws-sdk/node-http-handler" "3.289.0" + "@aws-sdk/protocol-http" "3.289.0" + "@aws-sdk/smithy-client" "3.289.0" + "@aws-sdk/types" "3.289.0" + "@aws-sdk/url-parser" "3.289.0" + "@aws-sdk/util-base64" "3.208.0" + "@aws-sdk/util-body-length-browser" "3.188.0" + "@aws-sdk/util-body-length-node" "3.208.0" + "@aws-sdk/util-defaults-mode-browser" "3.289.0" + "@aws-sdk/util-defaults-mode-node" "3.289.0" + "@aws-sdk/util-endpoints" "3.289.0" + "@aws-sdk/util-retry" "3.289.0" + "@aws-sdk/util-user-agent-browser" "3.289.0" + "@aws-sdk/util-user-agent-node" "3.289.0" + "@aws-sdk/util-utf8" "3.254.0" + tslib "^2.3.1" + "@aws-sdk/client-sts@3.262.0": version "3.262.0" resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.262.0.tgz#aad4263147676877a754b909030b101c2437e7b7" @@ -308,6 +436,48 @@ fast-xml-parser "4.0.11" tslib "^2.3.1" +"@aws-sdk/client-sts@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.289.0.tgz#4da58cfd24f6a974d7e07aae57753bf084637a51" + integrity sha512-n+8zDCzk0NvCIXX3MGS8RV/+/MkJso0jkqkPOgPcS8Kf7Zbjlx8FyeGQ5LS7HjhCDk+jExH/s9h1kd3sL1pHQA== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/config-resolver" "3.289.0" + "@aws-sdk/credential-provider-node" "3.289.0" + "@aws-sdk/fetch-http-handler" "3.289.0" + "@aws-sdk/hash-node" "3.289.0" + "@aws-sdk/invalid-dependency" "3.289.0" + "@aws-sdk/middleware-content-length" "3.289.0" + "@aws-sdk/middleware-endpoint" "3.289.0" + "@aws-sdk/middleware-host-header" "3.289.0" + "@aws-sdk/middleware-logger" "3.289.0" + "@aws-sdk/middleware-recursion-detection" "3.289.0" + "@aws-sdk/middleware-retry" "3.289.0" + "@aws-sdk/middleware-sdk-sts" "3.289.0" + "@aws-sdk/middleware-serde" "3.289.0" + "@aws-sdk/middleware-signing" "3.289.0" + "@aws-sdk/middleware-stack" "3.289.0" + "@aws-sdk/middleware-user-agent" "3.289.0" + "@aws-sdk/node-config-provider" "3.289.0" + "@aws-sdk/node-http-handler" "3.289.0" + "@aws-sdk/protocol-http" "3.289.0" + "@aws-sdk/smithy-client" "3.289.0" + "@aws-sdk/types" "3.289.0" + "@aws-sdk/url-parser" "3.289.0" + "@aws-sdk/util-base64" "3.208.0" + "@aws-sdk/util-body-length-browser" "3.188.0" + "@aws-sdk/util-body-length-node" "3.208.0" + "@aws-sdk/util-defaults-mode-browser" "3.289.0" + "@aws-sdk/util-defaults-mode-node" "3.289.0" + "@aws-sdk/util-endpoints" "3.289.0" + "@aws-sdk/util-retry" "3.289.0" + "@aws-sdk/util-user-agent-browser" "3.289.0" + "@aws-sdk/util-user-agent-node" "3.289.0" + "@aws-sdk/util-utf8" "3.254.0" + fast-xml-parser "4.1.2" + tslib "^2.3.1" + "@aws-sdk/config-resolver@3.259.0": version "3.259.0" resolved "https://registry.yarnpkg.com/@aws-sdk/config-resolver/-/config-resolver-3.259.0.tgz#b2c17b681f890dbe31bc1670da41ae653a734c84" @@ -319,6 +489,17 @@ "@aws-sdk/util-middleware" "3.257.0" tslib "^2.3.1" +"@aws-sdk/config-resolver@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/config-resolver/-/config-resolver-3.289.0.tgz#a6f148afe9ba57fff5e1168c128adbda15378772" + integrity sha512-QYrBJeFJwx9wL73xMJgSTS6zY5SQh0tbZXpVlSZcNDuOufsu5zdcZZCOp0I20yGf8zxKX59u7O73OUlppkk+Wg== + dependencies: + "@aws-sdk/signature-v4" "3.289.0" + "@aws-sdk/types" "3.289.0" + "@aws-sdk/util-config-provider" "3.208.0" + "@aws-sdk/util-middleware" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/credential-provider-env@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.257.0.tgz#131d06bafa738c7f2ce2e7ee12c227ff6a414ada" @@ -328,6 +509,15 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/credential-provider-env@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.289.0.tgz#4cbf2a0cf4b8d9c9d4438782c480b7a65918a3c1" + integrity sha512-h4yNEW2ZJATKVxL0Bvz/WWXUmBr+AhsTyjUNge734306lXNG5/FM7zYp2v6dSQWt02WwBXyfkP3lr+A0n4rHyA== + dependencies: + "@aws-sdk/property-provider" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/credential-provider-imds@3.259.0": version "3.259.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.259.0.tgz#23bfa858dd4e97a6d530b9e3b0f4497ab0a0f8c7" @@ -339,6 +529,17 @@ "@aws-sdk/url-parser" "3.257.0" tslib "^2.3.1" +"@aws-sdk/credential-provider-imds@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.289.0.tgz#8cf6a5c612c8193105d5891ff5afde1fb98cdca2" + integrity sha512-SIl+iLQpDR6HA9CKTebui7NLop5GxnCkufbM3tbSqrQcPcEfYLOwXpu5gpKO2unQzRykCoyRVia1lr7Pc9Hgdg== + dependencies: + "@aws-sdk/node-config-provider" "3.289.0" + "@aws-sdk/property-provider" "3.289.0" + "@aws-sdk/types" "3.289.0" + "@aws-sdk/url-parser" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/credential-provider-ini@3.261.0": version "3.261.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.261.0.tgz#435525bd8d8ceb28ee69a628e22c8f0ee5af1dca" @@ -354,6 +555,21 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/credential-provider-ini@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.289.0.tgz#99d1a5f3a0b92ff45af450c7240e47e5988c6178" + integrity sha512-kvNUn3v4FTRRiqCOXl46v51VTGOM76j5Szcrhkk9qeFW6zt4iFodp6tQ4ynDtDxYxOvjuEfm3ii1YN5nkI1uKA== + dependencies: + "@aws-sdk/credential-provider-env" "3.289.0" + "@aws-sdk/credential-provider-imds" "3.289.0" + "@aws-sdk/credential-provider-process" "3.289.0" + "@aws-sdk/credential-provider-sso" "3.289.0" + "@aws-sdk/credential-provider-web-identity" "3.289.0" + "@aws-sdk/property-provider" "3.289.0" + "@aws-sdk/shared-ini-file-loader" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/credential-provider-node@3.261.0": version "3.261.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.261.0.tgz#af7587b7d284556626e718e6345f0f40c509237e" @@ -370,6 +586,22 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/credential-provider-node@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.289.0.tgz#bf55caf0ce120f784614c5870f4308ba257ff38c" + integrity sha512-05CYPGnk5cDiOQDIaXNVibNOwQdI34MDiL17YkSfPv779A+uq4vqg/aBfL41BDJjr1gSGgyvVhlcUdBKnlp93Q== + dependencies: + "@aws-sdk/credential-provider-env" "3.289.0" + "@aws-sdk/credential-provider-imds" "3.289.0" + "@aws-sdk/credential-provider-ini" "3.289.0" + "@aws-sdk/credential-provider-process" "3.289.0" + "@aws-sdk/credential-provider-sso" "3.289.0" + "@aws-sdk/credential-provider-web-identity" "3.289.0" + "@aws-sdk/property-provider" "3.289.0" + "@aws-sdk/shared-ini-file-loader" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/credential-provider-process@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.257.0.tgz#7fd27f48606ad7c2af375b168c8e38dc938e3162" @@ -380,6 +612,16 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/credential-provider-process@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.289.0.tgz#ef91f39541a607dde73c3df81715d8f2b176991f" + integrity sha512-t39CJHj1/f2DcRbEUSJ1ixwDsgaElDpJPynn59MOdNnrSh5bYuYmkrum/GYXYSsk+HoSK21JvwgvjnrkA9WZKQ== + dependencies: + "@aws-sdk/property-provider" "3.289.0" + "@aws-sdk/shared-ini-file-loader" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/credential-provider-sso@3.261.0": version "3.261.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.261.0.tgz#6265828dad45b1ef67c43f712ddbcfc80e2c6fab" @@ -392,6 +634,18 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/credential-provider-sso@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.289.0.tgz#64bf7e3a2f017f5988174dd6193da6e8f187b1b6" + integrity sha512-8+DjOqj5JCpVdT4EJtdfis6OioAdiDKM1mvgDTG8R43MSThc+RGfzqaDJQdM+8+hzkYhxYfyI9XB0H+X3rDNsA== + dependencies: + "@aws-sdk/client-sso" "3.289.0" + "@aws-sdk/property-provider" "3.289.0" + "@aws-sdk/shared-ini-file-loader" "3.289.0" + "@aws-sdk/token-providers" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/credential-provider-web-identity@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.257.0.tgz#928f3234818c6acbf67bf157e4a366f920285e62" @@ -401,6 +655,23 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/credential-provider-web-identity@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.289.0.tgz#14d9fa1e5f237abafc04533e34b9750565874b9a" + integrity sha512-jZ9hQvr0I7Z2DekDtZytViYn7zNNJG06N0CinAJzzvreAQ1I61rU7mhaWc05jhBSdeA3f82XoDAgxqY4xIh9pQ== + dependencies: + "@aws-sdk/property-provider" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + +"@aws-sdk/endpoint-cache@3.208.0": + version "3.208.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/endpoint-cache/-/endpoint-cache-3.208.0.tgz#bd3083c3b85985fb04ec0ab760afc7b4132318c3" + integrity sha512-MkrCvaZhTb1qZCjcDH73t5n43h0Kr0GS+30lpXZ9PAnHJZPqv+vhWFPK0ZsFe1XktbS0WOoDR4ED+lWm0Dw7Rg== + dependencies: + mnemonist "0.38.3" + tslib "^2.3.1" + "@aws-sdk/eventstream-codec@3.258.0": version "3.258.0" resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-codec/-/eventstream-codec-3.258.0.tgz#58bb97a90b36d7695603cdb7895d5ac7e4006aef" @@ -457,6 +728,17 @@ "@aws-sdk/util-base64" "3.208.0" tslib "^2.3.1" +"@aws-sdk/fetch-http-handler@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.289.0.tgz#f09712c82d865423728539e26cbee20b91021e3c" + integrity sha512-tksh2GnDV1JaI+NO9x+pgyB3VNwjnUdtoMcFGmTDm1TrcPNj0FLX2hLiunlVG7fFMfGLXC2aco0sUra5/5US9Q== + dependencies: + "@aws-sdk/protocol-http" "3.289.0" + "@aws-sdk/querystring-builder" "3.289.0" + "@aws-sdk/types" "3.289.0" + "@aws-sdk/util-base64" "3.208.0" + tslib "^2.3.1" + "@aws-sdk/hash-blob-browser@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/hash-blob-browser/-/hash-blob-browser-3.257.0.tgz#44b2d849a1340bf340d9a32f17f71f50447e7e2c" @@ -477,6 +759,16 @@ "@aws-sdk/util-utf8" "3.254.0" tslib "^2.3.1" +"@aws-sdk/hash-node@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/hash-node/-/hash-node-3.289.0.tgz#f588be8b67762823d54e7814d174c3ee76127c62" + integrity sha512-fL7Pt4LU+tluHn0+BSIFVD2ZVJ5fuXvd1hQt4aTYrgkna1RR5v55Hdy2rNrp/syrkyE+Wv92S3hgZ7ZTBeXFZA== + dependencies: + "@aws-sdk/types" "3.289.0" + "@aws-sdk/util-buffer-from" "3.208.0" + "@aws-sdk/util-utf8" "3.254.0" + tslib "^2.3.1" + "@aws-sdk/hash-stream-node@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/hash-stream-node/-/hash-stream-node-3.257.0.tgz#cc97d195118a1d81ef08972680170d306ef4a28b" @@ -494,6 +786,14 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/invalid-dependency@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/invalid-dependency/-/invalid-dependency-3.289.0.tgz#29abc018752f92485c2aa07c6e4d48f676657726" + integrity sha512-VpXadvpqXFUA8gBH6TAAJzsKfEQ4IvsiD7d9b2B+jw1YtaPFTqEEuDjN6ngpad8PCPCNWl8CI6oBCdMOK+L48A== + dependencies: + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/is-array-buffer@3.201.0": version "3.201.0" resolved "https://registry.yarnpkg.com/@aws-sdk/is-array-buffer/-/is-array-buffer-3.201.0.tgz#06e557adc284fac2f26071c2944ae01f61b95854" @@ -530,6 +830,26 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/middleware-content-length@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-content-length/-/middleware-content-length-3.289.0.tgz#86a8f77faa6dc228a030bdcc0fd35947be920f8a" + integrity sha512-D7vGeuaAzKiq0aFPwme1Xy4x69Jn4v0YJ3Xa4J+keNep0yZ9LfU5KSngqsxeTefCqS+2tdaArkBN2VdexmPagw== + dependencies: + "@aws-sdk/protocol-http" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-endpoint-discovery@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.289.0.tgz#d4d8452cac45cd8f11a07977cd9d99b5cffdab71" + integrity sha512-VcCMvgwdGeSRI3h5fLS8c8jydM/fBbDQtiBMMVFhI2YBUVnEc2UBFlp2VjqH31ihnrHO5ogW/rMnsAgAFTDMdQ== + dependencies: + "@aws-sdk/config-resolver" "3.289.0" + "@aws-sdk/endpoint-cache" "3.208.0" + "@aws-sdk/protocol-http" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/middleware-endpoint@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-endpoint/-/middleware-endpoint-3.257.0.tgz#425ee4ab43807b34957685d782c84fd418a2526f" @@ -544,6 +864,20 @@ "@aws-sdk/util-middleware" "3.257.0" tslib "^2.3.1" +"@aws-sdk/middleware-endpoint@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-endpoint/-/middleware-endpoint-3.289.0.tgz#99b6c2e064e693c62873dad62c78c9bf551128d6" + integrity sha512-nxaQFOG1IurwCHWP22RxgTFZdILsdBg6wbg4GeFpNBtE3bi0zIUYKrUhpdRr/pZyGAboD1oD9iQtxuGb/M6f+w== + dependencies: + "@aws-sdk/middleware-serde" "3.289.0" + "@aws-sdk/protocol-http" "3.289.0" + "@aws-sdk/signature-v4" "3.289.0" + "@aws-sdk/types" "3.289.0" + "@aws-sdk/url-parser" "3.289.0" + "@aws-sdk/util-config-provider" "3.208.0" + "@aws-sdk/util-middleware" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/middleware-expect-continue@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.257.0.tgz#8c8a590fcac4feef6ac56c5289d339d49b97b159" @@ -575,6 +909,15 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/middleware-host-header@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.289.0.tgz#0fe22ed1930d600844666ea80ef2f6717c52bd57" + integrity sha512-yFBOKvKBnITO08JCx+65vXPe9Uo4gZuth/ka9v5swa4wtV8AP+kkOwFrNxSi2iAFLJ4Mg21vGQceeL0bErF6KQ== + dependencies: + "@aws-sdk/protocol-http" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/middleware-location-constraint@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.257.0.tgz#487aff3008488029d7f36855429f51d383ffd29d" @@ -591,6 +934,14 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/middleware-logger@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.289.0.tgz#fd6de1ebcef0ff3fffe4f407162542bdfb9d7065" + integrity sha512-c5W7AlOdoyTXRoNl2yOVkhbTjp8tX0z65GDb3+/1yYcv+GRtz67WMZscWMQJwEfdCLdDE2GtBe+t2xyFGnmJvA== + dependencies: + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/middleware-recursion-detection@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.257.0.tgz#83512e0228b41dfc37a337d2ad064cf6dc41f8df" @@ -600,6 +951,15 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/middleware-recursion-detection@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.289.0.tgz#8d185c2bb9f80215b51f3c1700914f04c4c84fe9" + integrity sha512-r2NrfnTG0UZRXeFjoyapAake7b1rUo6SC52/UV4Pdm8cHoYMmljnaGLjiAfzt6vWv6cSVCJq1r28Ne4slAoMAg== + dependencies: + "@aws-sdk/protocol-http" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/middleware-retry@3.259.0": version "3.259.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-retry/-/middleware-retry-3.259.0.tgz#18bbb2cd655fff1ea155dfcb9eaa2b583b67e42e" @@ -613,6 +973,19 @@ tslib "^2.3.1" uuid "^8.3.2" +"@aws-sdk/middleware-retry@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-retry/-/middleware-retry-3.289.0.tgz#68534fbd94c40feb11a22b134285db9089b47336" + integrity sha512-Su+iGv5mrFjVCXJmjohX00o3HzkwnhY0TDhIltgolB6ZfOqy3Dfopjj21OWtqY9VYCUiLGC4KRfeb2feyrz5BA== + dependencies: + "@aws-sdk/protocol-http" "3.289.0" + "@aws-sdk/service-error-classification" "3.289.0" + "@aws-sdk/types" "3.289.0" + "@aws-sdk/util-middleware" "3.289.0" + "@aws-sdk/util-retry" "3.289.0" + tslib "^2.3.1" + uuid "^8.3.2" + "@aws-sdk/middleware-sdk-s3@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.257.0.tgz#fe84c1fa5fdab1cc9b0d5d83d7e4ec24f68fc0be" @@ -635,6 +1008,18 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/middleware-sdk-sts@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.289.0.tgz#aa5b8a075aea6c1af5bbb292a26d085985373ad6" + integrity sha512-9WzUVPEqJcvggGCk9JHXnwhj7fjuMXE/JM3gx7eMSStJCcK+3BARZ1RZnggUN4vN9iTSzdA+r0OpC1XnUGKB2g== + dependencies: + "@aws-sdk/middleware-signing" "3.289.0" + "@aws-sdk/property-provider" "3.289.0" + "@aws-sdk/protocol-http" "3.289.0" + "@aws-sdk/signature-v4" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/middleware-serde@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-3.257.0.tgz#13c529b942dafffcb198d9333f8f8dc2a662c187" @@ -643,6 +1028,14 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/middleware-serde@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-3.289.0.tgz#095ed906dd0c3ca9afe0bd97aeada2f64ebd30e7" + integrity sha512-pygC+LsEBVAxOzfoxA9jgvqfO1PLivh8s2Yr/aNQOwx49fmTHMvPwRYUGDV38Du6bRYcKI6nxYqkbJFkQkRESQ== + dependencies: + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/middleware-signing@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.257.0.tgz#436c9e2fbbe1342c30572028e90ac62f7e90548f" @@ -655,6 +1048,18 @@ "@aws-sdk/util-middleware" "3.257.0" tslib "^2.3.1" +"@aws-sdk/middleware-signing@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.289.0.tgz#e262bea9bc1e61d52d7fbcf81c329a07fd60e783" + integrity sha512-9SLATNvibxg4hpr4ldU18LwB6AVzovONWeJLt49FKISz7ZwGF6WVJYUMWeScj4+Z51Gozi7+pUIaFn7i6N3UbA== + dependencies: + "@aws-sdk/property-provider" "3.289.0" + "@aws-sdk/protocol-http" "3.289.0" + "@aws-sdk/signature-v4" "3.289.0" + "@aws-sdk/types" "3.289.0" + "@aws-sdk/util-middleware" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/middleware-ssec@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.257.0.tgz#21adf9b6f6d4b2ac9d337e198a419ffb3922bbf9" @@ -670,6 +1075,13 @@ dependencies: tslib "^2.3.1" +"@aws-sdk/middleware-stack@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-stack/-/middleware-stack-3.289.0.tgz#e08558014f45622783e76c2d7cf85191434101b3" + integrity sha512-3rWx+UkV//dv/cLIrXmzIa+FZcn6n76JevGHYCTReiRpcvv+xECxgXH2crMYtzbu05WdxGYD6P0IP5tMwH0yXA== + dependencies: + tslib "^2.3.1" + "@aws-sdk/middleware-user-agent@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.257.0.tgz#9ca650f5909bd9b55879835088760173a9d3d249" @@ -679,6 +1091,15 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/middleware-user-agent@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.289.0.tgz#5650f57906b0fff32f739e47425c603f987aef11" + integrity sha512-XPhB9mgko66BouyxA+7z7SjUaNHyr58Xe/OB8GII5R/JiR3A/lpc8+jm9gEEpjEI/HpF8jLFDnTMbgabVAHOeA== + dependencies: + "@aws-sdk/protocol-http" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/node-config-provider@3.259.0": version "3.259.0" resolved "https://registry.yarnpkg.com/@aws-sdk/node-config-provider/-/node-config-provider-3.259.0.tgz#0b522020c4a0e445b41f7150ce624b7b63e96e68" @@ -689,6 +1110,16 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/node-config-provider@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/node-config-provider/-/node-config-provider-3.289.0.tgz#815a760a9ed6c4e5b5cd7e8bf62fa7d7dd2fe6fb" + integrity sha512-rR41c3Y7MYEP8TG9X1whHyrXEXOZzi4blSDqeJflwtNt3r3HvErGZiNBdVv368ycPPuu1YRSqTkgOYNCv02vlw== + dependencies: + "@aws-sdk/property-provider" "3.289.0" + "@aws-sdk/shared-ini-file-loader" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/node-http-handler@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/node-http-handler/-/node-http-handler-3.257.0.tgz#33e3ba0d8b0bf72a05be6c91e6b4cf90b8a7b786" @@ -700,6 +1131,17 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/node-http-handler@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/node-http-handler/-/node-http-handler-3.289.0.tgz#b1a8fc4bce4c257e8a15d7ecdebc25bca2afafb2" + integrity sha512-zKknSaOY2GNmqH/eoZndmQWoEKhYPV0qRZtAMxuS3DVI5fipBipNzbVBaXrHRjxARx7/VLWnvNArchRoHfOlmw== + dependencies: + "@aws-sdk/abort-controller" "3.289.0" + "@aws-sdk/protocol-http" "3.289.0" + "@aws-sdk/querystring-builder" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/property-provider@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/property-provider/-/property-provider-3.257.0.tgz#dd6872ace54f8fd691a15167490ab52e40306c58" @@ -708,6 +1150,14 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/property-provider@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/property-provider/-/property-provider-3.289.0.tgz#ff95153868c94b8def757a7a8d9eeb8603a1c874" + integrity sha512-Raf4lTWPTmEGFV7Lkbfet2n/4Ybz5vQiiU45l56kgIQA88mLUuE4dshgNsM0Zb2rflsTaiN1JR2+RS/8lNtI8A== + dependencies: + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/protocol-http@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/protocol-http/-/protocol-http-3.257.0.tgz#1452ce4f6a51e24297cc39f73aa889570dddd348" @@ -716,6 +1166,14 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/protocol-http@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/protocol-http/-/protocol-http-3.289.0.tgz#ebaa84ebd9ac1129459082c0990cd37d5355f2b1" + integrity sha512-/2jOQ3MJZx1xk6BHEOW47ItGo1tgA9cP9a2saYneon05VIV6OuYefO5pG2G0nPnImTbff++N7aioXe5XKrnorw== + dependencies: + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/querystring-builder@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-builder/-/querystring-builder-3.257.0.tgz#75e662fc451cf59763bdee52ba64b05e5cd2de0a" @@ -725,6 +1183,15 @@ "@aws-sdk/util-uri-escape" "3.201.0" tslib "^2.3.1" +"@aws-sdk/querystring-builder@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-builder/-/querystring-builder-3.289.0.tgz#75ddef075862746cbf6d92f71bb8715cedeef61f" + integrity sha512-llJCS8mAJfBYBjkKeriRmBuDr2jIozrMWhJOkz95SQGFsx1sKBPQMMOV6zunwhQux8bjtjf5wYiR1TM2jNUKqQ== + dependencies: + "@aws-sdk/types" "3.289.0" + "@aws-sdk/util-uri-escape" "3.201.0" + tslib "^2.3.1" + "@aws-sdk/querystring-parser@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-parser/-/querystring-parser-3.257.0.tgz#c8614e424d7d840c01be919161f61ef85eca46af" @@ -733,11 +1200,24 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/querystring-parser@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-parser/-/querystring-parser-3.289.0.tgz#0aa11faa53203a1cfc30d3e0c48d70284f378ec2" + integrity sha512-84zXKXIYtnTCrez/gGZIGuqfUJezzaOMm7BQwnOnq/sN21ou63jF3Q+tIMhLO/EvDcvmxEOlUXN1kfMQcjEjSw== + dependencies: + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/service-error-classification@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/service-error-classification/-/service-error-classification-3.257.0.tgz#a374e811ac587b9beb6e3fda77f2249570da7a8e" integrity sha512-FAyR0XsueGkkqDtkP03cTJQk52NdQ9sZelLynmmlGPUP75LApRPvFe1riKrou6+LsDbwVNVffj6mbDfIcOhaOw== +"@aws-sdk/service-error-classification@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/service-error-classification/-/service-error-classification-3.289.0.tgz#25c513c099126414ed2e8489290b3a4f0e0f2c4b" + integrity sha512-+d1Vlb45Bs2gbTmXpRCGQrX4AQDETjA5sx1zLvq1NZGSnTX6LdroYPtXu3dRWJwDHHQpCMN/XfFN8jTw0IzBOg== + "@aws-sdk/shared-ini-file-loader@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.257.0.tgz#513eee5c7ffa343bf5d91bdd73870fc5c47a4ad3" @@ -746,6 +1226,14 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/shared-ini-file-loader@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.289.0.tgz#ac3eb207374bef778638c75cc65233d9d9a64dae" + integrity sha512-XG9Pfn3itf3Z0p6nY6UuMVMhzZb+oX7L28oyby8REl8BAwfPkcziLxXlZsBHf6KcgYDG1R6z945hvIwZhJbjvA== + dependencies: + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/signature-v4-multi-region@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.257.0.tgz#20169126bcf95a40bb6608d5522121bee93eb55e" @@ -770,6 +1258,19 @@ "@aws-sdk/util-utf8" "3.254.0" tslib "^2.3.1" +"@aws-sdk/signature-v4@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4/-/signature-v4-3.289.0.tgz#be2f2533a52e13733e7ae88fbf083ec6357cc47a" + integrity sha512-IQyYHx3zp7PHxFA17YDb6WVx8ejXDxrsnKspFXgZQyoZOPfReqWQs32dcJYXff/IdSzxjwOpwBFbmIt2vbdKnQ== + dependencies: + "@aws-sdk/is-array-buffer" "3.201.0" + "@aws-sdk/types" "3.289.0" + "@aws-sdk/util-hex-encoding" "3.201.0" + "@aws-sdk/util-middleware" "3.289.0" + "@aws-sdk/util-uri-escape" "3.201.0" + "@aws-sdk/util-utf8" "3.254.0" + tslib "^2.3.1" + "@aws-sdk/smithy-client@3.261.0": version "3.261.0" resolved "https://registry.yarnpkg.com/@aws-sdk/smithy-client/-/smithy-client-3.261.0.tgz#538096a39198cf41fa8002467536e5af1958c518" @@ -779,6 +1280,15 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/smithy-client@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/smithy-client/-/smithy-client-3.289.0.tgz#d9ff0e50cb311662c8e4029791cfef2220d00b0a" + integrity sha512-miPMdnv4Ivv8RN65LJ9dxzkQNHn9Tp9wzZJXwBcPqGdXyRlkWSuIOIIhhAqQoV9R9ByeshnCWBpwqlITIjNPVw== + dependencies: + "@aws-sdk/middleware-stack" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/token-providers@3.261.0": version "3.261.0" resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.261.0.tgz#29144d2f3a6f15737cde69eb794e95d7ab76558f" @@ -790,6 +1300,17 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/token-providers@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.289.0.tgz#ecfaa3462966c77508a64b3498319e2bfcbc4476" + integrity sha512-fzvGIfJNoLR5g24ok8cRwc9AMLXoEOyfi+eHocAF6eyfe0NWlQtpsmLe7XXx5I9yZ51lclzV49rEz9ynp243RA== + dependencies: + "@aws-sdk/client-sso-oidc" "3.289.0" + "@aws-sdk/property-provider" "3.289.0" + "@aws-sdk/shared-ini-file-loader" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/types@3.257.0", "@aws-sdk/types@^3.222.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.257.0.tgz#4951ee3456cd9a46829516f5596c2b8a05ffe06a" @@ -797,6 +1318,13 @@ dependencies: tslib "^2.3.1" +"@aws-sdk/types@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.289.0.tgz#c1042bcefa21e90e754ba665094599fa8a7f35f8" + integrity sha512-wwUC+VwoNlEkgDzK/aJG3+zeMcYRcYFQV4mbZaicYdp3v8hmkUkJUhyxuZYl/FmY46WG+DYv+/Y3NilgfsE+Wg== + dependencies: + tslib "^2.3.1" + "@aws-sdk/url-parser@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser/-/url-parser-3.257.0.tgz#99b1abb302426f1b24c9777789fb0479d52d675d" @@ -806,6 +1334,15 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/url-parser@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser/-/url-parser-3.289.0.tgz#d2320e6174cc883abe2f03a27dcf918c40e0c5f0" + integrity sha512-rbtW3O6UBX+eWR/+UiCDNFUVwN8hp82JPy+NGv3NeOvRjBsxkKmcH4UJTHDIeT+suqTDNEdV5nz438u3dHdHrQ== + dependencies: + "@aws-sdk/querystring-parser" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/util-arn-parser@3.208.0": version "3.208.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.208.0.tgz#56b6ae4699c3140bb27dcede5146876fef04e823" @@ -860,6 +1397,16 @@ bowser "^2.11.0" tslib "^2.3.1" +"@aws-sdk/util-defaults-mode-browser@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.289.0.tgz#8f1f5e6926e18ba6f8a6c22d237e82649aca650c" + integrity sha512-sYrDwjX3s54cvGq69PJpP2vDpJ5BJXhg2KEHbK92Qr2AUqMUgidwZCw4oBaIqKDXcPIrjmhod31s3tTfYmtTMQ== + dependencies: + "@aws-sdk/property-provider" "3.289.0" + "@aws-sdk/types" "3.289.0" + bowser "^2.11.0" + tslib "^2.3.1" + "@aws-sdk/util-defaults-mode-node@3.261.0": version "3.261.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.261.0.tgz#a7c09e3912a0f23e42b5c183d2a297b632014f9f" @@ -872,6 +1419,25 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/util-defaults-mode-node@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.289.0.tgz#1badaf2383f5de055e9a23fce5151b9eb31f94a2" + integrity sha512-PsP40+9peN7kpEmQ2GhEAGwUwD9F/R/BI/1kzjW0nbBsMrTnkUnlZlaitwpBX/OWNV/YZTdVAOvD50j/ACyXlg== + dependencies: + "@aws-sdk/config-resolver" "3.289.0" + "@aws-sdk/credential-provider-imds" "3.289.0" + "@aws-sdk/node-config-provider" "3.289.0" + "@aws-sdk/property-provider" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + +"@aws-sdk/util-dynamodb@^3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-dynamodb/-/util-dynamodb-3.289.0.tgz#15637e95373b6b654407c4a22b9ae653acb36c41" + integrity sha512-U6PnoyXrBXLY6uSnjuRY4+Xgau41RoPakkzMsKiwd1Gl/XWs9pIoi5ounChSNVxhHbJ4IIQKSQ5ALeVMhWvUmQ== + dependencies: + tslib "^2.3.1" + "@aws-sdk/util-endpoints@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.257.0.tgz#40cc8f67b996f8ea173f43d0e58e57ca8c244e67" @@ -880,6 +1446,14 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/util-endpoints@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.289.0.tgz#556add88acaa0e77c2c8c356c876ea215ac60211" + integrity sha512-PmsgqL9jdNTz3p0eW83nZZGcngAdoIWidXCc32G5tIIYvJutdgkiObAaydtXaMgk5CRvjenngFf6Zg9JyVHOLQ== + dependencies: + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/util-hex-encoding@3.201.0": version "3.201.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.201.0.tgz#21d7ec319240ee68c33d938e71cb79830bea315d" @@ -901,6 +1475,13 @@ dependencies: tslib "^2.3.1" +"@aws-sdk/util-middleware@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-middleware/-/util-middleware-3.289.0.tgz#b8f2c9a08c23ed064054a19640d5a1c1911cefce" + integrity sha512-hw3WHQU9Wk7a1H3x+JhwMA4ECCleeuNlob3fXSYJmXgvZyuWfpMYZi4iSkqoWGFAXYpAtZZLIu45iIcd7F296g== + dependencies: + tslib "^2.3.1" + "@aws-sdk/util-retry@3.257.0": version "3.257.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-retry/-/util-retry-3.257.0.tgz#20454375267e120576c9f24316dad0ebc489dc4b" @@ -909,6 +1490,14 @@ "@aws-sdk/service-error-classification" "3.257.0" tslib "^2.3.1" +"@aws-sdk/util-retry@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-retry/-/util-retry-3.289.0.tgz#fb800797cf9908a8346311bc00dbb5c032e702e4" + integrity sha512-noFn++ZKH11ExTBqUU/b9wsOjqxYlDnN/8xq+9oCsyBnEZztVgM/AM3WP5qBPRskk1WzDprID5fb5V87113Uug== + dependencies: + "@aws-sdk/service-error-classification" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/util-stream-browser@3.258.0": version "3.258.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-stream-browser/-/util-stream-browser-3.258.0.tgz#a2f8b0dc3a82e617bd8a2b4c948b92ef84fcb5fb" @@ -947,6 +1536,15 @@ bowser "^2.11.0" tslib "^2.3.1" +"@aws-sdk/util-user-agent-browser@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.289.0.tgz#90dfb622d3f707d8cde9fb25c4bd548930821657" + integrity sha512-BDXYgNzzz2iNPTkl9MQf7pT4G80V6O6ICwJyH93a5EEdljl7oPrt8i4MS5S0BDAWx58LqjWtVw98GOZfy5BYhw== + dependencies: + "@aws-sdk/types" "3.289.0" + bowser "^2.11.0" + tslib "^2.3.1" + "@aws-sdk/util-user-agent-node@3.259.0": version "3.259.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.259.0.tgz#61141a0d64668ebcbbb1ac3dac1f497ca9f3707e" @@ -956,6 +1554,15 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/util-user-agent-node@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.289.0.tgz#048f86cf5dd4822f703aaac5f6e5adbf6cf6175f" + integrity sha512-f32g9KS7pwO6FQ9N1CtqQPIS6jhvwv/y0+NHNoo9zLTBH0jol3+C2ELIE3N1wB6xvwhsdPqR3WuOiNiCiv8YAQ== + dependencies: + "@aws-sdk/node-config-provider" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/util-utf8-browser@^3.0.0": version "3.259.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz#3275a6f5eb334f96ca76635b961d3c50259fd9ff" @@ -980,6 +1587,15 @@ "@aws-sdk/types" "3.257.0" tslib "^2.3.1" +"@aws-sdk/util-waiter@3.289.0": + version "3.289.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-waiter/-/util-waiter-3.289.0.tgz#c638563dd99fb677af3053fc6520d98a3a046ad5" + integrity sha512-HyTEJR8cVor9FS48I2ArMLAs7LJLz6Rkb/0dvudVw84zjNofRgoYQLoZFJHSsiUzVLd7jaaxidC9FKK3lqGz1g== + dependencies: + "@aws-sdk/abort-controller" "3.289.0" + "@aws-sdk/types" "3.289.0" + tslib "^2.3.1" + "@aws-sdk/xml-builder@3.201.0": version "3.201.0" resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.201.0.tgz#acf0869855460528114bec17f290b224fe19a3e2" @@ -9781,6 +10397,13 @@ fast-xml-parser@4.0.11: dependencies: strnum "^1.0.5" +fast-xml-parser@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.1.2.tgz#5a98c18238d28a57bbdfa9fe4cda01211fff8f4a" + integrity sha512-CDYeykkle1LiA/uqQyNwYpFbyF6Axec6YapmpUP+/RHWIoR1zKjocdvNaTsxCxZzQ6v9MLXaSYm9Qq0thv0DHg== + dependencies: + strnum "^1.0.5" + fastest-levenshtein@^1.0.12: version "1.0.16" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" @@ -12573,6 +13196,13 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mnemonist@0.38.3: + version "0.38.3" + resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.38.3.tgz#35ec79c1c1f4357cfda2fe264659c2775ccd7d9d" + integrity sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw== + dependencies: + obliterator "^1.6.1" + monaco-editor@^0.38.0: version "0.38.0" resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.38.0.tgz#7b3cd16f89b1b8867fcd3c96e67fccee791ff05c" @@ -13009,6 +13639,11 @@ objectorarray@^1.0.5: resolved "https://registry.yarnpkg.com/objectorarray/-/objectorarray-1.0.5.tgz#2c05248bbefabd8f43ad13b41085951aac5e68a5" integrity sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg== +obliterator@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-1.6.1.tgz#dea03e8ab821f6c4d96a299e17aef6a3af994ef3" + integrity sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig== + on-exit-leak-free@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4" From 6cb831d25a00cd53c762c23cb9ab5475d6e0b54e Mon Sep 17 00:00:00 2001 From: Guo Yong <97776029+YoNG-Zaii@users.noreply.github.com> Date: Tue, 21 Mar 2023 18:00:58 +0800 Subject: [PATCH 02/60] task: Port home page (product listing) (#77) --- .../features/layout/components/WebLayout.tsx | 5 +- .../web/features/merch/constants/queryKeys.ts | 9 ++ apps/web/features/merch/constants/routes.ts | 17 +++ apps/web/features/merch/functions/currency.ts | 7 + apps/web/features/merch/functions/stock.ts | 81 +++++++++++ apps/web/features/merch/services/api.tsx | 117 +++++++++++++++ apps/web/package.json | 2 + apps/web/pages/_app.tsx | 17 ++- apps/web/pages/merch/index.tsx | 82 +++++++++++ packages/ui/components/merch/Card.tsx | 69 +++++++++ packages/ui/components/merch/CartHeader.tsx | 47 +++++++ packages/ui/components/merch/Page.tsx | 37 +++++ packages/ui/components/merch/Skeleton.tsx | 18 +++ turbo.json | 12 +- yarn.lock | 133 +++++++++++++++++- 15 files changed, 641 insertions(+), 12 deletions(-) create mode 100644 apps/web/features/merch/constants/queryKeys.ts create mode 100644 apps/web/features/merch/constants/routes.ts create mode 100644 apps/web/features/merch/functions/currency.ts create mode 100644 apps/web/features/merch/functions/stock.ts create mode 100644 apps/web/features/merch/services/api.tsx create mode 100644 apps/web/pages/merch/index.tsx create mode 100644 packages/ui/components/merch/Card.tsx create mode 100644 packages/ui/components/merch/CartHeader.tsx create mode 100644 packages/ui/components/merch/Page.tsx create mode 100644 packages/ui/components/merch/Skeleton.tsx diff --git a/apps/web/features/layout/components/WebLayout.tsx b/apps/web/features/layout/components/WebLayout.tsx index e4ae582f..507c4284 100644 --- a/apps/web/features/layout/components/WebLayout.tsx +++ b/apps/web/features/layout/components/WebLayout.tsx @@ -3,7 +3,7 @@ import { FooterProps, Layout, NavBarProps } from "ui"; import { FaGithub, FaInstagram, FaLinkedin } from "react-icons/fa"; interface WebLayoutProps { - children: React.ReactNode + children: React.ReactNode; } export const WebLayout = ({ children }: WebLayoutProps) => { @@ -15,6 +15,7 @@ export const WebLayout = ({ children }: WebLayoutProps) => { { label: "Academics", href: "/academics" }, { label: "Learn", href: "/learn" }, { label: "Sponsors", href: "/sponsors" }, + { label: "Merch", href: "/merch" }, ], logoProps: { src: "/scse-logo/scse-logo-blue.png", @@ -61,5 +62,5 @@ export const WebLayout = ({ children }: WebLayoutProps) => { {children} - ) + ); }; diff --git a/apps/web/features/merch/constants/queryKeys.ts b/apps/web/features/merch/constants/queryKeys.ts new file mode 100644 index 00000000..030a6f70 --- /dev/null +++ b/apps/web/features/merch/constants/queryKeys.ts @@ -0,0 +1,9 @@ +export enum QueryKeys { + PRODUCTS = "PRODUCTS", + PRODUCT = "PRODUCT", + VOUCHER = "VOUCHER", + ORDER = "ORDER", + ORDERS = "ORDERS", + EMAIL = "EMAIL", + CHECKOUT = "CHECKOUT", +} diff --git a/apps/web/features/merch/constants/routes.ts b/apps/web/features/merch/constants/routes.ts new file mode 100644 index 00000000..9128a8fc --- /dev/null +++ b/apps/web/features/merch/constants/routes.ts @@ -0,0 +1,17 @@ +type Routes = { + HOME: string; + PRODUCT: string; + CART: string; + CHECKOUT: string; + ORDER_SUMMARY: string; +}; + +export const routes: Routes = { + HOME: "/merch", + PRODUCT: "/merch/product", + CART: "/merch/cart", + CHECKOUT: "/merch/checkout", + ORDER_SUMMARY: "/merch/order-summary", +}; + +export default routes; diff --git a/apps/web/features/merch/functions/currency.ts b/apps/web/features/merch/functions/currency.ts new file mode 100644 index 00000000..3b3fed5c --- /dev/null +++ b/apps/web/features/merch/functions/currency.ts @@ -0,0 +1,7 @@ +export const displayPrice = (amountInCents: number): string => { + const amountStr = amountInCents.toString() + const dollarStr = amountStr.slice(0, -2).padStart(1, '0') + const centStr = amountStr.slice(-2).padStart(2, '0') + const priceStr = dollarStr.concat(".").concat(centStr) + return `$${priceStr}` +} diff --git a/apps/web/features/merch/functions/stock.ts b/apps/web/features/merch/functions/stock.ts new file mode 100644 index 00000000..290c7028 --- /dev/null +++ b/apps/web/features/merch/functions/stock.ts @@ -0,0 +1,81 @@ +import { Product } from "types/lib/merch" + +/* +export const getQtyInStock = (product: ProductType, colorway: string, size: string): number => { + // returns remaining stock for specified colorway and size + if (product.stock[colorway] && product.stock[colorway][size]) { + return product.stock[colorway][size] + } + return 0; +} + +export const displayStock = (product: ProductType, colorway: string, size: string): string => { + // returns string describing remaining stock + if (product.stock[colorway] && product.stock[colorway][size]) { + const qty = product.stock[colorway][size]; + if (qty > 0) { + return `${qty} available`; + } + return "OUT OF STOCK"; + } + + return "ERROR: invalid color/size selected"; +} +*/ +export const isOutOfStock = (product: Product): boolean => { + // returns true if product is out of stock in all colorways and sizes + const totalQty = Object.values(product.stock).reduce((acc, stockByColor)=>{ + const colorQty = Object.values(stockByColor).reduce((acc2, qty)=>acc2+qty, 0); + return acc+colorQty + }, 0); + return totalQty <= 0; + +} + +/* +export const isColorwayAvailable = (product: ProductType, colorway: string): boolean => { + // returns true if colorway is available in any size + // returns false if colorway is out of stock in all sizes + const colorwayStock = Object.values(product.stock[colorway]).reduce( + (acc: any, size: any)=>acc+size, 0 + ); + return (colorwayStock > 0); +} + +export const isSizeAvailable = (product: ProductType, size: string): boolean => { + // returns true if size is available in any colorway + // returns false if size is out of stock in all colorways + const sizeStock = Object.values(product.stock).map(d => d[size]||0); + const totalQty = sizeStock.reduce((a, b) => { + return a + b; + }, 0); + return (totalQty > 0); +} + +export const getDefaultSize = (product: ProductType): string => { + const index = product.sizes.findIndex((size) => isSizeAvailable(product, size)); + if (index !== -1) { + return product.sizes[index]; + } + return ""; +} + +export const getDefaultColorway = (product: ProductType, size: string): string => { + const sizeIndex = product.sizes.indexOf(size); + if (sizeIndex === -1) { // no such size + return ""; + } + const colorwayStock = Object.values(product.stock).map(d => d[sizeIndex]); + const availColorwayIndex = colorwayStock.map((qty, idx) => qty > 0 ? idx : -1).filter(idx => idx !== -1); + if (availColorwayIndex.length > 0) { + return product.colorways[availColorwayIndex[0]]; + } + return ""; +} + +export const getDefaults = (product: ProductType): [string, string] => { + const size = getDefaultSize(product); + const colorway = getDefaultColorway(product, size); + return [colorway, size]; +} +*/ diff --git a/apps/web/features/merch/services/api.tsx b/apps/web/features/merch/services/api.tsx new file mode 100644 index 00000000..19da51e2 --- /dev/null +++ b/apps/web/features/merch/services/api.tsx @@ -0,0 +1,117 @@ +import { Product } from "types/lib/merch"; + +export class Api { + private API_ORIGIN: string; + + constructor() { + if (!process.env.NEXT_PUBLIC_MERCH_API_ORIGIN) { + throw new Error("NEXT_PUBLIC_MERCH_API_ORIGIN environment variable is not set") + } + this.API_ORIGIN = process.env.NEXT_PUBLIC_MERCH_API_ORIGIN || ""; + } + + // http methods + async get(urlPath: string): Promise> { + const response = await fetch(`${this.API_ORIGIN}${urlPath}`); + const convert = response.json() as unknown; // Convert to unknown type + return convert as Record + } + + /* + // eslint-disable-next-line class-methods-use-this + async post(urlPath: string, data: any): Promise { + const response = await fetch(`${this.API_ORIGIN}${urlPath}`, { + method: "POST", // *GET, POST, PUT, DELETE, etc. + mode: "cors", // no-cors, *cors, same-origin + cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached + credentials: "same-origin", // include, *same-origin, omit + headers: { + "Content-Type": "application/json", + // 'Content-Type': 'application/x-www-form-urlencoded', + }, + redirect: "follow", // manual, *follow, error + referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url + body: JSON.stringify(data), // body data type must match + }); + return response.json(); + } + */ + + // eslint-disable-next-line class-methods-use-this + async getProducts(): Promise { + try { + const res = await this.get("/products"); + console.log("product-list", res); + return res?.products ?? []; + } catch (e) { + if(e instanceof Error){ + throw new Error(e.message); + } + return [] + } + } + + /* + // eslint-disable-next-line class-methods-use-this + async getProduct(productId: string) { + try { + const res = await this.get(`/products/${productId}`); + console.log("product res", res); + return res; + } catch (e: any) { + throw new Error(e); + } + } + + async getOrder(userId: string, orderId: string) { + try { + const res = await this.get(`/orders/${orderId}`); + console.log("Order Summary response:", res); + return res; + } catch (e: any) { + throw new Error(e); + } + } + + async getOrderHistory(userId: string) { + try { + const res = await this.get(`/orders/${userId}`); + console.log("Order Summary response:", res); + return res.json(); + } catch (e: any) { + throw new Error(e); + } + } + + async postCheckoutCart( + items: CartItemType[], + email: string, + promoCode: string | null + ) { + try { + const res = await this.post(`/cart/checkout`, { + items, + promoCode: promoCode ?? "", + email, + }); + return res; + } catch (e: any) { + throw new Error(e); + } + } + + async postQuotation(items: CartItemType[], promoCode: string | null) { + try { + const res = await this.post(`/cart/quotation`, { + items, + promoCode: promoCode ?? "", + }); + return res; + } catch (e: any) { + throw new Error(e); + } + } + */ +} + +export const api = new Api(); diff --git a/apps/web/package.json b/apps/web/package.json index af82b560..b9238f0e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -20,6 +20,8 @@ "@chakra-ui/system": "^2.3.1", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", + "@tanstack/react-query": "^4.26.1", + "@tanstack/react-query-devtools": "^4.26.1", "framer-motion": "^7.6.4", "next": "13.4.6", "react": "18.2.0", diff --git a/apps/web/pages/_app.tsx b/apps/web/pages/_app.tsx index 4312a164..097e317b 100644 --- a/apps/web/pages/_app.tsx +++ b/apps/web/pages/_app.tsx @@ -1,5 +1,7 @@ import "../styles/globals.css"; import type { AppProps } from "next/app"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools" import { ChakraProvider } from "@chakra-ui/react"; import { theme } from "ui/theme"; import "@fontsource/work-sans/300.css"; @@ -10,13 +12,18 @@ import "ui/fonts/styles.css"; // for custom fonts not available on @fontsource import { WebLayout } from "@/features/layout"; +const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false } } }); + const App = ({ Component, pageProps }: AppProps) => { return ( - - - - - + + + + + + + + ); }; diff --git a/apps/web/pages/merch/index.tsx b/apps/web/pages/merch/index.tsx new file mode 100644 index 00000000..77e9737c --- /dev/null +++ b/apps/web/pages/merch/index.tsx @@ -0,0 +1,82 @@ +import React, { useState } from "react"; +import { Flex, Divider, Select, Heading, Grid } from "@chakra-ui/react"; +import { useQuery } from "@tanstack/react-query"; +import Card from "ui/components/merch/Card"; +import Page from "ui/components/merch/Page"; +import { QueryKeys } from "../../features/merch/constants/queryKeys"; +import { api } from "../../features/merch/services/api"; +import { Product } from "types/lib/merch"; +import ProductListSkeleton from "ui/components/merch/Skeleton"; +import { isOutOfStock } from "../../features/merch/functions/stock"; + +const MerchandiseList = () => { + const [selectedCategory, setSelectedCategory] = useState(""); + + const { data: products, isLoading } = useQuery([QueryKeys.PRODUCTS], () => api.getProducts(), {}); + + const categories = products?.map((product: Product) => product?.category); + const uniqueCategories = categories + ?.filter((c, idx) => categories.indexOf(c) === idx) + .filter(Boolean); + + const handleCategoryChange = (event: React.ChangeEvent) => { + setSelectedCategory(event.target.value); + }; + + return ( + + + + New Drop + + + + + {isLoading ? ( + + ) : ( + + {products + ?.filter((product: Product) => { + if (!product?.is_available) return false; + if (selectedCategory === "") return true; + return product?.category === selectedCategory; + }) + ?.map((item: Product, idx: number) => ( + + ))} + + )} + + ); +}; + +export default MerchandiseList; diff --git a/packages/ui/components/merch/Card.tsx b/packages/ui/components/merch/Card.tsx new file mode 100644 index 00000000..4a6c795d --- /dev/null +++ b/packages/ui/components/merch/Card.tsx @@ -0,0 +1,69 @@ +import { ArrowForwardIcon } from "@chakra-ui/icons"; +import { Box, Image, Text, GridItem, Flex, Badge, Center } from "@chakra-ui/react"; +import Link from "next/link"; +import { displayPrice } from "../../../../apps/web/features/merch/functions/currency"; +import { routes } from "../../../../apps/web/features/merch/constants/routes" + +type CardProps = { + _productId: string; + imgSrc?: string; + text: string; + price: number; + sizeRange: string; + isOutOfStock?: boolean; +}; + +const Card = ({ _productId, imgSrc, text, price, sizeRange, isOutOfStock }: CardProps) => { + return ( + + + +
+ +
+ + + {text} + {displayPrice(price)} + + {!isOutOfStock && ( + + + {sizeRange} + + + + )} + {isOutOfStock && ( + + + out of stock + + + )} + +
+ +
+ ); +}; + +export default Card; diff --git a/packages/ui/components/merch/CartHeader.tsx b/packages/ui/components/merch/CartHeader.tsx new file mode 100644 index 00000000..17d6cdeb --- /dev/null +++ b/packages/ui/components/merch/CartHeader.tsx @@ -0,0 +1,47 @@ +import { + Box, + Flex, + HStack, + Spacer, + Show, + Hide, + Icon, +} from "@chakra-ui/react"; +import Link from 'next/link'; +import routes from "../../../../apps/web/features/merch/constants/routes"; + + +const CartHeader = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default CartHeader; \ No newline at end of file diff --git a/packages/ui/components/merch/Page.tsx b/packages/ui/components/merch/Page.tsx new file mode 100644 index 00000000..7a7948ea --- /dev/null +++ b/packages/ui/components/merch/Page.tsx @@ -0,0 +1,37 @@ +import { ReactNode } from "react"; +import { Flex, FlexProps, Box } from "@chakra-ui/react"; +import CartHeader from "./CartHeader"; + +type PageProps = FlexProps & { + children: ReactNode; + hideHeader?: boolean; + contentWidth?: string; + contentPadding?: number[]; +}; + +const Page = ({ + children, + hideHeader = false, + contentWidth = "1400px", + contentPadding = [4, 6, 8], + ...props +}: PageProps) => { + return ( + + {!hideHeader && } + + {children} + + + ); +}; + +export default Page; \ No newline at end of file diff --git a/packages/ui/components/merch/Skeleton.tsx b/packages/ui/components/merch/Skeleton.tsx new file mode 100644 index 00000000..eae86104 --- /dev/null +++ b/packages/ui/components/merch/Skeleton.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { Grid, Skeleton, SkeletonText, GridItem } from "@chakra-ui/react"; + +const ProductListSkeleton: React.FC = () => { + return ( + + {new Array(8).fill(null).map((item: any) => ( + + + + {item} + + ))} + + ); +}; + +export default ProductListSkeleton; diff --git a/turbo.json b/turbo.json index a25ac8bd..dc65d53c 100644 --- a/turbo.json +++ b/turbo.json @@ -16,7 +16,8 @@ "S3_BUCKET", "FRONTEND_STAGING_DOMAIN", "PRODUCT_TABLE_NAME", - "ORDER_TABLE_NAME" + "ORDER_TABLE_NAME", + "NEXT_PUBLIC_MERCH_API_ORIGIN" ] }, "build": { @@ -34,7 +35,8 @@ "S3_BUCKET", "FRONTEND_STAGING_DOMAIN", "PRODUCT_TABLE_NAME", - "ORDER_TABLE_NAME" + "ORDER_TABLE_NAME", + "NEXT_PUBLIC_MERCH_API_ORIGIN" ], "outputs": ["dist/**", "build/**", "out/**", ".next/**"] }, @@ -56,7 +58,8 @@ "S3_BUCKET", "FRONTEND_STAGING_DOMAIN", "PRODUCT_TABLE_NAME", - "ORDER_TABLE_NAME" + "ORDER_TABLE_NAME", + "NEXT_PUBLIC_MERCH_API_ORIGIN" ] }, "serve": { @@ -73,7 +76,8 @@ "S3_BUCKET", "FRONTEND_STAGING_DOMAIN", "PRODUCT_TABLE_NAME", - "ORDER_TABLE_NAME" + "ORDER_TABLE_NAME", + "NEXT_PUBLIC_MERCH_API_ORIGIN" ] }, "lint": { diff --git a/yarn.lock b/yarn.lock index 8c0f80b5..d425f8aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6231,6 +6231,35 @@ pirates "^4.0.1" source-map-support "^0.5.13" +"@tanstack/match-sorter-utils@^8.7.0": + version "8.7.6" + resolved "https://registry.yarnpkg.com/@tanstack/match-sorter-utils/-/match-sorter-utils-8.7.6.tgz#ccf54a37447770e0cf0fe49a579c595fd2655b16" + integrity sha512-2AMpRiA6QivHOUiBpQAVxjiHAA68Ei23ZUMNaRJrN6omWiSFLoYrxGcT6BXtuzp0Jw4h6HZCmGGIM/gbwebO2A== + dependencies: + remove-accents "0.4.2" + +"@tanstack/query-core@4.26.1": + version "4.26.1" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.26.1.tgz#7a441086c4d3d79e1d156c0a355bd3567213626e" + integrity sha512-Zrx2pVQUP4ndnsu6+K/m8zerXSVY8QM+YSbxA1/jbBY21GeCd5oKfYl92oXPK0hPEUtoNuunIdiq0ZMqLos+Zg== + +"@tanstack/react-query-devtools@^4.26.1": + version "4.26.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-4.26.1.tgz#1895b2c6a257e461fa071a30202565d174e36238" + integrity sha512-ts2mA+fyFYFRi3Cee4xBk8Fx6waSFOM+yCkFqwJfGQRGjjTIMYMZPJv4wkv7vy12IVi1SYhL8au22LRKlXS1Zg== + dependencies: + "@tanstack/match-sorter-utils" "^8.7.0" + superjson "^1.10.0" + use-sync-external-store "^1.2.0" + +"@tanstack/react-query@^4.26.1": + version "4.26.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.26.1.tgz#d254f6b7b297b5ae4204c84e6622506e5ec77d09" + integrity sha512-i3dnz4TOARGIXrXQ5P7S25Zfi4noii/bxhcwPurh2nrf5EUCcAt/95TB2HSmMweUBx206yIMWUMEQ7ptd6zwDg== + dependencies: + "@tanstack/query-core" "4.26.1" + use-sync-external-store "^1.2.0" + "@testing-library/dom@^8.3.0", "@testing-library/dom@^8.5.0": version "8.19.0" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.19.0.tgz#bd3f83c217ebac16694329e413d9ad5fdcfd785f" @@ -8622,6 +8651,30 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +copy-anything@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.3.tgz#206767156f08da0e02efd392f71abcdf79643559" + integrity sha512-fpW2W/BqEzqPp29QS+MwwfisHCQZtiduTe/m8idFo0xbti9fIZ2WVhAsCv4ggFVH3AgCkVdpoOCtQC6gBrdhjw== + dependencies: + is-what "^4.1.8" + +copy-concurrently@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" + integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== + dependencies: + aproba "^1.1.1" + fs-write-stream-atomic "^1.0.8" + iferr "^0.1.5" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.0" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw== + copy-to-clipboard@3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae" @@ -11850,7 +11903,32 @@ is-weakset@^2.0.1: call-bind "^1.0.2" get-intrinsic "^1.1.1" -is-wsl@^2.1.1, is-wsl@^2.2.0: +is-what@^4.1.8: + version "4.1.8" + resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.8.tgz#0e2a8807fda30980ddb2571c79db3d209b14cbe4" + integrity sha512-yq8gMao5upkPoGEU9LsB2P+K3Kt8Q3fQFCGyNCWOAnJAMzEXVV9drYb0TXr42TTliLLhKIBvulgAXgtLLnwzGA== + +is-whitespace-character@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7" + integrity sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w== + +is-window@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d" + integrity sha512-uj00kdXyZb9t9RcAUAwMZAnkBUwdYGhYlt7djMXhfyhUCzwNba50tIiBKR7q0l7tdoBtFVw/3JmLY6fI3rmZmg== + +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-word-character@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.4.tgz#ce0e73216f98599060592f62ff31354ddbeb0230" + integrity sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA== + +is-wsl@^1.1.0, is-wsl@^2.1.1, is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== @@ -15681,6 +15759,37 @@ remark-slug@^6.0.0: mdast-util-to-string "^1.0.0" unist-util-visit "^2.0.0" +<<<<<<< HEAD +======= +remark-squeeze-paragraphs@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/remark-squeeze-paragraphs/-/remark-squeeze-paragraphs-4.0.0.tgz#76eb0e085295131c84748c8e43810159c5653ead" + integrity sha512-8qRqmL9F4nuLPIgl92XUuxI3pFxize+F1H0e/W3llTk0UsjJaj01+RrirkMw7P21RKe4X6goQhYRSvNWX+70Rw== + dependencies: + mdast-squeeze-paragraphs "^4.0.0" + +remove-accents@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5" + integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA== + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw== + +renderkid@^2.0.4: + version "2.0.7" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.7.tgz#464f276a6bdcee606f4a15993f9b29fc74ca8609" + integrity sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ== + dependencies: + css-select "^4.1.3" + dom-converter "^0.2.0" + htmlparser2 "^6.1.0" + lodash "^4.17.21" + strip-ansi "^3.0.1" + +>>>>>>> d3bfefd (task: Port home page (product listing) (#77)) renderkid@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" @@ -16665,6 +16774,7 @@ stylis@4.1.3: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7" integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA== +<<<<<<< HEAD stylis@4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" @@ -16682,6 +16792,14 @@ sucrase@^3.20.3: mz "^2.7.0" pirates "^4.0.1" ts-interface-checker "^0.1.9" +======= +superjson@^1.10.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/superjson/-/superjson-1.12.2.tgz#072471f1e6add2d95a38b77fef8c7a199d82103a" + integrity sha512-ugvUo9/WmvWOjstornQhsN/sR9mnGtWGYeTxFuqLb4AiT4QdUavjGFRALCPKWWnAiUJ4HTpytj5e0t5HoMRkXg== + dependencies: + copy-anything "^3.0.2" +>>>>>>> d3bfefd (task: Port home page (product listing) (#77)) supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" @@ -17506,6 +17624,19 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" +<<<<<<< HEAD +======= +use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +>>>>>>> d3bfefd (task: Port home page (product listing) (#77)) utf8-byte-length@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" From a37a6034253b47b3a00594d8e8d1f01175b94e38 Mon Sep 17 00:00:00 2001 From: Chan Wen Xu Date: Mon, 27 Mar 2023 21:22:58 +0800 Subject: [PATCH 03/60] feat(merch): Add CORS --- apps/merch/.env.example | 1 + apps/merch/package.json | 1 + apps/merch/src/index.ts | 12 ++++++++++++ yarn.lock | 33 ++++++++++++++++++++++----------- 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/apps/merch/.env.example b/apps/merch/.env.example index 0ea4aba9..685110c1 100644 --- a/apps/merch/.env.example +++ b/apps/merch/.env.example @@ -1,3 +1,4 @@ AWS_REGION= PRODUCT_TABLE_NAME= ORDER_TABLE_NAME= +CORS_ORIGIN=http://localhost:3001 diff --git a/apps/merch/package.json b/apps/merch/package.json index f7dd8f6b..9146abed 100644 --- a/apps/merch/package.json +++ b/apps/merch/package.json @@ -15,6 +15,7 @@ "dependencies": { "@aws-sdk/client-dynamodb": "^3.289.0", "@aws-sdk/util-dynamodb": "^3.289.0", + "cors": "^2.8.5", "express": "^4.17.1", "nodelogger": "*" }, diff --git a/apps/merch/src/index.ts b/apps/merch/src/index.ts index 908a3d57..7e21d371 100644 --- a/apps/merch/src/index.ts +++ b/apps/merch/src/index.ts @@ -1,5 +1,6 @@ import express from "express"; import path from "path"; +import cors from "cors"; import cookieParser from "cookie-parser"; import { nodeloggerMiddleware, Logger } from "nodelogger"; @@ -9,9 +10,20 @@ import ordersRouter from "./routes/orders"; import productsRouter from "./routes/products"; const app = express(); +const CORS_ORIGIN = process.env.CORS_ORIGIN; +let corsMiddleware = cors(); +if (CORS_ORIGIN) { + corsMiddleware = cors({ origin: CORS_ORIGIN }); +} else { + Logger.warn("========================================"); + Logger.warn(" CORS_ORIGIN was not set for merch app. "); + Logger.warn("Defaulting to allowing all CORS request!"); + Logger.warn("========================================"); +} // middleware app.use(nodeloggerMiddleware); +app.use(corsMiddleware); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); diff --git a/yarn.lock b/yarn.lock index d425f8aa..e9bda736 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8729,6 +8729,25 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cosmiconfig@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" + integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.7.2" + cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" @@ -13639,7 +13658,7 @@ nwsapi@^2.2.2: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw== -object-assign@^4.0.1, object-assign@^4.1.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -15759,8 +15778,6 @@ remark-slug@^6.0.0: mdast-util-to-string "^1.0.0" unist-util-visit "^2.0.0" -<<<<<<< HEAD -======= remark-squeeze-paragraphs@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/remark-squeeze-paragraphs/-/remark-squeeze-paragraphs-4.0.0.tgz#76eb0e085295131c84748c8e43810159c5653ead" @@ -15789,7 +15806,6 @@ renderkid@^2.0.4: lodash "^4.17.21" strip-ansi "^3.0.1" ->>>>>>> d3bfefd (task: Port home page (product listing) (#77)) renderkid@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" @@ -16774,7 +16790,6 @@ stylis@4.1.3: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7" integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA== -<<<<<<< HEAD stylis@4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" @@ -16792,14 +16807,13 @@ sucrase@^3.20.3: mz "^2.7.0" pirates "^4.0.1" ts-interface-checker "^0.1.9" -======= + superjson@^1.10.0: version "1.12.2" resolved "https://registry.yarnpkg.com/superjson/-/superjson-1.12.2.tgz#072471f1e6add2d95a38b77fef8c7a199d82103a" integrity sha512-ugvUo9/WmvWOjstornQhsN/sR9mnGtWGYeTxFuqLb4AiT4QdUavjGFRALCPKWWnAiUJ4HTpytj5e0t5HoMRkXg== dependencies: copy-anything "^3.0.2" ->>>>>>> d3bfefd (task: Port home page (product listing) (#77)) supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" @@ -17624,8 +17638,6 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" -<<<<<<< HEAD -======= use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" @@ -17636,7 +17648,6 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== ->>>>>>> d3bfefd (task: Port home page (product listing) (#77)) utf8-byte-length@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" @@ -17714,7 +17725,7 @@ value-equal@^1.0.1: resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== -vary@~1.1.2: +vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== From 5e856e036a8a4bff6ae659ac55203a1e6cfa739f Mon Sep 17 00:00:00 2001 From: Chan Wen Xu Date: Tue, 4 Apr 2023 01:14:34 +0800 Subject: [PATCH 04/60] fix: Add CORS types --- apps/merch/package.json | 1 + turbo.json | 3 +++ yarn.lock | 7 +++++++ 3 files changed, 11 insertions(+) diff --git a/apps/merch/package.json b/apps/merch/package.json index 9146abed..b2bee8ce 100644 --- a/apps/merch/package.json +++ b/apps/merch/package.json @@ -22,6 +22,7 @@ "devDependencies": { "@swc/core": "^1.3.64", "@types/cookie-parser": "^1.4.3", + "@types/cors": "^2.8.13", "@types/express": "^4.17.9", "@types/morgan": "^1.9.4", "cookie-parser": "^1.4.6", diff --git a/turbo.json b/turbo.json index dc65d53c..d890d86d 100644 --- a/turbo.json +++ b/turbo.json @@ -36,6 +36,7 @@ "FRONTEND_STAGING_DOMAIN", "PRODUCT_TABLE_NAME", "ORDER_TABLE_NAME", + "CORS_ORIGIN", "NEXT_PUBLIC_MERCH_API_ORIGIN" ], "outputs": ["dist/**", "build/**", "out/**", ".next/**"] @@ -59,6 +60,7 @@ "FRONTEND_STAGING_DOMAIN", "PRODUCT_TABLE_NAME", "ORDER_TABLE_NAME", + "CORS_ORIGIN", "NEXT_PUBLIC_MERCH_API_ORIGIN" ] }, @@ -77,6 +79,7 @@ "FRONTEND_STAGING_DOMAIN", "PRODUCT_TABLE_NAME", "ORDER_TABLE_NAME", + "CORS_ORIGIN", "NEXT_PUBLIC_MERCH_API_ORIGIN" ] }, diff --git a/yarn.lock b/yarn.lock index e9bda736..68fd342a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6391,6 +6391,13 @@ dependencies: "@types/express" "*" +"@types/cors@^2.8.13": + version "2.8.13" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.13.tgz#b8ade22ba455a1b8cb3b5d3f35910fd204f84f94" + integrity sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA== + dependencies: + "@types/node" "*" + "@types/detect-port@^1.3.0": version "1.3.3" resolved "https://registry.yarnpkg.com/@types/detect-port/-/detect-port-1.3.3.tgz#124c5d4c283f48a21f80826bcf39433b3e64aa81" From 4a15e7437a92064c504f83e1ce0a81b62863f6bf Mon Sep 17 00:00:00 2001 From: Chan Wen Xu Date: Fri, 19 May 2023 18:00:09 +0800 Subject: [PATCH 05/60] feat: Add pricing calculator --- packages/merch/lib/price.ts | 57 ++++++++++++++++++++++++++++++++++++ packages/merch/tsconfig.json | 13 ++++++++ packages/types/lib/merch.ts | 30 ++++++++++++++++--- 3 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 packages/merch/lib/price.ts create mode 100644 packages/merch/tsconfig.json diff --git a/packages/merch/lib/price.ts b/packages/merch/lib/price.ts new file mode 100644 index 00000000..49abb347 --- /dev/null +++ b/packages/merch/lib/price.ts @@ -0,0 +1,57 @@ +import { Cart, PricedCart, Product, Promotion } from "types"; + +export const calculatePricing = ( + products: Product[], + cart: Cart, + promotion?: Promotion +): PricedCart => { + const productMap: Record = {}; + if (promotion && promotion.redemptionsRemaining <= 0) { + throw new Error("no redemptions left for the provided promotion"); + } + for (const product of products) { + productMap[product.id] = product; + } + const pricedItems = cart.items.map((item) => { + const product = productMap[item.id]; + if (!product) { + throw new Error("unknown product ID: " + item.id); + } + let itemPrice = product.price * item.quantity; + if (!promotion) { + return { + ...item, + originalPrice: product.price, + discountedPrice: product.price, + }; + } + for (const discount of promotion.discounts) { + if (discount.appliesTo && !discount.appliesTo.includes(item.id)) { + continue; + } + if (discount.minimumQty && item.quantity < discount.minimumQty) { + continue; + } + switch (discount.promoType) { + case FIXED_VALUE: + itemPrice -= discount.promoValue; + break; + case PERCENTAGE: + itemPrice *= 1 - discount.promoValue; + itemPrice = Math.floor(itemPrice); + break; + } + } + itemPrice = Math.max(0, itemPrice); + return { + ...item, + originalPrice: product.price, + discountedPrice: itemPrice, + }; + }); + return { + promoCode: promotion?.promoCode, + total: pricedItems.reduce((acc, item) => acc + item.discountedPrice, 0), + items: pricedItems, + }; +}; diff --git a/packages/merch/tsconfig.json b/packages/merch/tsconfig.json new file mode 100644 index 00000000..9a3c9440 --- /dev/null +++ b/packages/merch/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "tsconfig/base.json", + "include": [ + "**/*.ts", + ], + "exclude": [ + "out", + "dist", + "build", + "node_modules", + ".turbo" + ] +} diff --git a/packages/types/lib/merch.ts b/packages/types/lib/merch.ts index 376cbaa0..6b18c63f 100644 --- a/packages/types/lib/merch.ts +++ b/packages/types/lib/merch.ts @@ -40,18 +40,40 @@ export interface Order { status: OrderStatus; } +export interface Cart { + items: { + id: string; + color: string; + size: string; + quantity: number; + }[]; +} + export interface Promotion { promoCode: string; + maxRedemptions: number; + redemptionsRemaining: number; discounts: Array<{ promoType: PromoType; promoValue: number; // percent off or fixed value off based on promoType property - appliesTo: Array; // array of product ids - minimumQty: number; // minimum quantity of items in the order to apply the discount - maxRedemptions: number; - redemptionsRemaining: number; + appliesTo?: Array; // array of product ids + minimumQty?: number; // minimum quantity of items in the order to apply the discount }>; } +export interface PricedCart { + promoCode?: string; + total: number; + items: { + id: string; + color: string; + size: string; + quantity: number; + originalPrice: number; + discountedPrice: number; + }[]; +} + enum PromoType { PERCENTAGE = "PERCENTAGE", FIXED_VALUE = "FIXED_VALUE", From 36e8840a21011b82f005cacd4f47cb1301967ff1 Mon Sep 17 00:00:00 2001 From: nicolelst Date: Mon, 27 Mar 2023 23:37:49 +0800 Subject: [PATCH 06/60] feat(merch): Implement product page --- apps/web/.env.example | 1 + .../web/features/merch/context/cart/index.tsx | 143 ++++++++ apps/web/features/merch/functions/cart.ts | 34 ++ apps/web/features/merch/functions/currency.ts | 12 +- apps/web/features/merch/functions/stock.ts | 161 +++++---- apps/web/features/merch/services/api.tsx | 2 +- apps/web/next.config.js | 5 + apps/web/pages/_app.tsx | 9 +- apps/web/pages/merch/index.tsx | 13 +- apps/web/pages/merch/product/[slug].tsx | 326 ++++++++++++++++++ package.json | 4 +- packages/types/lib/merch.ts | 83 +++++ .../components/carousel/AnimatedCarousel.tsx | 12 +- packages/ui/components/carousel/Carousel.tsx | 6 +- packages/ui/components/carousel/index.tsx | 3 +- packages/ui/components/merch/Card.tsx | 8 +- packages/ui/components/merch/CartHeader.tsx | 6 +- .../ui/components/merch/EmptyProductView.tsx | 21 ++ .../ui/components/merch/MerchCarousel.tsx | 115 ++++++ packages/ui/components/merch/Page.tsx | 8 +- .../ui/components/merch/SizeChartDialog.tsx | 33 ++ packages/ui/components/merch/SizeOption.tsx | 36 ++ packages/ui/components/merch/index.tsx | 8 + .../merch/skeleton/MerchDetailSkeleton.tsx | 28 ++ .../MerchListSkeleton.tsx} | 3 +- .../ui/components/merch/skeleton/index.tsx | 2 + yarn.lock | 31 ++ 27 files changed, 998 insertions(+), 115 deletions(-) create mode 100644 apps/web/features/merch/context/cart/index.tsx create mode 100644 apps/web/features/merch/functions/cart.ts create mode 100644 apps/web/pages/merch/product/[slug].tsx create mode 100644 packages/ui/components/merch/EmptyProductView.tsx create mode 100644 packages/ui/components/merch/MerchCarousel.tsx create mode 100644 packages/ui/components/merch/SizeChartDialog.tsx create mode 100644 packages/ui/components/merch/SizeOption.tsx create mode 100644 packages/ui/components/merch/index.tsx create mode 100644 packages/ui/components/merch/skeleton/MerchDetailSkeleton.tsx rename packages/ui/components/merch/{Skeleton.tsx => skeleton/MerchListSkeleton.tsx} (86%) create mode 100644 packages/ui/components/merch/skeleton/index.tsx diff --git a/apps/web/.env.example b/apps/web/.env.example index b8495e7b..748762c1 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1 +1,2 @@ WORDPRESS_API_URL= +NEXT_PUBLIC_MERCH_API_ORIGIN= diff --git a/apps/web/features/merch/context/cart/index.tsx b/apps/web/features/merch/context/cart/index.tsx new file mode 100644 index 00000000..a7c20c71 --- /dev/null +++ b/apps/web/features/merch/context/cart/index.tsx @@ -0,0 +1,143 @@ +import React, { useEffect, useReducer, useMemo, useContext } from "react"; +import { CartState, CartItem } from "types/lib/merch"; + +type ContextType = { + state: CartState; + dispatch: React.Dispatch; +} | null; + +export enum CartActionType { + RESET_CART = "RESET_CART", + INITALIZE = "initialize", + ADD_ITEM = "add_item", + UPDATE_QUANTITY = "update_quantity", + REMOVE_ITEM = "remove_item", + VALID_VOUCHER = "valid_voucher", + REMOVE_VOUCHER = "remove_voucher", + UPDATE_NAME = "update_name", + UPDATE_BILLING_EMAIL = "update_billing_email", +} + +export type CartAction = + | { type: CartActionType.RESET_CART; } + | { type: CartActionType.INITALIZE; payload: CartState; } + | { type: CartActionType.ADD_ITEM; payload: CartItem } + | { type: CartActionType.UPDATE_QUANTITY; payload: CartItem } + | { + type: CartActionType.REMOVE_ITEM; + payload: { productId: string; size: string ; colorway: string} + } + | { type: CartActionType.VALID_VOUCHER; payload: string } + | { type: CartActionType.REMOVE_VOUCHER; payload: null } + | { type: CartActionType.UPDATE_NAME; payload: string } + | { type: CartActionType.UPDATE_BILLING_EMAIL; payload: string }; + +const CartContext = React.createContext(null); + +const initState: CartState = { + items: [], + voucher: "", + name: "", + billingEmail: "", +}; + +export const cartReducer = (state: CartState, action: CartAction) => { + switch (action.type) { + case CartActionType.RESET_CART: { + return JSON.parse(JSON.stringify(initState)); + } + case CartActionType.INITALIZE: { + return { ...state, ...action.payload }; + } + case CartActionType.ADD_ITEM: { + // Find if there's an existing item already: + const { productId, size, colorway, quantity } = action.payload; + const idx = state.items.findIndex((x) => x.productId === productId && x.size === size && x.colorway == colorway); + const newQuantity = Math.min((state?.items[idx]?.quantity ?? 0) + quantity, 99); + return { + ...state, + items: + idx === -1 + ? [...state.items, action.payload] + : [ + ...state.items.slice(0, idx), + { ...state.items[idx], quantity: newQuantity }, + ...state.items.slice(idx + 1), + ], + }; + } + + case CartActionType.UPDATE_QUANTITY: { + const { productId, size, colorway, quantity } = action.payload; + const idx = state.items.findIndex((x) => x.productId === productId && x.size === size && x.colorway == colorway); + return { + ...state, + items: + idx === -1 + ? [...state.items] + : [...state.items.slice(0, idx), { ...state.items[idx], quantity }, ...state.items.slice(idx + 1)], + }; + } + case CartActionType.REMOVE_ITEM: { + const { productId, size, colorway } = action.payload; + return { + ...state, + items: [...state.items.filter((x) => !(x.productId === productId && x.size === size && x.colorway == colorway))], + }; + } + + case CartActionType.VALID_VOUCHER: { + return { ...state, voucher: action.payload }; + } + + case CartActionType.REMOVE_VOUCHER: { + return { ...state, voucher: "" }; + } + + case CartActionType.UPDATE_NAME: { + return { ...state, name: action.payload }; + } + + case CartActionType.UPDATE_BILLING_EMAIL: { + return { ...state, billingEmail: action.payload }; + } + + default: { + throw new Error(`Unhandled action type - ${JSON.stringify(action)}`); + } + } +}; + +export const useCartStore = () => { + const context = useContext(CartContext); + if (context === null) { + throw new Error("useCartStore must be used within a CartProvider."); + } + return context; +}; + +const initStorageCart: CartState = { voucher: "", name: "", billingEmail: "", items: [] }; + +interface CartProviderProps { + children: React.ReactNode; + } + +export const CartProvider: React.FC = ({ children }) => { + const [state, dispatch] = useReducer(cartReducer, initState); + const value = useMemo(() => ({ state, dispatch }), [state]); + + useEffect(() => { + const cartState: CartState = JSON.parse(JSON.stringify(initState)); + const storedCartData: CartState = JSON.parse(localStorage.getItem("cart") as string) ?? initStorageCart; + cartState.items = storedCartData.items; + cartState.name = storedCartData.name; + cartState.billingEmail = storedCartData.billingEmail; + dispatch({ type: CartActionType.INITALIZE, payload: cartState }); + }, []); + + useEffect(() => { + localStorage.setItem("cart", JSON.stringify(state)); + }, [state]); + + return {children}; +}; \ No newline at end of file diff --git a/apps/web/features/merch/functions/cart.ts b/apps/web/features/merch/functions/cart.ts new file mode 100644 index 00000000..0f4f977b --- /dev/null +++ b/apps/web/features/merch/functions/cart.ts @@ -0,0 +1,34 @@ +import { CartItem } from "types/lib/merch"; + +export const getQtyInCart = ( + cartItems: CartItem[], + productId: string, + colorway: string, + size: string +): number => { + const cartItem = cartItems.find((item) => { + return ( + item.productId === productId && + item.size === size && + item.colorway === colorway + ); + }); + + if (cartItem) { + return cartItem.quantity; + } + return 0; +}; + +export const displayQtyInCart = ( + cartItems: CartItem[], + productId: string, + colorway: string, + size: string +): string => { + const qty = getQtyInCart(cartItems, productId, colorway, size); + if (qty > 0) { + return `You have already added ${qty} to your cart.`; + } + return ""; +}; diff --git a/apps/web/features/merch/functions/currency.ts b/apps/web/features/merch/functions/currency.ts index 3b3fed5c..c33847dc 100644 --- a/apps/web/features/merch/functions/currency.ts +++ b/apps/web/features/merch/functions/currency.ts @@ -1,7 +1,7 @@ export const displayPrice = (amountInCents: number): string => { - const amountStr = amountInCents.toString() - const dollarStr = amountStr.slice(0, -2).padStart(1, '0') - const centStr = amountStr.slice(-2).padStart(2, '0') - const priceStr = dollarStr.concat(".").concat(centStr) - return `$${priceStr}` -} + const amountStr = amountInCents.toString(); + const dollarStr = amountStr.slice(0, -2).padStart(1, "0"); + const centStr = amountStr.slice(-2).padStart(2, "0"); + const priceStr = dollarStr.concat(".").concat(centStr); + return `$${priceStr}`; +}; diff --git a/apps/web/features/merch/functions/stock.ts b/apps/web/features/merch/functions/stock.ts index 290c7028..62133fc5 100644 --- a/apps/web/features/merch/functions/stock.ts +++ b/apps/web/features/merch/functions/stock.ts @@ -1,81 +1,98 @@ -import { Product } from "types/lib/merch" +import { Product } from "types/lib/merch"; -/* -export const getQtyInStock = (product: ProductType, colorway: string, size: string): number => { - // returns remaining stock for specified colorway and size - if (product.stock[colorway] && product.stock[colorway][size]) { - return product.stock[colorway][size] - } - return 0; -} +export const getQtyInStock = ( + product: Product, + colorway: string, + size: string +): number => { + // returns remaining stock for specified colorway and size + if (product.stock[colorway] && product.stock[colorway][size]) { + return product.stock[colorway][size]; + } + return 0; +}; -export const displayStock = (product: ProductType, colorway: string, size: string): string => { - // returns string describing remaining stock - if (product.stock[colorway] && product.stock[colorway][size]) { - const qty = product.stock[colorway][size]; - if (qty > 0) { - return `${qty} available`; - } - return "OUT OF STOCK"; +export const displayStock = ( + product: Product, + colorway: string, + size: string +): string => { + // returns string describing remaining stock + if (product.stock[colorway] && product.stock[colorway][size]) { + const qty = product.stock[colorway][size]; + if (qty > 0) { + return `${qty} available`; } - - return "ERROR: invalid color/size selected"; -} -*/ -export const isOutOfStock = (product: Product): boolean => { - // returns true if product is out of stock in all colorways and sizes - const totalQty = Object.values(product.stock).reduce((acc, stockByColor)=>{ - const colorQty = Object.values(stockByColor).reduce((acc2, qty)=>acc2+qty, 0); - return acc+colorQty - }, 0); - return totalQty <= 0; + return "OUT OF STOCK"; + } -} + return "ERROR: invalid color/size selected"; +}; -/* -export const isColorwayAvailable = (product: ProductType, colorway: string): boolean => { - // returns true if colorway is available in any size - // returns false if colorway is out of stock in all sizes - const colorwayStock = Object.values(product.stock[colorway]).reduce( - (acc: any, size: any)=>acc+size, 0 - ); - return (colorwayStock > 0); -} +export const isOutOfStock = (product: Product): boolean => { + // returns true if product is out of stock in all colorways and sizes + if (product && product.stock) { + const totalQty = Object.values(product.stock).reduce( + (acc, stockByColor) => { + const colorQty = Object.values(stockByColor).reduce( + (acc2, qty) => acc2 + qty, + 0 + ); + return acc + colorQty; + }, + 0 + ); + return totalQty <= 0; + } else { + return false; + } +}; -export const isSizeAvailable = (product: ProductType, size: string): boolean => { - // returns true if size is available in any colorway - // returns false if size is out of stock in all colorways - const sizeStock = Object.values(product.stock).map(d => d[size]||0); - const totalQty = sizeStock.reduce((a, b) => { - return a + b; - }, 0); - return (totalQty > 0); -} +export const isColorwayAvailable = ( + product: Product, + colorway: string +): boolean => { + // returns true if colorway is available in any size + // returns false if colorway is out of stock in all sizes + const colorwayStock = Object.values(product.stock[colorway]).reduce( + (acc: any, size: any) => acc + size, + 0 + ); + return colorwayStock > 0; +}; -export const getDefaultSize = (product: ProductType): string => { - const index = product.sizes.findIndex((size) => isSizeAvailable(product, size)); - if (index !== -1) { - return product.sizes[index]; - } - return ""; -} +export const isSizeAvailable = (product: Product, size: string): boolean => { + // returns true if size is available in any colorway + // returns false if size is out of stock in all colorways + const sizeStock = Object.values(product.stock).map((d) => d[size] || 0); + const totalQty = sizeStock.reduce((a, b) => { + return a + b; + }, 0); + return totalQty > 0; +}; -export const getDefaultColorway = (product: ProductType, size: string): string => { - const sizeIndex = product.sizes.indexOf(size); - if (sizeIndex === -1) { // no such size - return ""; - } - const colorwayStock = Object.values(product.stock).map(d => d[sizeIndex]); - const availColorwayIndex = colorwayStock.map((qty, idx) => qty > 0 ? idx : -1).filter(idx => idx !== -1); - if (availColorwayIndex.length > 0) { - return product.colorways[availColorwayIndex[0]]; - } - return ""; -} +export const getDefaultSize = (product: Product): string | null => { + const index1 = product.sizes.findIndex((size) => + isSizeAvailable(product, size) + ); + const index2 = product.sizes.findIndex( + (size, idx) => idx > index1 && isSizeAvailable(product, size) + ); + if (index1 !== -1 && index2 === -1) { + return product.sizes[index1]; // only 1 size available + } + return null; +}; -export const getDefaults = (product: ProductType): [string, string] => { - const size = getDefaultSize(product); - const colorway = getDefaultColorway(product, size); - return [colorway, size]; -} -*/ +export const getDefaultColorway = (product: Product): string | null => { + const index1 = product.colors.findIndex((color) => + isColorwayAvailable(product, color) + ); + const index2 = product.colors.findIndex( + (color, idx) => idx > index1 && isColorwayAvailable(product, color) + ); + if (index1 !== -1 && index2 === -1) { + return product.colors[index1]; // only 1 color available + } + return null; +}; diff --git a/apps/web/features/merch/services/api.tsx b/apps/web/features/merch/services/api.tsx index 19da51e2..bfac534c 100644 --- a/apps/web/features/merch/services/api.tsx +++ b/apps/web/features/merch/services/api.tsx @@ -51,7 +51,6 @@ export class Api { } } - /* // eslint-disable-next-line class-methods-use-this async getProduct(productId: string) { try { @@ -63,6 +62,7 @@ export class Api { } } + /* async getOrder(userId: string, orderId: string) { try { const res = await this.get(`/orders/${orderId}`); diff --git a/apps/web/next.config.js b/apps/web/next.config.js index f22563e4..3da9f01d 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -14,6 +14,11 @@ const nextConfig = { hostname: "clubs.ntu.edu.sg", pathname: "/csec/**", }, + { + protocol: "https", + hostname: "cdn.ntuscse.com", + pathname: "/merch/products/images/**", + }, ], }, transpilePackages: ["ui"], diff --git a/apps/web/pages/_app.tsx b/apps/web/pages/_app.tsx index 097e317b..6571ef14 100644 --- a/apps/web/pages/_app.tsx +++ b/apps/web/pages/_app.tsx @@ -11,6 +11,7 @@ import "@fontsource/work-sans/700.css"; import "ui/fonts/styles.css"; // for custom fonts not available on @fontsource import { WebLayout } from "@/features/layout"; +import { CartProvider } from "@/features/merch/context/cart"; const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false } } }); @@ -18,9 +19,11 @@ const App = ({ Component, pageProps }: AppProps) => { return ( - - - + + + + + diff --git a/apps/web/pages/merch/index.tsx b/apps/web/pages/merch/index.tsx index 77e9737c..fef04ff0 100644 --- a/apps/web/pages/merch/index.tsx +++ b/apps/web/pages/merch/index.tsx @@ -1,13 +1,12 @@ import React, { useState } from "react"; import { Flex, Divider, Select, Heading, Grid } from "@chakra-ui/react"; import { useQuery } from "@tanstack/react-query"; -import Card from "ui/components/merch/Card"; -import Page from "ui/components/merch/Page"; -import { QueryKeys } from "../../features/merch/constants/queryKeys"; -import { api } from "../../features/merch/services/api"; +import { Card, Page } from "ui/components/merch"; +import { QueryKeys } from "features/merch/constants/queryKeys"; +import { api } from "features/merch/services/api"; import { Product } from "types/lib/merch"; -import ProductListSkeleton from "ui/components/merch/Skeleton"; -import { isOutOfStock } from "../../features/merch/functions/stock"; +import { MerchListSkeleton } from "ui/components/merch/skeleton"; +import { isOutOfStock } from "features/merch/functions/stock"; const MerchandiseList = () => { const [selectedCategory, setSelectedCategory] = useState(""); @@ -49,7 +48,7 @@ const MerchandiseList = () => { {isLoading ? ( - + ) : ( ( + + {children} + +); + +const MerchDetail: React.FC = () => { + // Context hook. + const { state: cartState, dispatch: cartDispatch } = useCartStore(); + const router = useRouter(); + const productId = (router.query.slug ?? "") as string; + + const [quantity, setQuantity] = useState(1); + const [isDisabled, setIsDisabled] = useState(true); + const [selectedSize, setSelectedSize] = useState(null); + const [selectedColorway, setSelectedColorway] = useState(null); + const [maxQuantity, setMaxQuantity] = useState(1); + + const { isOpen, onOpen, onClose } = useDisclosure(); + + const { data: product, isLoading } = useQuery([QueryKeys.PRODUCT, productId], () => api.getProduct(productId), { + onSuccess: (data: Product) => { + setIsDisabled(!(data?.is_available === true)); + setSelectedSize(getDefaultSize(data)); + setSelectedColorway(getDefaultColorway(data)); + }, + }); + + //* In/decrement quantity + const handleQtyChangeCounter = (isAdd = true) => { + const value = isAdd ? 1 : -1; + if (!isAdd && quantity === 1) return; + if (isAdd && quantity >= maxQuantity) return; + setQuantity(quantity + value); + }; + + //* Manual input quantity. + const handleQtyChangeInput = (e: React.FormEvent): void => { + const target = e.target as HTMLInputElement; + if (Number.isNaN(parseInt(target.value, 10))) { + setQuantity(1); + return; + } + const value = parseInt(target.value, 10); + if (value <= 0) { + setQuantity(1); + } else if (value > maxQuantity) { + setQuantity(maxQuantity); + } else { + setQuantity(value); + } + }; + + const updateMaxQuantity = (colorway: string, size: string) => { + if (product) { + const stockQty = getQtyInStock(product, colorway, size); + const cartQty = getQtyInCart(cartState.items, product.id, colorway, size); + const max = (stockQty > cartQty) ? stockQty - cartQty : 0; + setMaxQuantity(max); + } + } + + const handleAddToCart = () => { + if (!selectedColorway || !selectedSize) { + return; + } + setIsDisabled(true); + const payload: CartAction = { + type: CartActionType.ADD_ITEM, + payload: { + productId, + quantity, + colorway: selectedColorway, + size: selectedSize, + }, + }; + cartDispatch(payload); + setMaxQuantity(maxQuantity - quantity); + setQuantity(1); + setIsDisabled(false); + }; + + const handleBuyNow = () => { + handleAddToCart(); + window.location.href = routes.CART; + }; + + const ProductNameSection = ( + + + {product?.name} + {!product?.is_available && ( + + unavailable + + )} + {product && isOutOfStock(product) && ( + + out of stock + + )} + + + {displayPrice(product?.price ?? 0)} + + + ); + + const renderSizeSection = ( + + + Sizes + {product?.size_chart && } + + + {product?.sizes?.map((size, idx) => { + return ( + { + setQuantity(1); + if (size !== selectedSize) { + setSelectedSize(size); + if (selectedColorway) { + updateMaxQuantity(selectedColorway, size) + } + } + else { + setSelectedSize(null); + } + }} + disabled={ + isDisabled || + (product ? + (!isSizeAvailable(product, size)) : // size is not available for all colorways + false + ) || + ((product && selectedColorway) ? + (getQtyInStock(product, selectedColorway, size) === 0) : // size is not available for selected colorway + false + ) + } + > + + {size} + + + ); + })} + + + ); + + const renderColorwaySection = ( + + + Colors + + + {product?.colors?.map((colorway, idx) => { + return ( + { + setQuantity(1); + if (colorway !== selectedColorway) { + setSelectedColorway(colorway); + if (selectedSize) { + updateMaxQuantity(colorway, selectedSize) + } + } + else { + setSelectedColorway(null); + } + }} + width="auto" + px={4} + disabled={ + isDisabled || + (product ? + (!isColorwayAvailable(product, colorway)) : // colorway is not available for all sizes + false + ) || + ((product && selectedSize) ? + (getQtyInStock(product, colorway, selectedSize) === 0) : // colorway is not available for selected size + false + ) + } + > + + {colorway} + + + ); + })} + + + ); + + const renderQuantitySection = ( + + Quantity + + handleQtyChangeCounter(false)}> + - + + + = maxQuantity} active={false} onClick={() => handleQtyChangeCounter(true)}> + + + +
+ + {(product && selectedColorway && selectedSize && (product.is_available === true)) ? displayStock(product, selectedColorway, selectedSize) : ""} + +
+
+ + + {(product && selectedColorway && selectedSize) ? displayQtyInCart(cartState.items, product.id, selectedColorway, selectedSize) : ""} + + + {(product && selectedColorway && selectedSize && (maxQuantity === 0)) ? "You have reached the maximum purchase quantity." : ""} + + +
+ ); + + const purchaseButtons = ( + + + + + ); + + const renderMerchDetails = () => { + return ( + + + + + + {ProductNameSection} + + {renderSizeSection} + {renderColorwaySection} + {renderQuantitySection} + + {purchaseButtons} + + {/* {renderDescription} */} + + + + ); + }; + + const renderMerchPage = () => { + if (isLoading) return ; + if (product === undefined || product === null) return ; + return renderMerchDetails(); + }; + + return {renderMerchPage()}; +}; + +export default MerchDetail; diff --git a/package.json b/package.json index 43e8fd79..1fdbc8af 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "yarn": ">= 1.22.17", "pnpm": "please-use-yarn" }, - "dependencies": {}, + "dependencies": { + "swiper": "^9.2.0" + }, "packageManager": "yarn@1.22.17" } diff --git a/packages/types/lib/merch.ts b/packages/types/lib/merch.ts index 6b18c63f..702c1a00 100644 --- a/packages/types/lib/merch.ts +++ b/packages/types/lib/merch.ts @@ -1,3 +1,4 @@ +// Product export interface Product { id: string; name: string; @@ -15,6 +16,7 @@ export interface Product { }; } +// Order export enum OrderStatus { PENDING_PAYMENT = 1, PAYMENT_COMPLETED = 2, @@ -49,6 +51,7 @@ export interface Cart { }[]; } +// Promotion export interface Promotion { promoCode: string; maxRedemptions: number; @@ -78,3 +81,83 @@ enum PromoType { PERCENTAGE = "PERCENTAGE", FIXED_VALUE = "FIXED_VALUE", } + +// Cart +export type CartItem = { + productId: string; + size: string; + colorway: string; + quantity: number; +}; + +export type CartState = { + voucher: string | null; + items: CartItem[]; + name: string; + billingEmail: string; +}; + +/* +export type ProductInfo = { + name: string; + image: string; + price: number; +}; + +export type CartPrice = { + currency: string; + subtotal: number; + discount: number; + grandTotal: number; +}; + +export type CartResponseDto = { + items: [ + { + id: string; + name: string; + price: number; + images: string[]; + sizes: string; + productCategory: string; + isAvailable: boolean; + quantity: number; + } + ]; + price: { + currency: string; + subtotal: number; + discount: number; + grandTotal: number; + }; +}; + +export type CheckoutResponseDto = { + orderId: string; + items: [ + { + id: string; + name: string; + price: number; + images: string[]; + sizes: string[]; + productCategory: string; + isAvailable: boolean; + quantity: number; + } + ]; + price: { + currency: string; + subtotal: number; + discount: number; + grandTotal: number; + }; + payment: { + paymentGateway: string; + clientSecret: string; + }; + email: string; +}; + +export type ProductInfoMap = Record; +*/ diff --git a/packages/ui/components/carousel/AnimatedCarousel.tsx b/packages/ui/components/carousel/AnimatedCarousel.tsx index 5e76a42c..2823a398 100644 --- a/packages/ui/components/carousel/AnimatedCarousel.tsx +++ b/packages/ui/components/carousel/AnimatedCarousel.tsx @@ -8,12 +8,14 @@ import { import { Image } from "../image"; import Link from "next/link"; +export type AnimatedCarouselItem = { + imageSrc: string; + href: string; + altText: string; +} + export interface AnimatedCarouselProps extends FlexProps { - items: Array<{ - imageSrc: string; - href: string; - altText: string; - }>; + items: Array; } const CarouselSlides = ({ items }: AnimatedCarouselProps) => { diff --git a/packages/ui/components/carousel/Carousel.tsx b/packages/ui/components/carousel/Carousel.tsx index c2d36464..3a7cbe03 100644 --- a/packages/ui/components/carousel/Carousel.tsx +++ b/packages/ui/components/carousel/Carousel.tsx @@ -1,7 +1,7 @@ -import React from "react"; -import { AnimatedCarousel, AnimatedCarouselProps } from "./AnimatedCarousel"; +import { AnimatedCarousel, AnimatedCarouselProps, AnimatedCarouselItem } from "./AnimatedCarousel"; -export interface CarouselProps extends AnimatedCarouselProps {} +export type CarouselItem = AnimatedCarouselItem; +export interface CarouselProps extends AnimatedCarouselProps {}; export const Carousel = ({ items }: CarouselProps) => { return ( diff --git a/packages/ui/components/carousel/index.tsx b/packages/ui/components/carousel/index.tsx index 73328e28..ff97b88d 100644 --- a/packages/ui/components/carousel/index.tsx +++ b/packages/ui/components/carousel/index.tsx @@ -1,2 +1,3 @@ export { Carousel } from "./Carousel"; -export type { CarouselProps } from "./Carousel"; \ No newline at end of file +export type { CarouselProps } from "./Carousel"; +export type { CarouselItem } from "./Carousel"; \ No newline at end of file diff --git a/packages/ui/components/merch/Card.tsx b/packages/ui/components/merch/Card.tsx index 4a6c795d..a2675a34 100644 --- a/packages/ui/components/merch/Card.tsx +++ b/packages/ui/components/merch/Card.tsx @@ -1,8 +1,8 @@ import { ArrowForwardIcon } from "@chakra-ui/icons"; import { Box, Image, Text, GridItem, Flex, Badge, Center } from "@chakra-ui/react"; import Link from "next/link"; -import { displayPrice } from "../../../../apps/web/features/merch/functions/currency"; -import { routes } from "../../../../apps/web/features/merch/constants/routes" +import { displayPrice } from "web/features/merch/functions/currency"; +import { routes } from "web/features/merch/constants/routes" type CardProps = { _productId: string; @@ -13,7 +13,7 @@ type CardProps = { isOutOfStock?: boolean; }; -const Card = ({ _productId, imgSrc, text, price, sizeRange, isOutOfStock }: CardProps) => { +export const Card = ({ _productId, imgSrc, text, price, sizeRange, isOutOfStock }: CardProps) => { return ( @@ -65,5 +65,3 @@ const Card = ({ _productId, imgSrc, text, price, sizeRange, isOutOfStock }: Card ); }; - -export default Card; diff --git a/packages/ui/components/merch/CartHeader.tsx b/packages/ui/components/merch/CartHeader.tsx index 17d6cdeb..d0359986 100644 --- a/packages/ui/components/merch/CartHeader.tsx +++ b/packages/ui/components/merch/CartHeader.tsx @@ -11,7 +11,7 @@ import Link from 'next/link'; import routes from "../../../../apps/web/features/merch/constants/routes"; -const CartHeader = () => { +export const CartHeader = () => { return ( @@ -42,6 +42,4 @@ const CartHeader = () => { ); -}; - -export default CartHeader; \ No newline at end of file +}; \ No newline at end of file diff --git a/packages/ui/components/merch/EmptyProductView.tsx b/packages/ui/components/merch/EmptyProductView.tsx new file mode 100644 index 00000000..259842b2 --- /dev/null +++ b/packages/ui/components/merch/EmptyProductView.tsx @@ -0,0 +1,21 @@ +import React, { useEffect } from "react"; +import { Center, Flex, Heading, Spinner, Text } from "@chakra-ui/react"; +import routes from "../../../../apps/web/features/merch/constants/routes"; + +export const EmptyProductView: React.FC = () => { + useEffect(() => { + setTimeout(() => { + window.location.href = routes.HOME; + }, 3000); + }, []); + + return ( +
+ + The item does not exist... + + Redirecting you in 3 seconds... + +
+ ); +}; diff --git a/packages/ui/components/merch/MerchCarousel.tsx b/packages/ui/components/merch/MerchCarousel.tsx new file mode 100644 index 00000000..830a5777 --- /dev/null +++ b/packages/ui/components/merch/MerchCarousel.tsx @@ -0,0 +1,115 @@ +import "swiper/css"; +import "swiper/less/autoplay"; +import { Flex, Box, Image } from "@chakra-ui/react"; +import { Controller } from "swiper"; +import { Swiper, SwiperSlide, useSwiper } from "swiper/react"; +import React, { ReactElement, useState } from "react"; +import { ChevronLeftIcon, ChevronRightIcon } from "@chakra-ui/icons"; + +export type Slide = { + id: string; + component: ReactElement; +}; + +export type MarkerProps = { + index: number; + slideIndex: number; + slideTo: (index: number) => void; +}; + +type CarouselProps = { + images: string[]; +}; + +const Marker: React.FC = (props: MarkerProps) => { + // Swiper Ref + const swiper = useSwiper(); + const { index, slideIndex, slideTo } = props; + + // Click Handler + const handleClick = () => { + slideTo(index); + swiper.slideTo(index); + }; + + return ( + + ); +}; + +const Controllerer = ({ length }: { length: number }) => { + const swiper = useSwiper(); + const [curIndex, setCurIndex] = useState(0); + const slideLeft = () => { + swiper.slidePrev(); + if (curIndex - 1 < 0) { + setCurIndex(length - 1); + } else { + setCurIndex((curIndex - 1) % length); + } + }; + const slideRight = () => { + swiper.slideNext(); + setCurIndex((curIndex + 1) % length); + }; + const slideTo = (index: number): void => { + setCurIndex(index); + }; + + if (length < 2) return null + + return ( + + + + {Array(length) + .fill(null) + .map((url, index) => ( + + ))} + + + + ); +}; + +export const MerchCarousel = ({ images }: CarouselProps) => { + const [controlledSwiper, setControlledSwiper] = useState(null); + + return ( + + + + {images.map((url, idx) => ( + + + + ))} + + + + + {images.map((url, idx) => ( + + ))} + + + + + ); +}; + diff --git a/packages/ui/components/merch/Page.tsx b/packages/ui/components/merch/Page.tsx index 7a7948ea..d6364352 100644 --- a/packages/ui/components/merch/Page.tsx +++ b/packages/ui/components/merch/Page.tsx @@ -1,6 +1,6 @@ import { ReactNode } from "react"; import { Flex, FlexProps, Box } from "@chakra-ui/react"; -import CartHeader from "./CartHeader"; +import { CartHeader } from "./CartHeader"; type PageProps = FlexProps & { children: ReactNode; @@ -9,7 +9,7 @@ type PageProps = FlexProps & { contentPadding?: number[]; }; -const Page = ({ +export const Page = ({ children, hideHeader = false, contentWidth = "1400px", @@ -32,6 +32,4 @@ const Page = ({ ); -}; - -export default Page; \ No newline at end of file +}; \ No newline at end of file diff --git a/packages/ui/components/merch/SizeChartDialog.tsx b/packages/ui/components/merch/SizeChartDialog.tsx new file mode 100644 index 00000000..0f999485 --- /dev/null +++ b/packages/ui/components/merch/SizeChartDialog.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { + Modal, + ModalOverlay, + ModalContent, + Image, + ModalHeader, + ModalBody, + ModalCloseButton, + Divider, +} from "@chakra-ui/react"; + +type SizeChartDialogType = { + sizeChart?: string; + isOpen: boolean; + onClose: () => void; +}; + +export const SizeChartDialog: React.FC = ({ sizeChart, isOpen, onClose }) => { + return ( + + + + Size Chart + + + + + + + + ); +}; diff --git a/packages/ui/components/merch/SizeOption.tsx b/packages/ui/components/merch/SizeOption.tsx new file mode 100644 index 00000000..4b62f025 --- /dev/null +++ b/packages/ui/components/merch/SizeOption.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { Box, BoxProps } from "@chakra-ui/react"; + +type SizeOptionType = BoxProps & { + active: boolean; + disabled?: boolean; + onClick: (param: any) => void; +}; + +export const SizeOption: React.FC = (props) => { + const { active = false, disabled = false, children } = props; + return ( + + {children} + + ); +}; diff --git a/packages/ui/components/merch/index.tsx b/packages/ui/components/merch/index.tsx new file mode 100644 index 00000000..9c62fae3 --- /dev/null +++ b/packages/ui/components/merch/index.tsx @@ -0,0 +1,8 @@ +export * from "./Card" +export * from "./CartHeader" +export * from "./EmptyProductView" +export * from "./MerchCarousel" +export * from "./Page" +export * from "./SizeChartDialog" +export * from "./SizeOption" +export * from "./skeleton" \ No newline at end of file diff --git a/packages/ui/components/merch/skeleton/MerchDetailSkeleton.tsx b/packages/ui/components/merch/skeleton/MerchDetailSkeleton.tsx new file mode 100644 index 00000000..3617a961 --- /dev/null +++ b/packages/ui/components/merch/skeleton/MerchDetailSkeleton.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { Flex, Skeleton, Divider, GridItem, Grid } from "@chakra-ui/react"; + +export const MerchDetailSkeleton: React.FC = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + ); +}; \ No newline at end of file diff --git a/packages/ui/components/merch/Skeleton.tsx b/packages/ui/components/merch/skeleton/MerchListSkeleton.tsx similarity index 86% rename from packages/ui/components/merch/Skeleton.tsx rename to packages/ui/components/merch/skeleton/MerchListSkeleton.tsx index eae86104..9ebde95f 100644 --- a/packages/ui/components/merch/Skeleton.tsx +++ b/packages/ui/components/merch/skeleton/MerchListSkeleton.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Grid, Skeleton, SkeletonText, GridItem } from "@chakra-ui/react"; -const ProductListSkeleton: React.FC = () => { +export const MerchListSkeleton: React.FC = () => { return ( {new Array(8).fill(null).map((item: any) => ( @@ -15,4 +15,3 @@ const ProductListSkeleton: React.FC = () => { ); }; -export default ProductListSkeleton; diff --git a/packages/ui/components/merch/skeleton/index.tsx b/packages/ui/components/merch/skeleton/index.tsx new file mode 100644 index 00000000..86c3dd87 --- /dev/null +++ b/packages/ui/components/merch/skeleton/index.tsx @@ -0,0 +1,2 @@ +export { MerchListSkeleton } from "./MerchListSkeleton"; +export { MerchDetailSkeleton } from "./MerchDetailSkeleton"; diff --git a/yarn.lock b/yarn.lock index 68fd342a..102a0386 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16541,6 +16541,30 @@ sshpk@^1.14.1: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +ssr-window@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/ssr-window/-/ssr-window-4.0.2.tgz#dc6b3ee37be86ac0e3ddc60030f7b3bc9b8553be" + integrity sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ== + +ssri@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" + integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q== + dependencies: + figgy-pudding "^3.5.1" + +ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" + +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" @@ -16870,6 +16894,13 @@ swc-minify-webpack-plugin@^2.1.0: resolved "https://registry.yarnpkg.com/swc-minify-webpack-plugin/-/swc-minify-webpack-plugin-2.1.1.tgz#2c63fe592d49541733d7557b3af8f97c7ffa78b9" integrity sha512-/9ud/libNWUC5p71vXWhW/O2Nc0essW8D9pY4P4ol0ceM8OcFbNr41R9YFqTkmktqUL2t0WwXau+FkR4T1+PJA== +swiper@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/swiper/-/swiper-9.2.0.tgz#40e950b1c0d516403b3bf7083560ebac22a6f325" + integrity sha512-lWK9toYumUQss+YuTL+Mt0+8twiMJEyzioER4bbS4rrGHlkeLrDM8uhtAmnpdijELrNscuNUujDgKoMQZfQGlQ== + dependencies: + ssr-window "^4.0.2" + symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" From 7c78c063d723fbe5a44f28a59eca10c990291d2e Mon Sep 17 00:00:00 2001 From: Chan Wen Xu Date: Thu, 25 May 2023 18:19:24 +0800 Subject: [PATCH 07/60] chore: Run prettier on all merch files --- .../web/features/merch/context/cart/index.tsx | 60 +++-- apps/web/features/merch/services/api.tsx | 10 +- apps/web/pages/merch/index.tsx | 70 ++--- apps/web/pages/merch/product/[slug].tsx | 245 ++++++++++++------ 4 files changed, 255 insertions(+), 130 deletions(-) diff --git a/apps/web/features/merch/context/cart/index.tsx b/apps/web/features/merch/context/cart/index.tsx index a7c20c71..57ea13e6 100644 --- a/apps/web/features/merch/context/cart/index.tsx +++ b/apps/web/features/merch/context/cart/index.tsx @@ -19,13 +19,13 @@ export enum CartActionType { } export type CartAction = - | { type: CartActionType.RESET_CART; } - | { type: CartActionType.INITALIZE; payload: CartState; } + | { type: CartActionType.RESET_CART } + | { type: CartActionType.INITALIZE; payload: CartState } | { type: CartActionType.ADD_ITEM; payload: CartItem } | { type: CartActionType.UPDATE_QUANTITY; payload: CartItem } - | { - type: CartActionType.REMOVE_ITEM; - payload: { productId: string; size: string ; colorway: string} + | { + type: CartActionType.REMOVE_ITEM; + payload: { productId: string; size: string; colorway: string }; } | { type: CartActionType.VALID_VOUCHER; payload: string } | { type: CartActionType.REMOVE_VOUCHER; payload: null } @@ -52,8 +52,14 @@ export const cartReducer = (state: CartState, action: CartAction) => { case CartActionType.ADD_ITEM: { // Find if there's an existing item already: const { productId, size, colorway, quantity } = action.payload; - const idx = state.items.findIndex((x) => x.productId === productId && x.size === size && x.colorway == colorway); - const newQuantity = Math.min((state?.items[idx]?.quantity ?? 0) + quantity, 99); + const idx = state.items.findIndex( + (x) => + x.productId === productId && x.size === size && x.colorway == colorway + ); + const newQuantity = Math.min( + (state?.items[idx]?.quantity ?? 0) + quantity, + 99 + ); return { ...state, items: @@ -69,20 +75,36 @@ export const cartReducer = (state: CartState, action: CartAction) => { case CartActionType.UPDATE_QUANTITY: { const { productId, size, colorway, quantity } = action.payload; - const idx = state.items.findIndex((x) => x.productId === productId && x.size === size && x.colorway == colorway); + const idx = state.items.findIndex( + (x) => + x.productId === productId && x.size === size && x.colorway == colorway + ); return { ...state, items: idx === -1 ? [...state.items] - : [...state.items.slice(0, idx), { ...state.items[idx], quantity }, ...state.items.slice(idx + 1)], + : [ + ...state.items.slice(0, idx), + { ...state.items[idx], quantity }, + ...state.items.slice(idx + 1), + ], }; } case CartActionType.REMOVE_ITEM: { const { productId, size, colorway } = action.payload; return { ...state, - items: [...state.items.filter((x) => !(x.productId === productId && x.size === size && x.colorway == colorway))], + items: [ + ...state.items.filter( + (x) => + !( + x.productId === productId && + x.size === size && + x.colorway == colorway + ) + ), + ], }; } @@ -96,7 +118,7 @@ export const cartReducer = (state: CartState, action: CartAction) => { case CartActionType.UPDATE_NAME: { return { ...state, name: action.payload }; - } + } case CartActionType.UPDATE_BILLING_EMAIL: { return { ...state, billingEmail: action.payload }; @@ -116,11 +138,16 @@ export const useCartStore = () => { return context; }; -const initStorageCart: CartState = { voucher: "", name: "", billingEmail: "", items: [] }; +const initStorageCart: CartState = { + voucher: "", + name: "", + billingEmail: "", + items: [], +}; interface CartProviderProps { - children: React.ReactNode; - } + children: React.ReactNode; +} export const CartProvider: React.FC = ({ children }) => { const [state, dispatch] = useReducer(cartReducer, initState); @@ -128,7 +155,8 @@ export const CartProvider: React.FC = ({ children }) => { useEffect(() => { const cartState: CartState = JSON.parse(JSON.stringify(initState)); - const storedCartData: CartState = JSON.parse(localStorage.getItem("cart") as string) ?? initStorageCart; + const storedCartData: CartState = + JSON.parse(localStorage.getItem("cart") as string) ?? initStorageCart; cartState.items = storedCartData.items; cartState.name = storedCartData.name; cartState.billingEmail = storedCartData.billingEmail; @@ -140,4 +168,4 @@ export const CartProvider: React.FC = ({ children }) => { }, [state]); return {children}; -}; \ No newline at end of file +}; diff --git a/apps/web/features/merch/services/api.tsx b/apps/web/features/merch/services/api.tsx index bfac534c..1633c1c3 100644 --- a/apps/web/features/merch/services/api.tsx +++ b/apps/web/features/merch/services/api.tsx @@ -5,7 +5,9 @@ export class Api { constructor() { if (!process.env.NEXT_PUBLIC_MERCH_API_ORIGIN) { - throw new Error("NEXT_PUBLIC_MERCH_API_ORIGIN environment variable is not set") + throw new Error( + "NEXT_PUBLIC_MERCH_API_ORIGIN environment variable is not set" + ); } this.API_ORIGIN = process.env.NEXT_PUBLIC_MERCH_API_ORIGIN || ""; } @@ -14,7 +16,7 @@ export class Api { async get(urlPath: string): Promise> { const response = await fetch(`${this.API_ORIGIN}${urlPath}`); const convert = response.json() as unknown; // Convert to unknown type - return convert as Record + return convert as Record; } /* @@ -44,10 +46,10 @@ export class Api { console.log("product-list", res); return res?.products ?? []; } catch (e) { - if(e instanceof Error){ + if (e instanceof Error) { throw new Error(e.message); } - return [] + return []; } } diff --git a/apps/web/pages/merch/index.tsx b/apps/web/pages/merch/index.tsx index fef04ff0..e584c61d 100644 --- a/apps/web/pages/merch/index.tsx +++ b/apps/web/pages/merch/index.tsx @@ -10,48 +10,58 @@ import { isOutOfStock } from "features/merch/functions/stock"; const MerchandiseList = () => { const [selectedCategory, setSelectedCategory] = useState(""); - - const { data: products, isLoading } = useQuery([QueryKeys.PRODUCTS], () => api.getProducts(), {}); + + const { data: products, isLoading } = useQuery( + [QueryKeys.PRODUCTS], + () => api.getProducts(), + {} + ); const categories = products?.map((product: Product) => product?.category); const uniqueCategories = categories ?.filter((c, idx) => categories.indexOf(c) === idx) .filter(Boolean); - const handleCategoryChange = (event: React.ChangeEvent) => { + const handleCategoryChange = ( + event: React.ChangeEvent + ) => { setSelectedCategory(event.target.value); }; return ( - - - - New Drop - - - - + + + + New Drop + + + + {isLoading ? ( ) : ( @@ -68,7 +78,9 @@ const MerchandiseList = () => { text={item?.name} price={item?.price} imgSrc={item?.images?.[0]} - sizeRange={`${item?.sizes?.[0]} - ${item.sizes?.[item.sizes.length - 1]}`} + sizeRange={`${item?.sizes?.[0]} - ${ + item.sizes?.[item.sizes.length - 1] + }`} isOutOfStock={isOutOfStock(item)} /> ))} diff --git a/apps/web/pages/merch/product/[slug].tsx b/apps/web/pages/merch/product/[slug].tsx index 559d3848..b5abb3f4 100644 --- a/apps/web/pages/merch/product/[slug].tsx +++ b/apps/web/pages/merch/product/[slug].tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { useRouter } from 'next/router' +import { useRouter } from "next/router"; import { Flex, Heading, @@ -14,14 +14,33 @@ import { Center, } from "@chakra-ui/react"; import { useQuery } from "@tanstack/react-query"; -import { EmptyProductView, MerchCarousel, MerchDetailSkeleton, Page, SizeChartDialog, SizeOption } from "ui/components/merch"; +import { + EmptyProductView, + MerchCarousel, + MerchDetailSkeleton, + Page, + SizeChartDialog, + SizeOption, +} from "ui/components/merch"; import { Product } from "types/lib/merch"; -import { CartAction, CartActionType, useCartStore } from "features/merch/context/cart"; +import { + CartAction, + CartActionType, + useCartStore, +} from "features/merch/context/cart"; import { api } from "features/merch/services/api"; -import { routes } from "features/merch/constants/routes" +import { routes } from "features/merch/constants/routes"; import { QueryKeys } from "features/merch/constants/queryKeys"; import { displayPrice } from "features/merch/functions/currency"; -import { displayStock, getDefaultColorway, getDefaultSize, getQtyInStock, isColorwayAvailable, isOutOfStock, isSizeAvailable } from "features/merch/functions/stock"; +import { + displayStock, + getDefaultColorway, + getDefaultSize, + getQtyInStock, + isColorwayAvailable, + isOutOfStock, + isSizeAvailable, +} from "features/merch/functions/stock"; import { displayQtyInCart, getQtyInCart } from "features/merch/functions/cart"; const GroupTitle = ({ children }: any) => ( @@ -40,20 +59,24 @@ const MerchDetail: React.FC = () => { const [isDisabled, setIsDisabled] = useState(true); const [selectedSize, setSelectedSize] = useState(null); const [selectedColorway, setSelectedColorway] = useState(null); - const [maxQuantity, setMaxQuantity] = useState(1); + const [maxQuantity, setMaxQuantity] = useState(1); const { isOpen, onOpen, onClose } = useDisclosure(); - const { data: product, isLoading } = useQuery([QueryKeys.PRODUCT, productId], () => api.getProduct(productId), { - onSuccess: (data: Product) => { - setIsDisabled(!(data?.is_available === true)); - setSelectedSize(getDefaultSize(data)); - setSelectedColorway(getDefaultColorway(data)); - }, - }); + const { data: product, isLoading } = useQuery( + [QueryKeys.PRODUCT, productId], + () => api.getProduct(productId), + { + onSuccess: (data: Product) => { + setIsDisabled(!(data?.is_available === true)); + setSelectedSize(getDefaultSize(data)); + setSelectedColorway(getDefaultColorway(data)); + }, + } + ); //* In/decrement quantity - const handleQtyChangeCounter = (isAdd = true) => { + const handleQtyChangeCounter = (isAdd = true) => { const value = isAdd ? 1 : -1; if (!isAdd && quantity === 1) return; if (isAdd && quantity >= maxQuantity) return; @@ -61,7 +84,7 @@ const MerchDetail: React.FC = () => { }; //* Manual input quantity. - const handleQtyChangeInput = (e: React.FormEvent): void => { + const handleQtyChangeInput = (e: React.FormEvent): void => { const target = e.target as HTMLInputElement; if (Number.isNaN(parseInt(target.value, 10))) { setQuantity(1); @@ -70,7 +93,7 @@ const MerchDetail: React.FC = () => { const value = parseInt(target.value, 10); if (value <= 0) { setQuantity(1); - } else if (value > maxQuantity) { + } else if (value > maxQuantity) { setQuantity(maxQuantity); } else { setQuantity(value); @@ -81,10 +104,10 @@ const MerchDetail: React.FC = () => { if (product) { const stockQty = getQtyInStock(product, colorway, size); const cartQty = getQtyInCart(cartState.items, product.id, colorway, size); - const max = (stockQty > cartQty) ? stockQty - cartQty : 0; + const max = stockQty > cartQty ? stockQty - cartQty : 0; setMaxQuantity(max); } - } + }; const handleAddToCart = () => { if (!selectedColorway || !selectedSize) { @@ -96,8 +119,8 @@ const MerchDetail: React.FC = () => { payload: { productId, quantity, - colorway: selectedColorway, - size: selectedSize, + colorway: selectedColorway, + size: selectedSize, }, }; cartDispatch(payload); @@ -116,12 +139,24 @@ const MerchDetail: React.FC = () => { {product?.name} {!product?.is_available && ( - + unavailable )} {product && isOutOfStock(product) && ( - + out of stock )} @@ -134,11 +169,18 @@ const MerchDetail: React.FC = () => { const renderSizeSection = ( - + Sizes - {product?.size_chart && } + {product?.size_chart && ( + + )} {product?.sizes?.map((size, idx) => { @@ -151,26 +193,26 @@ const MerchDetail: React.FC = () => { if (size !== selectedSize) { setSelectedSize(size); if (selectedColorway) { - updateMaxQuantity(selectedColorway, size) + updateMaxQuantity(selectedColorway, size); } - } - else { + } else { setSelectedSize(null); } }} disabled={ - isDisabled || - (product ? - (!isSizeAvailable(product, size)) : // size is not available for all colorways - false - ) || - ((product && selectedColorway) ? - (getQtyInStock(product, selectedColorway, size) === 0) : // size is not available for selected colorway - false - ) - } + isDisabled || + (product + ? !isSizeAvailable(product, size) // size is not available for all colorways + : false) || + (product && selectedColorway + ? getQtyInStock(product, selectedColorway, size) === 0 // size is not available for selected colorway + : false) + } > - + {size} @@ -182,7 +224,12 @@ const MerchDetail: React.FC = () => { const renderColorwaySection = ( - + Colors @@ -196,28 +243,28 @@ const MerchDetail: React.FC = () => { if (colorway !== selectedColorway) { setSelectedColorway(colorway); if (selectedSize) { - updateMaxQuantity(colorway, selectedSize) + updateMaxQuantity(colorway, selectedSize); } - } - else { + } else { setSelectedColorway(null); } }} width="auto" px={4} disabled={ - isDisabled || - (product ? - (!isColorwayAvailable(product, colorway)) : // colorway is not available for all sizes - false - ) || - ((product && selectedSize) ? - (getQtyInStock(product, colorway, selectedSize) === 0) : // colorway is not available for selected size - false - ) - } + isDisabled || + (product + ? !isColorwayAvailable(product, colorway) // colorway is not available for all sizes + : false) || + (product && selectedSize + ? getQtyInStock(product, colorway, selectedSize) === 0 // colorway is not available for selected size + : false) + } > - + {colorway} @@ -231,61 +278,93 @@ const MerchDetail: React.FC = () => { Quantity - handleQtyChangeCounter(false)}> + handleQtyChangeCounter(false)} + > - - = maxQuantity} active={false} onClick={() => handleQtyChangeCounter(true)}> + = maxQuantity + } + active={false} + onClick={() => handleQtyChangeCounter(true)} + > +
- - {(product && selectedColorway && selectedSize && (product.is_available === true)) ? displayStock(product, selectedColorway, selectedSize) : ""} - + + {product && + selectedColorway && + selectedSize && + product.is_available === true + ? displayStock(product, selectedColorway, selectedSize) + : ""} +
- - {(product && selectedColorway && selectedSize) ? displayQtyInCart(cartState.items, product.id, selectedColorway, selectedSize) : ""} - - - {(product && selectedColorway && selectedSize && (maxQuantity === 0)) ? "You have reached the maximum purchase quantity." : ""} - + + {product && selectedColorway && selectedSize + ? displayQtyInCart( + cartState.items, + product.id, + selectedColorway, + selectedSize + ) + : ""} + + + {product && selectedColorway && selectedSize && maxQuantity === 0 + ? "You have reached the maximum purchase quantity." + : ""} +
); const purchaseButtons = ( - - @@ -309,7 +388,11 @@ const MerchDetail: React.FC = () => { {/* {renderDescription} */} - +
); }; From cefa476614682188351d18d47af29e8233124720 Mon Sep 17 00:00:00 2001 From: Chan Wen Xu Date: Thu, 25 May 2023 18:52:55 +0800 Subject: [PATCH 08/60] cleanup: Merge Cart and CartItem --- .../web/features/merch/context/cart/index.tsx | 84 +++++++++--------- apps/web/features/merch/functions/cart.ts | 16 ++-- apps/web/features/merch/functions/stock.ts | 43 +++++----- apps/web/pages/merch/product/[slug].tsx | 85 ++++++++++--------- packages/types/lib/merch.ts | 37 ++++---- 5 files changed, 129 insertions(+), 136 deletions(-) diff --git a/apps/web/features/merch/context/cart/index.tsx b/apps/web/features/merch/context/cart/index.tsx index 57ea13e6..f047368f 100644 --- a/apps/web/features/merch/context/cart/index.tsx +++ b/apps/web/features/merch/context/cart/index.tsx @@ -3,7 +3,7 @@ import { CartState, CartItem } from "types/lib/merch"; type ContextType = { state: CartState; - dispatch: React.Dispatch; + dispatch: React.Dispatch; } | null; export enum CartActionType { @@ -35,74 +35,75 @@ export type CartAction = const CartContext = React.createContext(null); const initState: CartState = { - items: [], + cart: { + items: [], + }, voucher: "", name: "", billingEmail: "", }; -export const cartReducer = (state: CartState, action: CartAction) => { +export const cartReducer = ( + state: CartState, + action: CartAction +): CartState => { switch (action.type) { case CartActionType.RESET_CART: { - return JSON.parse(JSON.stringify(initState)); + return JSON.parse(JSON.stringify(initState)) as typeof initState; } case CartActionType.INITALIZE: { return { ...state, ...action.payload }; } case CartActionType.ADD_ITEM: { // Find if there's an existing item already: - const { productId, size, colorway, quantity } = action.payload; - const idx = state.items.findIndex( - (x) => - x.productId === productId && x.size === size && x.colorway == colorway + const { id, size, color, quantity } = action.payload; + const idx = state.cart.items.findIndex( + (x) => x.id === id && x.size === size && x.color === color ); const newQuantity = Math.min( - (state?.items[idx]?.quantity ?? 0) + quantity, + (state?.cart?.items[idx]?.quantity ?? 0) + quantity, 99 ); return { ...state, - items: - idx === -1 - ? [...state.items, action.payload] - : [ - ...state.items.slice(0, idx), - { ...state.items[idx], quantity: newQuantity }, - ...state.items.slice(idx + 1), - ], + cart: { + ...state, + items: + idx === -1 + ? [...state.cart.items, action.payload] + : [ + ...state.cart.items.slice(0, idx), + { ...state.cart.items[idx], quantity: newQuantity }, + ...state.cart.items.slice(idx + 1), + ], + }, }; } case CartActionType.UPDATE_QUANTITY: { - const { productId, size, colorway, quantity } = action.payload; - const idx = state.items.findIndex( - (x) => - x.productId === productId && x.size === size && x.colorway == colorway + const { id, size, color, quantity } = action.payload; + const idx = state.cart.items.findIndex( + (x) => x.id === id && x.size === size && x.color === color ); return { ...state, items: idx === -1 - ? [...state.items] + ? [...state.cart.items] : [ - ...state.items.slice(0, idx), - { ...state.items[idx], quantity }, - ...state.items.slice(idx + 1), + ...state.cart.items.slice(0, idx), + { ...state.cart.items[idx], quantity }, + ...state.cart.items.slice(idx + 1), ], }; } case CartActionType.REMOVE_ITEM: { - const { productId, size, colorway } = action.payload; + const { id, size, color } = action.payload; return { ...state, items: [ - ...state.items.filter( - (x) => - !( - x.productId === productId && - x.size === size && - x.colorway == colorway - ) + ...state.cart.items.filter( + (x) => !(x.id === id && x.size === size && x.color == color) ), ], }; @@ -138,13 +139,6 @@ export const useCartStore = () => { return context; }; -const initStorageCart: CartState = { - voucher: "", - name: "", - billingEmail: "", - items: [], -}; - interface CartProviderProps { children: React.ReactNode; } @@ -154,10 +148,14 @@ export const CartProvider: React.FC = ({ children }) => { const value = useMemo(() => ({ state, dispatch }), [state]); useEffect(() => { - const cartState: CartState = JSON.parse(JSON.stringify(initState)); + const cartState: CartState = JSON.parse( + JSON.stringify(initState) + ) as typeof initState; const storedCartData: CartState = - JSON.parse(localStorage.getItem("cart") as string) ?? initStorageCart; - cartState.items = storedCartData.items; + (JSON.parse( + localStorage.getItem("cart") as string + ) as typeof initState) ?? cartState; + cartState.cart.items = storedCartData.cart.items; cartState.name = storedCartData.name; cartState.billingEmail = storedCartData.billingEmail; dispatch({ type: CartActionType.INITALIZE, payload: cartState }); diff --git a/apps/web/features/merch/functions/cart.ts b/apps/web/features/merch/functions/cart.ts index 0f4f977b..ae88fad2 100644 --- a/apps/web/features/merch/functions/cart.ts +++ b/apps/web/features/merch/functions/cart.ts @@ -2,16 +2,12 @@ import { CartItem } from "types/lib/merch"; export const getQtyInCart = ( cartItems: CartItem[], - productId: string, - colorway: string, + id: string, + color: string, size: string ): number => { const cartItem = cartItems.find((item) => { - return ( - item.productId === productId && - item.size === size && - item.colorway === colorway - ); + return item.id === id && item.size === size && item.color === color; }); if (cartItem) { @@ -22,11 +18,11 @@ export const getQtyInCart = ( export const displayQtyInCart = ( cartItems: CartItem[], - productId: string, - colorway: string, + id: string, + color: string, size: string ): string => { - const qty = getQtyInCart(cartItems, productId, colorway, size); + const qty = getQtyInCart(cartItems, id, color, size); if (qty > 0) { return `You have already added ${qty} to your cart.`; } diff --git a/apps/web/features/merch/functions/stock.ts b/apps/web/features/merch/functions/stock.ts index 62133fc5..68d2e206 100644 --- a/apps/web/features/merch/functions/stock.ts +++ b/apps/web/features/merch/functions/stock.ts @@ -2,24 +2,24 @@ import { Product } from "types/lib/merch"; export const getQtyInStock = ( product: Product, - colorway: string, + color: string, size: string ): number => { - // returns remaining stock for specified colorway and size - if (product.stock[colorway] && product.stock[colorway][size]) { - return product.stock[colorway][size]; + // returns remaining stock for specified color and size + if (product.stock[color] && product.stock[color][size]) { + return product.stock[color][size]; } return 0; }; export const displayStock = ( product: Product, - colorway: string, + color: string, size: string ): string => { // returns string describing remaining stock - if (product.stock[colorway] && product.stock[colorway][size]) { - const qty = product.stock[colorway][size]; + if (product.stock[color] && product.stock[color][size]) { + const qty = product.stock[color][size]; if (qty > 0) { return `${qty} available`; } @@ -30,7 +30,7 @@ export const displayStock = ( }; export const isOutOfStock = (product: Product): boolean => { - // returns true if product is out of stock in all colorways and sizes + // returns true if product is out of stock in all colors and sizes if (product && product.stock) { const totalQty = Object.values(product.stock).reduce( (acc, stockByColor) => { @@ -48,22 +48,19 @@ export const isOutOfStock = (product: Product): boolean => { } }; -export const isColorwayAvailable = ( - product: Product, - colorway: string -): boolean => { - // returns true if colorway is available in any size - // returns false if colorway is out of stock in all sizes - const colorwayStock = Object.values(product.stock[colorway]).reduce( - (acc: any, size: any) => acc + size, +export const isColorAvailable = (product: Product, color: string): boolean => { + // returns true if color is available in any size + // returns false if color is out of stock in all sizes + const colorStock = Object.values(product.stock[color]).reduce( + (acc, size) => acc + size, 0 ); - return colorwayStock > 0; + return colorStock > 0; }; export const isSizeAvailable = (product: Product, size: string): boolean => { - // returns true if size is available in any colorway - // returns false if size is out of stock in all colorways + // returns true if size is available in any color + // returns false if size is out of stock in all colors const sizeStock = Object.values(product.stock).map((d) => d[size] || 0); const totalQty = sizeStock.reduce((a, b) => { return a + b; @@ -72,6 +69,7 @@ export const isSizeAvailable = (product: Product, size: string): boolean => { }; export const getDefaultSize = (product: Product): string | null => { + if (!product.sizes) return null; const index1 = product.sizes.findIndex((size) => isSizeAvailable(product, size) ); @@ -84,12 +82,13 @@ export const getDefaultSize = (product: Product): string | null => { return null; }; -export const getDefaultColorway = (product: Product): string | null => { +export const getDefaultColor = (product: Product): string | null => { + if (!product.colors) return null; const index1 = product.colors.findIndex((color) => - isColorwayAvailable(product, color) + isColorAvailable(product, color) ); const index2 = product.colors.findIndex( - (color, idx) => idx > index1 && isColorwayAvailable(product, color) + (color, idx) => idx > index1 && isColorAvailable(product, color) ); if (index1 !== -1 && index2 === -1) { return product.colors[index1]; // only 1 color available diff --git a/apps/web/pages/merch/product/[slug].tsx b/apps/web/pages/merch/product/[slug].tsx index b5abb3f4..97461e88 100644 --- a/apps/web/pages/merch/product/[slug].tsx +++ b/apps/web/pages/merch/product/[slug].tsx @@ -34,10 +34,10 @@ import { QueryKeys } from "features/merch/constants/queryKeys"; import { displayPrice } from "features/merch/functions/currency"; import { displayStock, - getDefaultColorway, + getDefaultColor, getDefaultSize, getQtyInStock, - isColorwayAvailable, + isColorAvailable, isOutOfStock, isSizeAvailable, } from "features/merch/functions/stock"; @@ -53,24 +53,24 @@ const MerchDetail: React.FC = () => { // Context hook. const { state: cartState, dispatch: cartDispatch } = useCartStore(); const router = useRouter(); - const productId = (router.query.slug ?? "") as string; + const id = (router.query.slug ?? "") as string; const [quantity, setQuantity] = useState(1); const [isDisabled, setIsDisabled] = useState(true); const [selectedSize, setSelectedSize] = useState(null); - const [selectedColorway, setSelectedColorway] = useState(null); + const [selectedColor, setSelectedColor] = useState(null); const [maxQuantity, setMaxQuantity] = useState(1); const { isOpen, onOpen, onClose } = useDisclosure(); const { data: product, isLoading } = useQuery( - [QueryKeys.PRODUCT, productId], - () => api.getProduct(productId), + [QueryKeys.PRODUCT, id], + () => api.getProduct(id), { onSuccess: (data: Product) => { setIsDisabled(!(data?.is_available === true)); setSelectedSize(getDefaultSize(data)); - setSelectedColorway(getDefaultColorway(data)); + setSelectedColor(getDefaultColor(data)); }, } ); @@ -100,26 +100,31 @@ const MerchDetail: React.FC = () => { } }; - const updateMaxQuantity = (colorway: string, size: string) => { + const updateMaxQuantity = (color: string, size: string) => { if (product) { - const stockQty = getQtyInStock(product, colorway, size); - const cartQty = getQtyInCart(cartState.items, product.id, colorway, size); + const stockQty = getQtyInStock(product, color, size); + const cartQty = getQtyInCart( + cartState.cart.items, + product.id, + color, + size + ); const max = stockQty > cartQty ? stockQty - cartQty : 0; setMaxQuantity(max); } }; const handleAddToCart = () => { - if (!selectedColorway || !selectedSize) { + if (!selectedColor || !selectedSize) { return; } setIsDisabled(true); const payload: CartAction = { type: CartActionType.ADD_ITEM, payload: { - productId, + id, quantity, - colorway: selectedColorway, + color: selectedColor, size: selectedSize, }, }; @@ -192,8 +197,8 @@ const MerchDetail: React.FC = () => { setQuantity(1); if (size !== selectedSize) { setSelectedSize(size); - if (selectedColorway) { - updateMaxQuantity(selectedColorway, size); + if (selectedColor) { + updateMaxQuantity(selectedColor, size); } } else { setSelectedSize(null); @@ -202,10 +207,10 @@ const MerchDetail: React.FC = () => { disabled={ isDisabled || (product - ? !isSizeAvailable(product, size) // size is not available for all colorways + ? !isSizeAvailable(product, size) // size is not available for all colors : false) || - (product && selectedColorway - ? getQtyInStock(product, selectedColorway, size) === 0 // size is not available for selected colorway + (product && selectedColor + ? getQtyInStock(product, selectedColor, size) === 0 // size is not available for selected color : false) } > @@ -222,7 +227,7 @@ const MerchDetail: React.FC = () => { ); - const renderColorwaySection = ( + const renderColorSection = ( { Colors - {product?.colors?.map((colorway, idx) => { + {product?.colors?.map((color, idx) => { return ( { setQuantity(1); - if (colorway !== selectedColorway) { - setSelectedColorway(colorway); + if (color !== selectedColor) { + setSelectedColor(color); if (selectedSize) { - updateMaxQuantity(colorway, selectedSize); + updateMaxQuantity(color, selectedSize); } } else { - setSelectedColorway(null); + setSelectedColor(null); } }} width="auto" @@ -254,10 +259,10 @@ const MerchDetail: React.FC = () => { disabled={ isDisabled || (product - ? !isColorwayAvailable(product, colorway) // colorway is not available for all sizes + ? !isColorAvailable(product, color) // color is not available for all sizes : false) || (product && selectedSize - ? getQtyInStock(product, colorway, selectedSize) === 0 // colorway is not available for selected size + ? getQtyInStock(product, color, selectedSize) === 0 // color is not available for selected size : false) } > @@ -265,7 +270,7 @@ const MerchDetail: React.FC = () => { textTransform="uppercase" fontSize={{ base: "sm", md: "md" }} > - {colorway} + {color} ); @@ -280,7 +285,7 @@ const MerchDetail: React.FC = () => { handleQtyChangeCounter(false)} @@ -296,13 +301,13 @@ const MerchDetail: React.FC = () => { borderRadius={0} maxWidth={100} placeholder="Item Count" - disabled={isDisabled || !(selectedColorway && selectedSize)} + disabled={isDisabled || !(selectedColor && selectedSize)} onChange={handleQtyChangeInput} /> = maxQuantity } active={false} @@ -313,27 +318,27 @@ const MerchDetail: React.FC = () => {
{product && - selectedColorway && + selectedColor && selectedSize && product.is_available === true - ? displayStock(product, selectedColorway, selectedSize) + ? displayStock(product, selectedColor, selectedSize) : ""}
- {product && selectedColorway && selectedSize + {product && selectedColor && selectedSize ? displayQtyInCart( - cartState.items, + cartState.cart.items, product.id, - selectedColorway, + selectedColor, selectedSize ) : ""} - {product && selectedColorway && selectedSize && maxQuantity === 0 + {product && selectedColor && selectedSize && maxQuantity === 0 ? "You have reached the maximum purchase quantity." : ""} @@ -351,7 +356,7 @@ const MerchDetail: React.FC = () => { variant="outline" onClick={handleAddToCart} disabled={ - isDisabled || !(selectedColorway && selectedSize) || maxQuantity === 0 + isDisabled || !(selectedColor && selectedSize) || maxQuantity === 0 } > ADD TO CART @@ -363,7 +368,7 @@ const MerchDetail: React.FC = () => { borderRadius={0} onClick={handleBuyNow} disabled={ - isDisabled || !(selectedColorway && selectedSize) || maxQuantity === 0 + isDisabled || !(selectedColor && selectedSize) || maxQuantity === 0 } > BUY NOW @@ -381,7 +386,7 @@ const MerchDetail: React.FC = () => { {ProductNameSection} {renderSizeSection} - {renderColorwaySection} + {renderColorSection} {renderQuantitySection} {purchaseButtons} diff --git a/packages/types/lib/merch.ts b/packages/types/lib/merch.ts index 702c1a00..0d850a36 100644 --- a/packages/types/lib/merch.ts +++ b/packages/types/lib/merch.ts @@ -42,13 +42,23 @@ export interface Order { status: OrderStatus; } +// Cart +export type CartState = { + cart: Cart; + voucher: string | null; + name: string; + billingEmail: string; +}; + export interface Cart { - items: { - id: string; - color: string; - size: string; - quantity: number; - }[]; + items: CartItem[]; +} + +export interface CartItem { + id: string; + color: string; + size: string; + quantity: number; } // Promotion @@ -82,21 +92,6 @@ enum PromoType { FIXED_VALUE = "FIXED_VALUE", } -// Cart -export type CartItem = { - productId: string; - size: string; - colorway: string; - quantity: number; -}; - -export type CartState = { - voucher: string | null; - items: CartItem[]; - name: string; - billingEmail: string; -}; - /* export type ProductInfo = { name: string; From 85797e4ed7d23ce9323e6401b9aa3f386d97b590 Mon Sep 17 00:00:00 2001 From: Chan Wen Xu Date: Mon, 29 May 2023 09:50:20 +0800 Subject: [PATCH 09/60] fix: Use state.cart.items instead of state.items --- .../web/features/merch/context/cart/index.tsx | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/apps/web/features/merch/context/cart/index.tsx b/apps/web/features/merch/context/cart/index.tsx index f047368f..b1f3b1f0 100644 --- a/apps/web/features/merch/context/cart/index.tsx +++ b/apps/web/features/merch/context/cart/index.tsx @@ -25,7 +25,7 @@ export type CartAction = | { type: CartActionType.UPDATE_QUANTITY; payload: CartItem } | { type: CartActionType.REMOVE_ITEM; - payload: { productId: string; size: string; colorway: string }; + payload: { id: string; size: string; color: string }; } | { type: CartActionType.VALID_VOUCHER; payload: string } | { type: CartActionType.REMOVE_VOUCHER; payload: null } @@ -67,7 +67,7 @@ export const cartReducer = ( return { ...state, cart: { - ...state, + ...state.cart, items: idx === -1 ? [...state.cart.items, action.payload] @@ -87,25 +87,31 @@ export const cartReducer = ( ); return { ...state, - items: - idx === -1 - ? [...state.cart.items] - : [ - ...state.cart.items.slice(0, idx), - { ...state.cart.items[idx], quantity }, - ...state.cart.items.slice(idx + 1), - ], + cart: { + ...state.cart, + items: + idx === -1 + ? [...state.cart.items] + : [ + ...state.cart.items.slice(0, idx), + { ...state.cart.items[idx], quantity }, + ...state.cart.items.slice(idx + 1), + ], + }, }; } case CartActionType.REMOVE_ITEM: { const { id, size, color } = action.payload; return { ...state, - items: [ - ...state.cart.items.filter( - (x) => !(x.id === id && x.size === size && x.color == color) - ), - ], + cart: { + ...state.cart, + items: [ + ...state.cart.items.filter( + (x) => !(x.id === id && x.size === size && x.color == color) + ), + ], + }, }; } From ce5aa0b33e83caf17a352c5aa4b95d7578979bab Mon Sep 17 00:00:00 2001 From: Dyllon Date: Mon, 29 May 2023 23:26:48 +0800 Subject: [PATCH 10/60] update CODEOWNERS --- CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index 5ac1819f..efc9abe9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -8,6 +8,8 @@ /apps/cms/** @jamiegoh /apps/merch/** @chanbakjsd /apps/web/** @xJQx +/apps/web/features/merch/** @chanbakjsd +/apps/web/pages/merch/** @chanbakjsd /packages/eslint-custom-config/** @realdyllon /packages/nodelogger/** @realdyllon From 68dd27ee7f790f38d77b14d1cc3358b3e6c3061e Mon Sep 17 00:00:00 2001 From: Dyllon Date: Mon, 29 May 2023 23:32:08 +0800 Subject: [PATCH 11/60] update CODEOWNERS --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index efc9abe9..987b6be1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -20,4 +20,5 @@ /packages/types/lib/cms.ts @jamiegoh /packages/types/lib/merch.ts @chanbakjsd /packages/ui/** @xJQx +/packages/ui/merch/** @chanbakjsd From a43a14cbd6efa484bdd9fbd476d1fd86dd6e992e Mon Sep 17 00:00:00 2001 From: YoNG-Zaii Date: Sun, 21 May 2023 11:27:34 +0800 Subject: [PATCH 12/60] feat(cart): Add cart button component (#95) --- packages/ui/components/merch/CartButton.tsx | 43 ++++++++++++++++++++ packages/ui/components/merch/CartHeader.tsx | 45 --------------------- packages/ui/components/merch/Page.tsx | 6 +-- packages/ui/components/merch/index.tsx | 3 +- packages/ui/components/navbar/MenuItems.tsx | 22 ++++++++-- 5 files changed, 65 insertions(+), 54 deletions(-) create mode 100644 packages/ui/components/merch/CartButton.tsx delete mode 100644 packages/ui/components/merch/CartHeader.tsx diff --git a/packages/ui/components/merch/CartButton.tsx b/packages/ui/components/merch/CartButton.tsx new file mode 100644 index 00000000..fd6104af --- /dev/null +++ b/packages/ui/components/merch/CartButton.tsx @@ -0,0 +1,43 @@ +import Link from "next/link" +import { Icon } from "@chakra-ui/react"; +import routes from "../../../../apps/web/features/merch/constants/routes"; + +const CartButton = () => { + return( + + + + ) +} + +/* +const CartButton = () => { + return( + + + + + + ) +} +*/ + +export default CartButton; \ No newline at end of file diff --git a/packages/ui/components/merch/CartHeader.tsx b/packages/ui/components/merch/CartHeader.tsx deleted file mode 100644 index d0359986..00000000 --- a/packages/ui/components/merch/CartHeader.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { - Box, - Flex, - HStack, - Spacer, - Show, - Hide, - Icon, -} from "@chakra-ui/react"; -import Link from 'next/link'; -import routes from "../../../../apps/web/features/merch/constants/routes"; - - -export const CartHeader = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; \ No newline at end of file diff --git a/packages/ui/components/merch/Page.tsx b/packages/ui/components/merch/Page.tsx index d6364352..ae488c6b 100644 --- a/packages/ui/components/merch/Page.tsx +++ b/packages/ui/components/merch/Page.tsx @@ -1,6 +1,5 @@ import { ReactNode } from "react"; import { Flex, FlexProps, Box } from "@chakra-ui/react"; -import { CartHeader } from "./CartHeader"; type PageProps = FlexProps & { children: ReactNode; @@ -11,14 +10,13 @@ type PageProps = FlexProps & { export const Page = ({ children, - hideHeader = false, contentWidth = "1400px", contentPadding = [4, 6, 8], ...props }: PageProps) => { return ( - {!hideHeader && } + ); -}; \ No newline at end of file +}; diff --git a/packages/ui/components/merch/index.tsx b/packages/ui/components/merch/index.tsx index 9c62fae3..d0f4edee 100644 --- a/packages/ui/components/merch/index.tsx +++ b/packages/ui/components/merch/index.tsx @@ -1,8 +1,7 @@ export * from "./Card" -export * from "./CartHeader" export * from "./EmptyProductView" export * from "./MerchCarousel" export * from "./Page" export * from "./SizeChartDialog" export * from "./SizeOption" -export * from "./skeleton" \ No newline at end of file +export * from "./skeleton" diff --git a/packages/ui/components/navbar/MenuItems.tsx b/packages/ui/components/navbar/MenuItems.tsx index 5a361375..c4650c4b 100644 --- a/packages/ui/components/navbar/MenuItems.tsx +++ b/packages/ui/components/navbar/MenuItems.tsx @@ -1,6 +1,9 @@ -import React from "react"; import { Box, Link, Stack, Text } from "@chakra-ui/react"; +import { useState, useEffect } from "react"; +import { useRouter } from "next/router"; import { MenuLink, MenuLinkProps } from "./MenuLink"; +import CartButton from "../merch/CartButton"; +import routes from "../../../../apps/web/features/merch/constants/routes"; interface MenuItemProps { isOpen?: boolean; @@ -8,6 +11,17 @@ interface MenuItemProps { } export const MenuItems = ({ isOpen = false, links }: MenuItemProps) => { + + const router = useRouter() + const [route, setRoute] = useState('') + + useEffect(() => { + setRoute(router.pathname), + [] + }) + + const regexp = /\/merch*/; + return ( { {/* CTA Button -> Contact */} + {/* If on merch site, change to Cart */} { w="max-content" display={{ base: "block", xl: "block" }} > - Contact + {route.match(regexp) ? : + Contact} ); From 4f52a324dc8026bcf651dbc795a5666cbd5e9fbe Mon Sep 17 00:00:00 2001 From: kingsmil Date: Mon, 5 Jun 2023 11:21:16 +0800 Subject: [PATCH 13/60] Port thank you page (#99) --- apps/web/.env.example | 1 + apps/web/features/merch/services/api.tsx | 7 +- apps/web/next.config.js | 6 + apps/web/package.json | 1 + apps/web/pages/merch/order-summary/[slug].tsx | 199 ++++++++++++++++++ packages/merch-helpers/README.md | 3 + packages/merch-helpers/package.json | 16 ++ packages/merch-helpers/src/index.ts | 2 + packages/merch-helpers/src/lib/orderstatus.ts | 26 +++ .../{merch => merch-helpers/src}/lib/price.ts | 6 +- .../{merch => merch-helpers}/tsconfig.json | 0 packages/types/lib/merch.ts | 2 +- packages/ui/components/merch/OrderItem.tsx | 75 +++++++ .../merch/skeleton/LoadingScreen.tsx | 24 +++ turbo.json | 3 +- yarn.lock | 99 +-------- 16 files changed, 364 insertions(+), 106 deletions(-) create mode 100644 apps/web/pages/merch/order-summary/[slug].tsx create mode 100644 packages/merch-helpers/README.md create mode 100644 packages/merch-helpers/package.json create mode 100644 packages/merch-helpers/src/index.ts create mode 100644 packages/merch-helpers/src/lib/orderstatus.ts rename packages/{merch => merch-helpers/src}/lib/price.ts (91%) rename packages/{merch => merch-helpers}/tsconfig.json (100%) create mode 100644 packages/ui/components/merch/OrderItem.tsx create mode 100644 packages/ui/components/merch/skeleton/LoadingScreen.tsx diff --git a/apps/web/.env.example b/apps/web/.env.example index 748762c1..4bdda633 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,2 +1,3 @@ WORDPRESS_API_URL= NEXT_PUBLIC_MERCH_API_ORIGIN= +NEXT_PUBLIC_FRONTEND_URL='https://clubs.ntu.edu.sg/csec/' diff --git a/apps/web/features/merch/services/api.tsx b/apps/web/features/merch/services/api.tsx index 1633c1c3..e860f6b1 100644 --- a/apps/web/features/merch/services/api.tsx +++ b/apps/web/features/merch/services/api.tsx @@ -1,4 +1,4 @@ -import { Product } from "types/lib/merch"; +import { Product } from 'types' export class Api { private API_ORIGIN: string; @@ -64,8 +64,7 @@ export class Api { } } - /* - async getOrder(userId: string, orderId: string) { + async getOrder(orderId: string) { try { const res = await this.get(`/orders/${orderId}`); console.log("Order Summary response:", res); @@ -74,7 +73,7 @@ export class Api { throw new Error(e); } } - + /* async getOrderHistory(userId: string) { try { const res = await this.get(`/orders/${userId}`); diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 3da9f01d..e1e40879 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -2,6 +2,7 @@ const nextConfig = { reactStrictMode: true, swcMinify: true, + transpilePackages: ['ui', 'merch-helpers'], images: { remotePatterns: [ { @@ -19,6 +20,11 @@ const nextConfig = { hostname: "cdn.ntuscse.com", pathname: "/merch/products/images/**", }, + { + protocol:"https", + hostname: "api.qrserver.com", + pathname: "/merch/order/**" + } ], }, transpilePackages: ["ui"], diff --git a/apps/web/package.json b/apps/web/package.json index b9238f0e..1ed5475c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -23,6 +23,7 @@ "@tanstack/react-query": "^4.26.1", "@tanstack/react-query-devtools": "^4.26.1", "framer-motion": "^7.6.4", + "merch-helpers": "*", "next": "13.4.6", "react": "18.2.0", "react-bootstrap": "^2.5.0", diff --git a/apps/web/pages/merch/order-summary/[slug].tsx b/apps/web/pages/merch/order-summary/[slug].tsx new file mode 100644 index 00000000..eb185eac --- /dev/null +++ b/apps/web/pages/merch/order-summary/[slug].tsx @@ -0,0 +1,199 @@ +import React, { useState } from "react"; +import { useRouter } from "next/router"; +import { Image, Badge, Button, Divider, Flex, Heading, Text, useBreakpointValue } from "@chakra-ui/react"; +import { useQuery } from "@tanstack/react-query"; +import { Page } from "ui/components/merch"; +import { Order, OrderStatus } from "types"; +import { api } from "features/merch/services/api"; +import { routes } from "features/merch/constants/routes"; +import { QueryKeys } from "features/merch/constants/queryKeys"; +import { displayPrice } from "features/merch/functions/currency"; +import Link from "next/link" +import LoadingScreen from "ui/components/merch/skeleton/LoadingScreen"; +import { getOrderStatusColor, renderOrderStatus } from "merch-helpers"; +import OrderItem from "ui/components/merch/OrderItem"; +const OrderSummary: React.FC = () => { +// Check if break point hit. KIV + const isMobile: boolean = useBreakpointValue({ base: true, md: false }) || false; + const router = useRouter(); + const orderSlug = (router.query.slug ?? "" )as string; + + const [showThankYou, setShowThankYou] = useState(false); + const [orderState, setOrderState] = useState(null); + // TODO: Fetch subtotal and total from server. + const [total, setTotal] = useState(0); + // Fetch and check if cart item is valid. Number(item.price) set to convert string to num + const { isLoading } = useQuery( + [QueryKeys.ORDER, orderSlug], + () => api.getOrder(orderSlug), + { + onSuccess: (data: Order) => { + console.log(data); + setOrderState(data); + setTotal( + data.items.reduce((acc, item) => { + return Number(item.price) * item.quantity + acc; + }, 0) + ); + setShowThankYou(true); + }, + } + ); + + const renderThankYouMessage = () => ( + <> + THANK YOU + Thank you for your purchase. We have received your order. + + + + + + ); + const renderOrderSummary = () => ( + <> + + {showThankYou && renderThankYouMessage()} + + + +
+ + + + {renderOrderStatus(orderState?.status ?? OrderStatus.PENDING_PAYMENT)} + + Order Number + + {orderState?.id.split("-")[0]} + + + {orderState?.id} + + + Order date:{" "} + {orderState?.transaction_time + ? new Date(`${orderState.transaction_time}`).toLocaleString( + "en-sg" + ) + : ""} + + {/*Last update: {orderState?.lastUpdate}*/} + + +
+
+ + + + Order Number + + {renderOrderStatus(orderState?.status ?? OrderStatus.PENDING_PAYMENT)} + + + + {orderState?.id.split("-")[0]} + + + {orderState?.id} + + + + + Order date:{" "} + {orderState?.transaction_time + ? new Date(`${orderState.transaction_time}`).toLocaleString( + "en-sg" + ) + : ""} + + {/*Last update: {orderState?.lastUpdate}*/} + + +
+ + {/*{orderState?.items.map((item) => (*/} + {/* */} + {/*))}*/} + + {orderState? : Order Not Found} + + + + + Item Subtotal: + Voucher Discount: + Total: + + + {displayPrice(total)} + + {/*{displayPrice(*/} + {/* (orderState?.billing?.subtotal ?? 0) -*/} + {/* (orderState?.billing?.total ?? 0)*/} + {/*)}*/} + 0 + + {displayPrice(total)} + + +
+ + + {/* TODO: QR Code generator based on Param. */} + QRCode + + Please screenshot this QR code and show it at SCSE Lounge to collect your order. + Alternatively, show the email receipt you have received. + + + For any assistance, please contact our email address: + merch@ntuscse.com + + + + ); + const renderSummaryPage = () => { + if (isLoading) return ; + //rmb to change this v + if (orderState === undefined || orderState === null){return ;} + return renderOrderSummary(); + }; + return {renderSummaryPage()}; +} +export default OrderSummary diff --git a/packages/merch-helpers/README.md b/packages/merch-helpers/README.md new file mode 100644 index 00000000..fc951a31 --- /dev/null +++ b/packages/merch-helpers/README.md @@ -0,0 +1,3 @@ +# merch-helpers + +todo... diff --git a/packages/merch-helpers/package.json b/packages/merch-helpers/package.json new file mode 100644 index 00000000..8f415bae --- /dev/null +++ b/packages/merch-helpers/package.json @@ -0,0 +1,16 @@ +{ + "name": "merch-helpers", + "version": "0.0.1", + "main": "src/index.ts", + "license": "Apache-2.0", + "scripts": { + "lint": "TIMING=1 eslint \"**/*.ts*\"", + "lint:fix": "TIMING=1 eslint --fix \"**/*.ts*\"" + }, + "devDependencies": { + "eslint": "^7.32.0", + "eslint-config-custom": "*", + "tsconfig": "*", + "typescript": "^4.5.2" + } +} diff --git a/packages/merch-helpers/src/index.ts b/packages/merch-helpers/src/index.ts new file mode 100644 index 00000000..756d6234 --- /dev/null +++ b/packages/merch-helpers/src/index.ts @@ -0,0 +1,2 @@ +export * from "./lib/orderstatus"; +export * from "./lib/price"; diff --git a/packages/merch-helpers/src/lib/orderstatus.ts b/packages/merch-helpers/src/lib/orderstatus.ts new file mode 100644 index 00000000..c0f43f9c --- /dev/null +++ b/packages/merch-helpers/src/lib/orderstatus.ts @@ -0,0 +1,26 @@ +import { OrderStatus } from 'types' +export const renderOrderStatus = (status: OrderStatus) => { + switch (status) { + case OrderStatus.ORDER_COMPLETED: + return "Order Collected"; + case OrderStatus.PAYMENT_COMPLETED: + return "Processing"; + case OrderStatus.PENDING_PAYMENT: + return "Order Received"; + default: + return "Item Delayed"; + } +}; + +export const getOrderStatusColor = (status: OrderStatus) => { + switch (status) { + case OrderStatus.ORDER_COMPLETED: + return "green.500"; + case OrderStatus.PAYMENT_COMPLETED: + return "primary.400"; + case OrderStatus.PENDING_PAYMENT: + return "primary.600"; + default: + return "red.500"; + } +}; diff --git a/packages/merch/lib/price.ts b/packages/merch-helpers/src/lib/price.ts similarity index 91% rename from packages/merch/lib/price.ts rename to packages/merch-helpers/src/lib/price.ts index 49abb347..97d46100 100644 --- a/packages/merch/lib/price.ts +++ b/packages/merch-helpers/src/lib/price.ts @@ -1,4 +1,4 @@ -import { Cart, PricedCart, Product, Promotion } from "types"; +import { Cart, PricedCart, Product, Promotion, PromoType } from "types"; export const calculatePricing = ( products: Product[], @@ -33,10 +33,10 @@ export const calculatePricing = ( continue; } switch (discount.promoType) { - case FIXED_VALUE: + case PromoType.FIXED_VALUE: itemPrice -= discount.promoValue; break; - case PERCENTAGE: + case PromoType.PERCENTAGE: itemPrice *= 1 - discount.promoValue; itemPrice = Math.floor(itemPrice); break; diff --git a/packages/merch/tsconfig.json b/packages/merch-helpers/tsconfig.json similarity index 100% rename from packages/merch/tsconfig.json rename to packages/merch-helpers/tsconfig.json diff --git a/packages/types/lib/merch.ts b/packages/types/lib/merch.ts index 0d850a36..77cadd56 100644 --- a/packages/types/lib/merch.ts +++ b/packages/types/lib/merch.ts @@ -87,7 +87,7 @@ export interface PricedCart { }[]; } -enum PromoType { +export enum PromoType { PERCENTAGE = "PERCENTAGE", FIXED_VALUE = "FIXED_VALUE", } diff --git a/packages/ui/components/merch/OrderItem.tsx b/packages/ui/components/merch/OrderItem.tsx new file mode 100644 index 00000000..75bc6582 --- /dev/null +++ b/packages/ui/components/merch/OrderItem.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import { Flex, Grid, GridItem, Image, Text, Divider, Box } from "@chakra-ui/react"; +import { displayPrice } from "web/features/merch/functions/currency"; +import { Order } from "types"; + +export type OrderItemProps = { + isMobile: boolean; + orderData: Order; +}; + +const OrderItem: React.FC = (props: OrderItemProps) => { + const { isMobile, orderData } = props; + + const flexItemConfig = { + alignItems: "center", + h: isMobile ? "auto" : 100, + justifyContent: isMobile ? "start" : "center", + }; + return ( + <> + {orderData.items.map((data, index) => ( +
+ + + + + + + + {data?.name} + + Size: + + {data.size} + + + + Color: + + {data.color} + + + + + + + + {isMobile && "Unit Price:"} {displayPrice(Number(data?.price) ?? 0)} + + + + + + + {isMobile && "Quantity:"} x{data?.quantity ?? 0} + + + + + + + {isMobile && "Subtotal:"} {displayPrice(Number(data.price) * data.quantity)} + + + + + + +
+ ))} + + ); +} + +export default OrderItem; diff --git a/packages/ui/components/merch/skeleton/LoadingScreen.tsx b/packages/ui/components/merch/skeleton/LoadingScreen.tsx new file mode 100644 index 00000000..2622b988 --- /dev/null +++ b/packages/ui/components/merch/skeleton/LoadingScreen.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { Flex, Spinner, Text } from "@chakra-ui/react"; + +export type LoadingScreenType = { + minH?: string; + text: string; +}; +const LoadingScreen: React.FC = (props) => { + const { minH = "50vh", text = "" } = props; + return ( + + + {text} + + ); +}; + +export default LoadingScreen; diff --git a/turbo.json b/turbo.json index d890d86d..1f8d1c8f 100644 --- a/turbo.json +++ b/turbo.json @@ -17,7 +17,8 @@ "FRONTEND_STAGING_DOMAIN", "PRODUCT_TABLE_NAME", "ORDER_TABLE_NAME", - "NEXT_PUBLIC_MERCH_API_ORIGIN" + "NEXT_PUBLIC_MERCH_API_ORIGIN", + "NEXT_PUBLIC_FRONTEND_URL" ] }, "build": { diff --git a/yarn.lock b/yarn.lock index 102a0386..7508f649 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8665,23 +8665,6 @@ copy-anything@^3.0.2: dependencies: is-what "^4.1.8" -copy-concurrently@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" - integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== - dependencies: - aproba "^1.1.1" - fs-write-stream-atomic "^1.0.8" - iferr "^0.1.5" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.0" - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw== - copy-to-clipboard@3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae" @@ -8744,17 +8727,6 @@ cors@^2.8.5: object-assign "^4" vary "^1" -cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.7.2" - cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" @@ -11934,27 +11906,7 @@ is-what@^4.1.8: resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.8.tgz#0e2a8807fda30980ddb2571c79db3d209b14cbe4" integrity sha512-yq8gMao5upkPoGEU9LsB2P+K3Kt8Q3fQFCGyNCWOAnJAMzEXVV9drYb0TXr42TTliLLhKIBvulgAXgtLLnwzGA== -is-whitespace-character@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7" - integrity sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w== - -is-window@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d" - integrity sha512-uj00kdXyZb9t9RcAUAwMZAnkBUwdYGhYlt7djMXhfyhUCzwNba50tIiBKR7q0l7tdoBtFVw/3JmLY6fI3rmZmg== - -is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -is-word-character@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.4.tgz#ce0e73216f98599060592f62ff31354ddbeb0230" - integrity sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA== - -is-wsl@^1.1.0, is-wsl@^2.1.1, is-wsl@^2.2.0: +is-wsl@^2.1.1, is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== @@ -15785,34 +15737,11 @@ remark-slug@^6.0.0: mdast-util-to-string "^1.0.0" unist-util-visit "^2.0.0" -remark-squeeze-paragraphs@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/remark-squeeze-paragraphs/-/remark-squeeze-paragraphs-4.0.0.tgz#76eb0e085295131c84748c8e43810159c5653ead" - integrity sha512-8qRqmL9F4nuLPIgl92XUuxI3pFxize+F1H0e/W3llTk0UsjJaj01+RrirkMw7P21RKe4X6goQhYRSvNWX+70Rw== - dependencies: - mdast-squeeze-paragraphs "^4.0.0" - remove-accents@0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5" integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA== -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw== - -renderkid@^2.0.4: - version "2.0.7" - resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.7.tgz#464f276a6bdcee606f4a15993f9b29fc74ca8609" - integrity sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ== - dependencies: - css-select "^4.1.3" - dom-converter "^0.2.0" - htmlparser2 "^6.1.0" - lodash "^4.17.21" - strip-ansi "^3.0.1" - renderkid@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" @@ -16546,25 +16475,6 @@ ssr-window@^4.0.2: resolved "https://registry.yarnpkg.com/ssr-window/-/ssr-window-4.0.2.tgz#dc6b3ee37be86ac0e3ddc60030f7b3bc9b8553be" integrity sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ== -ssri@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" - integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q== - dependencies: - figgy-pudding "^3.5.1" - -ssri@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" - integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== - dependencies: - minipass "^3.1.1" - -stable@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" - integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== - stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" @@ -17676,16 +17586,11 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" -use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0: +use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - utf8-byte-length@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" From f9b24355b59596a4e92540f5ce2e578e0f65bfda Mon Sep 17 00:00:00 2001 From: Chan Wen Xu Date: Fri, 9 Jun 2023 22:48:43 +0800 Subject: [PATCH 14/60] fix: Handle 404 in orders --- apps/merch/src/db/dynamodb.ts | 28 +++++++++++++++---- apps/merch/src/db/index.ts | 1 + apps/merch/src/db/orders.ts | 10 +++---- apps/merch/src/routes/orders.ts | 5 +++- apps/merch/src/routes/products.ts | 8 +++++- apps/web/features/merch/services/api.tsx | 28 +++++++++++++------ .../{order-summary => orders}/[slug].tsx | 10 +++---- packages/types/lib/merch.ts | 2 +- 8 files changed, 64 insertions(+), 28 deletions(-) rename apps/web/pages/merch/{order-summary => orders}/[slug].tsx (95%) diff --git a/apps/merch/src/db/dynamodb.ts b/apps/merch/src/db/dynamodb.ts index 832d6a80..3253c62d 100644 --- a/apps/merch/src/db/dynamodb.ts +++ b/apps/merch/src/db/dynamodb.ts @@ -1,13 +1,23 @@ import { - DynamoDB, - GetItemCommand, - ScanCommand, + DynamoDB, + GetItemCommand, + ScanCommand } from "@aws-sdk/client-dynamodb"; import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; import { Logger } from "nodelogger"; import { Order } from "types"; -const ORDER_TABLE_NAME = process.env.ORDER_TABLE_NAME; +const ORDER_TABLE_NAME = process.env.ORDER_TABLE_NAME ?? ""; + +// Due to Babel transpiling, extending Error will cause instanceof to not work +// properly. +export class NotFoundError { + message: string; + + constructor(item = "") { + this.message = "Item " + item + " not found."; + } +} export const getOrders = () => readTable(ORDER_TABLE_NAME); export const getOrder = (id: string) => @@ -25,7 +35,10 @@ export const readTable = async (tableName: string): Promise => { `LastEvaluatedKey was not undefined when querying ${tableName}, dropping additional items` ); } - return response.Items.map((item) => unmarshall(item)); + if (!response.Items) { + return []; + } + return response.Items.map((item) => unmarshall(item)) as T[]; }; // readItem retrieves the specified item from the table. @@ -39,5 +52,8 @@ export const readItem = async ( Key: marshall({ [keyID]: key }), }); const response = await client.send(command); - return unmarshall(response.Item); + if (!response.Item) { + throw new NotFoundError(key); + } + return unmarshall(response.Item) as T; }; diff --git a/apps/merch/src/db/index.ts b/apps/merch/src/db/index.ts index c2df39f8..fad2ac14 100644 --- a/apps/merch/src/db/index.ts +++ b/apps/merch/src/db/index.ts @@ -1,2 +1,3 @@ +export { NotFoundError } from "./dynamodb"; export * from "./orders"; export * from "./products"; diff --git a/apps/merch/src/db/orders.ts b/apps/merch/src/db/orders.ts index 18488984..d76f7b24 100644 --- a/apps/merch/src/db/orders.ts +++ b/apps/merch/src/db/orders.ts @@ -24,7 +24,7 @@ interface DynamoOrder { export const getOrder = async (id: string) => { const dynamoOrder = await readItem( - ORDER_TABLE_NAME, + ORDER_TABLE_NAME ?? "", id, "orderID" ); @@ -32,7 +32,7 @@ export const getOrder = async (id: string) => { }; const decodeOrder = (order: DynamoOrder): Order => { - let date: ?string; + let date: string | null; try { date = new Date(order.orderDateTime).toISOString(); } catch (e) { @@ -45,15 +45,15 @@ const decodeOrder = (order: DynamoOrder): Order => { id: item.id || "", name: item.name || "", category: item.product_category || "", - image: item.image || null, + image: item.image || undefined, color: item.colorway || "", size: item.size || "", price: item.price || 0, quantity: item.quantity || 1, })), - status: order.status || PENDING_PAYMENT, + status: order.status || OrderStatus.PENDING_PAYMENT, customer_email: order.customerEmail || "", transaction_id: order.transactionID || "", - transaction_time: date, + transaction_time: date || undefined, }; }; diff --git a/apps/merch/src/routes/orders.ts b/apps/merch/src/routes/orders.ts index b1819766..bf6b57d1 100644 --- a/apps/merch/src/routes/orders.ts +++ b/apps/merch/src/routes/orders.ts @@ -1,6 +1,6 @@ import { Router } from "express"; -import { getOrder } from "../db"; import { Order } from "types"; +import { getOrder, NotFoundError } from "../db"; const router = Router(); @@ -10,6 +10,9 @@ router.get("/:id", (req, res) => { res.json(censorDetails(order)); }) .catch((e) => { + if (e instanceof NotFoundError) { + return res.status(404).json({ error: "NOT_FOUND" }); + } console.warn(e); res.status(500).json({ error: "INTERNAL_SERVER_ERROR" }); }); diff --git a/apps/merch/src/routes/products.ts b/apps/merch/src/routes/products.ts index 5b064762..14278fb9 100644 --- a/apps/merch/src/routes/products.ts +++ b/apps/merch/src/routes/products.ts @@ -1,6 +1,6 @@ import { Router } from "express"; -import { getProduct, getProducts } from "../db"; import { Product } from "types"; +import { getProduct, getProducts, NotFoundError } from "../db"; const router = Router(); @@ -10,6 +10,9 @@ router.get("/", (req, res) => { res.json({ products }); }) .catch((e) => { + if (e instanceof NotFoundError) { + return res.status(404).json({ error: "NOT_FOUND" }); + } console.warn(e); res.status(500).json({ error: "INTERNAL_SERVER_ERROR" }); }); @@ -21,6 +24,9 @@ router.get("/:id", (req, res) => { res.json(product); }) .catch((e) => { + if (e instanceof NotFoundError) { + return res.status(404).json({ error: "NOT_FOUND" }); + } console.warn(e); res.status(500).json({ error: "INTERNAL_SERVER_ERROR" }); }); diff --git a/apps/web/features/merch/services/api.tsx b/apps/web/features/merch/services/api.tsx index e860f6b1..6573271d 100644 --- a/apps/web/features/merch/services/api.tsx +++ b/apps/web/features/merch/services/api.tsx @@ -1,4 +1,4 @@ -import { Product } from 'types' +import { Product } from "types"; export class Api { private API_ORIGIN: string; @@ -13,10 +13,13 @@ export class Api { } // http methods - async get(urlPath: string): Promise> { + async get(urlPath: string): Promise { const response = await fetch(`${this.API_ORIGIN}${urlPath}`); - const convert = response.json() as unknown; // Convert to unknown type - return convert as Record; + const convert = await response.json() as (T & {error: undefined}) | { error: string }; + if (convert.error) { + throw new Error(`Server error: ${convert.error}`); + } + return convert as T; } /* @@ -42,12 +45,12 @@ export class Api { // eslint-disable-next-line class-methods-use-this async getProducts(): Promise { try { - const res = await this.get("/products"); + const res = await this.get<{products: Product[]}>("/products"); console.log("product-list", res); - return res?.products ?? []; + return res.products; } catch (e) { if (e instanceof Error) { - throw new Error(e.message); + throw e; } return []; } @@ -60,17 +63,24 @@ export class Api { console.log("product res", res); return res; } catch (e: any) { - throw new Error(e); + if (e instanceof Error) { + throw e; + } } } async getOrder(orderId: string) { try { + if (!orderId) { + throw new Error("No order ID"); + } const res = await this.get(`/orders/${orderId}`); console.log("Order Summary response:", res); return res; } catch (e: any) { - throw new Error(e); + if (e instanceof Error) { + throw e; + } } } /* diff --git a/apps/web/pages/merch/order-summary/[slug].tsx b/apps/web/pages/merch/orders/[slug].tsx similarity index 95% rename from apps/web/pages/merch/order-summary/[slug].tsx rename to apps/web/pages/merch/orders/[slug].tsx index eb185eac..9cb0fb38 100644 --- a/apps/web/pages/merch/order-summary/[slug].tsx +++ b/apps/web/pages/merch/orders/[slug].tsx @@ -16,7 +16,7 @@ const OrderSummary: React.FC = () => { // Check if break point hit. KIV const isMobile: boolean = useBreakpointValue({ base: true, md: false }) || false; const router = useRouter(); - const orderSlug = (router.query.slug ?? "" )as string; + const orderSlug = router.query.slug as string | undefined; const [showThankYou, setShowThankYou] = useState(false); const [orderState, setOrderState] = useState(null); @@ -25,14 +25,14 @@ const OrderSummary: React.FC = () => { // Fetch and check if cart item is valid. Number(item.price) set to convert string to num const { isLoading } = useQuery( [QueryKeys.ORDER, orderSlug], - () => api.getOrder(orderSlug), + () => api.getOrder(orderSlug ?? ""), { + enabled: !!orderSlug, onSuccess: (data: Order) => { - console.log(data); setOrderState(data); setTotal( data.items.reduce((acc, item) => { - return Number(item.price) * item.quantity + acc; + return item.price * item.quantity + acc; }, 0) ); setShowThankYou(true); @@ -169,7 +169,7 @@ const OrderSummary: React.FC = () => { QRCode Date: Mon, 22 May 2023 22:29:26 +0800 Subject: [PATCH 15/60] feat: Init checkout feature (#102) --- apps/merch/package.json | 5 +- apps/merch/src/db/dynamodb.ts | 57 +++++++++- apps/merch/src/db/orders.ts | 64 ++++++++--- apps/merch/src/db/products.ts | 27 ++++- apps/merch/src/routes/checkout.ts | 169 +++++++++++++++++++++++++++++ apps/merch/src/routes/quotation.ts | 24 ++++ packages/types/lib/merch.ts | 35 ++++-- turbo.json | 12 ++ yarn.lock | 18 +++ 9 files changed, 381 insertions(+), 30 deletions(-) create mode 100644 apps/merch/src/routes/checkout.ts create mode 100644 apps/merch/src/routes/quotation.ts diff --git a/apps/merch/package.json b/apps/merch/package.json index b2bee8ce..e07084d4 100644 --- a/apps/merch/package.json +++ b/apps/merch/package.json @@ -17,7 +17,9 @@ "@aws-sdk/util-dynamodb": "^3.289.0", "cors": "^2.8.5", "express": "^4.17.1", - "nodelogger": "*" + "nodelogger": "*", + "stripe": "^12.5.0", + "uuid": "^9.0.0" }, "devDependencies": { "@swc/core": "^1.3.64", @@ -25,6 +27,7 @@ "@types/cors": "^2.8.13", "@types/express": "^4.17.9", "@types/morgan": "^1.9.4", + "@types/uuid": "^9.0.1", "cookie-parser": "^1.4.6", "morgan": "^1.10.0", "nodemon": "^2.0.6", diff --git a/apps/merch/src/db/dynamodb.ts b/apps/merch/src/db/dynamodb.ts index 3253c62d..b269609d 100644 --- a/apps/merch/src/db/dynamodb.ts +++ b/apps/merch/src/db/dynamodb.ts @@ -1,7 +1,9 @@ import { - DynamoDB, - GetItemCommand, - ScanCommand + DynamoDB, + GetItemCommand, + PutItemCommand, + ScanCommand, + UpdateItemCommand, } from "@aws-sdk/client-dynamodb"; import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; import { Logger } from "nodelogger"; @@ -57,3 +59,52 @@ export const readItem = async ( } return unmarshall(response.Item) as T; }; + +// writeItem adds the given item to the specified DynamoDB table +export const writeItem = async ( + tableName: string, + item: T +): Promise => { + const command = new PutItemCommand({ + TableName: tableName, + Item: marshall(item), + }); + try { + await client.send(command); + } catch (error: any) { + if (error.code === 'ConditionalCheckFailedException') { + Logger.warn(`Item already exists in table ${tableName}`); + return; + } + throw error; + } +}; + +// updateItem updates the specified item in the table. +export const updateItem = async ( + tableName: string, + key: string, + updateExpression?: string, + conditionExpression?: string, + expressionAttributeValues?: Record, + expressionAttributeNames?: Record, + keyID = "id" +): Promise => { + const command = new UpdateItemCommand({ + TableName: tableName, + Key: marshall({ [keyID]: key }), + ConditionExpression: conditionExpression, + UpdateExpression: updateExpression, + ExpressionAttributeValues: marshall(expressionAttributeValues), + ExpressionAttributeNames: expressionAttributeNames, + }); + try { + await client.send(command); + } catch (error: any) { + if (error.code === "ConditionalCheckFailedException") { + Logger.warn(`Item does not exist in table ${tableName}`); + return; + } + throw error; + } +}; diff --git a/apps/merch/src/db/orders.ts b/apps/merch/src/db/orders.ts index d76f7b24..b79a5dfb 100644 --- a/apps/merch/src/db/orders.ts +++ b/apps/merch/src/db/orders.ts @@ -1,21 +1,25 @@ -import { readItem } from "./dynamodb"; -import { Order, OrderStatus } from "types"; +import { readItem, writeItem } from "./dynamodb"; +import { v4 as uuidv4 } from "uuid"; +import { Order, OrderItem, OrderStatus, OrderHoldEntry } from "types"; -const ORDER_TABLE_NAME = process.env.ORDER_TABLE_NAME; +const ORDER_TABLE_NAME = process.env.ORDER_TABLE_NAME || ""; +const ORDER_HOLD_TABLE_NAME = process.env.ORDER_HOLD_TABLE_NAME || ""; + +interface DynamoOrderItem { + id: string; + image: string; + quantity: number; + size: string; + price: number; + name: string; + colorway: string; + product_category: string; +} interface DynamoOrder { orderID: string; paymentGateway: string; - orderItems: { - image: string; - quantity: number; - size: string; - price: number; - name: string; - colorway: string; - id: string; - product_category: string; - }[]; + orderItems: DynamoOrderItem[]; status: OrderStatus; customerEmail: string; transactionID: string; @@ -57,3 +61,37 @@ const decodeOrder = (order: DynamoOrder): Order => { transaction_time: date || undefined, }; }; + +const encodeOrderItem = (item: OrderItem): DynamoOrderItem => ({ + id: item.id, + image: item.image ? item.image : "", + quantity: item.quantity, + size: item.size, + price: item.price, + name: item.name, + colorway: item.color, + product_category: item.category, +}); + +const encodeOrder = (order: Order): DynamoOrder => ({ + orderID: order.id, + paymentGateway: order.payment_method || "", + orderItems: order.items.map(encodeOrderItem), + status: order.status || OrderStatus.PENDING_PAYMENT, + customerEmail: order.customer_email || "", + transactionID: order.transaction_id || "", + orderDateTime: order.transaction_time + ? new Date(order.transaction_time).toISOString() + : new Date().toISOString(), +}); + +export const createOrder = async (order: Order): Promise => { + const dynamoOrder = encodeOrder(order); + dynamoOrder.orderID = uuidv4(); + await writeItem(ORDER_TABLE_NAME, dynamoOrder); + return decodeOrder(dynamoOrder); +}; + +export const createOrderHoldEntry = async (orderHoldEntry: OrderHoldEntry): Promise => { + await writeItem(ORDER_HOLD_TABLE_NAME, orderHoldEntry); +}; \ No newline at end of file diff --git a/apps/merch/src/db/products.ts b/apps/merch/src/db/products.ts index b392817e..296fc0a1 100644 --- a/apps/merch/src/db/products.ts +++ b/apps/merch/src/db/products.ts @@ -1,7 +1,7 @@ -import { readItem, readTable } from "./dynamodb"; +import { readItem, readTable, updateItem } from "./dynamodb"; import { Product } from "types"; -const PRODUCT_TABLE_NAME = process.env.PRODUCT_TABLE_NAME; +const PRODUCT_TABLE_NAME = process.env.PRODUCT_TABLE_NAME || ""; export const getProducts = async () => { const dynamoProducts = await readTable(PRODUCT_TABLE_NAME); @@ -43,3 +43,26 @@ const decodeProduct = (product: DynamoProduct): Product => { stock: product.stock || {}, }; }; + +export const incrementStockCount = async (item_id: string, increment_value: number, size: string, color: string): Promise => { + const update_expression = `ADD stock.#color.#size :incrementValue`; + const condition_expression = "is_available = :isAvailable AND stock.#color.#size >= :incrementValue"; + const expression_attribute_values = { + ":incrementValue": { "N": String(increment_value) }, + ":isAvailable": { "BOOL": true }, + }; + const expression_attribute_names = { + "#color": color, + "#size": size, + }; + await updateItem( + PRODUCT_TABLE_NAME, + item_id, + update_expression, + condition_expression, + expression_attribute_values, + expression_attribute_names, + "id", + ); +}; + diff --git a/apps/merch/src/routes/checkout.ts b/apps/merch/src/routes/checkout.ts new file mode 100644 index 00000000..e11d77ba --- /dev/null +++ b/apps/merch/src/routes/checkout.ts @@ -0,0 +1,169 @@ +import { Router } from "express"; +import { Cart, Order, OrderItem, OrderStatus, PriceModel, ReservedProduct } from "types"; +import { createOrder, createOrderHoldEntry, getProduct, incrementStockCount } from "../db"; +import { v4 as uuidv4 } from 'uuid'; +import { Stripe } from 'stripe'; + +const router = Router(); + +const DEFAULT_ORDER_EXPIRY_TIME = "24"; +const frontendUrl: string = process.env.FRONTEND_STAGING_DOMAIN || ""; + +const STRIPE_KEY = process.env.STRIPE_SECRET_KEY || ""; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call +const stripe = new Stripe(STRIPE_KEY, { + apiVersion: '2022-11-15', +}); + +type CheckoutBody = Cart & { + email: string; +} + +router.post("/", (req, res) => { + const body = req.body as CheckoutBody; + const { email, items } = body; + if (!email) { + throw new Error("Billing email must be provided when checking out"); + } + if (!items.length) { + throw new Error("Cart must not be empty when checking out"); + } + + const itemsProductsPromise = generateOrderItemsFromCart(body); + const orderID = uuidv4(); + const pricePromise = itemsProductsPromise.then(itemsProducts => { + const price = calcCartValue(itemsProducts); + return { itemsProducts, price }; + }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + const paymentIntentPromise = pricePromise.then(({ itemsProducts, price }) => stripe.paymentIntents.create({ + payment_method_types: ['paynow'], + payment_method_data: { type: 'paynow' }, + amount: Math.floor(price.grandTotal * 100), // stripe payment amounts are in cents + currency: 'sgd', + receipt_email: body.email, + description: `SCSE Merch Purchase:\n${describeCart(itemsProducts, orderID)}`, + })); + const expiryPromise = Promise.resolve(process.env.ORDER_EXPIRY_TIME ?? DEFAULT_ORDER_EXPIRY_TIME) + .then(expiryTime => new Date(new Date().getTime() + (parseInt(expiryTime) * 60 * 60 * 1000)).toISOString()); + + Promise.all([itemsProductsPromise, pricePromise, paymentIntentPromise, expiryPromise]) + .then(([itemsProducts, price, paymentIntent, expiry]) => { + const orderID = uuidv4(); + const orderDateTime = new Date().toISOString(); + const customerEmail = body.email; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const transactionID = paymentIntent.id; + const paymentPlatform = 'stripe'; + const status = OrderStatus.PENDING_PAYMENT; + + const order: Order = { + id: orderID, + items: itemsProducts, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + transaction_id: transactionID, + transaction_time: orderDateTime, + payment_method: paymentPlatform, + customer_email: customerEmail, + status: status, + }; + + for (const orderItem of itemsProducts) { + void incrementStockCount(orderItem.id, -orderItem.quantity, orderItem.size, orderItem.color); + } + + const reservedProducts: ReservedProduct[] = body.items.map(item => { + return { + productID: item.productId, + qty: item.quantity + }; + }); + const orderHoldEntry = { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + transactionID, + expiry: Math.floor(new Date(expiry).getTime() / 1000), + reservedProducts, + }; + + createOrder(order) + .then(() => createOrderHoldEntry(orderHoldEntry)) + .then(() => { + res.json({ + orderId: orderID, + items: itemsProducts, + price: price, + payment: { + paymentGateway: 'stripe', + clientSecret: paymentIntent.client_secret, + }, + email: body.email, + expiry: Math.floor(new Date(expiry).getTime() / 1000), + }); + }) + .catch(err => { + console.error('Error creating order hold entry:', err); + res.status(500).json({ detail: 'Error unable to check out.' }); + }); + }) + .catch(e => { + if (e.code === 'ConditionalCheckFailedException') { + res.status(400).json({ detail: 'Current quantity cannot be less than 0 and must be available for sale' }); + } + }) +}) + + +export function calcCartValue(cartOrderItems: OrderItem[]): PriceModel { + let subtotal = 0; + for (const i of cartOrderItems) { + subtotal += i.price * i.quantity; + } + + const grandTotal = subtotal; + + return { + currency: 'sgd', + subtotal, + discount: 0, // todo + grandTotal, + }; +} + +function describeCart(cartOrderItems: OrderItem[], orderId: string): string { + const entries = [ + `${frontendUrl}/order-summary/${orderId} | ` + ]; + + for (const entry of cartOrderItems) { + const price = entry.price * entry.quantity; + let name = entry.name; + if (entry.size) { + name = `${entry.name} (Size: ${entry.size.toUpperCase()})`; + } + entries.push(`${name} x${entry.quantity} - S$${(price / 100).toFixed(2)}`); + } + + return entries.join('\n'); +} + +export async function generateOrderItemsFromCart(cart: Cart): Promise { + const cartOrderItems: OrderItem[] = []; + for (const item of cart.items) { + const product = await getProduct(item.productId); + if (!product) { + throw new Error(`productId ${item.productId} could not be found`); + } + cartOrderItems.push({ + id: product.id, + name: product.name, + price: product.price, + image: product.images[0], + category: product.category, + quantity: item.quantity, + size: item.size, + color: item.colorway + }); + } + return cartOrderItems; +} diff --git a/apps/merch/src/routes/quotation.ts b/apps/merch/src/routes/quotation.ts new file mode 100644 index 00000000..0873bf79 --- /dev/null +++ b/apps/merch/src/routes/quotation.ts @@ -0,0 +1,24 @@ +import { Router } from "express"; +import { Cart } from "types"; +import { calcCartValue, generateOrderItemsFromCart } from "./checkout"; + +const router = Router(); + +type QuotationBody = Cart + +router.post("/", (req, res) => { + const body = req.body as QuotationBody; + const itemsProductsPromise = generateOrderItemsFromCart(body); + itemsProductsPromise.then(itemsProducts => { + const price = calcCartValue(itemsProducts); + res.json({ + items: itemsProducts, + price: price, + }) + }) + .catch(err => { + console.error('Error creating quotation:', err); + res.status(500).json({ detail: 'Error unable to check out.' }); + }); +}) + diff --git a/packages/types/lib/merch.ts b/packages/types/lib/merch.ts index 89121fc4..87a1abfa 100644 --- a/packages/types/lib/merch.ts +++ b/packages/types/lib/merch.ts @@ -25,18 +25,9 @@ export enum OrderStatus { export interface Order { id: string; - items: { - id: string; - name: string; - category: string; - image?: string; - color: string; - size: string; - price: number; - quantity: number; - }[]; + items: OrderItem[]; transaction_id: string; - transaction_time?: string; + transaction_time: string | null; payment_method: string; customer_email: string; status: OrderStatus; @@ -62,6 +53,17 @@ export interface CartItem { } // Promotion +export interface OrderItem { + id: string; + name: string; + category: string; + image?: string; + color: string; + size: string; + price: number; + quantity: number; +} + export interface Promotion { promoCode: string; maxRedemptions: number; @@ -156,3 +158,14 @@ export type CheckoutResponseDto = { export type ProductInfoMap = Record; */ + +export type ReservedProduct = { + productID: string; + qty: number; +}; + +export type OrderHoldEntry = { + transactionID: string; + expiry: number; + reservedProducts: ReservedProduct[]; +} diff --git a/turbo.json b/turbo.json index 1f8d1c8f..a0e31000 100644 --- a/turbo.json +++ b/turbo.json @@ -17,6 +17,9 @@ "FRONTEND_STAGING_DOMAIN", "PRODUCT_TABLE_NAME", "ORDER_TABLE_NAME", + "ORDER_HOLD_TABLE_NAME", + "STRIPE_SECRET_KEY", + "ORDER_EXPIRY_TIME", "NEXT_PUBLIC_MERCH_API_ORIGIN", "NEXT_PUBLIC_FRONTEND_URL" ] @@ -38,6 +41,9 @@ "PRODUCT_TABLE_NAME", "ORDER_TABLE_NAME", "CORS_ORIGIN", + "STRIPE_SECRET_KEY", + "ORDER_HOLD_TABLE_NAME", + "ORDER_EXPIRY_TIME", "NEXT_PUBLIC_MERCH_API_ORIGIN" ], "outputs": ["dist/**", "build/**", "out/**", ".next/**"] @@ -62,6 +68,9 @@ "PRODUCT_TABLE_NAME", "ORDER_TABLE_NAME", "CORS_ORIGIN", + "ORDER_HOLD_TABLE_NAME", + "STRIPE_SECRET_KEY", + "ORDER_EXPIRY_TIME", "NEXT_PUBLIC_MERCH_API_ORIGIN" ] }, @@ -81,6 +90,9 @@ "PRODUCT_TABLE_NAME", "ORDER_TABLE_NAME", "CORS_ORIGIN", + "ORDER_HOLD_TABLE_NAME", + "STRIPE_SECRET_KEY", + "ORDER_EXPIRY_TIME", "NEXT_PUBLIC_MERCH_API_ORIGIN" ] }, diff --git a/yarn.lock b/yarn.lock index 7508f649..b1fc7190 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6633,6 +6633,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.7.tgz#8ccef136f240770c1379d50100796a6952f01f94" integrity sha512-LhFTglglr63mNXUSRYD8A+ZAIu5sFqNJ4Y2fPuY7UlrySJH87rRRlhtVmMHplmfk5WkoJGmDjE9oiTfyX94CpQ== +"@types/node@>=8.1.0": + version "20.1.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.1.4.tgz#83f148d2d1f5fe6add4c53358ba00d97fc4cdb71" + integrity sha512-At4pvmIOki8yuwLtd7BNHl3CiWNbtclUbNtScGx4OHfBd4/oWoJC8KRCIxXwkdndzhxOsPXihrsOoydxBjlE9Q== + "@types/node@^14.14.31": version "14.18.35" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.35.tgz#879c4659cb7b3fe515844f029c75079c941bb65c" @@ -6822,6 +6827,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/uuid@^9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.1.tgz#98586dc36aee8dacc98cc396dbca8d0429647aa6" + integrity sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA== + "@types/warning@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52" @@ -16677,6 +16687,14 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== +stripe@^12.5.0: + version "12.5.0" + resolved "https://registry.yarnpkg.com/stripe/-/stripe-12.5.0.tgz#965168a87103a985fac950a7184a71bb107ac904" + integrity sha512-eDBh4bv+Uo+GdhjnQ246lM5KaOReoBzxFltgW0HJqco/QEAgSYxZPOpFbd5+gJnZlRqHSB5B+Zqw273SlbdPag== + dependencies: + "@types/node" ">=8.1.0" + qs "^6.11.0" + strnum@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" From 3619d571edcce1dda8e6e376ed6976fd464d4d10 Mon Sep 17 00:00:00 2001 From: Chan Wen Xu Date: Mon, 12 Jun 2023 19:00:18 +0800 Subject: [PATCH 16/60] cleanup: Rework merch backend --- apps/merch/src/index.ts | 26 ++- apps/merch/src/lib/types.ts | 11 + apps/merch/src/routes/checkout.ts | 269 +++++++++++------------ apps/merch/src/routes/index.ts | 18 +- apps/merch/src/routes/orders.ts | 22 +- apps/merch/src/routes/products.ts | 16 +- apps/merch/src/routes/quotation.ts | 47 ++-- apps/merch/tsconfig.json | 3 +- apps/web/features/merch/services/api.ts | 96 ++++++++ apps/web/features/merch/services/api.tsx | 128 ----------- packages/merch-helpers/src/lib/price.ts | 43 +++- packages/types/lib/merch.ts | 115 ++++------ 12 files changed, 384 insertions(+), 410 deletions(-) create mode 100644 apps/merch/src/lib/types.ts create mode 100644 apps/web/features/merch/services/api.ts delete mode 100644 apps/web/features/merch/services/api.tsx diff --git a/apps/merch/src/index.ts b/apps/merch/src/index.ts index 7e21d371..29c2dea9 100644 --- a/apps/merch/src/index.ts +++ b/apps/merch/src/index.ts @@ -1,13 +1,13 @@ +import cookieParser from "cookie-parser"; +import cors from "cors"; import express from "express"; +import { Logger, nodeloggerMiddleware } from "nodelogger"; import path from "path"; -import cors from "cors"; -import cookieParser from "cookie-parser"; -import { nodeloggerMiddleware, Logger } from "nodelogger"; - -// import routers -import indexRouter from "./routes/index"; -import ordersRouter from "./routes/orders"; -import productsRouter from "./routes/products"; +import { checkout } from "./routes/checkout"; +import { index, notFound } from "./routes/index"; +import { orderGet } from "./routes/orders"; +import { productGet, productsAll } from "./routes/products"; +import { quotation } from "./routes/quotation"; const app = express(); const CORS_ORIGIN = process.env.CORS_ORIGIN; @@ -29,9 +29,13 @@ app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, "public"))); -app.get("/", indexRouter); -app.use("/orders", ordersRouter); -app.use("/products", productsRouter); +app.get("/", index); +app.get("/orders/:id", orderGet); +app.get("/products", productsAll); +app.get("/products/:id", productGet); +app.post("/quotation", quotation); +app.post("/checkout", checkout); +app.use(notFound); app.listen("3000", () => Logger.info("server started on port 3000")); diff --git a/apps/merch/src/lib/types.ts b/apps/merch/src/lib/types.ts new file mode 100644 index 00000000..a6976872 --- /dev/null +++ b/apps/merch/src/lib/types.ts @@ -0,0 +1,11 @@ +import { Error } from "types"; + +export interface Request { + body: unknown, + params: Record, +} + +export interface JSONResponse { + status: (code: number) => JSONResponse, + json: (response: T|Error) => void, +} diff --git a/apps/merch/src/routes/checkout.ts b/apps/merch/src/routes/checkout.ts index e11d77ba..952d6cf3 100644 --- a/apps/merch/src/routes/checkout.ts +++ b/apps/merch/src/routes/checkout.ts @@ -1,169 +1,146 @@ -import { Router } from "express"; -import { Cart, Order, OrderItem, OrderStatus, PriceModel, ReservedProduct } from "types"; -import { createOrder, createOrderHoldEntry, getProduct, incrementStockCount } from "../db"; -import { v4 as uuidv4 } from 'uuid'; -import { Stripe } from 'stripe'; - -const router = Router(); - -const DEFAULT_ORDER_EXPIRY_TIME = "24"; -const frontendUrl: string = process.env.FRONTEND_STAGING_DOMAIN || ""; +import { calculatePricing, describeCart, PricingError } from "merch-helpers"; +import { Stripe } from "stripe"; +import { + CheckoutRequest, + CheckoutResponse, + Order, + OrderHold, + OrderItem, + OrderStatus, + PricedCart, + Product, + ReservedProduct +} from "types"; +import { v4 as uuidv4 } from "uuid"; +import { + createOrder, + createOrderHoldEntry, + getProducts, + incrementStockCount +} from "../db"; +import { JSONResponse, Request } from "../lib/types"; + +const ORDER_EXPIRY_TIME = parseInt(process.env.ORDER_EXPIRY_TIME ?? "24"); const STRIPE_KEY = process.env.STRIPE_SECRET_KEY || ""; - -// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call const stripe = new Stripe(STRIPE_KEY, { - apiVersion: '2022-11-15', + apiVersion: "2022-11-15", }); -type CheckoutBody = Cart & { - email: string; -} +export const checkout = (req: Request, res: JSONResponse) => { + const body = CheckoutRequest.safeParse(req.body); + if (!body.success) { + return res.status(400).json({ + error: "INVALID_TYPE", + detail: body.error.format(), + }); + } -router.post("/", (req, res) => { - const body = req.body as CheckoutBody; - const { email, items } = body; + const cart = body.data; + const { email, items } = body.data; if (!email) { - throw new Error("Billing email must be provided when checking out"); + return res.status(400).json({ + error: "BAD_REQUEST", + detail: "Missing billing email", + }); } if (!items.length) { - throw new Error("Cart must not be empty when checking out"); + return res.status(400).json({ + error: "BAD_REQUEST", + detail: "Empty cart", + }); } - const itemsProductsPromise = generateOrderItemsFromCart(body); const orderID = uuidv4(); - const pricePromise = itemsProductsPromise.then(itemsProducts => { - const price = calcCartValue(itemsProducts); - return { itemsProducts, price }; - }); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call - const paymentIntentPromise = pricePromise.then(({ itemsProducts, price }) => stripe.paymentIntents.create({ - payment_method_types: ['paynow'], - payment_method_data: { type: 'paynow' }, - amount: Math.floor(price.grandTotal * 100), // stripe payment amounts are in cents - currency: 'sgd', - receipt_email: body.email, - description: `SCSE Merch Purchase:\n${describeCart(itemsProducts, orderID)}`, - })); - const expiryPromise = Promise.resolve(process.env.ORDER_EXPIRY_TIME ?? DEFAULT_ORDER_EXPIRY_TIME) - .then(expiryTime => new Date(new Date().getTime() + (parseInt(expiryTime) * 60 * 60 * 1000)).toISOString()); - - Promise.all([itemsProductsPromise, pricePromise, paymentIntentPromise, expiryPromise]) - .then(([itemsProducts, price, paymentIntent, expiry]) => { - const orderID = uuidv4(); - const orderDateTime = new Date().toISOString(); - const customerEmail = body.email; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - const transactionID = paymentIntent.id; - const paymentPlatform = 'stripe'; - const status = OrderStatus.PENDING_PAYMENT; + const orderTime = new Date(); + const expiryTimeMillis = orderTime.getTime() + ORDER_EXPIRY_TIME; + const expiryTime = new Date(expiryTimeMillis); + + getProducts() + .then((products: Product[]): [Product[], PricedCart] => { + // TODO: Fetch promotion. + return [products, calculatePricing(products, cart, undefined)]; + }) + .then(([products, cart]) => + Promise.all([ + products, + cart, + stripe.paymentIntents.create({ + payment_method_types: ["paynow"], + payment_method_data: { type: "paynow" }, + amount: cart.total, + currency: "sgd", + receipt_email: email, + description: `SCSE Merch Purchase:\n${describeCart( + products, + cart, + orderID + )}`, + }), + ]) + ) + .then(([products, cart, stripeIntent]) => { + const productMap: Record = {}; + for (const product of products) { + productMap[product.id] = product; + } + const transactionID = stripeIntent.id; + const orderItems = cart.items.map((item): OrderItem => { + const product = productMap[item.id]; + return { + id: item.id, + name: product.name, + category: product.category, + color: item.color, + size: item.size, + quantity: item.quantity, + price: item.discountedPrice, + }; + }); + const reserved = cart.items.map((item): ReservedProduct => ({ + id: item.id, + quantity: item.quantity, + })); const order: Order = { id: orderID, - items: itemsProducts, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + items: orderItems, transaction_id: transactionID, - transaction_time: orderDateTime, - payment_method: paymentPlatform, - customer_email: customerEmail, - status: status, + transaction_time: orderTime.toISOString(), + payment_method: "stripe", + customer_email: email, + status: OrderStatus.PENDING_PAYMENT, }; - - for (const orderItem of itemsProducts) { - void incrementStockCount(orderItem.id, -orderItem.quantity, orderItem.size, orderItem.color); + const orderHold: OrderHold = { + transaction_id: transactionID, + expiry: expiryTime.toISOString(), + reserved_products: reserved, } - const reservedProducts: ReservedProduct[] = body.items.map(item => { - return { - productID: item.productId, - qty: item.quantity - }; - }); - const orderHoldEntry = { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - transactionID, - expiry: Math.floor(new Date(expiry).getTime() / 1000), - reservedProducts, - }; + const stockIncrements = cart.items.map((item) => incrementStockCount( + item.id, -item.quantity, item.size, item.color, + )) - createOrder(order) - .then(() => createOrderHoldEntry(orderHoldEntry)) - .then(() => { - res.json({ - orderId: orderID, - items: itemsProducts, - price: price, - payment: { - paymentGateway: 'stripe', - clientSecret: paymentIntent.client_secret, - }, - email: body.email, - expiry: Math.floor(new Date(expiry).getTime() / 1000), - }); - }) - .catch(err => { - console.error('Error creating order hold entry:', err); - res.status(500).json({ detail: 'Error unable to check out.' }); - }); + return Promise.all([createOrder(order), stripeIntent, createOrderHoldEntry(orderHold), ...stockIncrements]) }) - .catch(e => { - if (e.code === 'ConditionalCheckFailedException') { - res.status(400).json({ detail: 'Current quantity cannot be less than 0 and must be available for sale' }); - } + .then(([order, stripeIntent]) => { + res.json({ + ...order, + expiry: expiryTime.toISOString(), + payment: { + method: "stripe", + client_secret: stripeIntent.client_secret ?? "", + }, + }) }) -}) - - -export function calcCartValue(cartOrderItems: OrderItem[]): PriceModel { - let subtotal = 0; - for (const i of cartOrderItems) { - subtotal += i.price * i.quantity; - } - - const grandTotal = subtotal; - - return { - currency: 'sgd', - subtotal, - discount: 0, // todo - grandTotal, - }; -} - -function describeCart(cartOrderItems: OrderItem[], orderId: string): string { - const entries = [ - `${frontendUrl}/order-summary/${orderId} | ` - ]; - - for (const entry of cartOrderItems) { - const price = entry.price * entry.quantity; - let name = entry.name; - if (entry.size) { - name = `${entry.name} (Size: ${entry.size.toUpperCase()})`; - } - entries.push(`${name} x${entry.quantity} - S$${(price / 100).toFixed(2)}`); - } - - return entries.join('\n'); -} - -export async function generateOrderItemsFromCart(cart: Cart): Promise { - const cartOrderItems: OrderItem[] = []; - for (const item of cart.items) { - const product = await getProduct(item.productId); - if (!product) { - throw new Error(`productId ${item.productId} could not be found`); - } - cartOrderItems.push({ - id: product.id, - name: product.name, - price: product.price, - image: product.images[0], - category: product.category, - quantity: item.quantity, - size: item.size, - color: item.colorway + .catch((e) => { + if (e instanceof PricingError) { + return res.status(400).json({ + error: "INVALID_REQUEST", + detail: e.message, + }); + } + console.warn(e); + return res.status(500).json({ error: "INTERNAL_SERVER_ERROR" }); }); - } - return cartOrderItems; -} +}; diff --git a/apps/merch/src/routes/index.ts b/apps/merch/src/routes/index.ts index 29eb613f..bdb5bf7e 100644 --- a/apps/merch/src/routes/index.ts +++ b/apps/merch/src/routes/index.ts @@ -1,9 +1,15 @@ -import { Router } from "express" +import { JSONResponse, Request } from "../lib/types"; -const router = Router() +const genericInfo = { + github: "https://github.com/ntuscse/website", + website: "https://ntuscse.com", + service: "Merch", +}; -router.get("/", (req, res) => { - res.json({ content: "Hello World" }); -}) +export const index = (req: Request, res: JSONResponse) => { + res.json(genericInfo); +}; -export default router +export const notFound = (req: Request, res: JSONResponse) => { + res.status(404).json({ error: "NOT_FOUND" }); +}; diff --git a/apps/merch/src/routes/orders.ts b/apps/merch/src/routes/orders.ts index bf6b57d1..bff2505e 100644 --- a/apps/merch/src/routes/orders.ts +++ b/apps/merch/src/routes/orders.ts @@ -1,10 +1,8 @@ -import { Router } from "express"; import { Order } from "types"; import { getOrder, NotFoundError } from "../db"; +import { JSONResponse, Request } from "../lib/types"; -const router = Router(); - -router.get("/:id", (req, res) => { +export const orderGet = (req: Request<"id">, res: JSONResponse) => { getOrder(req.params.id) .then((order: Order) => { res.json(censorDetails(order)); @@ -16,17 +14,15 @@ router.get("/:id", (req, res) => { console.warn(e); res.status(500).json({ error: "INTERNAL_SERVER_ERROR" }); }); -}); +}; const censorDetails = (order: Order): Order => { - const censored = { ...order }; const customerEmail = order.customer_email.split("@"); - censored.customer_email = - starCensor(customerEmail[0]) + "@" + customerEmail.slice(1).join("@"); - if (censored.transaction_id.length > 3) { - censored.transaction_id = starCensor(censored.transaction_id); - } - return censored; + return { + ...order, + customer_email: starCensor(customerEmail[0]) + "@" + customerEmail.slice(1).join("@"), + transaction_id: starCensor(order.transaction_id), + }; }; const starCensor = (text: string, lettersToKeep = 3): string => { @@ -35,5 +31,3 @@ const starCensor = (text: string, lettersToKeep = 3): string => { } return text.substring(0, lettersToKeep) + "*".repeat(text.length - 3); }; - -export default router; diff --git a/apps/merch/src/routes/products.ts b/apps/merch/src/routes/products.ts index 14278fb9..e076cd22 100644 --- a/apps/merch/src/routes/products.ts +++ b/apps/merch/src/routes/products.ts @@ -1,10 +1,8 @@ -import { Router } from "express"; -import { Product } from "types"; +import { Product, ProductsResponse } from "types"; import { getProduct, getProducts, NotFoundError } from "../db"; +import { JSONResponse, Request } from "../lib/types"; -const router = Router(); - -router.get("/", (req, res) => { +export const productsAll = (req: Request, res: JSONResponse) => { getProducts() .then((products: Product[]) => { res.json({ products }); @@ -16,9 +14,9 @@ router.get("/", (req, res) => { console.warn(e); res.status(500).json({ error: "INTERNAL_SERVER_ERROR" }); }); -}); +}; -router.get("/:id", (req, res) => { +export const productGet = (req: Request<"id">, res: JSONResponse) => { getProduct(req.params.id) .then((product: Product) => { res.json(product); @@ -30,6 +28,4 @@ router.get("/:id", (req, res) => { console.warn(e); res.status(500).json({ error: "INTERNAL_SERVER_ERROR" }); }); -}); - -export default router; +}; diff --git a/apps/merch/src/routes/quotation.ts b/apps/merch/src/routes/quotation.ts index 0873bf79..da4c7d34 100644 --- a/apps/merch/src/routes/quotation.ts +++ b/apps/merch/src/routes/quotation.ts @@ -1,24 +1,33 @@ -import { Router } from "express"; -import { Cart } from "types"; -import { calcCartValue, generateOrderItemsFromCart } from "./checkout"; +import { calculatePricing, PricingError } from "merch-helpers"; +import { PricedCart, Product, QuotationRequest } from "types"; +import { getProducts } from "../db"; +import { JSONResponse, Request } from "../lib/types"; -const router = Router(); +export const quotation = (req: Request, res: JSONResponse) => { + const body = QuotationRequest.safeParse(req.body); + if (!body.success) { + return res.status(400).json({ + error: "INVALID_TYPE", + detail: body.error.format(), + }); + } -type QuotationBody = Cart + const cart = body.data; -router.post("/", (req, res) => { - const body = req.body as QuotationBody; - const itemsProductsPromise = generateOrderItemsFromCart(body); - itemsProductsPromise.then(itemsProducts => { - const price = calcCartValue(itemsProducts); - res.json({ - items: itemsProducts, - price: price, - }) + getProducts() + .then((products: Product[]) => { + // TODO: Fetch promotion. + return calculatePricing(products, cart, undefined); }) - .catch(err => { - console.error('Error creating quotation:', err); - res.status(500).json({ detail: 'Error unable to check out.' }); + .then((cart: PricedCart) => res.json(cart)) + .catch((e) => { + if (e instanceof PricingError) { + return res.status(400).json({ + error: "INVALID_REQUEST", + detail: e.message, + }); + } + console.warn(e); + return res.status(500).json({ error: "INTERNAL_SERVER_ERROR" }); }); -}) - +}; diff --git a/apps/merch/tsconfig.json b/apps/merch/tsconfig.json index 59b0be75..af3d1304 100644 --- a/apps/merch/tsconfig.json +++ b/apps/merch/tsconfig.json @@ -10,7 +10,8 @@ "skipLibCheck": true, "outDir": "./dist", "rootDir": "./src", - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "strict": true }, "include": [ "src" diff --git a/apps/web/features/merch/services/api.ts b/apps/web/features/merch/services/api.ts new file mode 100644 index 00000000..bfdcf678 --- /dev/null +++ b/apps/web/features/merch/services/api.ts @@ -0,0 +1,96 @@ +import { + APIError, + Cart, + CheckoutRequest, + CheckoutResponse, + PricedCart, + Product, + ProductsResponse, + QuotationRequest +} from "types"; + +export class Api { + private API_ORIGIN: string; + + constructor() { + if (!process.env.NEXT_PUBLIC_MERCH_API_ORIGIN) { + throw new Error( + "NEXT_PUBLIC_MERCH_API_ORIGIN environment variable is not set" + ); + } + this.API_ORIGIN = process.env.NEXT_PUBLIC_MERCH_API_ORIGIN; + } + + // http methods + async get(urlPath: string): Promise { + const response = await fetch(`${this.API_ORIGIN}${urlPath}`); + type responseType = T | APIError; + const resp = (await response.json()) as responseType; + if ("error" in resp) { + console.error("Server error:", resp); + throw new Error(`Error ${resp.error}`); + } + return resp; + } + + async post(urlPath: string, data: R): Promise { + const response = await fetch(`${this.API_ORIGIN}${urlPath}`, { + method: "POST", + mode: "cors", + cache: "no-cache", + credentials: "same-origin", + headers: { + "Content-Type": "application/json", + }, + redirect: "follow", + referrerPolicy: "no-referrer", + body: JSON.stringify(data), + }); + type responseType = T | APIError; + const resp = (await response.json()) as responseType; + if ("error" in resp) { + console.error("Server error:", resp); + throw new Error(`Error ${resp.error}`); + } + return resp; + } + + async getProducts(): Promise { + const res = await this.get("/products"); + return res.products; + } + + async getProduct(productId: string) { + const res = await this.get(`/products/${productId}`); + console.log("product res", res); + return res; + } + + async getOrder(orderID: string): Promise { + if (!orderID) { + throw new Error("No order ID"); + } + const res = await this.get(`/orders/${orderID}`); + return res; + } + + async postCheckoutCart(cart: Cart, email: string, promoCode?: string) { + return await this.post( + `/cart/checkout`, + { + ...cart, + promoCode: promoCode, + email, + } + ); + } + + async postQuotation(cart: Cart, promoCode?: string) { + return await this.post(`/cart/quotation`, { + ...cart, + promoCode: promoCode, + }); + } +} + +export const api = new Api(); diff --git a/apps/web/features/merch/services/api.tsx b/apps/web/features/merch/services/api.tsx deleted file mode 100644 index 6573271d..00000000 --- a/apps/web/features/merch/services/api.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { Product } from "types"; - -export class Api { - private API_ORIGIN: string; - - constructor() { - if (!process.env.NEXT_PUBLIC_MERCH_API_ORIGIN) { - throw new Error( - "NEXT_PUBLIC_MERCH_API_ORIGIN environment variable is not set" - ); - } - this.API_ORIGIN = process.env.NEXT_PUBLIC_MERCH_API_ORIGIN || ""; - } - - // http methods - async get(urlPath: string): Promise { - const response = await fetch(`${this.API_ORIGIN}${urlPath}`); - const convert = await response.json() as (T & {error: undefined}) | { error: string }; - if (convert.error) { - throw new Error(`Server error: ${convert.error}`); - } - return convert as T; - } - - /* - // eslint-disable-next-line class-methods-use-this - async post(urlPath: string, data: any): Promise { - const response = await fetch(`${this.API_ORIGIN}${urlPath}`, { - method: "POST", // *GET, POST, PUT, DELETE, etc. - mode: "cors", // no-cors, *cors, same-origin - cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached - credentials: "same-origin", // include, *same-origin, omit - headers: { - "Content-Type": "application/json", - // 'Content-Type': 'application/x-www-form-urlencoded', - }, - redirect: "follow", // manual, *follow, error - referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url - body: JSON.stringify(data), // body data type must match - }); - return response.json(); - } - */ - - // eslint-disable-next-line class-methods-use-this - async getProducts(): Promise { - try { - const res = await this.get<{products: Product[]}>("/products"); - console.log("product-list", res); - return res.products; - } catch (e) { - if (e instanceof Error) { - throw e; - } - return []; - } - } - - // eslint-disable-next-line class-methods-use-this - async getProduct(productId: string) { - try { - const res = await this.get(`/products/${productId}`); - console.log("product res", res); - return res; - } catch (e: any) { - if (e instanceof Error) { - throw e; - } - } - } - - async getOrder(orderId: string) { - try { - if (!orderId) { - throw new Error("No order ID"); - } - const res = await this.get(`/orders/${orderId}`); - console.log("Order Summary response:", res); - return res; - } catch (e: any) { - if (e instanceof Error) { - throw e; - } - } - } - /* - async getOrderHistory(userId: string) { - try { - const res = await this.get(`/orders/${userId}`); - console.log("Order Summary response:", res); - return res.json(); - } catch (e: any) { - throw new Error(e); - } - } - - async postCheckoutCart( - items: CartItemType[], - email: string, - promoCode: string | null - ) { - try { - const res = await this.post(`/cart/checkout`, { - items, - promoCode: promoCode ?? "", - email, - }); - return res; - } catch (e: any) { - throw new Error(e); - } - } - - async postQuotation(items: CartItemType[], promoCode: string | null) { - try { - const res = await this.post(`/cart/quotation`, { - items, - promoCode: promoCode ?? "", - }); - return res; - } catch (e: any) { - throw new Error(e); - } - } - */ -} - -export const api = new Api(); diff --git a/packages/merch-helpers/src/lib/price.ts b/packages/merch-helpers/src/lib/price.ts index 97d46100..25e3bf6a 100644 --- a/packages/merch-helpers/src/lib/price.ts +++ b/packages/merch-helpers/src/lib/price.ts @@ -1,5 +1,38 @@ import { Cart, PricedCart, Product, Promotion, PromoType } from "types"; +const frontendURL = process.env.FRONTEND_STAGING_DOMAIN || ""; + +export class PricingError { + message: string; + + constructor(message: string) { + this.message = message; + } +} + +export const describeCart = (products: Product[], cart: PricedCart, orderID: string): string => { + const entries = [`${frontendURL}/orders/${orderID} | `]; + + const productMap: Record = {}; + for (const product of products) { + productMap[product.id] = product; + } + + for (const item of cart.items) { + const product = productMap[item.id]; + if (!product) { + throw new PricingError("unknown product ID: " + item.id); + } + let name = product.name; + if (item.size) { + name = `${product.name} (Size: ${item.size.toUpperCase()})`; + } + entries.push(`${name} x${item.quantity} - S$${item.discountedPrice}`); + } + + return entries.join("\n"); +} + export const calculatePricing = ( products: Product[], cart: Cart, @@ -7,7 +40,7 @@ export const calculatePricing = ( ): PricedCart => { const productMap: Record = {}; if (promotion && promotion.redemptionsRemaining <= 0) { - throw new Error("no redemptions left for the provided promotion"); + throw new PricingError("no redemptions left for the provided promotion"); } for (const product of products) { productMap[product.id] = product; @@ -15,7 +48,13 @@ export const calculatePricing = ( const pricedItems = cart.items.map((item) => { const product = productMap[item.id]; if (!product) { - throw new Error("unknown product ID: " + item.id); + throw new PricingError("unknown product ID: " + item.id); + } + if (!product.colors.includes(item.color)) { + throw new PricingError(`invalid color ${item.color} for product ${item.id}`); + } + if (!product.sizes.includes(item.size)) { + throw new PricingError(`invalid size ${item.color} for product ${item.id}`); } let itemPrice = product.price * item.quantity; if (!promotion) { diff --git a/packages/types/lib/merch.ts b/packages/types/lib/merch.ts index 87a1abfa..058a1b45 100644 --- a/packages/types/lib/merch.ts +++ b/packages/types/lib/merch.ts @@ -1,3 +1,5 @@ +import { z } from "zod"; + // Product export interface Product { id: string; @@ -41,16 +43,19 @@ export type CartState = { billingEmail: string; }; -export interface Cart { - items: CartItem[]; -} +export const CartItem = z.object({ + id: z.string(), + color: z.string(), + size: z.string(), + quantity: z.number().gt(0), +}); -export interface CartItem { - id: string; - color: string; - size: string; - quantity: number; -} +export const Cart = z.object({ + items: z.array(CartItem), +}); + +export type Cart = z.infer; +export type CartItem = z.infer; // Promotion export interface OrderItem { @@ -94,78 +99,42 @@ export enum PromoType { FIXED_VALUE = "FIXED_VALUE", } -/* -export type ProductInfo = { - name: string; - image: string; - price: number; +export type ReservedProduct = { + id: string; + quantity: number; }; -export type CartPrice = { - currency: string; - subtotal: number; - discount: number; - grandTotal: number; -}; +export type OrderHold = { + transaction_id: string; + expiry: string; + reserved_products: ReservedProduct[]; +} -export type CartResponseDto = { - items: [ - { - id: string; - name: string; - price: number; - images: string[]; - sizes: string; - productCategory: string; - isAvailable: boolean; - quantity: number; - } - ]; - price: { - currency: string; - subtotal: number; - discount: number; - grandTotal: number; - }; -}; +// API Types +export const QuotationRequest = Cart.merge(z.object({ + promoCode: z.string().optional(), +})); -export type CheckoutResponseDto = { - orderId: string; - items: [ - { - id: string; - name: string; - price: number; - images: string[]; - sizes: string[]; - productCategory: string; - isAvailable: boolean; - quantity: number; - } - ]; - price: { - currency: string; - subtotal: number; - discount: number; - grandTotal: number; - }; +export const CheckoutRequest = QuotationRequest.merge(z.object({ + email: z.string(), +})); + +export type QuotationRequest = z.infer; +export type CheckoutRequest = z.infer; + +export type CheckoutResponse = Order & { + expiry: string, payment: { - paymentGateway: string; - clientSecret: string; + method: "stripe", + client_secret: string, }; - email: string; }; -export type ProductInfoMap = Record; -*/ - -export type ReservedProduct = { - productID: string; - qty: number; +export type ProductsResponse = { + products: Product[], }; -export type OrderHoldEntry = { - transactionID: string; - expiry: number; - reservedProducts: ReservedProduct[]; +export type APIError = { + error: string, + detail?: string|object, } From b34d137bb2dad741244dcd386fab1a0ff0848eca Mon Sep 17 00:00:00 2001 From: Chan Wen Xu Date: Mon, 12 Jun 2023 19:42:39 +0800 Subject: [PATCH 17/60] chore: Move swiper dependency to web [SCSE-275] --- apps/web/package.json | 1 + package.json | 4 +--- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 1ed5475c..d9a35f04 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -29,6 +29,7 @@ "react-bootstrap": "^2.5.0", "react-dom": "18.2.0", "react-icons": "^4.8.0", + "swiper": "^9.4.0", "ui": "*" }, "devDependencies": { diff --git a/package.json b/package.json index 1fdbc8af..43e8fd79 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,6 @@ "yarn": ">= 1.22.17", "pnpm": "please-use-yarn" }, - "dependencies": { - "swiper": "^9.2.0" - }, + "dependencies": {}, "packageManager": "yarn@1.22.17" } diff --git a/yarn.lock b/yarn.lock index b1fc7190..0d59367e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16822,10 +16822,10 @@ swc-minify-webpack-plugin@^2.1.0: resolved "https://registry.yarnpkg.com/swc-minify-webpack-plugin/-/swc-minify-webpack-plugin-2.1.1.tgz#2c63fe592d49541733d7557b3af8f97c7ffa78b9" integrity sha512-/9ud/libNWUC5p71vXWhW/O2Nc0essW8D9pY4P4ol0ceM8OcFbNr41R9YFqTkmktqUL2t0WwXau+FkR4T1+PJA== -swiper@^9.2.0: - version "9.2.0" - resolved "https://registry.yarnpkg.com/swiper/-/swiper-9.2.0.tgz#40e950b1c0d516403b3bf7083560ebac22a6f325" - integrity sha512-lWK9toYumUQss+YuTL+Mt0+8twiMJEyzioER4bbS4rrGHlkeLrDM8uhtAmnpdijELrNscuNUujDgKoMQZfQGlQ== +swiper@^9.4.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/swiper/-/swiper-9.4.0.tgz#ceb7dd1d05b93702aceba42d94ee37c381beab6a" + integrity sha512-AKame5qkFnNKCJ8Bfn3YuOi/LoUuAESVdCv/BJH5fKgdAUFRtImZXV3gdYlYovXbasJDV6hHNWdQvdzPB6aebw== dependencies: ssr-window "^4.0.2" From a0a003df27ed2f146ef708709694793de7a4bbc8 Mon Sep 17 00:00:00 2001 From: Chan Wen Xu Date: Mon, 12 Jun 2023 19:52:06 +0800 Subject: [PATCH 18/60] fix: Add key to grid item --- packages/ui/components/merch/skeleton/MerchListSkeleton.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/ui/components/merch/skeleton/MerchListSkeleton.tsx b/packages/ui/components/merch/skeleton/MerchListSkeleton.tsx index 9ebde95f..6fa15b8f 100644 --- a/packages/ui/components/merch/skeleton/MerchListSkeleton.tsx +++ b/packages/ui/components/merch/skeleton/MerchListSkeleton.tsx @@ -4,14 +4,12 @@ import { Grid, Skeleton, SkeletonText, GridItem } from "@chakra-ui/react"; export const MerchListSkeleton: React.FC = () => { return ( - {new Array(8).fill(null).map((item: any) => ( - + {new Array(8).fill(null).map((_, i: number) => ( + - {item} ))} ); }; - From 71ccac4ef0c283b839ada59e8cab1d34b787877d Mon Sep 17 00:00:00 2001 From: Chan Wen Xu Date: Mon, 12 Jun 2023 20:01:34 +0800 Subject: [PATCH 19/60] chore: Rename JSONResponse to Response --- apps/merch/src/lib/types.ts | 8 ++++---- apps/merch/src/routes/checkout.ts | 4 ++-- apps/merch/src/routes/index.ts | 6 +++--- apps/merch/src/routes/orders.ts | 4 ++-- apps/merch/src/routes/products.ts | 6 +++--- apps/merch/src/routes/quotation.ts | 4 ++-- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/merch/src/lib/types.ts b/apps/merch/src/lib/types.ts index a6976872..dac9df75 100644 --- a/apps/merch/src/lib/types.ts +++ b/apps/merch/src/lib/types.ts @@ -1,11 +1,11 @@ -import { Error } from "types"; +import { APIError } from "types"; export interface Request { body: unknown, params: Record, } -export interface JSONResponse { - status: (code: number) => JSONResponse, - json: (response: T|Error) => void, +export interface Response { + status: (code: number) => Response, + json: (response: T|APIError) => void, } diff --git a/apps/merch/src/routes/checkout.ts b/apps/merch/src/routes/checkout.ts index 952d6cf3..964702f1 100644 --- a/apps/merch/src/routes/checkout.ts +++ b/apps/merch/src/routes/checkout.ts @@ -18,7 +18,7 @@ import { getProducts, incrementStockCount } from "../db"; -import { JSONResponse, Request } from "../lib/types"; +import { Request, Response } from "../lib/types"; const ORDER_EXPIRY_TIME = parseInt(process.env.ORDER_EXPIRY_TIME ?? "24"); @@ -27,7 +27,7 @@ const stripe = new Stripe(STRIPE_KEY, { apiVersion: "2022-11-15", }); -export const checkout = (req: Request, res: JSONResponse) => { +export const checkout = (req: Request, res: Response) => { const body = CheckoutRequest.safeParse(req.body); if (!body.success) { return res.status(400).json({ diff --git a/apps/merch/src/routes/index.ts b/apps/merch/src/routes/index.ts index bdb5bf7e..cd694ddd 100644 --- a/apps/merch/src/routes/index.ts +++ b/apps/merch/src/routes/index.ts @@ -1,4 +1,4 @@ -import { JSONResponse, Request } from "../lib/types"; +import { Request, Response } from "../lib/types"; const genericInfo = { github: "https://github.com/ntuscse/website", @@ -6,10 +6,10 @@ const genericInfo = { service: "Merch", }; -export const index = (req: Request, res: JSONResponse) => { +export const index = (req: Request, res: Response) => { res.json(genericInfo); }; -export const notFound = (req: Request, res: JSONResponse) => { +export const notFound = (req: Request, res: Response) => { res.status(404).json({ error: "NOT_FOUND" }); }; diff --git a/apps/merch/src/routes/orders.ts b/apps/merch/src/routes/orders.ts index bff2505e..2f4f8b6a 100644 --- a/apps/merch/src/routes/orders.ts +++ b/apps/merch/src/routes/orders.ts @@ -1,8 +1,8 @@ import { Order } from "types"; import { getOrder, NotFoundError } from "../db"; -import { JSONResponse, Request } from "../lib/types"; +import { Request, Response } from "../lib/types"; -export const orderGet = (req: Request<"id">, res: JSONResponse) => { +export const orderGet = (req: Request<"id">, res: Response) => { getOrder(req.params.id) .then((order: Order) => { res.json(censorDetails(order)); diff --git a/apps/merch/src/routes/products.ts b/apps/merch/src/routes/products.ts index e076cd22..d14f2378 100644 --- a/apps/merch/src/routes/products.ts +++ b/apps/merch/src/routes/products.ts @@ -1,8 +1,8 @@ import { Product, ProductsResponse } from "types"; import { getProduct, getProducts, NotFoundError } from "../db"; -import { JSONResponse, Request } from "../lib/types"; +import { Request, Response } from "../lib/types"; -export const productsAll = (req: Request, res: JSONResponse) => { +export const productsAll = (req: Request, res: Response) => { getProducts() .then((products: Product[]) => { res.json({ products }); @@ -16,7 +16,7 @@ export const productsAll = (req: Request, res: JSONResponse) = }); }; -export const productGet = (req: Request<"id">, res: JSONResponse) => { +export const productGet = (req: Request<"id">, res: Response) => { getProduct(req.params.id) .then((product: Product) => { res.json(product); diff --git a/apps/merch/src/routes/quotation.ts b/apps/merch/src/routes/quotation.ts index da4c7d34..890c9e8c 100644 --- a/apps/merch/src/routes/quotation.ts +++ b/apps/merch/src/routes/quotation.ts @@ -1,9 +1,9 @@ import { calculatePricing, PricingError } from "merch-helpers"; import { PricedCart, Product, QuotationRequest } from "types"; import { getProducts } from "../db"; -import { JSONResponse, Request } from "../lib/types"; +import { Request, Response } from "../lib/types"; -export const quotation = (req: Request, res: JSONResponse) => { +export const quotation = (req: Request, res: Response) => { const body = QuotationRequest.safeParse(req.body); if (!body.success) { return res.status(400).json({ From 702962910ba660aa02d3e22db69c581455acbecc Mon Sep 17 00:00:00 2001 From: Chan Wen Xu Date: Mon, 12 Jun 2023 23:12:57 +0800 Subject: [PATCH 20/60] feat: Add name and image to quotation --- apps/merch/src/routes/checkout.ts | 73 ++++++++++++------------- packages/merch-helpers/src/lib/price.ts | 10 +++- packages/types/lib/merch.ts | 3 +- 3 files changed, 45 insertions(+), 41 deletions(-) diff --git a/apps/merch/src/routes/checkout.ts b/apps/merch/src/routes/checkout.ts index 964702f1..2344d300 100644 --- a/apps/merch/src/routes/checkout.ts +++ b/apps/merch/src/routes/checkout.ts @@ -1,22 +1,22 @@ import { calculatePricing, describeCart, PricingError } from "merch-helpers"; import { Stripe } from "stripe"; import { - CheckoutRequest, - CheckoutResponse, - Order, - OrderHold, - OrderItem, - OrderStatus, - PricedCart, - Product, - ReservedProduct + CheckoutRequest, + CheckoutResponse, + Order, + OrderHold, + OrderItem, + OrderStatus, + PricedCart, + Product, + ReservedProduct, } from "types"; import { v4 as uuidv4 } from "uuid"; import { - createOrder, - createOrderHoldEntry, - getProducts, - incrementStockCount + createOrder, + createOrderHoldEntry, + getProducts, + incrementStockCount, } from "../db"; import { Request, Response } from "../lib/types"; @@ -63,7 +63,6 @@ export const checkout = (req: Request, res: Response) => { }) .then(([products, cart]) => Promise.all([ - products, cart, stripe.paymentIntents.create({ payment_method_types: ["paynow"], @@ -79,29 +78,24 @@ export const checkout = (req: Request, res: Response) => { }), ]) ) - .then(([products, cart, stripeIntent]) => { - const productMap: Record = {}; - for (const product of products) { - productMap[product.id] = product; - } - + .then(([cart, stripeIntent]) => { const transactionID = stripeIntent.id; - const orderItems = cart.items.map((item): OrderItem => { - const product = productMap[item.id]; - return { + const orderItems = cart.items.map( + (item): OrderItem => ({ id: item.id, - name: product.name, - category: product.category, + name: item.name, color: item.color, size: item.size, quantity: item.quantity, price: item.discountedPrice, - }; - }); - const reserved = cart.items.map((item): ReservedProduct => ({ - id: item.id, - quantity: item.quantity, - })); + }) + ); + const reserved = cart.items.map( + (item): ReservedProduct => ({ + id: item.id, + quantity: item.quantity, + }) + ); const order: Order = { id: orderID, items: orderItems, @@ -115,13 +109,18 @@ export const checkout = (req: Request, res: Response) => { transaction_id: transactionID, expiry: expiryTime.toISOString(), reserved_products: reserved, - } + }; - const stockIncrements = cart.items.map((item) => incrementStockCount( - item.id, -item.quantity, item.size, item.color, - )) + const stockIncrements = cart.items.map((item) => + incrementStockCount(item.id, -item.quantity, item.size, item.color) + ); - return Promise.all([createOrder(order), stripeIntent, createOrderHoldEntry(orderHold), ...stockIncrements]) + return Promise.all([ + createOrder(order), + stripeIntent, + createOrderHoldEntry(orderHold), + ...stockIncrements, + ]); }) .then(([order, stripeIntent]) => { res.json({ @@ -131,7 +130,7 @@ export const checkout = (req: Request, res: Response) => { method: "stripe", client_secret: stripeIntent.client_secret ?? "", }, - }) + }); }) .catch((e) => { if (e instanceof PricingError) { diff --git a/packages/merch-helpers/src/lib/price.ts b/packages/merch-helpers/src/lib/price.ts index 25e3bf6a..bb9b8c4a 100644 --- a/packages/merch-helpers/src/lib/price.ts +++ b/packages/merch-helpers/src/lib/price.ts @@ -60,8 +60,10 @@ export const calculatePricing = ( if (!promotion) { return { ...item, - originalPrice: product.price, - discountedPrice: product.price, + name: product.name, + image: product.images.length ? product.images[0] : undefined, + originalPrice: itemPrice, + discountedPrice: itemPrice, }; } for (const discount of promotion.discounts) { @@ -84,7 +86,9 @@ export const calculatePricing = ( itemPrice = Math.max(0, itemPrice); return { ...item, - originalPrice: product.price, + name: product.name, + image: product.images.length ? product.images[0] : undefined, + originalPrice: product.price * item.quantity, discountedPrice: itemPrice, }; }); diff --git a/packages/types/lib/merch.ts b/packages/types/lib/merch.ts index 058a1b45..2e9235dc 100644 --- a/packages/types/lib/merch.ts +++ b/packages/types/lib/merch.ts @@ -61,7 +61,6 @@ export type CartItem = z.infer; export interface OrderItem { id: string; name: string; - category: string; image?: string; color: string; size: string; @@ -86,6 +85,8 @@ export interface PricedCart { total: number; items: { id: string; + name: string; + image?: string; color: string; size: string; quantity: number; From 5e7d1e8df8c6ac58d67a9d45b329c6b2a6f92f08 Mon Sep 17 00:00:00 2001 From: YoNG-Zaii Date: Thu, 15 Jun 2023 21:16:05 +0800 Subject: [PATCH 21/60] fix: Merch top padding --- apps/web/pages/merch/index.tsx | 2 +- packages/ui/components/merch/Page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/pages/merch/index.tsx b/apps/web/pages/merch/index.tsx index e584c61d..7694953d 100644 --- a/apps/web/pages/merch/index.tsx +++ b/apps/web/pages/merch/index.tsx @@ -30,7 +30,7 @@ const MerchandiseList = () => { return ( - + New Drop diff --git a/packages/ui/components/merch/Page.tsx b/packages/ui/components/merch/Page.tsx index ae488c6b..5926381f 100644 --- a/packages/ui/components/merch/Page.tsx +++ b/packages/ui/components/merch/Page.tsx @@ -15,7 +15,7 @@ export const Page = ({ ...props }: PageProps) => { return ( - + Date: Sun, 11 Jun 2023 22:29:12 +0800 Subject: [PATCH 22/60] feat: add cart page (#96) [SCSE-271] --- apps/web/features/merch/constants/index.tsx | 2 + .../web/features/merch/context/cart/index.tsx | 33 +- apps/web/features/merch/functions/index.tsx | 3 + apps/web/features/merch/services/api.ts | 10 +- apps/web/pages/merch/cart/index.tsx | 367 ++++++++++++++++++ apps/web/pages/merch/index.tsx | 7 +- apps/web/pages/merch/orders/[slug].tsx | 4 +- apps/web/pages/merch/product/[slug].tsx | 23 +- packages/types/lib/merch.ts | 15 + packages/ui/components/layout/Layout.tsx | 6 +- packages/ui/components/merch/Card.tsx | 4 +- packages/ui/components/merch/CartButton.tsx | 2 +- .../ui/components/merch/EmptyProductView.tsx | 7 +- .../ui/components/merch/LoadingScreen.tsx | 21 + packages/ui/components/merch/SizeOption.tsx | 8 +- .../ui/components/merch/cart/CartCard.tsx | 17 + .../components/merch/cart/CartEmptyView.tsx | 23 ++ .../ui/components/merch/cart/CartHeader.tsx | 19 + .../ui/components/merch/cart/CartItemCard.tsx | 176 +++++++++ .../components/merch/cart/CartRemoveModal.tsx | 40 ++ packages/ui/components/merch/cart/index.tsx | 5 + packages/ui/components/merch/index.tsx | 2 + packages/ui/components/navbar/Logo.tsx | 8 +- packages/ui/components/navbar/MenuItems.tsx | 8 +- packages/ui/components/navbar/MenuLink.tsx | 3 +- 25 files changed, 755 insertions(+), 58 deletions(-) create mode 100644 apps/web/features/merch/constants/index.tsx create mode 100644 apps/web/features/merch/functions/index.tsx create mode 100644 apps/web/pages/merch/cart/index.tsx create mode 100644 packages/ui/components/merch/LoadingScreen.tsx create mode 100644 packages/ui/components/merch/cart/CartCard.tsx create mode 100644 packages/ui/components/merch/cart/CartEmptyView.tsx create mode 100644 packages/ui/components/merch/cart/CartHeader.tsx create mode 100644 packages/ui/components/merch/cart/CartItemCard.tsx create mode 100644 packages/ui/components/merch/cart/CartRemoveModal.tsx create mode 100644 packages/ui/components/merch/cart/index.tsx diff --git a/apps/web/features/merch/constants/index.tsx b/apps/web/features/merch/constants/index.tsx new file mode 100644 index 00000000..2f581e88 --- /dev/null +++ b/apps/web/features/merch/constants/index.tsx @@ -0,0 +1,2 @@ +export * from "./queryKeys" +export * from "./routes" \ No newline at end of file diff --git a/apps/web/features/merch/context/cart/index.tsx b/apps/web/features/merch/context/cart/index.tsx index b1f3b1f0..cde921a8 100644 --- a/apps/web/features/merch/context/cart/index.tsx +++ b/apps/web/features/merch/context/cart/index.tsx @@ -8,7 +8,7 @@ type ContextType = { export enum CartActionType { RESET_CART = "RESET_CART", - INITALIZE = "initialize", + INITIALIZE = "initialize", ADD_ITEM = "add_item", UPDATE_QUANTITY = "update_quantity", REMOVE_ITEM = "remove_item", @@ -19,8 +19,7 @@ export enum CartActionType { } export type CartAction = - | { type: CartActionType.RESET_CART } - | { type: CartActionType.INITALIZE; payload: CartState } + | { type: CartActionType.INITIALIZE; payload: CartState } | { type: CartActionType.ADD_ITEM; payload: CartItem } | { type: CartActionType.UPDATE_QUANTITY; payload: CartItem } | { @@ -48,10 +47,7 @@ export const cartReducer = ( action: CartAction ): CartState => { switch (action.type) { - case CartActionType.RESET_CART: { - return JSON.parse(JSON.stringify(initState)) as typeof initState; - } - case CartActionType.INITALIZE: { + case CartActionType.INITIALIZE: { return { ...state, ...action.payload }; } case CartActionType.ADD_ITEM: { @@ -154,20 +150,21 @@ export const CartProvider: React.FC = ({ children }) => { const value = useMemo(() => ({ state, dispatch }), [state]); useEffect(() => { - const cartState: CartState = JSON.parse( - JSON.stringify(initState) + const storedCartData: CartState = JSON.parse( + localStorage.getItem("cart") as string ) as typeof initState; - const storedCartData: CartState = - (JSON.parse( - localStorage.getItem("cart") as string - ) as typeof initState) ?? cartState; - cartState.cart.items = storedCartData.cart.items; - cartState.name = storedCartData.name; - cartState.billingEmail = storedCartData.billingEmail; - dispatch({ type: CartActionType.INITALIZE, payload: cartState }); + if (storedCartData) { + const cartState: CartState = JSON.parse( + JSON.stringify(initState) + ) as typeof initState; + cartState.cart.items = storedCartData.cart?.items; + cartState.name = storedCartData.name; + cartState.billingEmail = storedCartData.billingEmail; + dispatch({ type: CartActionType.INITIALIZE, payload: cartState }); + } }, []); - useEffect(() => { + if (state === initState) return; localStorage.setItem("cart", JSON.stringify(state)); }, [state]); diff --git a/apps/web/features/merch/functions/index.tsx b/apps/web/features/merch/functions/index.tsx new file mode 100644 index 00000000..317c01db --- /dev/null +++ b/apps/web/features/merch/functions/index.tsx @@ -0,0 +1,3 @@ +export * from "./cart" +export * from "./currency" +export * from "./stock" diff --git a/apps/web/features/merch/services/api.ts b/apps/web/features/merch/services/api.ts index bfdcf678..baf5bfc0 100644 --- a/apps/web/features/merch/services/api.ts +++ b/apps/web/features/merch/services/api.ts @@ -22,7 +22,7 @@ export class Api { } // http methods - async get(urlPath: string): Promise { + async get(urlPath: string): Promise { const response = await fetch(`${this.API_ORIGIN}${urlPath}`); type responseType = T | APIError; const resp = (await response.json()) as responseType; @@ -33,7 +33,7 @@ export class Api { return resp; } - async post(urlPath: string, data: R): Promise { + async post(urlPath: string, data: R): Promise { const response = await fetch(`${this.API_ORIGIN}${urlPath}`, { method: "POST", mode: "cors", @@ -85,10 +85,10 @@ export class Api { ); } - async postQuotation(cart: Cart, promoCode?: string) { - return await this.post(`/cart/quotation`, { + async postQuotation(cart: Cart, promoCode: string | null) { + return await this.post(`/quotation`, { ...cart, - promoCode: promoCode, + promoCode: promoCode ?? "", }); } } diff --git a/apps/web/pages/merch/cart/index.tsx b/apps/web/pages/merch/cart/index.tsx new file mode 100644 index 00000000..0251a9bd --- /dev/null +++ b/apps/web/pages/merch/cart/index.tsx @@ -0,0 +1,367 @@ +import React, { useRef, useState, FC, useEffect, useCallback } from "react"; +import Link from "next/link"; +import { + Button, + Flex, + Heading, + useBreakpointValue, + Divider, + useDisclosure, + Grid, + GridItem, + Text, + Input, + Spinner, + // FormControl, + // FormHelperText +} from "@chakra-ui/react"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import Joi from "joi"; +import _ from "lodash"; +// import { LinkIcon } from "@chakra-ui/icons"; +import { CartAction, CartActionType, useCartStore } from "features/merch/context/cart"; +import { + CartCard, + CartEmptyView, + CartHeader, + CartItemCard, + CartRemoveModal, + LoadingScreen, + Page +} from "ui/components/merch"; +import { api } from "features/merch/services/api"; +import { PricedCart } from "types/lib/merch"; +import { routes, QueryKeys } from "features/merch/constants"; +import { displayPrice } from "features/merch/functions"; +import { useRouter } from "next/router"; + +type ValidationType = { + error: boolean; + isLoading: boolean; +}; + +const Cart: FC = () => { + // Context hook. + const cartContext = useCartStore(); + const { state: cartState, dispatch: cartDispatch } = cartContext; + + const router = useRouter(); + const [reroute, setReroute] = useState(false); + + // Email input for billing. + // const [billingEmail, setBillingEmail] = useState(""); + const [validation, setValidation] = useState({ isLoading: false, error: false }); + + // Calculation of pricing + const [priceLoading, setPriceLoading] = useState(false); + const [priceInfo, setPriceInfo] = useState(0); + + const { data: products, isLoading: isProductsQueryLoading } = useQuery([QueryKeys.PRODUCTS], () => api.getProducts(), {}); + + + // Voucher section + // const [voucherInput, setVoucherInput] = useState(""); + // const [voucherError, setVoucherError] = useState(false); + + const emailValidator = Joi.string() + .email({ tlds: { allow: false } }) + .required() + .label("Email"); + + // Removal Modal cartStates + const { isOpen, onOpen, onClose } = useDisclosure(); + const toBeRemoved = useRef({ productId: "", size: "", color: "" }); + + // Check if break point hit. + const isMobile: boolean = useBreakpointValue({ base: true, md: false }) || false; + + const { isLoading: isCartLoading } = useMutation( + () => api.postQuotation(cartState.cart, cartState.voucher), + {} + ); + + const { mutate: calcCartPrice } = useMutation( + () => api.postQuotation(cartState.cart, cartState.voucher), + { + onSuccess: (data: PricedCart) => { + setPriceInfo(data.total); + }, + onSettled: () => { + setPriceLoading(false); + }, + } + ); + + // Apply voucher - TODO + // const { mutate: applyVoucher, isLoading: voucherLoading } = useMutation( + // () => api.postQuotation(cartState.cart, voucherInput), + // { + // onMutate: () => { + // setPriceLoading(true); + // }, + // onSuccess: (data: PricedCart) => { + // setPriceInfo(data.total); + // if (data.price.discount > 0) { + // // Voucher is valid + // cartDispatch({ type: CartActionType.VALID_VOUCHER, payload: voucherInput }); + // setVoucherError(false); + // setVoucherInput(""); + // } else { + // setVoucherError(true); + // } + // }, + // onSettled: () => { + // setPriceLoading(false); + // }, + // } + // ); + + // const handleRemoveVoucher = () => { + // setVoucherInput(""); + // cartDispatch({ type: CartActionType.REMOVE_VOUCHER, payload: null }); + // applyVoucher(); + // }; + + // Update Cart Item by Size & Id (To be changed next time: BE) + const removeItem = (productId: string, size: string, color: string) => { + setPriceLoading(true); + cartDispatch({ + type: CartActionType.REMOVE_ITEM, + payload: { id: productId, size: size, color: color }, + }); + onClose(); + }; + + // Set modal's ref value to size & productId pair. + const handleRemoveItem = (productId: string, size: string, color: string) => { + onOpen(); + toBeRemoved.current.size = size; + toBeRemoved.current.color = color; + toBeRemoved.current.productId = productId; + }; + + // Update Cart Item by Size & Id (To be changed next time: BE) + const onQuantityChange = (productId: string, size: string, color: string, qty: number) => { + setPriceLoading(true); + const action: CartAction = { + type: CartActionType.UPDATE_QUANTITY, + payload: { id: productId, size: size, color: color, quantity: qty }, + }; + cartDispatch(action); + }; + + const handleToCheckout = async () => { + setValidation({ isLoading: true, error: false }); + try { + await emailValidator.validateAsync(cartState.billingEmail); + cartDispatch({ type: CartActionType.UPDATE_BILLING_EMAIL, payload: cartState.billingEmail }); + setReroute(true); + } catch (error: any) { + setValidation({ isLoading: false, error: true }); + } + }; + + const CartHeading = ( + + Your Cart + + ); + + const PriceInfoSection = ( + + {priceLoading ? ( + + + Calculating your cart price + + ) : ( + <> + + + Item(s) subtotal + {displayPrice(priceInfo)} + {/* TODO {displayPrice(priceInfo.subtotal)} */} + + + Voucher Discount + {displayPrice(0)} + {/* TODO {displayPrice(priceInfo.discount)} */} + + + + Total + {displayPrice(priceInfo)} + + + + + { + cartDispatch({ + type: CartActionType.UPDATE_NAME, + payload: event.target.value, + }); + }} + variant="outline" + /> + + { + cartDispatch({ + type: CartActionType.UPDATE_BILLING_EMAIL, + payload: event.target.value, + }); + }} + variant="outline" + /> + + {validation.error && "*Invalid email format"} + + + + + + + + + + )} + + ); +/* TODO + const VoucherSection = ( + + + + ) => { + const target = e.target as HTMLInputElement; + setVoucherInput(target.value); + }} + /> + + + + {!cartState.voucher ? ( + Apply your voucher code! + ) : ( + + {voucherError && Invalid voucher} + {cartState.voucher && priceInfo.discount > 0 && ( + + Applied Voucher + + + )} + + )} + + + + ); +*/ + const renderCartView = () => ( + + + {!isMobile && } + {cartState.cart.items.map((item, index) => ( + <> + product.id === item.id)} + isLoading={isProductsQueryLoading} + isMobile={isMobile} + onRemove={handleRemoveItem} + onQuantityChange={onQuantityChange} + /> + {index !== cartState.cart.items.length - 1 && } + + ))} + + + {/* {VoucherSection} TODO*/} + {PriceInfoSection} + + + An email will be sent to you closer to the collection date. Our collection venue is at 50 Nanyang Ave, #32 + Block N4 #02a, Singapore 639798. + + + + removeItem(toBeRemoved.current.productId, toBeRemoved.current.size, toBeRemoved.current.color)} + /> + + ); + + const renderCartContent = () => { + if (isCartLoading) { + return ; + } + if (cartState.cart.items.length === 0) { + return ; + } + return renderCartView(); + }; + + const debounceCalc = useCallback(_.debounce(calcCartPrice, 2000), []); + + useEffect(() => { + debounceCalc(); + }, [cartState.cart.items, cartState.voucher]); + + useEffect(() => { + if (reroute) { + router.push(routes.CHECKOUT); + } + }, [reroute]); + + return ( + + {CartHeading} + {renderCartContent()} + + ); +}; + +export default Cart; diff --git a/apps/web/pages/merch/index.tsx b/apps/web/pages/merch/index.tsx index 7694953d..b47369d8 100644 --- a/apps/web/pages/merch/index.tsx +++ b/apps/web/pages/merch/index.tsx @@ -1,12 +1,11 @@ import React, { useState } from "react"; import { Flex, Divider, Select, Heading, Grid } from "@chakra-ui/react"; import { useQuery } from "@tanstack/react-query"; -import { Card, Page } from "ui/components/merch"; -import { QueryKeys } from "features/merch/constants/queryKeys"; +import { Card, MerchListSkeleton, Page } from "ui/components/merch"; +import { QueryKeys } from "features/merch/constants"; import { api } from "features/merch/services/api"; import { Product } from "types/lib/merch"; -import { MerchListSkeleton } from "ui/components/merch/skeleton"; -import { isOutOfStock } from "features/merch/functions/stock"; +import { isOutOfStock } from "features/merch/functions"; const MerchandiseList = () => { const [selectedCategory, setSelectedCategory] = useState(""); diff --git a/apps/web/pages/merch/orders/[slug].tsx b/apps/web/pages/merch/orders/[slug].tsx index 9cb0fb38..5f9e8a52 100644 --- a/apps/web/pages/merch/orders/[slug].tsx +++ b/apps/web/pages/merch/orders/[slug].tsx @@ -144,8 +144,8 @@ const OrderSummary: React.FC = () => { {displayPrice(total)} - - {/*{displayPrice(*/} + + {/*{displayPrice( TODO*/} {/* (orderState?.billing?.subtotal ?? 0) -*/} {/* (orderState?.billing?.total ?? 0)*/} {/*)}*/} diff --git a/apps/web/pages/merch/product/[slug].tsx b/apps/web/pages/merch/product/[slug].tsx index 97461e88..987849de 100644 --- a/apps/web/pages/merch/product/[slug].tsx +++ b/apps/web/pages/merch/product/[slug].tsx @@ -29,19 +29,20 @@ import { useCartStore, } from "features/merch/context/cart"; import { api } from "features/merch/services/api"; -import { routes } from "features/merch/constants/routes"; -import { QueryKeys } from "features/merch/constants/queryKeys"; -import { displayPrice } from "features/merch/functions/currency"; +import { routes, QueryKeys } from "features/merch/constants"; import { + displayPrice, + displayQtyInCart, displayStock, getDefaultColor, getDefaultSize, + getQtyInCart, getQtyInStock, isColorAvailable, isOutOfStock, isSizeAvailable, -} from "features/merch/functions/stock"; -import { displayQtyInCart, getQtyInCart } from "features/merch/functions/cart"; +} from "features/merch/functions"; + const GroupTitle = ({ children }: any) => ( @@ -136,7 +137,7 @@ const MerchDetail: React.FC = () => { const handleBuyNow = () => { handleAddToCart(); - window.location.href = routes.CART; + router.push(routes.CART); }; const ProductNameSection = ( @@ -192,7 +193,7 @@ const MerchDetail: React.FC = () => { return ( { setQuantity(1); if (size !== selectedSize) { @@ -242,7 +243,7 @@ const MerchDetail: React.FC = () => { return ( { setQuantity(1); if (color !== selectedColor) { @@ -284,10 +285,11 @@ const MerchDetail: React.FC = () => { Quantity handleQtyChangeCounter(false)} > - @@ -305,12 +307,13 @@ const MerchDetail: React.FC = () => { onChange={handleQtyChangeInput} /> = maxQuantity } - active={false} + active={false.toString()} onClick={() => handleQtyChangeCounter(true)} > + diff --git a/packages/types/lib/merch.ts b/packages/types/lib/merch.ts index 2e9235dc..1af08f8c 100644 --- a/packages/types/lib/merch.ts +++ b/packages/types/lib/merch.ts @@ -111,6 +111,21 @@ export type OrderHold = { reserved_products: ReservedProduct[]; } +export type ProductInfo = { + name: string; + image: string; + price: number; +}; + +export type ProductInfoMap = Record; + +export type CartPrice = { + currency: string; + subtotal: number; + discount: number; + grandTotal: number; +}; + // API Types export const QuotationRequest = Cart.merge(z.object({ promoCode: z.string().optional(), diff --git a/packages/ui/components/layout/Layout.tsx b/packages/ui/components/layout/Layout.tsx index 64f52451..fb56629b 100644 --- a/packages/ui/components/layout/Layout.tsx +++ b/packages/ui/components/layout/Layout.tsx @@ -1,8 +1,6 @@ -import { NavBar } from "../navbar"; -import { Footer } from "../footer"; import React from "react"; -import { NavBarProps, FooterProps } from "ui"; import { Box } from "@chakra-ui/react"; +import { NavBar, Footer, NavBarProps, FooterProps } from "ui"; interface LayoutProps { navbarProps: NavBarProps; @@ -13,7 +11,7 @@ interface LayoutProps { export const Layout = ({ navbarProps, footerProps, children }: LayoutProps) => { return ( <> -
+
NTU School of Computer Science & Engineering
diff --git a/packages/ui/components/merch/Card.tsx b/packages/ui/components/merch/Card.tsx index a2675a34..610e7032 100644 --- a/packages/ui/components/merch/Card.tsx +++ b/packages/ui/components/merch/Card.tsx @@ -1,8 +1,8 @@ import { ArrowForwardIcon } from "@chakra-ui/icons"; import { Box, Image, Text, GridItem, Flex, Badge, Center } from "@chakra-ui/react"; import Link from "next/link"; -import { displayPrice } from "web/features/merch/functions/currency"; -import { routes } from "web/features/merch/constants/routes" +import { displayPrice } from "web/features/merch/functions"; +import { routes } from "web/features/merch/constants" type CardProps = { _productId: string; diff --git a/packages/ui/components/merch/CartButton.tsx b/packages/ui/components/merch/CartButton.tsx index fd6104af..1b08ef8c 100644 --- a/packages/ui/components/merch/CartButton.tsx +++ b/packages/ui/components/merch/CartButton.tsx @@ -5,7 +5,7 @@ import routes from "../../../../apps/web/features/merch/constants/routes"; const CartButton = () => { return( - ) diff --git a/packages/ui/components/merch/EmptyProductView.tsx b/packages/ui/components/merch/EmptyProductView.tsx index 259842b2..3be061c7 100644 --- a/packages/ui/components/merch/EmptyProductView.tsx +++ b/packages/ui/components/merch/EmptyProductView.tsx @@ -1,11 +1,14 @@ import React, { useEffect } from "react"; import { Center, Flex, Heading, Spinner, Text } from "@chakra-ui/react"; -import routes from "../../../../apps/web/features/merch/constants/routes"; +import { useRouter } from "next/router"; +import { routes } from "web/features/merch/constants"; export const EmptyProductView: React.FC = () => { + const router = useRouter(); + useEffect(() => { setTimeout(() => { - window.location.href = routes.HOME; + router.push(routes.HOME); }, 3000); }, []); diff --git a/packages/ui/components/merch/LoadingScreen.tsx b/packages/ui/components/merch/LoadingScreen.tsx new file mode 100644 index 00000000..ca42fb86 --- /dev/null +++ b/packages/ui/components/merch/LoadingScreen.tsx @@ -0,0 +1,21 @@ +import { Flex, Spinner, Text } from "@chakra-ui/react"; + +export type LoadingScreenProps = { + minH?: string; + text: string; +}; + +export const LoadingScreen = ({ minH = "50vh", text = "" }: LoadingScreenProps) => { + return ( + + + {text} + + ); +}; \ No newline at end of file diff --git a/packages/ui/components/merch/SizeOption.tsx b/packages/ui/components/merch/SizeOption.tsx index 4b62f025..c8e5728a 100644 --- a/packages/ui/components/merch/SizeOption.tsx +++ b/packages/ui/components/merch/SizeOption.tsx @@ -2,13 +2,13 @@ import React from "react"; import { Box, BoxProps } from "@chakra-ui/react"; type SizeOptionType = BoxProps & { - active: boolean; + active: string; disabled?: boolean; onClick: (param: any) => void; }; export const SizeOption: React.FC = (props) => { - const { active = false, disabled = false, children } = props; + const { active = false.toString(), disabled = false, children } = props; return ( = (props) => { cursor={disabled ? "not-allowed" : "pointer"} borderWidth={1} borderColor="secondary.400" - color={active ? "#FFF" : "secondary.400"} - backgroundColor={active ? "red.600" : "#FFF"} + color={active == "true" ? "#FFF" : "secondary.400"} + backgroundColor={active == "true" ? "red.600" : "#FFF"} {...props} > {children} diff --git a/packages/ui/components/merch/cart/CartCard.tsx b/packages/ui/components/merch/cart/CartCard.tsx new file mode 100644 index 00000000..cfa8492c --- /dev/null +++ b/packages/ui/components/merch/cart/CartCard.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from "react"; +import { Flex, FlexProps, Text, Divider } from "@chakra-ui/react"; + +type CartCardProps = FlexProps & { + title?: string; + children: ReactNode; +}; + +export const CartCard = ({ children, title, ...props }: CartCardProps) => { + return ( + + {title} + + {children} + + ); +}; diff --git a/packages/ui/components/merch/cart/CartEmptyView.tsx b/packages/ui/components/merch/cart/CartEmptyView.tsx new file mode 100644 index 00000000..6bed5b38 --- /dev/null +++ b/packages/ui/components/merch/cart/CartEmptyView.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import Link from 'next/link'; +import { Center, Flex, Heading, Button } from "@chakra-ui/react"; +import { routes } from "web/features/merch/constants"; + +export const CartEmptyView: React.FC = () => ( +
+ + There are no items currently in your cart. + + + + +
+); \ No newline at end of file diff --git a/packages/ui/components/merch/cart/CartHeader.tsx b/packages/ui/components/merch/cart/CartHeader.tsx new file mode 100644 index 00000000..34cd1e76 --- /dev/null +++ b/packages/ui/components/merch/cart/CartHeader.tsx @@ -0,0 +1,19 @@ +import { Box, Grid, Heading } from "@chakra-ui/react"; + +export const CartHeader = () => { + return ( + + + + Product + + {["Unit Price", "Quantity", "Subtotal"].map((item, index) => ( + // eslint-disable-next-line react/no-array-index-key + + {item} + + ))} + + + ); +}; diff --git a/packages/ui/components/merch/cart/CartItemCard.tsx b/packages/ui/components/merch/cart/CartItemCard.tsx new file mode 100644 index 00000000..e85bf92a --- /dev/null +++ b/packages/ui/components/merch/cart/CartItemCard.tsx @@ -0,0 +1,176 @@ +import React from "react"; +import Link from "next/link"; +import { + Button, + Flex, + Grid, + GridItem, + Image, + Text, + Input, + InputLeftAddon, + InputRightAddon, + InputGroup, + Box, + Center, +} from "@chakra-ui/react"; +import { DeleteIcon, SmallCloseIcon } from "@chakra-ui/icons"; +import { CartItem, Product } from "types/lib/merch"; +import { displayPrice, getQtyInStock } from "web/features/merch/functions"; +import { routes } from "web/features/merch/constants"; + +export type CartItemProps = { + isMobile: boolean; + data: CartItem; + productInfo?: Product; + isLoading: boolean; + onRemove: (productId: string, size: string, color: string) => void; + onQuantityChange: (productId: string, size: string, color: string, qty: number) => void; +}; + +const MIN_ITEM_CNT = 1; + +export const CartItemCard: React.FC = ({ isMobile, data, onRemove, onQuantityChange, productInfo }) => { + const MAX_ITEM_CNT = productInfo ? getQtyInStock(productInfo, data.color, data.size) : 1; + const handleQtyChangeCounter = (isAdd: boolean = true) => { + const value = isAdd ? 1 : -1; + if (!isAdd && data.quantity === MIN_ITEM_CNT) { + onRemove(data.id, data.size, data.color); + return; + } + if (isAdd && data.quantity === MAX_ITEM_CNT) return; + onQuantityChange(data.id, data.size, data.color, data.quantity + value); + }; + + const handleQtyChangeInput = (e: React.FormEvent): void => { + const target = e.target as HTMLInputElement; + if (Number.isNaN(parseInt(target.value, 10))) { + onQuantityChange(data.id, data.size, data.color, MIN_ITEM_CNT); + } else { + const value = parseInt(target.value, 10); + if (value <= 0) { + onRemove(data.id, data.size, data.color); + } else if (value > MAX_ITEM_CNT) { + onQuantityChange(data.id, data.size, data.color, MAX_ITEM_CNT); + } else { + onQuantityChange(data.id, data.size, data.color, value); + } + } + }; + const unitPrice = displayPrice(productInfo?.price ?? 0); + const subTotalPrice = displayPrice((productInfo?.price ?? 0) * data.quantity); + + const quantityInput = ( + + + handleQtyChangeCounter(false)}> + - + + + handleQtyChangeCounter(true)}> + + + + +
+ + In stock: {MAX_ITEM_CNT} + +
+
+ ); + const desktopView = ( + + + + + + + + + + + {productInfo?.name} + + + + Size: + + {data.size} + + + + Color: + + {data.color} + + + + + + + {unitPrice} + + + + {quantityInput} + + + + {subTotalPrice} + + + + + + + ); + + const mobileView = ( + + + + + + + + {productInfo?.name} + + + + + + Size:{" "} + + {data.size} + + + + Color:{" "} + {data.color} + + + Unit Price: {unitPrice} + {quantityInput} + Subtotal: {subTotalPrice} + + + ); + return {!isMobile ? desktopView : mobileView}; +}; diff --git a/packages/ui/components/merch/cart/CartRemoveModal.tsx b/packages/ui/components/merch/cart/CartRemoveModal.tsx new file mode 100644 index 00000000..7f99b2f1 --- /dev/null +++ b/packages/ui/components/merch/cart/CartRemoveModal.tsx @@ -0,0 +1,40 @@ +import React from "react"; + +import { Button, Divider, Modal, ModalOverlay, ModalContent, ModalFooter, ModalBody, Text } from "@chakra-ui/react"; + +type CartRemoveModalType = { + isOpen: boolean; + onClose: () => void; + removeItem: () => void; +}; + +export const CartRemoveModal: React.FC = (props) => { + const { isOpen, onClose, removeItem } = props; + return ( + + + + + Do you want to remove this product? + + + + + + + + + ); +}; diff --git a/packages/ui/components/merch/cart/index.tsx b/packages/ui/components/merch/cart/index.tsx new file mode 100644 index 00000000..297129fa --- /dev/null +++ b/packages/ui/components/merch/cart/index.tsx @@ -0,0 +1,5 @@ +export * from "./CartCard" +export * from "./CartHeader" +export * from "./CartItemCard" +export * from "./CartEmptyView" +export * from "./CartRemoveModal" \ No newline at end of file diff --git a/packages/ui/components/merch/index.tsx b/packages/ui/components/merch/index.tsx index d0f4edee..07f5f004 100644 --- a/packages/ui/components/merch/index.tsx +++ b/packages/ui/components/merch/index.tsx @@ -1,5 +1,7 @@ +export * from "./cart" export * from "./Card" export * from "./EmptyProductView" +export * from "./LoadingScreen" export * from "./MerchCarousel" export * from "./Page" export * from "./SizeChartDialog" diff --git a/packages/ui/components/navbar/Logo.tsx b/packages/ui/components/navbar/Logo.tsx index 76a169b4..9892f57e 100644 --- a/packages/ui/components/navbar/Logo.tsx +++ b/packages/ui/components/navbar/Logo.tsx @@ -1,5 +1,5 @@ -import React from "react"; import { Box, Stack, Link } from "@chakra-ui/react"; +import NextLink from "next/link"; import { Image } from "../image"; export interface LogoProps { @@ -9,7 +9,11 @@ export interface LogoProps { export const Logo = ({ src, alt }: LogoProps) => { return ( - + { {/* CTA Button -> Contact */} {/* If on merch site, change to Cart */} { const router = useRouter(); return ( Date: Mon, 19 Jun 2023 21:58:39 +0800 Subject: [PATCH 23/60] cleanup: Remove quotation HTTP request --- apps/merch/src/index.ts | 2 - apps/merch/src/routes/quotation.ts | 33 ------- apps/web/features/merch/services/api.ts | 9 +- apps/web/pages/merch/cart/index.tsx | 124 +++++++++++++----------- packages/merch-helpers/src/lib/price.ts | 6 +- packages/types/lib/merch.ts | 4 +- 6 files changed, 74 insertions(+), 104 deletions(-) delete mode 100644 apps/merch/src/routes/quotation.ts diff --git a/apps/merch/src/index.ts b/apps/merch/src/index.ts index 29c2dea9..0e8b940e 100644 --- a/apps/merch/src/index.ts +++ b/apps/merch/src/index.ts @@ -7,7 +7,6 @@ import { checkout } from "./routes/checkout"; import { index, notFound } from "./routes/index"; import { orderGet } from "./routes/orders"; import { productGet, productsAll } from "./routes/products"; -import { quotation } from "./routes/quotation"; const app = express(); const CORS_ORIGIN = process.env.CORS_ORIGIN; @@ -33,7 +32,6 @@ app.get("/", index); app.get("/orders/:id", orderGet); app.get("/products", productsAll); app.get("/products/:id", productGet); -app.post("/quotation", quotation); app.post("/checkout", checkout); app.use(notFound); diff --git a/apps/merch/src/routes/quotation.ts b/apps/merch/src/routes/quotation.ts deleted file mode 100644 index 890c9e8c..00000000 --- a/apps/merch/src/routes/quotation.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { calculatePricing, PricingError } from "merch-helpers"; -import { PricedCart, Product, QuotationRequest } from "types"; -import { getProducts } from "../db"; -import { Request, Response } from "../lib/types"; - -export const quotation = (req: Request, res: Response) => { - const body = QuotationRequest.safeParse(req.body); - if (!body.success) { - return res.status(400).json({ - error: "INVALID_TYPE", - detail: body.error.format(), - }); - } - - const cart = body.data; - - getProducts() - .then((products: Product[]) => { - // TODO: Fetch promotion. - return calculatePricing(products, cart, undefined); - }) - .then((cart: PricedCart) => res.json(cart)) - .catch((e) => { - if (e instanceof PricingError) { - return res.status(400).json({ - error: "INVALID_REQUEST", - detail: e.message, - }); - } - console.warn(e); - return res.status(500).json({ error: "INTERNAL_SERVER_ERROR" }); - }); -}; diff --git a/apps/web/features/merch/services/api.ts b/apps/web/features/merch/services/api.ts index baf5bfc0..c4f239d8 100644 --- a/apps/web/features/merch/services/api.ts +++ b/apps/web/features/merch/services/api.ts @@ -74,7 +74,7 @@ export class Api { return res; } - async postCheckoutCart(cart: Cart, email: string, promoCode?: string) { + async postCheckoutCart(cart: Cart, email: string, promoCode?: string): Promise { return await this.post( `/cart/checkout`, { @@ -84,13 +84,6 @@ export class Api { } ); } - - async postQuotation(cart: Cart, promoCode: string | null) { - return await this.post(`/quotation`, { - ...cart, - promoCode: promoCode ?? "", - }); - } } export const api = new Api(); diff --git a/apps/web/pages/merch/cart/index.tsx b/apps/web/pages/merch/cart/index.tsx index 0251a9bd..eac4115a 100644 --- a/apps/web/pages/merch/cart/index.tsx +++ b/apps/web/pages/merch/cart/index.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, FC, useEffect, useCallback } from "react"; +import React, { useRef, useState, FC, useEffect } from "react"; import Link from "next/link"; import { Button, @@ -12,14 +12,14 @@ import { Text, Input, Spinner, - // FormControl, - // FormHelperText } from "@chakra-ui/react"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import Joi from "joi"; -import _ from "lodash"; -// import { LinkIcon } from "@chakra-ui/icons"; -import { CartAction, CartActionType, useCartStore } from "features/merch/context/cart"; +import { + CartAction, + CartActionType, + useCartStore, +} from "features/merch/context/cart"; import { CartCard, CartEmptyView, @@ -27,12 +27,12 @@ import { CartItemCard, CartRemoveModal, LoadingScreen, - Page + Page, } from "ui/components/merch"; import { api } from "features/merch/services/api"; -import { PricedCart } from "types/lib/merch"; import { routes, QueryKeys } from "features/merch/constants"; import { displayPrice } from "features/merch/functions"; +import { calculatePricing } from "merch-helpers"; import { useRouter } from "next/router"; type ValidationType = { @@ -49,20 +49,29 @@ const Cart: FC = () => { const [reroute, setReroute] = useState(false); // Email input for billing. - // const [billingEmail, setBillingEmail] = useState(""); - const [validation, setValidation] = useState({ isLoading: false, error: false }); + const [validation, setValidation] = useState({ + isLoading: false, + error: false, + }); // Calculation of pricing - const [priceLoading, setPriceLoading] = useState(false); - const [priceInfo, setPriceInfo] = useState(0); - - const { data: products, isLoading: isProductsQueryLoading } = useQuery([QueryKeys.PRODUCTS], () => api.getProducts(), {}); - + const [isCartLoading, setIsCartLoading] = useState(true); + const { data: products, isLoading: isProductsQueryLoading } = useQuery( + [QueryKeys.PRODUCTS], + () => api.getProducts(), + { + onSuccess: () => { + setIsCartLoading(false); + } + } + ); // Voucher section // const [voucherInput, setVoucherInput] = useState(""); // const [voucherError, setVoucherError] = useState(false); + const pricedCart = products ? calculatePricing(products, cartState.cart, undefined) : null; + const emailValidator = Joi.string() .email({ tlds: { allow: false } }) .required() @@ -73,24 +82,8 @@ const Cart: FC = () => { const toBeRemoved = useRef({ productId: "", size: "", color: "" }); // Check if break point hit. - const isMobile: boolean = useBreakpointValue({ base: true, md: false }) || false; - - const { isLoading: isCartLoading } = useMutation( - () => api.postQuotation(cartState.cart, cartState.voucher), - {} - ); - - const { mutate: calcCartPrice } = useMutation( - () => api.postQuotation(cartState.cart, cartState.voucher), - { - onSuccess: (data: PricedCart) => { - setPriceInfo(data.total); - }, - onSettled: () => { - setPriceLoading(false); - }, - } - ); + const isMobile: boolean = + useBreakpointValue({ base: true, md: false }) || false; // Apply voucher - TODO // const { mutate: applyVoucher, isLoading: voucherLoading } = useMutation( @@ -124,7 +117,6 @@ const Cart: FC = () => { // Update Cart Item by Size & Id (To be changed next time: BE) const removeItem = (productId: string, size: string, color: string) => { - setPriceLoading(true); cartDispatch({ type: CartActionType.REMOVE_ITEM, payload: { id: productId, size: size, color: color }, @@ -141,8 +133,12 @@ const Cart: FC = () => { }; // Update Cart Item by Size & Id (To be changed next time: BE) - const onQuantityChange = (productId: string, size: string, color: string, qty: number) => { - setPriceLoading(true); + const onQuantityChange = ( + productId: string, + size: string, + color: string, + qty: number + ) => { const action: CartAction = { type: CartActionType.UPDATE_QUANTITY, payload: { id: productId, size: size, color: color, quantity: qty }, @@ -154,7 +150,10 @@ const Cart: FC = () => { setValidation({ isLoading: true, error: false }); try { await emailValidator.validateAsync(cartState.billingEmail); - cartDispatch({ type: CartActionType.UPDATE_BILLING_EMAIL, payload: cartState.billingEmail }); + cartDispatch({ + type: CartActionType.UPDATE_BILLING_EMAIL, + payload: cartState.billingEmail, + }); setReroute(true); } catch (error: any) { setValidation({ isLoading: false, error: true }); @@ -169,7 +168,7 @@ const Cart: FC = () => { const PriceInfoSection = ( - {priceLoading ? ( + {!pricedCart ? ( Calculating your cart price @@ -177,20 +176,22 @@ const Cart: FC = () => { ) : ( <> - + Item(s) subtotal - {displayPrice(priceInfo)} - {/* TODO {displayPrice(priceInfo.subtotal)} */} + {displayPrice(pricedCart.subtotal)} Voucher Discount - {displayPrice(0)} - {/* TODO {displayPrice(priceInfo.discount)} */} + {displayPrice(pricedCart.discount)} - + Total - {displayPrice(priceInfo)} + {displayPrice(pricedCart.total)} @@ -233,7 +234,11 @@ const Cart: FC = () => { onClick={handleToCheckout} _hover={{ bg: "primary-blue" }} isLoading={validation.isLoading} - disabled={cartState.billingEmail.length === 0 || cartState.name.length === 0 || validation.isLoading} + disabled={ + cartState.billingEmail.length === 0 || + cartState.name.length === 0 || + validation.isLoading + } > CHECK OUT @@ -248,7 +253,7 @@ const Cart: FC = () => { )} ); -/* TODO + /* TODO const VoucherSection = ( @@ -300,13 +305,13 @@ const Cart: FC = () => { const renderCartView = () => ( - {!isMobile && } + {!isMobile && } {cartState.cart.items.map((item, index) => ( <> product.id === item.id)} + productInfo={products?.find((product) => product.id === item.id)} isLoading={isProductsQueryLoading} isMobile={isMobile} onRemove={handleRemoveItem} @@ -321,15 +326,22 @@ const Cart: FC = () => { {PriceInfoSection} - An email will be sent to you closer to the collection date. Our collection venue is at 50 Nanyang Ave, #32 - Block N4 #02a, Singapore 639798. + An email will be sent to you closer to the collection date. Our + collection venue is at 50 Nanyang Ave, #32 Block N4 #02a, Singapore + 639798. removeItem(toBeRemoved.current.productId, toBeRemoved.current.size, toBeRemoved.current.color)} + removeItem={() => + removeItem( + toBeRemoved.current.productId, + toBeRemoved.current.size, + toBeRemoved.current.color + ) + } /> ); @@ -344,12 +356,6 @@ const Cart: FC = () => { return renderCartView(); }; - const debounceCalc = useCallback(_.debounce(calcCartPrice, 2000), []); - - useEffect(() => { - debounceCalc(); - }, [cartState.cart.items, cartState.voucher]); - useEffect(() => { if (reroute) { router.push(routes.CHECKOUT); diff --git a/packages/merch-helpers/src/lib/price.ts b/packages/merch-helpers/src/lib/price.ts index bb9b8c4a..02e61f58 100644 --- a/packages/merch-helpers/src/lib/price.ts +++ b/packages/merch-helpers/src/lib/price.ts @@ -92,9 +92,13 @@ export const calculatePricing = ( discountedPrice: itemPrice, }; }); + const subtotal = pricedItems.reduce((acc, item) => acc + item.originalPrice, 0); + const total = pricedItems.reduce((acc, item) => acc + item.discountedPrice, 0); return { promoCode: promotion?.promoCode, - total: pricedItems.reduce((acc, item) => acc + item.discountedPrice, 0), + subtotal: subtotal, + discount: subtotal-total, + total: total, items: pricedItems, }; }; diff --git a/packages/types/lib/merch.ts b/packages/types/lib/merch.ts index 1af08f8c..7675ce7b 100644 --- a/packages/types/lib/merch.ts +++ b/packages/types/lib/merch.ts @@ -80,8 +80,10 @@ export interface Promotion { }>; } -export interface PricedCart { +export type PricedCart = { promoCode?: string; + subtotal: number; + discount: number; total: number; items: { id: string; From 672d80ab5e39d7363dcf093a36965148de050ee2 Mon Sep 17 00:00:00 2001 From: dyllon Date: Tue, 20 Jun 2023 00:23:13 +0800 Subject: [PATCH 24/60] feat: Add merch payment --- .gitignore | 6 +- apps/cms/.env.example | 4 +- apps/merch/.env.example | 2 + apps/merch/package.json | 16 +- apps/merch/src/db/dynamodb.ts | 2 +- apps/merch/src/db/orders.ts | 42 ++- apps/merch/src/db/products.ts | 25 +- apps/merch/src/index.ts | 18 +- apps/merch/src/routes/checkout.ts | 22 +- apps/merch/src/trpc/lib.ts | 17 + apps/merch/src/trpc/products/index.ts | 20 ++ apps/merch/src/trpc/router.ts | 17 + apps/merch/tsconfig.json | 11 +- apps/merch/tsoa.json | 12 + apps/web/.eslintrc.js | 9 +- .../merch/components/checkout/Skeleton.tsx | 46 +++ .../merch/components/checkout/StripeForm.tsx | 103 ++++++ apps/web/features/merch/constants/routes.ts | 4 +- .../web/features/merch/context/cart/index.tsx | 4 + .../features/merch/context/checkout/index.tsx | 42 +++ apps/web/features/merch/services/api.ts | 35 ++- apps/web/lib/trpc.ts | 39 +++ apps/web/package.json | 10 +- apps/web/pages/_app.tsx | 23 +- apps/web/pages/merch/cart/index.tsx | 10 +- apps/web/pages/merch/checkout/index.tsx | 188 +++++++++++ apps/web/pages/merch/product/[slug].tsx | 111 +++++-- apps/web/tsconfig.json | 7 +- packages/nodelogger/package.json | 6 +- packages/nodelogger/src/index.ts | 2 +- .../nodelogger/src/lib/morganMiddleware.ts | 16 +- packages/nodelogger/tsconfig.json | 3 + packages/types/lib/merch.ts | 46 ++- turbo.json | 12 +- yarn.lock | 296 ++++++++++++++++-- 35 files changed, 1055 insertions(+), 171 deletions(-) create mode 100644 apps/merch/src/trpc/lib.ts create mode 100644 apps/merch/src/trpc/products/index.ts create mode 100644 apps/merch/src/trpc/router.ts create mode 100644 apps/merch/tsoa.json create mode 100644 apps/web/features/merch/components/checkout/Skeleton.tsx create mode 100644 apps/web/features/merch/components/checkout/StripeForm.tsx create mode 100644 apps/web/features/merch/context/checkout/index.tsx create mode 100644 apps/web/lib/trpc.ts create mode 100644 apps/web/pages/merch/checkout/index.tsx diff --git a/.gitignore b/.gitignore index abbfb81b..40b99036 100644 --- a/.gitignore +++ b/.gitignore @@ -13,15 +13,11 @@ node_modules coverage # build +**/.next **/build **/out **/dist -# next.js -.next/ -out/ -build - # misc .DS_Store *.pem diff --git a/apps/cms/.env.example b/apps/cms/.env.example index 8ba55f46..53574c44 100644 --- a/apps/cms/.env.example +++ b/apps/cms/.env.example @@ -1,7 +1,7 @@ MONGODB_URI=mongodb://localhost/cms PAYLOAD_SECRET= -PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000 -PAYLOAD_PUBLIC_SERVER_PORT=3000 +PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3003 +PAYLOAD_PUBLIC_SERVER_PORT=3003 FRONTEND_STAGING_DOMAIN=https://dev.ntuscse.com S3_ACCESS_KEY_ID= S3_SECRET_ACCESS_KEY= diff --git a/apps/merch/.env.example b/apps/merch/.env.example index 685110c1..b90efd39 100644 --- a/apps/merch/.env.example +++ b/apps/merch/.env.example @@ -1,4 +1,6 @@ AWS_REGION= PRODUCT_TABLE_NAME= ORDER_TABLE_NAME= +ORDER_HOLD_TABLE_NAME= CORS_ORIGIN=http://localhost:3001 +STRIPE_SECRET_KEY= diff --git a/apps/merch/package.json b/apps/merch/package.json index e07084d4..dd7b6ce1 100644 --- a/apps/merch/package.json +++ b/apps/merch/package.json @@ -15,18 +15,23 @@ "dependencies": { "@aws-sdk/client-dynamodb": "^3.289.0", "@aws-sdk/util-dynamodb": "^3.289.0", + "@trpc/server": "^10.31.0", + "add": "^2.0.6", "cors": "^2.8.5", - "express": "^4.17.1", + "dotenv": "^16.3.1", + "express": "^4.18.2", "nodelogger": "*", "stripe": "^12.5.0", - "uuid": "^9.0.0" + "trpc-panel": "^1.3.4", + "uuid": "^9.0.0", + "yarn": "^1.22.19", + "zod": "^3.21.4" }, "devDependencies": { "@swc/core": "^1.3.64", "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.13", - "@types/express": "^4.17.9", - "@types/morgan": "^1.9.4", + "@types/express": "^4.17.17", "@types/uuid": "^9.0.1", "cookie-parser": "^1.4.6", "morgan": "^1.10.0", @@ -35,7 +40,6 @@ "tsconfig": "*", "tsup": "^6.7.0", "types": "*", - "typescript": "^4.8.4", - "winston": "^3.8.2" + "typescript": "^4.8.4" } } diff --git a/apps/merch/src/db/dynamodb.ts b/apps/merch/src/db/dynamodb.ts index b269609d..0664222c 100644 --- a/apps/merch/src/db/dynamodb.ts +++ b/apps/merch/src/db/dynamodb.ts @@ -72,7 +72,7 @@ export const writeItem = async ( try { await client.send(command); } catch (error: any) { - if (error.code === 'ConditionalCheckFailedException') { + if (error.code === "ConditionalCheckFailedException") { Logger.warn(`Item already exists in table ${tableName}`); return; } diff --git a/apps/merch/src/db/orders.ts b/apps/merch/src/db/orders.ts index b79a5dfb..edff273b 100644 --- a/apps/merch/src/db/orders.ts +++ b/apps/merch/src/db/orders.ts @@ -2,8 +2,15 @@ import { readItem, writeItem } from "./dynamodb"; import { v4 as uuidv4 } from "uuid"; import { Order, OrderItem, OrderStatus, OrderHoldEntry } from "types"; -const ORDER_TABLE_NAME = process.env.ORDER_TABLE_NAME || ""; -const ORDER_HOLD_TABLE_NAME = process.env.ORDER_HOLD_TABLE_NAME || ""; +const ORDER_TABLE_NAME = process.env.ORDER_TABLE_NAME; +const ORDER_HOLD_TABLE_NAME = process.env.ORDER_HOLD_TABLE_NAME; + +if (!ORDER_TABLE_NAME) { + throw new Error("ORDER_TABLE_NAME is not defined"); +} +if (!ORDER_HOLD_TABLE_NAME) { + throw new Error("ORDER_HOLD_TABLE_NAME is not defined"); +} interface DynamoOrderItem { id: string; @@ -13,7 +20,7 @@ interface DynamoOrderItem { price: number; name: string; colorway: string; - product_category: string; + // product_category: string; } interface DynamoOrder { @@ -26,6 +33,10 @@ interface DynamoOrder { orderDateTime: string; } +interface DynamoOrderHoldEntry { + // todo +} + export const getOrder = async (id: string) => { const dynamoOrder = await readItem( ORDER_TABLE_NAME ?? "", @@ -48,7 +59,7 @@ const decodeOrder = (order: DynamoOrder): Order => { items: order.orderItems.map((item) => ({ id: item.id || "", name: item.name || "", - category: item.product_category || "", + // category: item.product_category || "", image: item.image || undefined, color: item.colorway || "", size: item.size || "", @@ -58,7 +69,7 @@ const decodeOrder = (order: DynamoOrder): Order => { status: order.status || OrderStatus.PENDING_PAYMENT, customer_email: order.customerEmail || "", transaction_id: order.transactionID || "", - transaction_time: date || undefined, + transaction_time: date || null, }; }; @@ -70,16 +81,16 @@ const encodeOrderItem = (item: OrderItem): DynamoOrderItem => ({ price: item.price, name: item.name, colorway: item.color, - product_category: item.category, + // product_category: item.category, }); const encodeOrder = (order: Order): DynamoOrder => ({ + transactionID: order.transaction_id || "", orderID: order.id, paymentGateway: order.payment_method || "", orderItems: order.items.map(encodeOrderItem), status: order.status || OrderStatus.PENDING_PAYMENT, customerEmail: order.customer_email || "", - transactionID: order.transaction_id || "", orderDateTime: order.transaction_time ? new Date(order.transaction_time).toISOString() : new Date().toISOString(), @@ -92,6 +103,17 @@ export const createOrder = async (order: Order): Promise => { return decodeOrder(dynamoOrder); }; -export const createOrderHoldEntry = async (orderHoldEntry: OrderHoldEntry): Promise => { - await writeItem(ORDER_HOLD_TABLE_NAME, orderHoldEntry); -}; \ No newline at end of file +const encodeOrderHoldEntry = ( + _orderHoldEntry: OrderHoldEntry +): DynamoOrderHoldEntry => { + return { + // todo + }; +}; + +export const createOrderHoldEntry = async ( + orderHoldEntry: OrderHoldEntry +): Promise => { + const dynamoOrderHoldEntry = encodeOrderHoldEntry(orderHoldEntry); + await writeItem(ORDER_HOLD_TABLE_NAME, dynamoOrderHoldEntry); +}; diff --git a/apps/merch/src/db/products.ts b/apps/merch/src/db/products.ts index 296fc0a1..bb9c669d 100644 --- a/apps/merch/src/db/products.ts +++ b/apps/merch/src/db/products.ts @@ -1,7 +1,11 @@ import { readItem, readTable, updateItem } from "./dynamodb"; import { Product } from "types"; -const PRODUCT_TABLE_NAME = process.env.PRODUCT_TABLE_NAME || ""; +const PRODUCT_TABLE_NAME = process.env.PRODUCT_TABLE_NAME; + +if (!PRODUCT_TABLE_NAME) { + throw new Error("PRODUCT_TABLE_NAME not defined"); +} export const getProducts = async () => { const dynamoProducts = await readTable(PRODUCT_TABLE_NAME); @@ -35,7 +39,7 @@ const decodeProduct = (product: DynamoProduct): Product => { name: product.name || "", price: product.price || 0, category: product.product_category || "", - size_chart: product.size_chart || null, + size_chart: product.size_chart || undefined, images: product.images || [], colors: product.colorways || {}, is_available: product.is_available || false, @@ -44,12 +48,18 @@ const decodeProduct = (product: DynamoProduct): Product => { }; }; -export const incrementStockCount = async (item_id: string, increment_value: number, size: string, color: string): Promise => { +export const incrementStockCount = async ( + item_id: string, + increment_value: number, + size: string, + color: string +): Promise => { const update_expression = `ADD stock.#color.#size :incrementValue`; - const condition_expression = "is_available = :isAvailable AND stock.#color.#size >= :incrementValue"; + const condition_expression = + "is_available = :isAvailable AND stock.#color.#size >= :incrementValue"; const expression_attribute_values = { - ":incrementValue": { "N": String(increment_value) }, - ":isAvailable": { "BOOL": true }, + ":incrementValue": { N: String(increment_value) }, + ":isAvailable": { BOOL: true }, }; const expression_attribute_names = { "#color": color, @@ -62,7 +72,6 @@ export const incrementStockCount = async (item_id: string, increment_value: numb condition_expression, expression_attribute_values, expression_attribute_names, - "id", + "id" ); }; - diff --git a/apps/merch/src/index.ts b/apps/merch/src/index.ts index 0e8b940e..f8707836 100644 --- a/apps/merch/src/index.ts +++ b/apps/merch/src/index.ts @@ -3,10 +3,13 @@ import cors from "cors"; import express from "express"; import { Logger, nodeloggerMiddleware } from "nodelogger"; import path from "path"; +import { renderTrpcPanel } from "trpc-panel"; +import "dotenv/config"; import { checkout } from "./routes/checkout"; import { index, notFound } from "./routes/index"; import { orderGet } from "./routes/orders"; import { productGet, productsAll } from "./routes/products"; +import { appRouter, trpcMiddleware } from "./trpc/router"; const app = express(); const CORS_ORIGIN = process.env.CORS_ORIGIN; @@ -24,17 +27,28 @@ if (CORS_ORIGIN) { app.use(nodeloggerMiddleware); app.use(corsMiddleware); app.use(express.json()); -app.use(express.urlencoded({ extended: false })); +app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, "public"))); +// express routes app.get("/", index); app.get("/orders/:id", orderGet); app.get("/products", productsAll); app.get("/products/:id", productGet); app.post("/checkout", checkout); + +// trpc +app.use("/trpc", trpcMiddleware); +app.use("/trpc-panel", (_, res) => { + return res.send( + renderTrpcPanel(appRouter, { url: "http://localhost:3002/trpc" }) + ); +}); + app.use(notFound); -app.listen("3000", () => Logger.info("server started on port 3000")); +const port = 3002; +app.listen(port, () => Logger.info(`server started on port ${port}`)); export default app; diff --git a/apps/merch/src/routes/checkout.ts b/apps/merch/src/routes/checkout.ts index 2344d300..040a7b10 100644 --- a/apps/merch/src/routes/checkout.ts +++ b/apps/merch/src/routes/checkout.ts @@ -16,7 +16,7 @@ import { createOrder, createOrderHoldEntry, getProducts, - incrementStockCount, + // incrementStockCount, } from "../db"; import { Request, Response } from "../lib/types"; @@ -59,6 +59,7 @@ export const checkout = (req: Request, res: Response) => { getProducts() .then((products: Product[]): [Product[], PricedCart] => { // TODO: Fetch promotion. + console.log("calculating prices prices"); return [products, calculatePricing(products, cart, undefined)]; }) .then(([products, cart]) => @@ -79,6 +80,7 @@ export const checkout = (req: Request, res: Response) => { ]) ) .then(([cart, stripeIntent]) => { + console.log("creating order"); const transactionID = stripeIntent.id; const orderItems = cart.items.map( (item): OrderItem => ({ @@ -111,24 +113,30 @@ export const checkout = (req: Request, res: Response) => { reserved_products: reserved, }; - const stockIncrements = cart.items.map((item) => - incrementStockCount(item.id, -item.quantity, item.size, item.color) - ); + // TODO: fix and uncomment stock increment + hold order + + // const stockIncrements = cart.items.map((item) => + // incrementStockCount(item.id, -item.quantity, item.size, item.color) + // ); return Promise.all([ createOrder(order), stripeIntent, - createOrderHoldEntry(orderHold), - ...stockIncrements, + // createOrderHoldEntry(orderHold), + // ...stockIncrements, ]); }) .then(([order, stripeIntent]) => { + console.log("order created"); res.json({ ...order, expiry: expiryTime.toISOString(), + price: { + grandTotal: stripeIntent.amount, + }, payment: { method: "stripe", - client_secret: stripeIntent.client_secret ?? "", + clientSecret: stripeIntent.client_secret ?? "", }, }); }) diff --git a/apps/merch/src/trpc/lib.ts b/apps/merch/src/trpc/lib.ts new file mode 100644 index 00000000..be807f2c --- /dev/null +++ b/apps/merch/src/trpc/lib.ts @@ -0,0 +1,17 @@ +import { inferAsyncReturnType, initTRPC } from "@trpc/server"; +import * as trpcExpress from "@trpc/server/adapters/express"; + +export const createContext = ({ + req, + res, +}: trpcExpress.CreateExpressContextOptions) => ({}); // no context + +type Context = inferAsyncReturnType; + +const t = initTRPC.context().create(); // todo: add context to initTRPC + +export const router = t.router; +export const middleware = t.middleware; +export const publicProcedure = t.procedure; + +export const mergeRouters = t.mergeRouters; diff --git a/apps/merch/src/trpc/products/index.ts b/apps/merch/src/trpc/products/index.ts new file mode 100644 index 00000000..30d44776 --- /dev/null +++ b/apps/merch/src/trpc/products/index.ts @@ -0,0 +1,20 @@ +import { publicProcedure, router } from "../lib"; +import { z } from "zod"; +import { getProduct, getProducts } from "../../db"; + +export const productsRouter = router({ + getProducts: publicProcedure.query(async () => { + // retrieve products from database + const products = await getProducts(); + return { products }; + }), + getProduct: publicProcedure + .input( + z.object({ + id: z.string().nonempty("id"), + }) + ) + .query(({ input }) => { + return getProduct(input.id); + }), +}); diff --git a/apps/merch/src/trpc/router.ts b/apps/merch/src/trpc/router.ts new file mode 100644 index 00000000..4ad7e1ea --- /dev/null +++ b/apps/merch/src/trpc/router.ts @@ -0,0 +1,17 @@ +import { mergeRouters, publicProcedure, router } from "./lib"; +import * as trpcExpress from "@trpc/server/adapters/express"; +import { createContext } from "./lib"; +import { productsRouter } from "./products"; + +const greetingRouter = router({ + greeting: publicProcedure.query(() => "hello tRPC v10!"), +}); + +export const appRouter = mergeRouters(greetingRouter, productsRouter); + +export type AppRouter = typeof appRouter; + +export const trpcMiddleware = trpcExpress.createExpressMiddleware({ + router: appRouter, + createContext, +}); diff --git a/apps/merch/tsconfig.json b/apps/merch/tsconfig.json index af3d1304..6b507c40 100644 --- a/apps/merch/tsconfig.json +++ b/apps/merch/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "tsconfig/node.json", "compilerOptions": { + "composite": true, + "experimentalDecorators": true, "target": "es5", "lib": [ "esnext" @@ -9,9 +11,10 @@ "esModuleInterop": true, "skipLibCheck": true, "outDir": "./dist", - "rootDir": "./src", + "rootDir": ".", "allowSyntheticDefaultImports": true, - "strict": true + "strict": true, + "resolveJsonModule": true }, "include": [ "src" @@ -19,9 +22,9 @@ "exclude": [ "node_modules", "dist", - "build", + "build" ], "ts-node": { "transpileOnly": true - }, + } } diff --git a/apps/merch/tsoa.json b/apps/merch/tsoa.json new file mode 100644 index 00000000..043ca2eb --- /dev/null +++ b/apps/merch/tsoa.json @@ -0,0 +1,12 @@ +{ + "entryFile": "src/index.ts", + "noImplicitAdditionalProperties": "throw-on-extras", + "controllerPathGlobs": ["src/**/*Controller.ts"], + "spec": { + "outputDirectory": "dist", + "specVersion": 3 + }, + "routes": { + "routesDir": "dist" + } +} diff --git a/apps/web/.eslintrc.js b/apps/web/.eslintrc.js index 31799235..148f531c 100644 --- a/apps/web/.eslintrc.js +++ b/apps/web/.eslintrc.js @@ -1,12 +1,15 @@ module.exports = { root: true, extends: ["custom"], + parserOptions: { + project: ["./tsconfig.json"], + }, rules: { "no-restricted-imports": [ "error", { - "patterns": ["@features/*/*"] - } - ] + patterns: ["@features/*/*"], + }, + ], }, }; diff --git a/apps/web/features/merch/components/checkout/Skeleton.tsx b/apps/web/features/merch/components/checkout/Skeleton.tsx new file mode 100644 index 00000000..40621aa9 --- /dev/null +++ b/apps/web/features/merch/components/checkout/Skeleton.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { + Grid, + Skeleton, + SkeletonText, + GridItem, + Box, + Flex, +} from "@chakra-ui/react"; + +const ItemSkeleton: React.FC = () => { + return ( + + + + + ); +}; +const CheckoutSkeleton: React.FC = () => { + return ( + + + + + + + + + + + + + + + + + + + ); +}; + +export default CheckoutSkeleton; diff --git a/apps/web/features/merch/components/checkout/StripeForm.tsx b/apps/web/features/merch/components/checkout/StripeForm.tsx new file mode 100644 index 00000000..2912ac18 --- /dev/null +++ b/apps/web/features/merch/components/checkout/StripeForm.tsx @@ -0,0 +1,103 @@ +import React, { useState } from "react"; +import { useRouter } from "next/router"; +import { + Elements, + PaymentElement, + useStripe, + useElements, +} from "@stripe/react-stripe-js"; +import { loadStripe } from "@stripe/stripe-js"; +import { Button, useToast } from "@chakra-ui/react"; +import { CartAction, CartActionType, useCartStore } from "../../context/cart"; +import { useCheckoutStore } from "../../context/checkout"; +import { routes } from "@/features/merch/constants"; + +type StripeFormProps = { + clientSecret: string; +}; +// Make sure to call `loadStripe` outside of a component’s render to avoid +// recreating the `Stripe` object on every render. +const stripePromise = loadStripe( + `${process.env.NEXT_PUBLIC_PUBLISHABLE_STRIPE_KEY ?? ""}` +); + +const PaymentForm = () => { + const router = useRouter(); + const stripe = useStripe(); + const elements = useElements(); + const cartContext = useCartStore(); + const { dispatch: cartDispatch, state: cartState } = cartContext; + const [isLoading, setIsLoading] = useState(false); + const toast = useToast(); + + const { state: checkoutState } = useCheckoutStore(); + + const handleSubmit = async (event: React.FormEvent) => { + // We don't want to let default form submission happen here, + // which would refresh the page. + event.preventDefault(); + // Stripe.js has not yet loaded. + if (!stripe || !elements) return; + + setIsLoading(true); + const result = await stripe.confirmPayment({ + elements, + redirect: "if_required", + confirmParams: { + receipt_email: cartState.billingEmail, + }, + }); + setIsLoading(false); + if (result.error) { + // Show error to your customer (for example, payment details incomplete) + toast({ + title: "Error", + description: result?.error.message, + status: "error", + isClosable: true, + }); + } else { + // TODO: CURRENTLY HARDCODED ONLY 1 ORDER, CHANGE IN THE FUTURE WHEN BACKEND IMPLEMENTED + // TODO: MIGRATE INTO HELPER FUNCTION UNDER API 'setOrderPaymentSucess' + // TODO: remove userId as we do not have a login + // TODO: order ID to be generated iteratively with api call + setIsLoading(true); + + cartDispatch({ + type: CartActionType.RESET_CART, + }); + setIsLoading(false); + if (typeof checkoutState?.id === "string") { + await router.push(`${routes.ORDERS}/${checkoutState?.id}`); + } else { + // TODO: handle missing id + } + } + }; + return ( +
+ + + + ); +}; + +const StripeForm: React.FC = (props) => { + const { clientSecret } = props; + + return ( + + + + ); +}; + +export default StripeForm; diff --git a/apps/web/features/merch/constants/routes.ts b/apps/web/features/merch/constants/routes.ts index 9128a8fc..d811e090 100644 --- a/apps/web/features/merch/constants/routes.ts +++ b/apps/web/features/merch/constants/routes.ts @@ -3,7 +3,7 @@ type Routes = { PRODUCT: string; CART: string; CHECKOUT: string; - ORDER_SUMMARY: string; + ORDERS: string; }; export const routes: Routes = { @@ -11,7 +11,7 @@ export const routes: Routes = { PRODUCT: "/merch/product", CART: "/merch/cart", CHECKOUT: "/merch/checkout", - ORDER_SUMMARY: "/merch/order-summary", + ORDERS: "/merch/orders", }; export default routes; diff --git a/apps/web/features/merch/context/cart/index.tsx b/apps/web/features/merch/context/cart/index.tsx index cde921a8..ccde06ec 100644 --- a/apps/web/features/merch/context/cart/index.tsx +++ b/apps/web/features/merch/context/cart/index.tsx @@ -19,6 +19,7 @@ export enum CartActionType { } export type CartAction = + | { type: CartActionType.RESET_CART } | { type: CartActionType.INITIALIZE; payload: CartState } | { type: CartActionType.ADD_ITEM; payload: CartItem } | { type: CartActionType.UPDATE_QUANTITY; payload: CartItem } @@ -47,6 +48,9 @@ export const cartReducer = ( action: CartAction ): CartState => { switch (action.type) { + case CartActionType.RESET_CART: { + return initState; + } case CartActionType.INITIALIZE: { return { ...state, ...action.payload }; } diff --git a/apps/web/features/merch/context/checkout/index.tsx b/apps/web/features/merch/context/checkout/index.tsx new file mode 100644 index 00000000..9913b30b --- /dev/null +++ b/apps/web/features/merch/context/checkout/index.tsx @@ -0,0 +1,42 @@ +import React, { useContext, useMemo, useState } from "react"; +import { CheckoutResponse } from "types"; + +type ContextType = { + state: CheckoutResponse | null; + setState: React.Dispatch>; +} | null; + +const CheckoutContext = React.createContext(null); + +export const useCheckoutStore = () => { + const context = useContext(CheckoutContext); + if (context === null) { + throw new Error("useCheckoutStore must be used within a CheckoutProvider."); + } + return context; +}; + +interface CheckoutProviderProps { + children: React.ReactNode; +} + +export const CheckoutProvider: React.FC = ({ + children, +}) => { + const [checkoutState, setCheckoutState] = useState( + null + ); + + const value = useMemo( + () => ({ + state: checkoutState, + setState: setCheckoutState, + }), + [checkoutState] + ); + return ( + + {children} + + ); +}; diff --git a/apps/web/features/merch/services/api.ts b/apps/web/features/merch/services/api.ts index c4f239d8..1b0fd34e 100644 --- a/apps/web/features/merch/services/api.ts +++ b/apps/web/features/merch/services/api.ts @@ -1,12 +1,12 @@ import { - APIError, - Cart, - CheckoutRequest, - CheckoutResponse, - PricedCart, - Product, - ProductsResponse, - QuotationRequest + APIError, + Cart, + CheckoutRequest, + CheckoutResponse, + PricedCart, + Product, + ProductsResponse, + QuotationRequest, } from "types"; export class Api { @@ -74,15 +74,16 @@ export class Api { return res; } - async postCheckoutCart(cart: Cart, email: string, promoCode?: string): Promise { - return await this.post( - `/cart/checkout`, - { - ...cart, - promoCode: promoCode, - email, - } - ); + async postCheckoutCart( + cart: Cart, + email: string, + promoCode?: string + ): Promise { + return await this.post(`/checkout`, { + ...cart, + promoCode: promoCode, + email, + }); } } diff --git a/apps/web/lib/trpc.ts b/apps/web/lib/trpc.ts new file mode 100644 index 00000000..388b0c55 --- /dev/null +++ b/apps/web/lib/trpc.ts @@ -0,0 +1,39 @@ +import { httpBatchLink } from "@trpc/client"; +import { createTRPCNext } from "@trpc/next"; +import type { AppRouter } from "../../merch/src/trpc/router"; + +function getBaseUrl() { + if (process.env.NEXT_PUBLIC_MERCH_API_ORIGIN) + return process.env.NEXT_PUBLIC_MERCH_API_ORIGIN; + + // throw error if not set + throw new Error("NEXT_PUBLIC_MERCH_API_ORIGIN is not set"); +} + +export const trpc = createTRPCNext({ + config(_opts) { + return { + links: [ + httpBatchLink({ + /** + * If you want to use SSR, you need to use the server's full URL + * @link https://trpc.io/docs/ssr + **/ + url: `${getBaseUrl()}/trpc`, + + // You can pass any HTTP headers you wish here + // eslint-disable-next-line @typescript-eslint/require-await + async headers() { + return { + // authorization: getAuthCookie(), + }; + }, + }), + ], + }; + }, + /** + * @link https://trpc.io/docs/ssr + **/ + ssr: false, +}); diff --git a/apps/web/package.json b/apps/web/package.json index d9a35f04..88e77be9 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -20,8 +20,13 @@ "@chakra-ui/system": "^2.3.1", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", - "@tanstack/react-query": "^4.26.1", + "@stripe/react-stripe-js": "^2.1.1", + "@stripe/stripe-js": "^1.54.0", + "@tanstack/react-query": "^4.29.15", "@tanstack/react-query-devtools": "^4.26.1", + "@trpc/client": "^10.31.0", + "@trpc/next": "^10.31.0", + "@trpc/react-query": "^10.31.0", "framer-motion": "^7.6.4", "merch-helpers": "*", "next": "13.4.6", @@ -30,7 +35,8 @@ "react-dom": "18.2.0", "react-icons": "^4.8.0", "swiper": "^9.4.0", - "ui": "*" + "ui": "*", + "zod": "^3.21.4" }, "devDependencies": { "@testing-library/jest-dom": "^5.16.5", diff --git a/apps/web/pages/_app.tsx b/apps/web/pages/_app.tsx index 6571ef14..5af6470f 100644 --- a/apps/web/pages/_app.tsx +++ b/apps/web/pages/_app.tsx @@ -1,7 +1,7 @@ import "../styles/globals.css"; -import type { AppProps } from "next/app"; +import type { AppProps, AppType } from "next/app"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { ReactQueryDevtools } from "@tanstack/react-query-devtools" +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ChakraProvider } from "@chakra-ui/react"; import { theme } from "ui/theme"; import "@fontsource/work-sans/300.css"; @@ -12,17 +12,23 @@ import "ui/fonts/styles.css"; // for custom fonts not available on @fontsource import { WebLayout } from "@/features/layout"; import { CartProvider } from "@/features/merch/context/cart"; +import { CheckoutProvider } from "@/features/merch/context/checkout"; +import { trpc } from "@/lib/trpc"; -const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false } } }); +const queryClient = new QueryClient({ + defaultOptions: { queries: { refetchOnWindowFocus: false } }, +}); -const App = ({ Component, pageProps }: AppProps) => { +const App: AppType = ({ Component, pageProps }: AppProps) => { return ( - - - + + + + + @@ -30,4 +36,5 @@ const App = ({ Component, pageProps }: AppProps) => { ); }; -export default App; +// eslint-disable-next-line @typescript-eslint/no-unsafe-call +export default trpc.withTRPC(App); diff --git a/apps/web/pages/merch/cart/index.tsx b/apps/web/pages/merch/cart/index.tsx index eac4115a..e8354575 100644 --- a/apps/web/pages/merch/cart/index.tsx +++ b/apps/web/pages/merch/cart/index.tsx @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ + import React, { useRef, useState, FC, useEffect } from "react"; import Link from "next/link"; import { @@ -62,7 +64,7 @@ const Cart: FC = () => { { onSuccess: () => { setIsCartLoading(false); - } + }, } ); @@ -70,7 +72,9 @@ const Cart: FC = () => { // const [voucherInput, setVoucherInput] = useState(""); // const [voucherError, setVoucherError] = useState(false); - const pricedCart = products ? calculatePricing(products, cartState.cart, undefined) : null; + const pricedCart = products + ? calculatePricing(products, cartState.cart, undefined) + : null; const emailValidator = Joi.string() .email({ tlds: { allow: false } }) @@ -358,7 +362,7 @@ const Cart: FC = () => { useEffect(() => { if (reroute) { - router.push(routes.CHECKOUT); + void router.push(routes.CHECKOUT); } }, [reroute]); diff --git a/apps/web/pages/merch/checkout/index.tsx b/apps/web/pages/merch/checkout/index.tsx new file mode 100644 index 00000000..1ff4943e --- /dev/null +++ b/apps/web/pages/merch/checkout/index.tsx @@ -0,0 +1,188 @@ +import { CartEmptyView, Page } from "ui/components/merch"; +import { useCartStore } from "@/features/merch/context/cart"; +import { useEffect, useState } from "react"; +import { useCheckoutStore } from "@/features/merch/context/checkout"; +import { + Box, + Divider, + Flex, + Grid, + GridItem, + Heading, + Text, + Image, + Badge, +} from "@chakra-ui/react"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { QueryKeys, routes } from "@/features/merch/constants"; +import { api } from "@/features/merch/services/api"; +import CheckoutSkeleton from "@/features/merch/components/checkout/Skeleton"; +import Link from "next/link"; +import { displayPrice } from "@/features/merch/functions"; +import { useRouter } from "next/router"; +import StripeForm from "@/features/merch/components/checkout/StripeForm"; +import { CheckoutResponse } from "types"; + +const CheckoutPage = () => { + const [isLoading, setIsLoading] = useState(true); + const { state: cartState } = useCartStore(); + const { setState: setCheckoutState } = useCheckoutStore(); + + // Fetch and check if cart item is valid. + const { mutate: initCheckout } = useMutation( + () => + api.postCheckoutCart( + cartState.cart, + cartState.billingEmail, + cartState.voucher + ), + { + retry: false, + onMutate: () => { + setIsLoading(true); + }, + onSuccess: (data: CheckoutResponse) => { + setCheckoutState(data); + }, + onSettled: () => { + setIsLoading(false); + }, + } + ); + + const router = useRouter(); + + useEffect(() => { + if (!cartState.billingEmail) { + void router.push(routes.CART); + return; + } + initCheckout(); + }, []); + + return ( + + + Checkout + + {isLoading ? ( + + ) : cartState.cart.items.length === 0 ? ( + + ) : ( + + )} + + ); +}; + +const CheckoutView = () => { + const { state: checkoutState } = useCheckoutStore(); + return ( + + + {OrderSummary()} + + + {checkoutState?.payment?.clientSecret && ( + + )} + + + ); +}; + +const OrderSummary = () => { + const { state: cartState } = useCartStore(); + const { state: checkoutState } = useCheckoutStore(); + + const noOfItems = cartState.cart.items.length; + + const { data: products } = useQuery( + [QueryKeys.PRODUCTS], + () => api.getProducts(), + {} + ); + + return ( + + + Order Summary + + {`${noOfItems} item(s) Edit`} + + + {`Name: ${cartState.name}`} + {`Billing email: ${cartState.billingEmail}`} + {cartState.cart.items?.map((item) => { + const product = products?.find(({ id }) => id === item.id); + const subtotal = (product?.price ?? -1) * item.quantity; + return ( + + {product?.name} + + + + {product?.name} + + {displayPrice(subtotal)} + + + {`Color: ${item.color}`} + + + + {`Qty x${item.quantity}`} + + {item.size} + + + {displayPrice(product?.price ?? 0)} each + + + + ); + })} + + + + + {/* Subtotal: */} + {/* Discount: */} + Grand total: + + + {/* {displayPrice(checkoutState?.price?.subtotal ?? 0)} */} + {/* {displayPrice(checkoutState?.price?.discount ?? 0)} */} + + {displayPrice(checkoutState?.price?.grandTotal ?? 0)} + + + + + ); +}; + +export default CheckoutPage; diff --git a/apps/web/pages/merch/product/[slug].tsx b/apps/web/pages/merch/product/[slug].tsx index 987849de..3f04c029 100644 --- a/apps/web/pages/merch/product/[slug].tsx +++ b/apps/web/pages/merch/product/[slug].tsx @@ -1,19 +1,20 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ + import React, { useState } from "react"; import { useRouter } from "next/router"; import { - Flex, - Heading, - Text, - Divider, + Badge, Button, - Input, + Center, + Divider, + Flex, Grid, GridItem, - Badge, + Heading, + Input, + Text, useDisclosure, - Center, } from "@chakra-ui/react"; -import { useQuery } from "@tanstack/react-query"; import { EmptyProductView, MerchCarousel, @@ -28,21 +29,19 @@ import { CartActionType, useCartStore, } from "features/merch/context/cart"; -import { api } from "features/merch/services/api"; -import { routes, QueryKeys } from "features/merch/constants"; +import { routes } from "features/merch/constants"; import { displayPrice, displayQtyInCart, displayStock, - getDefaultColor, - getDefaultSize, getQtyInCart, getQtyInStock, isColorAvailable, isOutOfStock, isSizeAvailable, } from "features/merch/functions"; - +import { trpc } from "@/lib/trpc"; +import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next"; const GroupTitle = ({ children }: any) => ( @@ -50,7 +49,7 @@ const GroupTitle = ({ children }: any) => ( ); -const MerchDetail: React.FC = () => { +const MerchDetail = (props: InferGetStaticPropsType) => { // Context hook. const { state: cartState, dispatch: cartDispatch } = useCartStore(); const router = useRouter(); @@ -64,18 +63,21 @@ const MerchDetail: React.FC = () => { const { isOpen, onOpen, onClose } = useDisclosure(); - const { data: product, isLoading } = useQuery( - [QueryKeys.PRODUCT, id], - () => api.getProduct(id), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + const { data, isLoading } = trpc.getProduct.useQuery( { - onSuccess: (data: Product) => { - setIsDisabled(!(data?.is_available === true)); - setSelectedSize(getDefaultSize(data)); - setSelectedColor(getDefaultColor(data)); - }, + id, + }, + { + staleTime: Infinity, + refetchOnMount: false, + refetchOnWindowFocus: false, + initialData: props.product, // ssr magic } ); + const product = data as Product; + //* In/decrement quantity const handleQtyChangeCounter = (isAdd = true) => { const value = isAdd ? 1 : -1; @@ -135,9 +137,9 @@ const MerchDetail: React.FC = () => { setIsDisabled(false); }; - const handleBuyNow = () => { + const handleBuyNow = async () => { handleAddToCart(); - router.push(routes.CART); + await router.push(routes.CART); }; const ProductNameSection = ( @@ -320,10 +322,7 @@ const MerchDetail: React.FC = () => {
- {product && - selectedColor && - selectedSize && - product.is_available === true + {product && selectedColor && selectedSize && product.is_available ? displayStock(product, selectedColor, selectedSize) : ""} @@ -415,3 +414,59 @@ const MerchDetail: React.FC = () => { }; export default MerchDetail; + +export const getStaticProps: GetStaticProps<{ + slug: string; + product: Product | undefined; +}> = async ({ params }) => { + console.log("generating static props for /merch/product/[slug]"); + console.log("params", JSON.stringify(params)); + + // TODO: replace this with trpc/react-query call + if (!process.env.NEXT_PUBLIC_MERCH_API_ORIGIN) { + throw new Error("NEXT_PUBLIC_MERCH_API_ORIGIN is not defined"); + } + const res = await fetch( + `${ + process.env.NEXT_PUBLIC_MERCH_API_ORIGIN + }/trpc/getProduct?batch=1&input=${encodeURIComponent( + JSON.stringify({ "0": { id: params?.slug } }) + )}` + ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const product = (await res.json())[0].result.data as Product; + + return { + props: { + slug: params?.slug as string, + product: product, + }, + }; +}; + +// eslint-disable-next-line @typescript-eslint/require-await +export const getStaticPaths: GetStaticPaths = async () => { + console.log("generating static paths for /merch/product/[slug]"); + + // TODO: replace this with trpc/react-query call + if (!process.env.NEXT_PUBLIC_MERCH_API_ORIGIN) { + throw new Error("NEXT_PUBLIC_MERCH_API_ORIGIN is not defined"); + } + const res = await fetch( + `${process.env.NEXT_PUBLIC_MERCH_API_ORIGIN}/trpc/getProducts` + ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const products = (await res.json()).result.data.products as Product[]; + + return { + paths: products.map((product) => ({ + params: { + slug: product.id, + }, + })), + // https://nextjs.org/docs/pages/api-reference/functions/get-static-paths#fallback-blocking + fallback: "blocking", + }; +}; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 68cb0f4b..1e29842c 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -16,5 +16,10 @@ "@/features/*": ["features/*"], "@/lib/*": ["lib/*"] } - } + }, + "references": [ + { + "path": "../merch" + } + ] } diff --git a/packages/nodelogger/package.json b/packages/nodelogger/package.json index d5df223b..97af2cbd 100644 --- a/packages/nodelogger/package.json +++ b/packages/nodelogger/package.json @@ -17,8 +17,6 @@ "lint:fix": "TIMING=1 eslint --fix \"**/*.ts*\"" }, "dependencies": { - "@types/morgan": "^1.9.4", - "@types/winston": "^2.4.4", "morgan": "^1.10.0", "winston": "^3.8.2" }, @@ -26,6 +24,8 @@ "eslint": "^7.32.0", "eslint-config-custom": "*", "tsconfig": "*", - "typescript": "^4.5.2" + "typescript": "^4.5.2", + "@types/morgan": "^1.9.4", + "@types/winston": "^2.4.4" } } diff --git a/packages/nodelogger/src/index.ts b/packages/nodelogger/src/index.ts index e2a94e04..d34ecff7 100644 --- a/packages/nodelogger/src/index.ts +++ b/packages/nodelogger/src/index.ts @@ -1,4 +1,4 @@ import nodeloggerMiddleware from "./lib/morganMiddleware"; -import { Logger } from "./lib/winstonLogger" +import { Logger } from "./lib/winstonLogger"; export { Logger, nodeloggerMiddleware }; diff --git a/packages/nodelogger/src/lib/morganMiddleware.ts b/packages/nodelogger/src/lib/morganMiddleware.ts index f8990c2e..c8fe0845 100644 --- a/packages/nodelogger/src/lib/morganMiddleware.ts +++ b/packages/nodelogger/src/lib/morganMiddleware.ts @@ -1,15 +1,7 @@ import morgan, { StreamOptions } from "morgan"; -import { IncomingMessage } from "http"; - import { Logger } from "./winstonLogger"; -interface Request extends IncomingMessage { - body: { - query: string; - }; -} - const stream: StreamOptions = { write: (message) => Logger.http(message.substring(0, message.lastIndexOf("\n"))), @@ -20,14 +12,8 @@ const skip = () => { return env !== "development"; }; -const registerGraphQLToken = () => { - morgan.token("graphql-query", (req: Request) => `GraphQL ${req.body.query}`); -}; - -registerGraphQLToken(); - const morganMiddleware = morgan( - ":method :url :status :res[content-length] - :response-time ms\n:graphql-query", + ":method :url :status :res[content-length] - :response-time ms", { stream, skip } ); diff --git a/packages/nodelogger/tsconfig.json b/packages/nodelogger/tsconfig.json index b5340d5e..22ddefcc 100644 --- a/packages/nodelogger/tsconfig.json +++ b/packages/nodelogger/tsconfig.json @@ -29,6 +29,9 @@ "include": [ "src/**/*.ts" ], + "compilerOptions": { + "outDir": "dist", + }, "exclude": [ "dist", "build", diff --git a/packages/types/lib/merch.ts b/packages/types/lib/merch.ts index 7675ce7b..a10f00b9 100644 --- a/packages/types/lib/merch.ts +++ b/packages/types/lib/merch.ts @@ -38,7 +38,7 @@ export interface Order { // Cart export type CartState = { cart: Cart; - voucher: string | null; + voucher: string | undefined; name: string; billingEmail: string; }; @@ -95,7 +95,7 @@ export type PricedCart = { originalPrice: number; discountedPrice: number; }[]; -} +}; export enum PromoType { PERCENTAGE = "PERCENTAGE", @@ -111,7 +111,7 @@ export type OrderHold = { transaction_id: string; expiry: string; reserved_products: ReservedProduct[]; -} +}; export type ProductInfo = { name: string; @@ -129,30 +129,42 @@ export type CartPrice = { }; // API Types -export const QuotationRequest = Cart.merge(z.object({ - promoCode: z.string().optional(), -})); - -export const CheckoutRequest = QuotationRequest.merge(z.object({ - email: z.string(), -})); +export const QuotationRequest = Cart.merge( + z.object({ + promoCode: z.string().optional(), + }) +); + +export const CheckoutRequest = QuotationRequest.merge( + z.object({ + email: z.string(), + }) +); export type QuotationRequest = z.infer; export type CheckoutRequest = z.infer; export type CheckoutResponse = Order & { - expiry: string, + expiry: string; + price: { + grandTotal: number; + // todo: add rest of price object + }; payment: { - method: "stripe", - client_secret: string, + method: "stripe"; + clientSecret: string; }; }; export type ProductsResponse = { - products: Product[], + products: Product[]; }; export type APIError = { - error: string, - detail?: string|object, -} + error: string; + detail?: string | object; +}; + +export type OrderHoldEntry = { + // todo: ??? +}; diff --git a/turbo.json b/turbo.json index a0e31000..1ce18c17 100644 --- a/turbo.json +++ b/turbo.json @@ -21,7 +21,8 @@ "STRIPE_SECRET_KEY", "ORDER_EXPIRY_TIME", "NEXT_PUBLIC_MERCH_API_ORIGIN", - "NEXT_PUBLIC_FRONTEND_URL" + "NEXT_PUBLIC_FRONTEND_URL", + "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" ] }, "build": { @@ -44,7 +45,8 @@ "STRIPE_SECRET_KEY", "ORDER_HOLD_TABLE_NAME", "ORDER_EXPIRY_TIME", - "NEXT_PUBLIC_MERCH_API_ORIGIN" + "NEXT_PUBLIC_MERCH_API_ORIGIN", + "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" ], "outputs": ["dist/**", "build/**", "out/**", ".next/**"] }, @@ -71,7 +73,8 @@ "ORDER_HOLD_TABLE_NAME", "STRIPE_SECRET_KEY", "ORDER_EXPIRY_TIME", - "NEXT_PUBLIC_MERCH_API_ORIGIN" + "NEXT_PUBLIC_MERCH_API_ORIGIN", + "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" ] }, "serve": { @@ -93,7 +96,8 @@ "ORDER_HOLD_TABLE_NAME", "STRIPE_SECRET_KEY", "ORDER_EXPIRY_TIME", - "NEXT_PUBLIC_MERCH_API_ORIGIN" + "NEXT_PUBLIC_MERCH_API_ORIGIN", + "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" ] }, "lint": { diff --git a/yarn.lock b/yarn.lock index 0d59367e..2fcb7010 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6076,6 +6076,18 @@ "@types/express" "^4.7.0" file-system-cache "^2.0.0" +"@stripe/react-stripe-js@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@stripe/react-stripe-js/-/react-stripe-js-2.1.1.tgz#92fe1498bc9db4e0b51d63df3849214cc8021c55" + integrity sha512-aHm6cWOqmOyq3eKKlR42hgvrcZhzc9ijL+BeXJ5H3uaWUKKcooSsBys0tiKl0gLxYQHUjUCvrRSX8fYaPBmAKg== + dependencies: + prop-types "^15.7.2" + +"@stripe/stripe-js@^1.54.0": + version "1.54.0" + resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.54.0.tgz#f92f6b646533776a41bc62b650d2a03f1e282af8" + integrity sha512-nElTXkS+nMfDNMkWfLmyeqHQfMGJ1JjrjAVMibV61Oc/rYdUv0cKRYCi1l4ivZ5SySB3vQLcLolxbKBkbNznZA== + "@swc/core-darwin-arm64@1.3.37": version "1.3.37" resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.37.tgz#a92e075ae35f18a64aaf3823ea175f03564f8da1" @@ -6238,10 +6250,10 @@ dependencies: remove-accents "0.4.2" -"@tanstack/query-core@4.26.1": - version "4.26.1" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.26.1.tgz#7a441086c4d3d79e1d156c0a355bd3567213626e" - integrity sha512-Zrx2pVQUP4ndnsu6+K/m8zerXSVY8QM+YSbxA1/jbBY21GeCd5oKfYl92oXPK0hPEUtoNuunIdiq0ZMqLos+Zg== +"@tanstack/query-core@4.29.15": + version "4.29.15" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.29.15.tgz#6f8721341dbece517326a8e402d29ea3365538a8" + integrity sha512-Recc1d5rjHesKhzlH3Aw66v+vQxtB9OHEXP/vxgEcEJ0DwEpfe3EQ4id20vuBJHY2XRjfgWGmUs6ZgK6PSsTXA== "@tanstack/react-query-devtools@^4.26.1": version "4.26.1" @@ -6252,12 +6264,12 @@ superjson "^1.10.0" use-sync-external-store "^1.2.0" -"@tanstack/react-query@^4.26.1": - version "4.26.1" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.26.1.tgz#d254f6b7b297b5ae4204c84e6622506e5ec77d09" - integrity sha512-i3dnz4TOARGIXrXQ5P7S25Zfi4noii/bxhcwPurh2nrf5EUCcAt/95TB2HSmMweUBx206yIMWUMEQ7ptd6zwDg== +"@tanstack/react-query@^4.29.15": + version "4.29.15" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.29.15.tgz#83598e46964185199c59757e6b9c63c15eff81c1" + integrity sha512-1zDkv95ljuJ623hhbYU8YIprPW2x6774kh3IQNEuZav62+S+Zr26uUOrE2zGRp9I1uO5Liw/0uYB3dWXQP5+3Q== dependencies: - "@tanstack/query-core" "4.26.1" + "@tanstack/query-core" "4.29.15" use-sync-external-store "^1.2.0" "@testing-library/dom@^8.3.0", "@testing-library/dom@^8.5.0": @@ -6315,6 +6327,28 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== +"@trpc/client@^10.31.0": + version "10.31.0" + resolved "https://registry.yarnpkg.com/@trpc/client/-/client-10.31.0.tgz#64f6198741a0b96f21671e0c3c0e2e211bae3cc9" + integrity sha512-VCqbJEvFJb8C4hQFw7AD+dkQTjgEdV/QAzO4D+/cX5e93u5NpfNXI+PKS0QFXwG/zqgwQwVV6OkYc/D/MFwA6g== + +"@trpc/next@^10.31.0": + version "10.31.0" + resolved "https://registry.yarnpkg.com/@trpc/next/-/next-10.31.0.tgz#5d599b027db847e21ed9b50da55c853240cdcb4c" + integrity sha512-BZtZr7UKAs0tUTreCsYhy+/HjFNFl5KBwBS+Li6pCv9GwqCDqpoivesQz7LltO4Y4lOLXLm9tXQXtS1gfmF9yg== + dependencies: + react-ssr-prepass "^1.5.0" + +"@trpc/react-query@^10.31.0": + version "10.31.0" + resolved "https://registry.yarnpkg.com/@trpc/react-query/-/react-query-10.31.0.tgz#d12042b73138eafafd9efffc6b215a2712595731" + integrity sha512-+M8sIsbf6e4H5XYvHlzDqhaf+ybfUigA/9OL3wXRp2vXhCedEiIERCnwNuHWFDRASl9vjOcM33AuJ4sbOOINEA== + +"@trpc/server@^10.31.0": + version "10.31.0" + resolved "https://registry.yarnpkg.com/@trpc/server/-/server-10.31.0.tgz#2a757e814d9a779d6faa8b4fd6bda80691179a19" + integrity sha512-9EnRTSDE9nF11LZsvSOqNKqkRYzHqFX4ch5AJ6VIu8uta2vxVTN4FxxsNRSOluTzVYZDeaCISbwmOJ5iihCCIg== + "@trysound/sax@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" @@ -6467,7 +6501,7 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@*", "@types/express@^4.7.0": +"@types/express@*", "@types/express@^4.17.17", "@types/express@^4.7.0": version "4.17.17" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== @@ -7199,6 +7233,11 @@ acorn@^8.8.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== +add@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/add/-/add-2.0.6.tgz#248f0a9f6e5a528ef2295dbeec30532130ae2235" + integrity sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q== + address@^1.0.1: version "1.2.2" resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" @@ -7409,6 +7448,14 @@ aria-query@^5.0.0: dependencies: deep-equal "^2.0.5" +array-buffer-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" + integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== + dependencies: + call-bind "^1.0.2" + is-array-buffer "^3.0.1" + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -7450,6 +7497,17 @@ array.prototype.flatmap@^1.3.0, array.prototype.flatmap@^1.3.1: es-abstract "^1.20.4" es-shim-unscopables "^1.0.0" +array.prototype.reduce@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz#6b20b0daa9d9734dd6bc7ea66b5bbce395471eac" + integrity sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-array-method-boxes-properly "^1.0.0" + is-string "^1.0.7" + array.prototype.tosorted@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz#ccf44738aa2b5ac56578ffda97c03fd3e23dd532" @@ -9278,6 +9336,14 @@ define-lazy-prop@^2.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== +define-properties@^1.1.2, define-properties@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" + integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + define-properties@^1.1.3, define-properties@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" @@ -9540,6 +9606,11 @@ dotenv@^16.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.1.1.tgz#9105a6c486ab66076231fbe7cf4ba77131a61d65" integrity sha512-UGmzIqXU/4b6Vb3R1Vrfd/4vGgVlB+mO+vEixOdfRhLeppkyW2BMhuK7TL8d0el+q9c4lW9qK2wZYhNLFhXYLA== +dotenv@^16.3.1: + version "16.3.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" + integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== + dotenv@^8.2.0, dotenv@^8.6.0: version "8.6.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" @@ -9749,6 +9820,51 @@ es-abstract@^1.19.0, es-abstract@^1.20.4: string.prototype.trimstart "^1.0.6" unbox-primitive "^1.0.2" +es-abstract@^1.21.2: + version "1.21.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.2.tgz#a56b9695322c8a185dc25975aa3b8ec31d0e7eff" + integrity sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg== + dependencies: + array-buffer-byte-length "^1.0.0" + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.2.0" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + is-array-buffer "^3.0.2" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.10" + is-weakref "^1.0.2" + object-inspect "^1.12.3" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + safe-regex-test "^1.0.0" + string.prototype.trim "^1.2.7" + string.prototype.trimend "^1.0.6" + string.prototype.trimstart "^1.0.6" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.9" + +es-array-method-boxes-properly@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" + integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== + es-get-iterator@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7" @@ -9768,6 +9884,15 @@ es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.2.1.tgz#ba303831f63e6a394983fde2f97ad77b22324527" integrity sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg== +es-set-tostringtag@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" + integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + has-tostringtag "^1.0.0" + es-shim-unscopables@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" @@ -10881,6 +11006,11 @@ functions-have-names@^1.2.2: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +fuzzysort@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/fuzzysort/-/fuzzysort-2.0.4.tgz#a21d1ce8947eaf2797dc3b7c28c36db9d1165f84" + integrity sha512-Api1mJL+Ad7W7vnDZnWq5pGaXJjyencT+iKGia2PlHUcSsSzWwIQ3S1isiMpwpavjYtGd2FzhUIhnnhOULZgDw== + gauge@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" @@ -10915,6 +11045,16 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@ has "^1.0.3" has-symbols "^1.0.3" +get-intrinsic@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" + integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-proto "^1.0.1" + has-symbols "^1.0.3" + get-nonce@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" @@ -11105,6 +11245,13 @@ globals@^13.6.0, globals@^13.9.0: dependencies: type-fest "^0.20.2" +globalthis@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" + integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + dependencies: + define-properties "^1.1.3" + globby@^11.0.1, globby@^11.0.2, globby@^11.0.3, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" @@ -11225,6 +11372,11 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" @@ -11559,6 +11711,11 @@ inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, i resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + ini@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" @@ -11578,6 +11735,15 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +internal-slot@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" + integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== + dependencies: + get-intrinsic "^1.2.0" + has "^1.0.3" + side-channel "^1.0.4" + interpret@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -11610,7 +11776,7 @@ is-absolute-url@^3.0.0: resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== -is-arguments@^1.0.4, is-arguments@^1.1.0, is-arguments@^1.1.1: +is-arguments@^1.1.0, is-arguments@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== @@ -11627,6 +11793,15 @@ is-array-buffer@^3.0.1: get-intrinsic "^1.1.3" is-typed-array "^1.1.10" +is-array-buffer@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" + integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + is-typed-array "^1.1.10" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -11722,13 +11897,6 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-generator-function@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" - integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== - dependencies: - has-tostringtag "^1.0.0" - is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -11870,7 +12038,7 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" -is-typed-array@^1.1.10, is-typed-array@^1.1.3: +is-typed-array@^1.1.10, is-typed-array@^1.1.9: version "1.1.10" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== @@ -13637,6 +13805,11 @@ object-inspect@^1.12.2, object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== +object-inspect@^1.12.3: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + object-is@^1.0.1, object-is@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" @@ -13683,6 +13856,17 @@ object.fromentries@^2.0.5, object.fromentries@^2.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" +object.getownpropertydescriptors@^2.0.3: + version "2.1.6" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.6.tgz#5e5c384dd209fa4efffead39e3a0512770ccc312" + integrity sha512-lq+61g26E/BgHv0ZTFgRvi7NMEPuAxLkFU7rukXjc/AlwH4Am5xXVnIXy3un1bg/JPbXHrixRkK1itUzzPiIjQ== + dependencies: + array.prototype.reduce "^1.0.5" + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.21.2" + safe-array-concat "^1.0.0" + object.hasown@^1.1.1, object.hasown@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.2.tgz#f919e21fad4eb38a57bc6345b3afd496515c3f92" @@ -14030,6 +14214,14 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +path@^0.12.7: + version "0.12.7" + resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" + integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q== + dependencies: + process "^0.11.1" + util "^0.10.3" + pathe@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.0.tgz#e2e13f6c62b31a3289af4ba19886c230f295ec03" @@ -14980,7 +15172,7 @@ process-warning@^1.0.0: resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-1.0.0.tgz#980a0b25dc38cd6034181be4b7726d89066b4616" integrity sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q== -process@^0.11.10: +process@^0.11.1, process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== @@ -15483,6 +15675,11 @@ react-side-effect@^2.1.0: resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.2.tgz#dc6345b9e8f9906dc2eeb68700b615e0b4fe752a" integrity sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw== +react-ssr-prepass@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz#bc4ca7fcb52365e6aea11cc254a3d1bdcbd030c5" + integrity sha512-yFNHrlVEReVYKsLI5lF05tZoHveA5pGzjFbFJY/3pOqqjGOmMmqx83N4hIjN2n6E1AOa+eQEUxs3CgRnPmT0RQ== + react-style-singleton@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" @@ -15923,6 +16120,16 @@ rxjs@^7.5.1, rxjs@^7.5.4: dependencies: tslib "^2.1.0" +safe-array-concat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.0.tgz#2064223cba3c08d2ee05148eedbc563cd6d84060" + integrity sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + has-symbols "^1.0.3" + isarray "^2.0.5" + safe-buffer@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -16611,6 +16818,15 @@ string.prototype.matchall@^4.0.7, string.prototype.matchall@^4.0.8: regexp.prototype.flags "^1.4.3" side-channel "^1.0.4" +string.prototype.trim@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533" + integrity sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + string.prototype.trimend@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" @@ -17147,6 +17363,16 @@ triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== +trpc-panel@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/trpc-panel/-/trpc-panel-1.3.4.tgz#70276d0d24b6561b9e34158e83a5f8e59146bc89" + integrity sha512-u5/dCi/AAp2tpJcCL5ZCfrdJtHHu8hrtm2hzSBZCE7z9Tw6MB1rCcliSQvgMPIEXMQrgwXk4t4IedfWkxioKng== + dependencies: + fuzzysort "^2.0.4" + path "^0.12.7" + url "^0.11.0" + zod-to-json-schema "^3.20.0" + truncate-utf8-bytes@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" @@ -17392,6 +17618,15 @@ type@^2.7.2: resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== +typed-array-length@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" + integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + is-typed-array "^1.1.9" + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -17619,6 +17854,13 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +util@^0.10.3: + version "0.10.4" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" + integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== + dependencies: + inherits "2.0.3" + util@^0.12.0, util@^0.12.4: version "0.12.5" resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" @@ -17986,7 +18228,7 @@ which-collection@^1.0.1: is-weakmap "^2.0.1" is-weakset "^2.0.1" -which-typed-array@^1.1.2, which-typed-array@^1.1.8, which-typed-array@^1.1.9: +which-typed-array@^1.1.8, which-typed-array@^1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== @@ -18201,6 +18443,11 @@ yargs@^17.3.1: y18n "^5.0.5" yargs-parser "^21.1.1" +yarn@^1.22.19: + version "1.22.19" + resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.19.tgz#4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447" + integrity sha512-/0V5q0WbslqnwP91tirOvldvYISzaqhClxzyUKXYxs07yUILIs5jx/k6CFe8bvKSkds5w+eiOqta39Wk3WxdcQ== + yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" @@ -18219,7 +18466,12 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -zod@3.21.4: +zod-to-json-schema@^3.20.0: + version "3.21.2" + resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.21.2.tgz#74dbbf97863c8b3f0576beb44e0af52382596fe4" + integrity sha512-02yfKymfmIf2rM/5LYGlyw0daEel/f3MsSGMNJZWWf44ato+Y+diFugOpDtgvEUn3cYM5oDAGWW2NHeSD4mByw== + +zod@3.21.4, zod@^3.21.4: version "3.21.4" resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== From 288e6e6a34a1431f3d22e774bde0d316d4533dee Mon Sep 17 00:00:00 2001 From: Chan Wen Xu Date: Wed, 28 Jun 2023 23:46:19 +0800 Subject: [PATCH 25/60] fix: Enable product selection by default --- apps/web/pages/merch/product/[slug].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/pages/merch/product/[slug].tsx b/apps/web/pages/merch/product/[slug].tsx index 3f04c029..6758b92b 100644 --- a/apps/web/pages/merch/product/[slug].tsx +++ b/apps/web/pages/merch/product/[slug].tsx @@ -56,7 +56,7 @@ const MerchDetail = (props: InferGetStaticPropsType) => { const id = (router.query.slug ?? "") as string; const [quantity, setQuantity] = useState(1); - const [isDisabled, setIsDisabled] = useState(true); + const [isDisabled, setIsDisabled] = useState(false); const [selectedSize, setSelectedSize] = useState(null); const [selectedColor, setSelectedColor] = useState(null); const [maxQuantity, setMaxQuantity] = useState(1); From 2926dc5ae0904715ef5e31924af97c55e0bb3c80 Mon Sep 17 00:00:00 2001 From: Chan Wen Xu Date: Thu, 29 Jun 2023 00:29:45 +0800 Subject: [PATCH 26/60] chore: Fix yarn.lock --- packages/types/lib/cms.ts | 18 ++-- yarn.lock | 178 +++----------------------------------- 2 files changed, 20 insertions(+), 176 deletions(-) diff --git a/packages/types/lib/cms.ts b/packages/types/lib/cms.ts index 48a847d2..4e2f6a32 100644 --- a/packages/types/lib/cms.ts +++ b/packages/types/lib/cms.ts @@ -24,9 +24,9 @@ export interface Post { title?: string; category?: string | Category; tags?: string[] | Tag[]; - layout: ( + layout?: ( | { - columns: { + columns?: { width: 'oneThird' | 'half' | 'twoThirds' | 'full'; alignment: 'left' | 'center' | 'right'; richText?: { @@ -53,9 +53,9 @@ export interface Post { status?: 'draft' | 'published'; author?: string | User; publishedDate?: string; - _status?: 'draft' | 'published'; - createdAt: string; updatedAt: string; + createdAt: string; + _status?: 'draft' | 'published'; } export interface Tag { id: string; @@ -64,24 +64,26 @@ export interface Tag { export interface Media { id: string; alt?: string; + updatedAt: string; + createdAt: string; url?: string; filename?: string; mimeType?: string; filesize?: number; width?: number; height?: number; - createdAt: string; - updatedAt: string; } export interface User { id: string; name?: string; + updatedAt: string; + createdAt: string; email?: string; resetPasswordToken?: string; resetPasswordExpiration?: string; + salt?: string; + hash?: string; loginAttempts?: number; lockUntil?: string; - createdAt: string; - updatedAt: string; password?: string; } diff --git a/yarn.lock b/yarn.lock index 2fcb7010..8c302a56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7448,14 +7448,6 @@ aria-query@^5.0.0: dependencies: deep-equal "^2.0.5" -array-buffer-byte-length@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" - integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== - dependencies: - call-bind "^1.0.2" - is-array-buffer "^3.0.1" - array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -7497,17 +7489,6 @@ array.prototype.flatmap@^1.3.0, array.prototype.flatmap@^1.3.1: es-abstract "^1.20.4" es-shim-unscopables "^1.0.0" -array.prototype.reduce@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz#6b20b0daa9d9734dd6bc7ea66b5bbce395471eac" - integrity sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - es-array-method-boxes-properly "^1.0.0" - is-string "^1.0.7" - array.prototype.tosorted@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz#ccf44738aa2b5ac56578ffda97c03fd3e23dd532" @@ -9336,14 +9317,6 @@ define-lazy-prop@^2.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== -define-properties@^1.1.2, define-properties@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" - integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== - dependencies: - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - define-properties@^1.1.3, define-properties@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" @@ -9820,51 +9793,6 @@ es-abstract@^1.19.0, es-abstract@^1.20.4: string.prototype.trimstart "^1.0.6" unbox-primitive "^1.0.2" -es-abstract@^1.21.2: - version "1.21.2" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.2.tgz#a56b9695322c8a185dc25975aa3b8ec31d0e7eff" - integrity sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg== - dependencies: - array-buffer-byte-length "^1.0.0" - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - es-set-tostringtag "^2.0.1" - es-to-primitive "^1.2.1" - function.prototype.name "^1.1.5" - get-intrinsic "^1.2.0" - get-symbol-description "^1.0.0" - globalthis "^1.0.3" - gopd "^1.0.1" - has "^1.0.3" - has-property-descriptors "^1.0.0" - has-proto "^1.0.1" - has-symbols "^1.0.3" - internal-slot "^1.0.5" - is-array-buffer "^3.0.2" - is-callable "^1.2.7" - is-negative-zero "^2.0.2" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - is-string "^1.0.7" - is-typed-array "^1.1.10" - is-weakref "^1.0.2" - object-inspect "^1.12.3" - object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.4.3" - safe-regex-test "^1.0.0" - string.prototype.trim "^1.2.7" - string.prototype.trimend "^1.0.6" - string.prototype.trimstart "^1.0.6" - typed-array-length "^1.0.4" - unbox-primitive "^1.0.2" - which-typed-array "^1.1.9" - -es-array-method-boxes-properly@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" - integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== - es-get-iterator@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7" @@ -9884,15 +9812,6 @@ es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.2.1.tgz#ba303831f63e6a394983fde2f97ad77b22324527" integrity sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg== -es-set-tostringtag@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" - integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== - dependencies: - get-intrinsic "^1.1.3" - has "^1.0.3" - has-tostringtag "^1.0.0" - es-shim-unscopables@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" @@ -11045,16 +10964,6 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@ has "^1.0.3" has-symbols "^1.0.3" -get-intrinsic@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" - integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-proto "^1.0.1" - has-symbols "^1.0.3" - get-nonce@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" @@ -11245,13 +11154,6 @@ globals@^13.6.0, globals@^13.9.0: dependencies: type-fest "^0.20.2" -globalthis@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" - integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== - dependencies: - define-properties "^1.1.3" - globby@^11.0.1, globby@^11.0.2, globby@^11.0.3, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" @@ -11372,11 +11274,6 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" -has-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" - integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== - has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" @@ -11735,15 +11632,6 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" -internal-slot@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" - integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== - dependencies: - get-intrinsic "^1.2.0" - has "^1.0.3" - side-channel "^1.0.4" - interpret@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -11776,7 +11664,7 @@ is-absolute-url@^3.0.0: resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== -is-arguments@^1.1.0, is-arguments@^1.1.1: +is-arguments@^1.0.4, is-arguments@^1.1.0, is-arguments@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== @@ -11793,15 +11681,6 @@ is-array-buffer@^3.0.1: get-intrinsic "^1.1.3" is-typed-array "^1.1.10" -is-array-buffer@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" - integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.0" - is-typed-array "^1.1.10" - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -11897,6 +11776,13 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== +is-generator-function@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -12038,7 +11924,7 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" -is-typed-array@^1.1.10, is-typed-array@^1.1.9: +is-typed-array@^1.1.10, is-typed-array@^1.1.3: version "1.1.10" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== @@ -13805,11 +13691,6 @@ object-inspect@^1.12.2, object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== -object-inspect@^1.12.3: - version "1.12.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== - object-is@^1.0.1, object-is@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" @@ -13856,17 +13737,6 @@ object.fromentries@^2.0.5, object.fromentries@^2.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" -object.getownpropertydescriptors@^2.0.3: - version "2.1.6" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.6.tgz#5e5c384dd209fa4efffead39e3a0512770ccc312" - integrity sha512-lq+61g26E/BgHv0ZTFgRvi7NMEPuAxLkFU7rukXjc/AlwH4Am5xXVnIXy3un1bg/JPbXHrixRkK1itUzzPiIjQ== - dependencies: - array.prototype.reduce "^1.0.5" - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.21.2" - safe-array-concat "^1.0.0" - object.hasown@^1.1.1, object.hasown@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.2.tgz#f919e21fad4eb38a57bc6345b3afd496515c3f92" @@ -16120,16 +15990,6 @@ rxjs@^7.5.1, rxjs@^7.5.4: dependencies: tslib "^2.1.0" -safe-array-concat@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.0.tgz#2064223cba3c08d2ee05148eedbc563cd6d84060" - integrity sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.0" - has-symbols "^1.0.3" - isarray "^2.0.5" - safe-buffer@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -16818,15 +16678,6 @@ string.prototype.matchall@^4.0.7, string.prototype.matchall@^4.0.8: regexp.prototype.flags "^1.4.3" side-channel "^1.0.4" -string.prototype.trim@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533" - integrity sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - string.prototype.trimend@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" @@ -17618,15 +17469,6 @@ type@^2.7.2: resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== -typed-array-length@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" - integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== - dependencies: - call-bind "^1.0.2" - for-each "^0.3.3" - is-typed-array "^1.1.9" - typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -18228,7 +18070,7 @@ which-collection@^1.0.1: is-weakmap "^2.0.1" is-weakset "^2.0.1" -which-typed-array@^1.1.8, which-typed-array@^1.1.9: +which-typed-array@^1.1.2, which-typed-array@^1.1.8, which-typed-array@^1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== From 970951b54c94d0f2e8eb8b630e9299601bbdaf04 Mon Sep 17 00:00:00 2001 From: dyllon Date: Fri, 7 Jul 2023 18:21:42 +0800 Subject: [PATCH 27/60] fix web build --- apps/cms/package.json | 1 + apps/web/lib/trpc.ts | 39 ------------------------- apps/web/next.config.js | 1 - apps/web/pages/_app.tsx | 3 +- apps/web/pages/merch/product/[slug].tsx | 34 +++++++++++---------- apps/web/tsconfig.json | 7 +---- turbo.json | 5 ++++ 7 files changed, 26 insertions(+), 64 deletions(-) delete mode 100644 apps/web/lib/trpc.ts diff --git a/apps/cms/package.json b/apps/cms/package.json index 6bfcf754..2acfd1d1 100644 --- a/apps/cms/package.json +++ b/apps/cms/package.json @@ -9,6 +9,7 @@ "build:payload": "yarn generate:types && PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build", "build:server": "tsc", "build": "yarn copyfiles && yarn build:payload && yarn build:server", + "clean": "rm -rf dist && rm -rf build", "serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js BABEL_DISABLE_CACHE=1 NODE_ENV=production node dist/server.js", "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/", "generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types", diff --git a/apps/web/lib/trpc.ts b/apps/web/lib/trpc.ts deleted file mode 100644 index 388b0c55..00000000 --- a/apps/web/lib/trpc.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { httpBatchLink } from "@trpc/client"; -import { createTRPCNext } from "@trpc/next"; -import type { AppRouter } from "../../merch/src/trpc/router"; - -function getBaseUrl() { - if (process.env.NEXT_PUBLIC_MERCH_API_ORIGIN) - return process.env.NEXT_PUBLIC_MERCH_API_ORIGIN; - - // throw error if not set - throw new Error("NEXT_PUBLIC_MERCH_API_ORIGIN is not set"); -} - -export const trpc = createTRPCNext({ - config(_opts) { - return { - links: [ - httpBatchLink({ - /** - * If you want to use SSR, you need to use the server's full URL - * @link https://trpc.io/docs/ssr - **/ - url: `${getBaseUrl()}/trpc`, - - // You can pass any HTTP headers you wish here - // eslint-disable-next-line @typescript-eslint/require-await - async headers() { - return { - // authorization: getAuthCookie(), - }; - }, - }), - ], - }; - }, - /** - * @link https://trpc.io/docs/ssr - **/ - ssr: false, -}); diff --git a/apps/web/next.config.js b/apps/web/next.config.js index e1e40879..d9111bb6 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -27,7 +27,6 @@ const nextConfig = { } ], }, - transpilePackages: ["ui"], }; module.exports = nextConfig; diff --git a/apps/web/pages/_app.tsx b/apps/web/pages/_app.tsx index 5af6470f..b26cacb7 100644 --- a/apps/web/pages/_app.tsx +++ b/apps/web/pages/_app.tsx @@ -13,7 +13,6 @@ import "ui/fonts/styles.css"; // for custom fonts not available on @fontsource import { WebLayout } from "@/features/layout"; import { CartProvider } from "@/features/merch/context/cart"; import { CheckoutProvider } from "@/features/merch/context/checkout"; -import { trpc } from "@/lib/trpc"; const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false } }, @@ -37,4 +36,4 @@ const App: AppType = ({ Component, pageProps }: AppProps) => { }; // eslint-disable-next-line @typescript-eslint/no-unsafe-call -export default trpc.withTRPC(App); +export default App; diff --git a/apps/web/pages/merch/product/[slug].tsx b/apps/web/pages/merch/product/[slug].tsx index 6758b92b..f320b1cd 100644 --- a/apps/web/pages/merch/product/[slug].tsx +++ b/apps/web/pages/merch/product/[slug].tsx @@ -29,27 +29,32 @@ import { CartActionType, useCartStore, } from "features/merch/context/cart"; -import { routes } from "features/merch/constants"; +import { QueryKeys, routes } from "features/merch/constants"; import { displayPrice, displayQtyInCart, - displayStock, + displayStock, getDefaultColor, getDefaultSize, getQtyInCart, getQtyInStock, isColorAvailable, isOutOfStock, isSizeAvailable, } from "features/merch/functions"; -import { trpc } from "@/lib/trpc"; import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next"; +import { useQuery } from "@tanstack/react-query"; +import { api } from "@/features/merch/services/api"; -const GroupTitle = ({ children }: any) => ( +interface GroupTitleProps { + children: React.ReactNode; +} + +const GroupTitle = ({ children }: GroupTitleProps) => ( {children} ); -const MerchDetail = (props: InferGetStaticPropsType) => { +const MerchDetail = (_props: InferGetStaticPropsType) => { // Context hook. const { state: cartState, dispatch: cartDispatch } = useCartStore(); const router = useRouter(); @@ -63,21 +68,18 @@ const MerchDetail = (props: InferGetStaticPropsType) => { const { isOpen, onOpen, onClose } = useDisclosure(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - const { data, isLoading } = trpc.getProduct.useQuery( - { - id, - }, + const { data: product, isLoading } = useQuery( + [QueryKeys.PRODUCT, id], + () => api.getProduct(id), { - staleTime: Infinity, - refetchOnMount: false, - refetchOnWindowFocus: false, - initialData: props.product, // ssr magic + onSuccess: (data: Product) => { + setIsDisabled(!(data?.is_available === true)); + setSelectedSize(getDefaultSize(data)); + setSelectedColor(getDefaultColor(data)); + }, } ); - const product = data as Product; - //* In/decrement quantity const handleQtyChangeCounter = (isAdd = true) => { const value = isAdd ? 1 : -1; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 1e29842c..68cb0f4b 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -16,10 +16,5 @@ "@/features/*": ["features/*"], "@/lib/*": ["lib/*"] } - }, - "references": [ - { - "path": "../merch" - } - ] + } } diff --git a/turbo.json b/turbo.json index 1ce18c17..0e207b40 100644 --- a/turbo.json +++ b/turbo.json @@ -54,6 +54,11 @@ "dependsOn": ["^build"], "outputs": ["storybook-static"] }, + "clean": { + "cache": false, + "dependsOn": [], + "outputs": [] + }, "start": { "dependsOn": ["build"], "env": [ From 81e53c24c3ccd342bfbaeaaf995428ee3b23b1f1 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 00:42:53 +0800 Subject: [PATCH 28/60] update ci to use build:ci scripts for inter-app builds --- .github/workflows/ci.yml | 5 +++-- apps/merch/package.json | 1 + apps/web/package.json | 1 + turbo.json | 7 +++++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12e79126..3c3de92e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,6 @@ jobs: env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} # remote caching TURBO_TEAM: ${{ secrets.TURBO_TEAM }} # remote caching - WORDPRESS_API_URL: ${{ secrets.WORDPRESS_API_URL }} # for web steps: - name: Check out code @@ -48,6 +47,8 @@ jobs: echo "⭐️ You should run `yarn format` in the root of the project directory to fix these files" - name: Lint + Build + Unit Test - run: yarn turbo run lint build test cypress:start-headless --color + run: yarn turbo run lint build:ci test cypress:start-headless --color env: WORDPRESS_API_URL: 'https://clubs.ntu.edu.sg/csec/graphql' + # WORDPRESS_API_URL: ${{ secrets.WORDPRESS_API_URL }} # for web + diff --git a/apps/merch/package.json b/apps/merch/package.json index dd7b6ce1..b0919f0c 100644 --- a/apps/merch/package.json +++ b/apps/merch/package.json @@ -7,6 +7,7 @@ "scripts": { "build": "tsup src/index.ts --format cjs", "start": "node dist/index.js", + "start:ci": "nohup yarn start > /dev/null 2>&1 &", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist", "dev": "tsup src/index.ts --format cjs --watch --onSuccess \"node dist/index.js\"", "lint": "TIMING=1 eslint \"**/*.ts*\"", diff --git a/apps/web/package.json b/apps/web/package.json index 88e77be9..5819c13b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "next dev -p 3001", "build": "next build", + "build:ci": "next build", "start": "next start -p 3001", "lint": "next lint", "lint:fix": "next lint --fix", diff --git a/turbo.json b/turbo.json index 0e207b40..a7268ac1 100644 --- a/turbo.json +++ b/turbo.json @@ -50,6 +50,10 @@ ], "outputs": ["dist/**", "build/**", "out/**", ".next/**"] }, + "web#build:ci": { + "outputs": [".next/**"], + "dependsOn": ["^build", "merch#start:ci"] + }, "build-storybook": { "dependsOn": ["^build"], "outputs": ["storybook-static"] @@ -82,6 +86,9 @@ "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" ] }, + "start:ci": { + "dependsOn": ["build"] + }, "serve": { "dependsOn": ["build"], "env": [ From d89667359cc68bcb1dfabdf8d9817eec494eba2f Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 01:32:45 +0800 Subject: [PATCH 29/60] fix lint --- apps/merch/src/db/dynamodb.ts | 9 ++--- apps/merch/src/routes/checkout.ts | 28 ++++++++-------- apps/merch/src/trpc/lib.ts | 7 ++-- packages/ui/components/carousel/Carousel.tsx | 2 +- packages/ui/components/merch/CartButton.tsx | 33 +------------------ .../ui/components/merch/EmptyProductView.tsx | 2 +- .../ui/components/merch/MerchCarousel.tsx | 3 +- .../ui/components/merch/cart/CartItemCard.tsx | 18 +++++----- 8 files changed, 36 insertions(+), 66 deletions(-) diff --git a/apps/merch/src/db/dynamodb.ts b/apps/merch/src/db/dynamodb.ts index 0664222c..c64ba5eb 100644 --- a/apps/merch/src/db/dynamodb.ts +++ b/apps/merch/src/db/dynamodb.ts @@ -1,4 +1,5 @@ import { + ConditionalCheckFailedException, DynamoDB, GetItemCommand, PutItemCommand, @@ -71,8 +72,8 @@ export const writeItem = async ( }); try { await client.send(command); - } catch (error: any) { - if (error.code === "ConditionalCheckFailedException") { + } catch (error) { + if (error instanceof ConditionalCheckFailedException) { Logger.warn(`Item already exists in table ${tableName}`); return; } @@ -100,8 +101,8 @@ export const updateItem = async ( }); try { await client.send(command); - } catch (error: any) { - if (error.code === "ConditionalCheckFailedException") { + } catch (error) { + if (error instanceof ConditionalCheckFailedException) { Logger.warn(`Item does not exist in table ${tableName}`); return; } diff --git a/apps/merch/src/routes/checkout.ts b/apps/merch/src/routes/checkout.ts index 040a7b10..f6d7b901 100644 --- a/apps/merch/src/routes/checkout.ts +++ b/apps/merch/src/routes/checkout.ts @@ -4,17 +4,17 @@ import { CheckoutRequest, CheckoutResponse, Order, - OrderHold, + // OrderHold, OrderItem, OrderStatus, PricedCart, Product, - ReservedProduct, + // ReservedProduct, } from "types"; import { v4 as uuidv4 } from "uuid"; import { createOrder, - createOrderHoldEntry, + // createOrderHoldEntry, getProducts, // incrementStockCount, } from "../db"; @@ -92,12 +92,12 @@ export const checkout = (req: Request, res: Response) => { price: item.discountedPrice, }) ); - const reserved = cart.items.map( - (item): ReservedProduct => ({ - id: item.id, - quantity: item.quantity, - }) - ); + // const reserved = cart.items.map( + // (item): ReservedProduct => ({ + // id: item.id, + // quantity: item.quantity, + // }) + // ); const order: Order = { id: orderID, items: orderItems, @@ -107,11 +107,11 @@ export const checkout = (req: Request, res: Response) => { customer_email: email, status: OrderStatus.PENDING_PAYMENT, }; - const orderHold: OrderHold = { - transaction_id: transactionID, - expiry: expiryTime.toISOString(), - reserved_products: reserved, - }; + // const orderHold: OrderHold = { + // transaction_id: transactionID, + // expiry: expiryTime.toISOString(), + // reserved_products: reserved, + // }; // TODO: fix and uncomment stock increment + hold order diff --git a/apps/merch/src/trpc/lib.ts b/apps/merch/src/trpc/lib.ts index be807f2c..635600c1 100644 --- a/apps/merch/src/trpc/lib.ts +++ b/apps/merch/src/trpc/lib.ts @@ -1,10 +1,9 @@ import { inferAsyncReturnType, initTRPC } from "@trpc/server"; import * as trpcExpress from "@trpc/server/adapters/express"; -export const createContext = ({ - req, - res, -}: trpcExpress.CreateExpressContextOptions) => ({}); // no context +export const createContext = ({ req, res }: trpcExpress.CreateExpressContextOptions) => { + return { req, res } +}; // no context type Context = inferAsyncReturnType; diff --git a/packages/ui/components/carousel/Carousel.tsx b/packages/ui/components/carousel/Carousel.tsx index 3a7cbe03..dc293fab 100644 --- a/packages/ui/components/carousel/Carousel.tsx +++ b/packages/ui/components/carousel/Carousel.tsx @@ -1,7 +1,7 @@ import { AnimatedCarousel, AnimatedCarouselProps, AnimatedCarouselItem } from "./AnimatedCarousel"; export type CarouselItem = AnimatedCarouselItem; -export interface CarouselProps extends AnimatedCarouselProps {}; +export interface CarouselProps extends AnimatedCarouselProps {} export const Carousel = ({ items }: CarouselProps) => { return ( diff --git a/packages/ui/components/merch/CartButton.tsx b/packages/ui/components/merch/CartButton.tsx index 1b08ef8c..6f001d9b 100644 --- a/packages/ui/components/merch/CartButton.tsx +++ b/packages/ui/components/merch/CartButton.tsx @@ -1,6 +1,4 @@ -import Link from "next/link" import { Icon } from "@chakra-ui/react"; -import routes from "../../../../apps/web/features/merch/constants/routes"; const CartButton = () => { return( @@ -11,33 +9,4 @@ const CartButton = () => { ) } -/* -const CartButton = () => { - return( - - - - - - ) -} -*/ - -export default CartButton; \ No newline at end of file +export default CartButton; diff --git a/packages/ui/components/merch/EmptyProductView.tsx b/packages/ui/components/merch/EmptyProductView.tsx index 3be061c7..401dc415 100644 --- a/packages/ui/components/merch/EmptyProductView.tsx +++ b/packages/ui/components/merch/EmptyProductView.tsx @@ -8,7 +8,7 @@ export const EmptyProductView: React.FC = () => { useEffect(() => { setTimeout(() => { - router.push(routes.HOME); + router.push(routes.HOME).catch((err) => console.error(err)); }, 3000); }, []); diff --git a/packages/ui/components/merch/MerchCarousel.tsx b/packages/ui/components/merch/MerchCarousel.tsx index 830a5777..30f4b20d 100644 --- a/packages/ui/components/merch/MerchCarousel.tsx +++ b/packages/ui/components/merch/MerchCarousel.tsx @@ -3,6 +3,7 @@ import "swiper/less/autoplay"; import { Flex, Box, Image } from "@chakra-ui/react"; import { Controller } from "swiper"; import { Swiper, SwiperSlide, useSwiper } from "swiper/react"; +import type { Swiper as SwiperType } from "swiper"; import React, { ReactElement, useState } from "react"; import { ChevronLeftIcon, ChevronRightIcon } from "@chakra-ui/icons"; @@ -83,7 +84,7 @@ const Controllerer = ({ length }: { length: number }) => { }; export const MerchCarousel = ({ images }: CarouselProps) => { - const [controlledSwiper, setControlledSwiper] = useState(null); + const [controlledSwiper, setControlledSwiper] = useState(null); return ( diff --git a/packages/ui/components/merch/cart/CartItemCard.tsx b/packages/ui/components/merch/cart/CartItemCard.tsx index e85bf92a..dff850f6 100644 --- a/packages/ui/components/merch/cart/CartItemCard.tsx +++ b/packages/ui/components/merch/cart/CartItemCard.tsx @@ -32,7 +32,7 @@ const MIN_ITEM_CNT = 1; export const CartItemCard: React.FC = ({ isMobile, data, onRemove, onQuantityChange, productInfo }) => { const MAX_ITEM_CNT = productInfo ? getQtyInStock(productInfo, data.color, data.size) : 1; - const handleQtyChangeCounter = (isAdd: boolean = true) => { + const handleQtyChangeCounter = (isAdd: boolean) => { const value = isAdd ? 1 : -1; if (!isAdd && data.quantity === MIN_ITEM_CNT) { onRemove(data.id, data.size, data.color); @@ -79,9 +79,9 @@ export const CartItemCard: React.FC = ({ isMobile, data, onRemove
- + In stock: {MAX_ITEM_CNT} - +
); @@ -90,12 +90,12 @@ export const CartItemCard: React.FC = ({ isMobile, data, onRemove - @@ -134,7 +134,7 @@ export const CartItemCard: React.FC = ({ isMobile, data, onRemove @@ -151,7 +151,7 @@ export const CartItemCard: React.FC = ({ isMobile, data, onRemove {productInfo?.name} From 085326accf973df0044b8d9e45f1c18a11a9a359 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 01:51:18 +0800 Subject: [PATCH 30/60] fix ci --- apps/cms/package.json | 1 + apps/merch/package.json | 1 + apps/web/package.json | 2 +- turbo.json | 6 +++++- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/cms/package.json b/apps/cms/package.json index 2acfd1d1..1561cc7a 100644 --- a/apps/cms/package.json +++ b/apps/cms/package.json @@ -9,6 +9,7 @@ "build:payload": "yarn generate:types && PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build", "build:server": "tsc", "build": "yarn copyfiles && yarn build:payload && yarn build:server", + "build:ci": "yarn run build", "clean": "rm -rf dist && rm -rf build", "serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js BABEL_DISABLE_CACHE=1 NODE_ENV=production node dist/server.js", "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/", diff --git a/apps/merch/package.json b/apps/merch/package.json index b0919f0c..a8a5a1b5 100644 --- a/apps/merch/package.json +++ b/apps/merch/package.json @@ -6,6 +6,7 @@ "license": "Apache-2.0", "scripts": { "build": "tsup src/index.ts --format cjs", + "build:ci": "yarn run build", "start": "node dist/index.js", "start:ci": "nohup yarn start > /dev/null 2>&1 &", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist", diff --git a/apps/web/package.json b/apps/web/package.json index 5819c13b..1e220529 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "next dev -p 3001", "build": "next build", - "build:ci": "next build", + "build:ci": "yarn run build", "start": "next start -p 3001", "lint": "next lint", "lint:fix": "next lint --fix", diff --git a/turbo.json b/turbo.json index a7268ac1..b91dfd3a 100644 --- a/turbo.json +++ b/turbo.json @@ -50,6 +50,10 @@ ], "outputs": ["dist/**", "build/**", "out/**", ".next/**"] }, + "build:ci": { + "dependsOn": ["^build", "^build:ci"], + "outputs": ["dist/**", "build/**", "out/**", ".next/**"] + }, "web#build:ci": { "outputs": [".next/**"], "dependsOn": ["^build", "merch#start:ci"] @@ -87,7 +91,7 @@ ] }, "start:ci": { - "dependsOn": ["build"] + "dependsOn": ["build:ci"] }, "serve": { "dependsOn": ["build"], From 20dcd5663674a26867c6a18d5ddc85305e427f32 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 01:59:29 +0800 Subject: [PATCH 31/60] add missing env var for web#build --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c3de92e..f87676e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,4 +51,5 @@ jobs: env: WORDPRESS_API_URL: 'https://clubs.ntu.edu.sg/csec/graphql' # WORDPRESS_API_URL: ${{ secrets.WORDPRESS_API_URL }} # for web + NEXT_PUBLIC_MERCH_API_ORIGIN: 'http://localhost:3002' From 2c72dc20f6392a56e6acd9b6f6d804d991f56c9d Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 02:32:14 +0800 Subject: [PATCH 32/60] disable vercel auto preview deployment --- apps/web/vercel.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 apps/web/vercel.json diff --git a/apps/web/vercel.json b/apps/web/vercel.json new file mode 100644 index 00000000..45c873bf --- /dev/null +++ b/apps/web/vercel.json @@ -0,0 +1,5 @@ +{ + "git": { + "deploymentEnabled": false + } +} From 8d5f592b13532b7eb1c79cb26554f28c3e8bc894 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 04:30:51 +0800 Subject: [PATCH 33/60] add manual vercel preview deployments --- .github/workflows/ci.yml | 48 ++++++++++++++++++++++++++++++++++++---- .gitignore | 3 +++ apps/web/package.json | 1 + 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f87676e4..21f37c42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,14 +6,15 @@ on: pull_request: types: [opened, synchronize] +env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} # remote caching + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} # remote caching + jobs: build: name: Lint, Build and Test timeout-minutes: 15 runs-on: ubuntu-latest - env: - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} # remote caching - TURBO_TEAM: ${{ secrets.TURBO_TEAM }} # remote caching steps: - name: Check out code @@ -49,7 +50,46 @@ jobs: - name: Lint + Build + Unit Test run: yarn turbo run lint build:ci test cypress:start-headless --color env: + # web WORDPRESS_API_URL: 'https://clubs.ntu.edu.sg/csec/graphql' - # WORDPRESS_API_URL: ${{ secrets.WORDPRESS_API_URL }} # for web + # WORDPRESS_API_URL: ${{ secrets.WORDPRESS_API_URL }} # for web NEXT_PUBLIC_MERCH_API_ORIGIN: 'http://localhost:3002' + - name: Cache CI workflow for dependent jobs + uses: actions/cache/save@v3 + with: + path: . + key: CI-${{ runner.os }}-${{ github.run_id }} + + preview: + name: Vercel Preview Deployment (web) + timeout-minutes: 15 + runs-on: ubuntu-latest + needs: build + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID_WEB }} + + steps: + - name: Get build cache + uses: actions/cache/restore@v3 + with: + path: . + key: CI-${{ runner.os }}-${{ github.run_id }} + fail-on-cache-miss: true + + - name: Install Vercel CLI + run: npm install --global vercel@latest + + - name: Pull Vercel Environment Information + run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} + + - name: Copy Build Artifacts to .vercel dir + run: vercel build --token=${{ secrets.VERCEL_TOKEN }} + + - name: Deploy Project Artifacts to Vercel + run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} + + + diff --git a/.gitignore b/.gitignore index 40b99036..6e61c6c5 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,9 @@ yarn-error.log* # turbo .turbo +# vercel +.vercel + # jetbrains .idea diff --git a/apps/web/package.json b/apps/web/package.json index 1e220529..de3fd0fc 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,6 +6,7 @@ "dev": "next dev -p 3001", "build": "next build", "build:ci": "yarn run build", + "vercel-build": "echo 'skipping fresh build'", "start": "next start -p 3001", "lint": "next lint", "lint:fix": "next lint --fix", From 559d6e85f1ede9a8c4af61c4f69eabed05eaa569 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 05:13:14 +0800 Subject: [PATCH 34/60] update turbo.json --- apps/merch/package.json | 2 +- package.json | 1 + turbo.json | 6 ++++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/merch/package.json b/apps/merch/package.json index a8a5a1b5..79df3555 100644 --- a/apps/merch/package.json +++ b/apps/merch/package.json @@ -8,7 +8,7 @@ "build": "tsup src/index.ts --format cjs", "build:ci": "yarn run build", "start": "node dist/index.js", - "start:ci": "nohup yarn start > /dev/null 2>&1 &", + "start:ci": "nohup yarn run start > /dev/null 2>&1 &", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist", "dev": "tsup src/index.ts --format cjs --watch --onSuccess \"node dist/index.js\"", "lint": "TIMING=1 eslint \"**/*.ts*\"", diff --git a/package.json b/package.json index 43e8fd79..6cae2d39 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "format": "prettier --write .", "format:check": "prettier --check .", "build": "turbo run build", + "build:ci": "turbo run build:ci", "test": "turbo run test", "test:watch": "turbo run test:watch", "cypress": "turbo run cypress", diff --git a/turbo.json b/turbo.json index b91dfd3a..9d12d34c 100644 --- a/turbo.json +++ b/turbo.json @@ -51,7 +51,7 @@ "outputs": ["dist/**", "build/**", "out/**", ".next/**"] }, "build:ci": { - "dependsOn": ["^build", "^build:ci"], + "dependsOn": ["^build", "merch#start:ci", "^build:ci"], "outputs": ["dist/**", "build/**", "out/**", ".next/**"] }, "web#build:ci": { @@ -91,7 +91,9 @@ ] }, "start:ci": { - "dependsOn": ["build:ci"] + }, + "merch#start:ci": { + "dependsOn": ["merch#build"] }, "serve": { "dependsOn": ["build"], From eaf8ae1e93f4296e76dc190bea3b3ebed2a29f22 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 05:20:28 +0800 Subject: [PATCH 35/60] update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21f37c42..f0e17d27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: # web WORDPRESS_API_URL: 'https://clubs.ntu.edu.sg/csec/graphql' # WORDPRESS_API_URL: ${{ secrets.WORDPRESS_API_URL }} # for web - NEXT_PUBLIC_MERCH_API_ORIGIN: 'http://localhost:3002' + NEXT_PUBLIC_MERCH_API_ORIGIN: 'http://127.0.0.1:3002' - name: Cache CI workflow for dependent jobs uses: actions/cache/save@v3 From 053cca6071debe0c06b2d5b76ac61d3ed7c24d0f Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 05:37:10 +0800 Subject: [PATCH 36/60] update ci.yml --- .github/workflows/ci.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0e17d27..9101f67b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,9 +47,21 @@ jobs: echo "${{steps.prettier-run.outputs.prettier_output}}" echo "⭐️ You should run `yarn format` in the root of the project directory to fix these files" + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_CI }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_CI }} + aws-region: ap-southeast-1 + - name: Lint + Build + Unit Test run: yarn turbo run lint build:ci test cypress:start-headless --color env: + #merch + AWS_REGION: 'ap-southeast-1' + PRODUCT_TABLE_NAME: 'be-dev-products' + ORDER_TABLE_NAME: 'be-dev-orders' + ORDER_HOLD_TABLE_NAME: 'be-dev-orders-hold' # web WORDPRESS_API_URL: 'https://clubs.ntu.edu.sg/csec/graphql' # WORDPRESS_API_URL: ${{ secrets.WORDPRESS_API_URL }} # for web From 980ac34814c3f0bb11338c79068268d6ab88db7b Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 05:38:10 +0800 Subject: [PATCH 37/60] update ci.yml --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9101f67b..b22095a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ env: jobs: build: name: Lint, Build and Test - timeout-minutes: 15 + timeout-minutes: 10 runs-on: ubuntu-latest steps: @@ -54,6 +54,9 @@ jobs: aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_CI }} aws-region: ap-southeast-1 + - name: Check merch#start + run: yarn turbo run merch#start + - name: Lint + Build + Unit Test run: yarn turbo run lint build:ci test cypress:start-headless --color env: From 0907569553b49c03bc2068c8c36cf0a34a2bc2fe Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 05:42:07 +0800 Subject: [PATCH 38/60] update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b22095a6..465fdeb6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: aws-region: ap-southeast-1 - name: Check merch#start - run: yarn turbo run merch#start + run: yarn turbo run start --filter=merch - name: Lint + Build + Unit Test run: yarn turbo run lint build:ci test cypress:start-headless --color From 9f50e3a0e3117060a05f4068aaada82fe812f572 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 05:45:44 +0800 Subject: [PATCH 39/60] update ci.yml --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 465fdeb6..61f915f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,11 @@ jobs: - name: Check merch#start run: yarn turbo run start --filter=merch + env: + AWS_REGION: 'ap-southeast-1' + PRODUCT_TABLE_NAME: 'be-dev-products' + ORDER_TABLE_NAME: 'be-dev-orders' + ORDER_HOLD_TABLE_NAME: 'be-dev-orders-hold' - name: Lint + Build + Unit Test run: yarn turbo run lint build:ci test cypress:start-headless --color From eec74ceb1bbb9b072531c4381c701a76f1534ddb Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 05:48:50 +0800 Subject: [PATCH 40/60] update ci.yml --- .github/workflows/ci.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61f915f3..43d043ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,16 +54,8 @@ jobs: aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_CI }} aws-region: ap-southeast-1 - - name: Check merch#start - run: yarn turbo run start --filter=merch - env: - AWS_REGION: 'ap-southeast-1' - PRODUCT_TABLE_NAME: 'be-dev-products' - ORDER_TABLE_NAME: 'be-dev-orders' - ORDER_HOLD_TABLE_NAME: 'be-dev-orders-hold' - - name: Lint + Build + Unit Test - run: yarn turbo run lint build:ci test cypress:start-headless --color + run: yarn turbo run lint build:ci test cypress:start-headless --color --force env: #merch AWS_REGION: 'ap-southeast-1' @@ -72,7 +64,6 @@ jobs: ORDER_HOLD_TABLE_NAME: 'be-dev-orders-hold' # web WORDPRESS_API_URL: 'https://clubs.ntu.edu.sg/csec/graphql' - # WORDPRESS_API_URL: ${{ secrets.WORDPRESS_API_URL }} # for web NEXT_PUBLIC_MERCH_API_ORIGIN: 'http://127.0.0.1:3002' - name: Cache CI workflow for dependent jobs From 986a196ead92cd2f35789e16932fe619105db466 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 05:49:21 +0800 Subject: [PATCH 41/60] update ci.yml --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43d043ba..22c9cada 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ env: jobs: build: name: Lint, Build and Test - timeout-minutes: 10 + timeout-minutes: 15 runs-on: ubuntu-latest steps: @@ -55,7 +55,8 @@ jobs: aws-region: ap-southeast-1 - name: Lint + Build + Unit Test - run: yarn turbo run lint build:ci test cypress:start-headless --color --force +# run: yarn turbo run lint build:ci test cypress:start-headless --color --force + run: yarn turbo build:ci --color --force env: #merch AWS_REGION: 'ap-southeast-1' From ab90e97cb0b22fbad85112a1ff1f88bf91c0a66f Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 05:55:02 +0800 Subject: [PATCH 42/60] update ci.yml --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22c9cada..d6d8d238 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,8 +55,7 @@ jobs: aws-region: ap-southeast-1 - name: Lint + Build + Unit Test -# run: yarn turbo run lint build:ci test cypress:start-headless --color --force - run: yarn turbo build:ci --color --force + run: yarn turbo run lint build:ci test cypress:start-headless --color --force env: #merch AWS_REGION: 'ap-southeast-1' From 6a98b40c5cb8a3cd0393e1529c143abaf9eaf7f5 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 06:26:51 +0800 Subject: [PATCH 43/60] bump next.js to latest --- apps/web/package.json | 2 +- yarn.lock | 124 +++++++++++++++++++++--------------------- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index de3fd0fc..e7d53529 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -31,7 +31,7 @@ "@trpc/react-query": "^10.31.0", "framer-motion": "^7.6.4", "merch-helpers": "*", - "next": "13.4.6", + "next": "13.4.9", "react": "18.2.0", "react-bootstrap": "^2.5.0", "react-dom": "18.2.0", diff --git a/yarn.lock b/yarn.lock index 8c302a56..20208f59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5028,10 +5028,10 @@ pump "^3.0.0" tar-fs "^2.1.1" -"@next/env@13.4.6": - version "13.4.6" - resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.6.tgz#3f2041c7758660d7255707ae4cb9166519113dea" - integrity sha512-nqUxEtvDqFhmV1/awSg0K2XHNwkftNaiUqCYO9e6+MYmqNObpKVl7OgMkGaQ2SZnFx5YqF0t60ZJTlyJIDAijg== +"@next/env@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.9.tgz#b77759514dd56bfa9791770755a2482f4d6ca93e" + integrity sha512-vuDRK05BOKfmoBYLNi2cujG2jrYbEod/ubSSyqgmEx9n/W3eZaJQdRNhTfumO+qmq/QTzLurW487n/PM/fHOkw== "@next/eslint-plugin-next@12.3.4": version "12.3.4" @@ -5040,50 +5040,50 @@ dependencies: glob "7.1.7" -"@next/swc-darwin-arm64@13.4.6": - version "13.4.6" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.6.tgz#47485f3deaee6681b4a4036c74bb9c4b728d5ddd" - integrity sha512-ahi6VP98o4HV19rkOXPSUu+ovfHfUxbJQ7VVJ7gL2FnZRr7onEFC1oGQ6NQHpm8CxpIzSSBW79kumlFMOmZVjg== - -"@next/swc-darwin-x64@13.4.6": - version "13.4.6" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.6.tgz#a6a5b232ec0f2079224fb8ed6bf11dc479af1acf" - integrity sha512-13cXxKFsPJIJKzUqrU5XB1mc0xbUgYsRcdH6/rB8c4NMEbWGdtD4QoK9ShN31TZdePpD4k416Ur7p+deMIxnnA== - -"@next/swc-linux-arm64-gnu@13.4.6": - version "13.4.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.6.tgz#2a67144e863d9c45fdbd13c7827370e7f2a28405" - integrity sha512-Ti+NMHEjTNktCVxNjeWbYgmZvA2AqMMI2AMlzkXsU7W4pXCMhrryAmAIoo+7YdJbsx01JQWYVxGe62G6DoCLaA== - -"@next/swc-linux-arm64-musl@13.4.6": - version "13.4.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.6.tgz#5a191ac3575a70598e9e9c6e7264fc0b8a90b2db" - integrity sha512-OHoC6gO7XfjstgwR+z6UHKlvhqJfyMtNaJidjx3sEcfaDwS7R2lqR5AABi8PuilGgi0BO0O0sCXqLlpp3a0emQ== - -"@next/swc-linux-x64-gnu@13.4.6": - version "13.4.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.6.tgz#d38adf842a8b8f9de492454328fd32a2c53350f3" - integrity sha512-zHZxPGkUlpfNJCboUrFqwlwEX5vI9LSN70b8XEb0DYzzlrZyCyOi7hwDp/+3Urm9AB7YCAJkgR5Sp1XBVjHdfQ== - -"@next/swc-linux-x64-musl@13.4.6": - version "13.4.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.6.tgz#74c745774358b78be7f958e7a8b7d93936cd6ebc" - integrity sha512-K/Y8lYGTwTpv5ME8PSJxwxLolaDRdVy+lOd9yMRMiQE0BLUhtxtCWC9ypV42uh9WpLjoaD0joOsB9Q6mbrSGJg== - -"@next/swc-win32-arm64-msvc@13.4.6": - version "13.4.6" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.6.tgz#1e1e02c175573e64808fc1a7e8650e3e217f1edc" - integrity sha512-U6LtxEUrjBL2tpW+Kr1nHCSJWNeIed7U7l5o7FiKGGwGgIlFi4UHDiLI6TQ2lxi20fAU33CsruV3U0GuzMlXIw== - -"@next/swc-win32-ia32-msvc@13.4.6": - version "13.4.6" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.6.tgz#2b528ae3ec7f6e727f4f0d81a1015f63da55c7a6" - integrity sha512-eEBeAqpCfhdPSlCZCayjCiyIllVqy4tcqvm1xmg3BgJG0G5ITiMM4Cw2WVeRSgWDJqQGRyyb+q8Y2ltzhXOWsQ== - -"@next/swc-win32-x64-msvc@13.4.6": - version "13.4.6" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.6.tgz#38620bd68267ff13e50ecd432f1822eac51382a8" - integrity sha512-OrZs94AuO3ZS5tnqlyPRNgfWvboXaDQCi5aXGve3o3C+Sj0ctMUV9+Do+0zMvvLRumR8E0PTWKvtz9n5vzIsWw== +"@next/swc-darwin-arm64@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.9.tgz#0ed408d444bbc6b0a20f3506a9b4222684585677" + integrity sha512-TVzGHpZoVBk3iDsTOQA/R6MGmFp0+17SWXMEWd6zG30AfuELmSSMe2SdPqxwXU0gbpWkJL1KgfLzy5ReN0crqQ== + +"@next/swc-darwin-x64@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.9.tgz#a08fccdee68201522fe6618ec81f832084b222f8" + integrity sha512-aSfF1fhv28N2e7vrDZ6zOQ+IIthocfaxuMWGReB5GDriF0caTqtHttAvzOMgJgXQtQx6XhyaJMozLTSEXeNN+A== + +"@next/swc-linux-arm64-gnu@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.9.tgz#1798c2341bb841e96521433eed00892fb24abbd1" + integrity sha512-JhKoX5ECzYoTVyIy/7KykeO4Z2lVKq7HGQqvAH+Ip9UFn1MOJkOnkPRB7v4nmzqAoY+Je05Aj5wNABR1N18DMg== + +"@next/swc-linux-arm64-musl@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.9.tgz#cee04c51610eddd3638ce2499205083656531ea0" + integrity sha512-OOn6zZBIVkm/4j5gkPdGn4yqQt+gmXaLaSjRSO434WplV8vo2YaBNbSHaTM9wJpZTHVDYyjzuIYVEzy9/5RVZw== + +"@next/swc-linux-x64-gnu@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.9.tgz#1932d0367916adbc6844b244cda1d4182bd11f7a" + integrity sha512-iA+fJXFPpW0SwGmx/pivVU+2t4zQHNOOAr5T378PfxPHY6JtjV6/0s1vlAJUdIHeVpX98CLp9k5VuKgxiRHUpg== + +"@next/swc-linux-x64-musl@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.9.tgz#a66aa8c1383b16299b72482f6360facd5cde3c7a" + integrity sha512-rlNf2WUtMM+GAQrZ9gMNdSapkVi3koSW3a+dmBVp42lfugWVvnyzca/xJlN48/7AGx8qu62WyO0ya1ikgOxh6A== + +"@next/swc-win32-arm64-msvc@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.9.tgz#39482ee856c867177a612a30b6861c75e0736a4a" + integrity sha512-5T9ybSugXP77nw03vlgKZxD99AFTHaX8eT1ayKYYnGO9nmYhJjRPxcjU5FyYI+TdkQgEpIcH7p/guPLPR0EbKA== + +"@next/swc-win32-ia32-msvc@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.9.tgz#29db85e34b597ade1a918235d16a760a9213c190" + integrity sha512-ojZTCt1lP2ucgpoiFgrFj07uq4CZsq4crVXpLGgQfoFq00jPKRPgesuGPaz8lg1yLfvafkU3Jd1i8snKwYR3LA== + +"@next/swc-win32-x64-msvc@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.9.tgz#0c2758164cccd61bc5a1c6cd8284fe66173e4a2b" + integrity sha512-QbT03FXRNdpuL+e9pLnu+XajZdm/TtIXVYY4lA9t+9l0fLZbHXDYEKitAqxrOj37o3Vx5ufxiRAniaIebYDCgw== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -13482,12 +13482,12 @@ next-tick@1, next-tick@^1.1.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== -next@13.4.6: - version "13.4.6" - resolved "https://registry.yarnpkg.com/next/-/next-13.4.6.tgz#ebe52f5c74d60176d45b45e73f25a51103713ea4" - integrity sha512-sjVqjxU+U2aXZnYt4Ud6CTLNNwWjdSfMgemGpIQJcN3Z7Jni9xRWbR0ie5fQzCg87aLqQVhKA2ud2gPoqJ9lGw== +next@13.4.9: + version "13.4.9" + resolved "https://registry.yarnpkg.com/next/-/next-13.4.9.tgz#473de5997cb4c5d7a4fb195f566952a1cbffbeba" + integrity sha512-vtefFm/BWIi/eWOqf1GsmKG3cjKw1k3LjuefKRcL3iiLl3zWzFdPG3as6xtxrGO6gwTzzaO1ktL4oiHt/uvTjA== dependencies: - "@next/env" "13.4.6" + "@next/env" "13.4.9" "@swc/helpers" "0.5.1" busboy "1.6.0" caniuse-lite "^1.0.30001406" @@ -13496,15 +13496,15 @@ next@13.4.6: watchpack "2.4.0" zod "3.21.4" optionalDependencies: - "@next/swc-darwin-arm64" "13.4.6" - "@next/swc-darwin-x64" "13.4.6" - "@next/swc-linux-arm64-gnu" "13.4.6" - "@next/swc-linux-arm64-musl" "13.4.6" - "@next/swc-linux-x64-gnu" "13.4.6" - "@next/swc-linux-x64-musl" "13.4.6" - "@next/swc-win32-arm64-msvc" "13.4.6" - "@next/swc-win32-ia32-msvc" "13.4.6" - "@next/swc-win32-x64-msvc" "13.4.6" + "@next/swc-darwin-arm64" "13.4.9" + "@next/swc-darwin-x64" "13.4.9" + "@next/swc-linux-arm64-gnu" "13.4.9" + "@next/swc-linux-arm64-musl" "13.4.9" + "@next/swc-linux-x64-gnu" "13.4.9" + "@next/swc-linux-x64-musl" "13.4.9" + "@next/swc-win32-arm64-msvc" "13.4.9" + "@next/swc-win32-ia32-msvc" "13.4.9" + "@next/swc-win32-x64-msvc" "13.4.9" no-case@^3.0.4: version "3.0.4" From 1a3de6f45246b72726fd75c800722c376350d21e Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 18:19:56 +0800 Subject: [PATCH 44/60] update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6d8d238..5db857af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: aws-region: ap-southeast-1 - name: Lint + Build + Unit Test - run: yarn turbo run lint build:ci test cypress:start-headless --color --force + run: yarn turbo run build:ci --color --force env: #merch AWS_REGION: 'ap-southeast-1' From 28f8b950969b39ed617cdfeaa86aff99922f1719 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 18:37:50 +0800 Subject: [PATCH 45/60] update ci.yml --- .github/workflows/ci.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5db857af..15e3a81e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,7 +100,25 @@ jobs: run: vercel build --token=${{ secrets.VERCEL_TOKEN }} - name: Deploy Project Artifacts to Vercel - run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} + id: deploy-to-vercel + run: | + DEPLOYMENT_URL=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}) + echo "deployment-url=$DEPLOYMENT_URL" >> "$GITHUB_OUTPUT" + + - name: Print Deployment URL + env: + DEPLOYMENT_URL: ${{ steps.deploy-to-vercel.outputs.deployment-url }} + run: echo $DEPLOYMENT_URL + +# - name: Comment PR with preview link +# uses: thollander/actions-comment-pull-request@v2 +# env: +# +# with: +# message: | +# Deployment preview link: +# comment_tag: execution +# mode: recreate From ce7a43cff745ee230601454a9000d628dc2e23df Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 18:50:45 +0800 Subject: [PATCH 46/60] update ci.yml --- .github/workflows/ci.yml | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15e3a81e..0f539c2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,15 @@ jobs: runs-on: ubuntu-latest steps: + - name: Grab cache + uses: actions/cache/restore@v3 + with: + path: . + key: CI-${{ runner.os }}-${{ github.ref }}-${{ github.run_id }} + restore-keys: | + CI-${{ runner.os }}-${{ github.ref }}- + CI-${{ runner.os }}-refs/head/main- + - name: Check out code uses: actions/checkout@v3 with: @@ -70,7 +79,7 @@ jobs: uses: actions/cache/save@v3 with: path: . - key: CI-${{ runner.os }}-${{ github.run_id }} + key: CI-${{ runner.os }}-${{ github.ref }}-${{ github.run_id }} preview: name: Vercel Preview Deployment (web) @@ -110,6 +119,15 @@ jobs: DEPLOYMENT_URL: ${{ steps.deploy-to-vercel.outputs.deployment-url }} run: echo $DEPLOYMENT_URL + - name: Update deployment status (success) + if: success() + uses: chrnorm/deployment-status@v2 + with: + token: '${{ github.token }}' + environment-url: '${{ steps.deploy-to-vercel.outputs.deployment-url }}' + state: 'success' + deployment-id: ${{ steps.deployment.outputs.deployment_id }} + # - name: Comment PR with preview link # uses: thollander/actions-comment-pull-request@v2 # env: @@ -120,5 +138,13 @@ jobs: # comment_tag: execution # mode: recreate + - name: Update deployment status (failure) + if: failure() + uses: chrnorm/deployment-status@v2 + with: + token: '${{ github.token }}' + state: 'failure' + deployment-id: ${{ steps.deployment.outputs.deployment_id }} + From 6234f44d26ef997911245f6d009ac85c29f4adb3 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 18:57:29 +0800 Subject: [PATCH 47/60] update ci.yml --- .github/workflows/ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f539c2c..7e3ff96b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,7 @@ jobs: uses: actions/cache/restore@v3 with: path: . - key: CI-${{ runner.os }}-${{ github.run_id }} + key: CI-${{ runner.os }}-${{ github.ref }}-${{ github.run_id }} fail-on-cache-miss: true - name: Install Vercel CLI @@ -138,13 +138,13 @@ jobs: # comment_tag: execution # mode: recreate - - name: Update deployment status (failure) - if: failure() - uses: chrnorm/deployment-status@v2 - with: - token: '${{ github.token }}' - state: 'failure' - deployment-id: ${{ steps.deployment.outputs.deployment_id }} +# - name: Update deployment status (failure) +# if: failure() +# uses: chrnorm/deployment-status@v2 +# with: +# token: '${{ github.token }}' +# state: 'failure' +# deployment-id: ${{ steps.deployment.outputs.deployment_id }} From 4882cf1be2bc2a1b3e209e5862da46a286d8cb77 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 19:01:37 +0800 Subject: [PATCH 48/60] update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e3ff96b..ae339e10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,7 @@ jobs: aws-region: ap-southeast-1 - name: Lint + Build + Unit Test - run: yarn turbo run build:ci --color --force + run: yarn turbo run build:ci --color env: #merch AWS_REGION: 'ap-southeast-1' From 5f8be29665b9b284f72816d4de8557f1e55f9683 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 19:15:41 +0800 Subject: [PATCH 49/60] update ci.yml --- .github/workflows/ci.yml | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae339e10..165575b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,6 +86,8 @@ jobs: timeout-minutes: 15 runs-on: ubuntu-latest needs: build + permissions: + deployments: write env: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} @@ -119,6 +121,15 @@ jobs: DEPLOYMENT_URL: ${{ steps.deploy-to-vercel.outputs.deployment-url }} run: echo $DEPLOYMENT_URL + - uses: chrnorm/deployment-action@v2 + name: Create GitHub deployment + id: deployment + with: + token: '${{ github.token }}' + environment-url: http://my-app-url.com + environment: Preview + task: website + - name: Update deployment status (success) if: success() uses: chrnorm/deployment-status@v2 @@ -128,6 +139,14 @@ jobs: state: 'success' deployment-id: ${{ steps.deployment.outputs.deployment_id }} + - name: Update deployment status (failure) + if: failure() + uses: chrnorm/deployment-status@v2 + with: + token: '${{ github.token }}' + state: 'failure' + deployment-id: ${{ steps.deployment.outputs.deployment_id }} + # - name: Comment PR with preview link # uses: thollander/actions-comment-pull-request@v2 # env: @@ -138,13 +157,7 @@ jobs: # comment_tag: execution # mode: recreate -# - name: Update deployment status (failure) -# if: failure() -# uses: chrnorm/deployment-status@v2 -# with: -# token: '${{ github.token }}' -# state: 'failure' -# deployment-id: ${{ steps.deployment.outputs.deployment_id }} + From 784e8b13bf1082f52fd7e0c47a68ca3ed0c1bff2 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 19:22:25 +0800 Subject: [PATCH 50/60] update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 165575b8..af8e3595 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,7 +126,7 @@ jobs: id: deployment with: token: '${{ github.token }}' - environment-url: http://my-app-url.com + environment-url: '${{ steps.deploy-to-vercel.outputs.deployment-url }}' environment: Preview task: website From 10a540fac70d50b52f0a35ab6c32667934bad81c Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 19:25:15 +0800 Subject: [PATCH 51/60] update ci.yml --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af8e3595..e9535206 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,8 +127,7 @@ jobs: with: token: '${{ github.token }}' environment-url: '${{ steps.deploy-to-vercel.outputs.deployment-url }}' - environment: Preview - task: website + environment: Preview - website - name: Update deployment status (success) if: success() From 3eabeeb2d4bf51de668117acda819ae17d95da60 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 19:28:03 +0800 Subject: [PATCH 52/60] update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9535206..fd2614a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,7 +82,7 @@ jobs: key: CI-${{ runner.os }}-${{ github.ref }}-${{ github.run_id }} preview: - name: Vercel Preview Deployment (web) + name: Vercel Preview - web timeout-minutes: 15 runs-on: ubuntu-latest needs: build From a3c2fc864a2171ceb67997df676c52b09690b3b9 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 19:29:45 +0800 Subject: [PATCH 53/60] update ci.yml --- .github/workflows/ci.yml | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd2614a4..75dec588 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -146,15 +146,27 @@ jobs: state: 'failure' deployment-id: ${{ steps.deployment.outputs.deployment_id }} -# - name: Comment PR with preview link -# uses: thollander/actions-comment-pull-request@v2 -# env: -# -# with: -# message: | -# Deployment preview link: -# comment_tag: execution -# mode: recreate + - name: Comment PR with preview link if success + if: success() + uses: thollander/actions-comment-pull-request@v2 + env: + DEPLOYMENT_URL: ${{ steps.deploy-to-vercel.outputs.deployment-url }} + with: + message: | + Deployment preview link: ${{ env.DEPLOYMENT_URL }} + comment_tag: execution + mode: recreate + + - name: Comment PR with preview link if fail + if: failure() + uses: thollander/actions-comment-pull-request@v2 + env: + DEPLOYMENT_URL: ${{ steps.deploy-to-vercel.outputs.deployment-url }} + with: + message: | + Deployment preview link: ❌ + comment_tag: execution + mode: recreate From 3c3c9bc8438ee09c02eefde5735e1c8baef0f724 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 19:30:36 +0800 Subject: [PATCH 54/60] update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75dec588..0b915d04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,7 @@ jobs: aws-region: ap-southeast-1 - name: Lint + Build + Unit Test - run: yarn turbo run build:ci --color + run: yarn turbo run lint build:ci --color env: #merch AWS_REGION: 'ap-southeast-1' From 833305133868044b9ec191d000961a6c4b6f1790 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 19:36:57 +0800 Subject: [PATCH 55/60] update ci.yml --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b915d04..8cabbc71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,6 +88,7 @@ jobs: needs: build permissions: deployments: write + pull-requests: write env: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} @@ -157,6 +158,7 @@ jobs: comment_tag: execution mode: recreate + - name: Comment PR with preview link if fail if: failure() uses: thollander/actions-comment-pull-request@v2 From ca236247db705d9576ee788d9661894c0d46aa08 Mon Sep 17 00:00:00 2001 From: dyllon Date: Sun, 9 Jul 2023 19:54:46 +0800 Subject: [PATCH 56/60] update cd-staging.yml --- .github/workflows/cd-staging.yml | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/.github/workflows/cd-staging.yml b/.github/workflows/cd-staging.yml index dee42a57..09c1fd70 100644 --- a/.github/workflows/cd-staging.yml +++ b/.github/workflows/cd-staging.yml @@ -55,12 +55,39 @@ jobs: ghcr.io/${{ github.repository_owner }}/website/merch:${{ github.sha }} ghcr.io/${{ github.repository_owner }}/website/merch:latest + + build-web: + name: Build Web next.js app + runs-on: ubuntu-22.04 + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Setup Node.js environment + uses: actions/setup-node@v3 + with: + node-version: 16 + cache: "yarn" + + - name: Install dependencies + run: yarn install --prefer-offline --frozen-lockfile + + - name: Build web + run: yarn turbo run build --filter=web + + - name: Cache CD Staging workflow for dependent jobs + uses: actions/cache/save@v3 + with: + path: . + key: CD-Staging-${{ runner.os }}-${{ github.ref }}-${{ github.run_id }} + deploy-to-staging: name: Deploy To Staging runs-on: ubuntu-22.04 needs: - build-cms - build-merch + - build-web steps: - name: Checkout repo uses: actions/checkout@v3 @@ -81,3 +108,33 @@ jobs: docker compose pull && docker compose up -d ' + + deploy-to-vercel: + name: Deploy To Staging + runs-on: ubuntu-22.04 + needs: + - build-cms + - build-merch + - build-web + steps: + - name: Get build cache + uses: actions/cache/restore@v3 + with: + path: . + key: CD-Staging-${{ runner.os }}-${{ github.ref }}-${{ github.run_id }} + fail-on-cache-miss: true + + - name: Install Vercel CLI + run: npm install --global vercel@latest + + - name: Pull Vercel Environment Information + run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} + + - name: Copy Build Artifacts to .vercel dir + run: vercel build --token=${{ secrets.VERCEL_TOKEN }} + + - name: Deploy Project Artifacts to Vercel + id: deploy-to-vercel + run: | + DEPLOYMENT_URL=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}) + vercel alias --token=${{ secrets.VERCEL_TOKEN }} set "$DEPLOYMENT_URL" dev.ntuscse.com From 0a8c8164a3734e97f582fa159e28206f98ddeadc Mon Sep 17 00:00:00 2001 From: Guo Yong <97776029+YoNG-Zaii@users.noreply.github.com> Date: Tue, 21 Mar 2023 18:00:58 +0800 Subject: [PATCH 57/60] task: Port home page (product listing) (#77) --- apps/web/features/merch/services/api.tsx | 117 ++++++++++++++++++++ packages/ui/components/merch/CartHeader.tsx | 47 ++++++++ packages/ui/components/merch/Skeleton.tsx | 18 +++ 3 files changed, 182 insertions(+) create mode 100644 apps/web/features/merch/services/api.tsx create mode 100644 packages/ui/components/merch/CartHeader.tsx create mode 100644 packages/ui/components/merch/Skeleton.tsx diff --git a/apps/web/features/merch/services/api.tsx b/apps/web/features/merch/services/api.tsx new file mode 100644 index 00000000..19da51e2 --- /dev/null +++ b/apps/web/features/merch/services/api.tsx @@ -0,0 +1,117 @@ +import { Product } from "types/lib/merch"; + +export class Api { + private API_ORIGIN: string; + + constructor() { + if (!process.env.NEXT_PUBLIC_MERCH_API_ORIGIN) { + throw new Error("NEXT_PUBLIC_MERCH_API_ORIGIN environment variable is not set") + } + this.API_ORIGIN = process.env.NEXT_PUBLIC_MERCH_API_ORIGIN || ""; + } + + // http methods + async get(urlPath: string): Promise> { + const response = await fetch(`${this.API_ORIGIN}${urlPath}`); + const convert = response.json() as unknown; // Convert to unknown type + return convert as Record + } + + /* + // eslint-disable-next-line class-methods-use-this + async post(urlPath: string, data: any): Promise { + const response = await fetch(`${this.API_ORIGIN}${urlPath}`, { + method: "POST", // *GET, POST, PUT, DELETE, etc. + mode: "cors", // no-cors, *cors, same-origin + cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached + credentials: "same-origin", // include, *same-origin, omit + headers: { + "Content-Type": "application/json", + // 'Content-Type': 'application/x-www-form-urlencoded', + }, + redirect: "follow", // manual, *follow, error + referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url + body: JSON.stringify(data), // body data type must match + }); + return response.json(); + } + */ + + // eslint-disable-next-line class-methods-use-this + async getProducts(): Promise { + try { + const res = await this.get("/products"); + console.log("product-list", res); + return res?.products ?? []; + } catch (e) { + if(e instanceof Error){ + throw new Error(e.message); + } + return [] + } + } + + /* + // eslint-disable-next-line class-methods-use-this + async getProduct(productId: string) { + try { + const res = await this.get(`/products/${productId}`); + console.log("product res", res); + return res; + } catch (e: any) { + throw new Error(e); + } + } + + async getOrder(userId: string, orderId: string) { + try { + const res = await this.get(`/orders/${orderId}`); + console.log("Order Summary response:", res); + return res; + } catch (e: any) { + throw new Error(e); + } + } + + async getOrderHistory(userId: string) { + try { + const res = await this.get(`/orders/${userId}`); + console.log("Order Summary response:", res); + return res.json(); + } catch (e: any) { + throw new Error(e); + } + } + + async postCheckoutCart( + items: CartItemType[], + email: string, + promoCode: string | null + ) { + try { + const res = await this.post(`/cart/checkout`, { + items, + promoCode: promoCode ?? "", + email, + }); + return res; + } catch (e: any) { + throw new Error(e); + } + } + + async postQuotation(items: CartItemType[], promoCode: string | null) { + try { + const res = await this.post(`/cart/quotation`, { + items, + promoCode: promoCode ?? "", + }); + return res; + } catch (e: any) { + throw new Error(e); + } + } + */ +} + +export const api = new Api(); diff --git a/packages/ui/components/merch/CartHeader.tsx b/packages/ui/components/merch/CartHeader.tsx new file mode 100644 index 00000000..17d6cdeb --- /dev/null +++ b/packages/ui/components/merch/CartHeader.tsx @@ -0,0 +1,47 @@ +import { + Box, + Flex, + HStack, + Spacer, + Show, + Hide, + Icon, +} from "@chakra-ui/react"; +import Link from 'next/link'; +import routes from "../../../../apps/web/features/merch/constants/routes"; + + +const CartHeader = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default CartHeader; \ No newline at end of file diff --git a/packages/ui/components/merch/Skeleton.tsx b/packages/ui/components/merch/Skeleton.tsx new file mode 100644 index 00000000..eae86104 --- /dev/null +++ b/packages/ui/components/merch/Skeleton.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { Grid, Skeleton, SkeletonText, GridItem } from "@chakra-ui/react"; + +const ProductListSkeleton: React.FC = () => { + return ( + + {new Array(8).fill(null).map((item: any) => ( + + + + {item} + + ))} + + ); +}; + +export default ProductListSkeleton; From 073805850a3aa83687940aad2ff45fbb8f4ade9c Mon Sep 17 00:00:00 2001 From: nicolelst Date: Mon, 27 Mar 2023 23:37:49 +0800 Subject: [PATCH 58/60] feat(merch): Implement product page --- apps/web/features/merch/services/api.tsx | 2 +- package.json | 4 +++- packages/ui/components/merch/CartHeader.tsx | 6 ++---- packages/ui/components/merch/Skeleton.tsx | 18 ------------------ 4 files changed, 6 insertions(+), 24 deletions(-) delete mode 100644 packages/ui/components/merch/Skeleton.tsx diff --git a/apps/web/features/merch/services/api.tsx b/apps/web/features/merch/services/api.tsx index 19da51e2..bfac534c 100644 --- a/apps/web/features/merch/services/api.tsx +++ b/apps/web/features/merch/services/api.tsx @@ -51,7 +51,6 @@ export class Api { } } - /* // eslint-disable-next-line class-methods-use-this async getProduct(productId: string) { try { @@ -63,6 +62,7 @@ export class Api { } } + /* async getOrder(userId: string, orderId: string) { try { const res = await this.get(`/orders/${orderId}`); diff --git a/package.json b/package.json index 6cae2d39..2b1ed567 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,8 @@ "yarn": ">= 1.22.17", "pnpm": "please-use-yarn" }, - "dependencies": {}, + "dependencies": { + "swiper": "^9.2.0" + }, "packageManager": "yarn@1.22.17" } diff --git a/packages/ui/components/merch/CartHeader.tsx b/packages/ui/components/merch/CartHeader.tsx index 17d6cdeb..d0359986 100644 --- a/packages/ui/components/merch/CartHeader.tsx +++ b/packages/ui/components/merch/CartHeader.tsx @@ -11,7 +11,7 @@ import Link from 'next/link'; import routes from "../../../../apps/web/features/merch/constants/routes"; -const CartHeader = () => { +export const CartHeader = () => { return ( @@ -42,6 +42,4 @@ const CartHeader = () => { ); -}; - -export default CartHeader; \ No newline at end of file +}; \ No newline at end of file diff --git a/packages/ui/components/merch/Skeleton.tsx b/packages/ui/components/merch/Skeleton.tsx deleted file mode 100644 index eae86104..00000000 --- a/packages/ui/components/merch/Skeleton.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; -import { Grid, Skeleton, SkeletonText, GridItem } from "@chakra-ui/react"; - -const ProductListSkeleton: React.FC = () => { - return ( - - {new Array(8).fill(null).map((item: any) => ( - - - - {item} - - ))} - - ); -}; - -export default ProductListSkeleton; From c6d59c767f43a4235bbd62ce1d471ba5205b9b00 Mon Sep 17 00:00:00 2001 From: kingsmil Date: Mon, 10 Jul 2023 20:04:21 +0800 Subject: [PATCH 59/60] refactored ordernumber and qrcode --- apps/web/pages/merch/orders/[slug].tsx | 104 ++----------------- packages/ui/components/merch/OrderNumber.tsx | 76 ++++++++++++++ packages/ui/components/merch/QRCode.tsx | 41 ++++++++ 3 files changed, 124 insertions(+), 97 deletions(-) create mode 100644 packages/ui/components/merch/OrderNumber.tsx create mode 100644 packages/ui/components/merch/QRCode.tsx diff --git a/apps/web/pages/merch/orders/[slug].tsx b/apps/web/pages/merch/orders/[slug].tsx index 5f9e8a52..138191be 100644 --- a/apps/web/pages/merch/orders/[slug].tsx +++ b/apps/web/pages/merch/orders/[slug].tsx @@ -1,17 +1,18 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -import { Image, Badge, Button, Divider, Flex, Heading, Text, useBreakpointValue } from "@chakra-ui/react"; +import { Button, Divider, Flex, Heading, Text, useBreakpointValue } from "@chakra-ui/react"; import { useQuery } from "@tanstack/react-query"; import { Page } from "ui/components/merch"; -import { Order, OrderStatus } from "types"; +import { Order } from "types"; import { api } from "features/merch/services/api"; import { routes } from "features/merch/constants/routes"; import { QueryKeys } from "features/merch/constants/queryKeys"; import { displayPrice } from "features/merch/functions/currency"; import Link from "next/link" import LoadingScreen from "ui/components/merch/skeleton/LoadingScreen"; -import { getOrderStatusColor, renderOrderStatus } from "merch-helpers"; import OrderItem from "ui/components/merch/OrderItem"; +import QRCode from "ui/components/merch/QRCode"; +import OrderNumber from "ui/components/merch/OrderNumber"; const OrderSummary: React.FC = () => { // Check if break point hit. KIV const isMobile: boolean = useBreakpointValue({ base: true, md: false }) || false; @@ -65,69 +66,7 @@ const OrderSummary: React.FC = () => { overflow="hidden" flexDir="column" > -
- - - - {renderOrderStatus(orderState?.status ?? OrderStatus.PENDING_PAYMENT)} - - Order Number - - {orderState?.id.split("-")[0]} - - - {orderState?.id} - - - Order date:{" "} - {orderState?.transaction_time - ? new Date(`${orderState.transaction_time}`).toLocaleString( - "en-sg" - ) - : ""} - - {/*Last update: {orderState?.lastUpdate}*/} - - -
-
- - - - Order Number - - {renderOrderStatus(orderState?.status ?? OrderStatus.PENDING_PAYMENT)} - - - - {orderState?.id.split("-")[0]} - - - {orderState?.id} - - - - - Order date:{" "} - {orderState?.transaction_time - ? new Date(`${orderState.transaction_time}`).toLocaleString( - "en-sg" - ) - : ""} - - {/*Last update: {orderState?.lastUpdate}*/} - - -
+ {/*{orderState?.items.map((item) => (*/} {/* */} @@ -144,7 +83,7 @@ const OrderSummary: React.FC = () => {
{displayPrice(total)} - + {/*{displayPrice( TODO*/} {/* (orderState?.billing?.subtotal ?? 0) -*/} {/* (orderState?.billing?.total ?? 0)*/} @@ -156,36 +95,7 @@ const OrderSummary: React.FC = () => { - - {/* TODO: QR Code generator based on Param. */} - QRCode - - Please screenshot this QR code and show it at SCSE Lounge to collect your order. - Alternatively, show the email receipt you have received. - - - For any assistance, please contact our email address: - merch@ntuscse.com - - + ); const renderSummaryPage = () => { diff --git a/packages/ui/components/merch/OrderNumber.tsx b/packages/ui/components/merch/OrderNumber.tsx new file mode 100644 index 00000000..8c20838d --- /dev/null +++ b/packages/ui/components/merch/OrderNumber.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { Badge, Flex, Heading, Text } from "@chakra-ui/react"; +import { Order, OrderStatus } from "types"; +import { getOrderStatusColor, renderOrderStatus } from "merch-helpers"; + +interface OrderNumberProps { + orderState: Order | null; +} + +const OrderNumber: React.FC = ({ orderState }) => ( +
+ + + + {renderOrderStatus(orderState?.status ?? OrderStatus.PENDING_PAYMENT)} + + Order Number + + {orderState?.id.split("-")[0]} + + + {orderState?.id} + + + Order date:{" "} + {orderState?.transaction_time + ? new Date(`${orderState.transaction_time}`).toLocaleString( + "en-sg" + ) + : ""} + + {/*Last update: {orderState?.lastUpdate}*/} + + +
+
+ + + + Order Number + + {renderOrderStatus(orderState?.status ?? OrderStatus.PENDING_PAYMENT)} + + + + {orderState?.id.split("-")[0]} + + + {orderState?.id} + + + + + Order date:{" "} + {orderState?.transaction_time + ? new Date(`${orderState.transaction_time}`).toLocaleString( + "en-sg" + ) + : ""} + + {/*Last update: {orderState?.lastUpdate}*/} + + +
+); + +export default OrderNumber; diff --git a/packages/ui/components/merch/QRCode.tsx b/packages/ui/components/merch/QRCode.tsx new file mode 100644 index 00000000..c33c41a1 --- /dev/null +++ b/packages/ui/components/merch/QRCode.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Image, Flex, Text } from "@chakra-ui/react"; +import { Order } from "types"; + +interface QRCodeProps { + order: Order | null; +} + +const QRCode: React.FC = ({ order }) => ( + + QRCode + + Please screenshot this QR code and show it at SCSE Lounge to collect your order. + Alternatively, show the email receipt you have received. + + + For any assistance, please contact our email address: + merch@ntuscse.com + + +); + +export default QRCode; From 42317b89bf4cd1e3f35ef9deaee2a3522ce8821a Mon Sep 17 00:00:00 2001 From: kingsmil Date: Wed, 12 Jul 2023 09:53:50 +0800 Subject: [PATCH 60/60] fixed bugs, send only id as props --- apps/web/pages/merch/orders/[slug].tsx | 2 +- package.json | 4 +--- packages/ui/components/merch/QRCode.tsx | 5 ++--- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/web/pages/merch/orders/[slug].tsx b/apps/web/pages/merch/orders/[slug].tsx index 138191be..bf237002 100644 --- a/apps/web/pages/merch/orders/[slug].tsx +++ b/apps/web/pages/merch/orders/[slug].tsx @@ -95,7 +95,7 @@ const OrderSummary: React.FC = () => { - + ); const renderSummaryPage = () => { diff --git a/package.json b/package.json index 2b1ed567..6cae2d39 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,6 @@ "yarn": ">= 1.22.17", "pnpm": "please-use-yarn" }, - "dependencies": { - "swiper": "^9.2.0" - }, + "dependencies": {}, "packageManager": "yarn@1.22.17" } diff --git a/packages/ui/components/merch/QRCode.tsx b/packages/ui/components/merch/QRCode.tsx index c33c41a1..3e823999 100644 --- a/packages/ui/components/merch/QRCode.tsx +++ b/packages/ui/components/merch/QRCode.tsx @@ -1,9 +1,8 @@ import React from 'react'; import { Image, Flex, Text } from "@chakra-ui/react"; -import { Order } from "types"; interface QRCodeProps { - order: Order | null; + order: string | undefined; } const QRCode: React.FC = ({ order }) => ( @@ -19,7 +18,7 @@ const QRCode: React.FC = ({ order }) => (