diff --git a/.gitignore b/.gitignore index 3597b2d..b4ac71c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ vitest.config.*.timestamp* .nx .vscode + +.cursor/rules/nx-rules.mdc +.github/instructions/nx.instructions.md diff --git a/apps/be-api/src/app/app.ts b/apps/be-api/src/app/app.ts index 9e8ef2f..5c6162c 100644 --- a/apps/be-api/src/app/app.ts +++ b/apps/be-api/src/app/app.ts @@ -6,7 +6,6 @@ import * as path from 'path'; export interface AppOptions {} export async function app(fastify: FastifyInstance, opts: AppOptions) { - fastify.register(AutoLoad, { dir: path.join(__dirname, 'plugins'), options: { ...opts }, diff --git a/apps/fe-admin/src/components/ActionButtons/ActionButtons.module.scss b/apps/fe-admin/src/components/ActionButtons/ActionButtons.module.scss new file mode 100644 index 0000000..3330f28 --- /dev/null +++ b/apps/fe-admin/src/components/ActionButtons/ActionButtons.module.scss @@ -0,0 +1,4 @@ +.ActionButtons { + display: flex; + gap: 10px; +} diff --git a/apps/fe-admin/src/components/ActionButtons/ActionButtons.tsx b/apps/fe-admin/src/components/ActionButtons/ActionButtons.tsx new file mode 100644 index 0000000..af787f6 --- /dev/null +++ b/apps/fe-admin/src/components/ActionButtons/ActionButtons.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { MdDelete, MdEdit } from 'react-icons/md'; +import { CustomButton } from '@shopery/ui-shared'; +import styles from './ActionButtons.module.scss'; + +type ActionButtonsProps = { + onEdit?: () => void; + onDelete?: () => void; +}; + +const ActionButtons = ({ onEdit, onDelete }: ActionButtonsProps) => { + return ( +
+ + + + + + +
+ ); +}; + +export default ActionButtons; diff --git a/apps/fe-admin/src/components/User/User.tsx b/apps/fe-admin/src/components/User/User.tsx index f9703d9..f6c3544 100644 --- a/apps/fe-admin/src/components/User/User.tsx +++ b/apps/fe-admin/src/components/User/User.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Button } from '@shopery/ui-shared'; +import { CustomButton } from '@shopery/ui-shared'; import styles from './User.module.scss'; import { User } from '../../types'; import UserAvatarIcon from '@assets/icons/userAvatar.svg?react'; @@ -15,9 +15,9 @@ const UserComponent: React.FC = ({ user }) => {

{user.name}

- + ); }; diff --git a/apps/fe-admin/src/components/index.ts b/apps/fe-admin/src/components/index.ts index faac05a..018097f 100644 --- a/apps/fe-admin/src/components/index.ts +++ b/apps/fe-admin/src/components/index.ts @@ -1 +1,2 @@ export { default as Sidebar } from './Sidebar/Sidebar'; +export { default as ActionsButtons } from './ActionButtons/ActionButtons'; diff --git a/apps/fe-admin/src/data/OrderTableData.ts b/apps/fe-admin/src/data/OrderTableData.ts new file mode 100644 index 0000000..5f827ae --- /dev/null +++ b/apps/fe-admin/src/data/OrderTableData.ts @@ -0,0 +1,86 @@ +export const dataSource = [ + { + id: 1, + userId: 101, + totalNumberOfProducts: 2, + totalPrice: 50, + status: 'pending', + }, + { + id: 2, + userId: 102, + totalNumberOfProducts: 3, + totalPrice: 75, + status: 'shipped', + }, + { + id: 3, + userId: 103, + totalNumberOfProducts: 1, + totalPrice: 25, + status: 'delivered', + }, + { + id: 4, + userId: 104, + totalNumberOfProducts: 5, + totalPrice: 100, + status: 'pending', + }, + { + id: 5, + userId: 105, + totalNumberOfProducts: 4, + totalPrice: 80, + status: 'shipped', + }, + { + id: 6, + userId: 106, + totalNumberOfProducts: 2, + totalPrice: 60, + status: 'delivered', + }, + { + id: 7, + userId: 107, + totalNumberOfProducts: 3, + totalPrice: 90, + status: 'pending', + }, + { + id: 8, + userId: 108, + totalNumberOfProducts: 1, + totalPrice: 30, + status: 'shipped', + }, + { + id: 9, + userId: 109, + totalNumberOfProducts: 6, + totalPrice: 120, + status: 'delivered', + }, + { + id: 10, + userId: 110, + totalNumberOfProducts: 2, + totalPrice: 40, + status: 'pending', + }, + { + id: 11, + userId: 111, + totalNumberOfProducts: 3, + totalPrice: 70, + status: 'shipped', + }, + { + id: 12, + userId: 112, + totalNumberOfProducts: 4, + totalPrice: 90, + status: 'delivered', + }, +]; diff --git a/apps/fe-admin/src/data/index.ts b/apps/fe-admin/src/data/index.ts new file mode 100644 index 0000000..13410dd --- /dev/null +++ b/apps/fe-admin/src/data/index.ts @@ -0,0 +1 @@ +export { dataSource } from './OrderTableData'; diff --git a/apps/fe-admin/src/layouts/index.ts b/apps/fe-admin/src/layouts/index.ts index 39f6125..2aeeb85 100644 --- a/apps/fe-admin/src/layouts/index.ts +++ b/apps/fe-admin/src/layouts/index.ts @@ -1 +1 @@ -export { default as PageLayout } from './pageLayout/PageLayout'; +export { default as PageLayout } from './pageLayout/pageLayout'; diff --git a/apps/fe-admin/src/pages/DashboardPage.tsx b/apps/fe-admin/src/pages/DashboardPage.tsx index f703a91..1b33506 100644 --- a/apps/fe-admin/src/pages/DashboardPage.tsx +++ b/apps/fe-admin/src/pages/DashboardPage.tsx @@ -1,11 +1,11 @@ -import { Button } from '@shopery/ui-shared'; +import { CustomButton } from '@shopery/ui-shared'; const DashboardPage = () => { return (
- +
); }; diff --git a/apps/fe-admin/src/pages/OrderPage.tsx b/apps/fe-admin/src/pages/OrderPage.tsx deleted file mode 100644 index 60dc2b0..0000000 --- a/apps/fe-admin/src/pages/OrderPage.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -const OrderPage = () => { - return
Orders
; -}; - -export default OrderPage; diff --git a/apps/fe-admin/src/pages/OrderPage/OrderPage.module.scss b/apps/fe-admin/src/pages/OrderPage/OrderPage.module.scss new file mode 100644 index 0000000..e69de29 diff --git a/apps/fe-admin/src/pages/OrderPage/OrderPage.tsx b/apps/fe-admin/src/pages/OrderPage/OrderPage.tsx new file mode 100644 index 0000000..bc96090 --- /dev/null +++ b/apps/fe-admin/src/pages/OrderPage/OrderPage.tsx @@ -0,0 +1,163 @@ +import { + CustomButton, + CustomInput, + CustomModal, + CustomSelect, + CustomTable, +} from '@shopery/ui-shared'; +import { useDeferredValue, useState } from 'react'; +import { Controller, useForm, useWatch } from 'react-hook-form'; +import ActionButtons from '../../components/ActionButtons/ActionButtons'; +import { dataSource } from '../../data'; +import styles from './OrderPage.module.scss'; + +const OrderPage = () => { + const [orderData, setOrderData] = useState(dataSource); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [orderId, setOrderId] = useState(null); + const [editStatus, setEditStatus] = useState(null); + const { control } = useForm({ + defaultValues: { + search: '', + }, + }); + + const columns = [ + { title: 'Order ID', dataIndex: 'id', key: 'id' }, + { title: 'User', dataIndex: 'userId', key: 'userId' }, + { + title: 'Total number of products', + dataIndex: 'totalNumberOfProducts', + key: 'totalNumberOfProducts', + }, + { title: 'Total Price', dataIndex: 'totalPrice', key: 'totalPrice' }, + { title: 'Status', dataIndex: 'status', key: 'status' }, + { + title: 'Actions', + key: 'actions', + render: (record) => ( + handleOpenEditModal(record.id, record.status)} + onDelete={() => handleOpenDeleteModal(record.id)} + /> + ), + }, + ]; + + const statusOptions = [ + { value: 'pending', label: 'Pending' }, + { value: 'shipped', label: 'Shipped' }, + { value: 'delivered', label: 'Delivered' }, + ]; + + const handleOpenEditModal = (id: number, status: string) => { + setOrderId(id); + setIsEditModalOpen(true); + setEditStatus(status); + }; + + const handleOpenDeleteModal = (id: number) => { + setOrderId(id); + setIsDeleteModalOpen(true); + }; + + const searchValue = useWatch({ control, name: 'search' }); + const debouncedSearch = useDeferredValue(searchValue?.trim() ?? '', 400); + + const filteredData = orderData.filter((item) => { + if (!debouncedSearch) return true; + const searchStr = debouncedSearch.toLowerCase(); + + const matchesSearch = [ + item.id, + item.userId, + item.totalNumberOfProducts, + item.totalPrice, + item.status?.toLowerCase(), + ].some((field) => String(field).includes(searchStr)); + + return matchesSearch; + }); + + const handleDelete = (id: number) => { + setOrderData(orderData.filter((item) => item.id !== id)); + setIsDeleteModalOpen(false); + setOrderId(null); + }; + + const handleSave = () => { + if (typeof editStatus !== 'string') return; + setOrderData((prev) => + prev.map((item) => { + if (item.id === orderId) { + return { ...item, status: editStatus }; + } + return item; + }) + ); + setIsEditModalOpen(false); + setEditStatus(null); + setOrderId(null); + }; + + return ( +
+

Orders

+
+ ( + + )} + /> + + + + setIsEditModalOpen(false)} + title='Edit Order' + footer={ + handleSave()}> + Save + + } + > + setEditStatus(value)} + /> + + setIsDeleteModalOpen(false)} + title='Delete Order' + footer={ + { + handleDelete(orderId as number); + }} + > + Delete + + } + > +

