Merge branch 'dev' into render-main-page-data

This commit is contained in:
BunyodL 2025-05-02 05:57:04 +05:00
commit 2821025a3e
54 changed files with 1875 additions and 1280 deletions

View File

@ -25,8 +25,10 @@
"@radix-ui/react-toast": "^1.2.11", "@radix-ui/react-toast": "^1.2.11",
"@reduxjs/toolkit": "^2.7.0", "@reduxjs/toolkit": "^2.7.0",
"aos": "^2.3.4", "aos": "^2.3.4",
"axios": "^1.9.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cookies-next": "^5.1.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"embla-carousel-autoplay": "^8.6.0", "embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",

95
pnpm-lock.yaml generated
View File

@ -53,12 +53,18 @@ importers:
aos: aos:
specifier: ^2.3.4 specifier: ^2.3.4
version: 2.3.4 version: 2.3.4
axios:
specifier: ^1.9.0
version: 1.9.0
class-variance-authority: class-variance-authority:
specifier: ^0.7.1 specifier: ^0.7.1
version: 0.7.1 version: 0.7.1
clsx: clsx:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
cookies-next:
specifier: ^5.1.0
version: 5.1.0(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
date-fns: date-fns:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0 version: 4.1.0
@ -1334,6 +1340,9 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
available-typed-arrays@1.0.7: available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -1342,6 +1351,9 @@ packages:
resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==}
engines: {node: '>=4'} engines: {node: '>=4'}
axios@1.9.0:
resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==}
axobject-query@4.1.0: axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -1413,9 +1425,23 @@ packages:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'} engines: {node: '>=12.5.0'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
concat-map@0.0.1: concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
cookie@1.0.2:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'}
cookies-next@5.1.0:
resolution: {integrity: sha512-9Ekne+q8hfziJtnT9c1yDUBqT0eDMGgPrfPl4bpR3xwQHLTd/8gbSf6+IEkP/pjGsDZt1TGbC6emYmFYRbIXwQ==}
peerDependencies:
next: '>=15.0.0'
react: '>= 16.8.0'
cross-spawn@7.0.6: cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -1469,6 +1495,10 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
detect-libc@2.0.3: detect-libc@2.0.3:
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -1739,10 +1769,23 @@ packages:
flatted@3.3.3: flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
follow-redirects@1.15.9:
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
for-each@0.3.5: for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
form-data@4.0.2:
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
engines: {node: '>= 6'}
function-bind@1.1.2: function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
@ -2118,6 +2161,14 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
minimatch@10.0.1: minimatch@10.0.1:
resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
@ -2335,6 +2386,9 @@ packages:
prop-types@15.8.1: prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
punycode@2.3.1: punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -3836,12 +3890,22 @@ snapshots:
async-function@1.0.0: {} async-function@1.0.0: {}
asynckit@0.4.0: {}
available-typed-arrays@1.0.7: available-typed-arrays@1.0.7:
dependencies: dependencies:
possible-typed-array-names: 1.1.0 possible-typed-array-names: 1.1.0
axe-core@4.10.3: {} axe-core@4.10.3: {}
axios@1.9.0:
dependencies:
follow-redirects: 1.15.9
form-data: 4.0.2
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
axobject-query@4.1.0: {} axobject-query@4.1.0: {}
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
@ -3917,8 +3981,20 @@ snapshots:
color-string: 1.9.1 color-string: 1.9.1
optional: true optional: true
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
concat-map@0.0.1: {} concat-map@0.0.1: {}
cookie@1.0.2: {}
cookies-next@5.1.0(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0):
dependencies:
cookie: 1.0.2
next: 15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
cross-spawn@7.0.6: cross-spawn@7.0.6:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1
@ -3971,6 +4047,8 @@ snapshots:
has-property-descriptors: 1.0.2 has-property-descriptors: 1.0.2
object-keys: 1.1.1 object-keys: 1.1.1
delayed-stream@1.0.0: {}
detect-libc@2.0.3: {} detect-libc@2.0.3: {}
detect-node-es@1.1.0: {} detect-node-es@1.1.0: {}
@ -4396,10 +4474,19 @@ snapshots:
flatted@3.3.3: {} flatted@3.3.3: {}
follow-redirects@1.15.9: {}
for-each@0.3.5: for-each@0.3.5:
dependencies: dependencies:
is-callable: 1.2.7 is-callable: 1.2.7
form-data@4.0.2:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
mime-types: 2.1.35
function-bind@1.1.2: {} function-bind@1.1.2: {}
function.prototype.name@1.1.8: function.prototype.name@1.1.8:
@ -4752,6 +4839,12 @@ snapshots:
braces: 3.0.3 braces: 3.0.3
picomatch: 2.3.1 picomatch: 2.3.1
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
minimatch@10.0.1: minimatch@10.0.1:
dependencies: dependencies:
brace-expansion: 2.0.1 brace-expansion: 2.0.1
@ -4921,6 +5014,8 @@ snapshots:
object-assign: 4.1.1 object-assign: 4.1.1
react-is: 16.13.1 react-is: 16.13.1
proxy-from-env@1.1.0: {}
punycode@2.3.1: {} punycode@2.3.1: {}
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}

View File

@ -1,5 +1,17 @@
import AboutPage from "@/pages-templates/about"; import AboutPage from '@/pages-templates/about';
export default function About(){ import { mainPageApi } from '@/features/pages/api/pages.api';
return <AboutPage/>
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 <AboutPage content={data} />;
} }

View File

@ -0,0 +1,8 @@
import { HistoryItems, Reviews, Stations, TeamMembers } from '.';
export type AboutUsPageData = {
team: TeamMembers;
history: HistoryItems;
stations: Stations;
reviews: Reviews;
};

View File

@ -1,3 +1,13 @@
import {
presentDiscounts,
presentHistoryItems,
presentJobs,
presentPartners,
presentReviews,
presentStations,
presentTeamMembers,
} from '../presenters';
export type Root<T> = { records: T[] }; export type Root<T> = { records: T[] };
export interface Image { export interface Image {
@ -53,3 +63,30 @@ export type TextResponse = Root<{
_name: string; _name: string;
_znachenie: string | null; _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<typeof presentTeamMembers>;
export type HistoryItems = ReturnType<typeof presentHistoryItems>;
export type Stations = ReturnType<typeof presentStations>;
export type Partners = ReturnType<typeof presentPartners>;
export type Jobs = ReturnType<typeof presentJobs>;
export type Discounts = ReturnType<typeof presentDiscounts>;
export type Reviews = ReturnType<typeof presentReviews>;

View File

@ -1,14 +1,4 @@
import { import { Discounts, Jobs, Partners, Stations } from '.';
presentDiscounts,
presentJobs,
presentPartners,
presentStations,
} from '../presenters';
export type Partners = ReturnType<typeof presentPartners>;
export type Jobs = ReturnType<typeof presentJobs>;
export type Discounts = ReturnType<typeof presentDiscounts>;
export type Stations = ReturnType<typeof presentStations>;
export type MainPageData = { export type MainPageData = {
discounts: Discounts; discounts: Discounts;

View File

@ -2,16 +2,23 @@ import { isEmpty } from 'lodash';
import { import {
Discount, Discount,
History,
Image, Image,
Job, Job,
Partner, Partner,
Review,
Select,
Station, Station,
Team,
TextResponse, TextResponse,
} from '../@types'; } from '../@types';
export const presentImage = (images: Image[]) => export const presentImage = (images: Image[]) =>
isEmpty(images) ? null : `${process.env.TAYLOR_MEDIA_URL}/${images[0].url}`; 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) => export const presentPartners = (partners: Partner) =>
partners.records.map((record, index) => ({ partners.records.map((record, index) => ({
id: index + 1, id: index + 1,
@ -24,8 +31,22 @@ export const presentJobs = (jobs: Job) =>
id: index + 1, id: index + 1,
name: job._name, name: job._name,
tags: job._tags.map((tag) => tag.name), tags: job._tags.map((tag) => tag.name),
location: !isEmpty(job._localtio) ? job._localtio[0].name : null, location: presentSelect(job._localtio),
type: !isEmpty(job._type) ? job._type[0].name : null, 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) => export const presentDiscounts = (discounts: Discount) =>
@ -54,7 +75,7 @@ export const presentStations = (stations: Station) =>
electricCharge: station._propanCopy || false, electricCharge: station._propanCopy || false,
miniMarket: station._zaryadnayaStanci || false, miniMarket: station._zaryadnayaStanci || false,
toilet: station._miniMarketCop || false, toilet: station._miniMarketCop || false,
region: !isEmpty(station._region) ? station._region[0].name : null, region: presentSelect(station._region),
image: presentImage(station._foto), image: presentImage(station._foto),
})); }));
@ -63,3 +84,11 @@ export const presentTexts = (texts: TextResponse) =>
key: item._name, key: item._name,
value: item._znachenie, value: item._znachenie,
})); }));
export const presentReviews = (reviews: Review) =>
reviews.records.map((review) => ({
id: review.id,
fullname: review._name,
review: review._otzyv,
rating: review._rejting,
}));

View File

@ -0,0 +1,13 @@
import {
historyRequest,
reviewsRequest,
stationsWithImageRequest,
teamRequest,
} from './common';
export const aboutUsPageRequest = {
...teamRequest,
...historyRequest,
...stationsWithImageRequest,
...reviewsRequest,
};

View File

