Compare commits
22 Commits
ecca91b401
...
cad373568e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cad373568e | ||
|
|
cc5db77494 | ||
|
|
3142023c79 | ||
|
|
efae331aaf | ||
|
|
33c9368472 | ||
|
|
f28204416d | ||
|
|
6dc26611c3 | ||
|
|
ec2cd2cf56 | ||
|
|
2810c6b9fb | ||
|
|
83451cf358 | ||
|
|
e0592eb581 | ||
|
|
0f5be58093 | ||
|
|
240aaa81ae | ||
|
|
bd08e69065 | ||
|
|
82018cea2c | ||
|
|
5bc7d1d901 | ||
|
|
dba36ae458 | ||
|
|
4f5e56f855 | ||
|
|
b5b20b054d | ||
| fe52df1b7a | |||
|
|
a78467947d | ||
|
|
935b7f72e5 |
3
.env.example
Normal file
3
.env.example
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
TAYLOR_API_ENDPOINT=
|
||||||
|
TAYLOR_API_TOKEN=
|
||||||
|
TAYLOR_MEDIA_URL=
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -34,7 +34,7 @@ yarn-error.log*
|
|||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|||||||
@ -1,7 +1,15 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'media.bambooapp.ai',
|
||||||
|
pathname: '/files/**',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
|
"@pbe/react-yandex-maps": "^1.2.5",
|
||||||
"@radix-ui/react-collapsible": "^1.1.8",
|
"@radix-ui/react-collapsible": "^1.1.8",
|
||||||
"@radix-ui/react-dialog": "^1.1.11",
|
"@radix-ui/react-dialog": "^1.1.11",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.11",
|
"@radix-ui/react-dropdown-menu": "^2.1.11",
|
||||||
@ -23,13 +24,18 @@
|
|||||||
"@radix-ui/react-toast": "^1.2.11",
|
"@radix-ui/react-toast": "^1.2.11",
|
||||||
"@reduxjs/toolkit": "^2.7.0",
|
"@reduxjs/toolkit": "^2.7.0",
|
||||||
"aos": "^2.3.4",
|
"aos": "^2.3.4",
|
||||||
|
"axios": "^1.9.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cookies-next": "^5.1.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"embla-carousel-autoplay": "^8.6.0",
|
"embla-carousel-autoplay": "^8.6.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"json-to-graphql-query": "^2.3.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.501.0",
|
"lucide-react": "^0.501.0",
|
||||||
"next": "15.3.1",
|
"next": "15.3.1",
|
||||||
|
"next-redux-wrapper": "^8.1.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-day-picker": "8.10.1",
|
"react-day-picker": "8.10.1",
|
||||||
@ -52,7 +58,8 @@
|
|||||||
"@types/eslint": "^9.6.1",
|
"@types/eslint": "^9.6.1",
|
||||||
"@types/eslint-config-prettier": "^6.11.3",
|
"@types/eslint-config-prettier": "^6.11.3",
|
||||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||||
"@types/node": "^20",
|
"@types/lodash": "^4.17.16",
|
||||||
|
"@types/node": "^20.17.30",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@typescript-eslint/parser": "^8.30.1",
|
"@typescript-eslint/parser": "^8.30.1",
|
||||||
|
|||||||
151
pnpm-lock.yaml
generated
151
pnpm-lock.yaml
generated
@ -11,6 +11,9 @@ importers:
|
|||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^5.0.1
|
specifier: ^5.0.1
|
||||||
version: 5.0.1(react-hook-form@7.56.1(react@19.1.0))
|
version: 5.0.1(react-hook-form@7.56.1(react@19.1.0))
|
||||||
|
'@pbe/react-yandex-maps':
|
||||||
|
specifier: ^1.2.5
|
||||||
|
version: 1.2.5(react@19.1.0)
|
||||||
'@radix-ui/react-collapsible':
|
'@radix-ui/react-collapsible':
|
||||||
specifier: ^1.1.8
|
specifier: ^1.1.8
|
||||||
version: 1.1.8(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 1.1.8(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
@ -47,12 +50,18 @@ importers:
|
|||||||
aos:
|
aos:
|
||||||
specifier: ^2.3.4
|
specifier: ^2.3.4
|
||||||
version: 2.3.4
|
version: 2.3.4
|
||||||
|
axios:
|
||||||
|
specifier: ^1.9.0
|
||||||
|
version: 1.9.0
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
cookies-next:
|
||||||
|
specifier: ^5.1.0
|
||||||
|
version: 5.1.0(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
@ -62,12 +71,21 @@ importers:
|
|||||||
embla-carousel-react:
|
embla-carousel-react:
|
||||||
specifier: ^8.6.0
|
specifier: ^8.6.0
|
||||||
version: 8.6.0(react@19.1.0)
|
version: 8.6.0(react@19.1.0)
|
||||||
|
json-to-graphql-query:
|
||||||
|
specifier: ^2.3.0
|
||||||
|
version: 2.3.0
|
||||||
|
lodash:
|
||||||
|
specifier: ^4.17.21
|
||||||
|
version: 4.17.21
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.501.0
|
specifier: ^0.501.0
|
||||||
version: 0.501.0(react@19.1.0)
|
version: 0.501.0(react@19.1.0)
|
||||||
next:
|
next:
|
||||||
specifier: 15.3.1
|
specifier: 15.3.1
|
||||||
version: 15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
next-redux-wrapper:
|
||||||
|
specifier: ^8.1.0
|
||||||
|
version: 8.1.0(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.2)(react@19.1.0)(redux@5.0.1))(react@19.1.0)
|
||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.4.6
|
specifier: ^0.4.6
|
||||||
version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
@ -129,8 +147,11 @@ importers:
|
|||||||
'@types/eslint-plugin-tailwindcss':
|
'@types/eslint-plugin-tailwindcss':
|
||||||
specifier: ^3.17.0
|
specifier: ^3.17.0
|
||||||
version: 3.17.0
|
version: 3.17.0
|
||||||
|
'@types/lodash':
|
||||||
|
specifier: ^4.17.16
|
||||||
|
version: 4.17.16
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20
|
specifier: ^20.17.30
|
||||||
version: 20.17.30
|
version: 20.17.30
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^19
|
specifier: ^19
|
||||||
@ -508,6 +529,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
|
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
|
||||||
engines: {node: '>=12.4.0'}
|
engines: {node: '>=12.4.0'}
|
||||||
|
|
||||||
|
'@pbe/react-yandex-maps@1.2.5':
|
||||||
|
resolution: {integrity: sha512-cBojin5e1fPx9XVCAqHQJsCnHGMeBNsP0TrNfpWCrPFfxb30ye+JgcGr2mn767Gbr1d+RufBLRiUcX2kaiAwjQ==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.x || ^17.x || ^18.x
|
||||||
|
|
||||||
'@pkgr/core@0.2.4':
|
'@pkgr/core@0.2.4':
|
||||||
resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==}
|
resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==}
|
||||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||||
@ -1080,6 +1107,9 @@ packages:
|
|||||||
'@types/json5@0.0.29':
|
'@types/json5@0.0.29':
|
||||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||||
|
|
||||||
|
'@types/lodash@4.17.16':
|
||||||
|
resolution: {integrity: sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==}
|
||||||
|
|
||||||
'@types/node@20.17.30':
|
'@types/node@20.17.30':
|
||||||
resolution: {integrity: sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==}
|
resolution: {integrity: sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==}
|
||||||
|
|
||||||
@ -1094,6 +1124,9 @@ packages:
|
|||||||
'@types/use-sync-external-store@0.0.6':
|
'@types/use-sync-external-store@0.0.6':
|
||||||
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
|
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
|
||||||
|
|
||||||
|
'@types/yandex-maps@2.1.29':
|
||||||
|
resolution: {integrity: sha512-nuibRWj3RU/9KXlCzTrRtDE+n6V9l7NbT9JakicqZ5OXIdwyb6blvV2Uwn6lB58WYm3DSUDP2I2AWlnWMc8z2w==}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.30.1':
|
'@typescript-eslint/eslint-plugin@8.30.1':
|
||||||
resolution: {integrity: sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==}
|
resolution: {integrity: sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@ -1291,6 +1324,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
asynckit@0.4.0:
|
||||||
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
|
|
||||||
available-typed-arrays@1.0.7:
|
available-typed-arrays@1.0.7:
|
||||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -1299,6 +1335,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==}
|
resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
axios@1.9.0:
|
||||||
|
resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==}
|
||||||
|
|
||||||
axobject-query@4.1.0:
|
axobject-query@4.1.0:
|
||||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -1370,9 +1409,23 @@ packages:
|
|||||||
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
||||||
engines: {node: '>=12.5.0'}
|
engines: {node: '>=12.5.0'}
|
||||||
|
|
||||||
|
combined-stream@1.0.8:
|
||||||
|
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
|
||||||
|
cookie@1.0.2:
|
||||||
|
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
cookies-next@5.1.0:
|
||||||
|
resolution: {integrity: sha512-9Ekne+q8hfziJtnT9c1yDUBqT0eDMGgPrfPl4bpR3xwQHLTd/8gbSf6+IEkP/pjGsDZt1TGbC6emYmFYRbIXwQ==}
|
||||||
|
peerDependencies:
|
||||||
|
next: '>=15.0.0'
|
||||||
|
react: '>= 16.8.0'
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@ -1426,6 +1479,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
delayed-stream@1.0.0:
|
||||||
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
detect-libc@2.0.3:
|
detect-libc@2.0.3:
|
||||||
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
|
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -1696,10 +1753,23 @@ packages:
|
|||||||
flatted@3.3.3:
|
flatted@3.3.3:
|
||||||
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
||||||
|
|
||||||
|
follow-redirects@1.15.9:
|
||||||
|
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
peerDependencies:
|
||||||
|
debug: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
debug:
|
||||||
|
optional: true
|
||||||
|
|
||||||
for-each@0.3.5:
|
for-each@0.3.5:
|
||||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
form-data@4.0.2:
|
||||||
|
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
function-bind@1.1.2:
|
function-bind@1.1.2:
|
||||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||||
|
|
||||||
@ -1949,6 +2019,9 @@ packages:
|
|||||||
json-stable-stringify-without-jsonify@1.0.1:
|
json-stable-stringify-without-jsonify@1.0.1:
|
||||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||||
|
|
||||||
|
json-to-graphql-query@2.3.0:
|
||||||
|
resolution: {integrity: sha512-khZtaLLQ0HllFec+t89ZWduUZ0rmne/OpRm/39hyZUWDHNx9Yk4DgQzDtMeqd8zj2g5opBD4GHrdtH0JzKnN2g==}
|
||||||
|
|
||||||
json5@1.0.2:
|
json5@1.0.2:
|
||||||
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
|
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -2072,6 +2145,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
|
mime-db@1.52.0:
|
||||||
|
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
mime-types@2.1.35:
|
||||||
|
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
minimatch@10.0.1:
|
minimatch@10.0.1:
|
||||||
resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
|
resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@ -2102,6 +2183,13 @@ packages:
|
|||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
|
next-redux-wrapper@8.1.0:
|
||||||
|
resolution: {integrity: sha512-2hIau0hcI6uQszOtrvAFqgc0NkZegKYhBB7ZAKiG3jk7zfuQb4E7OV9jfxViqqojh3SEHdnFfPkN9KErttUKuw==}
|
||||||
|
peerDependencies:
|
||||||
|
next: '>=9'
|
||||||
|
react: '*'
|
||||||
|
react-redux: '*'
|
||||||
|
|
||||||
next-themes@0.4.6:
|
next-themes@0.4.6:
|
||||||
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -2282,6 +2370,9 @@ packages:
|
|||||||
prop-types@15.8.1:
|
prop-types@15.8.1:
|
||||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||||
|
|
||||||
|
proxy-from-env@1.1.0:
|
||||||
|
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||||
|
|
||||||
punycode@2.3.1:
|
punycode@2.3.1:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -2968,6 +3059,11 @@ snapshots:
|
|||||||
|
|
||||||
'@nolyfill/is-core-module@1.0.39': {}
|
'@nolyfill/is-core-module@1.0.39': {}
|
||||||
|
|
||||||
|
'@pbe/react-yandex-maps@1.2.5(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@types/yandex-maps': 2.1.29
|
||||||
|
react: 19.1.0
|
||||||
|
|
||||||
'@pkgr/core@0.2.4': {}
|
'@pkgr/core@0.2.4': {}
|
||||||
|
|
||||||
'@radix-ui/number@1.1.1': {}
|
'@radix-ui/number@1.1.1': {}
|
||||||
@ -3524,6 +3620,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/json5@0.0.29': {}
|
'@types/json5@0.0.29': {}
|
||||||
|
|
||||||
|
'@types/lodash@4.17.16': {}
|
||||||
|
|
||||||
'@types/node@20.17.30':
|
'@types/node@20.17.30':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.19.8
|
undici-types: 6.19.8
|
||||||
@ -3538,6 +3636,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/use-sync-external-store@0.0.6': {}
|
'@types/use-sync-external-store@0.0.6': {}
|
||||||
|
|
||||||
|
'@types/yandex-maps@2.1.29': {}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)':
|
'@typescript-eslint/eslint-plugin@8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.1
|
'@eslint-community/regexpp': 4.12.1
|
||||||
@ -3765,12 +3865,22 @@ snapshots:
|
|||||||
|
|
||||||
async-function@1.0.0: {}
|
async-function@1.0.0: {}
|
||||||
|
|
||||||
|
asynckit@0.4.0: {}
|
||||||
|
|
||||||
available-typed-arrays@1.0.7:
|
available-typed-arrays@1.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
possible-typed-array-names: 1.1.0
|
possible-typed-array-names: 1.1.0
|
||||||
|
|
||||||
axe-core@4.10.3: {}
|
axe-core@4.10.3: {}
|
||||||
|
|
||||||
|
axios@1.9.0:
|
||||||
|
dependencies:
|
||||||
|
follow-redirects: 1.15.9
|
||||||
|
form-data: 4.0.2
|
||||||
|
proxy-from-env: 1.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
|
||||||
axobject-query@4.1.0: {}
|
axobject-query@4.1.0: {}
|
||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
@ -3846,8 +3956,20 @@ snapshots:
|
|||||||
color-string: 1.9.1
|
color-string: 1.9.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
combined-stream@1.0.8:
|
||||||
|
dependencies:
|
||||||
|
delayed-stream: 1.0.0
|
||||||
|
|
||||||
concat-map@0.0.1: {}
|
concat-map@0.0.1: {}
|
||||||
|
|
||||||
|
cookie@1.0.2: {}
|
||||||
|
|
||||||
|
cookies-next@5.1.0(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
cookie: 1.0.2
|
||||||
|
next: 15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
react: 19.1.0
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
@ -3900,6 +4022,8 @@ snapshots:
|
|||||||
has-property-descriptors: 1.0.2
|
has-property-descriptors: 1.0.2
|
||||||
object-keys: 1.1.1
|
object-keys: 1.1.1
|
||||||
|
|
||||||
|
delayed-stream@1.0.0: {}
|
||||||
|
|
||||||
detect-libc@2.0.3: {}
|
detect-libc@2.0.3: {}
|
||||||
|
|
||||||
detect-node-es@1.1.0: {}
|
detect-node-es@1.1.0: {}
|
||||||
@ -4325,10 +4449,19 @@ snapshots:
|
|||||||
|
|
||||||
flatted@3.3.3: {}
|
flatted@3.3.3: {}
|
||||||
|
|
||||||
|
follow-redirects@1.15.9: {}
|
||||||
|
|
||||||
for-each@0.3.5:
|
for-each@0.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-callable: 1.2.7
|
is-callable: 1.2.7
|
||||||
|
|
||||||
|
form-data@4.0.2:
|
||||||
|
dependencies:
|
||||||
|
asynckit: 0.4.0
|
||||||
|
combined-stream: 1.0.8
|
||||||
|
es-set-tostringtag: 2.1.0
|
||||||
|
mime-types: 2.1.35
|
||||||
|
|
||||||
function-bind@1.1.2: {}
|
function-bind@1.1.2: {}
|
||||||
|
|
||||||
function.prototype.name@1.1.8:
|
function.prototype.name@1.1.8:
|
||||||
@ -4579,6 +4712,8 @@ snapshots:
|
|||||||
|
|
||||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||||
|
|
||||||
|
json-to-graphql-query@2.3.0: {}
|
||||||
|
|
||||||
json5@1.0.2:
|
json5@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
@ -4679,6 +4814,12 @@ snapshots:
|
|||||||
braces: 3.0.3
|
braces: 3.0.3
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
|
|
||||||
|
mime-db@1.52.0: {}
|
||||||
|
|
||||||
|
mime-types@2.1.35:
|
||||||
|
dependencies:
|
||||||
|
mime-db: 1.52.0
|
||||||
|
|
||||||
minimatch@10.0.1:
|
minimatch@10.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 2.0.1
|
brace-expansion: 2.0.1
|
||||||
@ -4701,6 +4842,12 @@ snapshots:
|
|||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
|
next-redux-wrapper@8.1.0(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.2)(react@19.1.0)(redux@5.0.1))(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
next: 15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
react: 19.1.0
|
||||||
|
react-redux: 9.2.0(@types/react@19.1.2)(react@19.1.0)(redux@5.0.1)
|
||||||
|
|
||||||
next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
@ -4842,6 +4989,8 @@ snapshots:
|
|||||||
object-assign: 4.1.1
|
object-assign: 4.1.1
|
||||||
react-is: 16.13.1
|
react-is: 16.13.1
|
||||||
|
|
||||||
|
proxy-from-env@1.1.0: {}
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|||||||
@ -1,308 +1,5 @@
|
|||||||
'use client';
|
import { CorporateDashboard } from "@/pages-templates/(dashboard)/corporate-dashboard";
|
||||||
|
|
||||||
import { format, subMonths } from 'date-fns';
|
export default function Corporate() {
|
||||||
import { ru } from 'date-fns/locale';
|
return <CorporateDashboard/>
|
||||||
import { Building2, CalendarIcon, LogOut, Wallet } from 'lucide-react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
|
||||||
import { Calendar } from '@/shared/shadcn-ui/calendar';
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/shared/shadcn-ui/card';
|
|
||||||
import { Label } from '@/shared/shadcn-ui/label';
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from '@/shared/shadcn-ui/popover';
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from '@/shared/shadcn-ui/table';
|
|
||||||
|
|
||||||
// import { CardsList } from '@/widgets/cards-list';
|
|
||||||
|
|
||||||
// Sample company data
|
|
||||||
const companyData = {
|
|
||||||
companyName: 'ООО «ТаджикТранс»',
|
|
||||||
numberOfCards: 12,
|
|
||||||
fund: 25000,
|
|
||||||
overdraft: 5000,
|
|
||||||
totalFund: 30000,
|
|
||||||
registrationDate: '10.03.2019',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sample transaction data
|
|
||||||
const generateTransactions = () => {
|
|
||||||
const stations = [
|
|
||||||
'АЗС Душанбе-Центр',
|
|
||||||
'АЗС Душанбе-Запад',
|
|
||||||
'АЗС Душанбе-Восток',
|
|
||||||
'АЗС Худжанд',
|
|
||||||
'АЗС Куляб',
|
|
||||||
];
|
|
||||||
|
|
||||||
const products = [
|
|
||||||
{ name: 'ДТ', price: 8.5 },
|
|
||||||
{ name: 'АИ-92', price: 9.2 },
|
|
||||||
{ name: 'АИ-95', price: 10.5 },
|
|
||||||
{ name: 'Z-100 Power', price: 11.8 },
|
|
||||||
{ name: 'Пропан', price: 6.3 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const transactions = [];
|
|
||||||
|
|
||||||
// Generate 50 random transactions over the last 6 months
|
|
||||||
for (let i = 0; i < 50; i++) {
|
|
||||||
const date = subMonths(new Date(), Math.random() * 6);
|
|
||||||
const station = stations[Math.floor(Math.random() * stations.length)];
|
|
||||||
const product = products[Math.floor(Math.random() * products.length)];
|
|
||||||
const quantity = Math.floor(Math.random() * 40) + 10; // 10-50 liters
|
|
||||||
const cost = product.price;
|
|
||||||
const total = quantity * cost;
|
|
||||||
|
|
||||||
transactions.push({
|
|
||||||
id: i + 1,
|
|
||||||
date,
|
|
||||||
station,
|
|
||||||
product: product.name,
|
|
||||||
quantity,
|
|
||||||
cost,
|
|
||||||
total,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by date (newest first)
|
|
||||||
return transactions.sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
||||||
};
|
|
||||||
|
|
||||||
const transactions = generateTransactions();
|
|
||||||
|
|
||||||
export default function CorporateDashboard() {
|
|
||||||
const [startDate, setStartDate] = useState<Date | undefined>(
|
|
||||||
subMonths(new Date(), 1),
|
|
||||||
);
|
|
||||||
const [endDate, setEndDate] = useState<Date | undefined>(new Date());
|
|
||||||
const [filteredTransactions, setFilteredTransactions] =
|
|
||||||
useState(transactions);
|
|
||||||
|
|
||||||
// Filter transactions by date range
|
|
||||||
const filterTransactions = () => {
|
|
||||||
if (!startDate || !endDate) return;
|
|
||||||
|
|
||||||
const filtered = transactions.filter((transaction) => {
|
|
||||||
const transactionDate = new Date(transaction.date);
|
|
||||||
return transactionDate >= startDate && transactionDate <= endDate;
|
|
||||||
});
|
|
||||||
|
|
||||||
setFilteredTransactions(filtered);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='flex min-h-screen flex-col'>
|
|
||||||
<main className='flex-1 py-10'>
|
|
||||||
<div className='container mx-auto max-w-6xl'>
|
|
||||||
<div className='mb-8 flex items-center justify-between'>
|
|
||||||
<h1 className='text-3xl font-bold'>Корпоративный кабинет</h1>
|
|
||||||
<Button variant='outline' className='gap-2'>
|
|
||||||
<LogOut className='h-4 w-4' />
|
|
||||||
Выйти
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mb-10 grid gap-3 md:grid-cols-3 md:gap-6'>
|
|
||||||
{/* Company Card */}
|
|
||||||
<Card className='md:col-span-2'>
|
|
||||||
<CardHeader className='pb-2'>
|
|
||||||
<CardTitle className='flex items-center gap-2'>
|
|
||||||
<Building2 className='h-5 w-5 text-red-600' />
|
|
||||||
Информация о компании
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className='grid gap-6 md:grid-cols-2'>
|
|
||||||
<div>
|
|
||||||
<div className='mb-4 space-y-1'>
|
|
||||||
<p className='text-sm text-gray-500'>Название компании</p>
|
|
||||||
<p className='font-medium'>{companyData.companyName}</p>
|
|
||||||
</div>
|
|
||||||
<div className='mb-4 space-y-1'>
|
|
||||||
<p className='text-sm text-gray-500'>Количество карт</p>
|
|
||||||
<p className='font-medium'>{companyData.numberOfCards}</p>
|
|
||||||
</div>
|
|
||||||
<div className='space-y-1'>
|
|
||||||
<p className='text-sm text-gray-500'>Дата регистрации</p>
|
|
||||||
<p className='font-medium'>
|
|
||||||
{companyData.registrationDate}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className='mb-4 space-y-1'>
|
|
||||||
<p className='text-sm text-gray-500'>Фонд</p>
|
|
||||||
<p className='font-medium'>
|
|
||||||
{companyData.fund.toLocaleString()} сомони
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className='mb-4 space-y-1'>
|
|
||||||
<p className='text-sm text-gray-500'>Овердрафт</p>
|
|
||||||
<p className='font-medium'>
|
|
||||||
{companyData.overdraft.toLocaleString()} сомони
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Fund Card */}
|
|
||||||
<Card className='bg-gradient-to-br from-red-600 to-red-800 text-white'>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className='flex items-center gap-2'>
|
|
||||||
<Wallet className='h-5 w-5' />
|
|
||||||
Общий фонд
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className='text-white/80'>
|
|
||||||
Доступные средства
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className='text-center'>
|
|
||||||
<p className='mb-2 text-4xl font-bold'>
|
|
||||||
{companyData.totalFund.toLocaleString()}
|
|
||||||
</p>
|
|
||||||
<p className='text-white/80'>сомони</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* <CardsList totalCards={companyData.numberOfCards} /> */}
|
|
||||||
|
|
||||||
{/* Transactions */}
|
|
||||||
<div className='space-y-6'>
|
|
||||||
<div className='flex flex-col items-start justify-between gap-4 md:flex-row md:items-center'>
|
|
||||||
<h2 className='text-2xl font-bold'>История операций</h2>
|
|
||||||
|
|
||||||
<div className='flex w-full flex-col gap-4 md:w-auto md:flex-row'>
|
|
||||||
<div className='grid grid-cols-2 gap-2'>
|
|
||||||
<div className='flex items-center gap-2'>
|
|
||||||
<Label htmlFor='start-date'>От</Label>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
className='w-full justify-start text-left font-normal'
|
|
||||||
>
|
|
||||||
<CalendarIcon className='mr-2 h-4 w-4' />
|
|
||||||
{startDate
|
|
||||||
? format(startDate, 'PP', { locale: ru })
|
|
||||||
: 'Выберите дату'}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className='w-auto p-0'>
|
|
||||||
<Calendar
|
|
||||||
mode='single'
|
|
||||||
selected={startDate}
|
|
||||||
onSelect={setStartDate}
|
|
||||||
initialFocus
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex items-center gap-2'>
|
|
||||||
<Label htmlFor='end-date'>До</Label>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
className='w-full justify-start text-left font-normal'
|
|
||||||
>
|
|
||||||
<CalendarIcon className='mr-2 h-4 w-4' />
|
|
||||||
{endDate
|
|
||||||
? format(endDate, 'PP', { locale: ru })
|
|
||||||
: 'Выберите дату'}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className='w-auto p-0'>
|
|
||||||
<Calendar
|
|
||||||
mode='single'
|
|
||||||
selected={endDate}
|
|
||||||
onSelect={setEndDate}
|
|
||||||
initialFocus
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className='mt-auto bg-red-600 hover:bg-red-700'
|
|
||||||
onClick={filterTransactions}
|
|
||||||
>
|
|
||||||
Применить
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='rounded-md border'>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Дата</TableHead>
|
|
||||||
<TableHead>Станция</TableHead>
|
|
||||||
<TableHead>Продукт</TableHead>
|
|
||||||
<TableHead className='text-right'>Кол-во (л)</TableHead>
|
|
||||||
<TableHead className='text-right'>Стоимость</TableHead>
|
|
||||||
<TableHead className='text-right'>Сумма</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{filteredTransactions.length > 0 ? (
|
|
||||||
filteredTransactions.map((transaction) => (
|
|
||||||
<TableRow key={transaction.id}>
|
|
||||||
<TableCell>
|
|
||||||
{format(transaction.date, 'dd.MM.yyyy')}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{transaction.station}</TableCell>
|
|
||||||
<TableCell>{transaction.product}</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
{transaction.quantity}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
{transaction.cost.toFixed(2)} сомони
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right font-medium'>
|
|
||||||
{transaction.total.toFixed(2)} сомони
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={6}
|
|
||||||
className='py-6 text-center text-gray-500'
|
|
||||||
>
|
|
||||||
Нет операций за выбранный период
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,110 +1,5 @@
|
|||||||
import { ArrowUpRight, Clock, CreditCard, LogOut, User } from 'lucide-react';
|
import { CustomerDashboard } from "@/pages-templates/(dashboard)/customer-dashboard";
|
||||||
|
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
export default function Customer() {
|
||||||
import {
|
return <CustomerDashboard/>
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/shared/shadcn-ui/card';
|
|
||||||
|
|
||||||
import { TransactionsTable } from '@/widgets/transactions-table';
|
|
||||||
|
|
||||||
// Sample customer data
|
|
||||||
const customerData = {
|
|
||||||
firstName: 'Алишер',
|
|
||||||
lastName: 'Рахмонов',
|
|
||||||
passportNumber: 'A12345678',
|
|
||||||
bonusPoints: 1250,
|
|
||||||
cardNumber: '5678-9012-3456-7890',
|
|
||||||
expiryDate: '12/2025',
|
|
||||||
registrationDate: '15.06.2020',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function CustomerDashboard() {
|
|
||||||
return (
|
|
||||||
<div className='flex min-h-screen flex-col'>
|
|
||||||
<main className='flex-1 py-10'>
|
|
||||||
<div className='container mx-auto max-w-6xl'>
|
|
||||||
<div className='mb-8 flex items-center justify-between'>
|
|
||||||
<h1 className='text-3xl font-bold'>Личный кабинет</h1>
|
|
||||||
<Button variant='outline' className='gap-2'>
|
|
||||||
<LogOut className='h-4 w-4' />
|
|
||||||
Выйти
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mb-10 grid gap-3 md:grid-cols-3 md:gap-6'>
|
|
||||||
{/* Bonus Card */}
|
|
||||||
<Card className='bg-gradient-to-br from-red-600 to-red-800 text-white'>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className='flex items-center gap-2'>
|
|
||||||
<CreditCard className='h-5 w-5' />
|
|
||||||
Бонусная карта
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className='text-white/80'>
|
|
||||||
Ваши накопленные бонусы
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className='text-center'>
|
|
||||||
<p className='mb-2 text-4xl font-bold'>
|
|
||||||
{customerData.bonusPoints}
|
|
||||||
</p>
|
|
||||||
<p className='text-white/80'>бонусных баллов</p>
|
|
||||||
</div>
|
|
||||||
<div className='mt-6 flex items-center justify-between'>
|
|
||||||
<div className='flex items-center gap-1 text-sm text-white/80'>
|
|
||||||
<Clock className='h-4 w-4' />
|
|
||||||
<span>Действует до: 31.12.2023</span>
|
|
||||||
</div>
|
|
||||||
<ArrowUpRight className='h-5 w-5 text-white/60' />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
{/* Customer Card */}
|
|
||||||
<Card className='md:col-span-2'>
|
|
||||||
<CardHeader className='pb-2'>
|
|
||||||
<CardTitle className='flex items-center gap-2'>
|
|
||||||
<User className='h-5 w-5 text-red-600' />
|
|
||||||
Информация о клиенте
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className='grid gap-6 md:grid-cols-2'>
|
|
||||||
<div>
|
|
||||||
<div className='mb-4 space-y-1'>
|
|
||||||
<p className='text-sm text-gray-500'>ФИО</p>
|
|
||||||
<p className='font-medium'>
|
|
||||||
{customerData.firstName} {customerData.lastName}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className='space-y-1'>
|
|
||||||
<p className='text-sm text-gray-500'>Дата регистрации</p>
|
|
||||||
<p className='font-medium'>
|
|
||||||
{customerData.registrationDate}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className='mb-4 space-y-1'>
|
|
||||||
<p className='text-sm text-gray-500'>Номер карты</p>
|
|
||||||
<p className='font-medium'>{customerData.cardNumber}</p>
|
|
||||||
</div>
|
|
||||||
<div className='mb-4 space-y-1'>
|
|
||||||
<p className='text-sm text-gray-500'>Срок действия</p>
|
|
||||||
<p className='font-medium'>{customerData.expiryDate}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TransactionsTable />
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
55
src/app/api-utlities/@types/index.ts
Normal file
55
src/app/api-utlities/@types/index.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
export type Root<T> = { records: T[] };
|
||||||
|
|
||||||
|
export interface Image {
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Select {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Discount = Root<{
|
||||||
|
_name: string;
|
||||||
|
_opisanie: string;
|
||||||
|
_do: string;
|
||||||
|
_foto: Image[];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type Job = Root<{
|
||||||
|
id: number;
|
||||||
|
_name: string;
|
||||||
|
_type: Select[];
|
||||||
|
_localtio: Select[];
|
||||||
|
_tags: Select[];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type Partner = Root<{
|
||||||
|
id: number;
|
||||||
|
_name: string;
|
||||||
|
_image: Image[];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type Station = Root<{
|
||||||
|
_name: string;
|
||||||
|
_opisanie: string;
|
||||||
|
_adress: string;
|
||||||
|
_chasyRaboty: Select;
|
||||||
|
_lat: number;
|
||||||
|
_long: number;
|
||||||
|
_avtomojka: boolean;
|
||||||
|
_dtCopy: boolean;
|
||||||
|
_ai92Copy: boolean;
|
||||||
|
_ai95Copy: boolean;
|
||||||
|
_z100Copy: boolean;
|
||||||
|
_propanCopy: boolean;
|
||||||
|
_zaryadnayaStanci: boolean;
|
||||||
|
_miniMarketCop: boolean;
|
||||||
|
_region: Select[];
|
||||||
|
_foto: Image[];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type TextResponse = Root<{
|
||||||
|
_name: string;
|
||||||
|
_znachenie: string | null;
|
||||||
|
}>;
|
||||||
18
src/app/api-utlities/@types/main.ts
Normal file
18
src/app/api-utlities/@types/main.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import {
|
||||||
|
presentDiscounts,
|
||||||
|
presentJobs,
|
||||||
|
presentPartners,
|
||||||
|
presentStations,
|
||||||
|
} from '../presenters';
|
||||||
|
|
||||||
|
export type Partners = ReturnType<typeof presentPartners>;
|
||||||
|
export type Jobs = ReturnType<typeof presentJobs>;
|
||||||
|
export type Discounts = ReturnType<typeof presentDiscounts>;
|
||||||
|
export type Stations = ReturnType<typeof presentStations>;
|
||||||
|
|
||||||
|
export type MainPageData = {
|
||||||
|
discounts: Discounts;
|
||||||
|
jobs: Jobs;
|
||||||
|
partners: Partners;
|
||||||
|
stations: Stations;
|
||||||
|
};
|
||||||
65
src/app/api-utlities/presenters/index.ts
Normal file
65
src/app/api-utlities/presenters/index.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Discount,
|
||||||
|
Image,
|
||||||
|
Job,
|
||||||
|
Partner,
|
||||||
|
Station,
|
||||||
|
TextResponse,
|
||||||
|
} from '../@types';
|
||||||
|
|
||||||
|
export const presentImage = (images: Image[]) =>
|
||||||
|
isEmpty(images) ? null : `${process.env.TAYLOR_MEDIA_URL}/${images[0].url}`;
|
||||||
|
|
||||||
|
export const presentPartners = (partners: Partner) =>
|
||||||
|
partners.records.map((record, index) => ({
|
||||||
|
id: index + 1,
|
||||||
|
name: record._name,
|
||||||
|
poster: presentImage(record._image),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const presentJobs = (jobs: Job) =>
|
||||||
|
jobs.records.map((job, index) => ({
|
||||||
|
id: index + 1,
|
||||||
|
name: job._name,
|
||||||
|
tags: job._tags.map((tag) => tag.name),
|
||||||
|
location: !isEmpty(job._localtio) ? job._localtio[0].name : null,
|
||||||
|
type: !isEmpty(job._type) ? job._type[0].name : null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const presentDiscounts = (discounts: Discount) =>
|
||||||
|
discounts.records.map((discount, index) => ({
|
||||||
|
id: index + 1,
|
||||||
|
name: discount._name,
|
||||||
|
description: discount._opisanie,
|
||||||
|
expiresAt: discount._do,
|
||||||
|
image: presentImage(discount._foto),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const presentStations = (stations: Station) =>
|
||||||
|
stations.records.map((station, index) => ({
|
||||||
|
id: index + 1,
|
||||||
|
name: station._name,
|
||||||
|
description: station._opisanie,
|
||||||
|
address: station._adress,
|
||||||
|
workingHours: station._chasyRaboty?.name || null,
|
||||||
|
latitude: station._lat,
|
||||||
|
longitude: station._long,
|
||||||
|
carWash: station._avtomojka || false,
|
||||||
|
ai92: station._dtCopy || false,
|
||||||
|
ai95: station._ai92Copy || false,
|
||||||
|
z100: station._ai95Copy || false,
|
||||||
|
propan: station._z100Copy || false,
|
||||||
|
electricCharge: station._propanCopy || false,
|
||||||
|
miniMarket: station._zaryadnayaStanci || false,
|
||||||
|
toilet: station._miniMarketCop || false,
|
||||||
|
region: !isEmpty(station._region) ? station._region[0].name : null,
|
||||||
|
image: presentImage(station._foto),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const presentTexts = (texts: TextResponse) =>
|
||||||
|
texts.records.map((item) => ({
|
||||||
|
key: item._name,
|
||||||
|
value: item._znachenie,
|
||||||
|
}));
|
||||||
78
src/app/api-utlities/requests/common.ts
Normal file
78
src/app/api-utlities/requests/common.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
export const stationsRequest = {
|
||||||
|
_azs: {
|
||||||
|
records: {
|
||||||
|
_name: true,
|
||||||
|
_opisanie: true,
|
||||||
|
_adress: true,
|
||||||
|
_chasyRaboty: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
_lat: true,
|
||||||
|
_long: true,
|
||||||
|
_avtomojka: true,
|
||||||
|
_dtCopy: true, // ai92
|
||||||
|
_ai92Copy: true, // ai95
|
||||||
|
_ai95Copy: true, // z100
|
||||||
|
_z100Copy: true, // propan
|
||||||
|
_propanCopy: true, // electricCharge
|
||||||
|
_zaryadnayaStanci: true, // miniMarket
|
||||||
|
_miniMarketCop: true, // toilet
|
||||||
|
_region: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
_foto: {
|
||||||
|
url: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const partnersRequest = {
|
||||||
|
_partners: {
|
||||||
|
records: {
|
||||||
|
_name: true,
|
||||||
|
_image: {
|
||||||
|
url: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const jobsRequest = {
|
||||||
|
_vacancies: {
|
||||||
|
records: {
|
||||||
|
_name: true,
|
||||||
|
_tags: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
_type: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
_localtio: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const discountsRequest = {
|
||||||
|
_akcii: {
|
||||||
|
records: {
|
||||||
|
_name: true,
|
||||||
|
_opisanie: true,
|
||||||
|
_do: true,
|
||||||
|
_foto: {
|
||||||
|
url: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const textsRequest = {
|
||||||
|
_kontentSajta: {
|
||||||
|
records: {
|
||||||
|
_name: true,
|
||||||
|
_znachenie: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
13
src/app/api-utlities/requests/main-page.request.ts
Normal file
13
src/app/api-utlities/requests/main-page.request.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import {
|
||||||
|
discountsRequest,
|
||||||
|
jobsRequest,
|
||||||
|
partnersRequest,
|
||||||
|
stationsRequest,
|
||||||
|
} from './common';
|
||||||
|
|
||||||
|
export const mainPageRequest = {
|
||||||
|
...partnersRequest,
|
||||||
|
...jobsRequest,
|
||||||
|
...discountsRequest,
|
||||||
|
...stationsRequest,
|
||||||
|
};
|
||||||
10
src/app/api-utlities/utilities/oriyo.client.ts
Normal file
10
src/app/api-utlities/utilities/oriyo.client.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Axios } from 'axios';
|
||||||
|
|
||||||
|
const oriyoClient = new Axios({
|
||||||
|
baseURL: process.env.ORIOYO_API_ENDPOINT || '',
|
||||||
|
headers: {
|
||||||
|
'Content-type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default oriyoClient;
|
||||||
25
src/app/api-utlities/utilities/taylor.client.ts
Normal file
25
src/app/api-utlities/utilities/taylor.client.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { jsonToGraphQLQuery } from 'json-to-graphql-query';
|
||||||
|
|
||||||
|
export const requestTaylor = async (query: object, variables?: object) => {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
query: jsonToGraphQLQuery({ query }),
|
||||||
|
variables,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(process.env.TAYLOR_API_ENDPOINT || '', {
|
||||||
|
body,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: process.env.TAYLOR_API_TOKEN || '',
|
||||||
|
'Content-type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsedResponse = await response.json();
|
||||||
|
|
||||||
|
if (parsedResponse.errors) {
|
||||||
|
throw parsedResponse.errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedResponse;
|
||||||
|
};
|
||||||
50
src/app/api/auth/login/route.ts
Normal file
50
src/app/api/auth/login/route.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import oriyoClient from '@/app/api-utlities/utilities/oriyo.client';
|
||||||
|
|
||||||
|
import { loginFormSchema } from '@/entities/auth/model/validation/login-form.schema';
|
||||||
|
|
||||||
|
import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
|
||||||
|
|
||||||
|
const routeHandler = async (req: NextRequest) => {
|
||||||
|
const body = await req.json();
|
||||||
|
|
||||||
|
const validatedBody = loginFormSchema
|
||||||
|
.merge(z.object({ type: z.enum(['bonus', 'corporate']) }))
|
||||||
|
.parse(body);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const oriyoResponse = await oriyoClient.get('/client/login', {
|
||||||
|
params: {
|
||||||
|
type: validatedBody.type,
|
||||||
|
phone: validatedBody.phoneNumber,
|
||||||
|
uid: validatedBody.cardNumber,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { token, card_id } = JSON.parse(oriyoResponse.data);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json({ error: 'Credentials error' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = NextResponse.json({ success: true });
|
||||||
|
|
||||||
|
response.cookies.set(
|
||||||
|
`${validatedBody.type}__token`,
|
||||||
|
JSON.stringify({ token, card_id }),
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
maxAge: 2 * 60 * 60,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('login error:', error);
|
||||||
|
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const POST = validationErrorHandler(routeHandler);
|
||||||
31
src/app/api/bonus/info/route.ts
Normal file
31
src/app/api/bonus/info/route.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import oriyoClient from '@/app/api-utlities/utilities/oriyo.client';
|
||||||
|
|
||||||
|
import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
|
||||||
|
|
||||||
|
export const routeHandler = async (req: NextRequest) => {
|
||||||
|
const bonusTokenData = req.cookies.get('bonus__token');
|
||||||
|
|
||||||
|
if (!bonusTokenData) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'User does not have access' },
|
||||||
|
{ status: 401 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { card_id, token } = JSON.parse(bonusTokenData.value);
|
||||||
|
|
||||||
|
const oriyoResponse = await oriyoClient.get('/client/info', {
|
||||||
|
params: {
|
||||||
|
card_id,
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(oriyoResponse.data, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GET = validationErrorHandler(routeHandler);
|
||||||
18
src/app/api/middlewares/error-handler.middleware.ts
Normal file
18
src/app/api/middlewares/error-handler.middleware.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { ZodError } from 'zod';
|
||||||
|
|
||||||
|
export const validationErrorHandler =
|
||||||
|
(handler: Function) => async (req: NextRequest, res: NextResponse) => {
|
||||||
|
try {
|
||||||
|
return await handler(req, res);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ZodError)
|
||||||
|
return NextResponse.json({ message: error.format() }, { status: 400 });
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: 'Server died for some reason' },
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
28
src/app/api/pages/main/route.ts
Normal file
28
src/app/api/pages/main/route.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import {
|
||||||
|
presentDiscounts,
|
||||||
|
presentJobs,
|
||||||
|
presentPartners,
|
||||||
|
presentStations,
|
||||||
|
} from '@/app/api-utlities/presenters';
|
||||||
|
import { mainPageRequest } from '@/app/api-utlities/requests/main-page.request';
|
||||||
|
import { requestTaylor } from '@/app/api-utlities/utilities/taylor.client';
|
||||||
|
|
||||||
|
import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
|
||||||
|
|
||||||
|
const routeHandler = async (request: Request) => {
|
||||||
|
const response = await requestTaylor(mainPageRequest);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
partners: presentPartners(response.data._partners),
|
||||||
|
jobs: presentJobs(response.data._vacancies),
|
||||||
|
discounts: presentDiscounts(response.data._akcii),
|
||||||
|
stations: presentStations(response.data._azs),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GET = validationErrorHandler(routeHandler);
|
||||||
14
src/app/api/text/route.ts
Normal file
14
src/app/api/text/route.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { presentTexts } from '@/app/api-utlities/presenters';
|
||||||
|
import { textsRequest } from '@/app/api-utlities/requests/common';
|
||||||
|
import { requestTaylor } from '@/app/api-utlities/utilities/taylor.client';
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const response = await requestTaylor(textsRequest);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify(presentTexts(response.data._kontentSajta)),
|
||||||
|
{
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,304 +1,5 @@
|
|||||||
import {
|
import { CharityPage } from "@/pages-templates/charity"
|
||||||
Calendar,
|
|
||||||
CheckCircle,
|
|
||||||
Heart,
|
|
||||||
Landmark,
|
|
||||||
MapPin,
|
|
||||||
Users,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
export default function Charity() {
|
||||||
import {
|
return <CharityPage />
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/shared/shadcn-ui/card';
|
|
||||||
|
|
||||||
import { CtaSection } from '@/widgets/cta-section';
|
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: 'Благотворительность | GasNetwork - Сеть заправок в Таджикистане',
|
|
||||||
description:
|
|
||||||
'Благотворительные проекты и инициативы GasNetwork. Мы помогаем обществу и заботимся о будущем.',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function CharityPage() {
|
|
||||||
return (
|
|
||||||
<div className='flex min-h-screen flex-col'>
|
|
||||||
<main className='flex-1'>
|
|
||||||
{/* Hero Section */}
|
|
||||||
<section className='relative'>
|
|
||||||
<div className='relative h-[400px] w-full overflow-hidden'>
|
|
||||||
<Image
|
|
||||||
src='/placeholder.svg?height=500&width=1920&text=Благотворительный+фонд+GasNetwork'
|
|
||||||
alt='Благотворительный фонд GasNetwork'
|
|
||||||
width={1920}
|
|
||||||
height={500}
|
|
||||||
className='object-cover'
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'>
|
|
||||||
<div className='container mx-auto'>
|
|
||||||
<div className='max-w-2xl space-y-6 text-white'>
|
|
||||||
<div className='inline-flex items-center justify-center rounded-full bg-red-600/20 p-2'>
|
|
||||||
<Heart className='size-6 text-red-500' />
|
|
||||||
</div>
|
|
||||||
<h1 className='text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl'>
|
|
||||||
Благотворительный фонд GasNetwork
|
|
||||||
</h1>
|
|
||||||
<p className='text-xl text-gray-200'>
|
|
||||||
Мы верим, что бизнес должен быть социально ответственным.
|
|
||||||
Наш фонд поддерживает образование, здравоохранение и
|
|
||||||
экологические инициативы в Таджикистане.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Mission Section */}
|
|
||||||
<section className='py-16'>
|
|
||||||
<div className='container mx-auto'>
|
|
||||||
<div className='grid items-center gap-12 md:grid-cols-2'>
|
|
||||||
<div>
|
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
|
||||||
<Heart className='h-6 w-6 text-red-600' />
|
|
||||||
</div>
|
|
||||||
<h2 className='mb-6 text-3xl font-bold tracking-tight sm:text-4xl'>
|
|
||||||
Наша миссия
|
|
||||||
</h2>
|
|
||||||
<p className='mb-6 text-gray-600'>
|
|
||||||
Благотворительный фонд GasNetwork был создан в 2020 году с
|
|
||||||
целью поддержки социально значимых проектов в Таджикистане. Мы
|
|
||||||
стремимся внести свой вклад в развитие общества и помочь тем,
|
|
||||||
кто в этом нуждается.
|
|
||||||
</p>
|
|
||||||
<p className='mb-6 text-gray-600'>
|
|
||||||
Наша миссия — создавать возможности для улучшения жизни людей
|
|
||||||
через образование, здравоохранение, экологические инициативы и
|
|
||||||
поддержку уязвимых групп населения.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className='space-y-4'>
|
|
||||||
<div className='flex items-start'>
|
|
||||||
<CheckCircle className='mr-3 h-6 w-6 flex-shrink-0 text-red-600' />
|
|
||||||
<div>
|
|
||||||
<h3 className='text-lg font-medium'>Прозрачность</h3>
|
|
||||||
<p className='text-gray-600'>
|
|
||||||
Мы публикуем ежегодные отчеты о всех наших проектах и
|
|
||||||
расходах, обеспечивая полную прозрачность нашей
|
|
||||||
деятельности.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-start'>
|
|
||||||
<CheckCircle className='mr-3 h-6 w-6 flex-shrink-0 text-red-600' />
|
|
||||||
<div>
|
|
||||||
<h3 className='text-lg font-medium'>Эффективность</h3>
|
|
||||||
<p className='text-gray-600'>
|
|
||||||
Мы тщательно выбираем проекты, которые могут принести
|
|
||||||
максимальную пользу обществу и имеют долгосрочное
|
|
||||||
влияние.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-start'>
|
|
||||||
<CheckCircle className='mr-3 h-6 w-6 flex-shrink-0 text-red-600' />
|
|
||||||
<div>
|
|
||||||
<h3 className='text-lg font-medium'>Сотрудничество</h3>
|
|
||||||
<p className='text-gray-600'>
|
|
||||||
Мы сотрудничаем с местными и международными
|
|
||||||
организациями для достижения наибольшего эффекта от
|
|
||||||
наших инициатив.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='relative h-[500px] overflow-hidden rounded-xl shadow-xl'>
|
|
||||||
<Image
|
|
||||||
src='/placeholder.svg?height=500&width=600&text=Наша+миссия'
|
|
||||||
alt='Наша миссия'
|
|
||||||
fill
|
|
||||||
className='object-cover'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Key Figures */}
|
|
||||||
<section className='bg-red-600 py-16 text-white'>
|
|
||||||
<div className='container mx-auto'>
|
|
||||||
<div className='mb-12 text-center'>
|
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
|
||||||
Наш вклад в цифрах
|
|
||||||
</h2>
|
|
||||||
<p className='mx-auto max-w-2xl text-white/80'>
|
|
||||||
За время существования нашего фонда мы достигли значительных
|
|
||||||
результатов
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className='grid grid-cols-1 gap-8 text-center md:grid-cols-3'>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<h3 className='text-4xl font-bold'>15+</h3>
|
|
||||||
<p className='text-white/80'>Реализованных проектов</p>
|
|
||||||
</div>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<h3 className='text-4xl font-bold'>1.2M</h3>
|
|
||||||
<p className='text-white/80'>Сомони пожертвований</p>
|
|
||||||
</div>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<h3 className='text-4xl font-bold'>5000+</h3>
|
|
||||||
<p className='text-white/80'>Людей получили помощь</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Upcoming Events */}
|
|
||||||
<section className='bg-gray-50 py-16'>
|
|
||||||
<div className='container mx-auto'>
|
|
||||||
<div className='mb-12 text-center'>
|
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
|
||||||
<Calendar className='h-6 w-6 text-red-600' />
|
|
||||||
</div>
|
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
|
||||||
Предстоящие мероприятия
|
|
||||||
</h2>
|
|
||||||
<p className='mx-auto max-w-2xl text-gray-600'>
|
|
||||||
Присоединяйтесь к нашим благотворительным мероприятиям и внесите
|
|
||||||
свой вклад в общее дело
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='grid gap-6 md:grid-cols-3'>
|
|
||||||
{[
|
|
||||||
{
|
|
||||||
title: 'Благотворительный марафон',
|
|
||||||
description:
|
|
||||||
'Ежегодный благотворительный марафон в поддержку детей с особыми потребностями.',
|
|
||||||
date: '15 июня 2023',
|
|
||||||
location: 'Парк Рудаки, Душанбе',
|
|
||||||
image: '/placeholder.svg?height=200&width=300&text=Марафон',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Экологическая акция',
|
|
||||||
description:
|
|
||||||
'Очистка берегов реки Варзоб от мусора и посадка деревьев.',
|
|
||||||
date: '22 июля 2023',
|
|
||||||
location: 'Река Варзоб, Душанбе',
|
|
||||||
image:
|
|
||||||
'/placeholder.svg?height=200&width=300&text=Экологическая+акция',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Сбор школьных принадлежностей',
|
|
||||||
description:
|
|
||||||
'Сбор школьных принадлежностей для детей из малообеспеченных семей к новому учебному году.',
|
|
||||||
date: '1-20 августа 2023',
|
|
||||||
location: 'Все заправки GasNetwork',
|
|
||||||
image:
|
|
||||||
'/placeholder.svg?height=200&width=300&text=Школьные+принадлежности',
|
|
||||||
},
|
|
||||||
].map((event, index) => (
|
|
||||||
<Card key={index} className='overflow-hidden'>
|
|
||||||
<div className='relative h-48 w-full'>
|
|
||||||
<Image
|
|
||||||
src={event.image || '/placeholder.svg'}
|
|
||||||
alt={event.title}
|
|
||||||
fill
|
|
||||||
className='object-cover'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{event.title}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className='space-y-4'>
|
|
||||||
<p className='text-gray-600'>{event.description}</p>
|
|
||||||
<div className='flex items-center text-sm text-gray-500'>
|
|
||||||
<Calendar className='mr-2 h-4 w-4' />
|
|
||||||
{event.date}
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center text-sm text-gray-500'>
|
|
||||||
<MapPin className='mr-2 h-4 w-4' />
|
|
||||||
{event.location}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter>
|
|
||||||
<Button className='w-full bg-red-600 hover:bg-red-700'>
|
|
||||||
Принять участие
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* How to Help */}
|
|
||||||
<section className='py-16'>
|
|
||||||
<div className='container mx-auto'>
|
|
||||||
<div className='mb-12 text-center'>
|
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
|
||||||
<Users className='h-6 w-6 text-red-600' />
|
|
||||||
</div>
|
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
|
||||||
Как вы можете помочь
|
|
||||||
</h2>
|
|
||||||
<p className='mx-auto max-w-2xl text-gray-600'>
|
|
||||||
Есть много способов внести свой вклад в наши благотворительные
|
|
||||||
инициативы
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='grid gap-3 md:grid-cols-2 md:gap-6 lg:grid-cols-4'>
|
|
||||||
{[
|
|
||||||
{
|
|
||||||
title: 'Сделать пожертвование',
|
|
||||||
description:
|
|
||||||
'Ваше пожертвование поможет нам реализовать больше проектов и помочь большему количеству людей.',
|
|
||||||
icon: <Landmark className='h-10 w-10 text-red-600' />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Стать волонтером',
|
|
||||||
description:
|
|
||||||
'Присоединяйтесь к нашей команде волонтеров и помогайте нам в реализации благотворительных проектов.',
|
|
||||||
icon: <Users className='h-10 w-10 text-red-600' />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Участвовать в мероприятиях',
|
|
||||||
description:
|
|
||||||
'Принимайте участие в наших благотворительных мероприятиях и акциях.',
|
|
||||||
icon: <Calendar className='h-10 w-10 text-red-600' />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Распространять информацию',
|
|
||||||
description:
|
|
||||||
'Расскажите о нашем фонде и его деятельности своим друзьям и знакомым.',
|
|
||||||
icon: <Heart className='h-10 w-10 text-red-600' />,
|
|
||||||
},
|
|
||||||
].map((item, index) => (
|
|
||||||
<Card key={index} className='text-center'>
|
|
||||||
<CardHeader>
|
|
||||||
<div className='mb-4 flex justify-center'>{item.icon}</div>
|
|
||||||
<CardTitle className='break-words hyphens-auto'>
|
|
||||||
{item.title}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<p className='text-gray-600'>{item.description}</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<CtaSection />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@ -1,7 +1,10 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { Inter } from 'next/font/google';
|
import { Inter } from 'next/font/google';
|
||||||
|
|
||||||
|
import { textControlApi } from '@/shared/language/api/text-control.api';
|
||||||
import { Providers } from '@/shared/providers/providers';
|
import { Providers } from '@/shared/providers/providers';
|
||||||
|
import { makeStore } from '@/shared/store';
|
||||||
|
import { TextItem } from '@/shared/types/text.types';
|
||||||
|
|
||||||
import { Footer } from '@/widgets/footer';
|
import { Footer } from '@/widgets/footer';
|
||||||
import { Header } from '@/widgets/header/ui';
|
import { Header } from '@/widgets/header/ui';
|
||||||
@ -16,20 +19,26 @@ export const metadata: Metadata = {
|
|||||||
'Качественное топливо, удобное расположение и отличный сервис для наших клиентов',
|
'Качественное топливо, удобное расположение и отличный сервис для наших клиентов',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
const store = makeStore();
|
||||||
|
|
||||||
|
const response = await store.dispatch(
|
||||||
|
textControlApi.endpoints.fetchText.initiate(),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang='en'
|
lang='ru'
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
className='scroll-smooth'
|
className='scroll-smooth'
|
||||||
style={{ scrollBehavior: 'smooth' }}
|
style={{ scrollBehavior: 'smooth' }}
|
||||||
>
|
>
|
||||||
<body className={`${inter.className} antialiased`}>
|
<body className={`${inter.className} antialiased min-w-2xs`}>
|
||||||
<Providers>
|
<Providers textItems={response.data as TextItem[]}>
|
||||||
<Header />
|
<Header />
|
||||||
{children}
|
{children}
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import LoginPage from "@/pages-templates/login";
|
import LoginPage from '@/pages-templates/login';
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
return <LoginPage/>
|
return <LoginPage />;
|
||||||
}
|
}
|
||||||
@ -8,16 +8,23 @@ import { PromotionsSection } from '@/widgets/promotions-section';
|
|||||||
import { StatsSection } from '@/widgets/stats-section';
|
import { StatsSection } from '@/widgets/stats-section';
|
||||||
import { VacanciesSection } from '@/widgets/vacancies-section';
|
import { VacanciesSection } from '@/widgets/vacancies-section';
|
||||||
|
|
||||||
export default function Home() {
|
import { MainPageData } from './api-utlities/@types/main';
|
||||||
|
|
||||||
|
export default async function Home() {
|
||||||
|
const mainPageData = (await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_BASE_URL}/api/pages/main`,
|
||||||
|
{ method: 'GET' },
|
||||||
|
).then((res) => res.json())) as MainPageData;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
<StatsSection />
|
<StatsSection />
|
||||||
<MapSection />
|
<MapSection stations={mainPageData.stations} />
|
||||||
<AboutSection />
|
<AboutSection />
|
||||||
<PromotionsSection />
|
<PromotionsSection discounts={mainPageData.discounts} />
|
||||||
<VacanciesSection />
|
<VacanciesSection jobs={mainPageData.jobs} />
|
||||||
<PartnersSection />
|
<PartnersSection partners={mainPageData.partners} />
|
||||||
<CharitySection />
|
<CharitySection />
|
||||||
<CtaSection />
|
<CtaSection />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
23
src/entities/auth/api/login.api.ts
Normal file
23
src/entities/auth/api/login.api.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { baseAPI } from '@/shared/api/base-api';
|
||||||
|
|
||||||
|
import { LoginParams, LoginResponse } from '../model/contracts/login.contract';
|
||||||
|
|
||||||
|
export const authenticationApi = baseAPI.injectEndpoints({
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
login: builder.query<LoginResponse, LoginParams>({
|
||||||
|
query: (data) => {
|
||||||
|
return {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
type: data.type,
|
||||||
|
phoneNumber: data.phoneNumber,
|
||||||
|
cardNumber: data.cardNumber,
|
||||||
|
},
|
||||||
|
url: '/auth/login',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { useLazyLoginQuery } = authenticationApi;
|
||||||
1
src/entities/auth/index.ts
Normal file
1
src/entities/auth/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { useLazyLoginQuery } from './api/login.api';
|
||||||
9
src/entities/auth/model/contracts/login.contract.ts
Normal file
9
src/entities/auth/model/contracts/login.contract.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { LoginFormData } from '@/entities/auth/model/validation/login-form.schema';
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginParams extends LoginFormData {
|
||||||
|
type: 'bonus' | 'corporate';
|
||||||
|
}
|
||||||
7
src/entities/auth/model/types/index.ts
Normal file
7
src/entities/auth/model/types/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface LoginData {
|
||||||
|
card_id: number;
|
||||||
|
created_at: string;
|
||||||
|
phone: string;
|
||||||
|
token: string;
|
||||||
|
uid: string;
|
||||||
|
}
|
||||||
20
src/entities/auth/model/validation/login-form.schema.ts
Normal file
20
src/entities/auth/model/validation/login-form.schema.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const loginFormSchema = z.object({
|
||||||
|
phoneNumber: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.regex(/^[0-9+\-() ]*$/, {
|
||||||
|
message:
|
||||||
|
'Номер телефона может содержать только цифры, пробелы и следующие символы: + - ( )',
|
||||||
|
})
|
||||||
|
.min(5, 'Номер телефона слишком короткий. Введите полный номер телефона')
|
||||||
|
.max(13, 'Номер телефона не может быть длиннее 13 символов'),
|
||||||
|
cardNumber: z
|
||||||
|
.string()
|
||||||
|
.min(6, 'Неверный номер карты. Введите полный номер карты')
|
||||||
|
.max(20, 'Номер карты не может быть длиннее 20 символов')
|
||||||
|
.trim(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type LoginFormData = z.infer<typeof loginFormSchema>;
|
||||||
17
src/entities/bonus/api/bonus.api.ts
Normal file
17
src/entities/bonus/api/bonus.api.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { baseAPI } from '@/shared/api/base-api';
|
||||||
|
|
||||||
|
import { ClientInfo } from '../model/types/bonus-client-info.type';
|
||||||
|
|
||||||
|
export const bonusApi = baseAPI.injectEndpoints({
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
fetchMyBonusInfo: builder.query<ClientInfo, any>({
|
||||||
|
query: () => {
|
||||||
|
return {
|
||||||
|
url: '/bonus/info',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { useFetchMyBonusInfoQuery } = bonusApi;
|
||||||
8
src/entities/bonus/model/types/bonus-client-info.type.ts
Normal file
8
src/entities/bonus/model/types/bonus-client-info.type.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export interface ClientInfo {
|
||||||
|
card_id: number;
|
||||||
|
fullname: string;
|
||||||
|
cardno: string;
|
||||||
|
reg_date: string;
|
||||||
|
end_date: string;
|
||||||
|
bonuses: string;
|
||||||
|
}
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const loginFormSchema = z.object({
|
|
||||||
phoneNumber: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.regex(/^[0-9+\-() ]*$/, {
|
|
||||||
message:
|
|
||||||
'Phone number can only contain numbers, spaces, and the following symbols: + - ( )',
|
|
||||||
})
|
|
||||||
.refine((val) => !val || val.length >= 5, {
|
|
||||||
message:
|
|
||||||
'Phone number is too short. Please enter a complete phone number',
|
|
||||||
}),
|
|
||||||
cardNumber: z.string().min(16).trim(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type LoginFormData = z.infer<typeof loginFormSchema>;
|
|
||||||
@ -5,7 +5,10 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { useLazyLoginQuery } from '@/entities/auth';
|
||||||
|
|
||||||
|
import { SubmitButton } from '@/shared/components/submit-button';
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -16,18 +19,20 @@ import {
|
|||||||
} from '@/shared/shadcn-ui/form';
|
} from '@/shared/shadcn-ui/form';
|
||||||
import { Input } from '@/shared/shadcn-ui/input';
|
import { Input } from '@/shared/shadcn-ui/input';
|
||||||
|
|
||||||
import { LoginFormData, loginFormSchema } from '../model/login-form.schema';
|
import {
|
||||||
import { useLanguage } from '@/shared/language';
|
LoginFormData,
|
||||||
|
loginFormSchema,
|
||||||
|
} from '../../../../entities/auth/model/validation/login-form.schema';
|
||||||
|
|
||||||
interface LoginFormProps {
|
interface LoginFormProps {
|
||||||
// onSubmit: (data: any) => Promise<void>;
|
type: 'bonus' | 'corporate';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LoginForm = ({}: LoginFormProps) => {
|
export const LoginForm = ({ type }: LoginFormProps) => {
|
||||||
const {t} = useLanguage()
|
const { t } = useTextController();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// const [login, results] = useLoginMutation();
|
const [login, { isLoading: isLoginLoading }] = useLazyLoginQuery();
|
||||||
|
|
||||||
const form = useForm<LoginFormData>({
|
const form = useForm<LoginFormData>({
|
||||||
resolver: zodResolver(loginFormSchema),
|
resolver: zodResolver(loginFormSchema),
|
||||||
@ -38,34 +43,26 @@ export const LoginForm = ({}: LoginFormProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async (data: LoginFormData) => {
|
const onSubmit = async (data: LoginFormData) => {
|
||||||
// const response = await login(data).unwrap();
|
await login({ ...data, type }).unwrap();
|
||||||
// const user = response.data;
|
|
||||||
// dispatch(
|
|
||||||
// setCredentials({
|
|
||||||
// user: {
|
|
||||||
// accessToken: user.accessToken,
|
|
||||||
// affiliateId: user.affiliateId,
|
|
||||||
// email: user.email,
|
|
||||||
// id: user.id,
|
|
||||||
// role: user.role,
|
|
||||||
// username: user.username,
|
|
||||||
// },
|
|
||||||
// }),
|
|
||||||
// );
|
|
||||||
toast.success('Logged in successfully!');
|
|
||||||
|
|
||||||
router.push('/customer-dashboard');
|
toast.success('Logged in successfully!');
|
||||||
|
router.push(
|
||||||
|
type === 'bonus' ? '/customer-dashboard' : '/corporate-dashboard',
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form} >
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4 mx-auto'>
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className='mx-auto space-y-4'
|
||||||
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='phoneNumber'
|
name='phoneNumber'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className='flex flex-col'>
|
||||||
<FormLabel>{t("auth.phoneNumber")}</FormLabel>
|
<FormLabel>{t('auth.phoneNumber')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type='tel'
|
type='tel'
|
||||||
@ -82,8 +79,8 @@ export const LoginForm = ({}: LoginFormProps) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name='cardNumber'
|
name='cardNumber'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className='flex flex-col'>
|
||||||
<FormLabel>{t("auth.cardNumber")}</FormLabel>
|
<FormLabel>{t('auth.cardNumber')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type='text'
|
type='text'
|
||||||
@ -95,16 +92,12 @@ export const LoginForm = ({}: LoginFormProps) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<SubmitButton
|
||||||
// isLoading={results.isLoading}
|
isLoading={isLoginLoading}
|
||||||
// title='Login'
|
|
||||||
type='submit'
|
type='submit'
|
||||||
className='w-full'
|
className='w-full'
|
||||||
// variant={'default'}
|
disabled={isLoginLoading}
|
||||||
// disabled={loading}
|
/>
|
||||||
>
|
|
||||||
Войти
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|||||||
4
src/features/map/model/index.ts
Normal file
4
src/features/map/model/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export type Point = {
|
||||||
|
id: number;
|
||||||
|
coordinates: [number, number];
|
||||||
|
};
|
||||||
@ -10,7 +10,9 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useLanguage } from '@/shared/language';
|
import { Stations } from '@/app/api-utlities/@types/main';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { Badge } from '@/shared/shadcn-ui/badge';
|
import { Badge } from '@/shared/shadcn-ui/badge';
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
import {
|
import {
|
||||||
@ -168,8 +170,14 @@ const allFilters = [
|
|||||||
// Extract unique cities from stations
|
// Extract unique cities from stations
|
||||||
const allCities = [...new Set(stations.map((station) => station.city))].sort();
|
const allCities = [...new Set(stations.map((station) => station.city))].sort();
|
||||||
|
|
||||||
export default function GasStationMap() {
|
interface GasStationMapProps {
|
||||||
const { t } = useLanguage();
|
stations: Stations;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GasStationMap({
|
||||||
|
stations: _stations,
|
||||||
|
}: GasStationMapProps) {
|
||||||
|
const { t } = useTextController();
|
||||||
const mapRef = useRef<HTMLDivElement>(null);
|
const mapRef = useRef<HTMLDivElement>(null);
|
||||||
const [activeFilters, setActiveFilters] = useState<string[]>([]);
|
const [activeFilters, setActiveFilters] = useState<string[]>([]);
|
||||||
const [activeCities, setActiveCities] = useState<string[]>([]);
|
const [activeCities, setActiveCities] = useState<string[]>([]);
|
||||||
|
|||||||
38
src/features/map/ui/yandex-map.tsx
Normal file
38
src/features/map/ui/yandex-map.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Map, Placemark, YMaps } from '@pbe/react-yandex-maps';
|
||||||
|
|
||||||
|
import { Point } from '../model';
|
||||||
|
|
||||||
|
type YandexMapProps = {
|
||||||
|
points: Point[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const YandexMap = ({ points }: YandexMapProps) => {
|
||||||
|
return (
|
||||||
|
<YMaps
|
||||||
|
query={{
|
||||||
|
apikey: process.env.NEXT_PUBLIC_YANDEX_MAP_API_KEY,
|
||||||
|
lang: 'ru_RU',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Map
|
||||||
|
defaultState={{
|
||||||
|
center: points[0].coordinates || [55.751574, 37.573856], // центр карты,
|
||||||
|
zoom: 11,
|
||||||
|
}}
|
||||||
|
className='rounded-md shadow-lg'
|
||||||
|
options={{
|
||||||
|
suppressMapOpenBlock: true,
|
||||||
|
suppressObsoleteBrowserNotifier: true,
|
||||||
|
}}
|
||||||
|
width={'100%'}
|
||||||
|
height={'500px'}
|
||||||
|
>
|
||||||
|
{points.map((point) => (
|
||||||
|
<Placemark key={point.id} geometry={point.coordinates} />
|
||||||
|
))}
|
||||||
|
</Map>
|
||||||
|
</YMaps>
|
||||||
|
);
|
||||||
|
};
|
||||||
201
src/pages-templates/(dashboard)/corporate-dashboard/index.tsx
Normal file
201
src/pages-templates/(dashboard)/corporate-dashboard/index.tsx
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { subMonths } from 'date-fns';
|
||||||
|
import { Building2, LogOut, Wallet } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/shared/shadcn-ui/card';
|
||||||
|
|
||||||
|
import { TransactionsTable } from '@/widgets/transactions-table';
|
||||||
|
|
||||||
|
// import { CardsList } from '@/widgets/cards-list';
|
||||||
|
|
||||||
|
// Sample company data
|
||||||
|
const companyData = {
|
||||||
|
companyName: 'ООО «ТаджикТранс»',
|
||||||
|
numberOfCards: 12,
|
||||||
|
fund: 25000,
|
||||||
|
overdraft: 5000,
|
||||||
|
totalFund: 30000,
|
||||||
|
registrationDate: '10.03.2019',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sample transaction data
|
||||||
|
const generateTransactions = () => {
|
||||||
|
const stations = [
|
||||||
|
'АЗС Душанбе-Центр',
|
||||||
|
'АЗС Душанбе-Запад',
|
||||||
|
'АЗС Душанбе-Восток',
|
||||||
|
'АЗС Худжанд',
|
||||||
|
'АЗС Куляб',
|
||||||
|
];
|
||||||
|
|
||||||
|
const products = [
|
||||||
|
{ name: 'ДТ', price: 8.5 },
|
||||||
|
{ name: 'АИ-92', price: 9.2 },
|
||||||
|
{ name: 'АИ-95', price: 10.5 },
|
||||||
|
{ name: 'Z-100 Power', price: 11.8 },
|
||||||
|
{ name: 'Пропан', price: 6.3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const transactions = [];
|
||||||
|
|
||||||
|
// Generate 50 random transactions over the last 6 months
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
const date = subMonths(new Date(), Math.random() * 6);
|
||||||
|
const station = stations[Math.floor(Math.random() * stations.length)];
|
||||||
|
const product = products[Math.floor(Math.random() * products.length)];
|
||||||
|
const quantity = Math.floor(Math.random() * 40) + 10; // 10-50 liters
|
||||||
|
const cost = product.price;
|
||||||
|
const total = quantity * cost;
|
||||||
|
|
||||||
|
transactions.push({
|
||||||
|
id: i + 1,
|
||||||
|
date,
|
||||||
|
station,
|
||||||
|
product: product.name,
|
||||||
|
quantity,
|
||||||
|
cost,
|
||||||
|
total,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by date (newest first)
|
||||||
|
return transactions.sort((a, b) => b.date.getTime() - a.date.getTime());
|
||||||
|
};
|
||||||
|
|
||||||
|
const transactions = generateTransactions();
|
||||||
|
|
||||||
|
export function CorporateDashboard() {
|
||||||
|
const [startDate, setStartDate] = useState<Date | undefined>(
|
||||||
|
subMonths(new Date(), 1),
|
||||||
|
);
|
||||||
|
const [endDate, setEndDate] = useState<Date | undefined>(new Date());
|
||||||
|
const [filteredTransactions, setFilteredTransactions] =
|
||||||
|
useState(transactions);
|
||||||
|
|
||||||
|
// Filter transactions by date range
|
||||||
|
const filterTransactions = () => {
|
||||||
|
if (!startDate || !endDate) return;
|
||||||
|
|
||||||
|
const filtered = transactions.filter((transaction) => {
|
||||||
|
const transactionDate = new Date(transaction.date);
|
||||||
|
return transactionDate >= startDate && transactionDate <= endDate;
|
||||||
|
});
|
||||||
|
|
||||||
|
setFilteredTransactions(filtered);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { t } = useTextController();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex min-h-screen flex-col px-2.5'>
|
||||||
|
<main className='flex-1 py-10'>
|
||||||
|
<div className='container mx-auto max-w-6xl'>
|
||||||
|
<div className='mb-8 flex items-center justify-between'>
|
||||||
|
<h1 className='text-3xl font-bold'>{t('corporate.pageTitle')}</h1>
|
||||||
|
<Button variant='outline' className='gap-2'>
|
||||||
|
<LogOut className='h-4 w-4' />
|
||||||
|
{t('corporate.logoutButton')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mb-10 grid gap-3 md:grid-cols-3 md:gap-6'>
|
||||||
|
{/* Company Card */}
|
||||||
|
<Card className='md:col-span-2'>
|
||||||
|
<CardHeader className='pb-2'>
|
||||||
|
<CardTitle className='flex items-center gap-2'>
|
||||||
|
<Building2 className='h-5 w-5 text-red-600' />
|
||||||
|
{t('corporate.companyCard.title')}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className='grid gap-6 md:grid-cols-2'>
|
||||||
|
<div>
|
||||||
|
<div className='mb-4 space-y-1'>
|
||||||
|
<p className='text-sm text-gray-500'>
|
||||||
|
{t('corporate.companyCard.companyNameLabel')}
|
||||||
|
</p>
|
||||||
|
<p className='font-medium'>{companyData.companyName}</p>
|
||||||
|
</div>
|
||||||
|
<div className='mb-4 space-y-1'>
|
||||||
|
<p className='text-sm text-gray-500'>
|
||||||
|
{t('corporate.companyCard.cardsCountLabel')}
|
||||||
|
</p>
|
||||||
|
<p className='font-medium'>{companyData.numberOfCards}</p>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-1'>
|
||||||
|
<p className='text-sm text-gray-500'>
|
||||||
|
{t('corporate.companyCard.registrationDateLabel')}
|
||||||
|
</p>
|
||||||
|
<p className='font-medium'>
|
||||||
|
{companyData.registrationDate}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className='mb-4 space-y-1'>
|
||||||
|
<p className='text-sm text-gray-500'>
|
||||||
|
{t('corporate.companyCard.fundLabel')}
|
||||||
|
</p>
|
||||||
|
<p className='font-medium'>
|
||||||
|
{companyData.fund.toLocaleString()}{' '}
|
||||||
|
{t('corporate.currency')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='mb-4 space-y-1'>
|
||||||
|
<p className='text-sm text-gray-500'>
|
||||||
|
{t('corporate.companyCard.overdraftLabel')}
|
||||||
|
</p>
|
||||||
|
<p className='font-medium'>
|
||||||
|
{companyData.overdraft.toLocaleString()}{' '}
|
||||||
|
{t('corporate.currency')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Fund Card */}
|
||||||
|
<Card className='bg-gradient-to-br from-red-600 to-red-800 text-white'>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className='flex items-center gap-2'>
|
||||||
|
<Wallet className='h-5 w-5' />
|
||||||
|
{t('corporate.fundCard.title')}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className='text-white/80'>
|
||||||
|
{t('corporate.fundCard.description')}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className='text-center'>
|
||||||
|
<p className='mb-2 text-4xl font-bold'>
|
||||||
|
{companyData.totalFund.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<p className='text-white/80'>
|
||||||
|
{t('corporate.fundCard.currency')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <CardsList totalCards={companyData.numberOfCards} /> */}
|
||||||
|
|
||||||
|
{/* Transactions */}
|
||||||
|
|
||||||
|
<TransactionsTable />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
124
src/pages-templates/(dashboard)/customer-dashboard/index.tsx
Normal file
124
src/pages-templates/(dashboard)/customer-dashboard/index.tsx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ArrowUpRight, Clock, CreditCard, LogOut, User } from 'lucide-react';
|
||||||
|
|
||||||
|
import { useFetchMyBonusInfoQuery } from '@/entities/bonus/api/bonus.api';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/shared/shadcn-ui/card';
|
||||||
|
|
||||||
|
import { TransactionsTable } from '@/widgets/transactions-table';
|
||||||
|
|
||||||
|
export function CustomerDashboard() {
|
||||||
|
const { t } = useTextController();
|
||||||
|
|
||||||
|
const { data, isLoading } = useFetchMyBonusInfoQuery({});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex min-h-screen flex-col px-2.5'>
|
||||||
|
<main className='flex-1 py-10'>
|
||||||
|
<div className='container mx-auto max-w-6xl'>
|
||||||
|
<div className='mb-8 flex items-center justify-between'>
|
||||||
|
<h1 className='text-3xl font-bold'>{t('customer.pageTitle')}</h1>
|
||||||
|
<Button variant='outline' className='gap-2'>
|
||||||
|
<LogOut className='h-4 w-4' />
|
||||||
|
{t('customer.logoutButton')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mb-10 grid gap-3 md:grid-cols-3 md:gap-6'>
|
||||||
|
<Card className='bg-gradient-to-br from-red-600 to-red-800 text-white'>
|
||||||
|
{!data || isLoading ? (
|
||||||
|
// TODO: Bunyod please add loader here
|
||||||
|
<>Loader here</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className='flex items-center gap-2'>
|
||||||
|
<CreditCard className='h-5 w-5' />
|
||||||
|
{t('customer.bonusCard.title')}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className='text-white/80'>
|
||||||
|
{t('customer.bonusCard.description')}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className='text-center'>
|
||||||
|
<p className='mb-2 text-4xl font-bold'>{data.bonuses}</p>
|
||||||
|
<p className='text-white/80'>
|
||||||
|
{t('customer.bonusCard.points')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='mt-6 flex items-center justify-between'>
|
||||||
|
<div className='flex items-center gap-1 text-sm text-white/80'>
|
||||||
|
<Clock className='h-4 w-4' />
|
||||||
|
<span>
|
||||||
|
{t('customer.bonusCard.validUntil')}{' '}
|
||||||
|
{new Date(data.end_date).toLocaleDateString('en-GB')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ArrowUpRight className='h-5 w-5 text-white/60' />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Bonus Card */}
|
||||||
|
{/* Customer Card */}
|
||||||
|
<Card className='md:col-span-2'>
|
||||||
|
<CardHeader className='pb-2'>
|
||||||
|
<CardTitle className='flex items-center gap-2'>
|
||||||
|
<User className='h-5 w-5 text-red-600' />
|
||||||
|
{t('customer.infoCard.title')}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{!data || isLoading ? (
|
||||||
|
// TODO: Bunyod please add loader here
|
||||||
|
<>Loader here</>
|
||||||
|
) : (
|
||||||
|
<div className='grid gap-6 md:grid-cols-2'>
|
||||||
|
<div>
|
||||||
|
<div className='mb-4 space-y-1'>
|
||||||
|
<p className='text-sm text-gray-500'>
|
||||||
|
{t('customer.infoCard.regDateLabel')}
|
||||||
|
</p>
|
||||||
|
<p className='font-medium'>{data.fullname}</p>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-1'>
|
||||||
|
<p className='text-sm text-gray-500'>
|
||||||
|
{t('customer.infoCard.regDateLabel')}
|
||||||
|
</p>
|
||||||
|
<p className='font-medium'>
|
||||||
|
{new Date(data.reg_date).toLocaleDateString('en-GB')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className='mb-4 space-y-1'>
|
||||||
|
<p className='text-sm text-gray-500'>
|
||||||
|
{t('customer.infoCard.cardNumberLabel')}
|
||||||
|
</p>
|
||||||
|
<p className='font-medium'>{data.cardno}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TransactionsTable />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,26 +1,19 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { Fuel, History, MapPin, Star, Target, Users } from 'lucide-react';
|
||||||
Award,
|
|
||||||
Fuel,
|
|
||||||
History,
|
|
||||||
MapPin,
|
|
||||||
Star,
|
|
||||||
Target,
|
|
||||||
Users,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
// import { useTranslation } from 'next-i18next';
|
// import { useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
import AnimatedCounter from '@/shared/components/animated-counter';
|
import AnimatedCounter from '@/shared/components/animated-counter';
|
||||||
import { useLanguage } from '@/shared/language';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
import { Card, CardContent } from '@/shared/shadcn-ui/card';
|
import { Card, CardContent } from '@/shared/shadcn-ui/card';
|
||||||
|
|
||||||
import { CompanyTimeline } from '@/widgets/about-page/company-timeline';
|
import { CompanyTimeline } from '@/widgets/about-page/company-timeline';
|
||||||
import { StationGallery } from '@/widgets/about-page/station-gallery';
|
import { StationGallery } from '@/widgets/about-page/station-gallery';
|
||||||
import { CtaSection } from '@/widgets/cta-section';
|
import { CtaSection } from '@/widgets/cta-section';
|
||||||
|
import Container from '@/shared/shadcn-ui/conteiner';
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'about.metadata.title',
|
title: 'about.metadata.title',
|
||||||
@ -28,7 +21,7 @@ export const metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
const { t } = useLanguage();
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-h-screen flex-col'>
|
<div className='flex min-h-screen flex-col'>
|
||||||
@ -44,7 +37,7 @@ export default function AboutPage() {
|
|||||||
className='object-cover'
|
className='object-cover'
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'>
|
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30 px-2'>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
<div className='max-w-2xl space-y-4 text-white'>
|
<div className='max-w-2xl space-y-4 text-white'>
|
||||||
<h1 className='text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl'>
|
<h1 className='text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl'>
|
||||||
@ -60,6 +53,7 @@ export default function AboutPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Company Overview */}
|
{/* Company Overview */}
|
||||||
|
<Container>
|
||||||
<section className='py-16'>
|
<section className='py-16'>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
<div className='grid items-center gap-12 md:grid-cols-2'>
|
<div className='grid items-center gap-12 md:grid-cols-2'>
|
||||||
@ -80,7 +74,7 @@ export default function AboutPage() {
|
|||||||
{t('about.overview.description3')}
|
{t('about.overview.description3')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className='mb-6 grid grid-cols-2 gap-4'>
|
<div className='mb-6 grid md:grid-cols-2 gap-4 grid-cols-1'>
|
||||||
{[0, 1, 2, 3].map((index) => (
|
{[0, 1, 2, 3].map((index) => (
|
||||||
<div key={index} className='flex items-start'>
|
<div key={index} className='flex items-start'>
|
||||||
<div className='mt-1 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-red-600'>
|
<div className='mt-1 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-red-600'>
|
||||||
@ -109,6 +103,7 @@ export default function AboutPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</Container>
|
||||||
|
|
||||||
{/* Stats Section */}
|
{/* Stats Section */}
|
||||||
<section className='bg-red-600 py-16 text-white'>
|
<section className='bg-red-600 py-16 text-white'>
|
||||||
@ -121,7 +116,7 @@ export default function AboutPage() {
|
|||||||
{t('about.stats.subtitle')}
|
{t('about.stats.subtitle')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='grid grid-cols-2 gap-8 text-center md:grid-cols-4'>
|
<div className='grid grid-cols-1 sm:grid-cols-2 gap-8 text-center md:grid-cols-4'>
|
||||||
{[0, 1, 2, 3].map((index) => (
|
{[0, 1, 2, 3].map((index) => (
|
||||||
<div key={index} className='space-y-2'>
|
<div key={index} className='space-y-2'>
|
||||||
<h3 className='text-4xl font-bold'>
|
<h3 className='text-4xl font-bold'>
|
||||||
@ -147,6 +142,7 @@ export default function AboutPage() {
|
|||||||
{/* Our History */}
|
{/* Our History */}
|
||||||
<section className='py-16'>
|
<section className='py-16'>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
|
<Container>
|
||||||
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
<History className='h-6 w-6 text-red-600' />
|
<History className='h-6 w-6 text-red-600' />
|
||||||
@ -158,12 +154,17 @@ export default function AboutPage() {
|
|||||||
{t('about.history.subtitle')}
|
{t('about.history.subtitle')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Container>
|
||||||
<CompanyTimeline />
|
<CompanyTimeline />
|
||||||
|
</Container>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Our Stations */}
|
{/* Our Stations */}
|
||||||
|
<Container>
|
||||||
<section className='bg-gray-50 py-16'>
|
<section className='bg-gray-50 py-16'>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
||||||
@ -191,8 +192,10 @@ export default function AboutPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</Container>
|
||||||
|
|
||||||
{/* Our Values */}
|
{/* Our Values */}
|
||||||
|
<Container>
|
||||||
<section className='py-16'>
|
<section className='py-16'>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
||||||
@ -229,8 +232,10 @@ export default function AboutPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</Container>
|
||||||
|
|
||||||
{/* Our Team */}
|
{/* Our Team */}
|
||||||
|
<Container>
|
||||||
<section className='bg-gray-50 py-16'>
|
<section className='bg-gray-50 py-16'>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
||||||
@ -272,8 +277,11 @@ export default function AboutPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</Container>
|
||||||
|
|
||||||
{/* Testimonials */}
|
{/* Testimonials */}
|
||||||
|
<Container>
|
||||||
|
|
||||||
<section className='py-16'>
|
<section className='py-16'>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
||||||
@ -317,6 +325,7 @@ export default function AboutPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</Container>
|
||||||
|
|
||||||
<CtaSection />
|
<CtaSection />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
288
src/pages-templates/charity/index.tsx
Normal file
288
src/pages-templates/charity/index.tsx
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Calendar,
|
||||||
|
CheckCircle,
|
||||||
|
Heart,
|
||||||
|
Landmark,
|
||||||
|
MapPin,
|
||||||
|
Users,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/shared/shadcn-ui/card';
|
||||||
|
|
||||||
|
import { CtaSection } from '@/widgets/cta-section';
|
||||||
|
import Container from '@/shared/shadcn-ui/conteiner';
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Благотворительность | GasNetwork - Сеть заправок в Таджикистане',
|
||||||
|
description:
|
||||||
|
'Благотворительные проекты и инициативы GasNetwork. Мы помогаем обществу и заботимся о будущем.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CharityPage() {
|
||||||
|
const { t } = useTextController();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex min-h-screen flex-col'>
|
||||||
|
<main className='flex-1'>
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className='relative'>
|
||||||
|
<div className='relative h-[400px] w-full overflow-hidden'>
|
||||||
|
<Image
|
||||||
|
src='/placeholder.svg?height=500&width=1920&text=Благотворительный+фонд+GasNetwork'
|
||||||
|
alt={t('charity.hero.imageAlt')}
|
||||||
|
width={1920}
|
||||||
|
height={500}
|
||||||
|
className='object-cover'
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'>
|
||||||
|
<Container>
|
||||||
|
<div className='container mx-auto'>
|
||||||
|
<div className='max-w-2xl space-y-6 text-white'>
|
||||||
|
<div className='inline-flex items-center justify-center rounded-full bg-red-600/20 p-2'>
|
||||||
|
<Heart className='size-6 text-red-500' />
|
||||||
|
</div>
|
||||||
|
<h1 className='text-3xl font-bold tracking-tight sm:text-5xl md:text-6xl'>
|
||||||
|
{t('charity.hero.title')}
|
||||||
|
</h1>
|
||||||
|
<p className='text-lg sm:text-xl text-gray-200'>
|
||||||
|
{t('charity.hero.subtitle')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Mission Section */}
|
||||||
|
<Container>
|
||||||
|
<section className='py-16'>
|
||||||
|
<div className='container mx-auto'>
|
||||||
|
<div className='grid items-center gap-12 md:grid-cols-2'>
|
||||||
|
<div>
|
||||||
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
|
<Heart className='h-6 w-6 text-red-600' />
|
||||||
|
</div>
|
||||||
|
<h2 className='mb-6 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
|
{t('charity.mission.title')}
|
||||||
|
</h2>
|
||||||
|
<p className='mb-6 text-gray-600'>
|
||||||
|
{t('charity.mission.description1')}
|
||||||
|
</p>
|
||||||
|
<p className='mb-6 text-gray-600'>
|
||||||
|
{t('charity.mission.description2')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className='space-y-4'>
|
||||||
|
{[0, 1, 2].map((index) => (
|
||||||
|
<div key={index} className='flex items-start'>
|
||||||
|
<CheckCircle className='mr-3 h-6 w-6 flex-shrink-0 text-red-600' />
|
||||||
|
<div>
|
||||||
|
<h3 className='text-lg font-medium'>
|
||||||
|
{t(`charity.mission.principles.${index}.title`)}
|
||||||
|
</h3>
|
||||||
|
<p className='text-gray-600'>
|
||||||
|
{t(`charity.mission.principles.${index}.description`)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='relative h-[500px] overflow-hidden rounded-xl shadow-xl'>
|
||||||
|
<Image
|
||||||
|
src='/placeholder.svg?height=500&width=600&text=Наша+миссия'
|
||||||
|
alt={t('charity.mission.imageAlt')}
|
||||||
|
fill
|
||||||
|
className='object-cover'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
{/* Key Figures */}
|
||||||
|
<section className='bg-red-600 py-16 text-white'>
|
||||||
|
<div className='container mx-auto'>
|
||||||
|
<div className='mb-12 text-center'>
|
||||||
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
|
{t('charity.stats.title')}
|
||||||
|
</h2>
|
||||||
|
<p className='mx-auto max-w-2xl text-white/80'>
|
||||||
|
{t('charity.stats.subtitle')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='grid grid-cols-1 gap-8 text-center md:grid-cols-3'>
|
||||||
|
{[0, 1, 2].map((index) => (
|
||||||
|
<div key={index} className='space-y-2'>
|
||||||
|
<h3 className='text-4xl font-bold'>
|
||||||
|
{t(`charity.stats.items.${index}.value`)}
|
||||||
|
</h3>
|
||||||
|
<p className='text-white/80'>
|
||||||
|
{t(`charity.stats.items.${index}.label`)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Upcoming Events */}
|
||||||
|
<Container>
|
||||||
|
<section className='bg-gray-50 py-16'>
|
||||||
|
<div className='container mx-auto'>
|
||||||
|
<div className='mb-12 text-center'>
|
||||||
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
|
<Calendar className='h-6 w-6 text-red-600' />
|
||||||
|
</div>
|
||||||
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
|
{t('charity.events.title')}
|
||||||
|
</h2>
|
||||||
|
<p className='mx-auto max-w-2xl text-gray-600'>
|
||||||
|
{t('charity.events.subtitle')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid gap-6 md:grid-cols-3'>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: 'Благотворительный марафон',
|
||||||
|
description:
|
||||||
|
'Ежегодный благотворительный марафон в поддержку детей с особыми потребностями.',
|
||||||
|
date: '15 июня 2023',
|
||||||
|
location: 'Парк Рудаки, Душанбе',
|
||||||
|
image: '/placeholder.svg?height=200&width=300&text=Марафон',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Экологическая акция',
|
||||||
|
description:
|
||||||
|
'Очистка берегов реки Варзоб от мусора и посадка деревьев.',
|
||||||
|
date: '22 июля 2023',
|
||||||
|
location: 'Река Варзоб, Душанбе',
|
||||||
|
image:
|
||||||
|
'/placeholder.svg?height=200&width=300&text=Экологическая+акция',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Сбор школьных принадлежностей',
|
||||||
|
description:
|
||||||
|
'Сбор школьных принадлежностей для детей из малообеспеченных семей к новому учебному году.',
|
||||||
|
date: '1-20 августа 2023',
|
||||||
|
location: 'Все заправки GasNetwork',
|
||||||
|
image:
|
||||||
|
'/placeholder.svg?height=200&width=300&text=Школьные+принадлежности',
|
||||||
|
},
|
||||||
|
].map((event, index) => (
|
||||||
|
<Card key={index} className='overflow-hidden flex flex-col justify-between'>
|
||||||
|
<div>
|
||||||
|
<div className='relative h-48 w-full'>
|
||||||
|
<Image
|
||||||
|
src={event.image || '/placeholder.svg'}
|
||||||
|
alt={event.title}
|
||||||
|
fill
|
||||||
|
className='object-cover'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className='text-xl lg:text-2xl'>{event.title}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='space-y-4'>
|
||||||
|
<p className='text-gray-600'>{event.description}</p>
|
||||||
|
<div className='flex items-center text-sm text-gray-500'>
|
||||||
|
<Calendar className='mr-2 h-4 w-4' />
|
||||||
|
{event.date}
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center text-sm text-gray-500'>
|
||||||
|
<MapPin className='mr-2 h-4 w-4' />
|
||||||
|
{event.location}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</div>
|
||||||
|
<CardFooter>
|
||||||
|
<Button className='w-full bg-red-600 hover:bg-red-700'>
|
||||||
|
{t(`charity.events.button`)}
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
{/* How to Help */}
|
||||||
|
<Container>
|
||||||
|
<section className='py-16'>
|
||||||
|
<div className='container mx-auto'>
|
||||||
|
<div className='mb-12 text-center'>
|
||||||
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
|
<Users className='h-6 w-6 text-red-600' />
|
||||||
|
</div>
|
||||||
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
|
{t('charity.help.title')}
|
||||||
|
</h2>
|
||||||
|
<p className='mx-auto max-w-2xl text-gray-600'>
|
||||||
|
{t('charity.help.subtitle')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid gap-3 md:grid-cols-2 md:gap-6 lg:grid-cols-4'>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: 'Сделать пожертвование',
|
||||||
|
description:
|
||||||
|
'Ваше пожертвование поможет нам реализовать больше проектов и помочь большему количеству людей.',
|
||||||
|
icon: <Landmark className='h-10 w-10 text-red-600' />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Стать волонтером',
|
||||||
|
description:
|
||||||
|
'Присоединяйтесь к нашей команде волонтеров и помогайте нам в реализации благотворительных проектов.',
|
||||||
|
icon: <Users className='h-10 w-10 text-red-600' />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Участвовать в мероприятиях',
|
||||||
|
description:
|
||||||
|
'Принимайте участие в наших благотворительных мероприятиях и акциях.',
|
||||||
|
icon: <Calendar className='h-10 w-10 text-red-600' />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Распространять информацию',
|
||||||
|
description:
|
||||||
|
'Расскажите о нашем фонде и его деятельности своим друзьям и знакомым.',
|
||||||
|
icon: <Heart className='h-10 w-10 text-red-600' />,
|
||||||
|
},
|
||||||
|
].map((item, index) => (
|
||||||
|
<Card key={index} className='text-center'>
|
||||||
|
<CardHeader>
|
||||||
|
<div className='mb-4 flex justify-center'>{item.icon}</div>
|
||||||
|
<CardTitle className='break-words hyphens-auto'>
|
||||||
|
{item.title}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className='text-gray-600'>{item.description}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Container>
|
||||||
|
<CtaSection />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,12 +3,13 @@
|
|||||||
import { Download, Eye } from 'lucide-react';
|
import { Download, Eye } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
import { useLanguage } from '@/shared/language';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
import { Card, CardContent } from '@/shared/shadcn-ui/card';
|
import { Card, CardContent } from '@/shared/shadcn-ui/card';
|
||||||
|
import Container from '@/shared/shadcn-ui/conteiner';
|
||||||
|
|
||||||
export function CertificatesPage() {
|
export function CertificatesPage() {
|
||||||
const { t } = useLanguage();
|
const { t } = useTextController();
|
||||||
|
|
||||||
// This data would typically come from an API or CMS
|
// This data would typically come from an API or CMS
|
||||||
// We're keeping it as-is since it's dynamic content
|
// We're keeping it as-is since it's dynamic content
|
||||||
@ -64,7 +65,7 @@ export function CertificatesPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Container>
|
||||||
<main className='container mx-auto py-10'>
|
<main className='container mx-auto py-10'>
|
||||||
<div className='mb-10 text-center'>
|
<div className='mb-10 text-center'>
|
||||||
<h1 className='mb-4 text-4xl font-bold'>{t('certificates.title')}</h1>
|
<h1 className='mb-4 text-4xl font-bold'>{t('certificates.title')}</h1>
|
||||||
@ -122,6 +123,6 @@ export function CertificatesPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
|
||||||
import { BenefitsSection } from '@/widgets/clients/ui/benefits-section';
|
import { BenefitsSection } from '@/widgets/clients/ui/benefits-section';
|
||||||
import { ServicesOverviewSection } from '@/widgets/clients/ui/services-overview-section';
|
import { ServicesOverviewSection } from '@/widgets/clients/ui/services-overview-section';
|
||||||
import { CtaSection } from '@/widgets/cta-section';
|
import { CtaSection } from '@/widgets/cta-section';
|
||||||
import { useLanguage } from '@/shared/language';
|
import Container from '@/shared/shadcn-ui/conteiner';
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Клиентам | GasNetwork - Сеть заправок в Таджикистане',
|
title: 'Клиентам | GasNetwork - Сеть заправок в Таджикистане',
|
||||||
@ -14,8 +16,7 @@ export const metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ClientsPage() {
|
export function ClientsPage() {
|
||||||
|
const { t } = useTextController();
|
||||||
const { t } = useLanguage()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-h-screen flex-col'>
|
<div className='flex min-h-screen flex-col'>
|
||||||
@ -32,6 +33,7 @@ export function ClientsPage() {
|
|||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'>
|
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'>
|
||||||
|
<Container>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
<div className='max-w-2xl space-y-4 text-white'>
|
<div className='max-w-2xl space-y-4 text-white'>
|
||||||
<h1 className='text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl'>
|
<h1 className='text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl'>
|
||||||
@ -42,6 +44,7 @@ export function ClientsPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -3,10 +3,11 @@
|
|||||||
import { Check, Percent } from 'lucide-react';
|
import { Check, Percent } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
import { useLanguage } from '@/shared/language';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { Card, CardContent } from '@/shared/shadcn-ui/card';
|
import { Card, CardContent } from '@/shared/shadcn-ui/card';
|
||||||
|
|
||||||
import { CtaSection } from '@/widgets/cta-section';
|
import { CtaSection } from '@/widgets/cta-section';
|
||||||
|
import Container from '@/shared/shadcn-ui/conteiner';
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Программа лояльности | GasNetwork - Сеть заправок в Таджикистане',
|
title: 'Программа лояльности | GasNetwork - Сеть заправок в Таджикистане',
|
||||||
@ -15,7 +16,7 @@ export const metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function LoyaltyPage() {
|
export function LoyaltyPage() {
|
||||||
const { t } = useLanguage();
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-h-screen flex-col'>
|
<div className='flex min-h-screen flex-col'>
|
||||||
@ -32,6 +33,7 @@ export function LoyaltyPage() {
|
|||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'>
|
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'>
|
||||||
|
<Container>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
<div className='max-w-2xl space-y-4 text-white'>
|
<div className='max-w-2xl space-y-4 text-white'>
|
||||||
<h1 className='text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl'>
|
<h1 className='text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl'>
|
||||||
@ -42,10 +44,12 @@ export function LoyaltyPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<Container>
|
||||||
{/* Program Overview */}
|
{/* Program Overview */}
|
||||||
<section className='py-16'>
|
<section className='py-16'>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
@ -320,6 +324,8 @@ export function LoyaltyPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
|
||||||
<CtaSection />
|
<CtaSection />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
|
import { deleteCookie, getCookie } from 'cookies-next';
|
||||||
import { Building2, Fuel, User } from 'lucide-react';
|
import { Building2, Fuel, User } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import { LoginForm } from '@/features/auth/login-form';
|
import { LoginForm } from '@/features/auth/login-form';
|
||||||
import { useLanguage } from '@/shared/language';
|
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@ -19,11 +22,37 @@ import {
|
|||||||
TabsList,
|
TabsList,
|
||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from '@/shared/shadcn-ui/tabs';
|
} from '@/shared/shadcn-ui/tabs';
|
||||||
|
import Container from '@/shared/shadcn-ui/conteiner';
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
label: 'auth.bonusClient',
|
||||||
|
type: 'bonus' as const,
|
||||||
|
title: 'auth.bonusLogin.title',
|
||||||
|
description: 'auth.bonusLogin.description',
|
||||||
|
Icon: User,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'auth.corporateClient',
|
||||||
|
type: 'corporate' as const,
|
||||||
|
title: 'auth.corporateLogin.title',
|
||||||
|
description: 'auth.corporateLogin.description',
|
||||||
|
Icon: Building2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const { t } = useLanguage();
|
const { t } = useTextController();
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const defaultTab = searchParams.get('tab') || 'bonus';
|
||||||
|
|
||||||
|
const handleTabChange = (tabType: string) => {
|
||||||
|
router.push(`?tab=${tabType}`, undefined, { shallow: true });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<div className='flex min-h-screen flex-col items-center justify-center'>
|
<div className='flex min-h-screen flex-col items-center justify-center'>
|
||||||
<main className='flex-1'>
|
<main className='flex-1'>
|
||||||
<div className='container max-w-6xl py-16'>
|
<div className='container max-w-6xl py-16'>
|
||||||
@ -34,49 +63,84 @@ export default function LoginPage() {
|
|||||||
<h1 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
<h1 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
{t('auth.title')}
|
{t('auth.title')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className='max-w-2xl text-gray-600'>
|
<p className='max-w-2xl text-gray-600'>{t('auth.description')}</p>
|
||||||
{t('auth.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mx-auto max-w-md'>
|
<div className='mx-auto max-w-lg'>
|
||||||
<Tabs defaultValue='bonus' className='w-full'>
|
<Tabs
|
||||||
<TabsList className='mb-8 grid w-full grid-cols-2'>
|
defaultValue={defaultTab}
|
||||||
<TabsTrigger value='bonus' className='text-base'>
|
value={defaultTab}
|
||||||
<User className='mr-2 h-4 w-4' /> {t('auth.bonusClient')}
|
onValueChange={handleTabChange}
|
||||||
</TabsTrigger>
|
className='w-full'
|
||||||
<TabsTrigger value='corporate' className='text-base'>
|
>
|
||||||
<Building2 className='mr-2 h-4 w-4' /> {t('auth.corporateClient')}
|
<TabsList className='mb-8 flex h-fit w-full flex-col sm:flex-row'>
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
return (
|
||||||
|
<TabsTrigger
|
||||||
|
key={tab.label}
|
||||||
|
value={tab.type}
|
||||||
|
className='w-full text-base'
|
||||||
|
>
|
||||||
|
<tab.Icon className='mr-2 h-4 w-4' /> {t(tab.label)}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value='bonus'>
|
{tabs.map((tab) => {
|
||||||
<Card>
|
const tabCookieName = `${tab.type}__token`;
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{t('auth.bonusLogin.title')}</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
{t('auth.bonusLogin.description')}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className='space-y-4'>
|
|
||||||
<LoginForm />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value='corporate'>
|
const authenticationCookie = getCookie(tabCookieName);
|
||||||
|
|
||||||
|
if (authenticationCookie) {
|
||||||
|
return (
|
||||||
|
<TabsContent key={tab.label} value={tab.type}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t('auth.corporateLogin.title')}</CardTitle>
|
<CardTitle>{t(tab.title)}</CardTitle>
|
||||||
<CardDescription>
|
|
||||||
{t('auth.corporateLogin.description')}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-4'>
|
<CardContent className='flex justify-center gap-2 space-y-4'>
|
||||||
<LoginForm/>
|
<Link
|
||||||
|
href={
|
||||||
|
tab.type === 'bonus'
|
||||||
|
? '/customer-dashboard'
|
||||||
|
: '/corporate-dashboard'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button className='flex items-center'>
|
||||||
|
Открыть
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
className='flex items-center gap-2'
|
||||||
|
onClick={() => {
|
||||||
|
deleteCookie(tabCookieName);
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Выйти
|
||||||
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabsContent key={tab.label} value={tab.type}>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t(tab.title)}</CardTitle>
|
||||||
|
<CardDescription>{t(tab.description)}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='space-y-4'>
|
||||||
|
<LoginForm type={tab.type} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<div className='mt-8 text-center text-sm text-gray-500'>
|
<div className='mt-8 text-center text-sm text-gray-500'>
|
||||||
@ -91,5 +155,6 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
const baseQuery = fetchBaseQuery({
|
const baseQuery = fetchBaseQuery({
|
||||||
baseUrl: process.env.NEXT_PUBLIC_API_URL,
|
baseUrl: process.env.NEXT_PUBLIC_API_URL,
|
||||||
@ -15,7 +16,25 @@ export const TAGS = {
|
|||||||
|
|
||||||
export const baseAPI = createApi({
|
export const baseAPI = createApi({
|
||||||
reducerPath: 'baseAPI',
|
reducerPath: 'baseAPI',
|
||||||
baseQuery,
|
baseQuery: async (args, api, extraOptions) => {
|
||||||
|
const result = await baseQuery(args, api, extraOptions);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
switch (result.error.status) {
|
||||||
|
case 401:
|
||||||
|
toast.error('Login credentials error');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 500:
|
||||||
|
toast.error('Server error, please try later');
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
tagTypes: Object.values(TAGS),
|
tagTypes: Object.values(TAGS),
|
||||||
endpoints: () => ({}),
|
endpoints: () => ({}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,14 +3,15 @@
|
|||||||
import { Users } from 'lucide-react';
|
import { Users } from 'lucide-react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
|
||||||
import AnimatedCounter from './animated-counter';
|
import AnimatedCounter from './animated-counter';
|
||||||
import { useLanguage } from '../language';
|
|
||||||
|
|
||||||
export default function AboutCounter() {
|
export default function AboutCounter() {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const sectionRef = useRef<HTMLDivElement>(null);
|
const sectionRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const {t} = useLanguage()
|
const { t } = useTextController();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
@ -44,7 +45,7 @@ export default function AboutCounter() {
|
|||||||
<h3 className='text-2xl font-bold text-gray-900'>
|
<h3 className='text-2xl font-bold text-gray-900'>
|
||||||
{isVisible ? <AnimatedCounter end={150} suffix='+' /> : '0+'}
|
{isVisible ? <AnimatedCounter end={150} suffix='+' /> : '0+'}
|
||||||
</h3>
|
</h3>
|
||||||
<p className='text-gray-600'>{t("about.stats.items.2.label")}</p>
|
<p className='text-gray-600'>{t('about.stats.items.2.label')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='transform rounded-lg bg-white p-3 shadow-md transition-transform hover:scale-105 sm:p-6'>
|
<div className='transform rounded-lg bg-white p-3 shadow-md transition-transform hover:scale-105 sm:p-6'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
@ -53,7 +54,7 @@ export default function AboutCounter() {
|
|||||||
<h3 className='text-2xl font-bold text-gray-900'>
|
<h3 className='text-2xl font-bold text-gray-900'>
|
||||||
{isVisible ? <AnimatedCounter end={5} suffix='M+' /> : '0M+'}
|
{isVisible ? <AnimatedCounter end={5} suffix='M+' /> : '0M+'}
|
||||||
</h3>
|
</h3>
|
||||||
<p className='text-gray-600'>{t("about.stats.items.4.label")}</p>
|
<p className='text-gray-600'>{t('about.stats.items.4.label')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='transform rounded-lg bg-white p-3 shadow-md transition-transform hover:scale-105 sm:p-6'>
|
<div className='transform rounded-lg bg-white p-3 shadow-md transition-transform hover:scale-105 sm:p-6'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
@ -66,7 +67,7 @@ export default function AboutCounter() {
|
|||||||
'0%'
|
'0%'
|
||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
<p className='text-gray-600'>{t("about.stats.items.5.label")}</p>
|
<p className='text-gray-600'>{t('about.stats.items.5.label')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChevronLeft, ChevronRight} from 'lucide-react';
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { Discounts } from '@/app/api-utlities/@types/main';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
import { Card, CardContent } from '@/shared/shadcn-ui/card';
|
import { Card, CardContent } from '@/shared/shadcn-ui/card';
|
||||||
import Link from 'next/link';
|
|
||||||
import { useLanguage } from '../language';
|
|
||||||
|
|
||||||
const promotions = [
|
const promotions = [
|
||||||
{
|
{
|
||||||
@ -41,11 +43,15 @@ const promotions = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function PromotionSlider() {
|
interface PromotionSliderProps {
|
||||||
|
discounts: Discounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PromotionSlider({ discounts }: PromotionSliderProps) {
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const [visibleItems, setVisibleItems] = useState(3);
|
const [visibleItems, setVisibleItems] = useState(3);
|
||||||
|
|
||||||
const { t } = useLanguage()
|
const { t } = useTextController();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
@ -85,7 +91,7 @@ export default function PromotionSlider() {
|
|||||||
transform: `translateX(-${currentIndex * (100 / visibleItems)}%)`,
|
transform: `translateX(-${currentIndex * (100 / visibleItems)}%)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{promotions.map((promo) => (
|
{discounts.map((promo) => (
|
||||||
<div
|
<div
|
||||||
key={promo.id}
|
key={promo.id}
|
||||||
className='w-full flex-none p-2 sm:w-1/2 lg:w-1/3'
|
className='w-full flex-none p-2 sm:w-1/2 lg:w-1/3'
|
||||||
@ -96,19 +102,21 @@ export default function PromotionSlider() {
|
|||||||
<div className='relative h-48'>
|
<div className='relative h-48'>
|
||||||
<Image
|
<Image
|
||||||
src={promo.image || '/placeholder.svg'}
|
src={promo.image || '/placeholder.svg'}
|
||||||
alt={promo.title}
|
alt={promo.name}
|
||||||
fill
|
fill
|
||||||
className='object-cover'
|
className='object-cover'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CardContent className='p-4'>
|
<CardContent className='p-4'>
|
||||||
<h3 className='mb-2 text-lg font-bold'>{promo.title}</h3>
|
<h3 className='mb-2 text-lg font-bold'>{promo.name}</h3>
|
||||||
<p className='mb-3 text-sm text-gray-600'>
|
<p className='mb-3 text-sm text-gray-600'>
|
||||||
{promo.description}
|
{promo.description}
|
||||||
</p>
|
</p>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
<span className='text-xs text-gray-500'>
|
<span className='text-xs text-gray-500'>
|
||||||
Действует до: {promo.validUntil}
|
{promo.expiresAt
|
||||||
|
? `Действует до: ${promo.expiresAt}`
|
||||||
|
: null}
|
||||||
</span>
|
</span>
|
||||||
<Link href='#'>
|
<Link href='#'>
|
||||||
<Button
|
<Button
|
||||||
@ -125,6 +133,8 @@ export default function PromotionSlider() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{discounts.length > 3 && (
|
||||||
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
size='icon'
|
size='icon'
|
||||||
@ -143,6 +153,8 @@ export default function PromotionSlider() {
|
|||||||
<ChevronRight className='h-4 w-4' />
|
<ChevronRight className='h-4 w-4' />
|
||||||
<span className='sr-only'>Следующий</span>
|
<span className='sr-only'>Следующий</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
41
src/shared/components/submit-button.tsx
Normal file
41
src/shared/components/submit-button.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Loader2Icon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
import { Button, type ButtonProps } from '@/shared/shadcn-ui/button';
|
||||||
|
|
||||||
|
interface SubmitButtonProps extends ButtonProps {
|
||||||
|
title?: string;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SubmitButton = ({
|
||||||
|
title = 'common.buttons.login',
|
||||||
|
size = 'default',
|
||||||
|
type = 'submit',
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
isLoading,
|
||||||
|
onClick,
|
||||||
|
...props
|
||||||
|
}: SubmitButtonProps) => {
|
||||||
|
const { t } = useTextController();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={onClick}
|
||||||
|
type={type}
|
||||||
|
size={size}
|
||||||
|
className={className}
|
||||||
|
disabled={isLoading || disabled}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2Icon className='animate-spin' />
|
||||||
|
) : (
|
||||||
|
(props.children ?? t(title))
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
12
src/shared/language/api/text-control.api.ts
Normal file
12
src/shared/language/api/text-control.api.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { baseAPI } from '@/shared/api/base-api';
|
||||||
|
import { TextItem } from '@/shared/types/text.types';
|
||||||
|
|
||||||
|
export const textControlApi = baseAPI.injectEndpoints({
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
fetchText: builder.query<TextItem[], void>({
|
||||||
|
query: () => '/text',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { useFetchTextQuery } = textControlApi;
|
||||||
@ -1,73 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { createContext, type ReactNode, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import enTranslations from '../locales/en.json';
|
|
||||||
import ruTranslations from '../locales/ru.json';
|
|
||||||
|
|
||||||
// Define available languages
|
|
||||||
export const languages = {
|
|
||||||
en: { name: 'English', flag: '🇬🇧' },
|
|
||||||
ru: { name: 'Русский', flag: '🇷🇺' },
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Language = keyof typeof languages;
|
|
||||||
|
|
||||||
// Define translations type with flat structure
|
|
||||||
export type Translations = Record<string, string>;
|
|
||||||
|
|
||||||
// Load translations
|
|
||||||
const translations: Record<Language, Translations> = {
|
|
||||||
en: enTranslations,
|
|
||||||
ru: ruTranslations,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create context
|
|
||||||
type LanguageContextType = {
|
|
||||||
language: Language;
|
|
||||||
setLanguage: (lang: Language) => void;
|
|
||||||
t: (key: string) => string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LanguageContext = createContext<LanguageContextType | undefined>(
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create provider
|
|
||||||
export function LanguageProvider({ children }: { children: ReactNode }) {
|
|
||||||
// Default to Russian, but check localStorage on client
|
|
||||||
const [language, setLanguageState] = useState<Language>('ru');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Check if we're in the browser
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
const savedLanguage = localStorage.getItem('language') as Language;
|
|
||||||
if (savedLanguage && languages[savedLanguage]) {
|
|
||||||
setLanguageState(savedLanguage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setLanguage = (lang: Language) => {
|
|
||||||
setLanguageState(lang);
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
localStorage.setItem('language', lang);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Translation function for flat structure
|
|
||||||
const t = (key: string): string => {
|
|
||||||
if (translations[language][key]) {
|
|
||||||
return translations[language][key];
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn(`Translation key not found: ${key}`);
|
|
||||||
return key;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LanguageContext.Provider value={{ language, setLanguage, t }}>
|
|
||||||
{children}
|
|
||||||
</LanguageContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
49
src/shared/language/context/text-control-provider.tsx
Normal file
49
src/shared/language/context/text-control-provider.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { createContext, type ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { TextItem } from '@/shared/types/text.types';
|
||||||
|
|
||||||
|
export type Translations = Record<string, string>;
|
||||||
|
|
||||||
|
// Create context
|
||||||
|
type TextControlContextType = {
|
||||||
|
t: (key: string) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TextControlContext = createContext<
|
||||||
|
TextControlContextType | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
// Create provider
|
||||||
|
|
||||||
|
export function TextControlProvider({
|
||||||
|
children,
|
||||||
|
textItems,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
textItems?: TextItem[];
|
||||||
|
}) {
|
||||||
|
const textMap = textItems?.reduce(
|
||||||
|
(pr, cr) => {
|
||||||
|
pr[cr.key] = cr.value;
|
||||||
|
|
||||||
|
return pr;
|
||||||
|
},
|
||||||
|
{} as Record<string, string | null>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Translation function for flat structure
|
||||||
|
const t = (key: string): string => {
|
||||||
|
if (textMap?.[key]) {
|
||||||
|
return textMap[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(`Translation key not found: ${key}`);
|
||||||
|
return key;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextControlContext.Provider value={{ t }}>
|
||||||
|
{children}
|
||||||
|
</TextControlContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
import { LanguageContext } from '../context/language-provider';
|
import { TextControlContext } from '../context/text-control-provider';
|
||||||
|
|
||||||
export function useLanguage() {
|
export function useTextController() {
|
||||||
const context = useContext(LanguageContext);
|
const context = useContext(TextControlContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error('useLanguage must be used within a LanguageProvider');
|
throw new Error('useLanguage must be used within a LanguageProvider');
|
||||||
}
|
}
|
||||||
@ -1,6 +1,2 @@
|
|||||||
export {
|
export { TextControlProvider } from './context/text-control-provider';
|
||||||
LanguageProvider,
|
export { useTextController } from './hooks/use-text-controller';
|
||||||
languages,
|
|
||||||
type Language,
|
|
||||||
} from './context/language-provider';
|
|
||||||
export { useLanguage } from './hooks/use-language';
|
|
||||||
|
|||||||
@ -1,108 +0,0 @@
|
|||||||
{
|
|
||||||
"common.buttons.readMore": "Read More",
|
|
||||||
"common.buttons.findStation": "Find Station",
|
|
||||||
"common.buttons.learnMore": "Learn More",
|
|
||||||
"common.buttons.download": "Download",
|
|
||||||
"common.buttons.view": "View",
|
|
||||||
"common.buttons.contactUs": "Contact Us",
|
|
||||||
"common.buttons.apply": "Apply",
|
|
||||||
"common.buttons.login": "Login",
|
|
||||||
"common.buttons.logout": "Logout",
|
|
||||||
"common.buttons.submit": "Submit",
|
|
||||||
"common.buttons.filter": "Filter",
|
|
||||||
"common.buttons.resetFilters": "Reset Filters",
|
|
||||||
"common.buttons.downloadApp": "Download App",
|
|
||||||
"common.buttons.purchaseCardAtGasStations": "Purchase a card at gas stations",
|
|
||||||
"common.buttons.sendResume": "Send Resume",
|
|
||||||
"common.buttons.showAllStations": "Show All Stations",
|
|
||||||
"common.buttons.allPromotions": "All Promotions",
|
|
||||||
|
|
||||||
"common.navigation.home": "Home",
|
|
||||||
"common.navigation.about": "About Us",
|
|
||||||
"common.navigation.clients": "For Clients",
|
|
||||||
"common.navigation.stations": "Our Stations",
|
|
||||||
"common.navigation.vacancies": "Vacancies",
|
|
||||||
"common.navigation.promotions": "Promotions",
|
|
||||||
"common.navigation.charity": "Charity",
|
|
||||||
"common.navigation.certificates": "Certificates",
|
|
||||||
"common.navigation.contacts": "Contacts",
|
|
||||||
|
|
||||||
"common.footer.contacts": "Contacts",
|
|
||||||
"common.footer.navigation": "Navigation",
|
|
||||||
"common.footer.yourEmail": "Your email",
|
|
||||||
"common.footer.rights": "All rights reserved.",
|
|
||||||
|
|
||||||
"home.hero.title": "Modern Gas Station Network in Tajikistan",
|
|
||||||
"home.hero.description": "Quality fuel, convenient locations, and excellent service for our customers",
|
|
||||||
|
|
||||||
"home.about.title": "About Our Company",
|
|
||||||
"home.about.description1": "Our gas station network is one of the leading in Tajikistan. We provide high-quality fuel and a high level of service for our customers for more than 15 years.",
|
|
||||||
"home.about.description2": "We are constantly developing, opening new stations and improving service at existing ones. Our goal is to make refueling as convenient and fast as possible for every customer.",
|
|
||||||
"home.about.features.quality.title": "Quality Fuel",
|
|
||||||
"home.about.features.quality.description": "We guarantee the high quality of our fuel",
|
|
||||||
"home.about.features.equipment.title": "Modern Equipment",
|
|
||||||
"home.about.features.equipment.description": "All our stations are equipped with modern equipment",
|
|
||||||
"home.about.features.staff.title": "Professional Staff",
|
|
||||||
"home.about.features.staff.description": "Our employees are professionals in their field",
|
|
||||||
|
|
||||||
"home.stations.title": "Our Stations",
|
|
||||||
"home.stations.description": "Find the nearest station in our network. We are located in convenient places throughout Tajikistan.",
|
|
||||||
|
|
||||||
"home.promotions.title": "Current Promotions",
|
|
||||||
"home.promotions.description": "Special offers and promotions for our customers. Refuel profitably!",
|
|
||||||
|
|
||||||
"home.vacancies.title": "Vacancies",
|
|
||||||
"home.vacancies.description": "Join our team of professionals. We offer stable work and opportunities for growth.",
|
|
||||||
"home.vacancies.all": "All vacancies",
|
|
||||||
"home.vacancies.office": "Office",
|
|
||||||
"home.vacancies.stations": "Stations",
|
|
||||||
"home.vacancies.fullTime": "Full time",
|
|
||||||
"home.vacancies.experience": "Experience from 1 year",
|
|
||||||
"home.vacancies.shiftWork": "Shift work",
|
|
||||||
"home.vacancies.training": "Training",
|
|
||||||
|
|
||||||
"home.partners.title": "Our Partners",
|
|
||||||
"home.partners.description": "We cooperate with leading companies to provide the best services to our customers.",
|
|
||||||
"home.partners.becomePartner": "Become Our Partner",
|
|
||||||
"home.partners.becomePartnerText": "We are open to cooperation and new partnerships. Contact us to discuss opportunities.",
|
|
||||||
|
|
||||||
"home.charity.title": "Charity Foundation",
|
|
||||||
"home.charity.description": "Our charity foundation was created to support socially significant projects in Tajikistan. We strive to contribute to the development of society and help those in need.",
|
|
||||||
"home.charity.directions": "Main directions of our foundation's activities:",
|
|
||||||
"home.charity.education": "Support for educational programs",
|
|
||||||
"home.charity.children": "Help for children from low-income families",
|
|
||||||
"home.charity.ecology": "Environmental initiatives",
|
|
||||||
"home.charity.sports": "Support for sports events",
|
|
||||||
"home.charity.learnMore": "Learn More About the Foundation",
|
|
||||||
|
|
||||||
"home.cta.title": "Join Us",
|
|
||||||
"home.cta.description": "Become part of our network. Get special offers, bonuses, and discounts.",
|
|
||||||
|
|
||||||
"certificates.title": "Our Certificates",
|
|
||||||
"certificates.description": "GasNetwork adheres to high standards of quality and safety. Our certificates confirm the compliance of our products and services with international and national standards.",
|
|
||||||
|
|
||||||
"certificates.issueDate": "Issue Date",
|
|
||||||
"certificates.expiryDate": "Valid Until",
|
|
||||||
"certificates.faq": "Frequently Asked Questions",
|
|
||||||
|
|
||||||
"auth.title": "Login to your account",
|
|
||||||
"auth.description": "Log in to your personal account to access information about your bonuses, transaction history and other features.",
|
|
||||||
"auth.bonusClient": "Bonus Client",
|
|
||||||
"auth.corporateClient": "Corporate Client",
|
|
||||||
"auth.bonusLogin.title": "Login for bonus clients",
|
|
||||||
"auth.bonusLogin.description": "Enter your phone number and bonus card number to log in to your personal account.",
|
|
||||||
"auth.corporateLogin.title": "Login for corporate clients",
|
|
||||||
"auth.corporateLogin.description": "Enter your phone number and corporate card number to log in to your personal account.",
|
|
||||||
"auth.phoneNumber": "Phone number",
|
|
||||||
"auth.cardNumber": "Card number",
|
|
||||||
"auth.loginIssues": "Having trouble logging in?",
|
|
||||||
|
|
||||||
"map.filters": "Filters",
|
|
||||||
"map.stationsList": "Stations List",
|
|
||||||
"map.noStations": "No stations matching the selected filters",
|
|
||||||
"map.ourStations": "Our Stations",
|
|
||||||
"map.totalStations": "Total stations",
|
|
||||||
"map.services": "Services",
|
|
||||||
"map.cities": "Cities",
|
|
||||||
"map.allCities": "All Cities"
|
|
||||||
}
|
|
||||||
@ -1,247 +0,0 @@
|
|||||||
{
|
|
||||||
"common.contacts.address": "ул. Рудаки 137, Душанбе, Таджикистан",
|
|
||||||
"common.contacts.tel": "+992 (37) 223-45-67",
|
|
||||||
"common.contacts.email": "info@gasnetwork.tj",
|
|
||||||
|
|
||||||
"common.buttons.readMore": "Подробнее",
|
|
||||||
"common.buttons.findStation": "Найти заправку",
|
|
||||||
"common.buttons.learnMore": "Узнать больше",
|
|
||||||
"common.buttons.download": "Скачать",
|
|
||||||
"common.buttons.view": "Просмотр",
|
|
||||||
"common.buttons.contactUs": "Связаться с нами",
|
|
||||||
"common.buttons.apply": "Подать заявку",
|
|
||||||
"common.buttons.login": "Вход",
|
|
||||||
"common.buttons.logout": "Выйти",
|
|
||||||
"common.buttons.submit": "Отправить",
|
|
||||||
"common.buttons.filter": "Фильтры",
|
|
||||||
"common.buttons.resetFilters": "Сбросить фильтры",
|
|
||||||
"common.buttons.downloadApp": "Скачать приложение",
|
|
||||||
"common.buttons.purchaseCardAtGasStations": "Приобретайте карту в сети АЗС",
|
|
||||||
"common.buttons.sendResume": "Отправить резюме",
|
|
||||||
"common.buttons.showAllStations": "Показать все заправки",
|
|
||||||
"common.buttons.allPromotions": "Все акции",
|
|
||||||
|
|
||||||
"common.navigation.home": "Главная",
|
|
||||||
"common.navigation.about": "О нас",
|
|
||||||
"common.navigation.clients": "Клиентам",
|
|
||||||
"common.navigation.stations": "Наши заправки",
|
|
||||||
"common.navigation.vacancies": "Вакансии",
|
|
||||||
"common.navigation.promotions": "Акции",
|
|
||||||
"common.navigation.charity": "Благотворительность",
|
|
||||||
"common.navigation.certificates": "Сертификаты",
|
|
||||||
"common.navigation.contacts": "Контакты",
|
|
||||||
|
|
||||||
"common.footer.contacts": "Контакты",
|
|
||||||
"common.footer.navigation": "Навигация",
|
|
||||||
"common.footer.yourEmail": "Ваш email",
|
|
||||||
"common.footer.rights": "Все права защищены.",
|
|
||||||
|
|
||||||
"home.hero.title": "Сеть современных заправок в Таджикистане",
|
|
||||||
"home.hero.description": "Качественное топливо, удобное расположение и отличный сервис для наших клиентов",
|
|
||||||
|
|
||||||
"home.stats.stations": "Заправок по стране",
|
|
||||||
"home.stats.daily": "Клиентов ежедневно",
|
|
||||||
"home.stats.years": "Лет на рынке",
|
|
||||||
"home.stats.mode": "Работаем круглосуточно",
|
|
||||||
|
|
||||||
"home.about.title": "О нашей компании",
|
|
||||||
"home.about.description1": "Наша сеть заправок является одной из ведущих в Таджикистане. Мы предоставляем качественное топливо и высокий уровень обслуживания для наших клиентов уже более 15 лет.",
|
|
||||||
"home.about.description2": "Мы постоянно развиваемся, открывая новые станции и улучшая сервис на существующих. Наша цель - сделать заправку автомобиля максимально удобной и быстрой для каждого клиента.",
|
|
||||||
"home.about.features.quality.title": "Качественное топливо",
|
|
||||||
"home.about.features.quality.description": "Мы гарантируем высокое качество нашего топлива",
|
|
||||||
"home.about.features.equipment.title": "Современное оборудование",
|
|
||||||
"home.about.features.equipment.description": "Все наши станции оснащены современным оборудованием",
|
|
||||||
"home.about.features.staff.title": "Профессиональный персонал",
|
|
||||||
"home.about.features.staff.description": "Наши сотрудники - профессионалы своего дела",
|
|
||||||
|
|
||||||
"about.hero.imageAlt": "О нашей компании",
|
|
||||||
"about.hero.title": "О нашей компании",
|
|
||||||
"about.hero.subtitle": "Узнайте больше о нашей истории, ценностях и миссии. Мы стремимся предоставлять лучший сервис и качественное топливо для наших клиентов.",
|
|
||||||
|
|
||||||
"about.overview.title": "Лидер на рынке Таджикистана",
|
|
||||||
"about.overview.description1": "GasNetwork - ведущая сеть автозаправочных станций в Таджикистане, предоставляющая высококачественное топливо и превосходный сервис. Наша компания была основана в 2008 году и с тех пор стала символом надежности и качества в энергетическом секторе страны.",
|
|
||||||
"about.overview.description2": "Мы гордимся тем, что предлагаем нашим клиентам только лучшее топливо, соответствующее международным стандартам качества. Наши заправочные станции оснащены современным оборудованием, которое обеспечивает быстрое и безопасное обслуживание.",
|
|
||||||
"about.overview.description3": "Наша миссия - сделать поездки наших клиентов комфортными и безопасными, предоставляя качественное топливо и отличный сервис по всей стране.",
|
|
||||||
"about.overview.imageAlt": "Главный офис GasNetwork",
|
|
||||||
|
|
||||||
"about.overview.benefits.0.title": "Качество",
|
|
||||||
"about.overview.benefits.0.description": "Топливо высшего стандарта",
|
|
||||||
"about.overview.benefits.1.title": "Сервис",
|
|
||||||
"about.overview.benefits.1.description": "Профессиональное обслуживание",
|
|
||||||
"about.overview.benefits.2.title": "Инновации",
|
|
||||||
"about.overview.benefits.2.description": "Современные технологии",
|
|
||||||
"about.overview.benefits.3.title": "Доступность",
|
|
||||||
"about.overview.benefits.3.description": "Станции по всей стране",
|
|
||||||
|
|
||||||
"about.stats.title": "GasNetwork в цифрах",
|
|
||||||
"about.stats.subtitle": "Наши достижения и рост за годы работы на рынке Таджикистана",
|
|
||||||
|
|
||||||
"about.stats.items.0.value": "25",
|
|
||||||
"about.stats.items.0.suffix": "+",
|
|
||||||
"about.stats.items.0.label": "Заправок по стране",
|
|
||||||
"about.stats.items.1.value": "15",
|
|
||||||
"about.stats.items.1.label": "Лет на рынке",
|
|
||||||
"about.stats.items.2.value": "150",
|
|
||||||
"about.stats.items.2.suffix": "+",
|
|
||||||
"about.stats.items.2.label": "Сотрудников",
|
|
||||||
"about.stats.items.3.value": "1000000",
|
|
||||||
"about.stats.items.3.suffix": "+",
|
|
||||||
"about.stats.items.3.label": "Клиентов в год",
|
|
||||||
"about.stats.items.4.label": "Литров топлива в месяц",
|
|
||||||
"about.stats.items.5.label": "Довольных клиентов",
|
|
||||||
|
|
||||||
"about.history.title": "Наша история",
|
|
||||||
"about.history.subtitle": "История развития нашей компании от небольшой сети до лидера рынка",
|
|
||||||
|
|
||||||
"about.stations.title": "Наши заправочные станции",
|
|
||||||
"about.stations.subtitle": "Современные заправочные станции, оснащенные по последнему слову техники",
|
|
||||||
"about.stations.description": "Наши заправочные станции расположены в стратегически важных точках по всему Таджикистану, обеспечивая удобный доступ для всех наших клиентов.",
|
|
||||||
"about.stations.buttonText": "Найти ближайшую заправку",
|
|
||||||
|
|
||||||
"about.values.title": "Наши ценности",
|
|
||||||
"about.values.subtitle": "Принципы, которыми мы руководствуемся в нашей работе",
|
|
||||||
"about.values.items.0.title": "Качество",
|
|
||||||
"about.values.items.0.description": "Мы предлагаем только высококачественное топливо, соответствующее международным стандартам. Регулярные проверки и контроль качества гарантируют, что наши клиенты получают лучшее.",
|
|
||||||
"about.values.items.1.title": "Клиентоориентированность",
|
|
||||||
"about.values.items.1.description": "Наши клиенты - наш главный приоритет. Мы стремимся предоставить лучший сервис, удобные условия и приятную атмосферу на каждой нашей заправке.",
|
|
||||||
"about.values.items.2.title": "Профессионализм",
|
|
||||||
"about.values.items.2.description": "Наши сотрудники - профессионалы своего дела. Мы постоянно инвестируем в их обучение и развитие, чтобы обеспечить высокий уровень обслуживания.",
|
|
||||||
|
|
||||||
"about.team.title": "Наша команда",
|
|
||||||
"about.team.subtitle": "Познакомьтесь с профессионалами, которые делают GasNetwork лучшей сетью заправок в Таджикистане",
|
|
||||||
"about.team.members.0.name": "Алишер Рахмонов",
|
|
||||||
"about.team.members.0.position": "Генеральный директор",
|
|
||||||
"about.team.members.1.name": "Фарида Каримова",
|
|
||||||
"about.team.members.1.position": "Финансовый директор",
|
|
||||||
"about.team.members.2.name": "Рустам Назаров",
|
|
||||||
"about.team.members.2.position": "Технический директор",
|
|
||||||
"about.team.members.3.name": "Зарина Шарипова",
|
|
||||||
"about.team.members.3.position": "Директор по маркетингу",
|
|
||||||
|
|
||||||
"about.testimonials.title": "Отзывы клиентов",
|
|
||||||
"about.testimonials.subtitle": "Что говорят о нас наши клиенты",
|
|
||||||
"about.testimonials.items.0.name": "Фархад К.",
|
|
||||||
"about.testimonials.items.0.text": "Я всегда заправляюсь только на GasNetwork. Качество топлива на высоте, а обслуживание всегда приветливое и быстрое.",
|
|
||||||
"about.testimonials.items.0.rating": "5",
|
|
||||||
"about.testimonials.items.1.name": "Нигина М.",
|
|
||||||
"about.testimonials.items.1.text": "Очень удобно, что заправки расположены по всему городу. Всегда чисто, есть кафе и магазин. Рекомендую!",
|
|
||||||
"about.testimonials.items.1.rating": "5",
|
|
||||||
"about.testimonials.items.2.name": "Джамшед Р.",
|
|
||||||
"about.testimonials.items.2.text": "Пользуюсь картой лояльности GasNetwork уже 3 года. Накопил много бонусов и получил немало приятных подарков. Отличный сервис!",
|
|
||||||
"about.testimonials.items.2.rating": "4",
|
|
||||||
|
|
||||||
"home.stations.title": "Наши заправки",
|
|
||||||
"home.stations.description": "Найдите ближайшую к вам заправку нашей сети. Мы расположены в удобных местах по всему Таджикистану.",
|
|
||||||
|
|
||||||
"home.promotions.title": "Актуальные акции",
|
|
||||||
"home.promotions.description": "Специальные предложения и акции для наших клиентов. Заправляйтесь выгодно!",
|
|
||||||
|
|
||||||
"home.vacancies.title": "Вакансии",
|
|
||||||
"home.vacancies.description": "Присоединяйтесь к нашей команде профессионалов. Мы предлагаем стабильную работу и возможности для роста.",
|
|
||||||
"home.vacancies.all": "Все вакансии",
|
|
||||||
"home.vacancies.office": "Офис",
|
|
||||||
"home.vacancies.stations": "Заправки",
|
|
||||||
"home.vacancies.fullTime": "Полный день",
|
|
||||||
"home.vacancies.experience": "Опыт от 1 года",
|
|
||||||
"home.vacancies.shiftWork": "Сменный график",
|
|
||||||
"home.vacancies.training": "Обучение",
|
|
||||||
|
|
||||||
"home.partners.title": "Наши партнеры",
|
|
||||||
"home.partners.description": "Мы сотрудничаем с ведущими компаниями для предоставления лучших услуг нашим клиентам.",
|
|
||||||
"home.partners.becomePartner": "Станьте нашим партнером",
|
|
||||||
"home.partners.becomePartnerText": "Мы открыты для сотрудничества и новых партнерских отношений. Свяжитесь с нами для обсуждения возможностей.",
|
|
||||||
|
|
||||||
"home.charity.title": "Благотворительный фонд",
|
|
||||||
"home.charity.description": "Наш благотворительный фонд был создан для поддержки социально значимых проектов в Таджикистане. Мы стремимся внести свой вклад в развитие общества и помочь тем, кто в этом нуждается.",
|
|
||||||
"home.charity.directions": "Основные направления деятельности нашего фонда:",
|
|
||||||
"home.charity.education": "Поддержка образовательных программ",
|
|
||||||
"home.charity.children": "Помощь детям из малообеспеченных семей",
|
|
||||||
"home.charity.ecology": "Экологические инициативы",
|
|
||||||
"home.charity.sports": "Поддержка спортивных мероприятий",
|
|
||||||
"home.charity.learnMore": "Подробнее о фонде",
|
|
||||||
|
|
||||||
"home.cta.title": "Присоединяйтесь к нам",
|
|
||||||
"home.cta.description": "Станьте частью нашей сети. Получайте специальные предложения, бонусы и скидки.",
|
|
||||||
|
|
||||||
"clients.title": "Для наших клиентов",
|
|
||||||
"clients.description": "Информация для клиентов: программа лояльности, топливные карты, сертификаты и способы оплаты.",
|
|
||||||
"clients.services.title": "Наши услуги для клиентов",
|
|
||||||
"clients.services.subtitle": "Мы стремимся сделать обслуживание на наших заправках максимально удобным и выгодным для вас",
|
|
||||||
"clients.benefits.title": "Преимущества для клиентов",
|
|
||||||
"clients.benefits.subtitle": "Став клиентом GasNetwork, вы получаете множество преимуществ, которые делают заправку вашего автомобиля более выгодной и удобной.",
|
|
||||||
|
|
||||||
"clients.loyalty.programm.about": "О программе лояльности",
|
|
||||||
"clients.loyalty.programm.about-description": "Программа лояльности GasNetwork — это возможность получать баллы за каждую покупку топлива и услуг на наших заправочных станциях. Накопленные баллы можно обменять на скидки, подарки или дополнительные услуги.",
|
|
||||||
"clients.loyalty.programm.about-description-2": "Участие в программе абсолютно бесплатное. Вам нужно только получить карту лояльности в любой нашей заправочной станции или зарегистрироваться в мобильном приложении.",
|
|
||||||
"clients.loyalty.programm.conditions-1": "1 литр = 1 балл",
|
|
||||||
"clients.loyalty.programm.conditions.description-1": "За каждый литр топлива вы получаете 1 балл",
|
|
||||||
"clients.loyalty.programm.conditions-2": "Дополнительные баллы",
|
|
||||||
"clients.loyalty.programm.conditions.description-2": "За покупки в магазине и кафе на заправке",
|
|
||||||
"clients.loyalty.programm.conditions-3": "Специальные акции",
|
|
||||||
"clients.loyalty.programm.conditions.description-3": "Удвоенные и утроенные баллы в праздничные дни",
|
|
||||||
|
|
||||||
"clients.loyalty.works.title": "Как это работает",
|
|
||||||
"clients.loyalty.works.description": "Простые шаги для участия в программе лояльности GasNetwork",
|
|
||||||
|
|
||||||
"clients.loyalty.works.stage-1": "Получите карту",
|
|
||||||
"clients.loyalty.works.stage.description-1": "Получите карту лояльности на любой заправке GasNetwork или зарегистрируйтесь в мобильном приложении",
|
|
||||||
"clients.loyalty.works.stage-2": "Заправляйтесь",
|
|
||||||
"clients.loyalty.works.stage.description-2": "Используйте карту при каждой заправке и покупке в магазинах на наших АЗС",
|
|
||||||
"clients.loyalty.works.stage-3": "Накапливайте баллы",
|
|
||||||
"clients.loyalty.works.stage.description-3": "Получайте баллы за каждую покупку и следите за их накоплением в приложении",
|
|
||||||
"clients.loyalty.works.stage-4": "Получайте выгоду",
|
|
||||||
"clients.loyalty.works.stage.description-4": "Обменивайте накопленные баллы на скидки, подарки или дополнительные услуги",
|
|
||||||
|
|
||||||
"clients.loyalty.works.levels.title": "Уровни лояльности",
|
|
||||||
"clients.loyalty.works.levels.description": "Чем больше вы заправляетесь, тем больше преимуществ получаете",
|
|
||||||
"clients.loyalty.works.levels.card.mark": "возврат баллами",
|
|
||||||
|
|
||||||
"clients.loyalty.works.levels.card-1.title": "Стандарт",
|
|
||||||
"clients.loyalty.works.levels.card-1.percent": "1%",
|
|
||||||
"clients.loyalty.works.levels.card-1.bonus-1": "1 балл за каждый литр топлива",
|
|
||||||
"clients.loyalty.works.levels.card-1.bonus-2": "Участие в акциях",
|
|
||||||
"clients.loyalty.works.levels.card-1.bonus-3": "Доступ к мобильному приложению",
|
|
||||||
|
|
||||||
"clients.loyalty.works.levels.card-2.title": "Золотой",
|
|
||||||
"clients.loyalty.works.levels.card-2.percent": "2%",
|
|
||||||
"clients.loyalty.works.levels.card-2.bonus-1": "2 балла за каждый литр топлива",
|
|
||||||
"clients.loyalty.works.levels.card-2.bonus-2": "Скидка 5% в кафе на заправках",
|
|
||||||
"clients.loyalty.works.levels.card-2.bonus-3": "Приоритетное обслуживание",
|
|
||||||
"clients.loyalty.works.levels.card-2.bonus-4": "Эксклюзивные акции",
|
|
||||||
|
|
||||||
"clients.loyalty.works.levels.card-3.title": "Платиновый",
|
|
||||||
"clients.loyalty.works.levels.card-3.percent": "3%",
|
|
||||||
"clients.loyalty.works.levels.card-3.bonus-1": "3 балла за каждый литр топлива",
|
|
||||||
"clients.loyalty.works.levels.card-3.bonus-2": "Скидка 10% в кафе на заправках",
|
|
||||||
"clients.loyalty.works.levels.card-3.bonus-3": "Бесплатная мойка раз в месяц",
|
|
||||||
"clients.loyalty.works.levels.card-3.bonus-4": "Персональный менеджер",
|
|
||||||
"clients.loyalty.works.levels.card-3.bonus-5": "VIP-обслуживание",
|
|
||||||
|
|
||||||
"certificates.title": "Наши сертификаты",
|
|
||||||
"certificates.description": "GasNetwork придерживается высоких стандартов качества и безопасности. Наши сертификаты подтверждают соответствие нашей продукции и услуг международным и национальным стандартам.",
|
|
||||||
"certificates.issueDate": "Дата выдачи",
|
|
||||||
"certificates.expiryDate": "Действителен до",
|
|
||||||
"certificates.faq": "Часто задаваемые вопросы",
|
|
||||||
|
|
||||||
"auth.title": "Вход в личный кабинет",
|
|
||||||
"auth.description": "Войдите в личный кабинет, чтобы получить доступ к информации о ваших бонусах, истории операций и другим возможностям.",
|
|
||||||
"auth.bonusClient": "Бонусный клиент",
|
|
||||||
"auth.corporateClient": "Корпоративный клиент",
|
|
||||||
"auth.bonusLogin.title": "Вход для бонусных клиентов",
|
|
||||||
"auth.bonusLogin.description": "Введите номер телефона и номер бонусной карты для входа в личный кабинет.",
|
|
||||||
"auth.corporateLogin.title": "Вход для корпоративных клиентов",
|
|
||||||
"auth.corporateLogin.description": "Введите номер телефона и номер корпоративной карты для входа в личный кабинет.",
|
|
||||||
"auth.phoneNumber": "Номер телефона",
|
|
||||||
"auth.cardNumber": "Номер карты",
|
|
||||||
"auth.loginIssues": "Возникли проблемы со входом?",
|
|
||||||
"auth.contactLink": "Свяжитесь с нами",
|
|
||||||
|
|
||||||
"map.filters": "Фильтры",
|
|
||||||
"map.stationsList": "Список заправок",
|
|
||||||
"map.noStations": "Нет заправок, соответствующих выбранным фильтрам",
|
|
||||||
"map.ourStations": "Наши заправки",
|
|
||||||
"map.totalStations": "Всего станций",
|
|
||||||
"map.services": "Услуги",
|
|
||||||
"map.cities": "Города",
|
|
||||||
"map.allCities": "Все города"
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Check, Globe } from 'lucide-react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { type Language, languages, useLanguage } from '@/shared/language';
|
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/shared/shadcn-ui/dropdown-menu';
|
|
||||||
|
|
||||||
export function LanguageSwitcher() {
|
|
||||||
const { language, setLanguage } = useLanguage();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant='ghost' size='sm' className='h-8 w-8 px-0'>
|
|
||||||
<Globe className='h-4 w-4' />
|
|
||||||
<span className='sr-only'>Switch language</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align='end'>
|
|
||||||
{Object.entries(languages).map(([code, { name, flag }]) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
key={code}
|
|
||||||
onClick={() => {
|
|
||||||
setLanguage(code as Language);
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className='mr-2'>{flag}</span>
|
|
||||||
{name}
|
|
||||||
{code === language && <Check className='ml-2 h-4 w-4' />}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -2,23 +2,25 @@
|
|||||||
|
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import { LanguageProvider } from '../language';
|
import { TextControlProvider } from '../language';
|
||||||
import { store } from '../store';
|
import { store } from '../store';
|
||||||
import { ThemeProvider } from '../theme/theme-provider';
|
import { ThemeProvider } from '../theme/theme-provider';
|
||||||
|
import { TextItem } from '../types/text.types';
|
||||||
import { AosProvider } from './aos-provider';
|
import { AosProvider } from './aos-provider';
|
||||||
import { Toaster } from './toaster';
|
import { Toaster } from './toaster';
|
||||||
|
|
||||||
type ProvidersProps = {
|
type ProvidersProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
textItems: TextItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Providers = ({ children }: ProvidersProps) => {
|
export const Providers = ({ children, textItems }: ProvidersProps) => {
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<LanguageProvider>
|
<TextControlProvider textItems={textItems}>
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute='class'
|
attribute='class'
|
||||||
defaultTheme='system'
|
defaultTheme='light'
|
||||||
enableSystem
|
enableSystem
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
@ -27,7 +29,7 @@ export const Providers = ({ children }: ProvidersProps) => {
|
|||||||
<Toaster />
|
<Toaster />
|
||||||
</AosProvider>
|
</AosProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</LanguageProvider>
|
</TextControlProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
10
src/shared/shadcn-ui/conteiner.tsx
Normal file
10
src/shared/shadcn-ui/conteiner.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
interface ContainerProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
export default function Container({children}: ContainerProps) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-2.5 py-1">{children}</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,15 +1,20 @@
|
|||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import { setupListeners } from '@reduxjs/toolkit/query/react';
|
import { setupListeners } from '@reduxjs/toolkit/query/react';
|
||||||
|
import { createWrapper } from 'next-redux-wrapper';
|
||||||
|
|
||||||
import { baseAPI } from '@/shared/api/base-api';
|
import { baseAPI } from '@/shared/api/base-api';
|
||||||
|
|
||||||
import { rootReducer } from './root-reducer';
|
import { rootReducer } from './root-reducer';
|
||||||
|
|
||||||
export const store = configureStore({
|
export const makeStore = () =>
|
||||||
|
configureStore({
|
||||||
reducer: rootReducer,
|
reducer: rootReducer,
|
||||||
middleware: (getDefaultMiddleware) =>
|
middleware: (getDefaultMiddleware) =>
|
||||||
getDefaultMiddleware().concat(baseAPI.middleware),
|
getDefaultMiddleware().concat(baseAPI.middleware),
|
||||||
devTools: process.env.NODE_ENV === 'development',
|
devTools: process.env.NODE_ENV === 'development',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const store = makeStore();
|
||||||
|
|
||||||
setupListeners(store.dispatch);
|
setupListeners(store.dispatch);
|
||||||
|
|
||||||
@ -17,3 +22,5 @@ setupListeners(store.dispatch);
|
|||||||
export type RootState = ReturnType<typeof store.getState>;
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
|
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
|
||||||
export type AppDispatch = typeof store.dispatch;
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
|
||||||
|
export const wrapper = createWrapper(makeStore);
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { combineReducers } from '@reduxjs/toolkit';
|
import { combineReducers } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import { baseAPI } from '@/shared/api/base-api';
|
import { baseAPI } from '@/shared/api/base-api';
|
||||||
|
|
||||||
export const rootReducer = combineReducers({
|
export const rootReducer = combineReducers({
|
||||||
|
|||||||
4
src/shared/types/text.types.ts
Normal file
4
src/shared/types/text.types.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface TextItem {
|
||||||
|
key: string;
|
||||||
|
value: string | null;
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@
|
|||||||
import { Calendar, ChevronDown, ChevronUp } from 'lucide-react';
|
import { Calendar, ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
import { Card, CardContent } from '@/shared/shadcn-ui/card';
|
import { Card, CardContent } from '@/shared/shadcn-ui/card';
|
||||||
|
|
||||||
@ -61,6 +62,8 @@ export function CompanyTimeline() {
|
|||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const displayEvents = expanded ? timelineEvents : timelineEvents.slice(0, 4);
|
const displayEvents = expanded ? timelineEvents : timelineEvents.slice(0, 4);
|
||||||
|
|
||||||
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<div className='absolute left-1/2 -z-10 -ml-0.5 hidden h-full w-0.5 bg-gradient-to-b from-red-200 via-red-200 to-transparent md:block' />
|
<div className='absolute left-1/2 -z-10 -ml-0.5 hidden h-full w-0.5 bg-gradient-to-b from-red-200 via-red-200 to-transparent md:block' />
|
||||||
@ -116,11 +119,13 @@ export function CompanyTimeline() {
|
|||||||
>
|
>
|
||||||
{expanded ? (
|
{expanded ? (
|
||||||
<>
|
<>
|
||||||
Свернуть <ChevronUp className='ml-2 size-4' />
|
{t('about.companyTimeline.rollUp.button')}{' '}
|
||||||
|
<ChevronUp className='ml-2 size-4' />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
Показать больше <ChevronDown className='ml-2 size-4' />
|
{t('about.companyTimeline.show.button')}{' '}
|
||||||
|
<ChevronDown className='ml-2 size-4' />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { ChevronLeft, ChevronRight, Maximize } from 'lucide-react';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -72,8 +73,10 @@ export function StationGallery() {
|
|||||||
setCurrentImage((prev) => (prev === 0 ? stations.length - 1 : prev - 1));
|
setCurrentImage((prev) => (prev === 0 ? stations.length - 1 : prev - 1));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='space-y-8'>
|
<div className='space-y-8 px-2'>
|
||||||
<div className='relative h-[400px] overflow-hidden rounded-xl shadow-xl md:h-[500px]'>
|
<div className='relative h-[400px] overflow-hidden rounded-xl shadow-xl md:h-[500px]'>
|
||||||
<Image
|
<Image
|
||||||
src={stations[currentImage].image || '/placeholder.svg'}
|
src={stations[currentImage].image || '/placeholder.svg'}
|
||||||
@ -93,7 +96,7 @@ export function StationGallery() {
|
|||||||
onClick={prevImage}
|
onClick={prevImage}
|
||||||
>
|
>
|
||||||
<ChevronLeft className='h-6 w-6' />
|
<ChevronLeft className='h-6 w-6' />
|
||||||
<span className='sr-only'>Предыдущая</span>
|
<span className='sr-only'>{t('about.gallery.previous')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -103,7 +106,7 @@ export function StationGallery() {
|
|||||||
onClick={nextImage}
|
onClick={nextImage}
|
||||||
>
|
>
|
||||||
<ChevronRight className='h-6 w-6' />
|
<ChevronRight className='h-6 w-6' />
|
||||||
<span className='sr-only'>Следующая</span>
|
<span className='sr-only'>{t('about.gallery.next')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Dialog>
|
<Dialog>
|
||||||
|
|||||||
@ -4,10 +4,10 @@ import { Users } from 'lucide-react';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
import AboutCounter from '@/shared/components/about-counter';
|
import AboutCounter from '@/shared/components/about-counter';
|
||||||
import { useLanguage } from '@/shared/language';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
|
||||||
export const AboutSection = () => {
|
export const AboutSection = () => {
|
||||||
const { t } = useLanguage();
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id='about' className='px-2 py-8 sm:py-16'>
|
<section id='about' className='px-2 py-8 sm:py-16'>
|
||||||
@ -64,7 +64,7 @@ const features: Array<Feature> = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const Features = () => {
|
const Features = () => {
|
||||||
const { t } = useLanguage();
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
|
|||||||
@ -4,11 +4,11 @@ import { ChevronRight, Heart } from 'lucide-react';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { useLanguage } from '@/shared/language';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
|
|
||||||
export const CharitySection = () => {
|
export const CharitySection = () => {
|
||||||
const { t } = useLanguage();
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id='charity' className='px-2 py-8 sm:py-16'>
|
<section id='charity' className='px-2 py-8 sm:py-16'>
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import { useLanguage } from '@/shared/language';
|
|
||||||
import { Percent } from 'lucide-react';
|
import { Percent } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
import Container from '@/shared/shadcn-ui/conteiner';
|
||||||
|
|
||||||
interface Benefit {
|
interface Benefit {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
@ -29,10 +31,10 @@ const benefits: Array<Benefit> = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const BenefitsSection = () => {
|
export const BenefitsSection = () => {
|
||||||
|
const { t } = useTextController();
|
||||||
const {t} = useLanguage()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<section className='bg-gray-50 py-16'>
|
<section className='bg-gray-50 py-16'>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
<div className='grid items-center gap-12 md:grid-cols-2'>
|
<div className='grid items-center gap-12 md:grid-cols-2'>
|
||||||
@ -74,5 +76,6 @@ export const BenefitsSection = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import { CreditCard, type LucideProps, Percent, Wallet } from 'lucide-react';
|
import { CreditCard, type LucideProps, Percent, Wallet } from 'lucide-react';
|
||||||
import { type ForwardRefExoticComponent, type RefAttributes } from 'react';
|
import { type ForwardRefExoticComponent, type RefAttributes } from 'react';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@ -10,7 +11,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/shared/shadcn-ui/card';
|
} from '@/shared/shadcn-ui/card';
|
||||||
import { useLanguage } from '@/shared/language';
|
import Container from '@/shared/shadcn-ui/conteiner';
|
||||||
|
|
||||||
interface ServiceOverview {
|
interface ServiceOverview {
|
||||||
title: string;
|
title: string;
|
||||||
@ -46,10 +47,10 @@ const servicesOverview: Array<ServiceOverview> = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const ServicesOverviewSection = () => {
|
export const ServicesOverviewSection = () => {
|
||||||
|
const { t } = useTextController();
|
||||||
const {t} = useLanguage()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<section className='py-16'>
|
<section className='py-16'>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
<div className='mb-12 text-center'>
|
<div className='mb-12 text-center'>
|
||||||
@ -84,5 +85,6 @@ export const ServicesOverviewSection = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { useLanguage } from '@/shared/language';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
|
|
||||||
export const CtaSection = () => {
|
export const CtaSection = () => {
|
||||||
const { t } = useLanguage();
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className='bg-red-600 px-2 py-8 text-white sm:py-16'>
|
<section className='bg-red-600 px-2 py-8 text-white sm:py-16'>
|
||||||
|
|||||||
@ -3,19 +3,20 @@
|
|||||||
import { Fuel, Mail, MapPin, Phone } from 'lucide-react';
|
import { Fuel, Mail, MapPin, Phone } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { useLanguage } from '@/shared/language';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
|
||||||
export const Footer = () => {
|
export const Footer = () => {
|
||||||
const { t } = useLanguage();
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className='bg-gray-900 px-4 py-12 text-white'>
|
<footer className='bg-gray-900 px-4 py-12 text-white'>
|
||||||
<div className='containe mx-autor'>
|
<div className='containe mx-autor'>
|
||||||
<div className='grid grid-cols-1 gap-8 md:grid-cols-3'>
|
<div className='grid grid-cols-1 gap-8 md:gap-4 md:grid-cols-3'>
|
||||||
|
<div className='flex md:justify-center md:items-center'>
|
||||||
<div>
|
<div>
|
||||||
<div className='mb-4 flex items-center gap-2'>
|
<div className='mb-4 flex items-center gap-2'>
|
||||||
<Fuel className='h-6 w-6 text-red-500' />
|
<Fuel className='h-6 w-6 text-red-500' />
|
||||||
<span className='text-xl font-bold'>GasNetwork</span>
|
<span className='text-xl font-bold'>{t('common.name')}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className='mb-4 text-gray-400'>{t('home.hero.description')}</p>
|
<p className='mb-4 text-gray-400'>{t('home.hero.description')}</p>
|
||||||
<div className='flex space-x-4'>
|
<div className='flex space-x-4'>
|
||||||
@ -59,6 +60,8 @@ export const Footer = () => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex md:justify-center md:items-center'>
|
||||||
<div>
|
<div>
|
||||||
<h3 className='mb-4 text-lg font-semibold'>
|
<h3 className='mb-4 text-lg font-semibold'>
|
||||||
{t('common.footer.contacts')}
|
{t('common.footer.contacts')}
|
||||||
@ -78,6 +81,8 @@ export const Footer = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex md:justify-center md:items-center'>
|
||||||
<div>
|
<div>
|
||||||
<h3 className='mb-4 text-lg font-semibold'>
|
<h3 className='mb-4 text-lg font-semibold'>
|
||||||
{t('common.footer.navigation')}
|
{t('common.footer.navigation')}
|
||||||
@ -120,9 +125,10 @@ export const Footer = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className='mt-8 border-t border-gray-800 pt-8 text-center text-gray-400'>
|
<div className='mt-8 border-t border-gray-800 pt-8 text-center text-gray-400'>
|
||||||
<p>
|
<p>
|
||||||
© {new Date().getFullYear()} GasNetwork.{' '}
|
© {new Date().getFullYear()} {t('common.name')}.{' '}
|
||||||
{t('common.footer.rights')}
|
{t('common.footer.rights')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { cn } from '@/shared/lib/utils';
|
import { cn } from '@/shared/lib/utils';
|
||||||
import {
|
import {
|
||||||
NavigationMenu,
|
NavigationMenu,
|
||||||
@ -11,11 +12,9 @@ import {
|
|||||||
NavigationMenuTrigger,
|
NavigationMenuTrigger,
|
||||||
navigationMenuTriggerStyle,
|
navigationMenuTriggerStyle,
|
||||||
} from '@/shared/shadcn-ui/navigation-menu';
|
} from '@/shared/shadcn-ui/navigation-menu';
|
||||||
import { useLanguage } from '@/shared/language';
|
|
||||||
|
|
||||||
export function DesktopNav() {
|
export function DesktopNav() {
|
||||||
|
const { t } = useTextController();
|
||||||
const { t } = useLanguage();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationMenu className='hidden lg:flex'>
|
<NavigationMenu className='hidden lg:flex'>
|
||||||
@ -41,7 +40,9 @@ export function DesktopNav() {
|
|||||||
</Link>
|
</Link>
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
<NavigationMenuItem>
|
<NavigationMenuItem>
|
||||||
<NavigationMenuTrigger>{t('common.navigation.clients')}</NavigationMenuTrigger>
|
<NavigationMenuTrigger>
|
||||||
|
{t('common.navigation.clients')}
|
||||||
|
</NavigationMenuTrigger>
|
||||||
<NavigationMenuContent>
|
<NavigationMenuContent>
|
||||||
<ul className='grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px]'>
|
<ul className='grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px]'>
|
||||||
<li className='row-span-4'>
|
<li className='row-span-4'>
|
||||||
@ -60,10 +61,10 @@ export function DesktopNav() {
|
|||||||
</NavigationMenuLink>
|
</NavigationMenuLink>
|
||||||
</li>
|
</li>
|
||||||
<ListItem href='/clients/loyalty' title='Программа лояльности'>
|
<ListItem href='/clients/loyalty' title='Программа лояльности'>
|
||||||
Накапливайте баллы и получайте скидки на топливо и услуги
|
{t('clients.header.loyalty.description')}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem href='/clients/certificates' title='Сертификаты'>
|
<ListItem href='/clients/certificates' title='Сертификаты'>
|
||||||
Подарочные сертификаты на топливо и услуги нашей сети
|
{t('clients.header.certificates.description')}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</ul>
|
</ul>
|
||||||
</NavigationMenuContent>
|
</NavigationMenuContent>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { UserCircle } from 'lucide-react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Logo } from '@/shared/assets/logo';
|
import { Logo } from '@/shared/assets/logo';
|
||||||
import { useLanguage } from '@/shared/language';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
// import { LanguageSwitcher } from '@/shared/language/ui/language-switcher';
|
// import { LanguageSwitcher } from '@/shared/language/ui/language-switcher';
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ import { DesktopNav } from './desktop-nav';
|
|||||||
import { MobileNav } from './mobile-nav';
|
import { MobileNav } from './mobile-nav';
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const { t } = useLanguage();
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className='sticky top-0 z-40 w-full border-b bg-white'>
|
<header className='sticky top-0 z-40 w-full border-b bg-white'>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { ChevronDown, ChevronRight, Menu } from 'lucide-react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
@ -16,6 +17,8 @@ export function MobileNav() {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [clientsOpen, setClientsOpen] = useState(false);
|
const [clientsOpen, setClientsOpen] = useState(false);
|
||||||
|
|
||||||
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={setOpen}>
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
@ -31,19 +34,19 @@ export function MobileNav() {
|
|||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
className='text-lg font-medium transition-colors hover:text-red-600'
|
className='text-lg font-medium transition-colors hover:text-red-600'
|
||||||
>
|
>
|
||||||
Главная
|
{t('common.navigation.home')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href='/about'
|
href='/about'
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
className='text-lg font-medium transition-colors hover:text-red-600'
|
className='text-lg font-medium transition-colors hover:text-red-600'
|
||||||
>
|
>
|
||||||
О нас
|
{t('common.navigation.about')}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Collapsible open={clientsOpen} onOpenChange={setClientsOpen}>
|
<Collapsible open={clientsOpen} onOpenChange={setClientsOpen}>
|
||||||
<CollapsibleTrigger className='data-[state=open]:animate-collapsible-down data-[state=closed]:animate-collapsible-up flex w-full items-center justify-between text-lg font-medium transition-all hover:text-red-600'>
|
<CollapsibleTrigger className='data-[state=open]:animate-collapsible-down data-[state=closed]:animate-collapsible-up flex w-full items-center justify-between text-lg font-medium transition-all hover:text-red-600'>
|
||||||
<span>Клиентам</span>
|
<span>{t('common.navigation.clients')}</span>
|
||||||
{clientsOpen ? (
|
{clientsOpen ? (
|
||||||
<ChevronDown className='h-5 w-5' />
|
<ChevronDown className='h-5 w-5' />
|
||||||
) : (
|
) : (
|
||||||
@ -56,28 +59,14 @@ export function MobileNav() {
|
|||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
className='block py-1 text-base transition-colors hover:text-red-600'
|
className='block py-1 text-base transition-colors hover:text-red-600'
|
||||||
>
|
>
|
||||||
Программа лояльности
|
{t('common.navigation.loyatly')}
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href='/clients/fuel-card'
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
className='block py-1 text-base transition-colors hover:text-red-600'
|
|
||||||
>
|
|
||||||
Топливная карта
|
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href='/clients/certificates'
|
href='/clients/certificates'
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
className='block py-1 text-base transition-colors hover:text-red-600'
|
className='block py-1 text-base transition-colors hover:text-red-600'
|
||||||
>
|
>
|
||||||
Сертификаты
|
{t('common.navigation.certificates')}
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href='/clients/payment'
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
className='block py-1 text-base transition-colors hover:text-red-600'
|
|
||||||
>
|
|
||||||
Способы оплаты
|
|
||||||
</Link>
|
</Link>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
@ -87,35 +76,35 @@ export function MobileNav() {
|
|||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
className='text-lg font-medium transition-colors hover:text-red-600'
|
className='text-lg font-medium transition-colors hover:text-red-600'
|
||||||
>
|
>
|
||||||
Наши заправки
|
{t('common.navigation.stations')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href='/#vacancies'
|
href='/#vacancies'
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
className='text-lg font-medium transition-colors hover:text-red-600'
|
className='text-lg font-medium transition-colors hover:text-red-600'
|
||||||
>
|
>
|
||||||
Вакансии
|
{t('common.navigation.vacancies')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href='/#promotions'
|
href='/#promotions'
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
className='text-lg font-medium transition-colors hover:text-red-600'
|
className='text-lg font-medium transition-colors hover:text-red-600'
|
||||||
>
|
>
|
||||||
Акции
|
{t('common.navigation.promotions')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href='/#partners'
|
href='/#partners'
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
className='text-lg font-medium transition-colors hover:text-red-600'
|
className='text-lg font-medium transition-colors hover:text-red-600'
|
||||||
>
|
>
|
||||||
Партнеры
|
{t('common.navigation.partners')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href='/#charity'
|
href='/#charity'
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
className='text-lg font-medium transition-colors hover:text-red-600'
|
className='text-lg font-medium transition-colors hover:text-red-600'
|
||||||
>
|
>
|
||||||
Благотворительность
|
{t('common.navigation.charity')}
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
|
|||||||
@ -4,11 +4,11 @@ import { MapPin } from 'lucide-react';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { useLanguage } from '@/shared/language';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
|
|
||||||
export const HeroSection = () => {
|
export const HeroSection = () => {
|
||||||
const { t } = useLanguage();
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className='relative'>
|
<section className='relative'>
|
||||||
|
|||||||
@ -2,16 +2,30 @@
|
|||||||
|
|
||||||
import { MapPin } from 'lucide-react';
|
import { MapPin } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Stations } from '@/app/api-utlities/@types/main';
|
||||||
|
|
||||||
import { GasStationMap } from '@/features/map';
|
import { GasStationMap } from '@/features/map';
|
||||||
|
import { Point } from '@/features/map/model';
|
||||||
|
import { YandexMap } from '@/features/map/ui/yandex-map';
|
||||||
|
|
||||||
import { useLanguage } from '@/shared/language';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
|
||||||
export const MapSection = () => {
|
interface MapSectionProps {
|
||||||
const { t } = useLanguage();
|
stations: Stations;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MapSection = ({ stations }: MapSectionProps) => {
|
||||||
|
const { t } = useTextController();
|
||||||
|
|
||||||
|
const points = stations.map((st) => ({
|
||||||
|
id: st.id,
|
||||||
|
coordinates: [st.latitude, st.longitude],
|
||||||
|
})) as Point[];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id='stations' className='bg-gray-50 px-2 py-8 sm:py-16'>
|
<section id='stations' className='bg-gray-50 px-2 py-8 sm:py-16'>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
|
<YandexMap points={points} />
|
||||||
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
<MapPin className='h-6 w-6 text-red-600' />
|
<MapPin className='h-6 w-6 text-red-600' />
|
||||||
@ -27,7 +41,7 @@ export const MapSection = () => {
|
|||||||
className='h-[500px] overflow-hidden rounded-xl border shadow-lg'
|
className='h-[500px] overflow-hidden rounded-xl border shadow-lg'
|
||||||
data-aos='fade-up'
|
data-aos='fade-up'
|
||||||
>
|
>
|
||||||
<GasStationMap />
|
{/* <GasStationMap stations={stations} /> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -4,11 +4,17 @@ import { Handshake } from 'lucide-react';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { useLanguage } from '@/shared/language';
|
import { Partners } from '@/app/api-utlities/@types/main';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
|
|
||||||
export const PartnersSection = () => {
|
interface PartnersSectionProps {
|
||||||
const { t } = useLanguage();
|
partners: Partners;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PartnersSection = ({ partners }: PartnersSectionProps) => {
|
||||||
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id='partners' className='bg-gray-50 px-2 py-8 sm:py-16'>
|
<section id='partners' className='bg-gray-50 px-2 py-8 sm:py-16'>
|
||||||
@ -26,20 +32,23 @@ export const PartnersSection = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid grid-cols-2 gap-4 sm:gap-8 md:grid-cols-4'>
|
<div className='grid grid-cols-2 gap-4 sm:gap-8 md:grid-cols-4'>
|
||||||
{[1, 2, 3, 4, 5, 6, 7, 8].map((partner) => (
|
{partners.map(({ id, name, poster }) => (
|
||||||
<div
|
<div
|
||||||
key={partner}
|
key={id}
|
||||||
className='flex h-32 flex-col items-center justify-center gap-0.5 rounded-lg bg-white p-6 shadow-md transition-transform hover:scale-105'
|
className='flex h-32 flex-col items-center justify-center gap-0.5 rounded-lg bg-white p-6 shadow-md transition-transform hover:scale-105'
|
||||||
data-aos='flip-left'
|
data-aos='flip-left'
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={`/placeholder.svg?height=80&width=160&text=Partner ${partner}`}
|
src={
|
||||||
alt={`Partner ${partner}`}
|
poster ??
|
||||||
|
`/placeholder.svg?height=80&width=160&text=Partner ${id}`
|
||||||
|
}
|
||||||
|
alt={`Partner ${id}`}
|
||||||
width={160}
|
width={160}
|
||||||
height={80}
|
height={80}
|
||||||
className='max-h-16 w-auto'
|
className='max-h-16 w-auto'
|
||||||
/>
|
/>
|
||||||
<h4 className='font-extralight'>Название</h4>
|
<h4 className='font-extralight'>{name}</h4>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,14 +2,20 @@
|
|||||||
|
|
||||||
import { Gift } from 'lucide-react';
|
import { Gift } from 'lucide-react';
|
||||||
|
|
||||||
import PromotionSlider from '@/shared/components/promotion-slider';
|
import { Discounts } from '@/app/api-utlities/@types/main';
|
||||||
import { useLanguage } from '@/shared/language';
|
|
||||||
|
|
||||||
export const PromotionsSection = () => {
|
import PromotionSlider from '@/shared/components/promotion-slider';
|
||||||
const { t } = useLanguage();
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
|
||||||
|
interface PromotionsSectionProps {
|
||||||
|
discounts: Discounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PromotionsSection = ({ discounts }: PromotionsSectionProps) => {
|
||||||
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id='promotions' className='bg-gray-50 py-8 px-2 sm:py-16'>
|
<section id='promotions' className='bg-gray-50 px-2 py-8 sm:py-16'>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
@ -22,7 +28,7 @@ export const PromotionsSection = () => {
|
|||||||
{t('home.promotions.description')}
|
{t('home.promotions.description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<PromotionSlider />
|
<PromotionSlider discounts={discounts} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,14 +2,15 @@
|
|||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
|
||||||
import AnimatedCounter from '../shared/components/animated-counter';
|
import AnimatedCounter from '../shared/components/animated-counter';
|
||||||
import { useLanguage } from '@/shared/language';
|
|
||||||
|
|
||||||
export function StatsSection() {
|
export function StatsSection() {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const sectionRef = useRef<HTMLDivElement>(null);
|
const sectionRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { t } = useLanguage()
|
const { t } = useTextController();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
@ -35,9 +36,12 @@ export function StatsSection() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section ref={sectionRef} className='bg-red-600 py-6 sm:py-12 px-2 text-white'>
|
<section
|
||||||
|
ref={sectionRef}
|
||||||
|
className='bg-red-600 px-2 py-6 text-white sm:py-12'
|
||||||
|
>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
<div className='grid grid-cols-2 gap-4 sm:gap-8 text-center md:grid-cols-4'>
|
<div className='grid grid-cols-2 gap-4 text-center sm:gap-8 md:grid-cols-4'>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<h3 className='text-3xl font-bold'>
|
<h3 className='text-3xl font-bold'>
|
||||||
{isVisible ? <AnimatedCounter end={25} suffix='+' /> : '0+'}
|
{isVisible ? <AnimatedCounter end={25} suffix='+' /> : '0+'}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { ru } from 'date-fns/locale';
|
|||||||
import { CalendarIcon } from 'lucide-react';
|
import { CalendarIcon } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
import { Calendar } from '@/shared/shadcn-ui/calendar';
|
import { Calendar } from '@/shared/shadcn-ui/calendar';
|
||||||
import { Label } from '@/shared/shadcn-ui/label';
|
import { Label } from '@/shared/shadcn-ui/label';
|
||||||
@ -99,15 +100,21 @@ export const TransactionsTable = () => {
|
|||||||
setFilteredTransactions(filtered);
|
setFilteredTransactions(filtered);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='space-y-6'>
|
<div className='space-y-6'>
|
||||||
<div className='flex flex-col items-start justify-between gap-4 md:flex-row md:items-center'>
|
<div className='flex flex-col items-start justify-between gap-4 md:flex-row md:items-center'>
|
||||||
<h2 className='text-2xl font-bold'>История операций</h2>
|
<h2 className='text-2xl font-bold'>
|
||||||
|
{t('corporate.transactions.title')}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<div className='flex w-full flex-col gap-4 md:w-auto md:flex-row'>
|
<div className='flex w-full flex-col gap-4 md:w-auto md:flex-row'>
|
||||||
<div className='grid grid-cols-2 gap-2'>
|
<div className='grid grid-cols-2 gap-2'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Label htmlFor='start-date'>От</Label>
|
<Label htmlFor='start-date'>
|
||||||
|
{t('corporate.transactions.dateFrom')}
|
||||||
|
</Label>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
@ -132,7 +139,9 @@ export const TransactionsTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Label htmlFor='end-date'>До</Label>
|
<Label htmlFor='end-date'>
|
||||||
|
{t('corporate.transactions.dateTo')}
|
||||||
|
</Label>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
@ -142,7 +151,7 @@ export const TransactionsTable = () => {
|
|||||||
<CalendarIcon className='mr-2 h-4 w-4' />
|
<CalendarIcon className='mr-2 h-4 w-4' />
|
||||||
{endDate
|
{endDate
|
||||||
? format(endDate, 'PP', { locale: ru })
|
? format(endDate, 'PP', { locale: ru })
|
||||||
: 'Выберите дату'}
|
: t('corporate.transactions.selectDate')}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className='w-auto p-0'>
|
<PopoverContent className='w-auto p-0'>
|
||||||
@ -161,7 +170,7 @@ export const TransactionsTable = () => {
|
|||||||
className='mt-auto bg-red-600 hover:bg-red-700'
|
className='mt-auto bg-red-600 hover:bg-red-700'
|
||||||
onClick={filterTransactions}
|
onClick={filterTransactions}
|
||||||
>
|
>
|
||||||
Применить
|
{t('corporate.transactions.applyButton')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -170,12 +179,24 @@ export const TransactionsTable = () => {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Дата</TableHead>
|
<TableHead>
|
||||||
<TableHead>Станция</TableHead>
|
{t('corporate.transactions.tableHeaders.date')}
|
||||||
<TableHead>Продукт</TableHead>
|
</TableHead>
|
||||||
<TableHead className='text-right'>Кол-во (л)</TableHead>
|
<TableHead>
|
||||||
<TableHead className='text-right'>Стоимость</TableHead>
|
{t('corporate.transactions.tableHeaders.station')}
|
||||||
<TableHead className='text-right'>Сумма</TableHead>
|
</TableHead>
|
||||||
|
<TableHead>
|
||||||
|
{t('corporate.transactions.tableHeaders.product')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className='text-right'>
|
||||||
|
{t('corporate.transactions.tableHeaders.quantity')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className='text-right'>
|
||||||
|
{t('corporate.transactions.tableHeaders.price')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className='text-right'>
|
||||||
|
{t('corporate.transactions.tableHeaders.total')}
|
||||||
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@ -191,10 +212,10 @@ export const TransactionsTable = () => {
|
|||||||
{transaction.quantity}
|
{transaction.quantity}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className='text-right'>
|
<TableCell className='text-right'>
|
||||||
{transaction.cost.toFixed(2)} сомони
|
{transaction.cost.toFixed(2)} {t('corporate.currency')}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className='text-right font-medium'>
|
<TableCell className='text-right font-medium'>
|
||||||
{transaction.total.toFixed(2)} сомони
|
{transaction.total.toFixed(2)} {t('corporate.currency')}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
@ -204,7 +225,7 @@ export const TransactionsTable = () => {
|
|||||||
colSpan={6}
|
colSpan={6}
|
||||||
className='py-6 text-center text-gray-500'
|
className='py-6 text-center text-gray-500'
|
||||||
>
|
>
|
||||||
Нет операций за выбранный период
|
{t('corporate.transactions.noTransactions')}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
import { Briefcase } from 'lucide-react';
|
import { Briefcase } from 'lucide-react';
|
||||||
|
|
||||||
import { useLanguage } from '@/shared/language';
|
import { Jobs } from '@/app/api-utlities/@types/main';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { cn } from '@/shared/lib/utils';
|
import { cn } from '@/shared/lib/utils';
|
||||||
import { Badge } from '@/shared/shadcn-ui/badge';
|
import { Badge } from '@/shared/shadcn-ui/badge';
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
@ -14,8 +16,25 @@ import {
|
|||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from '@/shared/shadcn-ui/tabs';
|
} from '@/shared/shadcn-ui/tabs';
|
||||||
|
|
||||||
export const VacanciesSection = () => {
|
interface VacanciesSectionProps {
|
||||||
const { t } = useLanguage();
|
jobs: Jobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VacanciesSection = ({ jobs }: VacanciesSectionProps) => {
|
||||||
|
const { t } = useTextController();
|
||||||
|
|
||||||
|
const jobsByType = new Map();
|
||||||
|
|
||||||
|
jobs.forEach((job) => {
|
||||||
|
const existing = jobsByType.get(job.type) || [];
|
||||||
|
jobsByType.set(job.type, [...existing, job]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const allVacancies = t('home.vacancies.all');
|
||||||
|
const officeVacancies = t('home.vacancies.office');
|
||||||
|
const stationsVacancies = t('home.vacancies.stations');
|
||||||
|
|
||||||
|
const jobsTabsTitle = [allVacancies, ...Array.from(jobsByType.keys())];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id='vacancies' className='px-2 py-8 sm:py-16'>
|
<section id='vacancies' className='px-2 py-8 sm:py-16'>
|
||||||
@ -32,53 +51,42 @@ export const VacanciesSection = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs defaultValue='all' className='mx-auto w-full max-w-3xl'>
|
<Tabs defaultValue={allVacancies} className='mx-auto w-full max-w-3xl'>
|
||||||
<TabsList className='mb-8 grid grid-cols-3'>
|
<TabsList className='mb-8 grid grid-cols-3'>
|
||||||
<TabsTrigger value='all'>{t('home.vacancies.all')}</TabsTrigger>
|
<TabsTrigger value={allVacancies}>
|
||||||
<TabsTrigger value='office'>{t('home.vacancies.office')}</TabsTrigger>
|
{t('home.vacancies.all')}
|
||||||
<TabsTrigger value='stations'>{t('home.vacancies.stations')}</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value={officeVacancies}>
|
||||||
|
{t('home.vacancies.office')}
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value={stationsVacancies}>
|
||||||
|
{t('home.vacancies.stations')}
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value='all' className='space-y-4'>
|
|
||||||
{[
|
<TabsContent value={allVacancies} className='space-y-4'>
|
||||||
'Оператор АЗС',
|
{jobs.map((job, index) => (
|
||||||
'Менеджер по продажам',
|
|
||||||
'Бухгалтер',
|
|
||||||
'Специалист по логистике',
|
|
||||||
].map((job, index) => (
|
|
||||||
<Vacancy
|
<Vacancy
|
||||||
key={index}
|
key={index}
|
||||||
jobTitle={job}
|
jobTitle={job.name}
|
||||||
location='Душанбе, Таджикистан'
|
location={job.location ?? ''}
|
||||||
tags={['Полный день', 'Опыт от 1 года']}
|
tags={job.tags}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value='office' className='space-y-4'>
|
|
||||||
{[
|
{Array.from(jobsByType.entries()).map(([type, jobs]) => (
|
||||||
'Менеджер по продажам',
|
<TabsContent key={type} value={type} className='space-y-4'>
|
||||||
'Бухгалтер',
|
{jobs.map((job: Jobs[number], index: number) => (
|
||||||
'Специалист по логистике',
|
|
||||||
].map((job, index) => (
|
|
||||||
<Vacancy
|
<Vacancy
|
||||||
key={index}
|
key={index}
|
||||||
jobTitle={job}
|
jobTitle={job.name}
|
||||||
location='Душанбе, Таджикистан'
|
location={job.location ?? ''}
|
||||||
tags={['Полный день', 'Опыт от 1 года']}
|
tags={job.tags}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value='stations' className='space-y-4'>
|
))}
|
||||||
{['Оператор АЗС', 'Заправщик', 'Менеджер станции'].map(
|
|
||||||
(job, index) => (
|
|
||||||
<Vacancy
|
|
||||||
key={index}
|
|
||||||
jobTitle={job}
|
|
||||||
location='Душанбе, Таджикистан'
|
|
||||||
tags={['Сменный график', 'Обучение']}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -92,7 +100,7 @@ interface VacancyProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Vacancy = ({ jobTitle, location, tags }: VacancyProps) => {
|
const Vacancy = ({ jobTitle, location, tags }: VacancyProps) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
|
"types": ["node"],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "next"
|
"name": "next"
|
||||||
@ -28,5 +29,5 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"],
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user