Are you sure you want to delete this order?

+
+
+ ); +}; + +export default OrderPage; diff --git a/apps/fe-admin/src/pages/index.ts b/apps/fe-admin/src/pages/index.ts index dbb44df..2f920ac 100644 --- a/apps/fe-admin/src/pages/index.ts +++ b/apps/fe-admin/src/pages/index.ts @@ -1,4 +1,4 @@ export { default as DashboardPage } from './DashboardPage'; -export { default as OrderPage } from './OrderPage'; +export { default as OrderPage } from './OrderPage/OrderPage'; export { default as ProductPage } from './ProductPage'; export { default as UserPage } from './UserPage'; diff --git a/libs/ui/ui-shared/src/lib/components/Button/Button.module.scss b/libs/ui/ui-shared/src/lib/components/CustomButton/CustomButton.module.scss similarity index 86% rename from libs/ui/ui-shared/src/lib/components/Button/Button.module.scss rename to libs/ui/ui-shared/src/lib/components/CustomButton/CustomButton.module.scss index d82ecfb..0369efa 100644 --- a/libs/ui/ui-shared/src/lib/components/Button/Button.module.scss +++ b/libs/ui/ui-shared/src/lib/components/CustomButton/CustomButton.module.scss @@ -32,6 +32,14 @@ color: $tertiary; } } +.danger { + background-color: $danger; + border: none; + color: $white; + &:hover { + filter: brightness(0.9); + } +} .small { padding: 10px 24px; font-size: $font-size-xs; diff --git a/libs/ui/ui-shared/src/lib/components/Button/Button.tsx b/libs/ui/ui-shared/src/lib/components/CustomButton/CustomButton.tsx similarity index 70% rename from libs/ui/ui-shared/src/lib/components/Button/Button.tsx rename to libs/ui/ui-shared/src/lib/components/CustomButton/CustomButton.tsx index a34d315..c80822a 100644 --- a/libs/ui/ui-shared/src/lib/components/Button/Button.tsx +++ b/libs/ui/ui-shared/src/lib/components/CustomButton/CustomButton.tsx @@ -1,14 +1,14 @@ import { clsx } from 'clsx'; import React, { ButtonHTMLAttributes } from 'react'; -import styles from './Button.module.scss'; +import styles from './CustomButton.module.scss'; export interface ButtonProps extends ButtonHTMLAttributes { - children: string; - variant?: 'fill' | 'border' | 'ghost'; + children: React.ReactNode; + variant?: 'fill' | 'border' | 'ghost' | 'danger'; size?: 'small' | 'medium' | 'large'; } -export const Button: React.FC = ({ +export const CustomButton: React.FC = ({ children, variant = 'fill', size = 'small', @@ -30,4 +30,4 @@ export const Button: React.FC = ({ ); }; -export default Button; +export default CustomButton; diff --git a/libs/ui/ui-shared/src/lib/components/CustomInput/CustomInput.tsx b/libs/ui/ui-shared/src/lib/components/CustomInput/CustomInput.tsx new file mode 100644 index 0000000..e74cf67 --- /dev/null +++ b/libs/ui/ui-shared/src/lib/components/CustomInput/CustomInput.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Input } from 'antd'; + +export type CustomInputProps = { + placeholder?: string; + onChange?: (e: React.ChangeEvent) => void; + error?: string; + hasError?: boolean; +}; + +export const CustomInput = ({ + placeholder, + onChange, + error, + hasError, + ...props +}: CustomInputProps) => { + return ( + + ); +}; + +export default CustomInput; diff --git a/libs/ui/ui-shared/src/lib/components/CustomModal/CustomModal.tsx b/libs/ui/ui-shared/src/lib/components/CustomModal/CustomModal.tsx new file mode 100644 index 0000000..dada601 --- /dev/null +++ b/libs/ui/ui-shared/src/lib/components/CustomModal/CustomModal.tsx @@ -0,0 +1,36 @@ +import { Modal } from 'antd'; + +export type CustomModalProps = { + children: React.ReactNode; + open: boolean; + onCancel?: () => void; + onOk?: () => void; + title?: string; + footer?: React.ReactNode | null; +}; + +export const CustomModal = ({ + children, + open, + onCancel, + onOk, + title, + footer, + ...props +}: CustomModalProps) => { + return ( + + {children} + + ); +}; + +export default CustomModal; diff --git a/libs/ui/ui-shared/src/lib/components/CustomSelect/CustomSelect.tsx b/libs/ui/ui-shared/src/lib/components/CustomSelect/CustomSelect.tsx new file mode 100644 index 0000000..838f4e4 --- /dev/null +++ b/libs/ui/ui-shared/src/lib/components/CustomSelect/CustomSelect.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { Select } from 'antd'; + +export const CustomSelect = ({ ...props }) => { + return ( +