@ -1,3 +1,5 @@
import { EnumType } from 'json-to-graphql-query';
export const stationsRequest = { export const stationsRequest = {
_azs: { _azs: {
records: { 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 = { export const partnersRequest = {
_partners: { _partners: {
records: { 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,
},
},
};

View File

@ -0,0 +1,10 @@
import { Axios } from 'axios';
const oriyoClient = new Axios({
baseURL: process.env.ORIOYO_API_ENDPOINT || '',
headers: {
'Content-type': 'application/json',
},
});
export default oriyoClient;

View File

@ -1,54 +1,39 @@
import { NextRequest, NextResponse } from 'next/server'; 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) => { import { loginFormSchema } from '@/entities/auth/model/validation/login-form.schema';
if (req.method !== 'GET') {
return NextResponse.json(
{ error: 'Method is not supported' },
{ status: 405 },
);
}
const { searchParams } = req.nextUrl; import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
const phoneNumber = searchParams.get('phoneNumber'); const routeHandler = async (req: NextRequest) => {
const cardNumber = searchParams.get('cardNumber'); const body = await req.json();
const type = searchParams.get('type');
if (!phoneNumber || !cardNumber || !type) { const validatedBody = loginFormSchema
return NextResponse.json({ error: 'Bad request' }, { status: 400 }); .merge(z.object({ type: z.enum(['bonus', 'corporate']) }))
} .parse(body);
try { try {
const loginRes = await fetch( const oriyoResponse = await oriyoClient.get('/client/login', {
`https://test.oriyo.tj/api/client/login?type=${type}&phone=${phoneNumber}&uid=${cardNumber}`, params: {
{ type: validatedBody.type,
method: 'GET', phone: validatedBody.phoneNumber,
uid: validatedBody.cardNumber,
}, },
); });
if (!loginRes.ok) { const parsedResponse = JSON.parse(oriyoResponse.data);
return NextResponse.json(
{ error: 'Error during login' },
{ status: 400 },
);
}
const data = (await loginRes.json()) as LoginData; if (!parsedResponse.token) {
return NextResponse.json({ error: 'Credentials error' }, { status: 401 });
const token = data.token;
if (!token) {
return NextResponse.json({ error: 'No auth token' }, { status: 401 });
} }
const response = NextResponse.json({ success: true }); const response = NextResponse.json({ success: true });
response.cookies.set('token', token, { response.cookies.set(`${validatedBody.type}__token`, oriyoResponse.data, {
httpOnly: true,
path: '/', path: '/',
maxAge: 2 * 60 * 60, maxAge: 2 * 60 * 60,
secure: process.env.NODE_ENV === 'production',
}); });
return response; return response;
@ -57,3 +42,5 @@ export const GET = async (req: NextRequest) => {
return NextResponse.json({ error: 'Server error' }, { status: 500 }); return NextResponse.json({ error: 'Server error' }, { status: 500 });
} }
}; };
export const POST = validationErrorHandler(routeHandler);

View File

@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from 'next/server';
import oriyoClient from '@/app/api-utlities/utilities/oriyo.client';
import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
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);

View File

@ -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<string, string>,
);
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);

View File

@ -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);

View File

@ -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 },
);
}
};

View File

@ -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' },
},
);
}

View File

@ -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' },
},
);
}

View File

@ -37,7 +37,7 @@ export default async function RootLayout({
className='scroll-smooth' className='scroll-smooth'
style={{ scrollBehavior: 'smooth' }} style={{ scrollBehavior: 'smooth' }}
> >
<body className={`${inter.className} antialiased`}> <body className={`${inter.className} min-w-2xs antialiased`}>
<Providers textItems={response.data as TextItem[]}> <Providers textItems={response.data as TextItem[]}>
<Header /> <Header />
{children} {children}

View File

@ -1,3 +1,7 @@
import { mainPageApi } from '@/features/pages/api/pages.api';
import { makeStore } from '@/shared/store';
import { AboutSection } from '@/widgets/about-section'; import { AboutSection } from '@/widgets/about-section';
import { CharitySection } from '@/widgets/charity-section'; import { CharitySection } from '@/widgets/charity-section';
import { CtaSection } from '@/widgets/cta-section'; import { CtaSection } from '@/widgets/cta-section';
@ -8,23 +12,24 @@ import { PromotionsSection } from '@/widgets/promotions-section';
import { StatsSection } from '@/widgets/stats-section'; import { StatsSection } from '@/widgets/stats-section';
import { VacanciesSection } from '@/widgets/vacancies-section'; import { VacanciesSection } from '@/widgets/vacancies-section';
import { MainPageData } from './api-utlities/@types/main';
export default async function Home() { export default async function Home() {
const mainPageData = (await fetch( const store = makeStore();
`${process.env.NEXT_PUBLIC_API_URL}/pages/main`,
{ method: 'GET' }, const { data, isLoading, error } = await store.dispatch(
).then((res) => res.json())) as MainPageData; mainPageApi.endpoints.fetchMainPageContent.initiate(),
);
if (isLoading || !data) return null;
return ( return (
<main> <main>
<HeroSection /> <HeroSection />
<StatsSection /> <StatsSection />
<MapSection stations={mainPageData.stations} /> <MapSection stations={data.stations} />
<AboutSection /> <AboutSection />
<PromotionsSection discounts={mainPageData.discounts} /> <PromotionsSection discounts={data.discounts} />
<VacanciesSection jobs={mainPageData.jobs} /> <VacanciesSection jobs={data.jobs} />
<PartnersSection partners={mainPageData.partners} /> <PartnersSection partners={data.partners} />
<CharitySection /> <CharitySection />
<CtaSection /> <CtaSection />
</main> </main>

View File

@ -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'; import { LoginParams, LoginResponse } from '../model/contracts/login.contract';
export const loginAPI = createApi({ export const authenticationApi = baseAPI.injectEndpoints({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }), endpoints: (builder) => ({
endpoints: (build) => ({ login: builder.query<LoginResponse, LoginParams>({
login: build.query<LoginResponse, LoginParams>({
query: (data) => { query: (data) => {
const params = new URLSearchParams({ return {
method: 'POST',
body: {
type: data.type, type: data.type,
phoneNumber: data.phoneNumber, phoneNumber: data.phoneNumber,
cardNumber: data.cardNumber, cardNumber: data.cardNumber,
}).toString(); },
url: '/auth/login',
return {
url: `/auth/login?${params}`,
}; };
}, },
}), }),
}), }),
}); });
export const { useLazyLoginQuery } = loginAPI; export const { useLazyLoginQuery } = authenticationApi;

View File

@ -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 { export interface LoginResponse {
success: boolean; success: boolean;

View File

@ -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<ClientInfo, any>({
query: () => {
return {
url: '/bonus/info',
};
},
}),
fetchBonusTransactions: builder.query<
TransactionResponse,
TransactionRequest
>({
query: (request) => {
return {
url: '/bonus/transactions',
params: request,
};
},
}),
}),
});
export const { useFetchMyBonusInfoQuery, useFetchBonusTransactionsQuery } =
bonusApi;

View File

@ -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;
}

View File

@ -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<CorporateInfoResponse, any>({
query: () => {
return {
url: '/corporate/info',
};
},
}),
}),
});
export const { useFetchMyCorporateInfoQuery } = corporateApi;

View File

@ -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;
}

View File

@ -19,7 +19,10 @@ import {
} from '@/shared/shadcn-ui/form'; } from '@/shared/shadcn-ui/form';
import { Input } from '@/shared/shadcn-ui/input'; import { Input } from '@/shared/shadcn-ui/input';
import { LoginFormData, loginFormSchema } from '../model/login-form.schema'; import {
LoginFormData,
loginFormSchema,
} from '../../../../entities/auth/model/validation/login-form.schema';
interface LoginFormProps { interface LoginFormProps {
type: 'bonus' | 'corporate'; type: 'bonus' | 'corporate';
@ -40,16 +43,12 @@ export const LoginForm = ({ type }: LoginFormProps) => {
}); });
const onSubmit = async (data: LoginFormData) => { const onSubmit = async (data: LoginFormData) => {
try {
await login({ ...data, type }).unwrap(); await login({ ...data, type }).unwrap();
toast.success('Logged in successfully!'); toast.success('Logged in successfully!');
router.push( router.push(
type === 'bonus' ? '/customer-dashboard' : '/corporate-dashboard', type === 'bonus' ? '/customer-dashboard' : '/corporate-dashboard',
); );
} catch (error) {
toast.error('An error occured during login');
}
}; };
return ( return (

View File

@ -10,7 +10,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { useMemo, useState } from '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 { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Badge } from '@/shared/shadcn-ui/badge'; import { Badge } from '@/shared/shadcn-ui/badge';

View File

@ -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<MainPageData, void>({
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<AboutUsPageData, void>({
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),
};
},
}),
}),
});

View File

