diff --git a/package-lock.json b/package-lock.json index ecc3cd3..d9bbe1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "i18next": "^25.6.2", "iconsax-react": "^0.0.8", "konva": "^10.0.9", + "moment-jalaali": "^0.10.4", "page-flip": "^2.0.7", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -32,6 +33,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@types/moment-jalaali": "^0.7.9", "@types/node": "^24.10.0", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", @@ -1909,6 +1911,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/moment-jalaali": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@types/moment-jalaali/-/moment-jalaali-0.7.9.tgz", + "integrity": "sha512-gsDOoAzRnCfQTbfdlUrCvX6R0wIto6CvwfvV2C3j4qJLK+DEiTK8Rl/xlOCBO9C6qeUfX8oyZ2UfjnXJTOvHSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "moment": ">=2.14.0" + } + }, "node_modules/@types/node": { "version": "24.10.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", @@ -3431,6 +3443,12 @@ "@types/react": "*" } }, + "node_modules/jalaali-js": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.8.tgz", + "integrity": "sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==", + "license": "MIT" + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -3920,6 +3938,38 @@ "node": "*" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-jalaali": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/moment-jalaali/-/moment-jalaali-0.10.4.tgz", + "integrity": "sha512-/eD0HeyvATznb5iE0G1BHjKRZAFEpJ9ZNUkcHwXhNgt1WJJVVzHD7+uDmqzZWVFLdbGme2gvIXKb3ezDYOXcZA==", + "license": "MIT", + "dependencies": { + "jalaali-js": "^1.2.7", + "moment": "^2.29.4", + "moment-timezone": "^0.5.46" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index c640522..b472570 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "i18next": "^25.6.2", "iconsax-react": "^0.0.8", "konva": "^10.0.9", + "moment-jalaali": "^0.10.4", "page-flip": "^2.0.7", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -34,6 +35,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@types/moment-jalaali": "^0.7.9", "@types/node": "^24.10.0", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", diff --git a/src/components/GridWrapper.tsx b/src/components/GridWrapper.tsx new file mode 100644 index 0000000..de5731d --- /dev/null +++ b/src/components/GridWrapper.tsx @@ -0,0 +1,125 @@ +import { clx } from "@/helpers/utils"; +import React from "react"; + +type GridWrapperProps = { + children: React.ReactNode; + desktop: number; // تعداد ستون‌ها در دسکتاپ + mobile: number; // تعداد ستون‌ها در موبایل + gapDesktop?: number; // فاصله بین آیتم‌ها در دسکتاپ. اگر مقدار > 12 باشد به عنوان px تفسیر می‌شود (مثلاً 24 => 24px) + gapMobile?: number; // فاصله بین آیتم‌ها در موبایل. اگر مقدار > 12 باشد به عنوان px تفسیر می‌شود + className?: string; +}; + +const GRID_COLS_CLASS: Record = { + 1: "grid-cols-1", + 2: "grid-cols-2", + 3: "grid-cols-3", + 4: "grid-cols-4", + 5: "grid-cols-5", + 6: "grid-cols-6", + 7: "grid-cols-7", + 8: "grid-cols-8", + 9: "grid-cols-9", + 10: "grid-cols-10", + 11: "grid-cols-11", + 12: "grid-cols-12", +}; + +const MD_GRID_COLS_CLASS: Record = { + 1: "md:grid-cols-1", + 2: "md:grid-cols-2", + 3: "md:grid-cols-3", + 4: "md:grid-cols-4", + 5: "md:grid-cols-5", + 6: "md:grid-cols-6", + 7: "md:grid-cols-7", + 8: "md:grid-cols-8", + 9: "md:grid-cols-9", + 10: "md:grid-cols-10", + 11: "md:grid-cols-11", + 12: "md:grid-cols-12", +}; + +// نسخه‌ی دسکتاپ با پیشوند lg: به صورت literal تا با purge مشکلی نداشته باشد +const LG_GRID_COLS_CLASS: Record = { + 1: "lg:grid-cols-1", + 2: "lg:grid-cols-2", + 3: "lg:grid-cols-3", + 4: "lg:grid-cols-4", + 5: "lg:grid-cols-5", + 6: "lg:grid-cols-6", + 7: "lg:grid-cols-7", + 8: "lg:grid-cols-8", + 9: "lg:grid-cols-9", + 10: "lg:grid-cols-10", + 11: "lg:grid-cols-11", + 12: "lg:grid-cols-12", +}; + +// نگاشت Gap بر اساس scale رایج Tailwind: gap-N که هر N معادل 4px است (N * 4px) +const GAP_CLASS: Record = { + 0: "gap-0", 1: "gap-1", 2: "gap-2", 3: "gap-3", 4: "gap-4", 5: "gap-5", + 6: "gap-6", 7: "gap-7", 8: "gap-8", 9: "gap-9", 10: "gap-10", 11: "gap-11", + 12: "gap-12", 13: "gap-13", 14: "gap-14", 15: "gap-15", 16: "gap-16", 17: "gap-17", + 18: "gap-18", 19: "gap-19", 20: "gap-20", 21: "gap-21", 22: "gap-22", 23: "gap-23", 24: "gap-24", +}; + +const LG_GAP_CLASS: Record = { + 0: "lg:gap-0", 1: "lg:gap-1", 2: "lg:gap-2", 3: "lg:gap-3", 4: "lg:gap-4", 5: "lg:gap-5", + 6: "lg:gap-6", 7: "lg:gap-7", 8: "lg:gap-8", 9: "lg:gap-9", 10: "lg:gap-10", 11: "lg:gap-11", + 12: "lg:gap-12", 13: "lg:gap-13", 14: "lg:gap-14", 15: "lg:gap-15", 16: "lg:gap-16", 17: "lg:gap-17", + 18: "lg:gap-18", 19: "lg:gap-19", 20: "lg:gap-20", 21: "lg:gap-21", 22: "lg:gap-22", 23: "lg:gap-23", 24: "lg:gap-24", +}; + +function clampToRange(value: number, min: number, max: number): number { + return Math.max(min, Math.min(max, value)); +} + +function getGridColsClass(count: number): string { + const safe = clampToRange(count, 1, 12); + return GRID_COLS_CLASS[safe]; +} + +function getLgGridColsClass(count: number): string { + const safe = clampToRange(count, 1, 12); + return LG_GRID_COLS_CLASS[safe]; +} + +function getMdGridColsClass(count: number): string { + const safe = clampToRange(count, 1, 12); + return MD_GRID_COLS_CLASS[safe]; +} + +function getGapClass(value?: number): string | undefined { + if (value === undefined) return undefined; + // اگر کاربر مقدار را به صورت px فرستاده باشد (بزرگ‌تر از 12)، آن را به scale تبدیل می‌کنیم: N = round(px/4) + const interpretedAsScale = value > 12 ? Math.round(value / 4) : value; + const safe = clampToRange(interpretedAsScale, 0, 24); + return GAP_CLASS[safe]; +} + +function getLgGapClass(value?: number): string | undefined { + if (value === undefined) return undefined; + const interpretedAsScale = value > 12 ? Math.round(value / 4) : value; + const safe = clampToRange(interpretedAsScale, 0, 24); + return LG_GAP_CLASS[safe]; +} + +export default function GridWrapper(props: GridWrapperProps) { + const { children, desktop, mobile, className, gapDesktop = 4, gapMobile = 4 } = props; + + const mobileCols = getGridColsClass(mobile); + const desktopColsMd = getMdGridColsClass(desktop); + const desktopColsLg = getLgGridColsClass(desktop); + + const mobileGap = getGapClass(gapMobile); + const desktopGap = getLgGapClass(gapDesktop); + + return ( +
+ {children} +
+ ); +} + + diff --git a/src/pages/catalogue/List.tsx b/src/pages/catalogue/List.tsx new file mode 100644 index 0000000..1cea77c --- /dev/null +++ b/src/pages/catalogue/List.tsx @@ -0,0 +1,25 @@ +import { type FC } from 'react' +import { useGetCatalog } from '../home/hooks/useHomeData' +import GridWrapper from '@/components/GridWrapper' +import CatalogueItem from './components/CatalogueItem' + +const CatalogueList: FC = () => { + + const { data } = useGetCatalog() + + return ( +
+ + { + data?.data?.map((item) => { + return ( + + ) + }) + } + +
+ ) +} + +export default CatalogueList \ No newline at end of file diff --git a/src/pages/catalogue/components/CatalogueItem.tsx b/src/pages/catalogue/components/CatalogueItem.tsx new file mode 100644 index 0000000..9179992 --- /dev/null +++ b/src/pages/catalogue/components/CatalogueItem.tsx @@ -0,0 +1,42 @@ +import { type FC } from 'react' +import { Edit, Eye, Share, Trash } from 'iconsax-react' +import type { CatalogItemType } from '../types/Types' +import moment from 'moment-jalaali' + +type Props = { + item: CatalogItemType +} + +const CatalogueItem: FC = ({ item }) => { + return ( +
+
+
+
+
+ {item.name} +
+
+ {moment(item.createdAt).format('jYYYY/jMM/jDD')} +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ ) +} + +export default CatalogueItem \ No newline at end of file diff --git a/src/pages/catalogue/types/Types.ts b/src/pages/catalogue/types/Types.ts new file mode 100644 index 0000000..bfc12c2 --- /dev/null +++ b/src/pages/catalogue/types/Types.ts @@ -0,0 +1,12 @@ +import type { BaseResponse } from "@/shared/types/SharedTypes"; + +export type CatalogItemType = { + content: string; + id: string; + name: string; + size: string; + status: string; + createdAt: string; +}; + +export type CatalogResponseType = BaseResponse; diff --git a/src/pages/home/service/HomeService.ts b/src/pages/home/service/HomeService.ts index 45da566..a95cded 100644 --- a/src/pages/home/service/HomeService.ts +++ b/src/pages/home/service/HomeService.ts @@ -1,5 +1,6 @@ import axios from "@/config/axios"; import type { CreateCatalogParamsType } from "../types/Types"; +import type { CatalogResponseType } from "@/pages/catalogue/types/Types"; export const createCatalog = async (params: CreateCatalogParamsType) => { const { data } = await axios.post("/catalogue", params); @@ -7,7 +8,7 @@ export const createCatalog = async (params: CreateCatalogParamsType) => { }; export const getCatalogs = async () => { - const { data } = await axios.get("/catalogue"); + const { data } = await axios.get("/catalogue"); return data; }; diff --git a/src/router/MainRouter.tsx b/src/router/MainRouter.tsx index 725d8aa..3b7381e 100644 --- a/src/router/MainRouter.tsx +++ b/src/router/MainRouter.tsx @@ -7,6 +7,7 @@ import { useSharedStore } from '@/shared/store/sharedStore' import Home from '@/pages/home/Home' import Editor from '@/pages/editor/Editor' import Viewer from '@/pages/viewer/Viewer' +import CatalogueList from '@/pages/catalogue/List' const MainRouter = () => { const { hasSubMenu } = useSharedStore() @@ -33,6 +34,7 @@ const MainRouter = () => { } /> } /> } /> + } /> diff --git a/src/shared/types/SharedTypes.ts b/src/shared/types/SharedTypes.ts index 8eb851c..56fabb5 100644 --- a/src/shared/types/SharedTypes.ts +++ b/src/shared/types/SharedTypes.ts @@ -17,3 +17,16 @@ export type PagerType = { prevPage: boolean | null; nextPage: string | null; }; + +export interface BaseResponse { + success: boolean; + statusCode: number; + status: number; + data: T; + meta?: { + limit: number; + page: number; + total: number; + totalPages: number; + }; +}