diff --git a/apps/cms/src/admin/components/AfterNavLinks.tsx b/apps/cms/src/admin/components/AfterNavLinks.tsx index 772f6076..ee7edcfc 100644 --- a/apps/cms/src/admin/components/AfterNavLinks.tsx +++ b/apps/cms/src/admin/components/AfterNavLinks.tsx @@ -19,6 +19,11 @@ const merchRoutes = [ label: "Products", href: "/admin/merch/products", }, + { + id: "merch_promotions", + label: "Promotions", + href: "/admin/merch/promotions", + }, ]; const MerchLinks: React.FC = () => { diff --git a/apps/cms/src/admin/graphics/Logos.tsx b/apps/cms/src/admin/graphics/Logos.tsx index 29e59d7c..590187b8 100644 --- a/apps/cms/src/admin/graphics/Logos.tsx +++ b/apps/cms/src/admin/graphics/Logos.tsx @@ -14,4 +14,4 @@ const SCSEImage = ({ classname }: SCSEImageProps) => { }; export const SCSEIcon = () => ; -export const SCSELogo = () => ; +export const SCSELogo = () => ; \ No newline at end of file diff --git a/apps/cms/src/admin/styles.scss b/apps/cms/src/admin/styles.scss index f7307f65..f4418f4e 100644 --- a/apps/cms/src/admin/styles.scss +++ b/apps/cms/src/admin/styles.scss @@ -1,2 +1,2 @@ header { -} +} \ No newline at end of file diff --git a/apps/cms/src/admin/utils/RenderCellFactory.tsx b/apps/cms/src/admin/utils/RenderCellFactory.tsx index 9000c172..5790390e 100644 --- a/apps/cms/src/admin/utils/RenderCellFactory.tsx +++ b/apps/cms/src/admin/utils/RenderCellFactory.tsx @@ -1,5 +1,6 @@ import React from "react"; import payload from "payload"; +import { Promotion } from "types"; export class RenderCellFactory { static get(element: unknown, key: string) { @@ -103,6 +104,25 @@ export class RenderCellFactory { }; } + if (key === "discounts" && typeof element[key] === "object") { + // Render discounts + const DiscountsComponent: React.FC<{ data: Promotion["discounts"] }> = ({ data }) => ( +
+
+
+
+ Promo Type: {data.promoType}
+ Promo Value: {data.promoValue}
+ Applies To: {data.appliesTo?.join(", ") || "All"}
+ Minimum Quantity: {data.minimumQty ?? "None"} +
+
+
+
+ ); + return (row: any, data: Promotion["discounts"]) => ; + } + if (typeof element[key] == "object") { const DateComponent: React.FC<{ children?: React.ReactNode }> = ({ children, diff --git a/apps/cms/src/admin/views/MerchProducts.tsx b/apps/cms/src/admin/views/MerchProducts.tsx index fbfce61e..f9dfea9a 100644 --- a/apps/cms/src/admin/views/MerchProducts.tsx +++ b/apps/cms/src/admin/views/MerchProducts.tsx @@ -37,7 +37,8 @@ const MerchProducts: AdminView = ({ user, canAccessAdmin }) => { const sampleProduct = data[0]; const keys = Object.keys(sampleProduct); for (const key of keys) { - const renderCell: React.FC<{ children?: React.ReactNode }> = RenderCellFactory.get(sampleProduct, key); + const renderCell: React.FC<{ children?: React.ReactNode }> = + RenderCellFactory.get(sampleProduct, key); const col: Column = { accessor: key, components: { diff --git a/apps/cms/src/admin/views/MerchPromotion.scss b/apps/cms/src/admin/views/MerchPromotion.scss new file mode 100644 index 00000000..ccd39949 --- /dev/null +++ b/apps/cms/src/admin/views/MerchPromotion.scss @@ -0,0 +1,3 @@ +table { + width: 100%; +} \ No newline at end of file diff --git a/apps/cms/src/admin/views/MerchPromotion.tsx b/apps/cms/src/admin/views/MerchPromotion.tsx new file mode 100644 index 00000000..8c5ef3f6 --- /dev/null +++ b/apps/cms/src/admin/views/MerchPromotion.tsx @@ -0,0 +1,133 @@ +import React, { useEffect, useState } from "react"; +import { Button } from "payload/components/elements"; +import { AdminView } from "payload/config"; +import ViewTemplate from "./ViewTemplate"; +import { Column } from "payload/dist/admin/components/elements/Table/types"; +import { RenderCellFactory } from "../utils/RenderCellFactory"; +import SortedColumn from "../utils/SortedColumn"; +import { Table } from "payload/dist/admin/components/elements/Table"; +import { Promotion } from "types"; +import PromotionsApi from "../../apis/promotions.api"; +import './MerchPromotion.scss'; + +const MerchPromotion: AdminView = ({ user, canAccessAdmin }) => { + // Get data from API + const [data, setData] = useState(null); + useEffect(() => { + PromotionsApi.getPromotions() + .then((res: Promotion[]) => setData(res)) + .catch((error) => console.log(error)); + }, []); + + // Output human-readable table headers based on the attribute names from the API + function prettifyKey(str: string): string { + let res = ""; + for (const i of str.split("_")) { + res += i.charAt(0).toUpperCase() + i.slice(1) + " "; + } + return res; + } + + // Do not load table until we receive the data + if (data == null) { + return
Loading...
; + } + + const tableCols = new Array(); + if (data && data.length > 0) { + const sampleProduct = data[0]; + const keys = Object.keys(sampleProduct); + for (const key of keys) { + const renderCell: React.FC<{ children?: React.ReactNode }> = + RenderCellFactory.get(sampleProduct, key); + const col: Column = { + accessor: key, + components: { + Heading: ( + + ), + renderCell: renderCell, + }, + label: "", + name: "", + active: true, + }; + tableCols.push(col); + } + } + + const editColumn: Column = { + accessor: "edit", + components: { + Heading:
Edit
, + renderCell: ({ children }) => ( + + ), + }, + label: "Edit", + name: "edit", + active: true, + }; + + tableCols.push(editColumn); + + const deleteColumn: Column = { + accessor: "delete", + components: { + Heading:
Delete
, + renderCell: ({ children }) => ( + + ), + }, + label: "Delete", + name: "delete", + active: true, + }; + + tableCols.push(deleteColumn); + + const handleEdit = (promotionID: string) => { + console.log(`Dummy. Promotion ID: ${promotionID}`); + }; + + const handleDelete = (promotionID: string) => { + console.log(`Dummy. Promotion ID: ${promotionID}`); + }; + + const handleCreatePromotion = () => { + console.log("Creating a new promotion..."); + }; + + console.log(tableCols); + + return ( + + +
+ + +
+ +
+ + + + + ); +}; + +export default MerchPromotion; \ No newline at end of file diff --git a/apps/cms/src/admin/views/MerchSales.tsx b/apps/cms/src/admin/views/MerchSales.tsx index 81d2d3f2..db3679a8 100644 --- a/apps/cms/src/admin/views/MerchSales.tsx +++ b/apps/cms/src/admin/views/MerchSales.tsx @@ -10,14 +10,15 @@ import { RenderCellFactory } from "../utils/RenderCellFactory"; import SortedColumn from "../utils/SortedColumn"; import { Table } from "payload/dist/admin/components/elements/Table"; + const MerchSales: AdminView = ({ user, canAccessAdmin }) => { - // Get data from API - const [data, setData] = useState(null); - useEffect(() => { - OrdersApi.getOrders() - .then((res: IOrder[]) => setData(res)) - .catch((error) => console.log(error)); - }, []); + // Get data from API + const [data, setData] = useState(null); + useEffect(() => { + OrdersApi.getOrders() + .then((res: IOrder[]) => setData(res)) + .catch((error) => console.log(error)); + }, []); // Output human-readable table headers based on the attribute names from the API function prettifyKey(str: string): string { @@ -30,7 +31,7 @@ const MerchSales: AdminView = ({ user, canAccessAdmin }) => { // Do not load table until we receive the data if (data == null) { - return
Loading...
; + return
Loading...
; } const tableCols = new Array(); @@ -92,7 +93,7 @@ const MerchSales: AdminView = ({ user, canAccessAdmin }) => { const handleEdit = (orderId: string) => { console.log(`Dummy. Order ID: ${orderId}`); - }; + } const handleDelete = (orderId: string) => { console.log(`Dummy. Order ID: ${orderId}`); diff --git a/apps/cms/src/admin/views/ViewTemplate.tsx b/apps/cms/src/admin/views/ViewTemplate.tsx index f2e6adfe..aefb58c4 100644 --- a/apps/cms/src/admin/views/ViewTemplate.tsx +++ b/apps/cms/src/admin/views/ViewTemplate.tsx @@ -38,7 +38,7 @@ const ViewTemplate = ({ label: title, }, ]); - }, [setStepNav]); + }, [setStepNav, title]); // If an unauthorized user tries to navigate straight to this page, // Boot 'em out diff --git a/apps/cms/src/apis/promotions.api.ts b/apps/cms/src/apis/promotions.api.ts new file mode 100644 index 00000000..cc23fd83 --- /dev/null +++ b/apps/cms/src/apis/promotions.api.ts @@ -0,0 +1,35 @@ +import { Promotion } from "types"; +import { PromoType } from "types"; +// todo turn into real api +class PromotionsApi { + async getPromotions(): Promise { + const res: Promotion[] = [ + { + promoCode: "MARCHSALES", + maxRedemptions: 10, + redemptionsRemaining: 10, + discounts: { + promoType: PromoType.FIXED_VALUE, + promoValue: 5, + appliesTo: ["1"], + minimumQty: 1, + }, + }, + { + promoCode: "TSHIRTPROMO", + maxRedemptions: 10, + redemptionsRemaining: 10, + discounts: { + promoType: PromoType.FIXED_VALUE, + promoValue: 5, + appliesTo: ["1"], + minimumQty: 1, + }, + }, + ]; + + return Promise.resolve(res); + } +} + +export default new PromotionsApi(); diff --git a/apps/cms/src/payload.config.ts b/apps/cms/src/payload.config.ts index 63a5afce..85033c0b 100644 --- a/apps/cms/src/payload.config.ts +++ b/apps/cms/src/payload.config.ts @@ -14,6 +14,7 @@ import AfterNavLinks from "./admin/components/AfterNavLinks"; import MerchSales from "./admin/views/MerchSales"; import MerchOverview from "./admin/views/MerchOverview"; import MerchProducts from "./admin/views/MerchProducts"; +import MerchPromotion from "./admin/views/MerchPromotion"; import { SCSEIcon, SCSELogo } from "./admin/graphics/Logos"; import BeforeNavLinks from "./admin/components/BeforeNavLinks"; import Order from "./collections/Orders"; @@ -51,6 +52,10 @@ export default buildConfig({ path: "/merch/products", Component: MerchProducts, }, + { + path: "/merch/promotions", + Component: MerchPromotion, + }, ], beforeNavLinks: BeforeNavLinks, afterNavLinks: AfterNavLinks, @@ -90,13 +95,13 @@ export default buildConfig({ }, plugins: isUsingCloudStore() ? [ - cloudStorage({ - collections: { - media: { - adapter: adapter, + cloudStorage({ + collections: { + media: { + adapter: adapter, + }, }, - }, - }), - ] + }), + ] : [], }); diff --git a/apps/cms/src/types.ts b/apps/cms/src/types.ts index ece41843..7236123c 100644 --- a/apps/cms/src/types.ts +++ b/apps/cms/src/types.ts @@ -1,6 +1,7 @@ /* tslint:disable */ +/* eslint-disable */ /** - * This file was automatically generated by Payload CMS. + * This file was automatically generated by Payload. * DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config, * and re-run `payload generate:types` to regenerate this file. */ @@ -89,7 +90,7 @@ export interface User { name?: string; updatedAt: string; createdAt: string; - email?: string; + email: string; resetPasswordToken?: string; resetPasswordExpiration?: string; salt?: string; diff --git a/apps/web/features/merch/components/checkout/Skeleton.tsx b/apps/web/features/merch/components/checkout/Skeleton.tsx index 06c3e9d6..0aa6b5c3 100644 --- a/apps/web/features/merch/components/checkout/Skeleton.tsx +++ b/apps/web/features/merch/components/checkout/Skeleton.tsx @@ -14,6 +14,7 @@ const ItemSkeleton = () => ( ); + const CheckoutSkeleton: React.FC = () => { return ( { }; export default CheckoutSkeleton; + diff --git a/packages/eslint-config-custom/index.js b/packages/eslint-config-custom/index.js index c92d6e71..813bb43c 100644 --- a/packages/eslint-config-custom/index.js +++ b/packages/eslint-config-custom/index.js @@ -27,6 +27,7 @@ module.exports = { "@next/next/no-html-link-for-pages": "off", // react "react/jsx-key": "off", + "react/display-name": "off", // cypress "cypress/no-assigning-return-values": "error", "cypress/no-unnecessary-waiting": "error", diff --git a/packages/merch-helpers/src/lib/price.ts b/packages/merch-helpers/src/lib/price.ts index 02e61f58..86651a52 100644 --- a/packages/merch-helpers/src/lib/price.ts +++ b/packages/merch-helpers/src/lib/price.ts @@ -66,7 +66,8 @@ export const calculatePricing = ( discountedPrice: itemPrice, }; } - for (const discount of promotion.discounts) { + const discountsArray = promotion.discounts as unknown as Promotion["discounts"][]; + for (const discount of discountsArray) { if (discount.appliesTo && !discount.appliesTo.includes(item.id)) { continue; } diff --git a/packages/types/src/lib/merch.ts b/packages/types/src/lib/merch.ts index ac5ddfc8..ac03f82d 100644 --- a/packages/types/src/lib/merch.ts +++ b/packages/types/src/lib/merch.ts @@ -72,12 +72,12 @@ export interface Promotion { promoCode: string; maxRedemptions: number; redemptionsRemaining: number; - discounts: Array<{ + discounts: { promoType: PromoType; promoValue: number; // percent off or fixed value off based on promoType property - appliesTo?: Array; // array of product ids + appliesTo?: string[]; // array of product ids minimumQty?: number; // minimum quantity of items in the order to apply the discount - }>; + }; } export type PricedCart = {