@ -4,6 +4,8 @@ import { subMonths } from 'date-fns';
import { Building2, LogOut, Wallet } from 'lucide-react'; import { Building2, LogOut, Wallet } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { useFetchMyCorporateInfoQuery } from '@/entities/corporate/api/corporate.api';
import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Button } from '@/shared/shadcn-ui/button'; import { Button } from '@/shared/shadcn-ui/button';
import { import {
@ -96,8 +98,10 @@ export function CorporateDashboard() {
const { t } = useTextController(); const { t } = useTextController();
const { data, isLoading } = useFetchMyCorporateInfoQuery({});
return ( return (
<div className='flex min-h-screen flex-col'> <div className='flex min-h-screen flex-col px-2.5'>
<main className='flex-1 py-10'> <main className='flex-1 py-10'>
<div className='container mx-auto max-w-6xl'> <div className='container mx-auto max-w-6xl'>
<div className='mb-8 flex items-center justify-between'> <div className='mb-8 flex items-center justify-between'>
@ -110,7 +114,11 @@ export function CorporateDashboard() {
<div className='mb-10 grid gap-3 md:grid-cols-3 md:gap-6'> <div className='mb-10 grid gap-3 md:grid-cols-3 md:gap-6'>
{/* Company Card */} {/* Company Card */}
<Card className='md:col-span-2'> <Card
data-aos='zoom-in'
data-aos-mirror='true'
className='md:col-span-2'
>
<CardHeader className='pb-2'> <CardHeader className='pb-2'>
<CardTitle className='flex items-center gap-2'> <CardTitle className='flex items-center gap-2'>
<Building2 className='h-5 w-5 text-red-600' /> <Building2 className='h-5 w-5 text-red-600' />
@ -118,26 +126,34 @@ export function CorporateDashboard() {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{!data ? (
<>Loading</>
) : (
<div className='grid gap-6 md:grid-cols-2'> <div className='grid gap-6 md:grid-cols-2'>
<div> <div>
<div className='mb-4 space-y-1'> <div className='mb-4 space-y-1'>
<p className='text-sm text-gray-500'> <p className='text-sm text-gray-500'>
{t('corporate.companyCard.companyNameLabel')} {t('corporate.companyCard.companyNameLabel')}
</p> </p>
<p className='font-medium'>{companyData.companyName}</p> <p className='truncate font-medium'>
{data.group_name}
</p>
</div> </div>
<div className='mb-4 space-y-1'> <div className='mb-4 space-y-1'>
<p className='text-sm text-gray-500'> <p className='text-sm text-gray-500'>
{t('corporate.companyCard.cardsCountLabel')} {t('corporate.companyCard.cardsCountLabel')}
</p> </p>
<p className='font-medium'>{companyData.numberOfCards}</p> <p className='font-medium'>{data.total_cards}</p>
</div> </div>
<div className='space-y-1'> <div className='space-y-1'>
<p className='text-sm text-gray-500'> <p className='text-sm text-gray-500'>
{t('corporate.companyCard.registrationDateLabel')} {t('corporate.companyCard.registrationDateLabel')}
</p> </p>
<p className='font-medium'> <p className='font-medium'>
{companyData.registrationDate} {new Date(data.created_at).toLocaleDateString(
'en-GB',
)}
</p> </p>
</div> </div>
</div> </div>
@ -147,8 +163,7 @@ export function CorporateDashboard() {
{t('corporate.companyCard.fundLabel')} {t('corporate.companyCard.fundLabel')}
</p> </p>
<p className='font-medium'> <p className='font-medium'>
{companyData.fund.toLocaleString()}{' '} {data.fund.toLocaleString()} {t('corporate.currency')}
{t('corporate.currency')}
</p> </p>
</div> </div>
<div className='mb-4 space-y-1'> <div className='mb-4 space-y-1'>
@ -156,17 +171,22 @@ export function CorporateDashboard() {
{t('corporate.companyCard.overdraftLabel')} {t('corporate.companyCard.overdraftLabel')}
</p> </p>
<p className='font-medium'> <p className='font-medium'>
{companyData.overdraft.toLocaleString()}{' '} {data.overdraft.toLocaleString()}{' '}
{t('corporate.currency')} {t('corporate.currency')}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
)}
</CardContent> </CardContent>
</Card> </Card>
{/* Fund Card */} {/* Fund Card */}
<Card className='bg-gradient-to-br from-red-600 to-red-800 text-white'> <Card
data-aos='zoom-in'
data-aos-mirror='true'
className='bg-gradient-to-br from-red-600 to-red-800 text-white'
>
<CardHeader> <CardHeader>
<CardTitle className='flex items-center gap-2'> <CardTitle className='flex items-center gap-2'>
<Wallet className='h-5 w-5' /> <Wallet className='h-5 w-5' />
@ -177,14 +197,18 @@ export function CorporateDashboard() {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{!data ? (
<>Loading</>
) : (
<div className='text-center'> <div className='text-center'>
<p className='mb-2 text-4xl font-bold'> <p className='mb-2 text-4xl font-bold'>
{companyData.totalFund.toLocaleString()} {data.fund_total?.toLocaleString()}
</p> </p>
<p className='text-white/80'> <p className='text-white/80'>
{t('corporate.fundCard.currency')} {t('corporate.fundCard.currency')}
</p> </p>
</div> </div>
)}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@ -2,6 +2,8 @@
import { ArrowUpRight, Clock, CreditCard, LogOut, User } from 'lucide-react'; 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 { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Button } from '@/shared/shadcn-ui/button'; import { Button } from '@/shared/shadcn-ui/button';
import { import {
@ -14,22 +16,13 @@ import {
import { TransactionsTable } from '@/widgets/transactions-table'; 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() { export function CustomerDashboard() {
const { t } = useTextController(); const { t } = useTextController();
const { data, isLoading } = useFetchMyBonusInfoQuery({});
return ( return (
<div className='flex min-h-screen flex-col'> <div className='flex min-h-screen flex-col px-2.5'>
<main className='flex-1 py-10'> <main className='flex-1 py-10'>
<div className='container mx-auto max-w-6xl'> <div className='container mx-auto max-w-6xl'>
<div className='mb-8 flex items-center justify-between'> <div className='mb-8 flex items-center justify-between'>
@ -41,8 +34,12 @@ export function CustomerDashboard() {
</div> </div>
<div className='mb-10 grid gap-3 md:grid-cols-3 md:gap-6'> <div className='mb-10 grid gap-3 md:grid-cols-3 md:gap-6'>
{/* Bonus Card */} <Card data-aos="zoom-in" data-aos-mirror="true" className='bg-gradient-to-br from-red-600 to-red-800 text-white'>
<Card className='bg-gradient-to-br from-red-600 to-red-800 text-white'> {!data || isLoading ? (
// TODO: Bunyod please add loader here
<>Loader here</>
) : (
<>
<CardHeader> <CardHeader>
<CardTitle className='flex items-center gap-2'> <CardTitle className='flex items-center gap-2'>
<CreditCard className='h-5 w-5' /> <CreditCard className='h-5 w-5' />
@ -54,9 +51,7 @@ export function CustomerDashboard() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className='text-center'> <div className='text-center'>
<p className='mb-2 text-4xl font-bold'> <p className='mb-2 text-4xl font-bold'>{data.bonuses}</p>
{customerData.bonusPoints}
</p>
<p className='text-white/80'> <p className='text-white/80'>
{t('customer.bonusCard.points')} {t('customer.bonusCard.points')}
</p> </p>
@ -64,14 +59,21 @@ export function CustomerDashboard() {
<div className='mt-6 flex items-center justify-between'> <div className='mt-6 flex items-center justify-between'>
<div className='flex items-center gap-1 text-sm text-white/80'> <div className='flex items-center gap-1 text-sm text-white/80'>
<Clock className='h-4 w-4' /> <Clock className='h-4 w-4' />
<span>{t('customer.bonusCard.validUntil')}</span> <span>
{t('customer.bonusCard.validUntil')}{' '}
{new Date(data.end_date).toLocaleDateString('en-GB')}
</span>
</div> </div>
<ArrowUpRight className='h-5 w-5 text-white/60' /> <ArrowUpRight className='h-5 w-5 text-white/60' />
</div> </div>
</CardContent> </CardContent>
</>
)}
</Card> </Card>
{/* Bonus Card */}
{/* Customer Card */} {/* Customer Card */}
<Card className='md:col-span-2'> <Card data-aos="zoom-in" data-aos-mirror="true" className='md:col-span-2'>
<CardHeader className='pb-2'> <CardHeader className='pb-2'>
<CardTitle className='flex items-center gap-2'> <CardTitle className='flex items-center gap-2'>
<User className='h-5 w-5 text-red-600' /> <User className='h-5 w-5 text-red-600' />
@ -79,22 +81,24 @@ export function CustomerDashboard() {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{!data || isLoading ? (
// TODO: Bunyod please add loader here
<>Loader here</>
) : (
<div className='grid gap-6 md:grid-cols-2'> <div className='grid gap-6 md:grid-cols-2'>
<div> <div>
<div className='mb-4 space-y-1'> <div className='mb-4 space-y-1'>
<p className='text-sm text-gray-500'> <p className='text-sm text-gray-500'>
{t('customer.infoCard.regDateLabel')} {t('customer.infoCard.regDateLabel')}
</p> </p>
<p className='font-medium'> <p className='font-medium'>{data.fullname}</p>
{customerData.firstName} {customerData.lastName}
</p>
</div> </div>
<div className='space-y-1'> <div className='space-y-1'>
<p className='text-sm text-gray-500'> <p className='text-sm text-gray-500'>
{t('customer.infoCard.regDateLabel')} {t('customer.infoCard.regDateLabel')}
</p> </p>
<p className='font-medium'> <p className='font-medium'>
{customerData.registrationDate} {new Date(data.reg_date).toLocaleDateString('en-GB')}
</p> </p>
</div> </div>
</div> </div>
@ -103,16 +107,11 @@ export function CustomerDashboard() {
<p className='text-sm text-gray-500'> <p className='text-sm text-gray-500'>
{t('customer.infoCard.cardNumberLabel')} {t('customer.infoCard.cardNumberLabel')}
</p> </p>
<p className='font-medium'>{customerData.cardNumber}</p> <p className='font-medium'>{data.cardno}</p>
</div>
<div className='mb-4 space-y-1'>
<p className='text-sm text-gray-500'>
{t('customer.infoCard.expiryDateLabel')}
</p>
<p className='font-medium'>{customerData.expiryDate}</p>
</div> </div>
</div> </div>
</div> </div>
)}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@ -3,12 +3,13 @@
import { Fuel, History, MapPin, Star, Target, Users } from 'lucide-react'; import { Fuel, History, MapPin, Star, Target, Users } from 'lucide-react';
import Image from 'next/image'; import Image from 'next/image';
// import { useTranslation } from 'next-i18next'; import { AboutUsPageData } from '@/app/api-utlities/@types/about-us';
import AnimatedCounter from '@/shared/components/animated-counter'; import AnimatedCounter from '@/shared/components/animated-counter';
import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Button } from '@/shared/shadcn-ui/button'; import { Button } from '@/shared/shadcn-ui/button';
import { Card, CardContent } from '@/shared/shadcn-ui/card'; import { Card, CardContent } from '@/shared/shadcn-ui/card';
import Container from '@/shared/shadcn-ui/conteiner';
import { CompanyTimeline } from '@/widgets/about-page/company-timeline'; import { CompanyTimeline } from '@/widgets/about-page/company-timeline';
import { StationGallery } from '@/widgets/about-page/station-gallery'; import { StationGallery } from '@/widgets/about-page/station-gallery';
@ -19,7 +20,11 @@ export const metadata = {
description: 'about.metadata.description', description: 'about.metadata.description',
}; };
export default function AboutPage() { export interface AboutPageProps {
content: AboutUsPageData;
}
export default function AboutPage({ content }: AboutPageProps) {
const { t } = useTextController(); const { t } = useTextController();
return ( return (
@ -36,8 +41,12 @@ export default function AboutPage() {
className='object-cover' className='object-cover'
priority priority
/> />
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'> <div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30 px-2'>
<div className='container mx-auto'> <div
data-aos='fade-down'
data-aos-duration='1000'
className='container mx-auto'
>
<div className='max-w-2xl space-y-4 text-white'> <div className='max-w-2xl space-y-4 text-white'>
<h1 className='text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl'> <h1 className='text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl'>
{t('about.hero.title')} {t('about.hero.title')}
@ -52,10 +61,11 @@ export default function AboutPage() {
</section> </section>
{/* Company Overview */} {/* Company Overview */}
<Container>
<section className='py-16'> <section className='py-16'>
<div className='container mx-auto'> <div className='container mx-auto'>
<div className='grid items-center gap-12 md:grid-cols-2'> <div className='grid items-center gap-12 md:grid-cols-2'>
<div> <div data-aos='fade-right'>
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'> <div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
<Fuel className='h-6 w-6 text-red-600' /> <Fuel className='h-6 w-6 text-red-600' />
</div> </div>
@ -72,7 +82,7 @@ export default function AboutPage() {
{t('about.overview.description3')} {t('about.overview.description3')}
</p> </p>
<div className='mb-6 grid grid-cols-2 gap-4'> <div className='mb-6 grid grid-cols-1 gap-4 md:grid-cols-2'>
{[0, 1, 2, 3].map((index) => ( {[0, 1, 2, 3].map((index) => (
<div key={index} className='flex items-start'> <div key={index} className='flex items-start'>
<div className='mt-1 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-red-600'> <div className='mt-1 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-red-600'>
@ -90,7 +100,10 @@ export default function AboutPage() {
))} ))}
</div> </div>
</div> </div>
<div className='relative h-[500px] overflow-hidden rounded-xl shadow-xl'> <div
data-aos='zoom-out-right'
className='relative h-[500px] overflow-hidden rounded-xl shadow-xl'
>
<Image <Image
src='/placeholder.svg?height=500&width=600&text=Главный+офис' src='/placeholder.svg?height=500&width=600&text=Главный+офис'
alt={t('about.overview.imageAlt')} alt={t('about.overview.imageAlt')}
@ -101,6 +114,7 @@ export default function AboutPage() {
</div> </div>
</div> </div>
</section> </section>
</Container>
{/* Stats Section */} {/* Stats Section */}
<section className='bg-red-600 py-16 text-white'> <section className='bg-red-600 py-16 text-white'>
@ -113,7 +127,7 @@ export default function AboutPage() {
{t('about.stats.subtitle')} {t('about.stats.subtitle')}
</p> </p>
</div> </div>
<div className='grid grid-cols-2 gap-8 text-center md:grid-cols-4'> <div className='grid grid-cols-1 gap-8 text-center sm:grid-cols-2 md:grid-cols-4'>
{[0, 1, 2, 3].map((index) => ( {[0, 1, 2, 3].map((index) => (
<div key={index} className='space-y-2'> <div key={index} className='space-y-2'>
<h3 className='text-4xl font-bold'> <h3 className='text-4xl font-bold'>
@ -139,6 +153,7 @@ export default function AboutPage() {
{/* Our History */} {/* Our History */}
<section className='py-16'> <section className='py-16'>
<div className='container mx-auto'> <div className='container mx-auto'>
<Container>
<div className='mb-12 flex flex-col items-center justify-center text-center'> <div className='mb-12 flex flex-col items-center justify-center text-center'>
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'> <div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
<History className='h-6 w-6 text-red-600' /> <History className='h-6 w-6 text-red-600' />
@ -150,12 +165,16 @@ export default function AboutPage() {
{t('about.history.subtitle')} {t('about.history.subtitle')}
</p> </p>
</div> </div>
</Container>
<CompanyTimeline /> <Container>
<CompanyTimeline timeline={content.history} />
</Container>
</div> </div>
</section> </section>
{/* Our Stations */} {/* Our Stations */}
<Container>
<section className='bg-gray-50 py-16'> <section className='bg-gray-50 py-16'>
<div className='container mx-auto'> <div className='container mx-auto'>
<div className='mb-12 flex flex-col items-center justify-center text-center'> <div className='mb-12 flex flex-col items-center justify-center text-center'>
@ -170,7 +189,7 @@ export default function AboutPage() {
</p> </p>
</div> </div>
<StationGallery /> <StationGallery stations={content.stations} />
<div className='mt-12 text-center'> <div className='mt-12 text-center'>
<p className='mx-auto mb-6 max-w-2xl text-gray-600'> <p className='mx-auto mb-6 max-w-2xl text-gray-600'>
@ -183,8 +202,10 @@ export default function AboutPage() {
</div> </div>
</div> </div>
</section> </section>
</Container>
{/* Our Values */} {/* Our Values */}
<Container>
<section className='py-16'> <section className='py-16'>
<div className='container mx-auto'> <div className='container mx-auto'>
<div className='mb-12 flex flex-col items-center justify-center text-center'> <div className='mb-12 flex flex-col items-center justify-center text-center'>
@ -199,7 +220,11 @@ export default function AboutPage() {
</p> </p>
</div> </div>
<div className='grid gap-8 md:grid-cols-3'> <div
data-aos='flip-up'
data-aos-duration='600'
className='grid gap-8 md:grid-cols-3'
>
{[0, 1, 2].map((index) => ( {[0, 1, 2].map((index) => (
<Card <Card
key={index} key={index}
@ -221,8 +246,10 @@ export default function AboutPage() {
</div> </div>
</div> </div>
</section> </section>
</Container>
{/* Our Team */} {/* Our Team */}
<Container>
<section className='bg-gray-50 py-16'> <section className='bg-gray-50 py-16'>
<div className='container mx-auto'> <div className='container mx-auto'>
<div className='mb-12 flex flex-col items-center justify-center text-center'> <div className='mb-12 flex flex-col items-center justify-center text-center'>
@ -237,35 +264,39 @@ export default function AboutPage() {
</p> </p>
</div> </div>
<div className='grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4'> <div
{[0, 1, 2, 3].map((index) => ( data-aos='flip-down'
data-aos-duration='600'
className='grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4'
>
{content.team.map((member, index) => (
<div <div
key={index} key={index}
className='overflow-hidden rounded-lg bg-white shadow-md transition-transform hover:scale-105' className='overflow-hidden rounded-lg bg-white shadow-md transition-transform hover:scale-105'
> >
<div className='relative h-64 w-full'> <div className='relative h-64 w-full'>
{member.photo && (
<Image <Image
src={`/placeholder.svg?height=300&width=300&text=${t(`about.team.members.${index}.name`)}`} src={member.photo}
alt={t(`about.team.members.${index}.name`)} alt={t(`about.team.members.${index}.name`)}
fill fill
className='object-cover' className='object-cover'
/> />
)}
</div> </div>
<div className='p-4 text-center'> <div className='p-4 text-center'>
<h3 className='text-lg font-bold'> <h3 className='text-lg font-bold'>{member.name}</h3>
{t(`about.team.members.${index}.name`)} <p className='text-gray-600'>{member.profession}</p>
</h3>
<p className='text-gray-600'>
{t(`about.team.members.${index}.position`)}
</p>
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div> </div>
</section> </section>
</Container>
{/* Testimonials */} {/* Testimonials */}
<Container>
<section className='py-16'> <section className='py-16'>
<div className='container mx-auto'> <div className='container mx-auto'>
<div className='mb-12 flex flex-col items-center justify-center text-center'> <div className='mb-12 flex flex-col items-center justify-center text-center'>
@ -280,8 +311,11 @@ export default function AboutPage() {
</p> </p>
</div> </div>
<div className='grid gap-8 md:grid-cols-3'> <div
{[0, 1, 2].map((index) => ( data-aos='zoom-out-right'
className='grid gap-8 md:grid-cols-3'
>
{content.reviews.map((review, index) => (
<Card <Card
key={index} key={index}
className='overflow-hidden transition-all hover:shadow-lg' className='overflow-hidden transition-all hover:shadow-lg'
@ -293,22 +327,21 @@ export default function AboutPage() {
.map((_, i) => ( .map((_, i) => (
<Star <Star
key={i} key={i}
className={`h-5 w-5 ${i < Number(t(`about.testimonials.items.${index}.rating`)) ? 'fill-yellow-400 text-yellow-400' : 'text-gray-300'}`} className={`h-5 w-5 ${i < Number(review.rating) ? 'fill-yellow-400 text-yellow-400' : 'text-gray-300'}`}
/> />
))} ))}
</div> </div>
<p className='mb-4 text-gray-600 italic'> <p className='mb-4 text-gray-600 italic'>
"{t(`about.testimonials.items.${index}.text`)}" "{review.review}"
</p>
<p className='font-semibold'>
{t(`about.testimonials.items.${index}.name`)}
</p> </p>
<p className='font-semibold'>{review.fullname}</p>
</CardContent> </CardContent>
</Card> </Card>
))} ))}
</div> </div>
</div> </div>
</section> </section>
</Container>
<CtaSection /> <CtaSection />
</main> </main>

View File

@ -21,6 +21,7 @@ import {
} from '@/shared/shadcn-ui/card'; } from '@/shared/shadcn-ui/card';
import { CtaSection } from '@/widgets/cta-section'; import { CtaSection } from '@/widgets/cta-section';
import Container from '@/shared/shadcn-ui/conteiner';
export const metadata = { export const metadata = {
title: 'Благотворительность | GasNetwork - Сеть заправок в Таджикистане', title: 'Благотворительность | GasNetwork - Сеть заправок в Таджикистане',
@ -46,24 +47,27 @@ export function CharityPage() {
priority priority
/> />
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'> <div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'>
<div className='container mx-auto'> <Container>
<div data-aos='fade-down' data-aos-duration='800' className='container mx-auto'>
<div className='max-w-2xl space-y-6 text-white'> <div className='max-w-2xl space-y-6 text-white'>
<div className='inline-flex items-center justify-center rounded-full bg-red-600/20 p-2'> <div className='inline-flex items-center justify-center rounded-full bg-red-600/20 p-2'>
<Heart className='size-6 text-red-500' /> <Heart className='size-6 text-red-500' />
</div> </div>
<h1 className='text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl'> <h1 className='text-3xl font-bold tracking-tight sm:text-5xl md:text-6xl'>
{t('charity.hero.title')} {t('charity.hero.title')}
</h1> </h1>
<p className='text-xl text-gray-200'> <p className='text-lg sm:text-xl text-gray-200'>
{t('charity.hero.subtitle')} {t('charity.hero.subtitle')}
</p> </p>
</div> </div>
</div> </div>
</Container>
</div> </div>
</div> </div>
</section> </section>
{/* Mission Section */} {/* Mission Section */}
<Container>
<section className='py-16'> <section className='py-16'>
<div className='container mx-auto'> <div className='container mx-auto'>
<div className='grid items-center gap-12 md:grid-cols-2'> <div className='grid items-center gap-12 md:grid-cols-2'>
@ -108,6 +112,7 @@ export function CharityPage() {
</div> </div>
</div> </div>
</section> </section>
</Container>
{/* Key Figures */} {/* Key Figures */}
<section className='bg-red-600 py-16 text-white'> <section className='bg-red-600 py-16 text-white'>
@ -136,6 +141,7 @@ export function CharityPage() {
</section> </section>
{/* Upcoming Events */} {/* Upcoming Events */}
<Container>
<section className='bg-gray-50 py-16'> <section className='bg-gray-50 py-16'>
<div className='container mx-auto'> <div className='container mx-auto'>
<div className='mb-12 text-center'> <div className='mb-12 text-center'>
@ -179,7 +185,8 @@ export function CharityPage() {
'/placeholder.svg?height=200&width=300&text=Школьные+принадлежности', '/placeholder.svg?height=200&width=300&text=Школьные+принадлежности',
}, },
].map((event, index) => ( ].map((event, index) => (
<Card key={index} className='overflow-hidden'> <Card key={index} className='overflow-hidden flex flex-col justify-between'>
<div>
<div className='relative h-48 w-full'> <div className='relative h-48 w-full'>
<Image <Image
src={event.image || '/placeholder.svg'} src={event.image || '/placeholder.svg'}
@ -189,7 +196,7 @@ export function CharityPage() {
/> />
</div> </div>
<CardHeader> <CardHeader>
<CardTitle>{event.title}</CardTitle> <CardTitle className='text-xl lg:text-2xl'>{event.title}</CardTitle>
</CardHeader> </CardHeader>
<CardContent className='space-y-4'> <CardContent className='space-y-4'>
<p className='text-gray-600'>{event.description}</p> <p className='text-gray-600'>{event.description}</p>
@ -202,6 +209,7 @@ export function CharityPage() {
{event.location} {event.location}
</div> </div>
</CardContent> </CardContent>
</div>
<CardFooter> <CardFooter>
<Button className='w-full bg-red-600 hover:bg-red-700'> <Button className='w-full bg-red-600 hover:bg-red-700'>
{t(`charity.events.button`)} {t(`charity.events.button`)}
@ -212,8 +220,10 @@ export function CharityPage() {
</div> </div>
</div> </div>
</section> </section>
</Container>
{/* How to Help */} {/* How to Help */}
<Container>
<section className='py-16'> <section className='py-16'>
<div className='container mx-auto'> <div className='container mx-auto'>
<div className='mb-12 text-center'> <div className='mb-12 text-center'>
@ -270,6 +280,7 @@ export function CharityPage() {
</div> </div>
</div> </div>
</section> </section>
</Container>
<CtaSection /> <CtaSection />
</main> </main>
</div> </div>

View File

@ -6,6 +6,7 @@ import Image from 'next/image';
import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Button } from '@/shared/shadcn-ui/button'; import { Button } from '@/shared/shadcn-ui/button';
import { Card, CardContent } from '@/shared/shadcn-ui/card'; import { Card, CardContent } from '@/shared/shadcn-ui/card';
import Container from '@/shared/shadcn-ui/conteiner';
export function CertificatesPage() { export function CertificatesPage() {
const { t } = useTextController(); const { t } = useTextController();
@ -64,7 +65,7 @@ export function CertificatesPage() {
]; ];
return ( return (
<> <Container>
<main className='container mx-auto py-10'> <main className='container mx-auto py-10'>
<div className='mb-10 text-center'> <div className='mb-10 text-center'>
<h1 className='mb-4 text-4xl font-bold'>{t('certificates.title')}</h1> <h1 className='mb-4 text-4xl font-bold'>{t('certificates.title')}</h1>
@ -76,6 +77,7 @@ export function CertificatesPage() {
<div className='grid gap-8 md:grid-cols-2 lg:grid-cols-3'> <div className='grid gap-8 md:grid-cols-2 lg:grid-cols-3'>
{certificates.map((certificate) => ( {certificates.map((certificate) => (
<Card <Card
data-aos='zoom-in'
key={certificate.id} key={certificate.id}
className='overflow-hidden transition-all duration-300 hover:shadow-lg' className='overflow-hidden transition-all duration-300 hover:shadow-lg'
> >
@ -122,6 +124,6 @@ export function CertificatesPage() {
))} ))}
</div> </div>
</main> </main>
</> </Container>
); );
} }

View File

@ -7,6 +7,7 @@ import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { BenefitsSection } from '@/widgets/clients/ui/benefits-section'; import { BenefitsSection } from '@/widgets/clients/ui/benefits-section';
import { ServicesOverviewSection } from '@/widgets/clients/ui/services-overview-section'; import { ServicesOverviewSection } from '@/widgets/clients/ui/services-overview-section';
import { CtaSection } from '@/widgets/cta-section'; import { CtaSection } from '@/widgets/cta-section';
import Container from '@/shared/shadcn-ui/conteiner';
export const metadata = { export const metadata = {
title: 'Клиентам | GasNetwork - Сеть заправок в Таджикистане', title: 'Клиентам | GasNetwork - Сеть заправок в Таджикистане',
@ -32,7 +33,8 @@ export function ClientsPage() {
priority priority
/> />
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'> <div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'>
<div className='container mx-auto'> <Container>
<div data-aos='fade-down' data-aos-duration="1000" className='container mx-auto'>
<div className='max-w-2xl space-y-4 text-white'> <div className='max-w-2xl space-y-4 text-white'>
<h1 className='text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl'> <h1 className='text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl'>
{t('clients.title')} {t('clients.title')}
@ -42,6 +44,7 @@ export function ClientsPage() {
</p> </p>
</div> </div>
</div> </div>
</Container>
</div> </div>
</div> </div>
</section> </section>

View File

@ -7,6 +7,7 @@ import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Card, CardContent } from '@/shared/shadcn-ui/card'; import { Card, CardContent } from '@/shared/shadcn-ui/card';
import { CtaSection } from '@/widgets/cta-section'; import { CtaSection } from '@/widgets/cta-section';
import Container from '@/shared/shadcn-ui/conteiner';
export const metadata = { export const metadata = {
title: 'Программа лояльности | GasNetwork - Сеть заправок в Таджикистане', title: 'Программа лояльности | GasNetwork - Сеть заправок в Таджикистане',
@ -32,7 +33,8 @@ export function LoyaltyPage() {
priority priority
/> />
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'> <div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'>
<div className='container mx-auto'> <Container>
<div data-aos='fade-down' data-aos-duration="800" className='container mx-auto'>
<div className='max-w-2xl space-y-4 text-white'> <div className='max-w-2xl space-y-4 text-white'>
<h1 className='text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl'> <h1 className='text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl'>
{t('clients.loyalty.title')} {t('clients.loyalty.title')}
@ -42,15 +44,17 @@ export function LoyaltyPage() {
</p> </p>
</div> </div>
</div> </div>
</Container>
</div> </div>
</div> </div>
</section> </section>
<Container>
{/* Program Overview */} {/* Program Overview */}
<section className='py-16'> <section className='py-16'>
<div className='container mx-auto'> <div className='container mx-auto'>
<div className='grid items-center gap-12 md:grid-cols-2'> <div className='grid items-center gap-12 md:grid-cols-2'>
<div> <div data-aos='fade-right'>
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'> <div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
<Percent className='h-6 w-6 text-red-600' /> <Percent className='h-6 w-6 text-red-600' />
</div> </div>
@ -106,7 +110,7 @@ export function LoyaltyPage() {
</div> </div>
</div> </div>
</div> </div>
<div className='relative h-[400px] overflow-hidden rounded-xl shadow-xl'> <div data-aos='fade-up' className='relative h-[400px] overflow-hidden rounded-xl shadow-xl'>
<Image <Image
src='/placeholder.svg?height=400&width=600&text=Программа+лояльности' src='/placeholder.svg?height=400&width=600&text=Программа+лояльности'
alt='Программа лояльности' alt='Программа лояльности'
@ -130,7 +134,7 @@ export function LoyaltyPage() {
</p> </p>
</div> </div>
<div className='grid gap-8 md:grid-cols-4'> <div data-aos='zoom-in-right' className='grid gap-8 md:grid-cols-4'>
<div className='text-center'> <div className='text-center'>
<div className='mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-red-600 text-2xl font-bold text-white'> <div className='mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-red-600 text-2xl font-bold text-white'>
1 1
@ -191,7 +195,7 @@ export function LoyaltyPage() {
</p> </p>
</div> </div>
<div className='grid gap-8 md:grid-cols-3'> <div data-aos='flip-down' className='grid gap-8 md:grid-cols-3'>
<Card className='overflow-hidden border-t-4 border-t-gray-400 transition-all hover:shadow-lg'> <Card className='overflow-hidden border-t-4 border-t-gray-400 transition-all hover:shadow-lg'>
<CardContent className='p-6'> <CardContent className='p-6'>
<h3 className='mb-4 text-center text-2xl font-bold'> <h3 className='mb-4 text-center text-2xl font-bold'>
@ -245,7 +249,7 @@ export function LoyaltyPage() {
<li className='flex items-center'> <li className='flex items-center'>
<Check className='mr-2 h-5 w-5 text-green-500' /> <Check className='mr-2 h-5 w-5 text-green-500' />
<span> <span>
{t('clients.loyalty.works.levels.card-1.bonus-1')} {t('clients.loyalty.works.levels.card-2.bonus-1')}
</span> </span>
</li> </li>
<li className='flex items-center'> <li className='flex items-center'>
@ -257,13 +261,13 @@ export function LoyaltyPage() {
<li className='flex items-center'> <li className='flex items-center'>
<Check className='mr-2 h-5 w-5 text-green-500' /> <Check className='mr-2 h-5 w-5 text-green-500' />
<span> <span>
{t('clients.loyalty.works.levels.card-3.bonus-3')} {t('clients.loyalty.works.levels.card-2.bonus-3')}
</span> </span>
</li> </li>
<li className='flex items-center'> <li className='flex items-center'>
<Check className='mr-2 h-5 w-5 text-green-500' /> <Check className='mr-2 h-5 w-5 text-green-500' />
<span> <span>
{t('clients.loyalty.works.levels.card-4.bonus-4')} {t('clients.loyalty.works.levels.card-2.bonus-4')}
</span> </span>
</li> </li>
</ul> </ul>
@ -320,6 +324,7 @@ export function LoyaltyPage() {
</div> </div>
</div> </div>
</section> </section>
</Container>
<CtaSection /> <CtaSection />
</main> </main>

View File

@ -1,11 +1,15 @@
'use client'; 'use client';
import { deleteCookie, getCookie } from 'cookies-next';
import { Building2, Fuel, User } from 'lucide-react'; import { Building2, Fuel, User } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import { Suspense } from 'react';
import { LoginForm } from '@/features/auth/login-form'; import { LoginForm } from '@/features/auth/login-form';
import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Button } from '@/shared/shadcn-ui/button';
import { import {
Card, Card,
CardContent, CardContent,
@ -13,6 +17,7 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from '@/shared/shadcn-ui/card'; } from '@/shared/shadcn-ui/card';
import Container from '@/shared/shadcn-ui/conteiner';
import { import {
Tabs, Tabs,
TabsContent, TabsContent,
@ -37,25 +42,24 @@ const tabs = [
}, },
]; ];
export default function LoginPage() { function LoginPageTabs() {
const { t } = useTextController(); 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 ( return (
<div className='flex min-h-screen flex-col items-center justify-center'> <Tabs
<main className='flex-1'> defaultValue={defaultTab}
<div className='container max-w-6xl py-16'> value={defaultTab}
<div className='mb-12 flex flex-col items-center text-center'> onValueChange={handleTabChange}
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'> className='w-full'
<Fuel className='h-6 w-6 text-red-600' /> >
</div>
<h1 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
{t('auth.title')}
</h1>
<p className='max-w-2xl text-gray-600'>{t('auth.description')}</p>
</div>
<div className='mx-auto max-w-lg'>
<Tabs defaultValue='bonus' className='w-full'>
<TabsList className='mb-8 flex h-fit w-full flex-col sm:flex-row'> <TabsList className='mb-8 flex h-fit w-full flex-col sm:flex-row'>
{tabs.map((tab) => { {tabs.map((tab) => {
return ( return (
@ -71,6 +75,43 @@ export default function LoginPage() {
</TabsList> </TabsList>
{tabs.map((tab) => { {tabs.map((tab) => {
const tabCookieName = `${tab.type}__token`;
const authenticationCookie = getCookie(tabCookieName);
if (authenticationCookie) {
return (
<TabsContent key={tab.label} value={tab.type}>
<Card>
<CardHeader>
<CardTitle>{t(tab.title)}</CardTitle>
</CardHeader>
<CardContent className='flex justify-center gap-2 space-y-4'>
<Link
href={
tab.type === 'bonus'
? '/customer-dashboard'
: '/corporate-dashboard'
}
>
<Button className='flex items-center'>Открыть</Button>
</Link>
<Button
variant='outline'
className='flex items-center gap-2'
onClick={() => {
deleteCookie(tabCookieName);
window.location.reload();
}}
>
Выйти
</Button>
</CardContent>
</Card>
</TabsContent>
);
}
return ( return (
<TabsContent key={tab.label} value={tab.type}> <TabsContent key={tab.label} value={tab.type}>
<Card> <Card>
@ -86,11 +127,39 @@ export default function LoginPage() {
); );
})} })}
</Tabs> </Tabs>
);
}
export default function LoginPage() {
const { t } = useTextController();
return (
<Container>
<div className='flex min-h-screen flex-col items-center justify-center'>
<main className='flex-1'>
<div className='container max-w-6xl py-16'>
<div className='mb-12 flex flex-col items-center text-center'>
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
<Fuel className='h-6 w-6 text-red-600' />
</div>
<h1 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
{t('auth.title')}
</h1>
<p className='max-w-2xl text-gray-600'>{t('auth.description')}</p>
</div>
<div data-aos='zoom-in' className='mx-auto max-w-lg'>
<Suspense>
<LoginPageTabs />
</Suspense>
<div className='mt-8 text-center text-sm text-gray-500'> <div className='mt-8 text-center text-sm text-gray-500'>
<p> <p>
{t('auth.loginIssues')}{' '} {t('auth.loginIssues')}{' '}
<Link href='/contact' className='text-red-600 hover:underline'> <Link
href='/contact'
className='text-red-600 hover:underline'
>
{t('auth.contactLink')} {t('auth.contactLink')}
</Link> </Link>
</p> </p>
@ -99,5 +168,6 @@ export default function LoginPage() {
</div> </div>
</main> </main>
</div> </div>
</Container>
); );
} }

View File

@ -1,4 +1,5 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { toast } from 'sonner';
const baseQuery = fetchBaseQuery({ const baseQuery = fetchBaseQuery({
baseUrl: process.env.NEXT_PUBLIC_API_URL, baseUrl: process.env.NEXT_PUBLIC_API_URL,
@ -15,7 +16,25 @@ export const TAGS = {
export const baseAPI = createApi({ export const baseAPI = createApi({
reducerPath: 'baseAPI', reducerPath: 'baseAPI',
baseQuery, baseQuery: async (args, api, extraOptions) => {
const result = await baseQuery(args, api, extraOptions);
if (result.error) {
switch (result.error.status) {
case 401:
toast.error('Login credentials error');
break;
case 500:
toast.error('Server error, please try later');
break;
default:
break;
}
}
return result;
},
tagTypes: Object.values(TAGS), tagTypes: Object.values(TAGS),
endpoints: () => ({}), endpoints: () => ({}),
}); });

View File

@ -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: () => ({}),
});

View File

@ -5,7 +5,7 @@ import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useEffect, useState } from 'react'; 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 { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Button } from '@/shared/shadcn-ui/button'; import { Button } from '@/shared/shadcn-ui/button';

View File

@ -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'; import { TextItem } from '@/shared/types/text.types';
export const textControlApi = baseAPI.injectEndpoints({ export const textControlApi = taylorAPI.injectEndpoints({
endpoints: (builder) => ({ endpoints: (builder) => ({
fetchText: builder.query<TextItem[], void>({ fetchText: builder.query<TextItem[], void>({
query: () => '/text', query: () => ({
url: '',
method: 'POST',
body: {
query: jsonToGraphQLQuery({ query: textsRequest }),
},
}),
transformResponse: (response: any) => {
return presentTexts(response.data._kontentSajta);
},
}), }),
}), }),
}); });
export const { useFetchTextQuery } = textControlApi;

View File

@ -0,0 +1,10 @@
"use client"
interface ContainerProps {
children: React.ReactNode
}
export default function Container({children}: ContainerProps) {
return (
<div className="container mx-auto px-2.5 py-1">{children}</div>
)
}

View File

@ -4,13 +4,16 @@ import { createWrapper } from 'next-redux-wrapper';
import { baseAPI } from '@/shared/api/base-api'; import { baseAPI } from '@/shared/api/base-api';
import { taylorAPI } from '../api/taylor-api';
import { rootReducer } from './root-reducer'; import { rootReducer } from './root-reducer';
export const makeStore = () => export const makeStore = () =>
configureStore({ configureStore({
reducer: rootReducer, reducer: rootReducer,
middleware: (getDefaultMiddleware) => middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(baseAPI.middleware), getDefaultMiddleware()
.concat(baseAPI.middleware)
.concat(taylorAPI.middleware),
devTools: process.env.NODE_ENV === 'development', devTools: process.env.NODE_ENV === 'development',
}); });

View File

@ -1,10 +1,10 @@
import { combineReducers } from '@reduxjs/toolkit'; import { combineReducers } from '@reduxjs/toolkit';
import { loginAPI } from '@/entities/auth/api/login.api';
import { baseAPI } from '@/shared/api/base-api'; import { baseAPI } from '@/shared/api/base-api';
import { taylorAPI } from '../api/taylor-api';
export const rootReducer = combineReducers({ export const rootReducer = combineReducers({
[baseAPI.reducerPath]: baseAPI.reducer, [baseAPI.reducerPath]: baseAPI.reducer,
[loginAPI.reducerPath]: loginAPI.reducer, [taylorAPI.reducerPath]: taylorAPI.reducer,
}); });

View File

@ -3,64 +3,19 @@
import { Calendar, ChevronDown, ChevronUp } from 'lucide-react'; import { Calendar, ChevronDown, ChevronUp } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { HistoryItems } from '@/app/api-utlities/@types';
import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Button } from '@/shared/shadcn-ui/button'; import { Button } from '@/shared/shadcn-ui/button';
import { Card, CardContent } from '@/shared/shadcn-ui/card'; import { Card, CardContent } from '@/shared/shadcn-ui/card';
const timelineEvents = [ export interface CompanyTimelineProps {
{ timeline: HistoryItems;
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 function CompanyTimeline() { export function CompanyTimeline({ timeline }: CompanyTimelineProps) {
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const displayEvents = expanded ? timelineEvents : timelineEvents.slice(0, 4); const displayEvents = expanded ? timeline : timeline.slice(0, 4);
const { t } = useTextController(); const { t } = useTextController();
@ -80,7 +35,7 @@ export function CompanyTimeline() {
<Calendar className='size-5 text-red-600' /> <Calendar className='size-5 text-red-600' />
</div> </div>
<h3 className='text-xl font-bold'>{event.year}</h3> <h3 className='text-xl font-bold'>{event.year}</h3>
<h4 className='mb-2 text-lg font-semibold'>{event.title}</h4> <h4 className='mb-2 text-lg font-semibold'>{event.name}</h4>
</div> </div>
<div <div
@ -108,7 +63,7 @@ export function CompanyTimeline() {
))} ))}
</div> </div>
{timelineEvents.length > 4 && ( {timeline.length > 4 && (
<div className='mt-8 text-center'> <div className='mt-8 text-center'>
<Button <Button
variant='outline' variant='outline'

View File

@ -4,6 +4,8 @@ import { ChevronLeft, ChevronRight, Maximize } from 'lucide-react';
import Image from 'next/image'; import Image from 'next/image';
import { useState } from 'react'; import { useState } from 'react';
import { Stations } from '@/app/api-utlities/@types';
import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Button } from '@/shared/shadcn-ui/button'; import { Button } from '@/shared/shadcn-ui/button';
import { import {
@ -15,55 +17,15 @@ import {
DialogTrigger, DialogTrigger,
} from '@/shared/shadcn-ui/dialog'; } from '@/shared/shadcn-ui/dialog';
interface Station { export interface StationGalleryProps {
id: number; stations: Stations;
name: string;
image: string;
location: string;
} }
const stations: Array<Station> = [ export function StationGallery({ stations }: StationGalleryProps) {
{
id: 1,
name: 'АЗС Душанбе-Центр',
image: '/placeholder.svg?height=400&width=600&text=АЗС+Душанбе-Центр',
location: 'ул. Рудаки 150, Душанбе',
},
{
id: 2,
name: 'АЗС Худжанд',
image: '/placeholder.svg?height=400&width=600&text=АЗС+Худжанд',
location: 'ул. Ленина 45, Худжанд',
},
{
id: 3,
name: 'АЗС Куляб',
image: '/placeholder.svg?height=400&width=600&text=АЗС+Куляб',
location: 'ул. Сомони 78, Куляб',
},
{
id: 4,
name: 'АЗС Бохтар',
image: '/placeholder.svg?height=400&width=600&text=АЗС+Бохтар',
location: 'ул. Айни 23, Бохтар',
},
{
id: 5,
name: 'АЗС Хорог',
image: '/placeholder.svg?height=400&width=600&text=АЗС+Хорог',
location: 'ул. Горная 12, Хорог',
},
{
id: 6,
name: 'АЗС Истаравшан',
image: '/placeholder.svg?height=400&width=600&text=АЗС+Истаравшан',
location: 'ул. Исмоили Сомони 34, Истаравшан',
},
];
export function StationGallery() {
const [currentImage, setCurrentImage] = useState(0); const [currentImage, setCurrentImage] = useState(0);
const [selectedStation, setSelectedStation] = useState<Station | null>(null); const [selectedStation, setSelectedStation] = useState<Stations[0] | null>(
null,
);
const nextImage = () => { const nextImage = () => {
setCurrentImage((prev) => (prev === stations.length - 1 ? 0 : prev + 1)); setCurrentImage((prev) => (prev === stations.length - 1 ? 0 : prev + 1));
@ -76,7 +38,7 @@ export function StationGallery() {
const { t } = useTextController(); const { t } = useTextController();
return ( return (
<div className='space-y-8'> <div className='space-y-8 px-2'>
<div className='relative h-[400px] overflow-hidden rounded-xl shadow-xl md:h-[500px]'> <div className='relative h-[400px] overflow-hidden rounded-xl shadow-xl md:h-[500px]'>
<Image <Image
src={stations[currentImage].image || '/placeholder.svg'} src={stations[currentImage].image || '/placeholder.svg'}
@ -86,7 +48,7 @@ export function StationGallery() {
/> />
<div className='absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-black/70 to-transparent p-6 text-white'> <div className='absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-black/70 to-transparent p-6 text-white'>
<h3 className='text-2xl font-bold'>{stations[currentImage].name}</h3> <h3 className='text-2xl font-bold'>{stations[currentImage].name}</h3>
<p className='text-white/80'>{stations[currentImage].location}</p> <p className='text-white/80'>{stations[currentImage].address}</p>
</div> </div>
<Button <Button
@ -125,7 +87,7 @@ export function StationGallery() {
<DialogHeader> <DialogHeader>
<DialogTitle>{stations[currentImage].name}</DialogTitle> <DialogTitle>{stations[currentImage].name}</DialogTitle>
<DialogDescription> <DialogDescription>
{stations[currentImage].location} {stations[currentImage].address}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className='relative h-[60vh] w-full'> <div className='relative h-[60vh] w-full'>

View File

@ -4,6 +4,7 @@ import { Percent } from 'lucide-react';
import Image from 'next/image'; import Image from 'next/image';
import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { useTextController } from '@/shared/language/hooks/use-text-controller';
import Container from '@/shared/shadcn-ui/conteiner';
interface Benefit { interface Benefit {
title: string; title: string;
@ -33,10 +34,11 @@ export const BenefitsSection = () => {
const { t } = useTextController(); const { t } = useTextController();
return ( return (
<Container>
<section className='bg-gray-50 py-16'> <section className='bg-gray-50 py-16'>
<div className='container mx-auto'> <div className='container mx-auto'>
<div className='grid items-center gap-12 md:grid-cols-2'> <div className='grid items-center gap-12 md:grid-cols-2'>
<div className='order-2 md:order-1'> <div data-aos='fade-right' data-aos-duration='4000' className='order-2 md:order-1'>
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'> <div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
<Percent className='h-6 w-6 text-red-600' /> <Percent className='h-6 w-6 text-red-600' />
</div> </div>
@ -63,7 +65,7 @@ export const BenefitsSection = () => {
})} })}
</div> </div>
</div> </div>
<div className='relative order-1 h-[400px] overflow-hidden rounded-xl shadow-xl md:order-2'> <div data-aos='fade-up' className='relative order-1 h-[400px] overflow-hidden rounded-xl shadow-xl md:order-2'>
<Image <Image
src='/placeholder.svg?height=400&width=600&text=Преимущества+для+клиентов' src='/placeholder.svg?height=400&width=600&text=Преимущества+для+клиентов'
alt='Преимущества для клиентов' alt='Преимущества для клиентов'
@ -74,5 +76,6 @@ export const BenefitsSection = () => {
</div> </div>
</div> </div>
</section> </section>
</Container>
); );
}; };

View File

@ -11,6 +11,7 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from '@/shared/shadcn-ui/card'; } from '@/shared/shadcn-ui/card';
import Container from '@/shared/shadcn-ui/conteiner';
interface ServiceOverview { interface ServiceOverview {
title: string; title: string;
@ -49,6 +50,7 @@ export const ServicesOverviewSection = () => {
const { t } = useTextController(); const { t } = useTextController();
return ( return (
<Container>
<section className='py-16'> <section className='py-16'>
<div className='container mx-auto'> <div className='container mx-auto'>
<div className='mb-12 text-center'> <div className='mb-12 text-center'>
@ -60,7 +62,7 @@ export const ServicesOverviewSection = () => {
</p> </p>
</div> </div>
<div className='grid gap-3 md:grid-cols-2 md:gap-6 lg:grid-cols-3'> <div data-aos='flip-up' data-aos-duration='600' className='grid gap-3 md:grid-cols-2 md:gap-6 lg:grid-cols-3'>
{servicesOverview.map(({ description, Icon, contentText, title }) => { {servicesOverview.map(({ description, Icon, contentText, title }) => {
return ( return (
<Card <Card
@ -83,5 +85,6 @@ export const ServicesOverviewSection = () => {
</div> </div>
</div> </div>
</section> </section>
</Container>
); );
}; };

View File

@ -11,7 +11,8 @@ export const Footer = () => {
return ( return (
<footer className='bg-gray-900 px-4 py-12 text-white'> <footer className='bg-gray-900 px-4 py-12 text-white'>
<div className='containe mx-autor'> <div className='containe mx-autor'>
<div className='grid grid-cols-1 gap-8 md:grid-cols-3'> <div className='grid grid-cols-1 gap-8 md:gap-4 md:grid-cols-3'>
<div className='flex md:justify-center md:items-center'>
<div> <div>
<div className='mb-4 flex items-center gap-2'> <div className='mb-4 flex items-center gap-2'>
<Fuel className='h-6 w-6 text-red-500' /> <Fuel className='h-6 w-6 text-red-500' />
@ -59,6 +60,8 @@ export const Footer = () => {
</a> </a>
</div> </div>
</div> </div>
</div>
<div className='flex md:justify-center md:items-center'>
<div> <div>
<h3 className='mb-4 text-lg font-semibold'> <h3 className='mb-4 text-lg font-semibold'>
{t('common.footer.contacts')} {t('common.footer.contacts')}
@ -78,6 +81,8 @@ export const Footer = () => {
</div> </div>
</div> </div>
</div> </div>
</div>
<div className='flex md:justify-center md:items-center'>
<div> <div>
<h3 className='mb-4 text-lg font-semibold'> <h3 className='mb-4 text-lg font-semibold'>
{t('common.footer.navigation')} {t('common.footer.navigation')}
@ -120,6 +125,7 @@ export const Footer = () => {
</ul> </ul>
</div> </div>
</div> </div>
</div>
<div className='mt-8 border-t border-gray-800 pt-8 text-center text-gray-400'> <div className='mt-8 border-t border-gray-800 pt-8 text-center text-gray-400'>
<p> <p>
&copy; {new Date().getFullYear()} {t('common.name')}.{' '} &copy; {new Date().getFullYear()} {t('common.name')}.{' '}

View File

@ -2,7 +2,7 @@
import { MapPin } from 'lucide-react'; import { MapPin } from 'lucide-react';
import { Stations } from '@/app/api-utlities/@types/main'; import { Stations } from '@/app/api-utlities/@types';
import { GasStationMap } from '@/features/map'; import { GasStationMap } from '@/features/map';

View File

@ -4,7 +4,7 @@ import { Handshake } from 'lucide-react';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { Partners } from '@/app/api-utlities/@types/main'; import { Partners } from '@/app/api-utlities/@types';
import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Button } from '@/shared/shadcn-ui/button'; import { Button } from '@/shared/shadcn-ui/button';

View File

@ -2,7 +2,7 @@
import { Gift } from 'lucide-react'; import { Gift } from 'lucide-react';
import { Discounts } from '@/app/api-utlities/@types/main'; import { Discounts } from '@/app/api-utlities/@types';
import PromotionSlider from '@/shared/components/promotion-slider'; import PromotionSlider from '@/shared/components/promotion-slider';
import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { useTextController } from '@/shared/language/hooks/use-text-controller';

View File

@ -3,7 +3,9 @@
import { format, subMonths } from 'date-fns'; import { format, subMonths } from 'date-fns';
import { ru } from 'date-fns/locale'; import { ru } from 'date-fns/locale';
import { CalendarIcon } from 'lucide-react'; import { CalendarIcon } from 'lucide-react';
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { useFetchBonusTransactionsQuery } from '@/entities/bonus/api/bonus.api';
import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Button } from '@/shared/shadcn-ui/button'; import { Button } from '@/shared/shadcn-ui/button';
@ -23,85 +25,30 @@ import {
TableRow, TableRow,
} from '@/shared/shadcn-ui/table'; } from '@/shared/shadcn-ui/table';
// Sample customer data
const customerData = {
firstName: 'Алишер',
lastName: 'Рахмонов',
passportNumber: 'A12345678',
bonusPoints: 1250,
cardNumber: '5678-9012-3456-7890',
expiryDate: '12/2025',
registrationDate: '15.06.2020',
};
// Sample transaction data
const generateTransactions = () => {
const stations = [
'АЗС Душанбе-Центр',
'АЗС Душанбе-Запад',
'АЗС Душанбе-Восток',
'АЗС Худжанд',
'АЗС Куляб',
];
const products = [
{ name: 'ДТ', price: 8.5 },
{ name: 'АИ-92', price: 9.2 },
{ name: 'АИ-95', price: 10.5 },
{ name: 'Z-100 Power', price: 11.8 },
{ name: 'Пропан', price: 6.3 },
];
const transactions = [];
// Generate 50 random transactions over the last 6 months
for (let i = 0; i < 50; i++) {
const date = subMonths(new Date(), Math.random() * 6);
const station = stations[Math.floor(Math.random() * stations.length)];
const product = products[Math.floor(Math.random() * products.length)];
const quantity = Math.floor(Math.random() * 40) + 10; // 10-50 liters
const cost = product.price;
const total = quantity * cost;
transactions.push({
id: i + 1,
date,
station,
product: product.name,
quantity,
cost,
total,
});
}
// Sort by date (newest first)
return transactions.sort((a, b) => b.date.getTime() - a.date.getTime());
};
const transactions = generateTransactions();
export const TransactionsTable = () => { export const TransactionsTable = () => {
const [startDate, setStartDate] = useState<Date | undefined>( const [startDate, setStartDate] = useState<Date>(subMonths(new Date(), 1));
subMonths(new Date(), 1), const [endDate, setEndDate] = useState<Date>(new Date());
);
const [endDate, setEndDate] = useState<Date | undefined>(new Date()); const { data, refetch } = useFetchBonusTransactionsQuery({
const [filteredTransactions, setFilteredTransactions] = limit: 100,
useState(transactions); page: 1,
start_date: format(startDate, 'yyyy-MM-dd'),
end_date: format(endDate, 'yyyy-MM-dd'),
});
// Filter transactions by date range // Filter transactions by date range
const filterTransactions = () => { const filterTransactions = () => {
if (!startDate || !endDate) return; if (!startDate || !endDate) return;
const filtered = transactions.filter((transaction) => { refetch();
const transactionDate = new Date(transaction.date);
return transactionDate >= startDate && transactionDate <= endDate;
});
setFilteredTransactions(filtered);
}; };
const { t } = useTextController(); const { t } = useTextController();
useEffect(() => {}, [startDate, endDate]);
if (!data) return null;
return ( return (
<div className='space-y-6'> <div className='space-y-6'>
<div className='flex flex-col items-start justify-between gap-4 md:flex-row md:items-center'> <div className='flex flex-col items-start justify-between gap-4 md:flex-row md:items-center'>
@ -200,22 +147,22 @@ export const TransactionsTable = () => {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{filteredTransactions.length > 0 ? ( {data.transactions.length > 0 ? (
filteredTransactions.map((transaction) => ( data.transactions.map((transaction) => (
<TableRow key={transaction.id}> <TableRow key={transaction.id}>
<TableCell> <TableCell>
{format(transaction.date, 'dd.MM.yyyy')} {format(new Date(transaction.date_create), 'dd.MM.yyyy')}
</TableCell> </TableCell>
<TableCell>{transaction.station}</TableCell> <TableCell>{transaction.station}</TableCell>
<TableCell>{transaction.product}</TableCell> <TableCell>{transaction.product_name}</TableCell>
<TableCell className='text-right'> <TableCell className='text-right'>
{transaction.quantity} {transaction.price_real}
</TableCell> </TableCell>
<TableCell className='text-right'> <TableCell className='text-right'>
{transaction.cost.toFixed(2)} {t('corporate.currency')} {transaction.amount} {t('corporate.currency')}
</TableCell> </TableCell>
<TableCell className='text-right font-medium'> <TableCell className='text-right font-medium'>
{transaction.total.toFixed(2)} {t('corporate.currency')} {transaction.sum_real} {t('corporate.currency')}
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))

View File

@ -2,7 +2,7 @@
import { Briefcase } from 'lucide-react'; import { Briefcase } from 'lucide-react';
import { Jobs } from '@/app/api-utlities/@types/main'; import { Jobs } from '@/app/api-utlities/@types';
import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { cn } from '@/shared/lib/utils'; import { cn } from '@/shared/lib/utils';