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 = {