Skip to content

Commit

Permalink
Fix/merch products (#139)
Browse files Browse the repository at this point in the history
* 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 <zhixuanchung03@gmail.com>
  • Loading branch information
3 people committed Jan 31, 2024
1 parent e31d02b commit c843e9f
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 24 deletions.
118 changes: 99 additions & 19 deletions apps/cms/src/admin/utils/RenderCellFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}) => (
<span>
{children.map((imageUrl: string, index: number) => (
<img
key={index}
src={imageUrl}
alt={`image ${index + 1}`}
style={{ paddingTop: 10, paddingBottom: 10 }}
/>
))}
</span>
);
const ImagesComponentCell = (row, data) => (
<ImagesComponent>{data}</ImagesComponent>
);
return ImagesComponentCell;
} else {
// If the element is an array of strings, render them
const StringsComponent: React.FC<{ children?: React.ReactNode[] }> = ({
children,
}) => (
<span>
{children.map((text: string, index: number) => (
<span key={index}>
{index > 0 && ", "} {text}
</span>
))}
</span>
);
const StringsComponentCell = (row, data) => (
<StringsComponent>{data}</StringsComponent>
);
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,
}) => (
<span>
<img src={children.toString()} alt="image of object"/>
<img src={children.toString()} alt="image of object" />
</span>
);
const ImageComponentCell = (row, data) => <ImageComponent>{data}</ImageComponent>;
const ImageComponentCell = (row, data) => (
<ImageComponent>{data}</ImageComponent>
);
return ImageComponentCell;
}
if (key === "stock") {
const ObjectComponent: React.FC<{ data: string }> = ({ data }) => (
<div>
{Object.entries(data).map(([subKey, value], index) => (
<div key={index}>
<strong>{subKey}:</strong>{" "}
<span>
{typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)}
</span>
</div>
))}
</div>
);
const ObjectComponentCell = (row, data: string) => (
<ObjectComponent data={data} />
);
return ObjectComponentCell;

}
if (typeof element[key] == "object") {
const DateComponent: React.FC<{ children?: React.ReactNode }> = ({
children,
}) => <span>{(children as unknown as Date).toDateString()}</span>;
const DateComponentCell = (row, data) => (
<DateComponent>{data}</DateComponent>
);
return DateComponentCell;
}

if (typeof element[key] == 'object') {
const DateComponent: React.FC<{children?: React.ReactNode}> = ({ children }) => (
<span>
{(children as unknown as Date).toDateString()}
</span>
if (typeof element[key] === "boolean") {
// If the element is a boolean, render "Yes" or "No"
const BooleanComponent: React.FC<{ children?: React.ReactNode }> = ({
children,
}) => <span>{children ? "Yes" : "No"}</span>;
const BooleanComponentCell = (row, data) => (
<BooleanComponent>{data}</BooleanComponent>
);
const DateComponentCell = (row, data) => <DateComponent>{data}</DateComponent>;
return DateComponentCell
return BooleanComponentCell;
}

const TextComponent: React.FC<{children?: React.ReactNode}> = ({ children }) => (
<span>
{children}
</span>
const TextComponent: React.FC<{ children?: React.ReactNode }> = ({
children,
}) => <span>{children}</span>;
const TextComponentCell = (row, data) => (
<TextComponent>{data}</TextComponent>
);
const TextComponentCell = (row, data) => <TextComponent>{data}</TextComponent>;
return TextComponentCell
return TextComponentCell;
}
}
102 changes: 97 additions & 5 deletions apps/cms/src/admin/views/MerchProducts.tsx
Original file line number Diff line number Diff line change
@@ -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<Product[]>(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 <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 = (orderId: string) => {
console.log(`Dummy. Order ID: ${orderId}`);
};

const handleDelete = (orderId: string) => {
console.log(`Dummy. Order ID: ${orderId}`);
};

console.log(tableCols);

return (
<ViewTemplate
user={user}
Expand All @@ -12,13 +106,11 @@ const MerchProducts: AdminView = ({ user, canAccessAdmin }) => {
keywords=""
title="Merchandise Products"
>
<p>
Here is a custom route that was added in the Payload config. It uses the
Default Template, so the sidebar is rendered.
</p>
<Button el="link" to={"/admin"} buttonStyle="primary">
Go to Main Admin View
</Button>

<Table data={data} columns={tableCols} />
</ViewTemplate>
);
};
Expand Down
64 changes: 64 additions & 0 deletions apps/cms/src/apis/products.api.ts
Original file line number Diff line number Diff line change
@@ -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<any> {
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();

0 comments on commit c843e9f

Please sign in to comment.