From 53212445c60a73bc7c5ff3171b13ca6a4462eafe Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Mon, 28 Apr 2025 10:15:35 +0330 Subject: [PATCH] structure + sidebar --- package.json | 10 +- pnpm-lock.yaml | 253 ++++++++++++++++++++++++++++++++ src/App.tsx | 21 ++- src/assets/images/logo.svg | 91 ++++++++++++ src/components/Button.tsx | 40 +++++ src/components/Error.tsx | 15 ++ src/components/Input.tsx | 115 +++++++++++++++ src/helpers/types.ts | 20 +++ src/helpers/utils.ts | 11 ++ src/index.css | 6 + src/langs/fa.json | 20 +++ src/pages/home/Home.tsx | 9 ++ src/router/AppRouter.tsx | 13 ++ src/shared/Footer.tsx | 86 +++++++++++ src/shared/Header.tsx | 173 ++++++++++++++++++++++ src/shared/Main.tsx | 15 ++ src/shared/SideBar.tsx | 135 +++++++++++++++++ src/shared/SideBarItem.tsx | 43 ++++++ src/shared/store/sharedStore.ts | 7 + src/shared/types/SharedTypes.ts | 4 + src/utils/Paths.ts | 11 ++ tsconfig.app.json | 22 ++- tsconfig.json | 20 ++- tsconfig.node.json | 20 ++- vite.config.ts | 3 +- 25 files changed, 1144 insertions(+), 19 deletions(-) create mode 100644 src/assets/images/logo.svg create mode 100644 src/components/Button.tsx create mode 100644 src/components/Error.tsx create mode 100644 src/components/Input.tsx create mode 100644 src/helpers/types.ts create mode 100644 src/helpers/utils.ts create mode 100644 src/langs/fa.json create mode 100644 src/pages/home/Home.tsx create mode 100644 src/router/AppRouter.tsx create mode 100644 src/shared/Footer.tsx create mode 100644 src/shared/Header.tsx create mode 100644 src/shared/Main.tsx create mode 100644 src/shared/SideBar.tsx create mode 100644 src/shared/SideBarItem.tsx create mode 100644 src/shared/store/sharedStore.ts create mode 100644 src/shared/types/SharedTypes.ts create mode 100644 src/utils/Paths.ts diff --git a/package.json b/package.json index 56d19a8..b8dd95a 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,17 @@ }, "dependencies": { "@tailwindcss/vite": "^4.1.4", + "i18next": "^25.0.1", + "iconsax-react": "^0.0.8", "react": "^19.0.0", "react-dom": "^19.0.0", - "tailwindcss": "^4.1.4" + "react-i18next": "^15.5.1", + "react-router-dom": "^7.5.2", + "react-spinners": "^0.17.0", + "tailwind-merge": "^3.2.0", + "tailwindcss": "^4.1.4", + "vite-tsconfig-paths": "^5.1.4", + "zustand": "^5.0.3" }, "devDependencies": { "@eslint/js": "^9.22.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 398ab24..0eaa0cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,15 +11,39 @@ importers: '@tailwindcss/vite': specifier: ^4.1.4 version: 4.1.4(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2)) + i18next: + specifier: ^25.0.1 + version: 25.0.1(typescript@5.7.3) + iconsax-react: + specifier: ^0.0.8 + version: 0.0.8(react@19.1.0) react: specifier: ^19.0.0 version: 19.1.0 react-dom: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) + react-i18next: + specifier: ^15.5.1 + version: 15.5.1(i18next@25.0.1(typescript@5.7.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.7.3) + react-router-dom: + specifier: ^7.5.2 + version: 7.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-spinners: + specifier: ^0.17.0 + version: 0.17.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + tailwind-merge: + specifier: ^3.2.0 + version: 3.2.0 tailwindcss: specifier: ^4.1.4 version: 4.1.4 + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.7.3)(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2)) + zustand: + specifier: ^5.0.3 + version: 5.0.3(@types/react@19.1.2)(react@19.1.0) devDependencies: '@eslint/js': specifier: ^9.22.0 @@ -128,6 +152,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.27.0': + resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.0': resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} engines: {node: '>=6.9.0'} @@ -709,6 +737,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -877,6 +909,9 @@ packages: resolution: {integrity: sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==} engines: {node: '>=18'} + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -887,6 +922,22 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + + i18next@25.0.1: + resolution: {integrity: sha512-8S8PyZbrymJZn3DaN70/34JYWNhsqrU6yA4MuzcygJBv+41dgNMocEA8h+kV1P7MCc1ll03lOTOIXE7mpNCicw==} + peerDependencies: + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true + + iconsax-react@0.0.8: + resolution: {integrity: sha512-l3dVk4zGtkkJHgvNYqAf0wDKqnKxXykee5/DoESGo2JvSYwaxajJUHSX2YrPRXSov8Hd8ClGFwJxCEaEjrFD1Q==} + peerDependencies: + react: '*' + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1022,6 +1073,10 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1054,6 +1109,10 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1097,6 +1156,9 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1109,14 +1171,59 @@ packages: peerDependencies: react: ^19.1.0 + react-i18next@15.5.1: + resolution: {integrity: sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA==} + peerDependencies: + i18next: '>= 23.2.3' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} + react-router-dom@7.5.2: + resolution: {integrity: sha512-yk1XW8Fj7gK7flpYBXF3yzd2NbX6P7Kxjvs2b5nu1M04rb5pg/Zc4fGdBNTeT4eDYL2bvzWNyKaIMJX/RKHTTg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.5.2: + resolution: {integrity: sha512-9Rw8r199klMnlGZ8VAsV/I8WrIF6IyJ90JQUdboupx1cdkgYqwnrYjH+I/nY/7cA1X5zia4mDJqH36npP7sxGQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-spinners@0.17.0: + resolution: {integrity: sha512-L/8HTylaBmIWwQzIjMq+0vyaRXuoAevzWoD35wKpNTxxtYXWZp+xtgkfD7Y4WItuX0YvdxMPU79+7VhhmbmuTQ==} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react@19.1.0: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1145,6 +1252,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1165,6 +1275,9 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + tailwind-merge@3.2.0: + resolution: {integrity: sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==} + tailwindcss@4.1.4: resolution: {integrity: sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==} @@ -1186,6 +1299,19 @@ packages: peerDependencies: typescript: '>=4.8.4' + tsconfck@3.1.5: + resolution: {integrity: sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + turbo-stream@2.4.0: + resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1211,6 +1337,14 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + vite@6.3.3: resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -1251,6 +1385,10 @@ packages: yaml: optional: true + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1267,6 +1405,24 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zustand@5.0.3: + resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==} + 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 + snapshots: '@ampproject/remapping@2.3.0': @@ -1361,6 +1517,10 @@ snapshots: '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.26.5 + '@babel/runtime@7.27.0': + dependencies: + regenerator-runtime: 0.14.1 + '@babel/template@7.27.0': dependencies: '@babel/code-frame': 7.26.2 @@ -1851,6 +2011,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@1.0.2: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2044,12 +2206,29 @@ snapshots: globals@16.0.0: {} + globrex@0.1.2: {} + graceful-fs@4.2.11: {} graphemer@1.4.0: {} has-flag@4.0.0: {} + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + + i18next@25.0.1(typescript@5.7.3): + dependencies: + '@babel/runtime': 7.27.0 + optionalDependencies: + typescript: 5.7.3 + + iconsax-react@0.0.8(react@19.1.0): + dependencies: + prop-types: 15.8.1 + react: 19.1.0 + ignore@5.3.2: {} import-fresh@3.3.1: @@ -2147,6 +2326,10 @@ snapshots: lodash.merge@4.6.2: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -2174,6 +2357,8 @@ snapshots: node-releases@2.0.19: {} + object-assign@4.1.1: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -2213,6 +2398,12 @@ snapshots: prelude-ls@1.2.1: {} + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -2222,10 +2413,44 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 + react-i18next@15.5.1(i18next@25.0.1(typescript@5.7.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.7.3): + dependencies: + '@babel/runtime': 7.27.0 + html-parse-stringify: 3.0.1 + i18next: 25.0.1(typescript@5.7.3) + react: 19.1.0 + optionalDependencies: + react-dom: 19.1.0(react@19.1.0) + typescript: 5.7.3 + + react-is@16.13.1: {} + react-refresh@0.17.0: {} + react-router-dom@7.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-router: 7.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + + react-router@7.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + cookie: 1.0.2 + react: 19.1.0 + set-cookie-parser: 2.7.1 + turbo-stream: 2.4.0 + optionalDependencies: + react-dom: 19.1.0(react@19.1.0) + + react-spinners@0.17.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react@19.1.0: {} + regenerator-runtime@0.14.1: {} + resolve-from@4.0.0: {} reusify@1.1.0: {} @@ -2266,6 +2491,8 @@ snapshots: semver@7.7.1: {} + set-cookie-parser@2.7.1: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -2280,6 +2507,8 @@ snapshots: dependencies: has-flag: 4.0.0 + tailwind-merge@3.2.0: {} + tailwindcss@4.1.4: {} tapable@2.2.1: {} @@ -2297,6 +2526,12 @@ snapshots: dependencies: typescript: 5.7.3 + tsconfck@3.1.5(typescript@5.7.3): + optionalDependencies: + typescript: 5.7.3 + + turbo-stream@2.4.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -2323,6 +2558,17 @@ snapshots: dependencies: punycode: 2.3.1 + vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2)): + dependencies: + debug: 4.4.0 + globrex: 0.1.2 + tsconfck: 3.1.5(typescript@5.7.3) + optionalDependencies: + vite: 6.3.3(jiti@2.4.2)(lightningcss@1.29.2) + transitivePeerDependencies: + - supports-color + - typescript + vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2): dependencies: esbuild: 0.25.3 @@ -2336,6 +2582,8 @@ snapshots: jiti: 2.4.2 lightningcss: 1.29.2 + void-elements@3.1.0: {} + which@2.0.2: dependencies: isexe: 2.0.0 @@ -2345,3 +2593,8 @@ snapshots: yallist@3.1.1: {} yocto-queue@0.1.0: {} + + zustand@5.0.3(@types/react@19.1.2)(react@19.1.0): + optionalDependencies: + '@types/react': 19.1.2 + react: 19.1.0 diff --git a/src/App.tsx b/src/App.tsx index e69e4af..f21863c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,28 @@ import { FC } from 'react' import './assets/fonts/irancell/style.css' +import { BrowserRouter } from 'react-router-dom' +import Main from './shared/Main' +import i18next from 'i18next' +import FaJson from './langs/fa.json' +import { I18nextProvider } from 'react-i18next' +i18next.init({ + interpolation: { escapeValue: false }, + lng: 'fa', + defaultNS: 'global', + resources: { + fa: { + global: FaJson + } + } +}) const App: FC = () => { return ( -
App
+ + +
+ + ) } 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..9b33f66 --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,40 @@ +import { ButtonHTMLAttributes, FC, memo, ReactNode } from 'react' +import MoonLoader from "react-spinners/MoonLoader" +import { XOR } from '../helpers/types' +import { clx } from '../helpers/utils' + +type Props = { + className?: string; + isLoading?: boolean, + percentage?: number, +} & ButtonHTMLAttributes & + XOR<{ children: ReactNode }, { label: string }>; + +const Button: FC = memo((props: Props) => { + + const buttonClass = clx( + 'flex rounded-xl items-center justify-center text-center h-10 text-sm bg-primary text-white w-full', + props.disabled && 'cursor-not-allowed opacity-60', + props.className + ); + + return ( + + ) +}) + +export default Button \ No newline at end of file diff --git a/src/components/Error.tsx b/src/components/Error.tsx new file mode 100644 index 0000000..7fec2e7 --- /dev/null +++ b/src/components/Error.tsx @@ -0,0 +1,15 @@ +import { 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..8822f29 --- /dev/null +++ b/src/components/Input.tsx @@ -0,0 +1,115 @@ +import { FC, InputHTMLAttributes, useEffect, useState } from 'react' +import { clx } from '../helpers/utils'; +import { Eye, 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' && + setShowPassword((oldValue) => !oldValue)} size={20} color='#8C90A3' className='absolute top-0 bottom-0 cursor-pointer my-auto left-3' /> + // setShowPassword((oldValue) => !oldValue)} src={EyeIcon} className='w-5 absolute top-0 bottom-0 cursor-pointer my-auto left-3' /> + } + + { + props.variant === 'search' && + + } + + { + props.error_text && + + } +
+
+ ) +} + +export default Input \ No newline at end of file diff --git a/src/helpers/types.ts b/src/helpers/types.ts new file mode 100644 index 0000000..d8f7039 --- /dev/null +++ b/src/helpers/types.ts @@ -0,0 +1,20 @@ +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[]; + }; + }; + }; +}; diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts new file mode 100644 index 0000000..aa728fe --- /dev/null +++ b/src/helpers/utils.ts @@ -0,0 +1,11 @@ +import { twMerge } from "tailwind-merge"; + +/** + * 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(" ")); +}; diff --git a/src/index.css b/src/index.css index d0b3294..9322ad6 100644 --- a/src/index.css +++ b/src/index.css @@ -40,3 +40,9 @@ input::placeholder, textarea::placeholder { color: #888888; } + +@theme { + --color-primary: #000000; + --color-secondary: #eaecf4; + --color-description: #888888; +} diff --git a/src/langs/fa.json b/src/langs/fa.json new file mode 100644 index 0000000..7f52466 --- /dev/null +++ b/src/langs/fa.json @@ -0,0 +1,20 @@ +{ + "sidebar": { + "menu": "منو", + "received": "دریافتی ها", + "sent": "ارسال شده ها", + "draft": "پیش نویس ها", + "archive": "آرشیو شده ها", + "profile": "پروفایل", + "trash": "سطل زباله", + "other": "سایر", + "favorite": "نشان شده ها", + "spam": "هرزنامه ها", + "setting": "تنظیمات", + "logout": "خروج", + "new_message": "پیام جدید" + }, + "header": { + "search": "جستجو" + } +} diff --git a/src/pages/home/Home.tsx b/src/pages/home/Home.tsx new file mode 100644 index 0000000..60a053a --- /dev/null +++ b/src/pages/home/Home.tsx @@ -0,0 +1,9 @@ +import { FC } from 'react' + +const Home: FC = () => { + return ( +
home page
+ ) +} + +export default Home \ No newline at end of file diff --git a/src/router/AppRouter.tsx b/src/router/AppRouter.tsx new file mode 100644 index 0000000..7a8f403 --- /dev/null +++ b/src/router/AppRouter.tsx @@ -0,0 +1,13 @@ +import { FC } from 'react' +import { Routes, Route } from 'react-router-dom' +import Home from '@/pages/home/Home' + +const AppRouter: FC = () => { + return ( + + } /> + + ) +} + +export default AppRouter \ No newline at end of file diff --git a/src/shared/Footer.tsx b/src/shared/Footer.tsx new file mode 100644 index 0000000..43901e5 --- /dev/null +++ b/src/shared/Footer.tsx @@ -0,0 +1,86 @@ +import { Element3, Element4, Home2, Messages3, NotificationStatus } from 'iconsax-react' +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { Pages } from '../config/Pages' +import { Link, useLocation } from 'react-router-dom' + +const Footer: FC = () => { + const { t } = useTranslation('global') + const location = useLocation() + + const isActive = (path: string) => location.pathname === path + + return ( +
+
+
+ +
+ +
+ {t('footer.my_services')} +
+
+ + +
+ +
+ {t('footer.other_services')} +
+
+ + +
+
+
+ +
+
+
+ {t('footer.home')} +
+
+ + +
+ +
+ {t('footer.announcements')} +
+
+ + +
+ +
+ {t('footer.tickets')} +
+
+ +
+
+ ) +} + +export default Footer diff --git a/src/shared/Header.tsx b/src/shared/Header.tsx new file mode 100644 index 0000000..82f6e3f --- /dev/null +++ b/src/shared/Header.tsx @@ -0,0 +1,173 @@ +import { FC, useEffect, useState } from 'react' +import Input from '../components/Input' +import { Element3, HambergerMenu, Wallet } from 'iconsax-react' +import { useTranslation } from 'react-i18next' +import { Link, useLocation } from 'react-router-dom' +import { Paths } from '@/utils/Paths' +// import Notifications from '../pages/notification/Notification' +import { useSharedStore } from './store/sharedStore' +// import { useGetProfile } from '../pages/profile/hooks/useProfileData' +// import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react' +// import { useGetWalletBalance } from '../pages/wallet/hooks/useWalletData' +// import { NumberFormat } from '../config/func' + +const Header: FC = () => { + + const location = useLocation(); + const [popoverKey, setPopoverKey] = useState(0); + const { t } = useTranslation('global') + const { setOpenSidebar, openSidebar } = useSharedStore() + // const { data } = useGetProfile() + // const getWalletBalance = useGetWalletBalance() + + console.log(popoverKey); + useEffect(() => { + + setPopoverKey((prevKey) => prevKey + 1); + }, [location.pathname]); + + return ( +
+ +
+ +
+
setOpenSidebar(!openSidebar)} className='xl:hidden block'> + +
+ {/* */} +
+ + + + + + + +
+
+ {/* {NumberFormat(getWalletBalance.data?.data?.balance) + ' ' + t('toman')} */} +
+
+ +
+
+ + {/* */} + {/* { + data && ( + + +
+
+ +
+
+
+ {data?.data?.user?.firstName + ' ' + data?.data?.user?.lastName} +
+ +
+
+
+ + +
+ setPopoverKey((prevKey) => prevKey + 1)} size={20} color='black' /> +
+ +
+ +
+ +
+ {data?.data?.user?.firstName + ' ' + data?.data?.user?.lastName} +
+ +
+ {data?.data?.user?.email} +
+ + +
+
+ } + title={t('header.wallet')} + link={Pages.wallet} + isActive + isWithoutLine + /> +
+
{t('home.balance')}
+
+ {NumberFormat(getWalletBalance.data?.data?.balance) + ' ' + t('toman')} +
+
+
+ + } + title={t('sidebar.transactions')} + isActive + link={Pages.transactions} + isWithoutLine + /> + + } + title={t('header.discounts')} + isActive + link={Pages.discounts} + isWithoutLine + /> + + } + title={t('header.financial_info')} + isActive + link={Pages.financial} + isWithoutLine + /> + + } + title={t('header.profile')} + isActive + link={Pages.profile} + isWithoutLine + /> + + } + title={t('header.setting')} + isActive + link={Pages.setting} + isWithoutLine + /> +
+ +
+ } + title={t('sidebar.logout')} + isActive + link={Pages.setting} + isWithoutLine + isLogout + /> +
+ +
+
+ ) + } */} +
+
+ ) +} + +export default Header \ No newline at end of file diff --git a/src/shared/Main.tsx b/src/shared/Main.tsx new file mode 100644 index 0000000..dfd43bd --- /dev/null +++ b/src/shared/Main.tsx @@ -0,0 +1,15 @@ +import { FC } from 'react' +import SideBar from './SideBar' +import Header from './Header' + +const Main: FC = () => { + return ( +
+ +
+ +
+ ) +} + +export default Main \ No newline at end of file diff --git a/src/shared/SideBar.tsx b/src/shared/SideBar.tsx new file mode 100644 index 0000000..b605492 --- /dev/null +++ b/src/shared/SideBar.tsx @@ -0,0 +1,135 @@ +import { FC } from 'react' +import LogoImage from '../assets/images/logo.svg' +import { Add, ArchiveTick, DirectInbox, DirectSend, Edit, InfoCircle, Logout, Setting2, Star, Trash } from 'iconsax-react' +import SideBarItem from './SideBarItem' +import { useLocation } from 'react-router-dom' +import { useSharedStore } from './store/sharedStore' +import { clx } from '../helpers/utils' +import { useTranslation } from 'react-i18next' +import { Paths } from '@/utils/Paths' +import Button from '@/components/Button' + +const SideBar: FC = () => { + + const { t } = useTranslation() + const { openSidebar, setOpenSidebar } = useSharedStore() + const location = useLocation() + + const isActive = (path: string) => location.pathname === path + + const iconSizeSideBar = 20 + + return ( + <> + { + openSidebar &&
setOpenSidebar(false)} /> + } +
+
+ +
+ +
+ +
+ +
+
+ {t('sidebar.menu')} +
+ +
+ } + title={t('sidebar.received')} + isActive={isActive(Paths.received)} + link={Paths.received} + /> + + } + title={t('sidebar.sent')} + isActive={isActive(Paths.sent)} + link={Paths.sent} + /> + + } + title={t('sidebar.draft')} + isActive={isActive(Paths.draft)} + link={Paths.draft} + /> + + } + title={t('sidebar.archive')} + isActive={isActive(Paths.archive)} + link={Paths.archive} + /> + + } + title={t('sidebar.trash')} + isActive={isActive(Paths.trash)} + link={Paths.trash} + /> +
+ +
+ {t('sidebar.other')} +
+ +
+ } + title={t('sidebar.favorite')} + isActive={isActive(Paths.favorite)} + link={Paths.favorite} + /> + + } + title={t('sidebar.spam')} + isActive={isActive(Paths.spam)} + link={Paths.spam} + /> +
+ +
+
+ } + title={t('sidebar.setting')} + isActive={isActive(Paths.setting)} + link={Paths.setting} + /> +
+
+ } + title={t('sidebar.logout')} + isActive={isActive('logout')} + link={`#`} + isLogout + /> +
+
+
+
+ + ) +} + +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..49dab92 --- /dev/null +++ b/src/shared/SideBarItem.tsx @@ -0,0 +1,43 @@ +import { FC, ReactNode } from 'react' +import { Link } from 'react-router-dom' +import { clx } from '../helpers/utils' + +type Props = { + icon: ReactNode, + title: string, + isActive: boolean, + link: string, + isLogout?: boolean, + isWithoutLine?: boolean +} + +const SideBarItem: FC = (props: Props) => { + + const handleLogout = async () => { + // await logout.mutateAsync() + // removeToken() + // removeRefreshToken() + // window.location.href = Pages.auth.login + } + + return ( + + { + !props.isWithoutLine && +
+ } +
+ {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..731c65a --- /dev/null +++ b/src/shared/store/sharedStore.ts @@ -0,0 +1,7 @@ +import { create } from "zustand"; +import { SharedStoreType } from "../types/SharedTypes"; + +export const useSharedStore = create((set) => ({ + openSidebar: false, + setOpenSidebar: (value) => set({ openSidebar: value }), +})); diff --git a/src/shared/types/SharedTypes.ts b/src/shared/types/SharedTypes.ts new file mode 100644 index 0000000..b681e51 --- /dev/null +++ b/src/shared/types/SharedTypes.ts @@ -0,0 +1,4 @@ +export type SharedStoreType = { + openSidebar: boolean; + setOpenSidebar: (value: boolean) => void; +}; diff --git a/src/utils/Paths.ts b/src/utils/Paths.ts new file mode 100644 index 0000000..63676d1 --- /dev/null +++ b/src/utils/Paths.ts @@ -0,0 +1,11 @@ +export const Paths = { + home: "/", + received: "/received", + sent: "/sent", + draft: "/draft", + archive: "/archive", + trash: "/trash", + favorite: "/favorite", + spam: "/spam", + setting: "/setting", +}; diff --git a/tsconfig.app.json b/tsconfig.app.json index 358ca9b..111a4d8 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -3,10 +3,13 @@ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2020", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], "module": "ESNext", "skipLibCheck": true, - /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, @@ -14,13 +17,20 @@ "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", - /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noUncheckedSideEffectImports": true, + "baseUrl": "./", + "paths": { + "@/*": [ + "src/*" + ] + } }, - "include": ["src"] -} + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 1ffef60..8eacc65 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,19 @@ { "files": [], "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] -} + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ], + "compilerOptions": { + "baseUrl": "./", + "paths": { + "@/*": [ + "src/*" + ] + } + } +} \ No newline at end of file diff --git a/tsconfig.node.json b/tsconfig.node.json index db0becc..275f6e2 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -2,23 +2,31 @@ "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "target": "ES2022", - "lib": ["ES2023"], + "lib": [ + "ES2023" + ], "module": "ESNext", "skipLibCheck": true, - /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, - /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noUncheckedSideEffectImports": true, + "baseUrl": "./", + "paths": { + "@/*": [ + "src/*" + ] + } }, - "include": ["vite.config.ts"] -} + "include": [ + "vite.config.ts" + ] +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 4ff4f8f..1cbd71f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,9 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; +import tsconfigPaths from "vite-tsconfig-paths"; // https://vite.dev/config/ export default defineConfig({ - plugins: [react(), tailwindcss()], + plugins: [react(), tailwindcss(), tsconfigPaths()], });