Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add merch promotions page #149

Merged
merged 25 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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" />;
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
134 changes: 134 additions & 0 deletions apps/cms/src/admin/views/MerchPromotion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
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";

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={{ position: "relative" }}>
<Button el="link" to={"/admin"} buttonStyle="primary">
Go to Main Admin View
</Button>
<div
style={{
position: "relative",
}}
>
<Button onClick={handleCreatePromotion} buttonStyle="primary">
Create Promotion
</Button>
</div>
</div>

<Table data={data} columns={tableCols} />
</ViewTemplate>
);
};

export default MerchPromotion;
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
Loading