diff --git a/.env b/.env index e69de29..d71477d 100644 --- a/.env +++ b/.env @@ -0,0 +1,2 @@ +VITE_TOKEN_NAME = 'dpage-editor-t' +VITE_REFRESH_TOKEN_NAME = 'dpage-editor-t' \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 44e509f..e5d66a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,15 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.1.17", + "i18next": "^25.6.2", + "iconsax-react": "^0.0.8", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-i18next": "^16.3.0", "react-router-dom": "^7.9.5", - "tailwindcss": "^4.1.17" + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.1.17", + "zustand": "^5.0.8" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -265,6 +270,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -1644,7 +1658,7 @@ "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -2214,7 +2228,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/debug": { @@ -2711,6 +2725,59 @@ "node": ">=8" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "25.6.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.6.2.tgz", + "integrity": "sha512-0GawNyVUw0yvJoOEBq1VHMAsqdM23XrHkMtl2gKEjviJQSLVXsrPqsoYAxBEugW5AB96I2pZkwRxyl8WZVoWdw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.27.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/iconsax-react": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/iconsax-react/-/iconsax-react-0.0.8.tgz", + "integrity": "sha512-l3dVk4zGtkkJHgvNYqAf0wDKqnKxXykee5/DoESGo2JvSYwaxajJUHSX2YrPRXSov8Hd8ClGFwJxCEaEjrFD1Q==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2801,7 +2868,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -3160,6 +3226,18 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3255,6 +3333,15 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3395,6 +3482,17 @@ "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3431,7 +3529,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3449,6 +3546,39 @@ "react": "^19.2.0" } }, + "node_modules/react-i18next": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.3.0.tgz", + "integrity": "sha512-XGYIVU6gCOL4UQsfp87WbbvBc2WvgdkEDI8r4TwACzFg1bXY8pd1d9Cw6u9WJ2soTKHKaF1xQEyWA3/dUvtAGw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.6.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -3663,6 +3793,16 @@ "node": ">=8" } }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.1.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", @@ -3771,7 +3911,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "peer": true, "bin": { @@ -3854,6 +3994,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", @@ -3959,6 +4108,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4004,6 +4162,35 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 5248d0a..1e16dea 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,15 @@ }, "dependencies": { "@tailwindcss/vite": "^4.1.17", + "i18next": "^25.6.2", + "iconsax-react": "^0.0.8", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-i18next": "^16.3.0", "react-router-dom": "^7.9.5", - "tailwindcss": "^4.1.17" + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.1.17", + "zustand": "^5.0.8" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/src/App.tsx b/src/App.tsx index 9eb2f57..147d8d8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,27 @@ import { type FC } from 'react' import './assets/fonts/irancell/style.css' import { BrowserRouter } from 'react-router-dom' +import MainRouter from './router/MainRouter' +import i18next from 'i18next' +import FaJson from '@/langs/fa.json' +import { I18nextProvider } from 'react-i18next' + +i18next.init({ + interpolation: { escapeValue: false }, + lng: 'fa', + resources: { + fa: { + global: FaJson + } + } +}) const App: FC = () => { return ( - + + + ) } diff --git a/src/assets/images/logo-small.svg b/src/assets/images/logo-small.svg new file mode 100644 index 0000000..af5e4f3 --- /dev/null +++ b/src/assets/images/logo-small.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/images/logo.svg b/src/assets/images/logo.svg new file mode 100644 index 0000000..200db25 --- /dev/null +++ b/src/assets/images/logo.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 0000000..07d0d98 --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,46 @@ +import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react' +import { clx } from '@/helpers/utils' + +type ButtonProps = { + variant?: 'primary' | 'secondary' | 'outline' + leftIcon?: ReactNode + rightIcon?: ReactNode +} & ButtonHTMLAttributes + +const variantClasses: Record, string> = { + primary: 'bg-black text-white hover:bg-black/90', + secondary: 'bg-white text-black border border-black hover:bg-black/5', + outline: 'border border-black text-black hover:bg-black/5' +} + +const Button = forwardRef((props, ref) => { + const { + children, + className, + variant = 'primary', + leftIcon, + rightIcon, + ...rest + } = props + + return ( + + ) +}) + +Button.displayName = 'Button' + +export default Button + diff --git a/src/components/Divider.tsx b/src/components/Divider.tsx new file mode 100644 index 0000000..bfb5a94 --- /dev/null +++ b/src/components/Divider.tsx @@ -0,0 +1,27 @@ +import { type FC } from 'react' + +import { clx } from '@/helpers/utils' + +type DividerProps = { + label?: string + className?: string + lineClassName?: string + labelClassName?: string +} + +const Divider: FC = ({ label, className, lineClassName, labelClassName }) => { + return ( +
+ + {label ? ( + + {label} + + ) : null} + +
+ ) +} + +export default Divider + diff --git a/src/components/Error.tsx b/src/components/Error.tsx new file mode 100644 index 0000000..dc6aeac --- /dev/null +++ b/src/components/Error.tsx @@ -0,0 +1,15 @@ +import { type FC } from 'react' + +type Props = { + errorText: string +} + +const Error: FC = (props: Props) => { + return ( +
+ {props.errorText} +
+ ) +} + +export default Error \ No newline at end of file diff --git a/src/components/Input.tsx b/src/components/Input.tsx new file mode 100644 index 0000000..656e9b0 --- /dev/null +++ b/src/components/Input.tsx @@ -0,0 +1,124 @@ +import { type FC, type InputHTMLAttributes, useEffect, useState } from 'react' +import { clx } from '../helpers/utils'; +import { Eye, EyeSlash, SearchNormal } from 'iconsax-react'; +import Error from './Error'; + +type Variant = "floating_outlined" | "primary" | "search"; + +type Props = { + label?: string; + className?: string; + variant?: Variant; + error_text?: string; + onEnter?: () => void; + unit?: string; + seprator?: boolean; + isNotRequired?: boolean; + onChangeSearchFinal?: (value: string) => void; +} & InputHTMLAttributes + +const formatNumber = (value: string | number): string => { + if (!value) return ''; + const inputValue = String(value).replace(/,/g, ''); + return inputValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); +}; + +const Input: FC = (props: Props) => { + + const [formattedValue, setFormattedValue] = useState( + props.value ? formatNumber(props.value as string) : '' + ); + + const [showPassword, setShowPassword] = useState(false) + const [search, setSearch] = useState('') + + const inputClass = clx( + 'w-full h-10 text-black block px-4 text-xs rounded-xl border border-border', + props.readOnly && 'bg-gray-100 border-0 text-description', + props.variant === 'search' && 'bg-[#EEF0F7] border-0 ps-10', + props.className + ); + + const handleInputChange = (event: React.ChangeEvent) => { + if (props.seprator) { + const inputValue = event.target.value.replace(/,/g, ''); // حذف کاماها + const formatted = formatNumber(inputValue); + + // به‌روزرسانی مقدار قالب‌بندی‌شده + setFormattedValue(formatted); + + // ارسال مقدار خام به `onChange` والد + props.onChange?.({ + ...event, + target: { + ...event.target, + value: inputValue, + }, + }); + } else { + props.onChange?.(event); + } + }; + + useEffect(() => { + if (props.value) { + setFormattedValue(formatNumber(props.value as string)); + } else { + setFormattedValue(''); + } + }, [props.value]) + + useEffect(() => { + if (props.variant === 'search' && props.onChangeSearchFinal) { + const timeout = setTimeout(() => { + props.onChangeSearchFinal?.(search) + }, 1000) + return () => clearTimeout(timeout) + } + }, [search, props]) + + + return ( +
+ + +
+ { + setSearch(e.target.value) + handleInputChange(e) + }} value={props.seprator ? formattedValue : props.value} type={props.type === 'password' && showPassword ? 'text' : props.type === 'password' ? 'password' : undefined} className={inputClass} /> + + { + props.type === 'password' && + + } + + { + props.variant === 'search' && + + } + + { + props.error_text && + + } +
+
+ ) +} + +export default Input \ No newline at end of file diff --git a/src/config/Paths.ts b/src/config/Paths.ts new file mode 100644 index 0000000..4015b07 --- /dev/null +++ b/src/config/Paths.ts @@ -0,0 +1,4 @@ +export const Paths = { + home: "/", + editor: "/editor", +}; diff --git a/src/helpers/func.ts b/src/helpers/func.ts new file mode 100644 index 0000000..3a11d9b --- /dev/null +++ b/src/helpers/func.ts @@ -0,0 +1,27 @@ +export const formatDate = (dateString: string): string => { + const date = new Date(dateString); + return new Intl.DateTimeFormat("fa-IR", { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }).format(date); +}; + +export const formatDateShort = (dateString: string): string => { + const date = new Date(dateString); + return new Intl.DateTimeFormat("fa-IR", { + year: "numeric", + month: "short", + day: "numeric", + }).format(date); +}; + +export const formatPrice = (price: number): string => { + return new Intl.NumberFormat("fa-IR", { + style: "decimal", + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(price); +}; diff --git a/src/helpers/types.ts b/src/helpers/types.ts new file mode 100644 index 0000000..c800121 --- /dev/null +++ b/src/helpers/types.ts @@ -0,0 +1,29 @@ +type Without = { [P in Exclude]?: never }; + +export type XOR = T | U extends object + ? (Without & U) | (Without & T) + : T | U; + +export type ItemsSelectType = { + value: string; + label: string; +}; + +export type ErrorType = Error & { + response?: { + data?: { + error: { + message: string[]; + }; + }; + }; +}; + +export interface IGeneralError { + status: number; + success: boolean; + error: { + message: string; + details: string[]; + }; +} diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts new file mode 100644 index 0000000..26a2e0a --- /dev/null +++ b/src/helpers/utils.ts @@ -0,0 +1,24 @@ +import { twMerge } from "tailwind-merge"; +import type { IGeneralError } from "./types"; + +/** + * Concatenates truthy classes into a space-separated string. + * + * @param classes - The classes to concatenate. + * @returns The concatenated classes. + */ +export const clx = (...classes: (string | boolean | undefined)[]): string => { + return twMerge(classes.filter(Boolean).join(" ")); +}; + +export const extractErrorMessage = ( + error: Error | unknown, + fallbackMessage: string = "خطایی رخ داده است" +): string => { + try { + const axiosError = error as { response?: { data: IGeneralError } }; + return axiosError?.response?.data?.error?.details?.[0] || fallbackMessage; + } catch { + return fallbackMessage; + } +}; diff --git a/src/langs/fa.json b/src/langs/fa.json new file mode 100644 index 0000000..75cce43 --- /dev/null +++ b/src/langs/fa.json @@ -0,0 +1,29 @@ +{ + "sidebar": { + "menu": "منو", + "home_page": "صفحه اصلی" + }, + "header": { + "search": "جستجو" + }, + "view": { + "title": "نمایش کاتالوگ", + "download_pdf": "دانلود PDF", + "sidebar": { + "title": "صفحات کاتالوگ", + "search_placeholder": "جستجوی صفحه", + "items": { + "cover": "صفحه جلد", + "details": "جزئیات محصول", + "gallery": "گالری تصاویر" + } + }, + "actions": { + "add_page": "افزودن صفحه جدید" + }, + "main": { + "upload_hint": "برای جایگزینی فایل جدید، PDF خود را آپلود کنید.", + "empty_state": "برای مشاهده محتوا، یکی از صفحات سایدبار را انتخاب کنید." + } + } +} diff --git a/src/pages/editor/Editor.tsx b/src/pages/editor/Editor.tsx new file mode 100644 index 0000000..68bfb04 --- /dev/null +++ b/src/pages/editor/Editor.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import EditorSidebar from './components/EditorSidebar' + +const Editor = () => { + return ( +
+ +
+ ) +} + +export default Editor \ No newline at end of file diff --git a/src/pages/editor/components/EditorSidebar.tsx b/src/pages/editor/components/EditorSidebar.tsx new file mode 100644 index 0000000..ca8e198 --- /dev/null +++ b/src/pages/editor/components/EditorSidebar.tsx @@ -0,0 +1,30 @@ + +import Logo from "@/assets/images/logo.svg"; +import { clx } from "@/helpers/utils"; +import { useSharedStore } from "@/shared/store/sharedStore"; + +const EditorSidebar = () => { + const { openSidebar, setOpenSidebar } = useSharedStore(); + + return ( + <> + {openSidebar ? ( +
setOpenSidebar(false)} /> + ) : null} + + + ); +}; + +export default EditorSidebar; + diff --git a/src/pages/home/Home.tsx b/src/pages/home/Home.tsx new file mode 100644 index 0000000..b52e216 --- /dev/null +++ b/src/pages/home/Home.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +const Home = () => { + return ( +
Home
+ ) +} + +export default Home \ No newline at end of file diff --git a/src/router/MainRouter.tsx b/src/router/MainRouter.tsx new file mode 100644 index 0000000..f1b57bf --- /dev/null +++ b/src/router/MainRouter.tsx @@ -0,0 +1,42 @@ +import SideBar from '@/shared/SideBar' +import Header from '@/shared/Header' +import { Route, Routes, useLocation } from 'react-router-dom' +import { Paths } from '@/config/Paths' +import { clx } from '@/helpers/utils' +import { useSharedStore } from '@/shared/store/sharedStore' +import Home from '@/pages/home/Home' +import Editor from '@/pages/editor/Editor' + +const MainRouter = () => { + const { hasSubMenu } = useSharedStore() + const location = useLocation() + const hiddenSideBarRoutes = [Paths.editor] + const shouldRenderSideBar = !hiddenSideBarRoutes.includes(location.pathname) + const routeHasLocalSidebar = location.pathname === Paths.editor + const hasSidebarSpace = shouldRenderSideBar || routeHasLocalSidebar + const headerSidebarSize = routeHasLocalSidebar ? 'wide' : 'default' + + return ( +
+ {shouldRenderSideBar ? : null} +
+
+
+
+ + } /> + } /> + +
+
+
+
+ ) +} + +export default MainRouter \ No newline at end of file diff --git a/src/shared/Header.tsx b/src/shared/Header.tsx new file mode 100644 index 0000000..c874609 --- /dev/null +++ b/src/shared/Header.tsx @@ -0,0 +1,130 @@ +import { type FC, useEffect } from 'react' +import Input from '@/components/Input' +// import { ArrowDown2, CloseCircle, HambergerMenu, Logout, Wallet } from 'iconsax-react' +// import AvatarImage from '../assets/images/avatar_image.png' +import { useTranslation } from 'react-i18next' +import { useLocation } from 'react-router-dom' +import { useSharedStore } from '@/shared/store/sharedStore' +import { HambergerMenu } from 'iconsax-react' +import { clx } from '@/helpers/utils' + +type SidebarSize = 'default' | 'wide' + +type HeaderProps = { + hasMainSidebar?: boolean + sidebarSize?: SidebarSize +} + +const sidebarSizeClasses: Record = { + default: { + base: 'xl:right-[285px] xl:w-[calc(100%-305px)]', + withSub: 'xl:right-[320px] xl:w-[calc(100%-340px)]', + }, + wide: { + base: 'xl:right-[390px] xl:w-[calc(100%-406px)]', + withSub: 'xl:right-[425px] xl:w-[calc(100%-441px)]', + }, +} + +const Header: FC = ({ hasMainSidebar = true, sidebarSize = 'default' }) => { + + // const location = useLocation(); + // const [popoverKey, setPopoverKey] = useState(0); + const { t } = useTranslation('global') + const { setOpenSidebar, openSidebar, hasSubMenu, setSearch } = useSharedStore() + const location = useLocation() + + // useEffect(() => { + // setPopoverKey((prevKey) => prevKey + 1); + // }, [location.pathname]); + + // const handleLogout = () => { + // removeToken() + // removeRefreshToken() + // window.location.href = Pages.auth.login + // } + + useEffect(() => { + setSearch('') + }, [location.pathname]) + + return ( +
+ +
+ setSearch(value)} + /> +
+
setOpenSidebar(!openSidebar)} className='xl:hidden block'> + +
+ {/* */} +
+ {/* + + */} + {/* */} + {/* { + data && ( + + +
+
+ +
+
+
+ {data?.data?.user?.firstName + ' ' + data?.data?.user?.lastName} +
+ +
+
+
+ + +
+ setPopoverKey((prevKey) => prevKey + 1)} size={20} color='#da2129' /> +
+ +
+ +
+ +
+ {data?.data?.user?.firstName + ' ' + data?.data?.user?.lastName} +
+ +
+ {data?.data?.user?.email} +
+ + +
+
handleLogout()} className='flex gap-2 items-center cursor-pointer'> + +
+ {t('sidebar.logout')} +
+
+
+ +
+
+ ) + } */} +
+
+ ) +} + +export default Header \ No newline at end of file diff --git a/src/shared/SideBar.tsx b/src/shared/SideBar.tsx new file mode 100644 index 0000000..86f29f6 --- /dev/null +++ b/src/shared/SideBar.tsx @@ -0,0 +1,110 @@ +import { type FC, useEffect } from 'react' +import LogoImage from '../assets/images/logo.svg' +import LogoSmall from '../assets/images/logo-small.svg' +import { useTranslation } from 'react-i18next' +import { + Home2, + Logout, +} from 'iconsax-react' +import SideBarItem from './SideBarItem' +import { useLocation } from 'react-router-dom' +import { useSharedStore } from './store/sharedStore' +import { clx } from '../helpers/utils' +import { Paths } from '../config/Paths' + + +const SideBar: FC = () => { + + const { t } = useTranslation('global') + const { openSidebar, setOpenSidebar, hasSubMenu, setSubMenuName, setSubtMenu, subMenuName } = useSharedStore() + const location = useLocation() + + const isActive = (name: string) => { + return window.location.href.includes(name) + } + + useEffect(() => { + const split = location.pathname.split('/') + + if (split[1] === 'dashboard' || split[1] === 'products') { + setSubMenuName(split[1]) + setSubtMenu(true) + } else { + setSubMenuName('') + setSubtMenu(false) + } + }, [location.pathname, setSubMenuName, setSubtMenu]) + + const iconSizeSideBar = 20 + + return ( + <> + { + openSidebar &&
setOpenSidebar(false)} /> + } +
+
+ { + !hasSubMenu ? + + : + + } +
+ +
+
+ {t('sidebar.menu')} +
+ +
+ } + title={t('sidebar.home_page')} + isActive={isActive(Paths.home)} + link={Paths.home} + activeName={Paths.home} + /> +
+ +
+
+ } + title="خروج" + isActive={isActive('logout')} + link={`#`} + isLogout + activeName='' + /> +
+
+
+
+ + {/* منوی فرعی */} + { + hasSubMenu && (openSidebar || window.innerWidth > 1139) && +
+ { + subMenuName === 'dashboard' ? null + // + : null + } +
+ } + + ) +} + + +export default SideBar \ No newline at end of file diff --git a/src/shared/SideBarItem.tsx b/src/shared/SideBarItem.tsx new file mode 100644 index 0000000..dbf3042 --- /dev/null +++ b/src/shared/SideBarItem.tsx @@ -0,0 +1,61 @@ +import { type FC, type ReactNode } from 'react' +import { Link } from 'react-router-dom' +import { clx } from '../helpers/utils' +import { useSharedStore } from './store/sharedStore' +// import { useGetAdminPermissions } from '../pages/users/hooks/useUserData' + +type Props = { + icon: ReactNode, + title: string, + isActive: boolean, + link: string, + isLogout?: boolean, + name?: string, + activeName?: string +} + +const SideBarItem: FC = (props: Props) => { + + const { hasSubMenu, setSubMenuName, setSubtMenu } = useSharedStore() + // const { data: adminPermissions } = useGetAdminPermissions() + const handleLogout = () => { + // removeToken() + // removeRefreshToken() + // window.location.href = Pages.auth.login + } + + const handleClick = () => { + if (props.name) { + setSubMenuName(props.name) + setSubtMenu(true) + } + } + // if (props.activeName === '' || props.activeName && adminPermissions?.data?.permissions?.some((permission: { name: string }) => permission.name === props.activeName)) { + if (true) { + return ( + +
+
+ {props.icon} +
+ {props.title} +
+
+ + + ) + } + +} + +export default SideBarItem \ No newline at end of file diff --git a/src/shared/store/sharedStore.ts b/src/shared/store/sharedStore.ts new file mode 100644 index 0000000..bd93a94 --- /dev/null +++ b/src/shared/store/sharedStore.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; +import { type SharedStoreType } from "../types/SharedTypes"; + +export const useSharedStore = create((set) => ({ + openSidebar: false, + setOpenSidebar: (value) => set({ openSidebar: value }), + hasSubMenu: false, + setSubtMenu: (value: boolean) => set({ hasSubMenu: value }), + subMenuName: "", + setSubMenuName: (value: string) => set({ subMenuName: value }), + search: "", + setSearch: (value: string) => set({ search: value }), +})); diff --git a/src/shared/types/SharedTypes.ts b/src/shared/types/SharedTypes.ts new file mode 100644 index 0000000..8eb851c --- /dev/null +++ b/src/shared/types/SharedTypes.ts @@ -0,0 +1,19 @@ +export type SharedStoreType = { + openSidebar: boolean; + setOpenSidebar: (value: boolean) => void; + hasSubMenu: boolean; + setSubtMenu: (value: boolean) => void; + subMenuName: string; + setSubMenuName: (value: string) => void; + search: string; + setSearch: (value: string) => void; +}; + +export type PagerType = { + page: number; + limit: number; + totalItems: number; + totalPages: number; + prevPage: boolean | null; + nextPage: string | null; +}; diff --git a/tsconfig.app.json b/tsconfig.app.json index a9b5a59..4d8c378 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -15,6 +15,10 @@ "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, /* Linting */ "strict": true, diff --git a/vite.config.ts b/vite.config.ts index adeab7d..db3fc25 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; +import { fileURLToPath, URL } from "node:url"; // https://vite.dev/config/ export default defineConfig({ @@ -12,4 +13,9 @@ export default defineConfig({ }), tailwindcss(), ], + resolve: { + alias: { + "@": fileURLToPath(new URL("./src", import.meta.url)), + }, + }, });