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 (
+
+
+
+
+
+ )
+}
+
+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()],
});