diff --git a/apps/cms/src/@types/IProduct.ts b/apps/cms/src/@types/IProduct.ts new file mode 100644 index 00000000..2c1f7f54 --- /dev/null +++ b/apps/cms/src/@types/IProduct.ts @@ -0,0 +1,3 @@ +import { Product } from "./Product"; + +export interface IProduct extends Product {} diff --git a/apps/cms/src/@types/Product.ts b/apps/cms/src/@types/Product.ts new file mode 100644 index 00000000..5b92fbb0 --- /dev/null +++ b/apps/cms/src/@types/Product.ts @@ -0,0 +1,24 @@ +export class Product { + constructor( + public id: string = "", + public name: string = "", + public price: number = 0, // Price of the product in cents ($19 = 1900). + public images: string[] = [], // URL of the images. + public sizes: ProductSizeTypes[] = [], + public productCategory: ProductCategoryType = "", + public isAvailable?: boolean + ) {} +} + +export type ProductSizeTypes = + | "3xs" + | "xxs" + | "xs" + | "s" + | "m" + | "l" + | "xl" + | "2xl" + | "3xl"; + +export type ProductCategoryType = string; diff --git a/apps/cms/src/admin/utils/RenderCellFactory.tsx b/apps/cms/src/admin/utils/RenderCellFactory.tsx index 97ec1ef0..e2e133af 100644 --- a/apps/cms/src/admin/utils/RenderCellFactory.tsx +++ b/apps/cms/src/admin/utils/RenderCellFactory.tsx @@ -2,42 +2,105 @@ import React from "react"; import payload from "payload"; export class RenderCellFactory { - static get(element: unknown, key: string) { - console.log(key) + console.log("key", key); if (element[key] == undefined) { // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - payload.logger.error(`Attribute ${key} cannot be found in element ${element.toString()}`); + payload.logger.error( + `Attribute ${key} cannot be found in element ${element.toString()}` + ); return null; } const isImageUrl = new RegExp("http(s?):\\/\\/.*.(jpg|png|jpeg)$"); + + if (Array.isArray(element[key])) { + if ( + (element[key] as string[]).every((item: string) => + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + isImageUrl.test((item as string).toString()) + ) + ) { + // If the element is an array, render images accordingly + const ImagesComponent: React.FC<{ children?: React.ReactNode[] }> = ({ + children, + }) => ( + + {children.map((imageUrl: string, index: number) => ( + {`image + ))} + + ); + const ImagesComponentCell = (row, data) => ( + {data} + ); + return ImagesComponentCell; + } else { + // If the element is an array of strings, render them + const StringsComponent: React.FC<{ children?: React.ReactNode[] }> = ({ + children, + }) => ( + + {children.map((text: string, index: number) => ( + + {index > 0 && ", "} {text} + + ))} + + ); + const StringsComponentCell = (row, data) => ( + {data} + ); + return StringsComponentCell; + } + } + if (isImageUrl.test((element[key] as string).toString())) { - const ImageComponent: React.FC<{children?: React.ReactNode}> = ({ children }) => ( + const ImageComponent: React.FC<{ children?: React.ReactNode }> = ({ + children, + }) => ( - image of object + image of object ); - const ImageComponentCell = (row, data) => {data}; + const ImageComponentCell = (row, data) => ( + {data} + ); return ImageComponentCell; } - if (typeof element[key] == 'object') { - const DateComponent: React.FC<{children?: React.ReactNode}> = ({ children }) => ( - - {(children as unknown as Date).toDateString()} - + if (typeof element[key] == "object") { + const DateComponent: React.FC<{ children?: React.ReactNode }> = ({ + children, + }) => {(children as unknown as Date).toDateString()}; + const DateComponentCell = (row, data) => ( + {data} + ); + return DateComponentCell; + } + + if (typeof element[key] === "boolean") { + // If the element is a boolean, render "Yes" or "No" + const BooleanComponent: React.FC<{ children?: React.ReactNode }> = ({ + children, + }) => {children ? "Yes" : "No"}; + const BooleanComponentCell = (row, data) => ( + {data} ); - const DateComponentCell = (row, data) => {data}; - return DateComponentCell + return BooleanComponentCell; } - const TextComponent: React.FC<{children?: React.ReactNode}> = ({ children }) => ( - - {children} - + const TextComponent: React.FC<{ children?: React.ReactNode }> = ({ + children, + }) => {children}; + const TextComponentCell = (row, data) => ( + {data} ); - const TextComponentCell = (row, data) => {data}; - return TextComponentCell + return TextComponentCell; } } diff --git a/apps/cms/src/admin/views/MerchProducts.tsx b/apps/cms/src/admin/views/MerchProducts.tsx index 07faa685..651d071f 100644 --- a/apps/cms/src/admin/views/MerchProducts.tsx +++ b/apps/cms/src/admin/views/MerchProducts.tsx @@ -1,9 +1,102 @@ -import React from "react"; +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 { Product } from "../../@types/Product"; +import { IProduct } from "../../@types/IProduct"; +import ProductsApi from "../../apis/products.api"; const MerchProducts: AdminView = ({ user, canAccessAdmin }) => { + // Get data from API + const [data, setData] = useState(null); + useEffect(() => { + ProductsApi.getProducts() + .then((res: IProduct[]) => 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(); + for (const key of Object.keys(new Product())) { + const renderCell: React.FC<{ children?: React.ReactNode }> = + RenderCellFactory.get(data[0], 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 = (orderId: string) => { + console.log(`Dummy. Order ID: ${orderId}`); + }; + + const handleDelete = (orderId: string) => { + console.log(`Dummy. Order ID: ${orderId}`); + }; + + console.log(tableCols); + return ( { keywords="" title="Merchandise Products" > -

- Here is a custom route that was added in the Payload config. It uses the - Default Template, so the sidebar is rendered. -

+ + ); }; diff --git a/apps/cms/src/admin/views/MerchSales.tsx b/apps/cms/src/admin/views/MerchSales.tsx index a809cc2f..81d2d3f2 100644 --- a/apps/cms/src/admin/views/MerchSales.tsx +++ b/apps/cms/src/admin/views/MerchSales.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; -import { Button } from 'payload/components/elements'; -import { AdminView } from 'payload/config'; +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 { Order } from "../../@types/Order"; @@ -11,34 +11,35 @@ 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) - ) + .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 { let res = ""; - for (const i of str.split('_')) { - res += i.charAt(0).toUpperCase() + i.slice(1) + " " + 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...
+ if (data == null) { + return
Loading...
; } const tableCols = new Array(); for (const key of Object.keys(new Order())) { - const renderCell: React.FC<{children?: React.ReactNode}> = RenderCellFactory.get(data[0], key); + const renderCellComponent = RenderCellFactory.get(data[0], key); + const renderCell: React.FC<{ children?: React.ReactNode }> = + renderCellComponent instanceof Promise + ? renderCellComponent + : renderCellComponent; const col: Column = { accessor: key, @@ -47,38 +48,73 @@ const MerchSales: AdminView = ({ user, canAccessAdmin }) => { + data={data as never[]} + /> ), - renderCell: renderCell + renderCell: renderCell, }, label: "", name: "", - active: true - } + active: true, + }; tableCols.push(col); } - console.log(tableCols) + + 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 = (orderId: string) => { + console.log(`Dummy. Order ID: ${orderId}`); + }; + + const handleDelete = (orderId: string) => { + console.log(`Dummy. Order ID: ${orderId}`); + }; + + console.log(tableCols); return ( - - - -
+ - +
+ ); }; -export default MerchSales +export default MerchSales; diff --git a/apps/cms/src/apis/orders.api.ts b/apps/cms/src/apis/orders.api.ts index d8d44d47..a18160f4 100644 --- a/apps/cms/src/apis/orders.api.ts +++ b/apps/cms/src/apis/orders.api.ts @@ -3,45 +3,48 @@ import { IOrder } from "../@types/IOrder"; // todo turn into real api class OrdersApi { // eslint-disable-next-line @typescript-eslint/require-await - async getOrders(): Promise { - const res = [] + async getOrders(): Promise { + const res: IOrder[] = []; const item1: IOrder = { colour: "black", date: new Date("2022-01-31"), - image_url: "https://i.kym-cdn.com/entries/icons/original/000/033/421/cover2.jpg", + image_url: + "https://i.kym-cdn.com/entries/icons/original/000/033/421/cover2.jpg", item: "graduation hat", order_id: "1", order_person: "kenneth west", qty: 2, - size: "M" - } + size: "M", + }; res.push(item1); const item2: IOrder = { colour: "white", date: new Date("2022-02-13"), - image_url: "https://i.kym-cdn.com/photos/images/newsfeed/002/164/493/b8b.jpg", + image_url: + "https://i.kym-cdn.com/photos/images/newsfeed/002/164/493/b8b.jpg", item: "scorpion", order_id: "2", order_person: "aubrey graham drake", qty: 1, - size: "L" - } + size: "L", + }; res.push(item2); const item3: IOrder = { colour: "beige", date: new Date("2010-02-13"), - image_url: "https://i.pinimg.com/474x/c0/f9/f1/c0f9f10a0061a8dd1080d7d9e560579c.jpg", + image_url: + "https://i.pinimg.com/474x/c0/f9/f1/c0f9f10a0061a8dd1080d7d9e560579c.jpg", item: "dat stick", order_id: "3", order_person: "rich brian", qty: 1, - size: "S" - } + size: "S", + }; res.push(item3); - return res as IOrder[]; + return res; } } diff --git a/apps/cms/src/apis/products.api.ts b/apps/cms/src/apis/products.api.ts new file mode 100644 index 00000000..b54a8ddc --- /dev/null +++ b/apps/cms/src/apis/products.api.ts @@ -0,0 +1,51 @@ +import { IProduct } from "../@types/IProduct"; + +// todo turn into real api +class ProductsApi { + // eslint-disable-next-line @typescript-eslint/require-await + async getProducts(): Promise { + const res: IProduct[] = [ + { + id: "1", + name: "product1", + price: 1000, + images: [ + "https://i.kym-cdn.com/entries/icons/original/000/033/421/cover2.jpg", + "https://i.pinimg.com/474x/c0/f9/f1/c0f9f10a0061a8dd1080d7d9e560579c.jpg", + ], + sizes: ["s", "m", "l", "xl"], + productCategory: "shirt", + isAvailable: true, + }, + { + id: "2", + name: "product2", + price: 2000, + images: [ + "https://i.kym-cdn.com/photos/images/newsfeed/002/164/493/b8b.jpg", + "https://i.kym-cdn.com/entries/icons/original/000/033/421/cover2.jpg", + ], + sizes: ["s", "m"], + productCategory: "sweater", + isAvailable: true, + }, + { + id: "3", + name: "product3", + price: 3000, + images: [ + "https://i.kym-cdn.com/entries/icons/original/000/033/421/cover2.jpg", + "https://i.kym-cdn.com/photos/images/newsfeed/002/164/493/b8b.jpg", + "https://i.pinimg.com/474x/c0/f9/f1/c0f9f10a0061a8dd1080d7d9e560579c.jpg", + ], + sizes: ["xs", "s", "m", "l"], + productCategory: "hat", + isAvailable: false, + }, + ]; + + return res; + } +} + +export default new ProductsApi();