diff --git a/package.json b/package.json index 44bb7bf..7b99b05 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,10 @@ "@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", + "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 dbc27a2..a4d7e28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,12 +53,18 @@ 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 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 @@ -1334,6 +1340,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'} @@ -1342,6 +1351,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'} @@ -1413,9 +1425,23 @@ 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==} + 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'} @@ -1469,6 +1495,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'} @@ -1739,10 +1769,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==} @@ -2118,6 +2161,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} @@ -2335,6 +2386,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'} @@ -3836,12 +3890,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: {} @@ -3917,8 +3981,20 @@ snapshots: color-string: 1.9.1 optional: true + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + 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 @@ -3971,6 +4047,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: {} @@ -4396,10 +4474,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: @@ -4752,6 +4839,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 @@ -4921,6 +5014,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/about/page.tsx b/src/app/about/page.tsx index 418c7de..778e5c7 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -1,5 +1,17 @@ -import AboutPage from "@/pages-templates/about"; +import AboutPage from '@/pages-templates/about'; -export default function About(){ - return +import { mainPageApi } from '@/features/pages/api/pages.api'; + +import { makeStore } from '@/shared/store'; + +export default async function About() { + const store = makeStore(); + + const { data } = await store.dispatch( + mainPageApi.endpoints.fetchAboutUsPageContent.initiate(), + ); + + if (!data) return null; + + return ; } diff --git a/src/app/api-utlities/@types/about-us.ts b/src/app/api-utlities/@types/about-us.ts new file mode 100644 index 0000000..8d5ea67 --- /dev/null +++ b/src/app/api-utlities/@types/about-us.ts @@ -0,0 +1,8 @@ +import { HistoryItems, Reviews, Stations, TeamMembers } from '.'; + +export type AboutUsPageData = { + team: TeamMembers; + history: HistoryItems; + stations: Stations; + reviews: Reviews; +}; diff --git a/src/app/api-utlities/@types/index.ts b/src/app/api-utlities/@types/index.ts index aa1e81d..8be73a6 100644 --- a/src/app/api-utlities/@types/index.ts +++ b/src/app/api-utlities/@types/index.ts @@ -1,3 +1,13 @@ +import { + presentDiscounts, + presentHistoryItems, + presentJobs, + presentPartners, + presentReviews, + presentStations, + presentTeamMembers, +} from '../presenters'; + export type Root = { records: T[] }; export interface Image { @@ -53,3 +63,30 @@ export type TextResponse = Root<{ _name: string; _znachenie: string | null; }>; + +export type Team = Root<{ + _foto: Image[]; + _zvanie: string; + _name: string; +}>; + +export type History = Root<{ + _name: string; + _god: string; + _opisanie: string; +}>; + +export type Review = Root<{ + id: number; + _name: string; + _otzyv: string; + _rejting: number; +}>; + +export type TeamMembers = ReturnType; +export type HistoryItems = ReturnType; +export type Stations = ReturnType; +export type Partners = ReturnType; +export type Jobs = ReturnType; +export type Discounts = ReturnType; +export type Reviews = ReturnType; diff --git a/src/app/api-utlities/@types/main.ts b/src/app/api-utlities/@types/main.ts index 881fcc3..e128c92 100644 --- a/src/app/api-utlities/@types/main.ts +++ b/src/app/api-utlities/@types/main.ts @@ -1,14 +1,4 @@ -import { - presentDiscounts, - presentJobs, - presentPartners, - presentStations, -} from '../presenters'; - -export type Partners = ReturnType; -export type Jobs = ReturnType; -export type Discounts = ReturnType; -export type Stations = ReturnType; +import { Discounts, Jobs, Partners, Stations } from '.'; export type MainPageData = { discounts: Discounts; diff --git a/src/app/api-utlities/presenters/index.ts b/src/app/api-utlities/presenters/index.ts index 7fd3220..c13e57a 100644 --- a/src/app/api-utlities/presenters/index.ts +++ b/src/app/api-utlities/presenters/index.ts @@ -2,16 +2,23 @@ import { isEmpty } from 'lodash'; import { Discount, + History, Image, Job, Partner, + Review, + Select, Station, + Team, TextResponse, } from '../@types'; export const presentImage = (images: Image[]) => isEmpty(images) ? null : `${process.env.TAYLOR_MEDIA_URL}/${images[0].url}`; +export const presentSelect = (selectItems: Select[]) => + !isEmpty(selectItems) ? selectItems[0].name : null; + export const presentPartners = (partners: Partner) => partners.records.map((record, index) => ({ id: index + 1, @@ -24,8 +31,22 @@ export const presentJobs = (jobs: Job) => 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, + location: presentSelect(job._localtio), + type: presentSelect(job._type), + })); + +export const presentTeamMembers = (members: Team) => + members.records.map((member) => ({ + name: member._name, + photo: presentImage(member._foto), + profession: member._zvanie, + })); + +export const presentHistoryItems = (historyItems: History) => + historyItems.records.map((item) => ({ + name: item._name, + year: item._god, + description: item._opisanie, })); export const presentDiscounts = (discounts: Discount) => @@ -54,7 +75,7 @@ export const presentStations = (stations: Station) => electricCharge: station._propanCopy || false, miniMarket: station._zaryadnayaStanci || false, toilet: station._miniMarketCop || false, - region: !isEmpty(station._region) ? station._region[0].name : null, + region: presentSelect(station._region), image: presentImage(station._foto), })); @@ -63,3 +84,11 @@ export const presentTexts = (texts: TextResponse) => key: item._name, value: item._znachenie, })); + +export const presentReviews = (reviews: Review) => + reviews.records.map((review) => ({ + id: review.id, + fullname: review._name, + review: review._otzyv, + rating: review._rejting, + })); diff --git a/src/app/api-utlities/requests/about-us-page.request.ts b/src/app/api-utlities/requests/about-us-page.request.ts new file mode 100644 index 0000000..ee16f00 --- /dev/null +++ b/src/app/api-utlities/requests/about-us-page.request.ts @@ -0,0 +1,13 @@ +import { + historyRequest, + reviewsRequest, + stationsWithImageRequest, + teamRequest, +} from './common'; + +export const aboutUsPageRequest = { + ...teamRequest, + ...historyRequest, + ...stationsWithImageRequest, + ...reviewsRequest, +}; diff --git a/src/app/api-utlities/requests/common.ts b/src/app/api-utlities/requests/common.ts index ff001c0..3d4ab14 100644 --- a/src/app/api-utlities/requests/common.ts +++ b/src/app/api-utlities/requests/common.ts @@ -1,3 +1,5 @@ +import { EnumType } from 'json-to-graphql-query'; + export const stationsRequest = { _azs: { records: { @@ -27,6 +29,48 @@ export const stationsRequest = { }, }; +export const stationsWithImageRequest = { + _azs: { + __args: { + filtersSet: { + conjunction: new EnumType('and'), + filtersSet: [ + { + field: new EnumType('_foto'), + operator: 'isNotEmpty', + value: [], + }, + ], + }, + }, + + 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: { @@ -76,3 +120,36 @@ export const textsRequest = { }, }, }; + +export const teamRequest = { + _komanda: { + records: { + _foto: { + url: true, + }, + _zvanie: true, + _name: true, + }, + }, +}; + +export const historyRequest = { + _istoriya: { + records: { + _name: true, + _god: true, + _opisanie: true, + }, + }, +}; + +export const reviewsRequest = { + _otzyvy: { + records: { + id: true, + _name: true, + _otzyv: true, + _rejting: true, + }, + }, +}; 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..8772735 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -1,54 +1,39 @@ 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 }, - ); - } +import { loginFormSchema } from '@/entities/auth/model/validation/login-form.schema'; - const { searchParams } = req.nextUrl; +import { validationErrorHandler } from '../../middlewares/error-handler.middleware'; - const phoneNumber = searchParams.get('phoneNumber'); - const cardNumber = searchParams.get('cardNumber'); - const type = searchParams.get('type'); +const routeHandler = async (req: NextRequest) => { + const body = await req.json(); - if (!phoneNumber || !cardNumber || !type) { - return NextResponse.json({ error: 'Bad request' }, { status: 400 }); - } + const validatedBody = loginFormSchema + .merge(z.object({ type: z.enum(['bonus', 'corporate']) })) + .parse(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 parsedResponse = JSON.parse(oriyoResponse.data); - const data = (await loginRes.json()) as LoginData; - - const token = data.token; - if (!token) { - return NextResponse.json({ error: 'No auth token' }, { status: 401 }); + if (!parsedResponse.token) { + 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`, oriyoResponse.data, { path: '/', maxAge: 2 * 60 * 60, - secure: process.env.NODE_ENV === 'production', }); return response; @@ -57,3 +42,5 @@ export const GET = async (req: NextRequest) => { return NextResponse.json({ error: 'Server error' }, { status: 500 }); } }; + +export const POST = validationErrorHandler(routeHandler); diff --git a/src/app/api/bonus/info/route.ts b/src/app/api/bonus/info/route.ts new file mode 100644 index 0000000..d7a8178 --- /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'; + +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/app/api/bonus/transactions/route.ts b/src/app/api/bonus/transactions/route.ts new file mode 100644 index 0000000..310e78a --- /dev/null +++ b/src/app/api/bonus/transactions/route.ts @@ -0,0 +1,63 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; + +import oriyoClient from '@/app/api-utlities/utilities/oriyo.client'; + +import { validationErrorHandler } from '../../middlewares/error-handler.middleware'; + +const validatedSchema = z.object({ + start_date: z.string().optional(), + end_date: z.string().optional(), + limit: z.coerce.number(), + page: z.coerce.number(), +}); + +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 params = Array.from(req.nextUrl.searchParams.entries()).reduce( + (pr, cr) => { + pr[cr[0]] = cr[1]; + + return pr; + }, + {} as Record, + ); + + const validatedRequest = validatedSchema.parse(params); + + const { card_id, token } = JSON.parse(bonusTokenData.value); + + const oriyoResponse = await oriyoClient.get('/client/transactions', { + params: { + card_id, + token, + limit: validatedRequest.limit, + page: validatedRequest.page, + type: 'bonus', + sort: 'id', + direction: 'desc', + start_date: validatedRequest.start_date, + end_date: validatedRequest.end_date, + }, + }); + + const parsedResponse = JSON.parse(oriyoResponse.data); + + if (parsedResponse.error) { + return NextResponse.json({ message: 'Fetch error' }, { status: 400 }); + } + + return new Response(oriyoResponse.data, { + headers: { 'Content-Type': 'application/json' }, + }); +}; + +export const GET = validationErrorHandler(routeHandler); diff --git a/src/app/api/corporate/info/route.ts b/src/app/api/corporate/info/route.ts new file mode 100644 index 0000000..b7e23ff --- /dev/null +++ b/src/app/api/corporate/info/route.ts @@ -0,0 +1,23 @@ +import { omit } from 'lodash'; +import { NextRequest, NextResponse } from 'next/server'; + +import { validationErrorHandler } from '../../middlewares/error-handler.middleware'; + +const routeHandler = async (req: NextRequest) => { + const bonusTokenData = req.cookies.get('corporate__token'); + + if (!bonusTokenData) { + return NextResponse.json( + { error: 'User does not have access' }, + { status: 401 }, + ); + } + + const parsedData = JSON.parse(bonusTokenData.value); + + return new Response(JSON.stringify(omit(parsedData, 'token')), { + headers: { 'Content-Type': 'application/json' }, + }); +}; + +export const GET = 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..d7fa719 --- /dev/null +++ b/src/app/api/middlewares/error-handler.middleware.ts @@ -0,0 +1,19 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { ZodError } from 'zod'; + +export const validationErrorHandler = + (handler: Function) => + async (req: NextRequest, ...args: any[]) => { + try { + return await handler(req, ...args); + } 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 deleted file mode 100644 index f17286f..0000000 --- a/src/app/api/pages/main/route.ts +++ /dev/null @@ -1,24 +0,0 @@ -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'; - -export async function GET(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' }, - }, - ); -} diff --git a/src/app/api/text/route.ts b/src/app/api/text/route.ts deleted file mode 100644 index a5bf3df..0000000 --- a/src/app/api/text/route.ts +++ /dev/null @@ -1,14 +0,0 @@ -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' }, - }, - ); -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7766960..a195aa5 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/app/page.tsx b/src/app/page.tsx index 0cba606..a340dee 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,7 @@ +import { mainPageApi } from '@/features/pages/api/pages.api'; + +import { makeStore } from '@/shared/store'; + import { AboutSection } from '@/widgets/about-section'; import { CharitySection } from '@/widgets/charity-section'; import { CtaSection } from '@/widgets/cta-section'; @@ -8,23 +12,24 @@ import { PromotionsSection } from '@/widgets/promotions-section'; import { StatsSection } from '@/widgets/stats-section'; import { VacanciesSection } from '@/widgets/vacancies-section'; -import { MainPageData } from './api-utlities/@types/main'; - export default async function Home() { - const mainPageData = (await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/pages/main`, - { method: 'GET' }, - ).then((res) => res.json())) as MainPageData; + const store = makeStore(); + + const { data, isLoading, error } = await store.dispatch( + mainPageApi.endpoints.fetchMainPageContent.initiate(), + ); + + if (isLoading || !data) return null; return (
- + - - - + + +
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/entities/bonus/api/bonus.api.ts b/src/entities/bonus/api/bonus.api.ts new file mode 100644 index 0000000..b83c712 --- /dev/null +++ b/src/entities/bonus/api/bonus.api.ts @@ -0,0 +1,33 @@ +import { baseAPI } from '@/shared/api/base-api'; + +import { + ClientInfo, + TransactionRequest, + TransactionResponse, +} from '../model/types/bonus-client-info.type'; + +export const bonusApi = baseAPI.injectEndpoints({ + endpoints: (builder) => ({ + fetchMyBonusInfo: builder.query({ + query: () => { + return { + url: '/bonus/info', + }; + }, + }), + fetchBonusTransactions: builder.query< + TransactionResponse, + TransactionRequest + >({ + query: (request) => { + return { + url: '/bonus/transactions', + params: request, + }; + }, + }), + }), +}); + +export const { useFetchMyBonusInfoQuery, useFetchBonusTransactionsQuery } = + 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..bc50a6c --- /dev/null +++ b/src/entities/bonus/model/types/bonus-client-info.type.ts @@ -0,0 +1,34 @@ +export interface ClientInfo { + card_id: number; + fullname: string; + cardno: string; + reg_date: string; + end_date: string; + bonuses: string; +} + +export interface TransactionResponse { + transactions: Transaction[]; + card_id: string; + current_page: number; + limit: number; + total_records: number; + total_pages: number; +} + +export interface Transaction { + id: number; + date_create: string; + station: string; + product_name: string; + amount: string; + price_real: string; + sum_real: string; +} + +export interface TransactionRequest { + start_date?: string; + end_date?: string; + page: number; + limit: number; +} diff --git a/src/entities/corporate/api/corporate.api.ts b/src/entities/corporate/api/corporate.api.ts new file mode 100644 index 0000000..32426ac --- /dev/null +++ b/src/entities/corporate/api/corporate.api.ts @@ -0,0 +1,17 @@ +import { baseAPI } from '@/shared/api/base-api'; + +import { CorporateInfoResponse } from '../model/types/corporate-client-info.type'; + +export const corporateApi = baseAPI.injectEndpoints({ + endpoints: (builder) => ({ + fetchMyCorporateInfo: builder.query({ + query: () => { + return { + url: '/corporate/info', + }; + }, + }), + }), +}); + +export const { useFetchMyCorporateInfoQuery } = corporateApi; diff --git a/src/entities/corporate/model/types/corporate-client-info.type.ts b/src/entities/corporate/model/types/corporate-client-info.type.ts new file mode 100644 index 0000000..3a069ae --- /dev/null +++ b/src/entities/corporate/model/types/corporate-client-info.type.ts @@ -0,0 +1,9 @@ +export interface CorporateInfoResponse { + created_at: string; + fund: string; + fund_total: string; + group_id: number; + group_name: string; + overdraft: string; + total_cards: number; +} 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/features/map/ui/gas-station-map.tsx b/src/features/map/ui/gas-station-map.tsx index c86273e..6bfa874 100644 --- a/src/features/map/ui/gas-station-map.tsx +++ b/src/features/map/ui/gas-station-map.tsx @@ -10,7 +10,7 @@ import { } from 'lucide-react'; import { useMemo, useState } from 'react'; -import { Stations } from '@/app/api-utlities/@types/main'; +import { Stations } from '@/app/api-utlities/@types'; import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { Badge } from '@/shared/shadcn-ui/badge'; diff --git a/src/features/pages/api/pages.api.ts b/src/features/pages/api/pages.api.ts new file mode 100644 index 0000000..6909d11 --- /dev/null +++ b/src/features/pages/api/pages.api.ts @@ -0,0 +1,59 @@ +import { jsonToGraphQLQuery } from 'json-to-graphql-query'; + +import { AboutUsPageData } from '@/app/api-utlities/@types/about-us'; +import { MainPageData } from '@/app/api-utlities/@types/main'; +import { + presentDiscounts, + presentHistoryItems, + presentJobs, + presentPartners, + presentReviews, + presentStations, + presentTeamMembers, +} from '@/app/api-utlities/presenters'; +import { aboutUsPageRequest } from '@/app/api-utlities/requests/about-us-page.request'; +import { mainPageRequest } from '@/app/api-utlities/requests/main-page.request'; + +import { taylorAPI } from '@/shared/api/taylor-api'; + +export const mainPageApi = taylorAPI.injectEndpoints({ + endpoints: (builder) => ({ + fetchMainPageContent: builder.query({ + query: () => ({ + url: '', + method: 'POST', + body: { + query: jsonToGraphQLQuery({ query: mainPageRequest }), + }, + }), + + transformResponse: (response: any) => { + return { + partners: presentPartners(response.data._partners), + jobs: presentJobs(response.data._vacancies), + discounts: presentDiscounts(response.data._akcii), + stations: presentStations(response.data._azs), + }; + }, + }), + + fetchAboutUsPageContent: builder.mutation({ + query: () => ({ + url: '', + method: 'POST', + body: { + query: jsonToGraphQLQuery({ query: aboutUsPageRequest }), + }, + }), + + transformResponse: (response: any) => { + return { + team: presentTeamMembers(response.data._komanda), + history: presentHistoryItems(response.data._istoriya), + stations: presentStations(response.data._azs), + reviews: presentReviews(response.data._otzyvy), + }; + }, + }), + }), +}); diff --git a/src/pages-templates/(dashboard)/corporate-dashboard/index.tsx b/src/pages-templates/(dashboard)/corporate-dashboard/index.tsx index 9e54f4a..6f0bc94 100644 --- a/src/pages-templates/(dashboard)/corporate-dashboard/index.tsx +++ b/src/pages-templates/(dashboard)/corporate-dashboard/index.tsx @@ -4,6 +4,8 @@ import { subMonths } from 'date-fns'; import { Building2, LogOut, Wallet } from 'lucide-react'; import { useState } from 'react'; +import { useFetchMyCorporateInfoQuery } from '@/entities/corporate/api/corporate.api'; + import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { Button } from '@/shared/shadcn-ui/button'; import { @@ -96,8 +98,10 @@ export function CorporateDashboard() { const { t } = useTextController(); + const { data, isLoading } = useFetchMyCorporateInfoQuery({}); + return ( -
+
@@ -110,7 +114,11 @@ export function CorporateDashboard() {
{/* Company Card */} - + @@ -118,55 +126,67 @@ export function CorporateDashboard() { -
-
-
-

- {t('corporate.companyCard.companyNameLabel')} -

-

{companyData.companyName}

+ {!data ? ( + <>Loading + ) : ( +
+
+
+

+ {t('corporate.companyCard.companyNameLabel')} +

+

+ {data.group_name} +

+
+
+

+ {t('corporate.companyCard.cardsCountLabel')} +

+

{data.total_cards}

+
+
+

+ {t('corporate.companyCard.registrationDateLabel')} +

+ +

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

+
-
-

- {t('corporate.companyCard.cardsCountLabel')} -

-

{companyData.numberOfCards}

-
-
-

- {t('corporate.companyCard.registrationDateLabel')} -

-

- {companyData.registrationDate} -

+
+
+

+ {t('corporate.companyCard.fundLabel')} +

+

+ {data.fund.toLocaleString()} {t('corporate.currency')} +

+
+
+

+ {t('corporate.companyCard.overdraftLabel')} +

+

+ {data.overdraft.toLocaleString()}{' '} + {t('corporate.currency')} +

+
-
-
-

- {t('corporate.companyCard.fundLabel')} -

-

- {companyData.fund.toLocaleString()}{' '} - {t('corporate.currency')} -

-
-
-

- {t('corporate.companyCard.overdraftLabel')} -

-

- {companyData.overdraft.toLocaleString()}{' '} - {t('corporate.currency')} -

-
-
-
+ )} {/* Fund Card */} - + @@ -177,14 +197,18 @@ export function CorporateDashboard() { -
-

- {companyData.totalFund.toLocaleString()} -

-

- {t('corporate.fundCard.currency')} -

-
+ {!data ? ( + <>Loading + ) : ( +
+

+ {data.fund_total?.toLocaleString()} +

+

+ {t('corporate.fundCard.currency')} +

+
+ )}
diff --git a/src/pages-templates/(dashboard)/customer-dashboard/index.tsx b/src/pages-templates/(dashboard)/customer-dashboard/index.tsx index d4181a1..97a7225 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,22 +16,13 @@ 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,37 +34,46 @@ 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}

-
-
-
+ )}
diff --git a/src/pages-templates/about/index.tsx b/src/pages-templates/about/index.tsx index 52cdf82..728f78c 100644 --- a/src/pages-templates/about/index.tsx +++ b/src/pages-templates/about/index.tsx @@ -3,12 +3,13 @@ import { Fuel, History, MapPin, Star, Target, Users } from 'lucide-react'; import Image from 'next/image'; -// import { useTranslation } from 'next-i18next'; +import { AboutUsPageData } from '@/app/api-utlities/@types/about-us'; import AnimatedCounter from '@/shared/components/animated-counter'; 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'; import { CompanyTimeline } from '@/widgets/about-page/company-timeline'; import { StationGallery } from '@/widgets/about-page/station-gallery'; @@ -19,7 +20,11 @@ export const metadata = { description: 'about.metadata.description', }; -export default function AboutPage() { +export interface AboutPageProps { + content: AboutUsPageData; +} + +export default function AboutPage({ content }: AboutPageProps) { const { t } = useTextController(); return ( @@ -36,8 +41,12 @@ export default function AboutPage() { className='object-cover' priority /> -
-
+
+

{t('about.hero.title')} @@ -52,55 +61,60 @@ 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 +127,7 @@ export default function AboutPage() { {t('about.stats.subtitle')}

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

@@ -139,176 +153,195 @@ 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')} +

+
+ +
+ {content.team.map((member, index) => ( +
+
+ {member.photo && ( + {t(`about.team.members.${index}.name`)} + )} +
+
+

{member.name}

+

{member.profession}

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

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

+

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

-

- {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`)} -

-
-
- ))} +
+ {content.reviews.map((review, index) => ( + + +
+ {Array(5) + .fill(0) + .map((_, i) => ( + + ))} +
+

+ "{review.review}" +

+

{review.fullname}

+
+
+ ))} +
-
-
+ +

diff --git a/src/pages-templates/charity/index.tsx b/src/pages-templates/charity/index.tsx index 43abbd3..0638bb3 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..1f6b6ff 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')}

@@ -76,6 +77,7 @@ export function CertificatesPage() {
{certificates.map((certificate) => ( @@ -122,6 +124,6 @@ export function CertificatesPage() { ))}
- +
); } diff --git a/src/pages-templates/clients/index.tsx b/src/pages-templates/clients/index.tsx index 2235798..42eb934 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..ac740c1 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,105 +33,108 @@ 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')} +

+
+
+
+
+
+ Программа лояльности
-
- + - {/* Program Overview */} -
-
-
-
-
- -
-

- {t('clients.loyalty.programm.about')} + {/* How It Works */} +
+
+
+

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

-

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

+ {t('clients.loyalty.works.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 @@ -177,149 +181,150 @@ export function LoyaltyPage() {
-
+ - {/* Loyalty Levels */} -
-
-
-

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

-

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

+ {/* 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-2.bonus-1')} + +
  • +
  • + + + {t('clients.loyalty.works.levels.card-2.bonus-2')} + +
  • +
  • + + + {t('clients.loyalty.works.levels.card-2.bonus-3')} + +
  • +
  • + + + {t('clients.loyalty.works.levels.card-2.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')} + +
  • +
+
+
+
- -
- - -

- {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 23046b4..40c4bb1 100644 --- a/src/pages-templates/login/index.tsx +++ b/src/pages-templates/login/index.tsx @@ -1,11 +1,15 @@ '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 { Suspense } from 'react'; 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, @@ -13,6 +17,7 @@ import { CardHeader, CardTitle, } from '@/shared/shadcn-ui/card'; +import Container from '@/shared/shadcn-ui/conteiner'; import { Tabs, TabsContent, @@ -37,67 +42,132 @@ const tabs = [ }, ]; +function LoginPageTabs() { + const { t } = useTextController(); + const router = useRouter(); + const searchParams = useSearchParams(); + + const defaultTab = searchParams.get('tab') || 'bonus'; + + const handleTabChange = (tabType: string) => { + router.push(`?tab=${tabType}`, undefined); + }; + + return ( + + + {tabs.map((tab) => { + return ( + + {t(tab.label)} + + ); + })} + + + {tabs.map((tab) => { + const tabCookieName = `${tab.type}__token`; + + const authenticationCookie = getCookie(tabCookieName); + + if (authenticationCookie) { + return ( + + + + {t(tab.title)} + + + + + + + + + + ); + } + + return ( + + + + {t(tab.title)} + {t(tab.description)} + + + + + + + ); + })} + + ); +} + export default function LoginPage() { const { t } = useTextController(); 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.title)} - {t(tab.description)} - - - - - - - ); - })} - - -
-

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

+
+

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

+
-
-
-
+
+
+ ); } 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/api/taylor-api.ts b/src/shared/api/taylor-api.ts new file mode 100644 index 0000000..200a98c --- /dev/null +++ b/src/shared/api/taylor-api.ts @@ -0,0 +1,14 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'; + +const baseQuery = fetchBaseQuery({ + baseUrl: process.env.TAYLOR_API_ENDPOINT, + headers: { + Authorization: process.env.TAYLOR_API_TOKEN || '', + }, +}); + +export const taylorAPI = createApi({ + reducerPath: 'taylorAPI', + baseQuery, + endpoints: () => ({}), +}); diff --git a/src/shared/components/promotion-slider.tsx b/src/shared/components/promotion-slider.tsx index fabe4be..06a75f7 100644 --- a/src/shared/components/promotion-slider.tsx +++ b/src/shared/components/promotion-slider.tsx @@ -5,7 +5,7 @@ import Image from 'next/image'; import Link from 'next/link'; import { useEffect, useState } from 'react'; -import { Discounts } from '@/app/api-utlities/@types/main'; +import { Discounts } from '@/app/api-utlities/@types'; import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { Button } from '@/shared/shadcn-ui/button'; diff --git a/src/shared/language/api/text-control.api.ts b/src/shared/language/api/text-control.api.ts index 1fe2b26..dcbce86 100644 --- a/src/shared/language/api/text-control.api.ts +++ b/src/shared/language/api/text-control.api.ts @@ -1,12 +1,25 @@ -import { baseAPI } from '@/shared/api/base-api'; +import { jsonToGraphQLQuery } from 'json-to-graphql-query'; + +import { presentTexts } from '@/app/api-utlities/presenters'; +import { textsRequest } from '@/app/api-utlities/requests/common'; + +import { taylorAPI } from '@/shared/api/taylor-api'; import { TextItem } from '@/shared/types/text.types'; -export const textControlApi = baseAPI.injectEndpoints({ +export const textControlApi = taylorAPI.injectEndpoints({ endpoints: (builder) => ({ fetchText: builder.query({ - query: () => '/text', + query: () => ({ + url: '', + method: 'POST', + body: { + query: jsonToGraphQLQuery({ query: textsRequest }), + }, + }), + + transformResponse: (response: any) => { + return presentTexts(response.data._kontentSajta); + }, }), }), }); - -export const { useFetchTextQuery } = textControlApi; 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/shared/store/index.ts b/src/shared/store/index.ts index fb9db85..599e8de 100644 --- a/src/shared/store/index.ts +++ b/src/shared/store/index.ts @@ -4,13 +4,16 @@ import { createWrapper } from 'next-redux-wrapper'; import { baseAPI } from '@/shared/api/base-api'; +import { taylorAPI } from '../api/taylor-api'; import { rootReducer } from './root-reducer'; export const makeStore = () => configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => - getDefaultMiddleware().concat(baseAPI.middleware), + getDefaultMiddleware() + .concat(baseAPI.middleware) + .concat(taylorAPI.middleware), devTools: process.env.NODE_ENV === 'development', }); diff --git a/src/shared/store/root-reducer.ts b/src/shared/store/root-reducer.ts index 56b999c..080aa28 100644 --- a/src/shared/store/root-reducer.ts +++ b/src/shared/store/root-reducer.ts @@ -1,10 +1,10 @@ import { combineReducers } from '@reduxjs/toolkit'; -import { loginAPI } from '@/entities/auth/api/login.api'; - import { baseAPI } from '@/shared/api/base-api'; +import { taylorAPI } from '../api/taylor-api'; + export const rootReducer = combineReducers({ [baseAPI.reducerPath]: baseAPI.reducer, - [loginAPI.reducerPath]: loginAPI.reducer, + [taylorAPI.reducerPath]: taylorAPI.reducer, }); diff --git a/src/widgets/about-page/company-timeline.tsx b/src/widgets/about-page/company-timeline.tsx index ea6e15d..1dcf68b 100644 --- a/src/widgets/about-page/company-timeline.tsx +++ b/src/widgets/about-page/company-timeline.tsx @@ -3,64 +3,19 @@ import { Calendar, ChevronDown, ChevronUp } from 'lucide-react'; import { useState } from 'react'; +import { HistoryItems } from '@/app/api-utlities/@types'; + import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { Button } from '@/shared/shadcn-ui/button'; import { Card, CardContent } from '@/shared/shadcn-ui/card'; -const timelineEvents = [ - { - year: '2008', - title: 'Основание компании', - description: - 'GasNetwork была основана с открытием первых трех заправочных станций в Душанбе. С самого начала компания поставила перед собой цель предоставлять качественное топливо и отличный сервис.', - }, - { - year: '2010', - title: 'Расширение сети', - description: - 'Открытие еще пяти заправочных станций в различных регионах Таджикистана. Начало формирования единого стандарта обслуживания на всех станциях сети.', - }, - { - year: '2012', - title: 'Внедрение программы лояльности', - description: - 'Запуск первой в Таджикистане программы лояльности для клиентов сети заправок. Введение карт постоянного клиента с накопительной системой бонусов.', - }, - { - year: '2014', - title: 'Модернизация оборудования', - description: - 'Масштабная программа по обновлению оборудования на всех заправочных станциях сети. Внедрение современных технологий для повышения качества обслуживания.', - }, - { - year: '2016', - title: 'Открытие 15-й заправки', - description: - 'Значительное расширение сети с открытием юбилейной 15-й заправочной станции. GasNetwork становится одной из крупнейших сетей заправок в Таджикистане.', - }, - { - year: '2018', - title: 'Запуск мобильного приложения', - description: - 'Разработка и запуск мобильного приложения для клиентов сети. Возможность отслеживать бонусы, находить ближайшие заправки и получать специальные предложения.', - }, - { - year: '2020', - title: 'Создание благотворительного фонда', - description: - 'Основание благотворительного фонда GasNetwork для поддержки социальных проектов в Таджикистане. Начало активной социальной деятельности компании.', - }, - { - year: '2023', - title: 'Современное развитие', - description: - 'Сегодня GasNetwork - это 25+ современных заправочных станций по всему Таджикистану, более 150 сотрудников и тысячи довольных клиентов ежедневно.', - }, -]; +export interface CompanyTimelineProps { + timeline: HistoryItems; +} -export function CompanyTimeline() { +export function CompanyTimeline({ timeline }: CompanyTimelineProps) { const [expanded, setExpanded] = useState(false); - const displayEvents = expanded ? timelineEvents : timelineEvents.slice(0, 4); + const displayEvents = expanded ? timeline : timeline.slice(0, 4); const { t } = useTextController(); @@ -80,7 +35,7 @@ export function CompanyTimeline() {

{event.year}

-

{event.title}

+

{event.name}

- {timelineEvents.length > 4 && ( + {timeline.length > 4 && (