Skip to content

Commit

Permalink
Add merch promotions page (#149)
Browse files Browse the repository at this point in the history
* Add merch promotions page
* Add button to create promotion
* Import Promotion class from types
* Fix linting issue

---------

Co-authored-by: limivann <71662324+limivann@users.noreply.github.com>
  • Loading branch information
spaceman03 and limivann authored Aug 28, 2024
1 parent 1e1f45c commit b9b6393
Show file tree
Hide file tree
Showing 16 changed files with 234 additions and 26 deletions.
5 changes: 5 additions & 0 deletions apps/cms/src/admin/components/AfterNavLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/cms/src/admin/graphics/Logos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ const SCSEImage = ({ classname }: SCSEImageProps) => {
};

export const SCSEIcon = () => <SCSEImage classname="scse-icon" />;
export const SCSELogo = () => <SCSEImage classname="scse-logo" />;
export const SCSELogo = () => <SCSEImage classname="scse-logo" />;
2 changes: 1 addition & 1 deletion apps/cms/src/admin/styles.scss
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
header {
}
}
20 changes: 20 additions & 0 deletions apps/cms/src/admin/utils/RenderCellFactory.tsx
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -103,6 +104,25 @@ export class RenderCellFactory {
};
}

if (key === "discounts" && typeof element[key] === "object") {
// Render discounts
const DiscountsComponent: React.FC<{ data: Promotion["discounts"] }> = ({ data }) => (
<div className="discount-container">
<div className="discount-item">
<div>
<div>
<strong>Promo Type:</strong> {data.promoType} <br />
<strong>Promo Value:</strong> {data.promoValue} <br />
<strong>Applies To:</strong> {data.appliesTo?.join(", ") || "All"} <br />
<strong>Minimum Quantity:</strong> {data.minimumQty ?? "None"}
</div>
</div>
</div>
</div>
);
return (row: any, data: Promotion["discounts"]) => <DiscountsComponent data={data} />;
}

if (typeof element[key] == "object") {
const DateComponent: React.FC<{ children?: React.ReactNode }> = ({
children,
Expand Down
3 changes: 2 additions & 1 deletion apps/cms/src/admin/views/MerchProducts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
3 changes: 3 additions & 0 deletions apps/cms/src/admin/views/MerchPromotion.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
table {
width: 100%;
}
133 changes: 133 additions & 0 deletions apps/cms/src/admin/views/MerchPromotion.tsx
Original file line number Diff line number Diff line change
@@ -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<Promotion[]>(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 <div> Loading... </div>;
}

const tableCols = new Array<Column>();
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: (
<SortedColumn
label={prettifyKey(key)}
name={key}
data={data as never[]}
/>
),
renderCell: renderCell,
},
label: "",
name: "",
active: true,
};
tableCols.push(col);
}
}

const editColumn: Column = {
accessor: "edit",
components: {
Heading: <div>Edit</div>,
renderCell: ({ children }) => (
<Button onClick={() => handleEdit(children as string)}>Edit</Button>
),
},
label: "Edit",
name: "edit",
active: true,
};

tableCols.push(editColumn);

const deleteColumn: Column = {
accessor: "delete",
components: {
Heading: <div>Delete</div>,
renderCell: ({ children }) => (
<Button onClick={() => handleDelete(children as string)}>Delete</Button>
),
},
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 (
<ViewTemplate
user={user}
canAccessAdmin={canAccessAdmin}
description=""
keywords=""
title="Merchandise Promotion"
>

<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Button el="link" to={"/admin"} buttonStyle="primary">
Go to Main Admin View
</Button>
<Button onClick={handleCreatePromotion} buttonStyle="primary">
Create Promotion
</Button>
</div>

<div className="table">
<Table data={data} columns={tableCols}/>
</div>

</ViewTemplate>
);
};

export default MerchPromotion;
19 changes: 10 additions & 9 deletions apps/cms/src/admin/views/MerchSales.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<IOrder[]>(null);
useEffect(() => {
OrdersApi.getOrders()
.then((res: IOrder[]) => setData(res))
.catch((error) => console.log(error));
}, []);
// Get data from API
const [data, setData] = useState<IOrder[]>(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 {
Expand All @@ -30,7 +31,7 @@ const MerchSales: AdminView = ({ user, canAccessAdmin }) => {

// Do not load table until we receive the data
if (data == null) {
return <div> Loading... </div>;
return <div>Loading...</div>;
}

const tableCols = new Array<Column>();
Expand Down Expand Up @@ -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}`);
Expand Down
2 changes: 1 addition & 1 deletion apps/cms/src/admin/views/ViewTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions apps/cms/src/apis/promotions.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Promotion } from "types";
import { PromoType } from "types";
// todo turn into real api
class PromotionsApi {
async getPromotions(): Promise<Promotion[]> {
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();
19 changes: 12 additions & 7 deletions apps/cms/src/payload.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -51,6 +52,10 @@ export default buildConfig({
path: "/merch/products",
Component: MerchProducts,
},
{
path: "/merch/promotions",
Component: MerchPromotion,
},
],
beforeNavLinks: BeforeNavLinks,
afterNavLinks: AfterNavLinks,
Expand Down Expand Up @@ -90,13 +95,13 @@ export default buildConfig({
},
plugins: isUsingCloudStore()
? [
cloudStorage({
collections: {
media: {
adapter: adapter,
cloudStorage({
collections: {
media: {
adapter: adapter,
},
},
},
}),
]
}),
]
: [],
});
5 changes: 3 additions & 2 deletions apps/cms/src/types.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
Expand Down Expand Up @@ -89,7 +90,7 @@ export interface User {
name?: string;
updatedAt: string;
createdAt: string;
email?: string;
email: string;
resetPasswordToken?: string;
resetPasswordExpiration?: string;
salt?: string;
Expand Down
2 changes: 2 additions & 0 deletions apps/web/features/merch/components/checkout/Skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const ItemSkeleton = () => (
<SkeletonText noOfLines={2} spacing="4" w="100%" />
</Flex>
);

const CheckoutSkeleton: React.FC = () => {
return (
<Grid
Expand Down Expand Up @@ -42,3 +43,4 @@ const CheckoutSkeleton: React.FC = () => {
};

export default CheckoutSkeleton;

1 change: 1 addition & 0 deletions packages/eslint-config-custom/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion packages/merch-helpers/src/lib/price.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
6 changes: 3 additions & 3 deletions packages/types/src/lib/merch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>; // 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 = {
Expand Down

0 comments on commit b9b6393

Please sign in to comment.