diff --git a/.gitignore b/.gitignore
index bcd7db157..6f75994e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ node_modules
.env.secrets
.env.site-configs
coverage
+.claude
\ No newline at end of file
diff --git a/admin/src/common/MasterMenu.tsx b/admin/src/common/MasterMenu.tsx
index a139d9119..a266ccaf1 100644
--- a/admin/src/common/MasterMenu.tsx
+++ b/admin/src/common/MasterMenu.tsx
@@ -1,4 +1,4 @@
-import { Assets, Dashboard, PageTree, Snips, Wrench } from "@comet/admin-icons";
+import { Assets, Dashboard, PageTree, Snips, Tag, Wrench } from "@comet/admin-icons";
import {
ContentScopeIndicator,
createRedirectsPage,
@@ -14,6 +14,7 @@ import { DashboardPage } from "@src/dashboard/DashboardPage";
import { Link } from "@src/documents/links/Link";
import { Page } from "@src/documents/pages/Page";
import { EditFooterPage } from "@src/footers/EditFooterPage";
+import { ProductsPage } from "@src/products/ProductsPage";
import { FormattedMessage } from "react-intl";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -50,6 +51,16 @@ export const masterMenuData: MasterMenuData = [
},
requiredPermission: "pageTree",
},
+ {
+ type: "route",
+ primary: ,
+ icon: ,
+ route: {
+ path: "/products",
+ component: ProductsPage,
+ },
+ requiredPermission: "products",
+ },
{
type: "route",
primary: ,
diff --git a/admin/src/common/components/enums/chipIcon/ChipIcon.tsx b/admin/src/common/components/enums/chipIcon/ChipIcon.tsx
new file mode 100644
index 000000000..639d1ba8e
--- /dev/null
+++ b/admin/src/common/components/enums/chipIcon/ChipIcon.tsx
@@ -0,0 +1,19 @@
+import { ChevronDown, ThreeDotSaving } from "@comet/admin-icons";
+import { type ChipProps } from "@mui/material";
+import { type FunctionComponent } from "react";
+
+type ChipIconProps = {
+ loading: boolean;
+ onClick?: ChipProps["onClick"];
+};
+
+export const ChipIcon: FunctionComponent = ({ loading, onClick }) => {
+ if (loading) {
+ return ;
+ }
+ if (onClick) {
+ return ;
+ }
+
+ return null;
+};
diff --git a/admin/src/common/components/enums/createTranslatableEnum/createTranslatableEnum.tsx b/admin/src/common/components/enums/createTranslatableEnum/createTranslatableEnum.tsx
new file mode 100644
index 000000000..22700b9ae
--- /dev/null
+++ b/admin/src/common/components/enums/createTranslatableEnum/createTranslatableEnum.tsx
@@ -0,0 +1,32 @@
+import type { FunctionComponent, ReactNode } from "react";
+import { FormattedMessage, type MessageDescriptor } from "react-intl";
+
+type ComponentProps = {
+ value: T;
+};
+type TranslatableEnumResult = {
+ Component: FunctionComponent>;
+ messageDescriptorMap: Record;
+ formattedMessageMap: Record;
+};
+
+export function createTranslatableEnum(messageDescriptorMap: Record): TranslatableEnumResult {
+ const formattedMessageMap = Object.keys(messageDescriptorMap).reduce(
+ (acc, key) => {
+ const k = key as T;
+ acc[k] = ;
+ return acc;
+ },
+ {} as Record,
+ );
+
+ const Component: FunctionComponent<{ value: T }> = ({ value }) => {
+ return formattedMessageMap[value];
+ };
+
+ return {
+ Component,
+ messageDescriptorMap,
+ formattedMessageMap,
+ };
+}
diff --git a/admin/src/common/components/enums/enumChip/EnumChip.tsx b/admin/src/common/components/enums/enumChip/EnumChip.tsx
new file mode 100644
index 000000000..c2eb8c35e
--- /dev/null
+++ b/admin/src/common/components/enums/enumChip/EnumChip.tsx
@@ -0,0 +1,50 @@
+import { type ChipProps as MuiChipProps, ListItemText, Menu, MenuItem } from "@mui/material";
+import { type ComponentType, type ReactNode, useState } from "react";
+
+export type EnumChipProps = {
+ chipMap: EnumChipMap;
+ formattedMessageMap: EnumFormattedMessageMap;
+ loading?: boolean;
+ onSelectItem?: (status: T) => void;
+ sortOrder?: T[];
+ value: T;
+};
+type EnumChipItemProps = { loading: boolean; onClick?: MuiChipProps["onClick"] };
+type EnumChipMap = Record>;
+type EnumFormattedMessageMap = Record;
+
+function keysFromObject(obj: T): (keyof T)[] {
+ return Object.keys(obj) as (keyof T)[];
+}
+
+export function EnumChip({ chipMap, formattedMessageMap, loading = false, onSelectItem, sortOrder = [], value }: EnumChipProps) {
+ const [anchorElement, setAnchorElement] = useState(null);
+
+ const StatusChip: ComponentType = chipMap[value];
+ const open = !!anchorElement;
+
+ return (
+ <>
+ setAnchorElement(event.currentTarget) : undefined} />
+
+
+ >
+ );
+}
diff --git a/admin/src/common/components/enums/messageDescriptorMapToValueOptions/messageDescriptorMapToValueOptions.ts b/admin/src/common/components/enums/messageDescriptorMapToValueOptions/messageDescriptorMapToValueOptions.ts
new file mode 100644
index 000000000..6c4f93f89
--- /dev/null
+++ b/admin/src/common/components/enums/messageDescriptorMapToValueOptions/messageDescriptorMapToValueOptions.ts
@@ -0,0 +1,13 @@
+import { type IntlShape, type MessageDescriptor } from "react-intl";
+
+type ValueOption = {
+ value: T;
+ label: string;
+};
+
+export function messageDescriptorMapToValueOptions(map: Record, intl: IntlShape): Array> {
+ return (Object.entries(map) as Array<[T, MessageDescriptor]>).map(([value, descriptor]) => ({
+ value,
+ label: intl.formatMessage(descriptor),
+ }));
+}
diff --git a/admin/src/common/components/enums/recordToOptions/recordToOptions.ts b/admin/src/common/components/enums/recordToOptions/recordToOptions.ts
new file mode 100644
index 000000000..8833e7bad
--- /dev/null
+++ b/admin/src/common/components/enums/recordToOptions/recordToOptions.ts
@@ -0,0 +1,13 @@
+import { type ReactNode } from "react";
+
+type Option = {
+ value: T;
+ label: ReactNode;
+};
+
+export function recordToOptions(record: Record): Array