From 5d322e0b85f97347a503e2f6ba60113d2bbf4013 Mon Sep 17 00:00:00 2001 From: iyzyman <101888183+Iyzyman@users.noreply.github.com> Date: Mon, 29 Jan 2024 21:45:32 +0800 Subject: [PATCH] Fix/merch products (#139) * Added merch products page * fix linting issues * Added merch products page * fix linting issues * Add delete and edit buttons for product page * Using type definition from merch.ts * Fix linting errors * Fix linting errors * import Product from types --------- Co-authored-by: LimIvan336 <71662324+LimIvan336@users.noreply.github.com> Co-authored-by: Chung Zhi Xuan --- .../cms/src/admin/utils/RenderCellFactory.tsx | 118 +++++++++++++++--- apps/cms/src/admin/views/MerchProducts.tsx | 102 ++++++++++++++- apps/cms/src/apis/products.api.ts | 64 ++++++++++ 3 files changed, 260 insertions(+), 24 deletions(-) create mode 100644 apps/cms/src/apis/products.api.ts diff --git a/apps/cms/src/admin/utils/RenderCellFactory.tsx b/apps/cms/src/admin/utils/RenderCellFactory.tsx index 97ec1ef0..1216fcc9 100644 --- a/apps/cms/src/admin/utils/RenderCellFactory.tsx +++ b/apps/cms/src/admin/utils/RenderCellFactory.tsx @@ -2,42 +2,122 @@ import React from "react"; import payload from "payload"; export class RenderCellFactory { - static get(element: unknown, key: string) { - console.log(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 (key === "stock") { + const ObjectComponent: React.FC<{ data: string }> = ({ data }) => ( +
+ {Object.entries(data).map(([subKey, value], index) => ( +
+ {subKey}:{" "} + + {typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)} + +
+ ))} +
+ ); + const ObjectComponentCell = (row, data: string) => ( + + ); + return ObjectComponentCell; + + } + 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] == 'object') { - const DateComponent: React.FC<{children?: React.ReactNode}> = ({ children }) => ( - - {(children as unknown as Date).toDateString()} - + 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..fbfce61e 100644 --- a/apps/cms/src/admin/views/MerchProducts.tsx +++ b/apps/cms/src/admin/views/MerchProducts.tsx @@ -1,9 +1,103 @@ -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"; +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: Product[]) => 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 = (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/apis/products.api.ts b/apps/cms/src/apis/products.api.ts new file mode 100644 index 00000000..62e10ef7 --- /dev/null +++ b/apps/cms/src/apis/products.api.ts @@ -0,0 +1,64 @@ +import { Product } from "types"; +// todo turn into real api +class ProductsApi { + // eslint-disable-next-line @typescript-eslint/require-await + async getProducts(): Promise { + const res: Product[] = [ + { + 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"], + category: "shirt", + is_available: true, + colors: ["black,white,blue"], + stock: { + black: { S: 10, M: 15, L: 20, XL: 5 }, + white: { S: 12, M: 17, L: 22, XL: 7 }, + blue: { S: 8, M: 13, L: 18, XL: 3 } + }, + }, + { + 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"], + category: "sweater", + is_available: true, + colors: ["blue"], + stock: { + blue: { S: 8, M: 13, L: 18, XL: 3 } + }, + }, + { + 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"], + category: "hat", + is_available: false, + colors: ["white"], + stock: { + white: { S: 12, M: 17, L: 22, XL: 7 } + }, + }, + ]; + + return res; + } +} + +export default new ProductsApi();