From 33c93684725463f5b13fdc95c1bef1abe286b51d Mon Sep 17 00:00:00 2001 From: Umar Adilov <99314948+adilovcode@users.noreply.github.com> Date: Wed, 30 Apr 2025 20:28:35 +0500 Subject: [PATCH 01/12] Refactoring auth --- package.json | 1 + pnpm-lock.yaml | 74 +++++++++++++++++++ .../api-utlities/utilities/oriyo.client.ts | 10 +++ src/app/api/auth/login/route.ts | 49 +++++------- 4 files changed, 102 insertions(+), 32 deletions(-) create mode 100644 src/app/api-utlities/utilities/oriyo.client.ts diff --git a/package.json b/package.json index a04c324..04d3507 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@radix-ui/react-toast": "^1.2.11", "@reduxjs/toolkit": "^2.7.0", "aos": "^2.3.4", + "axios": "^1.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a54e8a0..43ec80f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: aos: specifier: ^2.3.4 version: 2.3.4 + axios: + specifier: ^1.9.0 + version: 1.9.0 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -1306,6 +1309,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1314,6 +1320,9 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} + axios@1.9.0: + resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -1385,6 +1394,10 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} 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: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1441,6 +1454,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} @@ -1711,10 +1728,23 @@ packages: flatted@3.3.3: 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: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} 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: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -2090,6 +2120,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 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: resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} engines: {node: 20 || >=22} @@ -2307,6 +2345,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3792,12 +3833,22 @@ snapshots: async-function@1.0.0: {} + asynckit@0.4.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 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: {} balanced-match@1.0.2: {} @@ -3873,6 +3924,10 @@ snapshots: color-string: 1.9.1 optional: true + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + concat-map@0.0.1: {} cross-spawn@7.0.6: @@ -3927,6 +3982,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delayed-stream@1.0.0: {} + detect-libc@2.0.3: {} detect-node-es@1.1.0: {} @@ -4352,10 +4409,19 @@ snapshots: flatted@3.3.3: {} + follow-redirects@1.15.9: {} + for-each@0.3.5: dependencies: 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.prototype.name@1.1.8: @@ -4708,6 +4774,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minimatch@10.0.1: dependencies: brace-expansion: 2.0.1 @@ -4877,6 +4949,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proxy-from-env@1.1.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} diff --git a/src/app/api-utlities/utilities/oriyo.client.ts b/src/app/api-utlities/utilities/oriyo.client.ts new file mode 100644 index 0000000..291fa2c --- /dev/null +++ b/src/app/api-utlities/utilities/oriyo.client.ts @@ -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; diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index 774e8e7..df541d9 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -1,43 +1,28 @@ import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; -import { LoginData } from '@/entities/auth/model/types'; +import oriyoClient from '@/app/api-utlities/utilities/oriyo.client'; -export const GET = async (req: NextRequest) => { - if (req.method !== 'GET') { - return NextResponse.json( - { error: 'Method is not supported' }, - { status: 405 }, - ); - } - - const { searchParams } = req.nextUrl; - - const phoneNumber = searchParams.get('phoneNumber'); - const cardNumber = searchParams.get('cardNumber'); - const type = searchParams.get('type'); - - if (!phoneNumber || !cardNumber || !type) { - return NextResponse.json({ error: 'Bad request' }, { status: 400 }); - } +export const POST = async (req: NextRequest) => { + const validatedBody = z + .object({ + phoneNumber: z.string().min(9).max(9), + cardNumber: z.string().nonempty(), + type: z.enum(['corporate', 'bonus']), + }) + .parse(req.body); try { - const loginRes = await fetch( - `https://test.oriyo.tj/api/client/login?type=${type}&phone=${phoneNumber}&uid=${cardNumber}`, - { - method: 'GET', + const oriyoResponse = await oriyoClient.get('/client/login', { + params: { + type: validatedBody.type, + phone: validatedBody.phoneNumber, + uid: validatedBody.cardNumber, }, - ); + }); - if (!loginRes.ok) { - return NextResponse.json( - { error: 'Error during login' }, - { status: 400 }, - ); - } + const { token } = oriyoResponse.data; - const data = (await loginRes.json()) as LoginData; - - const token = data.token; if (!token) { return NextResponse.json({ error: 'No auth token' }, { status: 401 }); } From efae331aaf7bf51533506387fba15698472ab8e3 Mon Sep 17 00:00:00 2001 From: Umar Adilov <99314948+adilovcode@users.noreply.github.com> Date: Thu, 1 May 2025 01:16:11 +0500 Subject: [PATCH 02/12] Added authentication support --- package.json | 1 + pnpm-lock.yaml | 21 +++++++ src/app/api/auth/login/route.ts | 28 +++++----- .../middlewares/error-handler.middleware.ts | 18 ++++++ src/app/api/pages/main/route.ts | 8 ++- src/entities/auth/api/login.api.ts | 25 ++++----- .../auth/model/contracts/login.contract.ts | 2 +- .../model/validation}/login-form.schema.ts | 0 .../auth/login-form/ui/login-form.tsx | 19 +++---- src/pages-templates/login/index.tsx | 56 ++++++++++++++++++- src/shared/api/base-api.ts | 21 ++++++- src/shared/store/root-reducer.ts | 3 - 12 files changed, 158 insertions(+), 44 deletions(-) create mode 100644 src/app/api/middlewares/error-handler.middleware.ts rename src/{features/auth/login-form/model => entities/auth/model/validation}/login-form.schema.ts (100%) diff --git a/package.json b/package.json index 04d3507..6bedcc6 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "axios": "^1.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cookies-next": "^5.1.0", "date-fns": "^4.1.0", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43ec80f..531d1c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: clsx: specifier: ^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: specifier: ^4.1.0 version: 4.1.0 @@ -1401,6 +1404,16 @@ packages: concat-map@0.0.1: 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: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -3930,6 +3943,14 @@ snapshots: 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: dependencies: path-key: 3.1.1 diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index df541d9..a51a83c 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -3,14 +3,16 @@ import { z } from 'zod'; import oriyoClient from '@/app/api-utlities/utilities/oriyo.client'; -export const POST = async (req: NextRequest) => { - const validatedBody = z - .object({ - phoneNumber: z.string().min(9).max(9), - cardNumber: z.string().nonempty(), - type: z.enum(['corporate', 'bonus']), - }) - .parse(req.body); +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', { @@ -21,19 +23,17 @@ export const POST = async (req: NextRequest) => { }, }); - const { token } = oriyoResponse.data; + const { token } = JSON.parse(oriyoResponse.data); if (!token) { - return NextResponse.json({ error: 'No auth token' }, { status: 401 }); + return NextResponse.json({ error: 'Credentials error' }, { status: 401 }); } const response = NextResponse.json({ success: true }); - response.cookies.set('token', token, { - httpOnly: true, + response.cookies.set(`${validatedBody.type}__token`, token, { path: '/', maxAge: 2 * 60 * 60, - secure: process.env.NODE_ENV === 'production', }); return response; @@ -42,3 +42,5 @@ export const POST = async (req: NextRequest) => { return NextResponse.json({ error: 'Server error' }, { status: 500 }); } }; + +export const POST = validationErrorHandler(routeHandler); diff --git a/src/app/api/middlewares/error-handler.middleware.ts b/src/app/api/middlewares/error-handler.middleware.ts new file mode 100644 index 0000000..a3046e8 --- /dev/null +++ b/src/app/api/middlewares/error-handler.middleware.ts @@ -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 }, + ); + } + }; diff --git a/src/app/api/pages/main/route.ts b/src/app/api/pages/main/route.ts index f17286f..deb8f1b 100644 --- a/src/app/api/pages/main/route.ts +++ b/src/app/api/pages/main/route.ts @@ -7,7 +7,9 @@ import { import { mainPageRequest } from '@/app/api-utlities/requests/main-page.request'; import { requestTaylor } from '@/app/api-utlities/utilities/taylor.client'; -export async function GET(request: Request) { +import { validationErrorHandler } from '../../middlewares/error-handler.middleware'; + +const routeHandler = async (request: Request) => { const response = await requestTaylor(mainPageRequest); return new Response( @@ -21,4 +23,6 @@ export async function GET(request: Request) { headers: { 'Content-Type': 'application/json' }, }, ); -} +}; + +export const GET = validationErrorHandler(routeHandler); diff --git a/src/entities/auth/api/login.api.ts b/src/entities/auth/api/login.api.ts index 99ad4ed..5760186 100644 --- a/src/entities/auth/api/login.api.ts +++ b/src/entities/auth/api/login.api.ts @@ -1,24 +1,23 @@ -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import { baseAPI } from '@/shared/api/base-api'; import { LoginParams, LoginResponse } from '../model/contracts/login.contract'; -export const loginAPI = createApi({ - baseQuery: fetchBaseQuery({ baseUrl: '/api' }), - endpoints: (build) => ({ - login: build.query({ +export const authenticationApi = baseAPI.injectEndpoints({ + endpoints: (builder) => ({ + login: builder.query({ query: (data) => { - const params = new URLSearchParams({ - type: data.type, - phoneNumber: data.phoneNumber, - cardNumber: data.cardNumber, - }).toString(); - return { - url: `/auth/login?${params}`, + method: 'POST', + body: { + type: data.type, + phoneNumber: data.phoneNumber, + cardNumber: data.cardNumber, + }, + url: '/auth/login', }; }, }), }), }); -export const { useLazyLoginQuery } = loginAPI; +export const { useLazyLoginQuery } = authenticationApi; diff --git a/src/entities/auth/model/contracts/login.contract.ts b/src/entities/auth/model/contracts/login.contract.ts index cb3a3da..b315099 100644 --- a/src/entities/auth/model/contracts/login.contract.ts +++ b/src/entities/auth/model/contracts/login.contract.ts @@ -1,4 +1,4 @@ -import { LoginFormData } from '@/features/auth/login-form/model/login-form.schema'; +import { LoginFormData } from '@/entities/auth/model/validation/login-form.schema'; export interface LoginResponse { success: boolean; diff --git a/src/features/auth/login-form/model/login-form.schema.ts b/src/entities/auth/model/validation/login-form.schema.ts similarity index 100% rename from src/features/auth/login-form/model/login-form.schema.ts rename to src/entities/auth/model/validation/login-form.schema.ts diff --git a/src/features/auth/login-form/ui/login-form.tsx b/src/features/auth/login-form/ui/login-form.tsx index 30d7044..1af10a3 100644 --- a/src/features/auth/login-form/ui/login-form.tsx +++ b/src/features/auth/login-form/ui/login-form.tsx @@ -19,7 +19,10 @@ import { } from '@/shared/shadcn-ui/form'; import { Input } from '@/shared/shadcn-ui/input'; -import { LoginFormData, loginFormSchema } from '../model/login-form.schema'; +import { + LoginFormData, + loginFormSchema, +} from '../../../../entities/auth/model/validation/login-form.schema'; interface LoginFormProps { type: 'bonus' | 'corporate'; @@ -40,16 +43,12 @@ export const LoginForm = ({ type }: LoginFormProps) => { }); const onSubmit = async (data: LoginFormData) => { - try { - await login({ ...data, type }).unwrap(); + await login({ ...data, type }).unwrap(); - toast.success('Logged in successfully!'); - router.push( - type === 'bonus' ? '/customer-dashboard' : '/corporate-dashboard', - ); - } catch (error) { - toast.error('An error occured during login'); - } + toast.success('Logged in successfully!'); + router.push( + type === 'bonus' ? '/customer-dashboard' : '/corporate-dashboard', + ); }; return ( diff --git a/src/pages-templates/login/index.tsx b/src/pages-templates/login/index.tsx index 23046b4..c248838 100644 --- a/src/pages-templates/login/index.tsx +++ b/src/pages-templates/login/index.tsx @@ -1,11 +1,14 @@ 'use client'; +import { deleteCookie, getCookie } from 'cookies-next'; import { Building2, Fuel, User } from 'lucide-react'; import Link from 'next/link'; +import { useRouter, useSearchParams } from 'next/navigation'; import { LoginForm } from '@/features/auth/login-form'; import { useTextController } from '@/shared/language/hooks/use-text-controller'; +import { Button } from '@/shared/shadcn-ui/button'; import { Card, CardContent, @@ -39,6 +42,13 @@ const tabs = [ export default function LoginPage() { 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 (
@@ -55,7 +65,12 @@ export default function LoginPage() {
- + {tabs.map((tab) => { return ( @@ -71,6 +86,45 @@ export default function LoginPage() { {tabs.map((tab) => { + const tabCookieName = `${tab.type}__token`; + + const authenticationCookie = getCookie(tabCookieName); + + if (authenticationCookie) { + return ( + + + + {t(tab.title)} + + + + + + + + + + ); + } + return ( diff --git a/src/shared/api/base-api.ts b/src/shared/api/base-api.ts index 5ecaa52..71616da 100644 --- a/src/shared/api/base-api.ts +++ b/src/shared/api/base-api.ts @@ -1,4 +1,5 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import { toast } from 'sonner'; const baseQuery = fetchBaseQuery({ baseUrl: process.env.NEXT_PUBLIC_API_URL, @@ -15,7 +16,25 @@ export const TAGS = { export const baseAPI = createApi({ 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), endpoints: () => ({}), }); diff --git a/src/shared/store/root-reducer.ts b/src/shared/store/root-reducer.ts index 56b999c..1b1a8cd 100644 --- a/src/shared/store/root-reducer.ts +++ b/src/shared/store/root-reducer.ts @@ -1,10 +1,7 @@ import { combineReducers } from '@reduxjs/toolkit'; -import { loginAPI } from '@/entities/auth/api/login.api'; - import { baseAPI } from '@/shared/api/base-api'; export const rootReducer = combineReducers({ [baseAPI.reducerPath]: baseAPI.reducer, - [loginAPI.reducerPath]: loginAPI.reducer, }); From 3142023c7938a06276df488622840fafe894624a Mon Sep 17 00:00:00 2001 From: Umar Adilov <99314948+adilovcode@users.noreply.github.com> Date: Thu, 1 May 2025 01:45:12 +0500 Subject: [PATCH 03/12] Loading bonus clients from API --- src/app/api/auth/login/route.ts | 14 +- src/app/api/bonus/info/route.ts | 31 ++++ src/entities/bonus/api/bonus.api.ts | 17 +++ .../model/types/bonus-client-info.type.ts | 8 + .../(dashboard)/customer-dashboard/index.tsx | 137 +++++++++--------- 5 files changed, 133 insertions(+), 74 deletions(-) create mode 100644 src/app/api/bonus/info/route.ts create mode 100644 src/entities/bonus/api/bonus.api.ts create mode 100644 src/entities/bonus/model/types/bonus-client-info.type.ts diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index a51a83c..7dbe591 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -23,7 +23,7 @@ const routeHandler = async (req: NextRequest) => { }, }); - const { token } = JSON.parse(oriyoResponse.data); + const { token, card_id } = JSON.parse(oriyoResponse.data); if (!token) { return NextResponse.json({ error: 'Credentials error' }, { status: 401 }); @@ -31,10 +31,14 @@ const routeHandler = async (req: NextRequest) => { const response = NextResponse.json({ success: true }); - response.cookies.set(`${validatedBody.type}__token`, token, { - path: '/', - maxAge: 2 * 60 * 60, - }); + response.cookies.set( + `${validatedBody.type}__token`, + JSON.stringify({ token, card_id }), + { + path: '/', + maxAge: 2 * 60 * 60, + }, + ); return response; } catch (error) { diff --git a/src/app/api/bonus/info/route.ts b/src/app/api/bonus/info/route.ts new file mode 100644 index 0000000..efb8011 --- /dev/null +++ b/src/app/api/bonus/info/route.ts @@ -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); diff --git a/src/entities/bonus/api/bonus.api.ts b/src/entities/bonus/api/bonus.api.ts new file mode 100644 index 0000000..b8d31bf --- /dev/null +++ b/src/entities/bonus/api/bonus.api.ts @@ -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({ + query: () => { + return { + url: '/bonus/info', + }; + }, + }), + }), +}); + +export const { useFetchMyBonusInfoQuery } = bonusApi; diff --git a/src/entities/bonus/model/types/bonus-client-info.type.ts b/src/entities/bonus/model/types/bonus-client-info.type.ts new file mode 100644 index 0000000..dd7138f --- /dev/null +++ b/src/entities/bonus/model/types/bonus-client-info.type.ts @@ -0,0 +1,8 @@ +export interface ClientInfo { + card_id: number; + fullname: string; + cardno: string; + reg_date: string; + end_date: string; + bonuses: string; +} diff --git a/src/pages-templates/(dashboard)/customer-dashboard/index.tsx b/src/pages-templates/(dashboard)/customer-dashboard/index.tsx index d4181a1..2e0a319 100644 --- a/src/pages-templates/(dashboard)/customer-dashboard/index.tsx +++ b/src/pages-templates/(dashboard)/customer-dashboard/index.tsx @@ -2,6 +2,8 @@ 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 { @@ -14,20 +16,11 @@ import { 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 function CustomerDashboard() { const { t } = useTextController(); + const { data, isLoading } = useFetchMyBonusInfoQuery({}); + return (
@@ -41,35 +34,44 @@ export function CustomerDashboard() {
- {/* Bonus Card */} - - - - {t('customer.bonusCard.title')} - - - {t('customer.bonusCard.description')} - - - -
-

- {customerData.bonusPoints} -

-

- {t('customer.bonusCard.points')} -

-
-
-
- - {t('customer.bonusCard.validUntil')} -
- -
-
+ {!data || isLoading ? ( + // TODO: Bunyod please add loader here + <>Loader here + ) : ( + <> + + + + {t('customer.bonusCard.title')} + + + {t('customer.bonusCard.description')} + + + +
+

{data.bonuses}

+

+ {t('customer.bonusCard.points')} +

+
+
+
+ + + {t('customer.bonusCard.validUntil')}{' '} + {new Date(data.end_date).toLocaleDateString('en-GB')} + +
+ +
+
+ + )}
+ + {/* Bonus Card */} {/* Customer Card */} @@ -79,40 +81,37 @@ export function CustomerDashboard() { -
-
-
-

- {t('customer.infoCard.regDateLabel')} -

-

- {customerData.firstName} {customerData.lastName} -

+ {!data || isLoading ? ( + // TODO: Bunyod please add loader here + <>Loader here + ) : ( +
+
+
+

+ {t('customer.infoCard.regDateLabel')} +

+

{data.fullname}

+
+
+

+ {t('customer.infoCard.regDateLabel')} +

+

+ {new Date(data.reg_date).toLocaleDateString('en-GB')} +

+
-
-

- {t('customer.infoCard.regDateLabel')} -

-

- {customerData.registrationDate} -

+
+
+

+ {t('customer.infoCard.cardNumberLabel')} +

+

{data.cardno}

+
-
-
-

- {t('customer.infoCard.cardNumberLabel')} -

-

{customerData.cardNumber}

-
-
-

- {t('customer.infoCard.expiryDateLabel')} -

-

{customerData.expiryDate}

-
-
-
+ )}
From e98c079ba674147a0d8d6d955bce7478936bd829 Mon Sep 17 00:00:00 2001 From: khadiatullo Date: Thu, 1 May 2025 00:58:42 +0300 Subject: [PATCH 04/12] change: change the text --- src/pages-templates/clients/loyalty/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages-templates/clients/loyalty/index.tsx b/src/pages-templates/clients/loyalty/index.tsx index b47394f..24140d5 100644 --- a/src/pages-templates/clients/loyalty/index.tsx +++ b/src/pages-templates/clients/loyalty/index.tsx @@ -245,7 +245,7 @@ export function LoyaltyPage() {
  • - {t('clients.loyalty.works.levels.card-1.bonus-1')} + {t('clients.loyalty.works.levels.card-2.bonus-1')}
  • @@ -257,13 +257,13 @@ export function LoyaltyPage() {
  • - {t('clients.loyalty.works.levels.card-3.bonus-3')} + {t('clients.loyalty.works.levels.card-2.bonus-3')}
  • - {t('clients.loyalty.works.levels.card-4.bonus-4')} + {t('clients.loyalty.works.levels.card-2.bonus-4')}
  • From cad373568e7ce6370a0185f59159a6cdc9871f96 Mon Sep 17 00:00:00 2001 From: khadiatullo Date: Thu, 1 May 2025 05:14:46 +0300 Subject: [PATCH 05/12] update: added adaptability for pages --- src/app/layout.tsx | 2 +- .../(dashboard)/corporate-dashboard/index.tsx | 2 +- .../(dashboard)/customer-dashboard/index.tsx | 2 +- src/pages-templates/about/index.tsx | 403 +++++++------ src/pages-templates/charity/index.tsx | 361 ++++++------ .../clients/certificates/index.tsx | 5 +- src/pages-templates/clients/index.tsx | 21 +- src/pages-templates/clients/loyalty/index.tsx | 550 +++++++++--------- src/pages-templates/login/index.tsx | 169 +++--- src/shared/shadcn-ui/conteiner.tsx | 10 + src/widgets/about-page/station-gallery.tsx | 2 +- src/widgets/clients/ui/benefits-section.tsx | 75 +-- .../clients/ui/services-overview-section.tsx | 67 ++- src/widgets/footer.tsx | 212 +++---- 14 files changed, 972 insertions(+), 909 deletions(-) create mode 100644 src/shared/shadcn-ui/conteiner.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7766960..6406af6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -37,7 +37,7 @@ export default async function RootLayout({ className='scroll-smooth' style={{ scrollBehavior: 'smooth' }} > - +
    {children} diff --git a/src/pages-templates/(dashboard)/corporate-dashboard/index.tsx b/src/pages-templates/(dashboard)/corporate-dashboard/index.tsx index 9e54f4a..a99a4e3 100644 --- a/src/pages-templates/(dashboard)/corporate-dashboard/index.tsx +++ b/src/pages-templates/(dashboard)/corporate-dashboard/index.tsx @@ -97,7 +97,7 @@ export function CorporateDashboard() { const { t } = useTextController(); return ( -
    +
    diff --git a/src/pages-templates/(dashboard)/customer-dashboard/index.tsx b/src/pages-templates/(dashboard)/customer-dashboard/index.tsx index 2e0a319..8a5aca8 100644 --- a/src/pages-templates/(dashboard)/customer-dashboard/index.tsx +++ b/src/pages-templates/(dashboard)/customer-dashboard/index.tsx @@ -22,7 +22,7 @@ export function CustomerDashboard() { const { data, isLoading } = useFetchMyBonusInfoQuery({}); return ( -
    +
    diff --git a/src/pages-templates/about/index.tsx b/src/pages-templates/about/index.tsx index 52cdf82..11366c0 100644 --- a/src/pages-templates/about/index.tsx +++ b/src/pages-templates/about/index.tsx @@ -13,6 +13,7 @@ import { Card, CardContent } from '@/shared/shadcn-ui/card'; import { CompanyTimeline } from '@/widgets/about-page/company-timeline'; import { StationGallery } from '@/widgets/about-page/station-gallery'; import { CtaSection } from '@/widgets/cta-section'; +import Container from '@/shared/shadcn-ui/conteiner'; export const metadata = { title: 'about.metadata.title', @@ -36,7 +37,7 @@ export default function AboutPage() { className='object-cover' priority /> -
    +

    @@ -52,55 +53,57 @@ export default function AboutPage() { {/* Company Overview */} -
    -
    -
    -
    -
    - -
    -

    - {t('about.overview.title')} -

    -

    - {t('about.overview.description1')} -

    -

    - {t('about.overview.description2')} -

    -

    - {t('about.overview.description3')} -

    + +
    +
    +
    +
    +
    + +
    +

    + {t('about.overview.title')} +

    +

    + {t('about.overview.description1')} +

    +

    + {t('about.overview.description2')} +

    +

    + {t('about.overview.description3')} +

    -
    - {[0, 1, 2, 3].map((index) => ( -
    -
    - +
    + {[0, 1, 2, 3].map((index) => ( +
    +
    + +
    +
    +

    + {t(`about.overview.benefits.${index}.title`)} +

    +

    + {t(`about.overview.benefits.${index}.description`)} +

    +
    -
    -

    - {t(`about.overview.benefits.${index}.title`)} -

    -

    - {t(`about.overview.benefits.${index}.description`)} -

    -
    -
    - ))} + ))} +
    +
    +
    + {t('about.overview.imageAlt')}
    -
    -
    - {t('about.overview.imageAlt')}
    -
    -
    +
    + {/* Stats Section */}
    @@ -113,7 +116,7 @@ export default function AboutPage() { {t('about.stats.subtitle')}

    -
    +
    {[0, 1, 2, 3].map((index) => (

    @@ -139,176 +142,190 @@ export default function AboutPage() { {/* Our History */}
    -
    -
    - + +
    +
    + +
    +

    + {t('about.history.title')} +

    +

    + {t('about.history.subtitle')} +

    -

    - {t('about.history.title')} -

    -

    - {t('about.history.subtitle')} -

    -
    + + + + + -
    {/* Our Stations */} -
    -
    -
    -
    - + +
    +
    +
    +
    + +
    +

    + {t('about.stations.title')} +

    +

    + {t('about.stations.subtitle')} +

    -

    - {t('about.stations.title')} -

    -

    - {t('about.stations.subtitle')} -

    -
    - + -
    -

    - {t('about.stations.description')} -

    - +
    +

    + {t('about.stations.description')} +

    + +
    -
    -
    + + {/* Our Values */} -
    -
    -
    -
    - + +
    +
    +
    +
    + +
    +

    + {t('about.values.title')} +

    +

    + {t('about.values.subtitle')} +

    -

    - {t('about.values.title')} -

    -

    - {t('about.values.subtitle')} -

    -
    -
    - {[0, 1, 2].map((index) => ( - - -
    - -
    -

    - {t(`about.values.items.${index}.title`)} -

    -

    - {t(`about.values.items.${index}.description`)} -

    -
    -
    - ))} +
    + {[0, 1, 2].map((index) => ( + + +
    + +
    +

    + {t(`about.values.items.${index}.title`)} +

    +

    + {t(`about.values.items.${index}.description`)} +

    +
    +
    + ))} +
    -
    -
    + + {/* Our Team */} -
    -
    -
    -
    - -
    -

    - {t('about.team.title')} -

    -

    - {t('about.team.subtitle')} -

    -
    - -
    - {[0, 1, 2, 3].map((index) => ( -
    -
    - {t(`about.team.members.${index}.name`)} -
    -
    -

    - {t(`about.team.members.${index}.name`)} -

    -

    - {t(`about.team.members.${index}.position`)} -

    -
    + +
    +
    +
    +
    +
    - ))} +

    + {t('about.team.title')} +

    +

    + {t('about.team.subtitle')} +

    +
    + +
    + {[0, 1, 2, 3].map((index) => ( +
    +
    + {t(`about.team.members.${index}.name`)} +
    +
    +

    + {t(`about.team.members.${index}.name`)} +

    +

    + {t(`about.team.members.${index}.position`)} +

    +
    +
    + ))} +
    -
    -
    + + {/* Testimonials */} -
    -
    -
    -
    - -
    -

    - {t('about.testimonials.title')} -

    -

    - {t('about.testimonials.subtitle')} -

    -
    + -
    - {[0, 1, 2].map((index) => ( - - -
    - {Array(5) - .fill(0) - .map((_, i) => ( - - ))} -
    -

    - "{t(`about.testimonials.items.${index}.text`)}" -

    -

    - {t(`about.testimonials.items.${index}.name`)} -

    -
    -
    - ))} +
    +
    +
    +
    + +
    +

    + {t('about.testimonials.title')} +

    +

    + {t('about.testimonials.subtitle')} +

    +
    + +
    + {[0, 1, 2].map((index) => ( + + +
    + {Array(5) + .fill(0) + .map((_, i) => ( + + ))} +
    +

    + "{t(`about.testimonials.items.${index}.text`)}" +

    +

    + {t(`about.testimonials.items.${index}.name`)} +

    +
    +
    + ))} +
    -
    -
    + +

    diff --git a/src/pages-templates/charity/index.tsx b/src/pages-templates/charity/index.tsx index 43abbd3..c33997e 100644 --- a/src/pages-templates/charity/index.tsx +++ b/src/pages-templates/charity/index.tsx @@ -21,6 +21,7 @@ import { } from '@/shared/shadcn-ui/card'; import { CtaSection } from '@/widgets/cta-section'; +import Container from '@/shared/shadcn-ui/conteiner'; export const metadata = { title: 'Благотворительность | GasNetwork - Сеть заправок в Таджикистане', @@ -46,68 +47,72 @@ export function CharityPage() { priority />
    -
    -
    -
    - + +
    +
    +
    + +
    +

    + {t('charity.hero.title')} +

    +

    + {t('charity.hero.subtitle')} +

    -

    - {t('charity.hero.title')} -

    -

    - {t('charity.hero.subtitle')} -

    -
    +
    {/* Mission Section */} -
    -
    -
    -
    -
    - -
    -

    - {t('charity.mission.title')} -

    -

    - {t('charity.mission.description1')} -

    -

    - {t('charity.mission.description2')} -

    + +
    +
    +
    +
    +
    + +
    +

    + {t('charity.mission.title')} +

    +

    + {t('charity.mission.description1')} +

    +

    + {t('charity.mission.description2')} +

    -
    - {[0, 1, 2].map((index) => ( -
    - -
    -

    - {t(`charity.mission.principles.${index}.title`)} -

    -

    - {t(`charity.mission.principles.${index}.description`)} -

    +
    + {[0, 1, 2].map((index) => ( +
    + +
    +

    + {t(`charity.mission.principles.${index}.title`)} +

    +

    + {t(`charity.mission.principles.${index}.description`)} +

    +
    -
    - ))} + ))} +
    +
    +
    + {t('charity.mission.imageAlt')}
    -
    -
    - {t('charity.mission.imageAlt')}
    -
    -
    +
    + {/* Key Figures */}
    @@ -136,140 +141,146 @@ export function CharityPage() {
    {/* Upcoming Events */} -
    -
    -
    -
    - + +
    +
    +
    +
    + +
    +

    + {t('charity.events.title')} +

    +

    + {t('charity.events.subtitle')} +

    -

    - {t('charity.events.title')} -

    -

    - {t('charity.events.subtitle')} -

    -
    -
    - {[ - { - 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) => ( - -
    - {event.title} -
    - - {event.title} - - -

    {event.description}

    -
    - - {event.date} +
    + {[ + { + 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) => ( + +
    +
    + {event.title} +
    + + {event.title} + + +

    {event.description}

    +
    + + {event.date} +
    +
    + + {event.location} +
    +
    -
    - - {event.location} -
    - - - - -
    - ))} + + + + + ))} +
    -
    -
    +
    + {/* How to Help */} -
    -
    -
    -
    - + +
    +
    +
    +
    + +
    +

    + {t('charity.help.title')} +

    +

    + {t('charity.help.subtitle')} +

    -

    - {t('charity.help.title')} -

    -

    - {t('charity.help.subtitle')} -

    -
    -
    - {[ - { - title: 'Сделать пожертвование', - description: - 'Ваше пожертвование поможет нам реализовать больше проектов и помочь большему количеству людей.', - icon: , - }, - { - title: 'Стать волонтером', - description: - 'Присоединяйтесь к нашей команде волонтеров и помогайте нам в реализации благотворительных проектов.', - icon: , - }, - { - title: 'Участвовать в мероприятиях', - description: - 'Принимайте участие в наших благотворительных мероприятиях и акциях.', - icon: , - }, - { - title: 'Распространять информацию', - description: - 'Расскажите о нашем фонде и его деятельности своим друзьям и знакомым.', - icon: , - }, - ].map((item, index) => ( - - -
    {item.icon}
    - - {item.title} - -
    - -

    {item.description}

    -
    -
    - ))} +
    + {[ + { + title: 'Сделать пожертвование', + description: + 'Ваше пожертвование поможет нам реализовать больше проектов и помочь большему количеству людей.', + icon: , + }, + { + title: 'Стать волонтером', + description: + 'Присоединяйтесь к нашей команде волонтеров и помогайте нам в реализации благотворительных проектов.', + icon: , + }, + { + title: 'Участвовать в мероприятиях', + description: + 'Принимайте участие в наших благотворительных мероприятиях и акциях.', + icon: , + }, + { + title: 'Распространять информацию', + description: + 'Расскажите о нашем фонде и его деятельности своим друзьям и знакомым.', + icon: , + }, + ].map((item, index) => ( + + +
    {item.icon}
    + + {item.title} + +
    + +

    {item.description}

    +
    +
    + ))} +
    -
    -
    + +
    diff --git a/src/pages-templates/clients/certificates/index.tsx b/src/pages-templates/clients/certificates/index.tsx index d6db9da..0d20136 100644 --- a/src/pages-templates/clients/certificates/index.tsx +++ b/src/pages-templates/clients/certificates/index.tsx @@ -6,6 +6,7 @@ import Image from 'next/image'; import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { Button } from '@/shared/shadcn-ui/button'; import { Card, CardContent } from '@/shared/shadcn-ui/card'; +import Container from '@/shared/shadcn-ui/conteiner'; export function CertificatesPage() { const { t } = useTextController(); @@ -64,7 +65,7 @@ export function CertificatesPage() { ]; return ( - <> +

    {t('certificates.title')}

    @@ -122,6 +123,6 @@ export function CertificatesPage() { ))}
    - +
    ); } diff --git a/src/pages-templates/clients/index.tsx b/src/pages-templates/clients/index.tsx index 2235798..c63d125 100644 --- a/src/pages-templates/clients/index.tsx +++ b/src/pages-templates/clients/index.tsx @@ -7,6 +7,7 @@ import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { BenefitsSection } from '@/widgets/clients/ui/benefits-section'; import { ServicesOverviewSection } from '@/widgets/clients/ui/services-overview-section'; import { CtaSection } from '@/widgets/cta-section'; +import Container from '@/shared/shadcn-ui/conteiner'; export const metadata = { title: 'Клиентам | GasNetwork - Сеть заправок в Таджикистане', @@ -32,16 +33,18 @@ export function ClientsPage() { priority />
    -
    -
    -

    - {t('clients.title')} -

    -

    - {t('clients.description')} -

    + +
    +
    +

    + {t('clients.title')} +

    +

    + {t('clients.description')} +

    +
    -
    +
    diff --git a/src/pages-templates/clients/loyalty/index.tsx b/src/pages-templates/clients/loyalty/index.tsx index b47394f..141fd5a 100644 --- a/src/pages-templates/clients/loyalty/index.tsx +++ b/src/pages-templates/clients/loyalty/index.tsx @@ -7,6 +7,7 @@ import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { Card, CardContent } from '@/shared/shadcn-ui/card'; import { CtaSection } from '@/widgets/cta-section'; +import Container from '@/shared/shadcn-ui/conteiner'; export const metadata = { title: 'Программа лояльности | GasNetwork - Сеть заправок в Таджикистане', @@ -32,294 +33,299 @@ export function LoyaltyPage() { priority />
    -
    -
    -

    - {t('clients.loyalty.title')} -

    -

    - {t('clients.loyalty.description')} + +

    +
    +

    + {t('clients.loyalty.title')} +

    +

    + {t('clients.loyalty.description')} +

    +
    +
    + +
    +
    + + + + {/* Program Overview */} +
    +
    +
    +
    +
    + +
    +

    + {t('clients.loyalty.programm.about')} +

    +

    + {t('clients.loyalty.programm.about-description')} +

    +

    + {t('clients.loyalty.programm.about-description-2')} +

    + +
    +
    +
    + +
    +
    +

    + {t('clients.loyalty.programm.conditions-1')} +

    +

    + {t('clients.loyalty.programm.conditions.description-1')} +

    +
    +
    +
    +
    + +
    +
    +

    + {t('clients.loyalty.programm.conditions-2')} +

    +

    + {t('clients.loyalty.programm.conditions.description-2')} +

    +
    +
    +
    +
    + +
    +
    +

    + {t('clients.loyalty.programm.conditions-3')} +

    +

    + {t('clients.loyalty.programm.conditions.description-3')} +

    +
    +
    +
    +
    +
    + Программа лояльности +
    +
    +
    +
    + + {/* How It Works */} +
    +
    +
    +

    + {t('clients.loyalty.works.title')} +

    +

    + {t('clients.loyalty.works.description')} +

    +
    + +
    +
    +
    + 1 +
    +

    + {t('clients.loyalty.works.stage-1')} +

    +

    + {t('clients.loyalty.works.stage.description-1')} +

    +
    +
    +
    + 2 +
    +

    + {t('clients.loyalty.works.stage-2')} +

    +

    + {t('clients.loyalty.works.stage.description-2')} +

    +
    +
    +
    + 3 +
    +

    + {t('clients.loyalty.works.stage-3')} +

    +

    + {t('clients.loyalty.works.stage.description-3')} +

    +
    +
    +
    + 4 +
    +

    + {t('clients.loyalty.works.stage-4')} +

    +

    + {t('clients.loyalty.works.stage.description-4')}

    -
    - - - {/* Program Overview */} -
    -
    -
    -
    -
    - -
    -

    - {t('clients.loyalty.programm.about')} +

    + + {/* Loyalty Levels */} +
    +
    +
    +

    + {t('clients.loyalty.works.levels.title')}

    -

    - {t('clients.loyalty.programm.about-description')} -

    -

    - {t('clients.loyalty.programm.about-description-2')} +

    + {t('clients.loyalty.works.levels.description')}

    +
    -
    -
    -
    - -
    -
    -

    - {t('clients.loyalty.programm.conditions-1')} -

    -

    - {t('clients.loyalty.programm.conditions.description-1')} +

    + + +

    + {t('clients.loyalty.works.levels.card-1.title')} +

    +
    + + {t('clients.loyalty.works.levels.card-1.percent')} + +

    + {t('clients.loyalty.works.levels.card.mark')}

    -
    -
    -
    - -
    -
    -

    - {t('clients.loyalty.programm.conditions-2')} -

    -

    - {t('clients.loyalty.programm.conditions.description-2')} +

      +
    • + + + {t('clients.loyalty.works.levels.card-1.bonus-1')} + +
    • +
    • + + + {t('clients.loyalty.works.levels.card-1.bonus-2')} + +
    • +
    • + + + {t('clients.loyalty.works.levels.card-1.bonus-3')} + +
    • +
    + + + + + +

    + {t('clients.loyalty.works.levels.card-2.title')} +

    +
    + + {t('clients.loyalty.works.levels.card-2.percent')} + +

    + {t('clients.loyalty.works.levels.card.mark')}

    -
    -
    -
    - -
    -
    -

    - {t('clients.loyalty.programm.conditions-3')} -

    -

    - {t('clients.loyalty.programm.conditions.description-3')} +

      +
    • + + + {t('clients.loyalty.works.levels.card-1.bonus-1')} + +
    • +
    • + + + {t('clients.loyalty.works.levels.card-2.bonus-2')} + +
    • +
    • + + + {t('clients.loyalty.works.levels.card-3.bonus-3')} + +
    • +
    • + + + {t('clients.loyalty.works.levels.card-4.bonus-4')} + +
    • +
    + + + + + +

    + {t('clients.loyalty.works.levels.card-3.title')} +

    +
    + + {t('clients.loyalty.works.levels.card-3.percent')} + +

    + {t('clients.loyalty.works.levels.card.mark')}

    -
    -
    -
    -
    - Программа лояльности +
      +
    • + + + {t('clients.loyalty.works.levels.card-3.bonus-1')} + +
    • +
    • + + + {t('clients.loyalty.works.levels.card-3.bonus-2')} + +
    • +
    • + + + {t('clients.loyalty.works.levels.card-3.bonus-3')} + +
    • +
    • + + + {t('clients.loyalty.works.levels.card-3.bonus-4')} + +
    • +
    • + + + {t('clients.loyalty.works.levels.card-3.bonus-5')} + +
    • +
    + +
    -
    -
    + + - {/* How It Works */} -
    -
    -
    -

    - {t('clients.loyalty.works.title')} -

    -

    - {t('clients.loyalty.works.description')} -

    -
    - -
    -
    -
    - 1 -
    -

    - {t('clients.loyalty.works.stage-1')} -

    -

    - {t('clients.loyalty.works.stage.description-1')} -

    -
    -
    -
    - 2 -
    -

    - {t('clients.loyalty.works.stage-2')} -

    -

    - {t('clients.loyalty.works.stage.description-2')} -

    -
    -
    -
    - 3 -
    -

    - {t('clients.loyalty.works.stage-3')} -

    -

    - {t('clients.loyalty.works.stage.description-3')} -

    -
    -
    -
    - 4 -
    -

    - {t('clients.loyalty.works.stage-4')} -

    -

    - {t('clients.loyalty.works.stage.description-4')} -

    -
    -
    -
    -
    - - {/* Loyalty Levels */} -
    -
    -
    -

    - {t('clients.loyalty.works.levels.title')} -

    -

    - {t('clients.loyalty.works.levels.description')} -

    -
    - -
    - - -

    - {t('clients.loyalty.works.levels.card-1.title')} -

    -
    - - {t('clients.loyalty.works.levels.card-1.percent')} - -

    - {t('clients.loyalty.works.levels.card.mark')} -

    -
    -
      -
    • - - - {t('clients.loyalty.works.levels.card-1.bonus-1')} - -
    • -
    • - - - {t('clients.loyalty.works.levels.card-1.bonus-2')} - -
    • -
    • - - - {t('clients.loyalty.works.levels.card-1.bonus-3')} - -
    • -
    -
    -
    - - - -

    - {t('clients.loyalty.works.levels.card-2.title')} -

    -
    - - {t('clients.loyalty.works.levels.card-2.percent')} - -

    - {t('clients.loyalty.works.levels.card.mark')} -

    -
    -
      -
    • - - - {t('clients.loyalty.works.levels.card-1.bonus-1')} - -
    • -
    • - - - {t('clients.loyalty.works.levels.card-2.bonus-2')} - -
    • -
    • - - - {t('clients.loyalty.works.levels.card-3.bonus-3')} - -
    • -
    • - - - {t('clients.loyalty.works.levels.card-4.bonus-4')} - -
    • -
    -
    -
    - - - -

    - {t('clients.loyalty.works.levels.card-3.title')} -

    -
    - - {t('clients.loyalty.works.levels.card-3.percent')} - -

    - {t('clients.loyalty.works.levels.card.mark')} -

    -
    -
      -
    • - - - {t('clients.loyalty.works.levels.card-3.bonus-1')} - -
    • -
    • - - - {t('clients.loyalty.works.levels.card-3.bonus-2')} - -
    • -
    • - - - {t('clients.loyalty.works.levels.card-3.bonus-3')} - -
    • -
    • - - - {t('clients.loyalty.works.levels.card-3.bonus-4')} - -
    • -
    • - - - {t('clients.loyalty.works.levels.card-3.bonus-5')} - -
    • -
    -
    -
    -
    -
    -
    diff --git a/src/pages-templates/login/index.tsx b/src/pages-templates/login/index.tsx index c248838..712d049 100644 --- a/src/pages-templates/login/index.tsx +++ b/src/pages-templates/login/index.tsx @@ -22,6 +22,7 @@ import { TabsList, TabsTrigger, } from '@/shared/shadcn-ui/tabs'; +import Container from '@/shared/shadcn-ui/conteiner'; const tabs = [ { @@ -51,107 +52,109 @@ export default function LoginPage() { }; return ( -
    -
    -
    -
    -
    - + +
    +
    +
    +
    +
    + +
    +

    + {t('auth.title')} +

    +

    {t('auth.description')}

    -

    - {t('auth.title')} -

    -

    {t('auth.description')}

    -
    -
    - - +
    + + + {tabs.map((tab) => { + return ( + + {t(tab.label)} + + ); + })} + + {tabs.map((tab) => { - return ( - - {t(tab.label)} - - ); - })} - + const tabCookieName = `${tab.type}__token`; - {tabs.map((tab) => { - const tabCookieName = `${tab.type}__token`; + const authenticationCookie = getCookie(tabCookieName); - const authenticationCookie = getCookie(tabCookieName); + if (authenticationCookie) { + return ( + + + + {t(tab.title)} + + + + + + + + + + ); + } - if (authenticationCookie) { return ( {t(tab.title)} + {t(tab.description)} - - - - - + + ); - } + })} + - return ( - - - - {t(tab.title)} - {t(tab.description)} - - - - - - - ); - })} - - -
    -

    - {t('auth.loginIssues')}{' '} - - {t('auth.contactLink')} - -

    +
    +

    + {t('auth.loginIssues')}{' '} + + {t('auth.contactLink')} + +

    +
    -
    -
    -
    +
    +
    + ); } diff --git a/src/shared/shadcn-ui/conteiner.tsx b/src/shared/shadcn-ui/conteiner.tsx new file mode 100644 index 0000000..43ea538 --- /dev/null +++ b/src/shared/shadcn-ui/conteiner.tsx @@ -0,0 +1,10 @@ +"use client" + +interface ContainerProps { + children: React.ReactNode +} +export default function Container({children}: ContainerProps) { + return ( +
    {children}
    + ) +} \ No newline at end of file diff --git a/src/widgets/about-page/station-gallery.tsx b/src/widgets/about-page/station-gallery.tsx index 16bfa53..c47b752 100644 --- a/src/widgets/about-page/station-gallery.tsx +++ b/src/widgets/about-page/station-gallery.tsx @@ -76,7 +76,7 @@ export function StationGallery() { const { t } = useTextController(); return ( -
    +
    { const { t } = useTextController(); return ( -
    -
    -
    -
    -
    - -
    -

    - {t('clients.benefits.title')} -

    -

    - {t('clients.benefits.subtitle')} -

    + +
    +
    +
    +
    +
    + +
    +

    + {t('clients.benefits.title')} +

    +

    + {t('clients.benefits.subtitle')} +

    -
    - {benefits.map(({ title, description }) => { - return ( -
    -
    - +
    + {benefits.map(({ title, description }) => { + return ( +
    +
    + +
    +
    +

    {title}

    +

    {description}

    +
    -
    -

    {title}

    -

    {description}

    -
    -
    - ); - })} + ); + })} +
    +
    +
    + Преимущества для клиентов
    -
    -
    - Преимущества для клиентов
    -
    -
    +
    + ); }; diff --git a/src/widgets/clients/ui/services-overview-section.tsx b/src/widgets/clients/ui/services-overview-section.tsx index e212173..f32bdd5 100644 --- a/src/widgets/clients/ui/services-overview-section.tsx +++ b/src/widgets/clients/ui/services-overview-section.tsx @@ -11,6 +11,7 @@ import { CardHeader, CardTitle, } from '@/shared/shadcn-ui/card'; +import Container from '@/shared/shadcn-ui/conteiner'; interface ServiceOverview { title: string; @@ -49,39 +50,41 @@ export const ServicesOverviewSection = () => { const { t } = useTextController(); return ( -
    -
    -
    -

    - {t('clients.services.title')} -

    -

    - {t('clients.services.subtitle')} -

    -
    + +
    +
    +
    +

    + {t('clients.services.title')} +

    +

    + {t('clients.services.subtitle')} +

    +
    -
    - {servicesOverview.map(({ description, Icon, contentText, title }) => { - return ( - - -
    - -
    - {title} - {description} -
    - -

    {contentText}

    -
    -
    - ); - })} +
    + {servicesOverview.map(({ description, Icon, contentText, title }) => { + return ( + + +
    + +
    + {title} + {description} +
    + +

    {contentText}

    +
    +
    + ); + })} +
    -
    -
    +
    + ); }; diff --git a/src/widgets/footer.tsx b/src/widgets/footer.tsx index c921b99..856aee1 100644 --- a/src/widgets/footer.tsx +++ b/src/widgets/footer.tsx @@ -11,113 +11,119 @@ export const Footer = () => { return (