Compare commits
No commits in common. "dev" and "add-pages" have entirely different histories.
32
Dockerfile
32
Dockerfile
@ -1,32 +0,0 @@
|
|||||||
FROM node:20-alpine AS builder
|
|
||||||
|
|
||||||
# Enable corepack and install pnpm
|
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
|
||||||
|
|
||||||
# Disable interactive prompts
|
|
||||||
ENV CI=true
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy package.json and pnpm-lock.yaml first for caching
|
|
||||||
COPY package.json pnpm-lock.yaml ./
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
# Copy the rest of the files
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build the application
|
|
||||||
RUN pnpm build
|
|
||||||
|
|
||||||
FROM node:20-alpine AS runner
|
|
||||||
|
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=builder /app ./
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
CMD ["pnpm", "start"]
|
|
||||||
@ -8,11 +8,6 @@ const nextConfig: NextConfig = {
|
|||||||
hostname: 'media.bambooapp.ai',
|
hostname: 'media.bambooapp.ai',
|
||||||
pathname: '/files/**',
|
pathname: '/files/**',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
protocol: 'https',
|
|
||||||
hostname: 'taylordb.ai',
|
|
||||||
pathname: '/media/**',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
25
package.json
25
package.json
@ -7,7 +7,7 @@
|
|||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint ."
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
@ -23,31 +23,27 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.0",
|
"@radix-ui/react-slot": "^1.2.0",
|
||||||
"@radix-ui/react-tabs": "^1.1.8",
|
"@radix-ui/react-tabs": "^1.1.8",
|
||||||
"@radix-ui/react-toast": "^1.2.11",
|
"@radix-ui/react-toast": "^1.2.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.6",
|
|
||||||
"@reduxjs/toolkit": "^2.7.0",
|
"@reduxjs/toolkit": "^2.7.0",
|
||||||
"@taylordb/query-builder": "^0.10.1",
|
|
||||||
"aos": "^2.3.4",
|
"aos": "^2.3.4",
|
||||||
"axios": "^1.9.0",
|
"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",
|
"cookies-next": "^5.1.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"date-fns-tz": "^3.2.0",
|
|
||||||
"embla-carousel-autoplay": "^8.6.0",
|
"embla-carousel-autoplay": "^8.6.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"json-to-graphql-query": "^2.3.0",
|
"json-to-graphql-query": "^2.3.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.501.0",
|
"lucide-react": "^0.501.0",
|
||||||
"next": "16.0.10",
|
"next": "15.3.1",
|
||||||
"next-redux-wrapper": "^8.1.0",
|
"next-redux-wrapper": "^8.1.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "19.2.3",
|
"react": "^19.0.0",
|
||||||
"react-day-picker": "8.10.1",
|
"react-day-picker": "8.10.1",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.56.1",
|
"react-hook-form": "^7.56.1",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"swiper": "^11.2.6",
|
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tailwindcss-animated": "^2.0.0",
|
"tailwindcss-animated": "^2.0.0",
|
||||||
@ -55,6 +51,7 @@
|
|||||||
"zod": "^3.24.3"
|
"zod": "^3.24.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3",
|
||||||
"@eslint/js": "^9.25.0",
|
"@eslint/js": "^9.25.0",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||||
@ -64,11 +61,11 @@
|
|||||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/node": "^20.17.30",
|
"@types/node": "^20.17.30",
|
||||||
"@types/react": "19.2.7",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "^19",
|
||||||
"@typescript-eslint/parser": "^8.30.1",
|
"@typescript-eslint/parser": "^8.30.1",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.0.10",
|
"eslint-config-next": "15.3.1",
|
||||||
"eslint-config-prettier": "^10.1.2",
|
"eslint-config-prettier": "^10.1.2",
|
||||||
"eslint-plugin-import-x": "^4.10.6",
|
"eslint-plugin-import-x": "^4.10.6",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
@ -80,11 +77,5 @@
|
|||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"typescript-eslint": "^8.30.1"
|
"typescript-eslint": "^8.30.1"
|
||||||
},
|
|
||||||
"pnpm": {
|
|
||||||
"overrides": {
|
|
||||||
"@types/react": "19.2.7",
|
|
||||||
"@types/react-dom": "19.2.3"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2443
pnpm-lock.yaml
generated
2443
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 419 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 112 KiB |
@ -1 +0,0 @@
|
|||||||
<svg fill="none" viewBox="0 0 142 80" xmlns="http://www.w3.org/2000/svg"><title></title><rect fill="#F2F4F5" height="80" stroke="#E3E4E6" width="142"></rect><path d="M84.8156 47.5402L77.8157 33.9407C77.5479 33.4222 77.0282 33.0703 76.4349 33.0108C75.8434 32.9445 75.2572 33.1876 74.8844 33.6381L68.8854 40.9206L64.9707 38.386C64.5717 38.1293 64.08 38.0392 63.6162 38.1412C63.149 38.2432 62.7447 38.5271 62.4997 38.9249L57.2497 47.4246C56.9242 47.9499 56.9172 48.6026 57.227 49.1381C57.5367 49.6719 58.1195 50 58.7495 50H83.2494C83.8567 50 84.4184 49.694 84.7386 49.1925C85.0589 48.691 85.0851 48.0655 84.8156 47.5402Z" fill="#CACBCC"></path><path d="M67.5 33C69.433 33 71 31.433 71 29.5C71 27.567 69.433 26 67.5 26C65.567 26 64 27.567 64 29.5C64 31.433 65.567 33 67.5 33Z" fill="#CACBCC"></path><defs><clipPath id="clip0"><rect fill="white" height="65" width="127"></rect></clipPath></defs></svg>
|
|
||||||
|
Before Width: | Height: | Size: 896 B |
@ -1,15 +1,15 @@
|
|||||||
import AboutPage from '@/pages-templates/about';
|
import AboutPage from '@/pages-templates/about';
|
||||||
|
|
||||||
import { fetchAboutUsPageContent } from '@/features/pages/services/pages.service';
|
import { mainPageApi } from '@/features/pages/api/pages.api';
|
||||||
|
|
||||||
export const metadata = {
|
import { makeStore } from '@/shared/store';
|
||||||
title: 'О нас',
|
|
||||||
description:
|
|
||||||
'Узнайте больше о нашей компании, истории и ценностях. Качественное топливо и отличный сервис.',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function About() {
|
export default async function About() {
|
||||||
const data = await fetchAboutUsPageContent();
|
const store = makeStore();
|
||||||
|
|
||||||
|
const { data } = await store.dispatch(
|
||||||
|
mainPageApi.endpoints.fetchAboutUsPageContent.initiate(),
|
||||||
|
);
|
||||||
|
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
|
|||||||
8
src/app/api-utlities/@types/about-us.ts
Normal file
8
src/app/api-utlities/@types/about-us.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { HistoryItems, Reviews, Stations, TeamMembers } from '.';
|
||||||
|
|
||||||
|
export type AboutUsPageData = {
|
||||||
|
team: TeamMembers;
|
||||||
|
history: HistoryItems;
|
||||||
|
stations: Stations;
|
||||||
|
reviews: Reviews;
|
||||||
|
};
|
||||||
@ -1,6 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
presentCertificates,
|
|
||||||
presentCharities,
|
|
||||||
presentDiscounts,
|
presentDiscounts,
|
||||||
presentHistoryItems,
|
presentHistoryItems,
|
||||||
presentJobs,
|
presentJobs,
|
||||||
@ -22,90 +20,67 @@ export interface Select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Discount = Root<{
|
export type Discount = Root<{
|
||||||
zagolovok: string;
|
_name: string;
|
||||||
opisanie: string;
|
_opisanie: string;
|
||||||
do: string;
|
_do: string;
|
||||||
foto: Image[];
|
_foto: Image[];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type Job = Root<{
|
export type Job = Root<{
|
||||||
id: number;
|
id: number;
|
||||||
zagolovok: string;
|
_name: string;
|
||||||
tip: Select[];
|
_type: Select[];
|
||||||
lokaciya: Select[];
|
_localtio: Select[];
|
||||||
tegi: Select[];
|
_tags: Select[];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type Partner = Root<{
|
export type Partner = Root<{
|
||||||
id: number;
|
id: number;
|
||||||
nazvanie: string;
|
_name: string;
|
||||||
izobrozhenie: Image[];
|
_image: Image[];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type Station = Root<{
|
export type Station = Root<{
|
||||||
imya: string;
|
_name: string;
|
||||||
opisanie: string;
|
_opisanie: string;
|
||||||
adress: string;
|
_adress: string;
|
||||||
chasyRaboty: Select[];
|
_chasyRaboty: Select;
|
||||||
lat: number;
|
_lat: number;
|
||||||
long: number;
|
_long: number;
|
||||||
avtomojka: boolean;
|
_avtomojka: boolean;
|
||||||
ai92: boolean;
|
_dtCopy: boolean;
|
||||||
ai95: boolean;
|
_ai92Copy: boolean;
|
||||||
z100: boolean;
|
_ai95Copy: boolean;
|
||||||
propan: boolean;
|
_z100Copy: boolean;
|
||||||
zaryadnayaStanciya: boolean;
|
_propanCopy: boolean;
|
||||||
dt: boolean;
|
_zaryadnayaStanci: boolean;
|
||||||
miniMarket: boolean;
|
_miniMarketCop: boolean;
|
||||||
tualet: boolean;
|
_region: Select[];
|
||||||
region: Select[];
|
_foto: Image[];
|
||||||
foto: Image[];
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type TextResponse = Root<{
|
export type TextResponse = Root<{
|
||||||
klyuchNeIzmenyat: string;
|
_name: string;
|
||||||
znachenie: string | null;
|
_znachenie: string | null;
|
||||||
}>;
|
|
||||||
|
|
||||||
export type MediaResponse = Root<{
|
|
||||||
mestopolozheniya: string;
|
|
||||||
foto: Image[];
|
|
||||||
klyuchNeIzmenyat: string;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type Team = Root<{
|
export type Team = Root<{
|
||||||
foto: Image[];
|
_foto: Image[];
|
||||||
zvanie: string;
|
_zvanie: string;
|
||||||
polnoeImya: string;
|
_name: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type History = Root<{
|
export type History = Root<{
|
||||||
zagolovok: string;
|
_name: string;
|
||||||
god: string;
|
_god: string;
|
||||||
opisanie: string;
|
_opisanie: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type Review = Root<{
|
export type Review = Root<{
|
||||||
id: number;
|
id: number;
|
||||||
polnoeImya: string;
|
_name: string;
|
||||||
otzyv: string;
|
_otzyv: string;
|
||||||
rejting: number;
|
_rejting: number;
|
||||||
}>;
|
|
||||||
|
|
||||||
export type Charity = Root<{
|
|
||||||
zagolovok: string;
|
|
||||||
opisanie: string;
|
|
||||||
data: string;
|
|
||||||
lokaciya: string;
|
|
||||||
foto: Image[];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type Certificate = Root<{
|
|
||||||
nazvanie: string;
|
|
||||||
opisanie: string;
|
|
||||||
dataVydachi: string;
|
|
||||||
dejstvitelenDo: string;
|
|
||||||
foto: Image[];
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type TeamMembers = ReturnType<typeof presentTeamMembers>;
|
export type TeamMembers = ReturnType<typeof presentTeamMembers>;
|
||||||
@ -115,5 +90,3 @@ export type Partners = ReturnType<typeof presentPartners>;
|
|||||||
export type Jobs = ReturnType<typeof presentJobs>;
|
export type Jobs = ReturnType<typeof presentJobs>;
|
||||||
export type Discounts = ReturnType<typeof presentDiscounts>;
|
export type Discounts = ReturnType<typeof presentDiscounts>;
|
||||||
export type Reviews = ReturnType<typeof presentReviews>;
|
export type Reviews = ReturnType<typeof presentReviews>;
|
||||||
export type Charities = ReturnType<typeof presentCharities>;
|
|
||||||
export type Certificates = ReturnType<typeof presentCertificates>;
|
|
||||||
|
|||||||
8
src/app/api-utlities/@types/main.ts
Normal file
8
src/app/api-utlities/@types/main.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Discounts, Jobs, Partners, Stations } from '.';
|
||||||
|
|
||||||
|
export type MainPageData = {
|
||||||
|
discounts: Discounts;
|
||||||
|
jobs: Jobs;
|
||||||
|
partners: Partners;
|
||||||
|
stations: Stations;
|
||||||
|
};
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import {
|
|
||||||
Certificates,
|
|
||||||
Charities,
|
|
||||||
Discounts,
|
|
||||||
HistoryItems,
|
|
||||||
Jobs,
|
|
||||||
Partners,
|
|
||||||
Reviews,
|
|
||||||
Stations,
|
|
||||||
TeamMembers,
|
|
||||||
} from '.';
|
|
||||||
|
|
||||||
export type AboutUsPageData = {
|
|
||||||
team: TeamMembers;
|
|
||||||
history: HistoryItems;
|
|
||||||
stations: Stations;
|
|
||||||
reviews: Reviews;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MainPageData = {
|
|
||||||
discounts: Discounts;
|
|
||||||
jobs: Jobs;
|
|
||||||
partners: Partners;
|
|
||||||
stations: Stations;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CharityPageData = {
|
|
||||||
charities: Charities;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CertificatesPageData = {
|
|
||||||
certificates: Certificates;
|
|
||||||
};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export class AuthorizationError extends Error {}
|
|
||||||
@ -1,13 +1,10 @@
|
|||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Certificate,
|
|
||||||
Charity,
|
|
||||||
Discount,
|
Discount,
|
||||||
History,
|
History,
|
||||||
Image,
|
Image,
|
||||||
Job,
|
Job,
|
||||||
MediaResponse,
|
|
||||||
Partner,
|
Partner,
|
||||||
Review,
|
Review,
|
||||||
Select,
|
Select,
|
||||||
@ -25,102 +22,73 @@ export const presentSelect = (selectItems: Select[]) =>
|
|||||||
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,
|
||||||
name: record.nazvanie,
|
name: record._name,
|
||||||
poster: presentImage(record.izobrozhenie),
|
poster: presentImage(record._image),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const presentJobs = (jobs: Job) =>
|
export const presentJobs = (jobs: Job) =>
|
||||||
jobs.records.map((job, index) => ({
|
jobs.records.map((job, index) => ({
|
||||||
id: index + 1,
|
id: index + 1,
|
||||||
name: job.zagolovok,
|
name: job._name,
|
||||||
tags: job.tegi.map((tag) => tag.name),
|
tags: job._tags.map((tag) => tag.name),
|
||||||
location: presentSelect(job.lokaciya),
|
location: presentSelect(job._localtio),
|
||||||
type: presentSelect(job.tip),
|
type: presentSelect(job._type),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const presentTeamMembers = (members: Team) =>
|
export const presentTeamMembers = (members: Team) =>
|
||||||
members.records.map((member) => ({
|
members.records.map((member) => ({
|
||||||
name: member.polnoeImya,
|
name: member._name,
|
||||||
photo: presentImage(member.foto),
|
photo: presentImage(member._foto),
|
||||||
profession: member.zvanie,
|
profession: member._zvanie,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const presentHistoryItems = (historyItems: History) =>
|
export const presentHistoryItems = (historyItems: History) =>
|
||||||
historyItems.records.map((item) => ({
|
historyItems.records.map((item) => ({
|
||||||
name: item.zagolovok,
|
name: item._name,
|
||||||
year: item.god,
|
year: item._god,
|
||||||
description: item.opisanie,
|
description: item._opisanie,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const presentDiscounts = (discounts: Discount) =>
|
export const presentDiscounts = (discounts: Discount) =>
|
||||||
discounts.records.map((discount, index) => ({
|
discounts.records.map((discount, index) => ({
|
||||||
id: index + 1,
|
id: index + 1,
|
||||||
name: discount.zagolovok,
|
name: discount._name,
|
||||||
description: discount.opisanie,
|
description: discount._opisanie,
|
||||||
expiresAt: discount.do,
|
expiresAt: discount._do,
|
||||||
image: presentImage(discount.foto),
|
image: presentImage(discount._foto),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const presentStations = (stations: Station) =>
|
export const presentStations = (stations: Station) =>
|
||||||
stations.records.map((station, index) => ({
|
stations.records.map((station, index) => ({
|
||||||
id: index + 1,
|
id: index + 1,
|
||||||
name: station.imya,
|
name: station._name,
|
||||||
description: station.opisanie,
|
description: station._opisanie,
|
||||||
address: station.adress,
|
address: station._adress,
|
||||||
workingHours: presentSelect(station.chasyRaboty),
|
workingHours: station._chasyRaboty?.name || null,
|
||||||
latitude: station.lat,
|
latitude: station._lat,
|
||||||
longitude: station.long,
|
longitude: station._long,
|
||||||
carWash: station.avtomojka || false,
|
carWash: station._avtomojka || false,
|
||||||
ai92: station.ai92 || false,
|
ai92: station._dtCopy || false,
|
||||||
ai95: station.ai95 || false,
|
ai95: station._ai92Copy || false,
|
||||||
dt: station.dt || false,
|
z100: station._ai95Copy || false,
|
||||||
z100: station.z100 || false,
|
propan: station._z100Copy || false,
|
||||||
propan: station.propan || false,
|
electricCharge: station._propanCopy || false,
|
||||||
electricCharge: station.zaryadnayaStanciya || false,
|
miniMarket: station._zaryadnayaStanci || false,
|
||||||
miniMarket: station.miniMarket || false,
|
toilet: station._miniMarketCop || false,
|
||||||
toilet: station.tualet || false,
|
region: presentSelect(station._region),
|
||||||
region: presentSelect(station.region),
|
image: presentImage(station._foto),
|
||||||
image: presentImage(station.foto),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const presentTexts = (texts: TextResponse) =>
|
export const presentTexts = (texts: TextResponse) =>
|
||||||
texts.records.map((item) => ({
|
texts.records.map((item) => ({
|
||||||
key: item.klyuchNeIzmenyat,
|
key: item._name,
|
||||||
value: item.znachenie,
|
value: item._znachenie,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const presentMedia = (media: MediaResponse) => {
|
|
||||||
return media.records.map((record) => ({
|
|
||||||
key: record.klyuchNeIzmenyat,
|
|
||||||
name: record.mestopolozheniya,
|
|
||||||
photo: presentImage(record.foto),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const presentReviews = (reviews: Review) =>
|
export const presentReviews = (reviews: Review) =>
|
||||||
reviews.records.map((review) => ({
|
reviews.records.map((review) => ({
|
||||||
id: review.id,
|
id: review.id,
|
||||||
fullname: review.polnoeImya,
|
fullname: review._name,
|
||||||
review: review.otzyv,
|
review: review._otzyv,
|
||||||
rating: review.rejting,
|
rating: review._rejting,
|
||||||
}));
|
|
||||||
|
|
||||||
export const presentCharities = (charities: Charity) =>
|
|
||||||
charities.records.map((charity, index) => ({
|
|
||||||
id: index + 1,
|
|
||||||
name: charity.zagolovok,
|
|
||||||
description: charity.opisanie,
|
|
||||||
date: charity.data,
|
|
||||||
location: charity.lokaciya,
|
|
||||||
image: presentImage(charity.foto),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const presentCertificates = (certificates: Certificate) =>
|
|
||||||
certificates.records.map((certificate, index) => ({
|
|
||||||
id: index + 1,
|
|
||||||
name: certificate.nazvanie,
|
|
||||||
description: certificate.opisanie,
|
|
||||||
issuedAt: certificate.dataVydachi,
|
|
||||||
validUntil: certificate.dejstvitelenDo,
|
|
||||||
image: presentImage(certificate.foto),
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -1,238 +0,0 @@
|
|||||||
import { isEmpty } from 'lodash';
|
|
||||||
|
|
||||||
import {
|
|
||||||
AttachmentColumnValue,
|
|
||||||
TableRaws,
|
|
||||||
} from '@/shared/types/database.types';
|
|
||||||
|
|
||||||
// Helper to get image URL from Attachment array
|
|
||||||
export const getAttachmentUrl = (
|
|
||||||
attachments: AttachmentColumnValue[] | undefined | null,
|
|
||||||
): string | null => {
|
|
||||||
if (isEmpty(attachments) || !attachments?.[0]) return null;
|
|
||||||
const attachment = attachments[0];
|
|
||||||
return `${process.env.TAYLOR_MEDIA_URL}/${attachment.url}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper to get link select name (link to selectTable returns object with name)
|
|
||||||
export const getLinkSelectName = (
|
|
||||||
link: { name: string } | undefined | null,
|
|
||||||
): string | null => {
|
|
||||||
return link?.name || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper to get multiple link select names (for arrays of links)
|
|
||||||
export const getLinkSelectNames = (
|
|
||||||
links: Array<{ name: string }> | undefined | null,
|
|
||||||
): string[] => {
|
|
||||||
if (!links || isEmpty(links)) return [];
|
|
||||||
return links.map((link) => link.name);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Presenters for TaylorDB query builder results (direct array format, no wrapper)
|
|
||||||
|
|
||||||
export const presentPartnersFromTaylor = (
|
|
||||||
partners: TableRaws<'partnyory'>[],
|
|
||||||
): Array<{ id: number; name: string; poster: string | null }> => {
|
|
||||||
return partners.map((partner, index) => ({
|
|
||||||
id: index + 1,
|
|
||||||
name: partner.nazvanie || '',
|
|
||||||
poster: getAttachmentUrl(partner.izobrozhenie),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const presentJobsFromTaylor = (
|
|
||||||
jobs: TableRaws<'vakansii'>[],
|
|
||||||
): Array<{
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
tags: string[];
|
|
||||||
location: string | null;
|
|
||||||
type: string | null;
|
|
||||||
}> => {
|
|
||||||
return jobs.map((job, index) => ({
|
|
||||||
id: index + 1,
|
|
||||||
name: job.zagolovok || '',
|
|
||||||
// tegi is a LinkColumnType, so it returns objects when loaded with .with()
|
|
||||||
tags: Array.isArray(job.tegi)
|
|
||||||
? (job.tegi as Array<{ name: string }>).map((tag) => tag.name)
|
|
||||||
: [],
|
|
||||||
// tip and lokaciya are SingleSelectColumnType, which return arrays of strings
|
|
||||||
location: job.lokaciya?.[0] || null,
|
|
||||||
type: job.tip?.[0] || null,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const presentDiscountsFromTaylor = (
|
|
||||||
discounts: TableRaws<'akcii'>[],
|
|
||||||
): Array<{
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
expiresAt: string;
|
|
||||||
image: string | null;
|
|
||||||
}> => {
|
|
||||||
return discounts.map((discount, index) => ({
|
|
||||||
id: index + 1,
|
|
||||||
name: discount.zagolovok || '',
|
|
||||||
description: discount.opisanie || '',
|
|
||||||
expiresAt: discount.do || '',
|
|
||||||
image: getAttachmentUrl(discount.foto),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const presentStationsFromTaylor = (
|
|
||||||
stations: TableRaws<'azs'>[],
|
|
||||||
): Array<{
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
address: string;
|
|
||||||
workingHours: string | null;
|
|
||||||
latitude: number;
|
|
||||||
longitude: number;
|
|
||||||
carWash: boolean;
|
|
||||||
ai92: boolean;
|
|
||||||
ai95: boolean;
|
|
||||||
dt: boolean;
|
|
||||||
z100: boolean;
|
|
||||||
propan: boolean;
|
|
||||||
electricCharge: boolean;
|
|
||||||
miniMarket: boolean;
|
|
||||||
toilet: boolean;
|
|
||||||
region: string | null;
|
|
||||||
image: string | null;
|
|
||||||
}> => {
|
|
||||||
return stations.map((station, index) => ({
|
|
||||||
id: index + 1,
|
|
||||||
name: station.imya || '',
|
|
||||||
description: station.opisanie || '',
|
|
||||||
address: station.adress || '',
|
|
||||||
// chasyRaboty and region are SingleSelectColumnType, which return arrays of strings
|
|
||||||
workingHours: station.chasyRaboty?.[0] || null,
|
|
||||||
// Parse string coordinates to numbers
|
|
||||||
latitude: parseFloat(station.lat || '0') || 0,
|
|
||||||
longitude: parseFloat(station.long || '0') || 0,
|
|
||||||
carWash: station.avtomojka || false,
|
|
||||||
ai92: station.ai92 || false,
|
|
||||||
ai95: station.ai95 || false,
|
|
||||||
dt: station.dt || false,
|
|
||||||
z100: station.z100 || false,
|
|
||||||
propan: station.propan || false,
|
|
||||||
electricCharge: station.zaryadnayaStanciya || false,
|
|
||||||
miniMarket: station.miniMarket || false,
|
|
||||||
toilet: station.tualet || false,
|
|
||||||
region: station.region?.[0] || null,
|
|
||||||
image: getAttachmentUrl(station.foto),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const presentTeamMembersFromTaylor = (
|
|
||||||
members: TableRaws<'komanda'>[],
|
|
||||||
): Array<{
|
|
||||||
name: string;
|
|
||||||
photo: string | null;
|
|
||||||
profession: string;
|
|
||||||
}> => {
|
|
||||||
return members.map((member) => ({
|
|
||||||
name: member.polnoeImya || '',
|
|
||||||
photo: getAttachmentUrl(member.foto),
|
|
||||||
profession: member.zvanie || '',
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const presentHistoryItemsFromTaylor = (
|
|
||||||
historyItems: TableRaws<'istoriyaKompanii'>[],
|
|
||||||
): Array<{
|
|
||||||
name: string;
|
|
||||||
year: string;
|
|
||||||
description: string;
|
|
||||||
}> => {
|
|
||||||
return historyItems.map((item) => ({
|
|
||||||
name: item.zagolovok || '',
|
|
||||||
year: String(item.god || ''),
|
|
||||||
description: item.opisanie || '',
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const presentReviewsFromTaylor = (
|
|
||||||
reviews: TableRaws<'otzyvy'>[],
|
|
||||||
): Array<{
|
|
||||||
id: number;
|
|
||||||
fullname: string;
|
|
||||||
review: string;
|
|
||||||
rating: number;
|
|
||||||
}> => {
|
|
||||||
return reviews.map((review) => ({
|
|
||||||
id: review.id || 0,
|
|
||||||
fullname: review.polnoeImya || '',
|
|
||||||
review: review.otzyv || '',
|
|
||||||
rating: review.rejting || 0,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const presentCharitiesFromTaylor = (
|
|
||||||
charities: TableRaws<'blagotvoritelnyjFond'>[],
|
|
||||||
): Array<{
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
date: string;
|
|
||||||
location: string;
|
|
||||||
image: string | null;
|
|
||||||
}> => {
|
|
||||||
return charities.map((charity, index) => ({
|
|
||||||
id: index + 1,
|
|
||||||
name: charity.zagolovok || '',
|
|
||||||
description: charity.opisanie || '',
|
|
||||||
date: charity.data || '',
|
|
||||||
location: charity.lokaciya || '',
|
|
||||||
image: getAttachmentUrl(charity.foto),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const presentCertificatesFromTaylor = (
|
|
||||||
certificates: TableRaws<'sertifikaty'>[],
|
|
||||||
): Array<{
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
issuedAt: string;
|
|
||||||
validUntil: string;
|
|
||||||
image: string | null;
|
|
||||||
}> => {
|
|
||||||
return certificates.map((certificate, index) => ({
|
|
||||||
id: index + 1,
|
|
||||||
name: certificate.nazvanie || '',
|
|
||||||
description: certificate.opisanie || '',
|
|
||||||
issuedAt: certificate.dataVydachi || '',
|
|
||||||
validUntil: certificate.dejstvitelenDo || '',
|
|
||||||
image: getAttachmentUrl(certificate.foto),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const presentTextsFromTaylor = (
|
|
||||||
texts: TableRaws<'tekstovyjKontentSajta'>[],
|
|
||||||
): Array<{
|
|
||||||
key: string;
|
|
||||||
value: string | null;
|
|
||||||
}> => {
|
|
||||||
return texts.map((item) => ({
|
|
||||||
key: item.klyuchNeIzmenyat || '',
|
|
||||||
value: item.znachenie || null,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const presentMediaFromTaylor = (
|
|
||||||
media: TableRaws<'mediaKontentSajta'>[],
|
|
||||||
): Array<{
|
|
||||||
key: string;
|
|
||||||
name: string;
|
|
||||||
photo: string | null;
|
|
||||||
}> => {
|
|
||||||
return media.map((record) => ({
|
|
||||||
key: record.klyuchNeIzmenyat || '',
|
|
||||||
name: record.mestopolozheniya || '',
|
|
||||||
photo: getAttachmentUrl(record.foto),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
13
src/app/api-utlities/requests/about-us-page.request.ts
Normal file
13
src/app/api-utlities/requests/about-us-page.request.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import {
|
||||||
|
historyRequest,
|
||||||
|
reviewsRequest,
|
||||||
|
stationsWithImageRequest,
|
||||||
|
teamRequest,
|
||||||
|
} from './common';
|
||||||
|
|
||||||
|
export const aboutUsPageRequest = {
|
||||||
|
...teamRequest,
|
||||||
|
...historyRequest,
|
||||||
|
...stationsWithImageRequest,
|
||||||
|
...reviewsRequest,
|
||||||
|
};
|
||||||
@ -1,29 +1,28 @@
|
|||||||
import { EnumType, VariableType } from 'json-to-graphql-query';
|
import { EnumType } from 'json-to-graphql-query';
|
||||||
|
|
||||||
export const stationsRequest = {
|
export const stationsRequest = {
|
||||||
azs: {
|
_azs: {
|
||||||
records: {
|
records: {
|
||||||
imya: true,
|
_name: true,
|
||||||
opisanie: true,
|
_opisanie: true,
|
||||||
adress: true,
|
_adress: true,
|
||||||
chasyRaboty: {
|
_chasyRaboty: {
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
lat: true,
|
_lat: true,
|
||||||
long: true,
|
_long: true,
|
||||||
avtomojka: true,
|
_avtomojka: true,
|
||||||
ai92: true,
|
_dtCopy: true, // ai92
|
||||||
ai95: true,
|
_ai92Copy: true, // ai95
|
||||||
z100: true,
|
_ai95Copy: true, // z100
|
||||||
propan: true,
|
_z100Copy: true, // propan
|
||||||
zaryadnayaStanciya: true,
|
_propanCopy: true, // electricCharge
|
||||||
dt: true,
|
_zaryadnayaStanci: true, // miniMarket
|
||||||
miniMarket: true,
|
_miniMarketCop: true, // toilet
|
||||||
tualet: true,
|
_region: {
|
||||||
region: {
|
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
foto: {
|
_foto: {
|
||||||
url: true,
|
url: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -31,13 +30,13 @@ export const stationsRequest = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const stationsWithImageRequest = {
|
export const stationsWithImageRequest = {
|
||||||
azs: {
|
_azs: {
|
||||||
__args: {
|
__args: {
|
||||||
filtersSet: {
|
filtersSet: {
|
||||||
conjunction: new EnumType('and'),
|
conjunction: new EnumType('and'),
|
||||||
filtersSet: [
|
filtersSet: [
|
||||||
{
|
{
|
||||||
field: new EnumType('foto'),
|
field: new EnumType('_foto'),
|
||||||
operator: 'isNotEmpty',
|
operator: 'isNotEmpty',
|
||||||
value: [],
|
value: [],
|
||||||
},
|
},
|
||||||
@ -46,27 +45,26 @@ export const stationsWithImageRequest = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
records: {
|
records: {
|
||||||
imya: true,
|
_name: true,
|
||||||
opisanie: true,
|
_opisanie: true,
|
||||||
adress: true,
|
_adress: true,
|
||||||
chasyRaboty: {
|
_chasyRaboty: {
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
lat: true,
|
_lat: true,
|
||||||
long: true,
|
_long: true,
|
||||||
avtomojka: true,
|
_avtomojka: true,
|
||||||
ai92: true,
|
_dtCopy: true, // ai92
|
||||||
ai95: true,
|
_ai92Copy: true, // ai95
|
||||||
z100: true,
|
_ai95Copy: true, // z100
|
||||||
propan: true,
|
_z100Copy: true, // propan
|
||||||
zaryadnayaStanciya: true,
|
_propanCopy: true, // electricCharge
|
||||||
dt: true,
|
_zaryadnayaStanci: true, // miniMarket
|
||||||
miniMarket: true,
|
_miniMarketCop: true, // toilet
|
||||||
tualet: true,
|
_region: {
|
||||||
region: {
|
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
foto: {
|
_foto: {
|
||||||
url: true,
|
url: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -74,10 +72,10 @@ export const stationsWithImageRequest = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const partnersRequest = {
|
export const partnersRequest = {
|
||||||
partnyory: {
|
_partners: {
|
||||||
records: {
|
records: {
|
||||||
nazvanie: true,
|
_name: true,
|
||||||
izobrozhenie: {
|
_image: {
|
||||||
url: true,
|
url: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -85,16 +83,16 @@ export const partnersRequest = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const jobsRequest = {
|
export const jobsRequest = {
|
||||||
vakansii: {
|
_vacancies: {
|
||||||
records: {
|
records: {
|
||||||
zagolovok: true,
|
_name: true,
|
||||||
tegi: {
|
_tags: {
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
tip: {
|
_type: {
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
lokaciya: {
|
_localtio: {
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -102,123 +100,56 @@ export const jobsRequest = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const discountsRequest = {
|
export const discountsRequest = {
|
||||||
akcii: {
|
_akcii: {
|
||||||
records: {
|
records: {
|
||||||
zagolovok: true,
|
_name: true,
|
||||||
opisanie: true,
|
_opisanie: true,
|
||||||
do: true,
|
_do: true,
|
||||||
foto: {
|
_foto: {
|
||||||
name: true,
|
url: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const textsRequest = {
|
export const textsRequest = {
|
||||||
tekstovyjKontentSajta: {
|
_kontentSajta: {
|
||||||
records: {
|
records: {
|
||||||
znachenie: true,
|
_name: true,
|
||||||
klyuchNeIzmenyat: true,
|
_znachenie: true,
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mediaRequest = {
|
|
||||||
mediaKontentSajta: {
|
|
||||||
records: {
|
|
||||||
mestopolozheniya: true,
|
|
||||||
foto: {
|
|
||||||
id: true,
|
|
||||||
url: true,
|
|
||||||
},
|
|
||||||
klyuchNeIzmenyat: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const teamRequest = {
|
export const teamRequest = {
|
||||||
komanda: {
|
_komanda: {
|
||||||
records: {
|
records: {
|
||||||
foto: {
|
_foto: {
|
||||||
url: true,
|
url: true,
|
||||||
},
|
},
|
||||||
zvanie: true,
|
_zvanie: true,
|
||||||
polnoeImya: true,
|
_name: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const historyRequest = {
|
export const historyRequest = {
|
||||||
istoriyaKompanii: {
|
_istoriya: {
|
||||||
records: {
|
records: {
|
||||||
zagolovok: true,
|
_name: true,
|
||||||
god: true,
|
_god: true,
|
||||||
opisanie: true,
|
_opisanie: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const reviewsRequest = {
|
export const reviewsRequest = {
|
||||||
otzyvy: {
|
_otzyvy: {
|
||||||
__args: {
|
|
||||||
filtersSet: {
|
|
||||||
conjunction: new EnumType('and'),
|
|
||||||
filtersSet: [
|
|
||||||
{
|
|
||||||
field: new EnumType('status'),
|
|
||||||
operator: 'contains',
|
|
||||||
value: 'Опубликовано',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
records: {
|
records: {
|
||||||
id: true,
|
id: true,
|
||||||
polnoeImya: true,
|
_name: true,
|
||||||
otzyv: true,
|
_otzyv: true,
|
||||||
rejting: true,
|
_rejting: true,
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const charityRequest = {
|
|
||||||
blagotvoritelnyjFond: {
|
|
||||||
records: {
|
|
||||||
zagolovok: true,
|
|
||||||
opisanie: true,
|
|
||||||
data: true,
|
|
||||||
lokaciya: true,
|
|
||||||
foto: {
|
|
||||||
url: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const certificatesRequest = {
|
|
||||||
sertifikaty: {
|
|
||||||
records: {
|
|
||||||
nazvanie: true,
|
|
||||||
opisanie: true,
|
|
||||||
dataVydachi: true,
|
|
||||||
dejstvitelenDo: true,
|
|
||||||
foto: {
|
|
||||||
url: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createReviewMutation = {
|
|
||||||
__variables: {
|
|
||||||
review: 'TableOtzyvyMutationParameters',
|
|
||||||
},
|
|
||||||
otzyvy: {
|
|
||||||
createRecord: {
|
|
||||||
__args: {
|
|
||||||
records: [new VariableType('review')],
|
|
||||||
},
|
|
||||||
id: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
13
src/app/api-utlities/requests/main-page.request.ts
Normal file
13
src/app/api-utlities/requests/main-page.request.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import {
|
||||||
|
discountsRequest,
|
||||||
|
jobsRequest,
|
||||||
|
partnersRequest,
|
||||||
|
stationsRequest,
|
||||||
|
} from './common';
|
||||||
|
|
||||||
|
export const mainPageRequest = {
|
||||||
|
...partnersRequest,
|
||||||
|
...jobsRequest,
|
||||||
|
...discountsRequest,
|
||||||
|
...stationsRequest,
|
||||||
|
};
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { NextRequest } from 'next/server';
|
|
||||||
|
|
||||||
export const getParams = (request: NextRequest) =>
|
|
||||||
Array.from(request.nextUrl.searchParams.entries()).reduce(
|
|
||||||
(pr, cr) => {
|
|
||||||
pr[cr[0]] = cr[1];
|
|
||||||
|
|
||||||
return pr;
|
|
||||||
},
|
|
||||||
{} as Record<string, string>,
|
|
||||||
);
|
|
||||||
@ -5,10 +5,6 @@ const oriyoClient = new Axios({
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-type': 'application/json',
|
'Content-type': 'application/json',
|
||||||
},
|
},
|
||||||
|
|
||||||
transformResponse: (response) => {
|
|
||||||
return JSON.parse(response);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default oriyoClient;
|
export default oriyoClient;
|
||||||
@ -2,7 +2,7 @@ import { jsonToGraphQLQuery } from 'json-to-graphql-query';
|
|||||||
|
|
||||||
export const requestTaylor = async (query: object, variables?: object) => {
|
export const requestTaylor = async (query: object, variables?: object) => {
|
||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
query: jsonToGraphQLQuery(query),
|
query: jsonToGraphQLQuery({ query }),
|
||||||
variables,
|
variables,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -12,7 +12,6 @@ export const requestTaylor = async (query: object, variables?: object) => {
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: process.env.TAYLOR_API_TOKEN || '',
|
Authorization: process.env.TAYLOR_API_TOKEN || '',
|
||||||
'Content-type': 'application/json',
|
'Content-type': 'application/json',
|
||||||
schema: 'readable',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import oriyoClient from '@/app/api-utlities/clients/oriyo.client';
|
import oriyoClient from '@/app/api-utlities/utilities/oriyo.client';
|
||||||
|
|
||||||
import { loginFormSchema } from '@/entities/auth/model/validation/login-form.schema';
|
import { loginFormSchema } from '@/entities/auth/model/validation/login-form.schema';
|
||||||
|
|
||||||
@ -14,41 +14,33 @@ const routeHandler = async (req: NextRequest) => {
|
|||||||
.merge(z.object({ type: z.enum(['bonus', 'corporate']) }))
|
.merge(z.object({ type: z.enum(['bonus', 'corporate']) }))
|
||||||
.parse(body);
|
.parse(body);
|
||||||
|
|
||||||
const oriyoResponse = await (() => {
|
try {
|
||||||
switch (validatedBody.type) {
|
const oriyoResponse = await oriyoClient.get('/client/login', {
|
||||||
case 'corporate':
|
params: {
|
||||||
return oriyoClient.get('/corporatecard', {
|
type: validatedBody.type,
|
||||||
params: {
|
phone: validatedBody.phoneNumber,
|
||||||
phone: validatedBody.phoneNumber,
|
uid: validatedBody.cardNumber,
|
||||||
uid: validatedBody.cardNumber,
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
default:
|
const parsedResponse = JSON.parse(oriyoResponse.data);
|
||||||
return oriyoClient.get('/bonuscard', {
|
|
||||||
params: {
|
if (!parsedResponse.token) {
|
||||||
phone: validatedBody.phoneNumber,
|
return NextResponse.json({ error: 'Credentials error' }, { status: 401 });
|
||||||
uid: validatedBody.cardNumber,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})();
|
|
||||||
|
|
||||||
if (oriyoResponse.data.error)
|
const response = NextResponse.json({ success: true });
|
||||||
return NextResponse.json({ error: 'Credentials error' }, { status: 401 });
|
|
||||||
|
|
||||||
const response = NextResponse.json({ success: true });
|
response.cookies.set(`${validatedBody.type}__token`, oriyoResponse.data, {
|
||||||
|
|
||||||
response.cookies.set(
|
|
||||||
`${validatedBody.type}__token`,
|
|
||||||
JSON.stringify(oriyoResponse.data),
|
|
||||||
{
|
|
||||||
path: '/',
|
path: '/',
|
||||||
maxAge: 2 * 60 * 60,
|
maxAge: 2 * 60 * 60,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('login error:', error);
|
||||||
|
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const POST = validationErrorHandler(routeHandler);
|
export const POST = validationErrorHandler(routeHandler);
|
||||||
|
|||||||
@ -1,31 +1,31 @@
|
|||||||
import { RequestCookie } from 'next/dist/compiled/@edge-runtime/cookies';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { NextRequest } from 'next/server';
|
|
||||||
|
|
||||||
import oriyoClient from '@/app/api-utlities/clients/oriyo.client';
|
import oriyoClient from '@/app/api-utlities/utilities/oriyo.client';
|
||||||
import { AuthorizationError } from '@/app/api-utlities/errors/authorization.error';
|
|
||||||
|
|
||||||
import { authorizationMiddleware } from '../../middlewares/auth.middleware';
|
|
||||||
import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
|
import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
|
||||||
|
|
||||||
const routeHandler = async (req: NextRequest, requestCookie: RequestCookie) => {
|
const routeHandler = async (req: NextRequest) => {
|
||||||
const { card_id, token } = JSON.parse(requestCookie.value);
|
const bonusTokenData = req.cookies.get('bonus__token');
|
||||||
|
|
||||||
const oriyoResponse = await oriyoClient.get('/bonuscardinfo', {
|
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: {
|
params: {
|
||||||
card_id,
|
card_id,
|
||||||
token,
|
token,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (oriyoResponse.status === 401) {
|
return new Response(oriyoResponse.data, {
|
||||||
throw new AuthorizationError();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response(JSON.stringify(oriyoResponse.data), {
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GET = validationErrorHandler(
|
export const GET = validationErrorHandler(routeHandler);
|
||||||
authorizationMiddleware(routeHandler, 'bonus__token'),
|
|
||||||
);
|
|
||||||
|
|||||||
@ -1,12 +1,8 @@
|
|||||||
import { RequestCookie } from 'next/dist/compiled/@edge-runtime/cookies';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { NextRequest } from 'next/server';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import oriyoClient from '@/app/api-utlities/clients/oriyo.client';
|
import oriyoClient from '@/app/api-utlities/utilities/oriyo.client';
|
||||||
import { AuthorizationError } from '@/app/api-utlities/errors/authorization.error';
|
|
||||||
import { getParams } from '@/app/api-utlities/utilities/get-params';
|
|
||||||
|
|
||||||
import { authorizationMiddleware } from '../../middlewares/auth.middleware';
|
|
||||||
import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
|
import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
|
||||||
|
|
||||||
const validatedSchema = z.object({
|
const validatedSchema = z.object({
|
||||||
@ -16,12 +12,30 @@ const validatedSchema = z.object({
|
|||||||
page: z.coerce.number(),
|
page: z.coerce.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const routeHandler = async (req: NextRequest, requestCookie: RequestCookie) => {
|
const routeHandler = async (req: NextRequest) => {
|
||||||
const validatedRequest = validatedSchema.parse(getParams(req));
|
const bonusTokenData = req.cookies.get('bonus__token');
|
||||||
|
|
||||||
const { card_id, token } = JSON.parse(requestCookie.value);
|
if (!bonusTokenData) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'User does not have access' },
|
||||||
|
{ status: 401 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const oriyoResponse = await oriyoClient.get('/bonuscardts', {
|
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: {
|
params: {
|
||||||
card_id,
|
card_id,
|
||||||
token,
|
token,
|
||||||
@ -35,32 +49,15 @@ const routeHandler = async (req: NextRequest, requestCookie: RequestCookie) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (oriyoResponse.status === 404)
|
const parsedResponse = JSON.parse(oriyoResponse.data);
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
transactions: [],
|
|
||||||
card_id,
|
|
||||||
current_page: validatedRequest.page,
|
|
||||||
limit: validatedRequest.limit,
|
|
||||||
total_records: 0,
|
|
||||||
total_pages: 0,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (oriyoResponse.status === 401) {
|
if (parsedResponse.error) {
|
||||||
throw new AuthorizationError();
|
return NextResponse.json({ message: 'Fetch error' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oriyoResponse.data.error) throw oriyoResponse.data;
|
return new Response(oriyoResponse.data, {
|
||||||
|
|
||||||
return new Response(JSON.stringify(oriyoResponse.data), {
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GET = validationErrorHandler(
|
export const GET = validationErrorHandler(routeHandler);
|
||||||
authorizationMiddleware(routeHandler, 'bonus__token'),
|
|
||||||
);
|
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
import { revalidatePath } from 'next/cache';
|
|
||||||
import { NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
revalidatePath('/', 'layout');
|
|
||||||
revalidatePath('/', 'page');
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
|
||||||
} catch (err) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to drop cache', detail: err },
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,18 +1,23 @@
|
|||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import { RequestCookie } from 'next/dist/compiled/@edge-runtime/cookies';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { NextRequest } from 'next/server';
|
|
||||||
|
|
||||||
import { authorizationMiddleware } from '../../middlewares/auth.middleware';
|
|
||||||
import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
|
import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
|
||||||
|
|
||||||
const routeHandler = async (req: NextRequest, requestCookie: RequestCookie) => {
|
const routeHandler = async (req: NextRequest) => {
|
||||||
const parsedData = JSON.parse(requestCookie.value);
|
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')), {
|
return new Response(JSON.stringify(omit(parsedData, 'token')), {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GET = validationErrorHandler(
|
export const GET = validationErrorHandler(routeHandler);
|
||||||
authorizationMiddleware(routeHandler, 'corporate__token'),
|
|
||||||
);
|
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
import { RequestCookie } from 'next/dist/compiled/@edge-runtime/cookies';
|
|
||||||
import { NextRequest } from 'next/server';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import oriyoClient from '@/app/api-utlities/clients/oriyo.client';
|
|
||||||
import { AuthorizationError } from '@/app/api-utlities/errors/authorization.error';
|
|
||||||
import { getParams } from '@/app/api-utlities/utilities/get-params';
|
|
||||||
|
|
||||||
import { authorizationMiddleware } from '../../middlewares/auth.middleware';
|
|
||||||
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, requestCookie: RequestCookie) => {
|
|
||||||
const validatedRequest = validatedSchema.parse(getParams(req));
|
|
||||||
|
|
||||||
const { group_id, token } = JSON.parse(requestCookie.value);
|
|
||||||
|
|
||||||
const oriyoResponse = await oriyoClient.get('/corporatecardts', {
|
|
||||||
params: {
|
|
||||||
group_id,
|
|
||||||
token,
|
|
||||||
limit: validatedRequest.limit,
|
|
||||||
page: validatedRequest.page,
|
|
||||||
type: 'corporate',
|
|
||||||
sort: 'id',
|
|
||||||
direction: 'desc',
|
|
||||||
start_date: validatedRequest.start_date,
|
|
||||||
end_date: validatedRequest.end_date,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (oriyoResponse.status === 404)
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
transactions: [],
|
|
||||||
current_page: validatedRequest.page,
|
|
||||||
limit: validatedRequest.limit,
|
|
||||||
total_records: 0,
|
|
||||||
total_pages: 0,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (oriyoResponse.status === 401) {
|
|
||||||
throw new AuthorizationError();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oriyoResponse.data.error) throw oriyoResponse.data;
|
|
||||||
|
|
||||||
return new Response(JSON.stringify(oriyoResponse.data), {
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GET = validationErrorHandler(
|
|
||||||
authorizationMiddleware(routeHandler, 'corporate__token'),
|
|
||||||
);
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
import { AuthorizationError } from '@/app/api-utlities/errors/authorization.error';
|
|
||||||
|
|
||||||
export const authorizationMiddleware =
|
|
||||||
(handler: Function, authorizationTokenKey: string) =>
|
|
||||||
async (req: NextRequest, ...args: any[]) => {
|
|
||||||
const requestedToken = req.cookies.get(authorizationTokenKey);
|
|
||||||
|
|
||||||
if (!requestedToken) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'User does not have access' },
|
|
||||||
{ status: 401 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await handler(req, requestedToken, ...args);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof AuthorizationError) {
|
|
||||||
const response = NextResponse.json(
|
|
||||||
{ message: 'Authorization session was timed out' },
|
|
||||||
{ status: 401 },
|
|
||||||
);
|
|
||||||
|
|
||||||
response.cookies.delete(authorizationTokenKey);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
import { NextRequest } from 'next/server';
|
|
||||||
|
|
||||||
import { requestTaylor } from '@/app/api-utlities/clients/taylor.client';
|
|
||||||
import { createReviewMutation } from '@/app/api-utlities/requests/common';
|
|
||||||
|
|
||||||
import { reviewSchema } from '@/features/review-form/model/review-form.schema';
|
|
||||||
|
|
||||||
export const POST = async (req: NextRequest) => {
|
|
||||||
const body = await req.json();
|
|
||||||
|
|
||||||
const validatedRequest = reviewSchema.parse(body);
|
|
||||||
|
|
||||||
await requestTaylor(
|
|
||||||
{ mutation: createReviewMutation },
|
|
||||||
{
|
|
||||||
review: {
|
|
||||||
polnoeImya: validatedRequest.name,
|
|
||||||
otzyv: validatedRequest.reviewMessage,
|
|
||||||
rejting: validatedRequest.rating,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Response(JSON.stringify({ success: true }), {
|
|
||||||
status: 201,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,17 +1,5 @@
|
|||||||
import { CharityPage } from '@/pages-templates/charity';
|
import { CharityPage } from "@/pages-templates/charity"
|
||||||
|
|
||||||
import { fetchCharityPageContent } from '@/features/pages/services/pages.service';
|
export default function Charity() {
|
||||||
|
return <CharityPage />
|
||||||
export const metadata = {
|
}
|
||||||
title: 'Благотворительность',
|
|
||||||
description:
|
|
||||||
'Благотворительные проекты и инициативы Ориё. Мы помогаем обществу и заботимся о будущем.',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function Charity() {
|
|
||||||
const data = await fetchCharityPageContent();
|
|
||||||
|
|
||||||
if (!data) return null;
|
|
||||||
|
|
||||||
return <CharityPage content={data} />;
|
|
||||||
}
|
|
||||||
@ -1,17 +1,5 @@
|
|||||||
import { CertificatesPage } from '@/pages-templates/clients/certificates';
|
import { CertificatesPage } from '@/pages-templates/clients/certificates';
|
||||||
|
|
||||||
import { fetchCertificatesPageContent } from '@/features/pages/services/pages.service';
|
export default function Certificates() {
|
||||||
|
return <CertificatesPage />;
|
||||||
export const metadata = {
|
|
||||||
title: 'Сертификаты',
|
|
||||||
description:
|
|
||||||
'Ориё придерживается высоких стандартов качества и безопасности.',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function Certificates() {
|
|
||||||
const data = await fetchCertificatesPageContent();
|
|
||||||
|
|
||||||
if (!data) return null;
|
|
||||||
|
|
||||||
return <CertificatesPage content={data} />;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,5 @@
|
|||||||
import { LoyaltyPage } from '@/pages-templates/clients/loyalty';
|
import { LoyaltyPage } from '@/pages-templates/clients/loyalty';
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: 'Программа лояльности',
|
|
||||||
description:
|
|
||||||
'Программа лояльности Ориё: накапливайте баллы и получайте скидки на топливо и услуги.',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Loyalty() {
|
export default function Loyalty() {
|
||||||
return <LoyaltyPage />;
|
return <LoyaltyPage />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,5 @@
|
|||||||
import { ClientsPage } from "@/pages-templates/clients"
|
import { ClientsPage } from "@/pages-templates/clients"
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: 'Клиентам',
|
|
||||||
description:
|
|
||||||
'Информация для клиентов: программа лояльности, топливные карты, сертификаты и способы оплаты.',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Clients() {
|
export default function Clients() {
|
||||||
return <ClientsPage />;
|
return <ClientsPage />;
|
||||||
}
|
}
|
||||||
@ -1,13 +1,9 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { Inter } from 'next/font/google';
|
import { Inter } from 'next/font/google';
|
||||||
|
|
||||||
import {
|
import { textControlApi } from '@/shared/language/api/text-control.api';
|
||||||
fetchMediaContent,
|
|
||||||
fetchTextContent,
|
|
||||||
} from '@/features/pages/services/pages.service';
|
|
||||||
|
|
||||||
import { Providers } from '@/shared/providers/providers';
|
import { Providers } from '@/shared/providers/providers';
|
||||||
import { MediaItem } from '@/shared/types/media.type';
|
import { makeStore } from '@/shared/store';
|
||||||
import { TextItem } from '@/shared/types/text.types';
|
import { TextItem } from '@/shared/types/text.types';
|
||||||
|
|
||||||
import { Footer } from '@/widgets/footer';
|
import { Footer } from '@/widgets/footer';
|
||||||
@ -18,10 +14,7 @@ import './globals.css';
|
|||||||
const inter = Inter({ subsets: ['latin', 'cyrillic'] });
|
const inter = Inter({ subsets: ['latin', 'cyrillic'] });
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: 'GasNetwork - Сеть заправок в Таджикистане',
|
||||||
template: '%s | Ориё - Сеть заправок в Таджикистане',
|
|
||||||
default: 'Ориё - Сеть заправок в Таджикистане',
|
|
||||||
},
|
|
||||||
description:
|
description:
|
||||||
'Качественное топливо, удобное расположение и отличный сервис для наших клиентов',
|
'Качественное топливо, удобное расположение и отличный сервис для наших клиентов',
|
||||||
};
|
};
|
||||||
@ -31,11 +24,12 @@ export default async function RootLayout({
|
|||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
// Fetch texts and media using TaylorDB query builder
|
const store = makeStore();
|
||||||
const [textItems, mediaItems] = await Promise.all([
|
|
||||||
fetchTextContent(),
|
const response = await store.dispatch(
|
||||||
fetchMediaContent(),
|
textControlApi.endpoints.fetchText.initiate(),
|
||||||
]);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang='ru'
|
lang='ru'
|
||||||
@ -44,10 +38,7 @@ export default async function RootLayout({
|
|||||||
style={{ scrollBehavior: 'smooth' }}
|
style={{ scrollBehavior: 'smooth' }}
|
||||||
>
|
>
|
||||||
<body className={`${inter.className} min-w-2xs antialiased`}>
|
<body className={`${inter.className} min-w-2xs antialiased`}>
|
||||||
<Providers
|
<Providers textItems={response.data as TextItem[]}>
|
||||||
textItems={textItems as TextItem[]}
|
|
||||||
mediaItems={mediaItems as MediaItem[]}
|
|
||||||
>
|
|
||||||
<Header />
|
<Header />
|
||||||
{children}
|
{children}
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { fetchMainPageContent } from '@/features/pages/services/pages.service';
|
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 { AppDownloadSection } from '@/widgets/app-download-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';
|
||||||
import { HeroSection } from '@/widgets/hero-section';
|
import { HeroSection } from '@/widgets/hero-section';
|
||||||
@ -12,14 +13,17 @@ import { StatsSection } from '@/widgets/stats-section';
|
|||||||
import { VacanciesSection } from '@/widgets/vacancies-section';
|
import { VacanciesSection } from '@/widgets/vacancies-section';
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const data = await fetchMainPageContent();
|
const store = makeStore();
|
||||||
|
|
||||||
if (!data) return null;
|
const { data, isLoading, error } = await store.dispatch(
|
||||||
|
mainPageApi.endpoints.fetchMainPageContent.initiate(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading || !data) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
<AppDownloadSection />
|
|
||||||
<StatsSection />
|
<StatsSection />
|
||||||
<MapSection stations={data.stations} />
|
<MapSection stations={data.stations} />
|
||||||
<AboutSection />
|
<AboutSection />
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { baseAPI } from '@/shared/api/base-api';
|
import { baseAPI } from '@/shared/api/base-api';
|
||||||
|
|
||||||
import { ClientInfo } from '../model/types/bonus-client-info.type';
|
|
||||||
import {
|
import {
|
||||||
BonusTransactionRequest,
|
ClientInfo,
|
||||||
BonusTransactionResponse,
|
TransactionRequest,
|
||||||
} from '../model/types/bonus-transactions.type';
|
TransactionResponse,
|
||||||
|
} from '../model/types/bonus-client-info.type';
|
||||||
|
|
||||||
export const bonusApi = baseAPI.injectEndpoints({
|
export const bonusApi = baseAPI.injectEndpoints({
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
@ -16,8 +16,8 @@ export const bonusApi = baseAPI.injectEndpoints({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
fetchBonusTransactions: builder.query<
|
fetchBonusTransactions: builder.query<
|
||||||
BonusTransactionResponse,
|
TransactionResponse,
|
||||||
BonusTransactionRequest
|
TransactionRequest
|
||||||
>({
|
>({
|
||||||
query: (request) => {
|
query: (request) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -6,3 +6,29 @@ export interface ClientInfo {
|
|||||||
end_date: string;
|
end_date: string;
|
||||||
bonuses: 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;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
export interface BonusTransactionResponse {
|
|
||||||
transactions: BonusTransaction[];
|
|
||||||
current_page: number;
|
|
||||||
limit: number;
|
|
||||||
total_records: number;
|
|
||||||
total_pages: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BonusTransaction {
|
|
||||||
id: number;
|
|
||||||
date_create: string;
|
|
||||||
station?: string;
|
|
||||||
product_name: string;
|
|
||||||
amount: string;
|
|
||||||
price_real: string;
|
|
||||||
sum_real: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BonusTransactionRequest {
|
|
||||||
start_date?: string;
|
|
||||||
end_date?: string;
|
|
||||||
page: number;
|
|
||||||
limit: number;
|
|
||||||
}
|
|
||||||
@ -1,10 +1,6 @@
|
|||||||
import { baseAPI } from '@/shared/api/base-api';
|
import { baseAPI } from '@/shared/api/base-api';
|
||||||
|
|
||||||
import { CorporateInfoResponse } from '../model/types/corporate-client-info.type';
|
import { CorporateInfoResponse } from '../model/types/corporate-client-info.type';
|
||||||
import {
|
|
||||||
CorporateTransactionRequest,
|
|
||||||
CorporateTransactionResponse,
|
|
||||||
} from '../model/types/corporate-transactions.type';
|
|
||||||
|
|
||||||
export const corporateApi = baseAPI.injectEndpoints({
|
export const corporateApi = baseAPI.injectEndpoints({
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
@ -15,21 +11,7 @@ export const corporateApi = baseAPI.injectEndpoints({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
fetchCorporateTransactions: builder.query<
|
|
||||||
CorporateTransactionResponse,
|
|
||||||
CorporateTransactionRequest
|
|
||||||
>({
|
|
||||||
query: (request) => {
|
|
||||||
return {
|
|
||||||
url: '/corporate/transactions',
|
|
||||||
params: request,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const { useFetchMyCorporateInfoQuery } = corporateApi;
|
||||||
useFetchMyCorporateInfoQuery,
|
|
||||||
useFetchCorporateTransactionsQuery,
|
|
||||||
} = corporateApi;
|
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
export interface CorporateTransactionResponse {
|
|
||||||
transactions: CorporateTransaction[];
|
|
||||||
current_page: number;
|
|
||||||
limit: number;
|
|
||||||
total_records: number;
|
|
||||||
total_pages: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CorporateTransaction {
|
|
||||||
date_create: string;
|
|
||||||
station_name?: string;
|
|
||||||
product_name: string;
|
|
||||||
amount: string;
|
|
||||||
price_real: string;
|
|
||||||
sum_real: string;
|
|
||||||
uid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CorporateTransactionRequest {
|
|
||||||
start_date?: string;
|
|
||||||
end_date?: string;
|
|
||||||
page: number;
|
|
||||||
limit: number;
|
|
||||||
}
|
|
||||||
@ -8,7 +8,7 @@ import {
|
|||||||
List,
|
List,
|
||||||
MapPin,
|
MapPin,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { Stations } from '@/app/api-utlities/@types';
|
import { Stations } from '@/app/api-utlities/@types';
|
||||||
|
|
||||||
@ -46,6 +46,7 @@ interface FilterPanelProps {
|
|||||||
setActiveFilterTab: (tab: string) => void;
|
setActiveFilterTab: (tab: string) => void;
|
||||||
resetFilters: () => void;
|
resetFilters: () => void;
|
||||||
resetCities: () => void;
|
resetCities: () => void;
|
||||||
|
t: (key: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пропсы для панели списка станций
|
// Пропсы для панели списка станций
|
||||||
@ -56,7 +57,8 @@ interface StationListPanelProps {
|
|||||||
selectedStation: number | null;
|
selectedStation: number | null;
|
||||||
activeFilters: string[];
|
activeFilters: string[];
|
||||||
activeCities: string[];
|
activeCities: string[];
|
||||||
handleMapStationClick: (id: number) => void;
|
setSelectedStation: (id: number | null) => void;
|
||||||
|
t: (key: string) => string;
|
||||||
filterToFieldMap: { [key: string]: keyof Stations[number] };
|
filterToFieldMap: { [key: string]: keyof Stations[number] };
|
||||||
allFilters: string[];
|
allFilters: string[];
|
||||||
resetFilters: () => void;
|
resetFilters: () => void;
|
||||||
@ -78,9 +80,8 @@ function FilterPanel({
|
|||||||
setActiveFilterTab,
|
setActiveFilterTab,
|
||||||
resetFilters,
|
resetFilters,
|
||||||
resetCities,
|
resetCities,
|
||||||
|
t,
|
||||||
}: FilterPanelProps) {
|
}: FilterPanelProps) {
|
||||||
const { t } = useTextController();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`absolute top-0 bottom-0 left-0 z-20 transform bg-white shadow-lg transition-transform duration-300 ${
|
className={`absolute top-0 bottom-0 left-0 z-20 transform bg-white shadow-lg transition-transform duration-300 ${
|
||||||
@ -205,39 +206,13 @@ function StationListPanel({
|
|||||||
selectedStation,
|
selectedStation,
|
||||||
activeFilters,
|
activeFilters,
|
||||||
activeCities,
|
activeCities,
|
||||||
handleMapStationClick,
|
setSelectedStation,
|
||||||
|
t,
|
||||||
filterToFieldMap,
|
filterToFieldMap,
|
||||||
allFilters,
|
allFilters,
|
||||||
resetCities,
|
resetCities,
|
||||||
resetFilters,
|
resetFilters,
|
||||||
}: StationListPanelProps) {
|
}: StationListPanelProps) {
|
||||||
const { t } = useTextController();
|
|
||||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedStation || !scrollContainerRef.current) return;
|
|
||||||
|
|
||||||
const selectedStationItem = document.getElementById(
|
|
||||||
`station_${selectedStation}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedStationItem) {
|
|
||||||
const container = scrollContainerRef.current;
|
|
||||||
const itemRect = selectedStationItem.getBoundingClientRect();
|
|
||||||
const containerRect = container.getBoundingClientRect();
|
|
||||||
|
|
||||||
// Calculate the item's position relative to the container
|
|
||||||
const itemTopRelativeToContainer =
|
|
||||||
itemRect.top - containerRect.top + container.scrollTop - 10;
|
|
||||||
|
|
||||||
// Scroll the container to bring the item into view
|
|
||||||
container.scrollTo({
|
|
||||||
top: itemTopRelativeToContainer,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [selectedStation]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`absolute top-0 right-0 bottom-0 z-20 transform bg-white shadow-lg transition-transform duration-300 ${
|
className={`absolute top-0 right-0 bottom-0 z-20 transform bg-white shadow-lg transition-transform duration-300 ${
|
||||||
@ -254,11 +229,7 @@ function StationListPanel({
|
|||||||
<Badge>{stations.length}</Badge>
|
<Badge>{stations.length}</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className='overflow-y-auto' style={{ height: 'calc(100% - 60px)' }}>
|
||||||
className='overflow-y-auto'
|
|
||||||
style={{ height: 'calc(100% - 60px)' }}
|
|
||||||
ref={scrollContainerRef}
|
|
||||||
>
|
|
||||||
{stations.length > 0 ? (
|
{stations.length > 0 ? (
|
||||||
<div className='p-2'>
|
<div className='p-2'>
|
||||||
{stations.map((station) => {
|
{stations.map((station) => {
|
||||||
@ -269,13 +240,12 @@ function StationListPanel({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={station.id}
|
key={station.id}
|
||||||
id={`station_${station.id}`}
|
|
||||||
className={`mb-2 cursor-pointer rounded-lg border p-3 transition-colors ${
|
className={`mb-2 cursor-pointer rounded-lg border p-3 transition-colors ${
|
||||||
selectedStation === station.id
|
selectedStation === station.id
|
||||||
? 'border-blue-500 bg-blue-50'
|
? 'border-blue-500 bg-blue-50'
|
||||||
: 'border-gray-200 hover:bg-gray-50'
|
: 'border-gray-200 hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleMapStationClick(station.id)}
|
onClick={() => setSelectedStation(station.id)}
|
||||||
>
|
>
|
||||||
<div className='flex items-start justify-between'>
|
<div className='flex items-start justify-between'>
|
||||||
<h4 className='font-medium'>{station.name}</h4>
|
<h4 className='font-medium'>{station.name}</h4>
|
||||||
@ -365,14 +335,9 @@ export default function GasStationMap({ stations }: GasStationMapProps) {
|
|||||||
const [isStationListOpen, setIsStationListOpen] = useState(false);
|
const [isStationListOpen, setIsStationListOpen] = useState(false);
|
||||||
const [activeFilterTab, setActiveFilterTab] = useState('cities');
|
const [activeFilterTab, setActiveFilterTab] = useState('cities');
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedStation === null) return;
|
|
||||||
setIsStationListOpen(true);
|
|
||||||
}, [selectedStation]);
|
|
||||||
|
|
||||||
// Все доступные фильтры
|
// Все доступные фильтры
|
||||||
const allFilters = [
|
const allFilters = [
|
||||||
'ДТ',
|
// 'ДТ', -> нет значения в интерфейсе - TODO: поправить
|
||||||
'АИ-92',
|
'АИ-92',
|
||||||
'АИ-95',
|
'АИ-95',
|
||||||
'Z-100 Power',
|
'Z-100 Power',
|
||||||
@ -386,7 +351,6 @@ export default function GasStationMap({ stations }: GasStationMapProps) {
|
|||||||
// Маппинг фильтров на поля Station
|
// Маппинг фильтров на поля Station
|
||||||
const filterToFieldMap: { [key: string]: keyof Stations[number] } = {
|
const filterToFieldMap: { [key: string]: keyof Stations[number] } = {
|
||||||
'АИ-92': 'ai92',
|
'АИ-92': 'ai92',
|
||||||
ДТ: 'dt',
|
|
||||||
'АИ-95': 'ai95',
|
'АИ-95': 'ai95',
|
||||||
'Z-100 Power': 'z100',
|
'Z-100 Power': 'z100',
|
||||||
Пропан: 'propan',
|
Пропан: 'propan',
|
||||||
@ -440,16 +404,6 @@ export default function GasStationMap({ stations }: GasStationMapProps) {
|
|||||||
[filteredStations],
|
[filteredStations],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleMapStationClick = (stationId: number) => {
|
|
||||||
setSelectedStation(() => {
|
|
||||||
if (selectedStation !== null && selectedStation === stationId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return stationId;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Переключение фильтра услуг
|
// Переключение фильтра услуг
|
||||||
const toggleFilter = (filter: string) => {
|
const toggleFilter = (filter: string) => {
|
||||||
setActiveFilters((prev) =>
|
setActiveFilters((prev) =>
|
||||||
@ -500,6 +454,7 @@ export default function GasStationMap({ stations }: GasStationMapProps) {
|
|||||||
setActiveFilterTab={setActiveFilterTab}
|
setActiveFilterTab={setActiveFilterTab}
|
||||||
resetFilters={resetFilters}
|
resetFilters={resetFilters}
|
||||||
resetCities={resetCities}
|
resetCities={resetCities}
|
||||||
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Station list panel */}
|
{/* Station list panel */}
|
||||||
@ -510,7 +465,8 @@ export default function GasStationMap({ stations }: GasStationMapProps) {
|
|||||||
selectedStation={selectedStation}
|
selectedStation={selectedStation}
|
||||||
activeFilters={activeFilters}
|
activeFilters={activeFilters}
|
||||||
activeCities={activeCities}
|
activeCities={activeCities}
|
||||||
handleMapStationClick={handleMapStationClick}
|
setSelectedStation={setSelectedStation}
|
||||||
|
t={t}
|
||||||
filterToFieldMap={filterToFieldMap}
|
filterToFieldMap={filterToFieldMap}
|
||||||
allFilters={allFilters}
|
allFilters={allFilters}
|
||||||
resetFilters={resetFilters}
|
resetFilters={resetFilters}
|
||||||
@ -519,11 +475,7 @@ export default function GasStationMap({ stations }: GasStationMapProps) {
|
|||||||
|
|
||||||
{/* Map */}
|
{/* Map */}
|
||||||
<div className='h-full w-full'>
|
<div className='h-full w-full'>
|
||||||
<YandexMap
|
<YandexMap points={points} />
|
||||||
points={points}
|
|
||||||
selectedStation={selectedStation}
|
|
||||||
handleMapStationClick={handleMapStationClick}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Control buttons */}
|
{/* Control buttons */}
|
||||||
|
|||||||
@ -1,60 +1,28 @@
|
|||||||
import {
|
'use client';
|
||||||
GeolocationControl,
|
|
||||||
Map,
|
import { Map, Placemark, YMaps } from '@pbe/react-yandex-maps';
|
||||||
Placemark,
|
import React from 'react';
|
||||||
YMaps,
|
|
||||||
} from '@pbe/react-yandex-maps';
|
|
||||||
import { isEmpty } from 'lodash';
|
|
||||||
import { useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
import { Point } from '../model';
|
import { Point } from '../model';
|
||||||
|
|
||||||
type YandexMapProps = {
|
type YandexMapProps = {
|
||||||
points: Point[];
|
points: Point[];
|
||||||
selectedStation: number | null;
|
|
||||||
handleMapStationClick: (id: number) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapCenter = [38.53575, 68.77905];
|
const mapCenter = [55.751574, 37.573856];
|
||||||
|
|
||||||
export const YandexMap = ({
|
|
||||||
points,
|
|
||||||
selectedStation,
|
|
||||||
handleMapStationClick,
|
|
||||||
}: YandexMapProps) => {
|
|
||||||
const mapRef = useRef<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!mapRef.current) return;
|
|
||||||
|
|
||||||
if (selectedStation !== null) {
|
|
||||||
const selectedPoint = points.find(
|
|
||||||
(point) => point.id === selectedStation,
|
|
||||||
);
|
|
||||||
if (selectedPoint) {
|
|
||||||
mapRef.current
|
|
||||||
.setCenter(selectedPoint.coordinates, mapRef.current.getZoom(), {
|
|
||||||
duration: 1000,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
mapRef.current.setZoom(13, { duration: 300 });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mapRef.current.setZoom(11, { duration: 300 });
|
|
||||||
}
|
|
||||||
}, [selectedStation, points]);
|
|
||||||
|
|
||||||
|
export const YandexMap = ({ points }: YandexMapProps) => {
|
||||||
return (
|
return (
|
||||||
<YMaps
|
<YMaps
|
||||||
query={{
|
query={{
|
||||||
apikey: process.env.NEXT_PUBLIC_YANDEX_MAP_API_KEY,
|
apikey: process.env.NEXT_PUBLIC_YANDEX_MAP_API_KEY,
|
||||||
lang: 'ru_RU',
|
lang: 'ru_RU',
|
||||||
|
// load: 'geoObject.addon.balloon',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Map
|
<Map
|
||||||
defaultState={{
|
defaultState={{
|
||||||
center: !isEmpty(points) ? points[0].coordinates : mapCenter,
|
center: points[0].coordinates || mapCenter,
|
||||||
zoom: 11,
|
zoom: 11,
|
||||||
behaviors: ['drag', 'multiTouch', 'dblClickZoom', 'scrollZoom'],
|
behaviors: ['drag', 'multiTouch', 'dblClickZoom', 'scrollZoom'],
|
||||||
}}
|
}}
|
||||||
@ -66,37 +34,19 @@ export const YandexMap = ({
|
|||||||
suppressObsoleteBrowserNotifier: true,
|
suppressObsoleteBrowserNotifier: true,
|
||||||
}}
|
}}
|
||||||
className='h-full max-h-[500px] w-full overflow-hidden rounded-md shadow-lg'
|
className='h-full max-h-[500px] w-full overflow-hidden rounded-md shadow-lg'
|
||||||
instanceRef={(ref) => {
|
|
||||||
mapRef.current = ref;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{points.map((point) => {
|
{points.map((point) => (
|
||||||
const isSelectedStation = selectedStation === point.id;
|
<Placemark
|
||||||
return (
|
key={point.id}
|
||||||
<Placemark
|
geometry={point.coordinates}
|
||||||
key={point.id}
|
options={{
|
||||||
geometry={point.coordinates}
|
iconLayout: 'default#image',
|
||||||
options={{
|
iconImageHref: '/map/oriyo-marker.png',
|
||||||
iconLayout: 'default#image',
|
iconImageSize: [64, 64],
|
||||||
iconImageHref:
|
iconImageOffset: [-24, -36],
|
||||||
!selectedStation || isSelectedStation
|
}}
|
||||||
? '/map/oriyo-marker.png'
|
/>
|
||||||
: '/map/oriyo-inactive-marker.png',
|
))}
|
||||||
iconImageSize: isSelectedStation ? [70, 70] : [64, 64],
|
|
||||||
iconImageOffset: isSelectedStation ? [-28, -40] : [-24, -36],
|
|
||||||
}}
|
|
||||||
onClick={() => handleMapStationClick(point.id)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<GeolocationControl
|
|
||||||
options={{
|
|
||||||
position: {
|
|
||||||
bottom: 20,
|
|
||||||
right: 20,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Map>
|
</Map>
|
||||||
</YMaps>
|
</YMaps>
|
||||||
);
|
);
|
||||||
|
|||||||
59
src/features/pages/api/pages.api.ts
Normal file
59
src/features/pages/api/pages.api.ts
Normal 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),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
@ -1,193 +0,0 @@
|
|||||||
import {
|
|
||||||
AboutUsPageData,
|
|
||||||
CertificatesPageData,
|
|
||||||
CharityPageData,
|
|
||||||
MainPageData,
|
|
||||||
} from '@/app/api-utlities/@types/pages';
|
|
||||||
import {
|
|
||||||
presentCertificatesFromTaylor,
|
|
||||||
presentCharitiesFromTaylor,
|
|
||||||
presentDiscountsFromTaylor,
|
|
||||||
presentHistoryItemsFromTaylor,
|
|
||||||
presentJobsFromTaylor,
|
|
||||||
presentMediaFromTaylor,
|
|
||||||
presentPartnersFromTaylor,
|
|
||||||
presentReviewsFromTaylor,
|
|
||||||
presentStationsFromTaylor,
|
|
||||||
presentTeamMembersFromTaylor,
|
|
||||||
presentTextsFromTaylor,
|
|
||||||
} from '@/app/api-utlities/presenters/taylor-presenters';
|
|
||||||
|
|
||||||
import { taylorQueryBuilder } from '@/shared/api/taylor-query-builder';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches main page content using TaylorDB query builder
|
|
||||||
* Replaces the RTK Query GraphQL approach with type-safe query builder
|
|
||||||
*/
|
|
||||||
export async function fetchMainPageContent(): Promise<MainPageData> {
|
|
||||||
// Use batch queries to fetch all data in a single request
|
|
||||||
const [partnersData, jobsData, discountsData, stationsData] =
|
|
||||||
await taylorQueryBuilder
|
|
||||||
.batch([
|
|
||||||
// Fetch partners
|
|
||||||
taylorQueryBuilder
|
|
||||||
.selectFrom('partnyory')
|
|
||||||
.selectAll()
|
|
||||||
.with({
|
|
||||||
izobrozhenie: (qb) => qb.selectAll(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Fetch jobs
|
|
||||||
taylorQueryBuilder
|
|
||||||
.selectFrom('vakansii')
|
|
||||||
.selectAll()
|
|
||||||
.with({
|
|
||||||
tegi: (qb) => qb.select(['name']),
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Fetch discounts
|
|
||||||
taylorQueryBuilder
|
|
||||||
.selectFrom('akcii')
|
|
||||||
.selectAll()
|
|
||||||
.with({
|
|
||||||
foto: (qb) => qb.selectAll(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Fetch stations
|
|
||||||
taylorQueryBuilder
|
|
||||||
.selectFrom('azs')
|
|
||||||
.selectAll()
|
|
||||||
.with({
|
|
||||||
foto: (qb) => qb.selectAll(),
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
console.log('Loading main page content...');
|
|
||||||
|
|
||||||
return {
|
|
||||||
partners: presentPartnersFromTaylor(partnersData),
|
|
||||||
jobs: presentJobsFromTaylor(jobsData),
|
|
||||||
discounts: presentDiscountsFromTaylor(discountsData),
|
|
||||||
stations: presentStationsFromTaylor(stationsData),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches about us page content using TaylorDB query builder
|
|
||||||
*/
|
|
||||||
export async function fetchAboutUsPageContent(): Promise<AboutUsPageData> {
|
|
||||||
// Use batch queries to fetch all data in a single request
|
|
||||||
const [teamData, historyData, stationsData, reviewsData] =
|
|
||||||
await taylorQueryBuilder
|
|
||||||
.batch([
|
|
||||||
// Fetch team members
|
|
||||||
taylorQueryBuilder
|
|
||||||
.selectFrom('komanda')
|
|
||||||
.selectAll()
|
|
||||||
.with({
|
|
||||||
foto: (qb) => qb.selectAll(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Fetch history items
|
|
||||||
taylorQueryBuilder.selectFrom('istoriyaKompanii').selectAll(),
|
|
||||||
|
|
||||||
// Fetch stations
|
|
||||||
taylorQueryBuilder
|
|
||||||
.selectFrom('azs')
|
|
||||||
.selectAll()
|
|
||||||
.where('foto', 'isNotEmpty')
|
|
||||||
.with({
|
|
||||||
foto: (qb) => qb.selectAll(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Fetch reviews (filtered by published status)
|
|
||||||
taylorQueryBuilder
|
|
||||||
.selectFrom('otzyvy')
|
|
||||||
.selectAll()
|
|
||||||
.where('status', '=', 'Опубликовано'),
|
|
||||||
])
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
console.log('Loading about us page content...');
|
|
||||||
|
|
||||||
return {
|
|
||||||
team: presentTeamMembersFromTaylor(teamData),
|
|
||||||
history: presentHistoryItemsFromTaylor(historyData),
|
|
||||||
stations: presentStationsFromTaylor(stationsData),
|
|
||||||
reviews: presentReviewsFromTaylor(reviewsData),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches charity page content using TaylorDB query builder
|
|
||||||
*/
|
|
||||||
export async function fetchCharityPageContent(): Promise<CharityPageData> {
|
|
||||||
const charitiesData = await taylorQueryBuilder
|
|
||||||
.selectFrom('blagotvoritelnyjFond')
|
|
||||||
.selectAll()
|
|
||||||
.with({
|
|
||||||
foto: (qb) => qb.selectAll(),
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
console.log('Loading charity page content...');
|
|
||||||
|
|
||||||
return {
|
|
||||||
charities: presentCharitiesFromTaylor(charitiesData),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches certificates page content using TaylorDB query builder
|
|
||||||
*/
|
|
||||||
export async function fetchCertificatesPageContent(): Promise<CertificatesPageData> {
|
|
||||||
const certificatesData = await taylorQueryBuilder
|
|
||||||
.selectFrom('sertifikaty')
|
|
||||||
.selectAll()
|
|
||||||
.with({
|
|
||||||
foto: (qb) => qb.selectAll(),
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
console.log('Loading certificates page content...');
|
|
||||||
|
|
||||||
return {
|
|
||||||
certificates: presentCertificatesFromTaylor(certificatesData),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches text content using TaylorDB query builder
|
|
||||||
*/
|
|
||||||
export async function fetchTextContent(): Promise<
|
|
||||||
Array<{ key: string; value: string | null }>
|
|
||||||
> {
|
|
||||||
const textsData = await taylorQueryBuilder
|
|
||||||
.selectFrom('tekstovyjKontentSajta')
|
|
||||||
.selectAll()
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
console.log('Loading text content...');
|
|
||||||
|
|
||||||
return presentTextsFromTaylor(textsData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches media content using TaylorDB query builder
|
|
||||||
*/
|
|
||||||
export async function fetchMediaContent(): Promise<
|
|
||||||
Array<{ key: string; name: string; photo: string | null }>
|
|
||||||
> {
|
|
||||||
const mediaData = await taylorQueryBuilder
|
|
||||||
.selectFrom('mediaKontentSajta')
|
|
||||||
.selectAll()
|
|
||||||
.with({
|
|
||||||
foto: (qb) => qb.selectAll(),
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
console.log('Loading media content...');
|
|
||||||
|
|
||||||
return presentMediaFromTaylor(mediaData);
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import { baseAPI } from '@/shared/api/base-api';
|
|
||||||
|
|
||||||
import { ReviewFormValues } from '../model/review-form.schema';
|
|
||||||
|
|
||||||
export const reviewsAPI = baseAPI.injectEndpoints({
|
|
||||||
endpoints: (build) => ({
|
|
||||||
createReview: build.mutation<void, ReviewFormValues>({
|
|
||||||
query: (body) => ({
|
|
||||||
url: 'reviews/create',
|
|
||||||
method: 'POST',
|
|
||||||
body,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { useCreateReviewMutation } = reviewsAPI;
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const reviewSchema = z.object({
|
|
||||||
name: z
|
|
||||||
.string()
|
|
||||||
.min(2, { message: 'Имя должно содержать не менее 2 символов' }),
|
|
||||||
rating: z.number().min(1, { message: 'Пожалуйста, выберите рейтинг' }).max(5),
|
|
||||||
reviewMessage: z
|
|
||||||
.string()
|
|
||||||
.min(10, { message: 'Отзыв должен содержать не менее 10 символов' }),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ReviewFormValues = z.infer<typeof reviewSchema>;
|
|
||||||
@ -1,217 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { Loader2, Plus, Star } from 'lucide-react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
import { useTextController } from '@/shared/language';
|
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@/shared/shadcn-ui/dialog';
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from '@/shared/shadcn-ui/form';
|
|
||||||
import { Input } from '@/shared/shadcn-ui/input';
|
|
||||||
import { Textarea } from '@/shared/shadcn-ui/textarea';
|
|
||||||
|
|
||||||
import { useCreateReviewMutation } from '../api/reviews.api';
|
|
||||||
import { ReviewFormValues, reviewSchema } from '../model/review-form.schema';
|
|
||||||
|
|
||||||
export function ReviewForm() {
|
|
||||||
const { t } = useTextController();
|
|
||||||
const [openReviewFormDialog, setOpenReviewFormDialog] = useState(false);
|
|
||||||
const [hoveredStar, setHoveredStar] = useState(0);
|
|
||||||
|
|
||||||
const [createReview, { isLoading: isSubmitting }] = useCreateReviewMutation();
|
|
||||||
|
|
||||||
const form = useForm<ReviewFormValues>({
|
|
||||||
resolver: zodResolver(reviewSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: '',
|
|
||||||
rating: 0,
|
|
||||||
reviewMessage: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async (data: ReviewFormValues) => {
|
|
||||||
try {
|
|
||||||
await createReview(data);
|
|
||||||
|
|
||||||
toast.success(t('about.review-form.dialog.successResponse'), {
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
|
|
||||||
form.reset();
|
|
||||||
setOpenReviewFormDialog(false);
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(t('about.review-form.dialog.errorResponse'), {
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const StarRating = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
value: number;
|
|
||||||
onChange: (value: number) => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{[1, 2, 3, 4, 5].map((star) => (
|
|
||||||
<button
|
|
||||||
key={star}
|
|
||||||
type='button'
|
|
||||||
onClick={() => onChange(star)}
|
|
||||||
onMouseEnter={() => setHoveredStar(star)}
|
|
||||||
onMouseLeave={() => setHoveredStar(0)}
|
|
||||||
className='cursor-pointer transition-transform hover:scale-110 focus:outline-none'
|
|
||||||
aria-label={`Rate ${star} stars out of 5`}
|
|
||||||
>
|
|
||||||
<Star
|
|
||||||
className={`h-8 w-8 ${
|
|
||||||
star <= (hoveredStar || value)
|
|
||||||
? 'fill-yellow-400 text-yellow-400'
|
|
||||||
: 'text-gray-300'
|
|
||||||
} transition-colors`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Dialog
|
|
||||||
open={openReviewFormDialog}
|
|
||||||
onOpenChange={setOpenReviewFormDialog}
|
|
||||||
>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button className='flex shadow-lg transition-all duration-300 hover:scale-105'>
|
|
||||||
<Plus />
|
|
||||||
<span>{t('common.buttons.addReview')}</span>
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className='overflow-hidden rounded-xl border-none bg-white/95 p-0 shadow-xl backdrop-blur-sm sm:max-w-[500px]'>
|
|
||||||
<div className='p-6'>
|
|
||||||
<DialogHeader className='pb-4'>
|
|
||||||
<DialogTitle className='text-center text-2xl font-bold'>
|
|
||||||
{t('about.review-form.dialog.title')}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription className='pt-2 text-center'>
|
|
||||||
{t('about.review-form.dialog.description')}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className='space-y-6'
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='name'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className='flex flex-col'>
|
|
||||||
<FormLabel>
|
|
||||||
{t('about.review-form.dialog.field.name')}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder={t(
|
|
||||||
'about.review-form.dialog.field.name.placeholder',
|
|
||||||
)}
|
|
||||||
{...field}
|
|
||||||
className='bg-white/50'
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='rating'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className='space-y-3'>
|
|
||||||
<FormLabel className='block'>
|
|
||||||
{t('about.review-form.dialog.field.rating')}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<StarRating
|
|
||||||
value={field.value}
|
|
||||||
onChange={(value) => field.onChange(value)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='reviewMessage'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className='flex flex-col'>
|
|
||||||
<FormLabel>
|
|
||||||
{t('about.review-form.dialog.field.reviewMessage')}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
placeholder={t(
|
|
||||||
'about.review-form.dialog.field.reviewMessage.placeholder',
|
|
||||||
)}
|
|
||||||
className='min-h-[120px] resize-none bg-white/50'
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t('about.review-form.dialog.noteMessage')}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DialogFooter className='pt-2'>
|
|
||||||
<Button
|
|
||||||
type='submit'
|
|
||||||
className='w-full'
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
|
||||||
Отправка...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
t('common.buttons.sendReview')
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,17 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { deleteCookie } from 'cookies-next';
|
import { subMonths } from 'date-fns';
|
||||||
import { Building2, LogOut, Wallet } from 'lucide-react';
|
import { Building2, LogOut, Wallet } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import {
|
import { useFetchMyCorporateInfoQuery } from '@/entities/corporate/api/corporate.api';
|
||||||
useFetchCorporateTransactionsQuery,
|
|
||||||
useFetchMyCorporateInfoQuery,
|
|
||||||
} from '@/entities/corporate/api/corporate.api';
|
|
||||||
import { CorporateTransactionRequest } from '@/entities/corporate/model/types/corporate-transactions.type';
|
|
||||||
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { formatDate } from '@/shared/lib/format-date';
|
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -20,45 +15,98 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/shared/shadcn-ui/card';
|
} from '@/shared/shadcn-ui/card';
|
||||||
import { TableCell, TableHead, TableRow } from '@/shared/shadcn-ui/table';
|
|
||||||
|
|
||||||
import { TransactionsTable } from '@/widgets/transactions-table';
|
import { TransactionsTable } from '@/widgets/transactions-table';
|
||||||
|
|
||||||
|
// import { CardsList } from '@/widgets/cards-list';
|
||||||
|
|
||||||
|
// Sample company data
|
||||||
|
const companyData = {
|
||||||
|
companyName: 'ООО «ТаджикТранс»',
|
||||||
|
numberOfCards: 12,
|
||||||
|
fund: 25000,
|
||||||
|
overdraft: 5000,
|
||||||
|
totalFund: 30000,
|
||||||
|
registrationDate: '10.03.2019',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sample transaction data
|
||||||
|
const generateTransactions = () => {
|
||||||
|
const stations = [
|
||||||
|
'АЗС Душанбе-Центр',
|
||||||
|
'АЗС Душанбе-Запад',
|
||||||
|
'АЗС Душанбе-Восток',
|
||||||
|
'АЗС Худжанд',
|
||||||
|
'АЗС Куляб',
|
||||||
|
];
|
||||||
|
|
||||||
|
const products = [
|
||||||
|
{ name: 'ДТ', price: 8.5 },
|
||||||
|
{ name: 'АИ-92', price: 9.2 },
|
||||||
|
{ name: 'АИ-95', price: 10.5 },
|
||||||
|
{ name: 'Z-100 Power', price: 11.8 },
|
||||||
|
{ name: 'Пропан', price: 6.3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const transactions = [];
|
||||||
|
|
||||||
|
// Generate 50 random transactions over the last 6 months
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
const date = subMonths(new Date(), Math.random() * 6);
|
||||||
|
const station = stations[Math.floor(Math.random() * stations.length)];
|
||||||
|
const product = products[Math.floor(Math.random() * products.length)];
|
||||||
|
const quantity = Math.floor(Math.random() * 40) + 10; // 10-50 liters
|
||||||
|
const cost = product.price;
|
||||||
|
const total = quantity * cost;
|
||||||
|
|
||||||
|
transactions.push({
|
||||||
|
id: i + 1,
|
||||||
|
date,
|
||||||
|
station,
|
||||||
|
product: product.name,
|
||||||
|
quantity,
|
||||||
|
cost,
|
||||||
|
total,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by date (newest first)
|
||||||
|
return transactions.sort((a, b) => b.date.getTime() - a.date.getTime());
|
||||||
|
};
|
||||||
|
|
||||||
|
const transactions = generateTransactions();
|
||||||
|
|
||||||
export function CorporateDashboard() {
|
export function CorporateDashboard() {
|
||||||
const { t } = useTextController();
|
const [startDate, setStartDate] = useState<Date | undefined>(
|
||||||
|
subMonths(new Date(), 1),
|
||||||
|
);
|
||||||
|
const [endDate, setEndDate] = useState<Date | undefined>(new Date());
|
||||||
|
const [filteredTransactions, setFilteredTransactions] =
|
||||||
|
useState(transactions);
|
||||||
|
|
||||||
const { data } = useFetchMyCorporateInfoQuery({});
|
// Filter transactions by date range
|
||||||
|
const filterTransactions = () => {
|
||||||
|
if (!startDate || !endDate) return;
|
||||||
|
|
||||||
const [request, setTransactionFetchRequest] =
|
const filtered = transactions.filter((transaction) => {
|
||||||
useState<CorporateTransactionRequest>({
|
const transactionDate = new Date(transaction.date);
|
||||||
limit: 10,
|
return transactionDate >= startDate && transactionDate <= endDate;
|
||||||
page: 1,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
setFilteredTransactions(filtered);
|
||||||
data: transactionsResponse,
|
};
|
||||||
refetch,
|
|
||||||
isFetching,
|
|
||||||
} = useFetchCorporateTransactionsQuery(request);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const { t } = useTextController();
|
||||||
refetch();
|
|
||||||
}, [request]);
|
const { data, isLoading } = useFetchMyCorporateInfoQuery({});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-h-screen flex-col px-2.5'>
|
<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-4 flex flex-col items-start gap-4 sm:mb-8 sm:flex-row sm:items-center sm:justify-between'>
|
<div className='mb-4 flex flex-col gap-4 items-start sm:justify-between sm:items-center sm:mb-8 sm:flex-row'>
|
||||||
<h1 className='text-3xl font-bold'>{t('corporate.pageTitle')}</h1>
|
<h1 className='text-3xl font-bold'>{t('corporate.pageTitle')}</h1>
|
||||||
<Button
|
<Button variant='outline' className='gap-2'>
|
||||||
variant='outline'
|
|
||||||
className='gap-2'
|
|
||||||
onClick={() => {
|
|
||||||
deleteCookie(`corporate__token`);
|
|
||||||
window.location.reload();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LogOut className='h-4 w-4' />
|
<LogOut className='h-4 w-4' />
|
||||||
{t('corporate.logoutButton')}
|
{t('corporate.logoutButton')}
|
||||||
</Button>
|
</Button>
|
||||||
@ -66,11 +114,7 @@ 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
|
<Card data-aos='zoom-in' data-aos-mirror="true" className='md:col-span-2'>
|
||||||
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' />
|
||||||
@ -134,11 +178,7 @@ export function CorporateDashboard() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Fund Card */}
|
{/* Fund Card */}
|
||||||
<Card
|
<Card data-aos='zoom-in' data-aos-mirror="true" className='bg-gradient-to-br from-red-600 to-red-800 text-white'>
|
||||||
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' />
|
||||||
@ -165,66 +205,11 @@ export function CorporateDashboard() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TransactionsTable
|
{/* <CardsList totalCards={companyData.numberOfCards} /> */}
|
||||||
isLoading={isFetching}
|
|
||||||
renderHeaders={() => (
|
{/* Transactions */}
|
||||||
<TableRow>
|
|
||||||
<TableHead>
|
<TransactionsTable />
|
||||||
{t('corporate.transactions.tableHeaders.date')}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead>
|
|
||||||
{t('corporate.transactions.tableHeaders.card')}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead>
|
|
||||||
{t('corporate.transactions.tableHeaders.station')}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead>
|
|
||||||
{t('corporate.transactions.tableHeaders.product')}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className='text-right'>
|
|
||||||
{t('corporate.transactions.tableHeaders.quantity')}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className='text-right'>
|
|
||||||
{t('corporate.transactions.tableHeaders.price')}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className='text-right'>
|
|
||||||
{t('corporate.transactions.tableHeaders.total')}
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
renderRow={(transaction, index) => (
|
|
||||||
<TableRow key={index}>
|
|
||||||
<TableCell>
|
|
||||||
{formatDate(
|
|
||||||
new Date(transaction.date_create),
|
|
||||||
'dd.MM.yyyy HH:mm',
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{transaction.uid}</TableCell>
|
|
||||||
<TableCell>{transaction.station_name}</TableCell>
|
|
||||||
<TableCell>{transaction.product_name}</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
{transaction.amount}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
{transaction.price_real} {t('corporate.currency')}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right font-medium'>
|
|
||||||
{transaction.sum_real} {t('corporate.currency')}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
data={
|
|
||||||
transactionsResponse || {
|
|
||||||
limit: 10,
|
|
||||||
current_page: 1,
|
|
||||||
total_pages: 0,
|
|
||||||
total_records: 0,
|
|
||||||
transactions: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onChange={setTransactionFetchRequest}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,18 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { deleteCookie } from 'cookies-next';
|
|
||||||
import { ArrowUpRight, Clock, CreditCard, LogOut, User } from 'lucide-react';
|
import { ArrowUpRight, Clock, CreditCard, LogOut, User } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import {
|
import { useFetchMyBonusInfoQuery } from '@/entities/bonus/api/bonus.api';
|
||||||
useFetchBonusTransactionsQuery,
|
|
||||||
useFetchMyBonusInfoQuery,
|
|
||||||
} from '@/entities/bonus/api/bonus.api';
|
|
||||||
import { BonusTransactionRequest } from '@/entities/bonus/model/types/bonus-transactions.type';
|
|
||||||
|
|
||||||
import Loader from '@/shared/components/loader';
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { formatDate } from '@/shared/lib/format-date';
|
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -21,58 +13,31 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/shared/shadcn-ui/card';
|
} from '@/shared/shadcn-ui/card';
|
||||||
import { TableCell, TableHead, TableRow } from '@/shared/shadcn-ui/table';
|
|
||||||
|
|
||||||
import { TransactionsTable } from '@/widgets/transactions-table';
|
import { TransactionsTable } from '@/widgets/transactions-table';
|
||||||
|
import Loader from '@/shared/components/loader';
|
||||||
|
|
||||||
export function CustomerDashboard() {
|
export function CustomerDashboard() {
|
||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
|
|
||||||
const [request, setTransactionFetchRequest] =
|
|
||||||
useState<BonusTransactionRequest>({
|
|
||||||
limit: 10,
|
|
||||||
page: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data, isLoading } = useFetchMyBonusInfoQuery({});
|
const { data, isLoading } = useFetchMyBonusInfoQuery({});
|
||||||
|
|
||||||
const {
|
|
||||||
data: transactionsResponse,
|
|
||||||
refetch,
|
|
||||||
isFetching: isTransactionLoading,
|
|
||||||
} = useFetchBonusTransactionsQuery(request);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refetch();
|
|
||||||
}, [request]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-h-screen flex-col px-2.5'>
|
<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'>
|
||||||
<h1 className='text-3xl font-bold'>{t('customer.pageTitle')}</h1>
|
<h1 className='text-3xl font-bold'>{t('customer.pageTitle')}</h1>
|
||||||
<Button
|
<Button variant='outline' className='gap-2'>
|
||||||
variant='outline'
|
|
||||||
className='gap-2'
|
|
||||||
onClick={() => {
|
|
||||||
deleteCookie(`bonus__token`);
|
|
||||||
window.location.reload();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LogOut className='h-4 w-4' />
|
<LogOut className='h-4 w-4' />
|
||||||
{t('customer.logoutButton')}
|
{t('customer.logoutButton')}
|
||||||
</Button>
|
</Button>
|
||||||
</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'>
|
||||||
<Card
|
<Card data-aos="zoom-in" data-aos-mirror="true" className='bg-gradient-to-br from-red-600 to-red-800 text-white'>
|
||||||
data-aos='zoom-in'
|
|
||||||
data-aos-mirror='true'
|
|
||||||
className='bg-gradient-to-br from-red-600 to-red-800 text-white'
|
|
||||||
>
|
|
||||||
{!data || isLoading ? (
|
{!data || isLoading ? (
|
||||||
<Loader />
|
<Loader/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@ -108,11 +73,7 @@ export function CustomerDashboard() {
|
|||||||
|
|
||||||
{/* Bonus Card */}
|
{/* Bonus Card */}
|
||||||
{/* Customer Card */}
|
{/* Customer Card */}
|
||||||
<Card
|
<Card data-aos="zoom-in" data-aos-mirror="true" className='md:col-span-2'>
|
||||||
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' />
|
||||||
@ -121,7 +82,7 @@ export function CustomerDashboard() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{!data || isLoading ? (
|
{!data || isLoading ? (
|
||||||
<Loader />
|
<Loader/>
|
||||||
) : (
|
) : (
|
||||||
<div className='grid gap-6 md:grid-cols-2'>
|
<div className='grid gap-6 md:grid-cols-2'>
|
||||||
<div>
|
<div>
|
||||||
@ -154,59 +115,7 @@ export function CustomerDashboard() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TransactionsTable
|
<TransactionsTable />
|
||||||
isLoading={isTransactionLoading}
|
|
||||||
data={
|
|
||||||
transactionsResponse || {
|
|
||||||
limit: 10,
|
|
||||||
current_page: 1,
|
|
||||||
total_pages: 0,
|
|
||||||
total_records: 0,
|
|
||||||
transactions: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
renderHeaders={() => (
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>
|
|
||||||
{t('corporate.transactions.tableHeaders.date')}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead>
|
|
||||||
{t('corporate.transactions.tableHeaders.station')}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead>
|
|
||||||
{t('corporate.transactions.tableHeaders.product')}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className='text-right'>
|
|
||||||
{t('corporate.transactions.tableHeaders.quantity')}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className='text-right'>
|
|
||||||
{t('corporate.transactions.tableHeaders.price')}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className='text-right'>
|
|
||||||
{t('corporate.transactions.tableHeaders.total')}
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
renderRow={(transaction) => (
|
|
||||||
<TableRow key={transaction.id}>
|
|
||||||
<TableCell>
|
|
||||||
{formatDate(transaction.date_create, 'dd.MM.yyyy HH:mm')}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{transaction.station}</TableCell>
|
|
||||||
<TableCell>{transaction.product_name}</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
{transaction.amount}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
{transaction.price_real} {t('corporate.currency')}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right font-medium'>
|
|
||||||
{transaction.sum_real} {t('corporate.currency')}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
onChange={setTransactionFetchRequest}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,31 +2,30 @@
|
|||||||
|
|
||||||
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 Link from 'next/link';
|
|
||||||
|
|
||||||
import { AboutUsPageData } from '@/app/api-utlities/@types/pages';
|
import { AboutUsPageData } from '@/app/api-utlities/@types/about-us';
|
||||||
|
|
||||||
import { ReviewForm } from '@/features/review-form/ui';
|
|
||||||
|
|
||||||
import AnimatedCounter from '@/shared/components/animated-counter';
|
import AnimatedCounter from '@/shared/components/animated-counter';
|
||||||
import { Container } from '@/shared/components/container';
|
|
||||||
import { Review } from '@/shared/components/review';
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { useMediaController } from '@/shared/media/hooks/use-media-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';
|
||||||
import { CtaSection } from '@/widgets/cta-section';
|
import { CtaSection } from '@/widgets/cta-section';
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'about.metadata.title',
|
||||||
|
description: 'about.metadata.description',
|
||||||
|
};
|
||||||
|
|
||||||
export interface AboutPageProps {
|
export interface AboutPageProps {
|
||||||
content: AboutUsPageData;
|
content: AboutUsPageData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AboutPage({ content }: AboutPageProps) {
|
export default function AboutPage({ content }: AboutPageProps) {
|
||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
const { m } = useMediaController();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-h-screen flex-col'>
|
<div className='flex min-h-screen flex-col'>
|
||||||
@ -35,14 +34,12 @@ export default function AboutPage({ content }: AboutPageProps) {
|
|||||||
<section className='relative'>
|
<section className='relative'>
|
||||||
<div className='relative h-[400px] w-full overflow-hidden'>
|
<div className='relative h-[400px] w-full overflow-hidden'>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src='/placeholder.svg?height=400&width=1920&text=Наша+История'
|
||||||
m('about.hero-section.banner') ||
|
|
||||||
'/placeholder.svg?height=400&width=1920&text=Наша+История'
|
|
||||||
}
|
|
||||||
alt={t('about.hero.imageAlt')}
|
alt={t('about.hero.imageAlt')}
|
||||||
|
width={1920}
|
||||||
|
height={400}
|
||||||
className='object-cover'
|
className='object-cover'
|
||||||
priority
|
priority
|
||||||
fill
|
|
||||||
/>
|
/>
|
||||||
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30 px-2'>
|
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30 px-2'>
|
||||||
<div
|
<div
|
||||||
@ -65,62 +62,63 @@ export default function AboutPage({ content }: AboutPageProps) {
|
|||||||
|
|
||||||
{/* Company Overview */}
|
{/* Company Overview */}
|
||||||
<Container>
|
<Container>
|
||||||
<div className='grid items-center gap-12 md:grid-cols-2'>
|
<section className='py-16'>
|
||||||
<div data-aos='fade-right'>
|
<div className='container mx-auto'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='grid items-center gap-12 md:grid-cols-2'>
|
||||||
<Fuel className='h-6 w-6 text-red-600' />
|
<div data-aos='fade-right'>
|
||||||
</div>
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
<h2 className='mb-6 text-3xl font-bold tracking-tight sm:text-4xl'>
|
<Fuel className='h-6 w-6 text-red-600' />
|
||||||
{t('about.overview.title')}
|
|
||||||
</h2>
|
|
||||||
<p className='mb-6 text-gray-600'>
|
|
||||||
{t('about.overview.description1')}
|
|
||||||
</p>
|
|
||||||
<p className='mb-6 text-gray-600'>
|
|
||||||
{t('about.overview.description2')}
|
|
||||||
</p>
|
|
||||||
<p className='mb-6 text-gray-600'>
|
|
||||||
{t('about.overview.description3')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className='mb-6 grid grid-cols-1 gap-4 md:grid-cols-2'>
|
|
||||||
{[0, 1, 2, 3].map((index) => (
|
|
||||||
<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'>
|
|
||||||
<span className='text-xs text-white'>✓</span>
|
|
||||||
</div>
|
|
||||||
<div className='ml-3'>
|
|
||||||
<h3 className='text-lg font-medium'>
|
|
||||||
{t(`about.overview.benefits.${index}.title`)}
|
|
||||||
</h3>
|
|
||||||
<p className='text-gray-600'>
|
|
||||||
{t(`about.overview.benefits.${index}.description`)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<h2 className='mb-6 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
|
{t('about.overview.title')}
|
||||||
|
</h2>
|
||||||
|
<p className='mb-6 text-gray-600'>
|
||||||
|
{t('about.overview.description1')}
|
||||||
|
</p>
|
||||||
|
<p className='mb-6 text-gray-600'>
|
||||||
|
{t('about.overview.description2')}
|
||||||
|
</p>
|
||||||
|
<p className='mb-6 text-gray-600'>
|
||||||
|
{t('about.overview.description3')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className='mb-6 grid grid-cols-1 gap-4 md:grid-cols-2'>
|
||||||
|
{[0, 1, 2, 3].map((index) => (
|
||||||
|
<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'>
|
||||||
|
<span className='text-xs text-white'>✓</span>
|
||||||
|
</div>
|
||||||
|
<div className='ml-3'>
|
||||||
|
<h3 className='text-lg font-medium'>
|
||||||
|
{t(`about.overview.benefits.${index}.title`)}
|
||||||
|
</h3>
|
||||||
|
<p className='text-gray-600'>
|
||||||
|
{t(`about.overview.benefits.${index}.description`)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-aos='zoom-out-right'
|
||||||
|
className='relative h-[500px] overflow-hidden rounded-xl shadow-xl'
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src='/placeholder.svg?height=500&width=600&text=Главный+офис'
|
||||||
|
alt={t('about.overview.imageAlt')}
|
||||||
|
fill
|
||||||
|
className='object-cover'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</section>
|
||||||
data-aos='zoom-out-right'
|
|
||||||
className='relative h-[500px] overflow-hidden rounded-xl shadow-xl'
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={
|
|
||||||
m('about.second-section.banner') ||
|
|
||||||
'/placeholder.svg?height=500&width=600&text=Главный+офис'
|
|
||||||
}
|
|
||||||
alt={t('about.overview.imageAlt')}
|
|
||||||
fill
|
|
||||||
className='object-cover'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
{/* Stats Section */}
|
{/* Stats Section */}
|
||||||
<section className='bg-red-600 text-white'>
|
<section className='bg-red-600 py-16 text-white'>
|
||||||
<Container>
|
<div className='container mx-auto'>
|
||||||
<div className='mb-12 text-center'>
|
<div className='mb-12 text-center'>
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
{t('about.stats.title')}
|
{t('about.stats.title')}
|
||||||
@ -149,161 +147,200 @@ export default function AboutPage({ content }: AboutPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Our History */}
|
{/* Our History */}
|
||||||
<Container>
|
<section className='py-16'>
|
||||||
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
<div className='container mx-auto'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<Container>
|
||||||
<History className='h-6 w-6 text-red-600' />
|
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
||||||
</div>
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
<History className='h-6 w-6 text-red-600' />
|
||||||
{t('about.history.title')}
|
</div>
|
||||||
</h2>
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
<p className='max-w-2xl text-gray-600'>
|
{t('about.history.title')}
|
||||||
{t('about.history.subtitle')}
|
</h2>
|
||||||
</p>
|
<p className='max-w-2xl text-gray-600'>
|
||||||
</div>
|
{t('about.history.subtitle')}
|
||||||
</Container>
|
</p>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
|
||||||
<Container>
|
<Container>
|
||||||
<CompanyTimeline timeline={content.history} />
|
<CompanyTimeline timeline={content.history} />
|
||||||
</Container>
|
</Container>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Our Stations */}
|
{/* Our Stations */}
|
||||||
<section className='bg-gray-50'>
|
<Container>
|
||||||
<Container>
|
<section className='bg-gray-50 py-16'>
|
||||||
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
<div className='container mx-auto'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
||||||
<MapPin className='h-6 w-6 text-red-600' />
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
|
<MapPin className='h-6 w-6 text-red-600' />
|
||||||
|
</div>
|
||||||
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
|
{t('about.stations.title')}
|
||||||
|
</h2>
|
||||||
|
<p className='max-w-2xl text-gray-600'>
|
||||||
|
{t('about.stations.subtitle')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
|
||||||
{t('about.stations.title')}
|
<StationGallery stations={content.stations} />
|
||||||
</h2>
|
|
||||||
<p className='max-w-2xl text-gray-600'>
|
<div className='mt-12 text-center'>
|
||||||
{t('about.stations.subtitle')}
|
<p className='mx-auto mb-6 max-w-2xl text-gray-600'>
|
||||||
</p>
|
{t('about.stations.description')}
|
||||||
</div>
|
</p>
|
||||||
<StationGallery stations={content.stations} />
|
|
||||||
<div className='mt-12 text-center'>
|
|
||||||
<p className='mx-auto mb-6 max-w-2xl text-gray-600'>
|
|
||||||
{t('about.stations.description')}
|
|
||||||
</p>
|
|
||||||
<Link href='/#stations'>
|
|
||||||
<Button className='bg-red-600 hover:bg-red-700'>
|
<Button className='bg-red-600 hover:bg-red-700'>
|
||||||
{t('about.stations.buttonText')}{' '}
|
{t('about.stations.buttonText')}{' '}
|
||||||
<MapPin className='ml-2 h-4 w-4' />
|
<MapPin className='ml-2 h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</section>
|
||||||
</section>
|
</Container>
|
||||||
|
|
||||||
{/* Our Values */}
|
{/* Our Values */}
|
||||||
<Container>
|
<Container>
|
||||||
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
<section className='py-16'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='container mx-auto'>
|
||||||
<Target className='h-6 w-6 text-red-600' />
|
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
||||||
</div>
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
<Target className='h-6 w-6 text-red-600' />
|
||||||
{t('about.values.title')}
|
</div>
|
||||||
</h2>
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
<p className='max-w-2xl text-gray-600'>
|
{t('about.values.title')}
|
||||||
{t('about.values.subtitle')}
|
</h2>
|
||||||
</p>
|
<p className='max-w-2xl text-gray-600'>
|
||||||
</div>
|
{t('about.values.subtitle')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='grid gap-8 md:grid-cols-3'>
|
<div
|
||||||
{[0, 1, 2].map((index) => (
|
data-aos='flip-up'
|
||||||
<Card
|
|
||||||
data-aos='flip-left'
|
|
||||||
data-aos-duration='600'
|
data-aos-duration='600'
|
||||||
key={index}
|
className='grid gap-8 md:grid-cols-3'
|
||||||
className='overflow-hidden transition-all hover:shadow-lg'
|
|
||||||
>
|
>
|
||||||
<CardContent className='p-6'>
|
{[0, 1, 2].map((index) => (
|
||||||
<div className='mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-red-100'>
|
<Card
|
||||||
<Star className='h-6 w-6 text-red-600' />
|
key={index}
|
||||||
</div>
|
className='overflow-hidden transition-all hover:shadow-lg'
|
||||||
<h3 className='mb-2 text-xl font-bold'>
|
>
|
||||||
{t(`about.values.items.${index}.title`)}
|
<CardContent className='p-6'>
|
||||||
</h3>
|
<div className='mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-red-100'>
|
||||||
<p className='text-gray-600'>
|
<Star className='h-6 w-6 text-red-600' />
|
||||||
{t(`about.values.items.${index}.description`)}
|
</div>
|
||||||
</p>
|
<h3 className='mb-2 text-xl font-bold'>
|
||||||
</CardContent>
|
{t(`about.values.items.${index}.title`)}
|
||||||
</Card>
|
</h3>
|
||||||
))}
|
<p className='text-gray-600'>
|
||||||
</div>
|
{t(`about.values.items.${index}.description`)}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
{/* Our Team */}
|
{/* Our Team */}
|
||||||
<section className='bg-gray-50'>
|
<Container>
|
||||||
<Container>
|
<section className='bg-gray-50 py-16'>
|
||||||
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
<div className='container mx-auto px-2'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
||||||
<Users className='h-6 w-6 text-red-600' />
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
</div>
|
<Users className='h-6 w-6 text-red-600' />
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
|
||||||
{t('about.team.title')}
|
|
||||||
</h2>
|
|
||||||
<p className='max-w-2xl text-gray-600'>
|
|
||||||
{t('about.team.subtitle')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
data-aos='flip-left'
|
|
||||||
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
|
|
||||||
key={index}
|
|
||||||
className='overflow-hidden rounded-lg bg-white shadow-md transition-transform hover:scale-105'
|
|
||||||
>
|
|
||||||
<div className='relative h-64 w-full'>
|
|
||||||
{member.photo && (
|
|
||||||
<Image
|
|
||||||
src={member.photo}
|
|
||||||
alt={t(`about.team.members.${index}.name`)}
|
|
||||||
fill
|
|
||||||
className='object-cover'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className='p-4 text-center'>
|
|
||||||
<h3 className='text-lg font-bold'>{member.name}</h3>
|
|
||||||
<p className='text-gray-600'>{member.profession}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
|
{t('about.team.title')}
|
||||||
|
</h2>
|
||||||
|
<p className='max-w-2xl text-gray-600'>
|
||||||
|
{t('about.team.subtitle')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
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
|
||||||
|
key={index}
|
||||||
|
className='overflow-hidden rounded-lg bg-white shadow-md transition-transform hover:scale-105'
|
||||||
|
>
|
||||||
|
<div className='relative h-64 w-full'>
|
||||||
|
{member.photo && (
|
||||||
|
<Image
|
||||||
|
src={member.photo}
|
||||||
|
alt={t(`about.team.members.${index}.name`)}
|
||||||
|
fill
|
||||||
|
className='object-cover'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='p-4 text-center'>
|
||||||
|
<h3 className='text-lg font-bold'>{member.name}</h3>
|
||||||
|
<p className='text-gray-600'>{member.profession}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</section>
|
||||||
</section>
|
</Container>
|
||||||
|
|
||||||
{/* Testimonials */}
|
{/* Testimonials */}
|
||||||
<Container>
|
<Container>
|
||||||
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
<section className='py-16'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='container mx-auto'>
|
||||||
<Star className='h-6 w-6 text-red-600' />
|
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
||||||
</div>
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
<Star className='h-6 w-6 text-red-600' />
|
||||||
{t('about.testimonials.title')}
|
</div>
|
||||||
</h2>
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
<p className='max-w-2xl text-gray-600'>
|
{t('about.testimonials.title')}
|
||||||
{t('about.testimonials.subtitle')}
|
</h2>
|
||||||
</p>
|
<p className='max-w-2xl text-gray-600'>
|
||||||
</div>
|
{t('about.testimonials.subtitle')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div data-aos='zoom-out-right' className='grid gap-8 md:grid-cols-3'>
|
<div
|
||||||
{content.reviews.map((review, index) => (
|
data-aos='zoom-out-right'
|
||||||
<Review key={review.id} review={review} />
|
className='grid gap-8 md:grid-cols-3'
|
||||||
))}
|
>
|
||||||
</div>
|
{content.reviews.map((review, index) => (
|
||||||
<div className='mt-4 flex w-full justify-center'>
|
<Card
|
||||||
<ReviewForm />
|
key={index}
|
||||||
</div>
|
className='overflow-hidden transition-all hover:shadow-lg'
|
||||||
|
>
|
||||||
|
<CardContent className='p-6'>
|
||||||
|
<div className='mb-4 flex'>
|
||||||
|
{Array(5)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
|
<Star
|
||||||
|
key={i}
|
||||||
|
className={`h-5 w-5 ${i < Number(review.rating) ? 'fill-yellow-400 text-yellow-400' : 'text-gray-300'}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className='mb-4 text-gray-600 italic'>
|
||||||
|
"{review.review}"
|
||||||
|
</p>
|
||||||
|
<p className='font-semibold'>{review.fullname}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<CtaSection />
|
<CtaSection />
|
||||||
|
|||||||
@ -10,27 +10,28 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
import { CharityPageData } from '@/app/api-utlities/@types/pages';
|
|
||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { useMediaController } from '@/shared/media/hooks/use-media-controller';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} 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 interface CharityPageProps {
|
export const metadata = {
|
||||||
content: CharityPageData;
|
title: 'Благотворительность | GasNetwork - Сеть заправок в Таджикистане',
|
||||||
}
|
description:
|
||||||
|
'Благотворительные проекты и инициативы GasNetwork. Мы помогаем обществу и заботимся о будущем.',
|
||||||
|
};
|
||||||
|
|
||||||
export function CharityPage({ content }: CharityPageProps) {
|
export function CharityPage() {
|
||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
const { m } = useMediaController();
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-h-screen flex-col'>
|
<div className='flex min-h-screen flex-col'>
|
||||||
<main className='flex-1'>
|
<main className='flex-1'>
|
||||||
@ -38,27 +39,27 @@ export function CharityPage({ content }: CharityPageProps) {
|
|||||||
<section className='relative'>
|
<section className='relative'>
|
||||||
<div className='relative h-[400px] w-full overflow-hidden'>
|
<div className='relative h-[400px] w-full overflow-hidden'>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src='/placeholder.svg?height=500&width=1920&text=Благотворительный+фонд+GasNetwork'
|
||||||
m('charity.hero-section.banner') ||
|
|
||||||
'/placeholder.svg?height=500&width=1920&text=Благотворительный+фонд+Ориё'
|
|
||||||
}
|
|
||||||
alt={t('charity.hero.imageAlt')}
|
alt={t('charity.hero.imageAlt')}
|
||||||
|
width={1920}
|
||||||
|
height={500}
|
||||||
className='object-cover'
|
className='object-cover'
|
||||||
priority
|
priority
|
||||||
fill
|
|
||||||
/>
|
/>
|
||||||
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'>
|
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'>
|
||||||
<Container data-aos='fade-down' data-aos-duration='800'>
|
<Container>
|
||||||
<div className='max-w-2xl space-y-6 text-white'>
|
<div data-aos='fade-down' data-aos-duration='800' className='container mx-auto'>
|
||||||
<div className='inline-flex items-center justify-center rounded-full bg-red-600/20 p-2'>
|
<div className='max-w-2xl space-y-6 text-white'>
|
||||||
<Heart className='size-6 text-red-500' />
|
<div className='inline-flex items-center justify-center rounded-full bg-red-600/20 p-2'>
|
||||||
|
<Heart className='size-6 text-red-500' />
|
||||||
|
</div>
|
||||||
|
<h1 className='text-3xl font-bold tracking-tight sm:text-5xl md:text-6xl'>
|
||||||
|
{t('charity.hero.title')}
|
||||||
|
</h1>
|
||||||
|
<p className='text-lg sm:text-xl text-gray-200'>
|
||||||
|
{t('charity.hero.subtitle')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<h1 className='text-3xl font-bold tracking-tight sm:text-5xl md:text-6xl'>
|
|
||||||
{t('charity.hero.title')}
|
|
||||||
</h1>
|
|
||||||
<p className='text-lg text-gray-200 sm:text-xl'>
|
|
||||||
{t('charity.hero.subtitle')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
@ -67,193 +68,218 @@ export function CharityPage({ content }: CharityPageProps) {
|
|||||||
|
|
||||||
{/* Mission Section */}
|
{/* Mission Section */}
|
||||||
<Container>
|
<Container>
|
||||||
<div className='grid items-center gap-12 md:grid-cols-2'>
|
<section className='py-16'>
|
||||||
<div>
|
<div className='container mx-auto'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='grid items-center gap-12 md:grid-cols-2'>
|
||||||
<Heart className='h-6 w-6 text-red-600' />
|
<div>
|
||||||
</div>
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
<h2 className='mb-6 text-3xl font-bold tracking-tight sm:text-4xl'>
|
<Heart className='h-6 w-6 text-red-600' />
|
||||||
{t('charity.mission.title')}
|
|
||||||
</h2>
|
|
||||||
<p className='mb-6 text-gray-600'>
|
|
||||||
{t('charity.mission.description1')}
|
|
||||||
</p>
|
|
||||||
<p className='mb-6 text-gray-600'>
|
|
||||||
{t('charity.mission.description2')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className='space-y-4'>
|
|
||||||
{[0, 1, 2].map((index) => (
|
|
||||||
<div
|
|
||||||
data-aos='fade-right'
|
|
||||||
key={index}
|
|
||||||
className='flex items-start'
|
|
||||||
>
|
|
||||||
<CheckCircle className='mr-3 h-6 w-6 flex-shrink-0 text-red-600' />
|
|
||||||
<div>
|
|
||||||
<h3 className='text-lg font-medium'>
|
|
||||||
{t(`charity.mission.principles.${index}.title`)}
|
|
||||||
</h3>
|
|
||||||
<p className='text-gray-600'>
|
|
||||||
{t(`charity.mission.principles.${index}.description`)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<h2 className='mb-6 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
|
{t('charity.mission.title')}
|
||||||
|
</h2>
|
||||||
|
<p className='mb-6 text-gray-600'>
|
||||||
|
{t('charity.mission.description1')}
|
||||||
|
</p>
|
||||||
|
<p className='mb-6 text-gray-600'>
|
||||||
|
{t('charity.mission.description2')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className='space-y-4'>
|
||||||
|
{[0, 1, 2].map((index) => (
|
||||||
|
<div data-aos='fade-right' key={index} className='flex items-start'>
|
||||||
|
<CheckCircle className='mr-3 h-6 w-6 flex-shrink-0 text-red-600' />
|
||||||
|
<div>
|
||||||
|
<h3 className='text-lg font-medium'>
|
||||||
|
{t(`charity.mission.principles.${index}.title`)}
|
||||||
|
</h3>
|
||||||
|
<p className='text-gray-600'>
|
||||||
|
{t(`charity.mission.principles.${index}.description`)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-aos='fade-right' className='relative h-[500px] overflow-hidden rounded-xl shadow-xl'>
|
||||||
|
<Image
|
||||||
|
src='/placeholder.svg?height=500&width=600&text=Наша+миссия'
|
||||||
|
alt={t('charity.mission.imageAlt')}
|
||||||
|
fill
|
||||||
|
className='object-cover'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</section>
|
||||||
data-aos='fade-right'
|
|
||||||
className='relative h-[500px] overflow-hidden rounded-xl shadow-xl'
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={
|
|
||||||
m('charity.second-section.banner') ||
|
|
||||||
'/placeholder.svg?height=500&width=600&text=Наша+миссия'
|
|
||||||
}
|
|
||||||
alt={t('charity.mission.imageAlt')}
|
|
||||||
fill
|
|
||||||
className='object-cover'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
{/* Key Figures */}
|
{/* Key Figures */}
|
||||||
<Container className='bg-red-600 text-white'>
|
<section className='bg-red-600 py-16 text-white'>
|
||||||
<div className='mb-12 text-center'>
|
<div className='container mx-auto'>
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
<div className='mb-12 text-center'>
|
||||||
{t('charity.stats.title')}
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
</h2>
|
{t('charity.stats.title')}
|
||||||
<p className='mx-auto max-w-2xl text-white/80'>
|
</h2>
|
||||||
{t('charity.stats.subtitle')}
|
<p className='mx-auto max-w-2xl text-white/80'>
|
||||||
</p>
|
{t('charity.stats.subtitle')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='grid grid-cols-1 gap-8 text-center md:grid-cols-3'>
|
||||||
|
{[0, 1, 2].map((index) => (
|
||||||
|
<div key={index} className='space-y-2'>
|
||||||
|
<h3 className='text-4xl font-bold'>
|
||||||
|
{t(`charity.stats.items.${index}.value`)}
|
||||||
|
</h3>
|
||||||
|
<p className='text-white/80'>
|
||||||
|
{t(`charity.stats.items.${index}.label`)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='grid grid-cols-1 gap-8 text-center md:grid-cols-3'>
|
</section>
|
||||||
{[0, 1, 2].map((index) => (
|
|
||||||
<div key={index} className='space-y-2'>
|
|
||||||
<h3 className='text-4xl font-bold'>
|
|
||||||
{t(`charity.stats.items.${index}.value`)}
|
|
||||||
</h3>
|
|
||||||
<p className='text-white/80'>
|
|
||||||
{t(`charity.stats.items.${index}.label`)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
|
|
||||||
{/* Upcoming Events */}
|
{/* Upcoming Events */}
|
||||||
<Container className='bg-gray-50'>
|
<Container>
|
||||||
<div className='mb-12 text-center'>
|
<section className='bg-gray-50 py-16'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='container mx-auto'>
|
||||||
<Calendar className='h-6 w-6 text-red-600' />
|
<div className='mb-12 text-center'>
|
||||||
</div>
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
<Calendar className='h-6 w-6 text-red-600' />
|
||||||
{t('charity.events.title')}
|
|
||||||
</h2>
|
|
||||||
<p className='mx-auto max-w-2xl text-gray-600'>
|
|
||||||
{t('charity.events.subtitle')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='grid gap-6 md:grid-cols-3'>
|
|
||||||
{content.charities.map((event, index) => (
|
|
||||||
<Card
|
|
||||||
data-aos='zoom-in-up'
|
|
||||||
key={index}
|
|
||||||
className='flex flex-col justify-between overflow-hidden'
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div className='relative h-48 w-full'>
|
|
||||||
<Image
|
|
||||||
src={event.image || '/placeholder.svg'}
|
|
||||||
alt={event.name}
|
|
||||||
fill
|
|
||||||
className='object-cover'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className='text-xl lg:text-2xl'>
|
|
||||||
{event.name}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className='space-y-4'>
|
|
||||||
<p className='text-gray-600'>{event.description}</p>
|
|
||||||
<div className='flex items-center text-sm text-gray-500'>
|
|
||||||
<Calendar className='mr-2 h-4 w-4' />
|
|
||||||
{event.date}
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center text-sm text-gray-500'>
|
|
||||||
<MapPin className='mr-2 h-4 w-4' />
|
|
||||||
{event.location}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</div>
|
</div>
|
||||||
{/* <CardFooter>
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
<Button className='w-full bg-red-600 hover:bg-red-700'>
|
{t('charity.events.title')}
|
||||||
{t(`charity.events.button`)}
|
</h2>
|
||||||
</Button>
|
<p className='mx-auto max-w-2xl text-gray-600'>
|
||||||
</CardFooter> */}
|
{t('charity.events.subtitle')}
|
||||||
</Card>
|
</p>
|
||||||
))}
|
</div>
|
||||||
</div>
|
|
||||||
|
<div className='grid gap-6 md:grid-cols-3'>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: 'Благотворительный марафон',
|
||||||
|
description:
|
||||||
|
'Ежегодный благотворительный марафон в поддержку детей с особыми потребностями.',
|
||||||
|
date: '15 июня 2023',
|
||||||
|
location: 'Парк Рудаки, Душанбе',
|
||||||
|
image: '/placeholder.svg?height=200&width=300&text=Марафон',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Экологическая акция',
|
||||||
|
description:
|
||||||
|
'Очистка берегов реки Варзоб от мусора и посадка деревьев.',
|
||||||
|
date: '22 июля 2023',
|
||||||
|
location: 'Река Варзоб, Душанбе',
|
||||||
|
image:
|
||||||
|
'/placeholder.svg?height=200&width=300&text=Экологическая+акция',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Сбор школьных принадлежностей',
|
||||||
|
description:
|
||||||
|
'Сбор школьных принадлежностей для детей из малообеспеченных семей к новому учебному году.',
|
||||||
|
date: '1-20 августа 2023',
|
||||||
|
location: 'Все заправки GasNetwork',
|
||||||
|
image:
|
||||||
|
'/placeholder.svg?height=200&width=300&text=Школьные+принадлежности',
|
||||||
|
},
|
||||||
|
].map((event, index) => (
|
||||||
|
<Card data-aos='zoom-in-up' key={index} className='overflow-hidden flex flex-col justify-between'>
|
||||||
|
<div>
|
||||||
|
<div className='relative h-48 w-full'>
|
||||||
|
<Image
|
||||||
|
src={event.image || '/placeholder.svg'}
|
||||||
|
alt={event.title}
|
||||||
|
fill
|
||||||
|
className='object-cover'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className='text-xl lg:text-2xl'>{event.title}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='space-y-4'>
|
||||||
|
<p className='text-gray-600'>{event.description}</p>
|
||||||
|
<div className='flex items-center text-sm text-gray-500'>
|
||||||
|
<Calendar className='mr-2 h-4 w-4' />
|
||||||
|
{event.date}
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center text-sm text-gray-500'>
|
||||||
|
<MapPin className='mr-2 h-4 w-4' />
|
||||||
|
{event.location}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</div>
|
||||||
|
<CardFooter>
|
||||||
|
<Button className='w-full bg-red-600 hover:bg-red-700'>
|
||||||
|
{t(`charity.events.button`)}
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
{/* How to Help */}
|
{/* How to Help */}
|
||||||
<Container>
|
<Container>
|
||||||
<div className='mb-12 text-center'>
|
<section className='py-16'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='container mx-auto'>
|
||||||
<Users className='h-6 w-6 text-red-600' />
|
<div className='mb-12 text-center'>
|
||||||
</div>
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
<Users className='h-6 w-6 text-red-600' />
|
||||||
{t('charity.help.title')}
|
</div>
|
||||||
</h2>
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
<p className='mx-auto max-w-2xl text-gray-600'>
|
{t('charity.help.title')}
|
||||||
{t('charity.help.subtitle')}
|
</h2>
|
||||||
</p>
|
<p className='mx-auto max-w-2xl text-gray-600'>
|
||||||
</div>
|
{t('charity.help.subtitle')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='grid gap-3 md:grid-cols-2 md:gap-6 lg:grid-cols-4'>
|
<div className='grid gap-3 md:grid-cols-2 md:gap-6 lg:grid-cols-4'>
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
title: 'Сделать пожертвование',
|
title: 'Сделать пожертвование',
|
||||||
description:
|
description:
|
||||||
'Ваше пожертвование поможет нам реализовать больше проектов и помочь большему количеству людей.',
|
'Ваше пожертвование поможет нам реализовать больше проектов и помочь большему количеству людей.',
|
||||||
icon: <Landmark className='h-10 w-10 text-red-600' />,
|
icon: <Landmark className='h-10 w-10 text-red-600' />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Стать волонтером',
|
title: 'Стать волонтером',
|
||||||
description:
|
description:
|
||||||
'Присоединяйтесь к нашей команде волонтеров и помогайте нам в реализации благотворительных проектов.',
|
'Присоединяйтесь к нашей команде волонтеров и помогайте нам в реализации благотворительных проектов.',
|
||||||
icon: <Users className='h-10 w-10 text-red-600' />,
|
icon: <Users className='h-10 w-10 text-red-600' />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Участвовать в мероприятиях',
|
title: 'Участвовать в мероприятиях',
|
||||||
description:
|
description:
|
||||||
'Принимайте участие в наших благотворительных мероприятиях и акциях.',
|
'Принимайте участие в наших благотворительных мероприятиях и акциях.',
|
||||||
icon: <Calendar className='h-10 w-10 text-red-600' />,
|
icon: <Calendar className='h-10 w-10 text-red-600' />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Распространять информацию',
|
title: 'Распространять информацию',
|
||||||
description:
|
description:
|
||||||
'Расскажите о нашем фонде и его деятельности своим друзьям и знакомым.',
|
'Расскажите о нашем фонде и его деятельности своим друзьям и знакомым.',
|
||||||
icon: <Heart className='h-10 w-10 text-red-600' />,
|
icon: <Heart className='h-10 w-10 text-red-600' />,
|
||||||
},
|
},
|
||||||
].map((item, index) => (
|
].map((item, index) => (
|
||||||
<Card data-aos='zoom-in' key={index} className='text-center'>
|
<Card data-aos='zoom-in' key={index} className='text-center'>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className='mb-4 flex justify-center'>{item.icon}</div>
|
<div className='mb-4 flex justify-center'>{item.icon}</div>
|
||||||
<CardTitle className='break-words hyphens-auto'>
|
<CardTitle className='break-words hyphens-auto'>
|
||||||
{item.title}
|
{item.title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className='text-gray-600'>{item.description}</p>
|
<p className='text-gray-600'>{item.description}</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</Container>
|
</Container>
|
||||||
<CtaSection />
|
<CtaSection />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -3,23 +3,70 @@
|
|||||||
import { Download, Eye } from 'lucide-react';
|
import { Download, Eye } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
import { CertificatesPageData } from '@/app/api-utlities/@types/pages';
|
|
||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
|
||||||
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 interface CertificatesPageProps {
|
export function CertificatesPage() {
|
||||||
content: CertificatesPageData;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CertificatesPage({ content }: CertificatesPageProps) {
|
|
||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
|
|
||||||
|
// This data would typically come from an API or CMS
|
||||||
|
// We're keeping it as-is since it's dynamic content
|
||||||
|
const certificates = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'ISO 9001:2015',
|
||||||
|
description: 'Сертификат системы менеджмента качества',
|
||||||
|
image: '/placeholder.svg?height=400&width=300',
|
||||||
|
issueDate: '15.03.2022',
|
||||||
|
expiryDate: '15.03.2025',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'ISO 14001:2015',
|
||||||
|
description: 'Сертификат экологического менеджмента',
|
||||||
|
image: '/placeholder.svg?height=400&width=300',
|
||||||
|
issueDate: '10.05.2022',
|
||||||
|
expiryDate: '10.05.2025',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'OHSAS 18001',
|
||||||
|
description: 'Сертификат системы управления охраной труда',
|
||||||
|
image: '/placeholder.svg?height=400&width=300',
|
||||||
|
issueDate: '22.07.2022',
|
||||||
|
expiryDate: '22.07.2025',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'Сертификат качества топлива',
|
||||||
|
description: 'Подтверждение соответствия топлива стандартам качества',
|
||||||
|
image: '/placeholder.svg?height=400&width=300',
|
||||||
|
issueDate: '05.01.2023',
|
||||||
|
expiryDate: '05.01.2024',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: 'Сертификат соответствия',
|
||||||
|
description: 'Соответствие услуг национальным стандартам',
|
||||||
|
image: '/placeholder.svg?height=400&width=300',
|
||||||
|
issueDate: '18.09.2022',
|
||||||
|
expiryDate: '18.09.2025',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
title: 'Лицензия на хранение ГСМ',
|
||||||
|
description: 'Разрешение на хранение горюче-смазочных материалов',
|
||||||
|
image: '/placeholder.svg?height=400&width=300',
|
||||||
|
issueDate: '30.11.2021',
|
||||||
|
expiryDate: '30.11.2026',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<Container>
|
||||||
<Container>
|
<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>
|
||||||
<p className='mx-auto max-w-2xl text-lg text-gray-600'>
|
<p className='mx-auto max-w-2xl text-lg text-gray-600'>
|
||||||
@ -28,7 +75,7 @@ export function CertificatesPage({ content }: CertificatesPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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'>
|
||||||
{content.certificates.map((certificate) => (
|
{certificates.map((certificate) => (
|
||||||
<Card
|
<Card
|
||||||
data-aos='zoom-in'
|
data-aos='zoom-in'
|
||||||
key={certificate.id}
|
key={certificate.id}
|
||||||
@ -37,24 +84,24 @@ export function CertificatesPage({ content }: CertificatesPageProps) {
|
|||||||
<div className='relative h-[300px] w-full overflow-hidden bg-gray-100'>
|
<div className='relative h-[300px] w-full overflow-hidden bg-gray-100'>
|
||||||
<Image
|
<Image
|
||||||
src={certificate.image || '/placeholder.svg'}
|
src={certificate.image || '/placeholder.svg'}
|
||||||
alt={certificate.name}
|
alt={certificate.title}
|
||||||
fill
|
fill
|
||||||
className='object-contain p-4'
|
className='object-contain p-4'
|
||||||
sizes='(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw'
|
sizes='(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CardContent className='p-6'>
|
<CardContent className='p-6'>
|
||||||
<h3 className='mb-2 text-xl font-bold'>{certificate.name}</h3>
|
<h3 className='mb-2 text-xl font-bold'>{certificate.title}</h3>
|
||||||
<p className='mb-4 text-gray-600'>{certificate.description}</p>
|
<p className='mb-4 text-gray-600'>{certificate.description}</p>
|
||||||
<div className='mb-4 text-sm text-gray-500'>
|
<div className='mb-4 text-sm text-gray-500'>
|
||||||
<p>
|
<p>
|
||||||
{t('certificates.issueDate')}: {certificate.issuedAt}
|
{t('certificates.issueDate')}: {certificate.issueDate}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{t('certificates.expiryDate')}: {certificate.validUntil}
|
{t('certificates.expiryDate')}: {certificate.expiryDate}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/* <div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
size='sm'
|
size='sm'
|
||||||
@ -71,12 +118,12 @@ export function CertificatesPage({ content }: CertificatesPageProps) {
|
|||||||
<Download size={16} />
|
<Download size={16} />
|
||||||
<span>{t('common.buttons.download')}</span>
|
<span>{t('common.buttons.download')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div> */}
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</main>
|
||||||
</main>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,17 +2,21 @@
|
|||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { useMediaController } from '@/shared/media/hooks/use-media-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 = {
|
||||||
|
title: 'Клиентам | GasNetwork - Сеть заправок в Таджикистане',
|
||||||
|
description:
|
||||||
|
'Информация для клиентов: программа лояльности, топливные карты, сертификаты и способы оплаты.',
|
||||||
|
};
|
||||||
|
|
||||||
export function ClientsPage() {
|
export function ClientsPage() {
|
||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
const { m } = useMediaController();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-h-screen flex-col'>
|
<div className='flex min-h-screen flex-col'>
|
||||||
@ -21,22 +25,16 @@ export function ClientsPage() {
|
|||||||
<section className='relative'>
|
<section className='relative'>
|
||||||
<div className='relative h-[400px] w-full overflow-hidden'>
|
<div className='relative h-[400px] w-full overflow-hidden'>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src='/placeholder.svg?height=400&width=1920&text=Для+наших+клиентов'
|
||||||
m('clients.hero-section.banner') ||
|
|
||||||
'/placeholder.svg?height=400&width=1920&text=Для+наших+клиентов'
|
|
||||||
}
|
|
||||||
alt='Для наших клиентов'
|
alt='Для наших клиентов'
|
||||||
|
width={1920}
|
||||||
|
height={400}
|
||||||
className='object-cover'
|
className='object-cover'
|
||||||
priority
|
priority
|
||||||
fill
|
|
||||||
/>
|
/>
|
||||||
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'>
|
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'>
|
||||||
<Container className='py-0'>
|
<Container>
|
||||||
<div
|
<div data-aos='fade-down' data-aos-duration="1000" className='container mx-auto'>
|
||||||
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')}
|
||||||
|
|||||||
@ -1,20 +1,24 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Percent } from 'lucide-react';
|
import { Check, Percent } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { useMediaController } from '@/shared/media/hooks/use-media-controller';
|
import { Card, CardContent } from '@/shared/shadcn-ui/card';
|
||||||
|
|
||||||
// import LoyaltyLevels from '@/widgets/clients/loyalty/ui/loyalty-levels';
|
|
||||||
import { CtaSection } from '@/widgets/cta-section';
|
import { CtaSection } from '@/widgets/cta-section';
|
||||||
|
import Container from '@/shared/shadcn-ui/conteiner';
|
||||||
|
|
||||||
import ProgrammImg from '../../../../public/clients/loyatly/03a771e7-5d76-4111-a516-801aa925659f.jpg';
|
import ProgrammImg from '../../../../public/clients/loyatly/03a771e7-5d76-4111-a516-801aa925659f.jpg'
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Программа лояльности | GasNetwork - Сеть заправок в Таджикистане',
|
||||||
|
description:
|
||||||
|
'Программа лояльности GasNetwork: накапливайте баллы и получайте скидки на топливо и услуги.',
|
||||||
|
};
|
||||||
|
|
||||||
export function LoyaltyPage() {
|
export function LoyaltyPage() {
|
||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
const { m } = useMediaController();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-h-screen flex-col'>
|
<div className='flex min-h-screen flex-col'>
|
||||||
@ -23,115 +27,116 @@ export function LoyaltyPage() {
|
|||||||
<section className='relative'>
|
<section className='relative'>
|
||||||
<div className='relative h-[400px] w-full overflow-hidden'>
|
<div className='relative h-[400px] w-full overflow-hidden'>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src='/placeholder.svg?height=400&width=1920&text=Программа+лояльности'
|
||||||
m('loyalty.hero-section.banner') ||
|
|
||||||
'/placeholder.svg?height=400&width=1920&text=Программа+лояльности'
|
|
||||||
}
|
|
||||||
alt='Программа лояльности'
|
alt='Программа лояльности'
|
||||||
|
width={1920}
|
||||||
|
height={400}
|
||||||
className='object-cover'
|
className='object-cover'
|
||||||
priority
|
priority
|
||||||
fill
|
|
||||||
/>
|
/>
|
||||||
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'>
|
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30'>
|
||||||
<Container data-aos='fade-down' data-aos-duration='800'>
|
<Container>
|
||||||
<div className='max-w-2xl space-y-4 text-white'>
|
<div data-aos='fade-down' data-aos-duration="800" className='container mx-auto'>
|
||||||
<h1 className='text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl'>
|
<div className='max-w-2xl space-y-4 text-white'>
|
||||||
{t('clients.loyalty.title')}
|
<h1 className='text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl'>
|
||||||
</h1>
|
{t('clients.loyalty.title')}
|
||||||
<p className='text-lg text-gray-200'>
|
</h1>
|
||||||
{t('clients.loyalty.description')}
|
<p className='text-lg text-gray-200'>
|
||||||
</p>
|
{t('clients.loyalty.description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Program Overview */}
|
|
||||||
<Container>
|
<Container>
|
||||||
<div className='grid items-center gap-12 md:grid-cols-2'>
|
{/* Program Overview */}
|
||||||
<div data-aos='fade-right'>
|
<section className='py-16'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='container mx-auto'>
|
||||||
<Percent className='h-6 w-6 text-red-600' />
|
<div className='grid items-center gap-12 md:grid-cols-2'>
|
||||||
</div>
|
<div data-aos='fade-right'>
|
||||||
<h2 className='mb-6 text-3xl font-bold tracking-tight sm:text-4xl'>
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
{t('clients.loyalty.programm.about')}
|
<Percent className='h-6 w-6 text-red-600' />
|
||||||
</h2>
|
</div>
|
||||||
<p className='mb-6 text-gray-600'>
|
<h2 className='mb-6 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
{t('clients.loyalty.programm.about-description')}
|
{t('clients.loyalty.programm.about')}
|
||||||
</p>
|
</h2>
|
||||||
<p className='mb-6 text-gray-600'>
|
<p className='mb-6 text-gray-600'>
|
||||||
{t('clients.loyalty.programm.about-description-2')}
|
{t('clients.loyalty.programm.about-description')}
|
||||||
</p>
|
</p>
|
||||||
|
<p className='mb-6 text-gray-600'>
|
||||||
|
{t('clients.loyalty.programm.about-description-2')}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
<div className='flex items-start'>
|
<div 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'>
|
||||||
<span className='text-xs text-white'>✓</span>
|
<span className='text-xs text-white'>✓</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='ml-3'>
|
<div className='ml-3'>
|
||||||
<h3 className='text-lg font-medium'>
|
<h3 className='text-lg font-medium'>
|
||||||
{t('clients.loyalty.programm.conditions-1')}
|
{t('clients.loyalty.programm.conditions-1')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className='text-gray-600'>
|
<p className='text-gray-600'>
|
||||||
{t('clients.loyalty.programm.conditions.description-1')}
|
{t('clients.loyalty.programm.conditions.description-1')}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div 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'>
|
||||||
|
<span className='text-xs text-white'>✓</span>
|
||||||
|
</div>
|
||||||
|
<div className='ml-3'>
|
||||||
|
<h3 className='text-lg font-medium'>
|
||||||
|
{t('clients.loyalty.programm.conditions-2')}
|
||||||
|
</h3>
|
||||||
|
<p className='text-gray-600'>
|
||||||
|
{t('clients.loyalty.programm.conditions.description-2')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div 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'>
|
||||||
|
<span className='text-xs text-white'>✓</span>
|
||||||
|
</div>
|
||||||
|
<div className='ml-3'>
|
||||||
|
<h3 className='text-lg font-medium'>
|
||||||
|
{t('clients.loyalty.programm.conditions-3')}
|
||||||
|
</h3>
|
||||||
|
<p className='text-gray-600'>
|
||||||
|
{t('clients.loyalty.programm.conditions.description-3')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-start'>
|
<div data-aos='fade-up' className='relative h-[400px] overflow-hidden rounded-xl shadow-xl'>
|
||||||
<div className='mt-1 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-red-600'>
|
<Image
|
||||||
<span className='text-xs text-white'>✓</span>
|
src={ProgrammImg}
|
||||||
</div>
|
alt='Программа лояльности'
|
||||||
<div className='ml-3'>
|
fill
|
||||||
<h3 className='text-lg font-medium'>
|
className="w-full object-contain p-2.5"
|
||||||
{t('clients.loyalty.programm.conditions-2')}
|
priority
|
||||||
</h3>
|
/>
|
||||||
<p className='text-gray-600'>
|
|
||||||
{t('clients.loyalty.programm.conditions.description-2')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div 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'>
|
|
||||||
<span className='text-xs text-white'>✓</span>
|
|
||||||
</div>
|
|
||||||
<div className='ml-3'>
|
|
||||||
<h3 className='text-lg font-medium'>
|
|
||||||
{t('clients.loyalty.programm.conditions-3')}
|
|
||||||
</h3>
|
|
||||||
<p className='text-gray-600'>
|
|
||||||
{t('clients.loyalty.programm.conditions.description-3')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</section>
|
||||||
data-aos='fade-up'
|
|
||||||
className='relative h-[400px] overflow-hidden rounded-xl shadow-xl'
|
{/* How It Works */}
|
||||||
>
|
<section className='bg-gray-50 py-16 px-2'>
|
||||||
<Image
|
<div className='container mx-auto'>
|
||||||
src={m('loyalty.second-section.banner') || ProgrammImg}
|
<div className='mb-12 text-center'>
|
||||||
alt='Программа лояльности'
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
fill
|
{t('clients.loyalty.works.title')}
|
||||||
className='w-full object-contain p-2.5'
|
</h2>
|
||||||
priority
|
<p className='mx-auto max-w-2xl text-gray-600'>
|
||||||
/>
|
{t('clients.loyalty.works.description')}
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
|
||||||
|
|
||||||
{/* How It Works */}
|
|
||||||
<section className='bg-gray-50'>
|
|
||||||
<Container>
|
|
||||||
<div className='mb-12 text-center'>
|
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
|
||||||
{t('clients.loyalty.works.title')}
|
|
||||||
</h2>
|
|
||||||
<p className='mx-auto max-w-2xl text-gray-600'>
|
|
||||||
{t('clients.loyalty.works.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className='grid gap-8 sm:grid-cols-2 lg:grid-cols-4'>
|
<div className='grid gap-8 sm:grid-cols-2 lg:grid-cols-4'>
|
||||||
<div data-aos='zoom-in-up' className='text-center'>
|
<div data-aos='zoom-in-up' 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'>
|
||||||
@ -178,10 +183,151 @@ export function LoyaltyPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* <LoyaltyLevels /> */}
|
{/* Loyalty Levels */}
|
||||||
|
<section className='py-16'>
|
||||||
|
<div className='container mx-auto'>
|
||||||
|
<div className='mb-12 text-center'>
|
||||||
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
|
{t('clients.loyalty.works.levels.title')}
|
||||||
|
</h2>
|
||||||
|
<p className='mx-auto max-w-2xl text-gray-600'>
|
||||||
|
{t('clients.loyalty.works.levels.description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid gap-8 md:grid-cols-3'>
|
||||||
|
<Card data-aos='flip-left' data-aos-duration='500' className='overflow-hidden border-t-4 border-t-gray-400 transition-all hover:shadow-lg'>
|
||||||
|
<CardContent className='p-6'>
|
||||||
|
<h3 className='mb-4 text-center text-2xl font-bold'>
|
||||||
|
{t('clients.loyalty.works.levels.card-1.title')}
|
||||||
|
</h3>
|
||||||
|
<div className='mb-6 text-center'>
|
||||||
|
<span className='text-4xl font-bold'>
|
||||||
|
{t('clients.loyalty.works.levels.card-1.percent')}
|
||||||
|
</span>
|
||||||
|
<p className='text-sm text-gray-600'>
|
||||||
|
{t('clients.loyalty.works.levels.card.mark')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul className='space-y-2'>
|
||||||
|
<li className='flex items-center'>
|
||||||
|
<Check className='mr-2 h-5 w-5 text-green-500' />
|
||||||
|
<span>
|
||||||
|
{t('clients.loyalty.works.levels.card-1.bonus-1')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center'>
|
||||||
|
<Check className='mr-2 h-5 w-5 text-green-500' />
|
||||||
|
<span>
|
||||||
|
{t('clients.loyalty.works.levels.card-1.bonus-2')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center'>
|
||||||
|
<Check className='mr-2 h-5 w-5 text-green-500' />
|
||||||
|
<span>
|
||||||
|
{t('clients.loyalty.works.levels.card-1.bonus-3')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card data-aos='flip-left' data-aos-duration='500' className='overflow-hidden border-t-4 border-t-yellow-500 transition-all hover:shadow-lg'>
|
||||||
|
<CardContent className='p-6'>
|
||||||
|
<h3 className='mb-4 text-center text-2xl font-bold'>
|
||||||
|
{t('clients.loyalty.works.levels.card-2.title')}
|
||||||
|
</h3>
|
||||||
|
<div className='mb-6 text-center'>
|
||||||
|
<span className='text-4xl font-bold'>
|
||||||
|
{t('clients.loyalty.works.levels.card-2.percent')}
|
||||||
|
</span>
|
||||||
|
<p className='text-sm text-gray-600'>
|
||||||
|
{t('clients.loyalty.works.levels.card.mark')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul className='space-y-2'>
|
||||||
|
<li className='flex items-center'>
|
||||||
|
<Check className='mr-2 h-5 w-5 text-green-500' />
|
||||||
|
<span>
|
||||||
|
{t('clients.loyalty.works.levels.card-2.bonus-1')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center'>
|
||||||
|
<Check className='mr-2 h-5 w-5 text-green-500' />
|
||||||
|
<span>
|
||||||
|
{t('clients.loyalty.works.levels.card-2.bonus-2')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center'>
|
||||||
|
<Check className='mr-2 h-5 w-5 text-green-500' />
|
||||||
|
<span>
|
||||||
|
{t('clients.loyalty.works.levels.card-2.bonus-3')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center'>
|
||||||
|
<Check className='mr-2 h-5 w-5 text-green-500' />
|
||||||
|
<span>
|
||||||
|
{t('clients.loyalty.works.levels.card-2.bonus-4')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card data-aos='flip-left' data-aos-duration='500' className='overflow-hidden border-t-4 border-t-red-600 transition-all hover:shadow-lg'>
|
||||||
|
<CardContent className='p-6'>
|
||||||
|
<h3 className='mb-4 text-center text-2xl font-bold'>
|
||||||
|
{t('clients.loyalty.works.levels.card-3.title')}
|
||||||
|
</h3>
|
||||||
|
<div className='mb-6 text-center'>
|
||||||
|
<span className='text-4xl font-bold'>
|
||||||
|
{t('clients.loyalty.works.levels.card-3.percent')}
|
||||||
|
</span>
|
||||||
|
<p className='text-sm text-gray-600'>
|
||||||
|
{t('clients.loyalty.works.levels.card.mark')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul className='space-y-2'>
|
||||||
|
<li className='flex items-center'>
|
||||||
|
<Check className='mr-2 h-5 w-5 text-green-500' />
|
||||||
|
<span>
|
||||||
|
{t('clients.loyalty.works.levels.card-3.bonus-1')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center'>
|
||||||
|
<Check className='mr-2 h-5 w-5 text-green-500' />
|
||||||
|
<span>
|
||||||
|
{t('clients.loyalty.works.levels.card-3.bonus-2')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center'>
|
||||||
|
<Check className='mr-2 h-5 w-5 text-green-500' />
|
||||||
|
<span>
|
||||||
|
{t('clients.loyalty.works.levels.card-3.bonus-3')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center'>
|
||||||
|
<Check className='mr-2 h-5 w-5 text-green-500' />
|
||||||
|
<span>
|
||||||
|
{t('clients.loyalty.works.levels.card-3.bonus-4')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center'>
|
||||||
|
<Check className='mr-2 h-5 w-5 text-green-500' />
|
||||||
|
<span>
|
||||||
|
{t('clients.loyalty.works.levels.card-3.bonus-5')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Container>
|
||||||
|
|
||||||
<CtaSection />
|
<CtaSection />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { Suspense } from 'react';
|
|||||||
|
|
||||||
import { LoginForm } from '@/features/auth/login-form';
|
import { LoginForm } from '@/features/auth/login-form';
|
||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
|
||||||
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 {
|
||||||
@ -18,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,
|
||||||
@ -66,7 +66,7 @@ function LoginPageTabs() {
|
|||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
key={tab.label}
|
key={tab.label}
|
||||||
value={tab.type}
|
value={tab.type}
|
||||||
className='w-full cursor-pointer text-base'
|
className='w-full text-base'
|
||||||
>
|
>
|
||||||
<tab.Icon className='mr-2 h-4 w-4' /> {t(tab.label)}
|
<tab.Icon className='mr-2 h-4 w-4' /> {t(tab.label)}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@ -134,38 +134,40 @@ export default function LoginPage() {
|
|||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className='flex min-h-screen flex-col items-center justify-center'>
|
<Container>
|
||||||
<div className='flex-1'>
|
<div className='flex min-h-screen flex-col items-center justify-center'>
|
||||||
<Container className='max-w-6xl'>
|
<main className='flex-1'>
|
||||||
<div className='mb-12 flex flex-col items-center text-center'>
|
<div className='container max-w-6xl py-16'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='mb-12 flex flex-col items-center text-center'>
|
||||||
<Fuel className='h-6 w-6 text-red-600' />
|
<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>
|
||||||
<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'>
|
<div data-aos='zoom-in' className='mx-auto max-w-lg'>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<LoginPageTabs />
|
<LoginPageTabs />
|
||||||
</Suspense>
|
</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
|
<Link
|
||||||
href={`mailto:${t('auth.loginForm.contactUs.mail')}`}
|
href='/contact'
|
||||||
className='text-red-600 hover:underline'
|
className='text-red-600 hover:underline'
|
||||||
>
|
>
|
||||||
{t('auth.contactLink')}
|
{t('auth.contactLink')}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
26
src/proxy.ts
26
src/proxy.ts
@ -1,26 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
export function proxy(req: NextRequest) {
|
|
||||||
const url = req.nextUrl.clone();
|
|
||||||
const path = url.pathname;
|
|
||||||
|
|
||||||
if (
|
|
||||||
path.startsWith('/customer-dashboard') ||
|
|
||||||
path.startsWith('/corporate-dashboard')
|
|
||||||
) {
|
|
||||||
const token = req.cookies.get(
|
|
||||||
`${path.includes('customer') ? 'bonus' : 'corporate'}__token`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
url.pathname = '/login';
|
|
||||||
return NextResponse.redirect(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
matcher: ['/customer-dashboard/:path*', '/corporate-dashboard/:path*'],
|
|
||||||
};
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||||
import { has } from 'lodash';
|
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
const baseQuery = fetchBaseQuery({
|
const baseQuery = fetchBaseQuery({
|
||||||
@ -23,15 +22,7 @@ export const baseAPI = createApi({
|
|||||||
if (result.error) {
|
if (result.error) {
|
||||||
switch (result.error.status) {
|
switch (result.error.status) {
|
||||||
case 401:
|
case 401:
|
||||||
if (
|
toast.error('Login credentials error');
|
||||||
has(result.error.data, 'error') &&
|
|
||||||
result.error.data.error === 'Credentials error'
|
|
||||||
) {
|
|
||||||
toast.error('Login credentials error');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.location.href = '/login';
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 500:
|
case 500:
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
export enum FetchTags {
|
|
||||||
TAYLOR = 'taylor',
|
|
||||||
}
|
|
||||||
@ -1,15 +1,9 @@
|
|||||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query';
|
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query';
|
||||||
|
|
||||||
import { FetchTags } from './tags';
|
|
||||||
|
|
||||||
const baseQuery = fetchBaseQuery({
|
const baseQuery = fetchBaseQuery({
|
||||||
baseUrl: process.env.TAYLOR_API_ENDPOINT,
|
baseUrl: process.env.TAYLOR_API_ENDPOINT,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: process.env.TAYLOR_API_TOKEN || '',
|
Authorization: process.env.TAYLOR_API_TOKEN || '',
|
||||||
schema: 'readable',
|
|
||||||
},
|
|
||||||
next: {
|
|
||||||
tags: [FetchTags.TAYLOR],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
import { createQueryBuilder } from '@taylordb/query-builder';
|
|
||||||
|
|
||||||
import { TaylorDatabase } from '../types/database.types';
|
|
||||||
|
|
||||||
// Initialize TaylorDB query builder instance
|
|
||||||
// Note: If you have generated types from taylor.types.ts, you can import them here
|
|
||||||
// import { TaylorDatabase } from '@/path/to/taylor.types';
|
|
||||||
|
|
||||||
export const taylorQueryBuilder = createQueryBuilder<TaylorDatabase>({
|
|
||||||
baseId: process.env.TAYLOR_BASE_ID || '',
|
|
||||||
baseUrl: process.env.TAYLOR_API_ENDPOINT || '',
|
|
||||||
apiKey: process.env.TAYLOR_API_TOKEN || '',
|
|
||||||
});
|
|
||||||
@ -1,21 +1,11 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { useMediaController } from '../media/hooks/use-media-controller';
|
|
||||||
|
|
||||||
export const Logo = () => {
|
export const Logo = () => {
|
||||||
const { m } = useMediaController();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link className='flex items-center gap-2' href={'/'}>
|
<Link className='flex items-center gap-2' href={'/'}>
|
||||||
<Image
|
<Image src='/logo-new.png' alt='oriyo-logo' width={110} height={40} />
|
||||||
src={m('logo') || '/logo-new.png'}
|
{/* <span className='text-xl font-bold'>Ориё</span> */}
|
||||||
alt='oriyo-logo'
|
|
||||||
width={110}
|
|
||||||
height={40}
|
|
||||||
/>
|
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,66 +1,74 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Users } from 'lucide-react';
|
import { Users } from 'lucide-react';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
|
||||||
import { useIntersectionObserver } from '../hooks/use-intersection-observer';
|
|
||||||
import AnimatedCounter from './animated-counter';
|
import AnimatedCounter from './animated-counter';
|
||||||
|
|
||||||
const stats = [
|
|
||||||
{
|
|
||||||
value: 'about.stats.items.1.value',
|
|
||||||
suffix: 'about.stats.items.1.suffix',
|
|
||||||
label: 'about.stats.items.1.label',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'about.stats.items.2.value',
|
|
||||||
suffix: 'about.stats.items.2.suffix',
|
|
||||||
label: 'about.stats.items.2.label',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'about.stats.items.3.value',
|
|
||||||
suffix: 'about.stats.items.3.suffix',
|
|
||||||
label: 'about.stats.items.3.label',
|
|
||||||
decimals: 1,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function AboutCounter() {
|
export default function AboutCounter() {
|
||||||
const { t } = useTextController();
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const { sectionRef, isVisible } = useIntersectionObserver<HTMLDivElement>();
|
const sectionRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const toNumber = (value: string) => {
|
const { t } = useTextController();
|
||||||
return Number(t(value));
|
|
||||||
};
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const [entry] = entries;
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setIsVisible(true);
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
threshold: 0.1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sectionRef.current) {
|
||||||
|
observer.observe(sectionRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={sectionRef} className='my-8 grid grid-cols-1 gap-6 text-center'>
|
||||||
ref={sectionRef}
|
<div className='transform rounded-lg bg-white p-3 shadow-md transition-transform hover:scale-105 sm:p-6'>
|
||||||
className='my-4 grid grid-cols-1 gap-6 text-center md:my-8'
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
>
|
<Users className='h-6 w-6 text-red-600' />
|
||||||
{stats.map((stat, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className='transform rounded-lg bg-white p-3 shadow-md transition-transform hover:scale-105 sm:p-6'
|
|
||||||
>
|
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
|
||||||
<Users className='h-6 w-6 text-red-600' />
|
|
||||||
</div>
|
|
||||||
<h3 className='text-2xl font-bold text-gray-900'>
|
|
||||||
{isVisible ? (
|
|
||||||
<AnimatedCounter
|
|
||||||
end={toNumber(stat.value)}
|
|
||||||
suffix={t(stat.suffix)}
|
|
||||||
decimals={stat.decimals || 0}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
`0${t(stat.suffix)}`
|
|
||||||
)}
|
|
||||||
</h3>
|
|
||||||
<p className='text-gray-600'>{t(stat.label)}</p>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<h3 className='text-2xl font-bold text-gray-900'>
|
||||||
|
{isVisible ? <AnimatedCounter end={150} suffix='+' /> : '0+'}
|
||||||
|
</h3>
|
||||||
|
<p className='text-gray-600'>{t('about.stats.items.2.label')}</p>
|
||||||
|
</div>
|
||||||
|
<div className='transform rounded-lg bg-white p-3 shadow-md transition-transform hover:scale-105 sm:p-6'>
|
||||||
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
|
<Users className='h-6 w-6 text-red-600' />
|
||||||
|
</div>
|
||||||
|
<h3 className='text-2xl font-bold text-gray-900'>
|
||||||
|
{isVisible ? <AnimatedCounter end={5} suffix='M+' /> : '0M+'}
|
||||||
|
</h3>
|
||||||
|
<p className='text-gray-600'>{t('about.stats.items.4.label')}</p>
|
||||||
|
</div>
|
||||||
|
<div className='transform rounded-lg bg-white p-3 shadow-md transition-transform hover:scale-105 sm:p-6'>
|
||||||
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
|
<Users className='h-6 w-6 text-red-600' />
|
||||||
|
</div>
|
||||||
|
<h3 className='text-2xl font-bold text-gray-900'>
|
||||||
|
{isVisible ? (
|
||||||
|
<AnimatedCounter end={98} suffix='%' decimals={1} />
|
||||||
|
) : (
|
||||||
|
'0%'
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<p className='text-gray-600'>{t('about.stats.items.5.label')}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
|
||||||
|
|
||||||
export const AppStoreButtons = ({ className = '' }: { className?: string }) => {
|
|
||||||
const { t } = useTextController();
|
|
||||||
|
|
||||||
const playStoreLink = t('play.google.com');
|
|
||||||
const appStoreLink = t('app.store');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`flex flex-wrap gap-4 ${className}`}>
|
|
||||||
{/* Google Play Button */}
|
|
||||||
<a
|
|
||||||
href={playStoreLink}
|
|
||||||
target='_blank'
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
className='flex items-center gap-3 rounded-xl bg-black px-5 py-2.5 text-white transition-all hover:scale-105 hover:bg-gray-900 shadow-lg'
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox='0 0 32 32'
|
|
||||||
className='w-7 h-7'
|
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
|
||||||
fill='none'
|
|
||||||
>
|
|
||||||
<mask id='mask0_87_8320' maskUnits='userSpaceOnUse' x='7' y='3' width='24' height='26' style={{ maskType: 'alpha' }}>
|
|
||||||
<path d='M30.0484 14.4004C31.3172 15.0986 31.3172 16.9014 30.0484 17.5996L9.75627 28.7659C8.52052 29.4459 7 28.5634 7 27.1663L7 4.83374C7 3.43657 8.52052 2.55415 9.75627 3.23415L30.0484 14.4004Z' fill='#C4C4C4'/>
|
|
||||||
</mask>
|
|
||||||
<g mask='url(#mask0_87_8320)'>
|
|
||||||
<path d='M7.63473 28.5466L20.2923 15.8179L7.84319 3.29883C7.34653 3.61721 7 4.1669 7 4.8339V27.1664C7 27.7355 7.25223 28.2191 7.63473 28.5466Z' fill='url(#paint0_linear_87_8320)'/>
|
|
||||||
<path d='M30.048 14.4003C31.3169 15.0985 31.3169 16.9012 30.048 17.5994L24.9287 20.4165L20.292 15.8175L24.6923 11.4531L30.048 14.4003Z' fill='url(#paint1_linear_87_8320)'/>
|
|
||||||
<path d='M24.9292 20.4168L20.2924 15.8179L7.63477 28.5466C8.19139 29.0232 9.02389 29.1691 9.75635 28.766L24.9292 20.4168Z' fill='url(#paint2_linear_87_8320)'/>
|
|
||||||
<path d='M7.84277 3.29865L20.2919 15.8177L24.6922 11.4533L9.75583 3.23415C9.11003 2.87878 8.38646 2.95013 7.84277 3.29865Z' fill='url(#paint3_linear_87_8320)'/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id='paint0_linear_87_8320' x1='15.6769' y1='10.874' x2='7.07106' y2='19.5506' gradientUnits='userSpaceOnUse'>
|
|
||||||
<stop stopColor='#00C3FF'/>
|
|
||||||
<stop offset='1' stopColor='#1BE2FA'/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id='paint1_linear_87_8320' x1='20.292' y1='15.8176' x2='31.7381' y2='15.8176' gradientUnits='userSpaceOnUse'>
|
|
||||||
<stop stopColor='#FFCE00'/>
|
|
||||||
<stop offset='1' stopColor='#FFEA00'/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id='paint2_linear_87_8320' x1='7.36932' y1='30.1004' x2='22.595' y2='17.8937' gradientUnits='userSpaceOnUse'>
|
|
||||||
<stop stopColor='#DE2453'/>
|
|
||||||
<stop offset='1' stopColor='#FE3944'/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id='paint3_linear_87_8320' x1='8.10725' y1='1.90137' x2='22.5971' y2='13.7365' gradientUnits='userSpaceOnUse'>
|
|
||||||
<stop stopColor='#11D574'/>
|
|
||||||
<stop offset='1' stopColor='#01F176'/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
<div className='flex flex-col items-start leading-none'>
|
|
||||||
<span className='text-[10px] uppercase font-medium opacity-80 mb-0.5'>Get it on</span>
|
|
||||||
<span className='text-base font-bold'>Google Play</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{/* App Store Button */}
|
|
||||||
<a
|
|
||||||
href={appStoreLink}
|
|
||||||
target='_blank'
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
className='flex items-center gap-3 rounded-xl bg-black px-5 py-2.5 text-white transition-all hover:scale-105 hover:bg-gray-900 shadow-lg'
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox='0 0 32 32'
|
|
||||||
className='w-7 h-7'
|
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
|
||||||
fill='none'
|
|
||||||
>
|
|
||||||
<circle cx='16' cy='16' r='14' fill='url(#paint0_linear_87_8317)'/>
|
|
||||||
<path d='M18.4468 8.65403C18.7494 8.12586 18.5685 7.45126 18.0428 7.14727C17.5171 6.84328 16.8456 7.02502 16.543 7.55318L16.0153 8.47442L15.4875 7.55318C15.1849 7.02502 14.5134 6.84328 13.9877 7.14727C13.462 7.45126 13.2811 8.12586 13.5837 8.65403L14.748 10.6864L11.0652 17.1149H8.09831C7.49173 17.1149 7 17.6089 7 18.2183C7 18.8277 7.49173 19.3217 8.09831 19.3217H18.4324C18.523 19.0825 18.6184 18.6721 18.5169 18.2949C18.3644 17.7279 17.8 17.1149 16.8542 17.1149H13.5997L18.4468 8.65403Z' fill='white'/>
|
|
||||||
<path d='M11.6364 20.5419C11.449 20.3328 11.0292 19.9987 10.661 19.8888C10.0997 19.7211 9.67413 19.8263 9.45942 19.9179L8.64132 21.346C8.33874 21.8741 8.51963 22.5487 9.04535 22.8527C9.57107 23.1567 10.2425 22.975 10.5451 22.4468L11.6364 20.5419Z' fill='white'/>
|
|
||||||
<path d='M22.2295 19.3217H23.9017C24.5083 19.3217 25 18.8277 25 18.2183C25 17.6089 24.5083 17.1149 23.9017 17.1149H20.9653L17.6575 11.3411C17.4118 11.5757 16.9407 12.175 16.8695 12.8545C16.778 13.728 16.9152 14.4636 17.3271 15.1839C18.7118 17.6056 20.0987 20.0262 21.4854 22.4468C21.788 22.975 22.4594 23.1567 22.9852 22.8527C23.5109 22.5487 23.6918 21.8741 23.3892 21.346L22.2295 19.3217Z' fill='white'/>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id='paint0_linear_87_8317' x1='16' y1='2' x2='16' y2='30' gradientUnits='userSpaceOnUse'>
|
|
||||||
<stop stopColor='#2AC9FA'/>
|
|
||||||
<stop offset='1' stopColor='#1F65EB'/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
<div className='flex flex-col items-start leading-none'>
|
|
||||||
<span className='text-[10px] uppercase font-medium opacity-80 mb-0.5'>Download on the</span>
|
|
||||||
<span className='text-base font-bold'>App Store</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { ComponentProps } from 'react';
|
|
||||||
|
|
||||||
import { cn } from '../lib/utils';
|
|
||||||
|
|
||||||
interface ContainerProps extends ComponentProps<'div'> {}
|
|
||||||
|
|
||||||
export function Container({ children, className, ...props }: ContainerProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn('container mx-auto px-2.5 py-8 sm:py-16', className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -10,9 +10,7 @@ import { Discounts } from '@/app/api-utlities/@types/index';
|
|||||||
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 PromoImg from '../../../public/main/#promotions/995b9daa-959f-4bd5-9135-ef7c47148f2c.jpg'
|
||||||
import PromoImg from '../../../public/main/#promotions/995b9daa-959f-4bd5-9135-ef7c47148f2c.jpg';
|
|
||||||
|
|
||||||
const promotions = [
|
const promotions = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -107,7 +105,7 @@ export default function PromotionSlider({ discounts }: PromotionSliderProps) {
|
|||||||
src={PromoImg}
|
src={PromoImg}
|
||||||
alt={promo.name}
|
alt={promo.name}
|
||||||
fill
|
fill
|
||||||
className='rounded-lg object-contain p-2'
|
className='object-contain p-2 rounded-lg'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -122,7 +120,7 @@ export default function PromotionSlider({ discounts }: PromotionSliderProps) {
|
|||||||
? `Действует до: ${promo.expiresAt}`
|
? `Действует до: ${promo.expiresAt}`
|
||||||
: null}
|
: null}
|
||||||
</span>
|
</span>
|
||||||
{/* <Link href='#'>
|
<Link href='#'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
size='sm'
|
size='sm'
|
||||||
@ -130,7 +128,7 @@ export default function PromotionSlider({ discounts }: PromotionSliderProps) {
|
|||||||
>
|
>
|
||||||
{t('common.buttons.readMore')}
|
{t('common.buttons.readMore')}
|
||||||
</Button>
|
</Button>
|
||||||
</Link> */}
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
import { Star } from 'lucide-react';
|
|
||||||
|
|
||||||
export const Rating = ({ rating }: { rating: number }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{Array(5)
|
|
||||||
.fill(0)
|
|
||||||
.map((_, i) => (
|
|
||||||
<Star
|
|
||||||
key={i}
|
|
||||||
className={`h-5 w-5 ${i < Number(rating) ? 'fill-yellow-400 text-yellow-400' : 'text-gray-300'}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Reviews } from '@/app/api-utlities/@types';
|
|
||||||
|
|
||||||
import { Rating } from '@/shared/components/rating';
|
|
||||||
import { Card, CardContent } from '@/shared/shadcn-ui/card';
|
|
||||||
|
|
||||||
type ReviewProps = {
|
|
||||||
review: Reviews[number];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Review = ({ review }: ReviewProps) => {
|
|
||||||
return (
|
|
||||||
<Card className='overflow-hidden transition-all hover:shadow-lg'>
|
|
||||||
<CardContent className='p-6'>
|
|
||||||
<div className='mb-4 flex'>
|
|
||||||
<Rating rating={review.rating} />
|
|
||||||
</div>
|
|
||||||
<p className='mb-4 text-gray-600 italic'>"{review.review}"</p>
|
|
||||||
<p className='font-semibold'>{review.fullname}</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import { Loader2 } from 'lucide-react';
|
|
||||||
|
|
||||||
import { cn } from '../lib/utils';
|
|
||||||
|
|
||||||
interface SpinnerProps {
|
|
||||||
className?: string;
|
|
||||||
size?: 'sm' | 'md' | 'lg';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Spinner({ className, size = 'md' }: SpinnerProps) {
|
|
||||||
const sizeClasses = {
|
|
||||||
sm: 'h-4 w-4',
|
|
||||||
md: 'h-6 w-6',
|
|
||||||
lg: 'h-8 w-8',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Loader2 className={cn(`animate-spin ${sizeClasses[size]}`, className)} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import { cn } from '../lib/utils';
|
|
||||||
import { Spinner } from './spinner';
|
|
||||||
|
|
||||||
interface TableLoadingOverlayProps {
|
|
||||||
isLoading: boolean;
|
|
||||||
message?: string;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TableLoadingOverlay({
|
|
||||||
isLoading,
|
|
||||||
message = 'Загрузка данных...',
|
|
||||||
className,
|
|
||||||
}: TableLoadingOverlayProps) {
|
|
||||||
if (!isLoading) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'absolute inset-0 z-10 flex flex-col items-center justify-center bg-white/80 backdrop-blur-[1px] transition-opacity duration-200',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Spinner size='lg' className='mb-2 text-red-600' />
|
|
||||||
<p className='font-medium text-gray-700'>{message}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
export const useIntersectionObserver = <T extends HTMLElement>() => {
|
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
|
||||||
const sectionRef = useRef<T>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
const [entry] = entries;
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
setIsVisible(true);
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
threshold: 0.1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (sectionRef.current) {
|
|
||||||
observer.observe(sectionRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
observer.disconnect();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { sectionRef, isVisible };
|
|
||||||
};
|
|
||||||
25
src/shared/language/api/text-control.api.ts
Normal file
25
src/shared/language/api/text-control.api.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { jsonToGraphQLQuery } from 'json-to-graphql-query';
|
||||||
|
|
||||||
|
import { presentTexts } from '@/app/api-utlities/presenters';
|
||||||
|
import { textsRequest } from '@/app/api-utlities/requests/common';
|
||||||
|
|
||||||
|
import { taylorAPI } from '@/shared/api/taylor-api';
|
||||||
|
import { TextItem } from '@/shared/types/text.types';
|
||||||
|
|
||||||
|
export const textControlApi = taylorAPI.injectEndpoints({
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
fetchText: builder.query<TextItem[], void>({
|
||||||
|
query: () => ({
|
||||||
|
url: '',
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
query: jsonToGraphQLQuery({ query: textsRequest }),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
transformResponse: (response: any) => {
|
||||||
|
return presentTexts(response.data._kontentSajta);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
@ -7,9 +7,7 @@ import { TextControlContext } from '../context/text-control-provider';
|
|||||||
export function useTextController() {
|
export function useTextController() {
|
||||||
const context = useContext(TextControlContext);
|
const context = useContext(TextControlContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error(
|
throw new Error('useLanguage must be used within a LanguageProvider');
|
||||||
'useTextController must be used within a TextControlProvider',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof context.t !== 'function') {
|
if (typeof context.t !== 'function') {
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
import { formatInTimeZone } from 'date-fns-tz';
|
|
||||||
|
|
||||||
export const formatDate = (
|
|
||||||
date: Date | string,
|
|
||||||
formatStr: string = 'dd.MM.yyyy HH:mm',
|
|
||||||
) => {
|
|
||||||
const utcDate = new Date(date);
|
|
||||||
return formatInTimeZone(utcDate, 'UTC', formatStr);
|
|
||||||
};
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import { jsonToGraphQLQuery } from 'json-to-graphql-query';
|
|
||||||
|
|
||||||
import { presentMedia } from '@/app/api-utlities/presenters';
|
|
||||||
import { mediaRequest } from '@/app/api-utlities/requests/common';
|
|
||||||
|
|
||||||
import { taylorAPI } from '@/shared/api/taylor-api';
|
|
||||||
import { MediaItem } from '@/shared/types/media.type';
|
|
||||||
|
|
||||||
export const mediaControlApi = taylorAPI.injectEndpoints({
|
|
||||||
endpoints: (builder) => ({
|
|
||||||
fetchMedia: builder.query<MediaItem[], void>({
|
|
||||||
query: () => ({
|
|
||||||
url: '',
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
query: jsonToGraphQLQuery({ query: mediaRequest }),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
transformResponse: (response: any) => {
|
|
||||||
return presentMedia(response.data.mediaKontentSajta);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { createContext, type ReactNode } from 'react';
|
|
||||||
|
|
||||||
import { MediaItem } from '@/shared/types/media.type';
|
|
||||||
|
|
||||||
export type MediaMap = Record<string, MediaItem>;
|
|
||||||
|
|
||||||
type MediaControlContextType = {
|
|
||||||
m: (key: string) => string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MediaControlContext = createContext<
|
|
||||||
MediaControlContextType | undefined
|
|
||||||
>(undefined);
|
|
||||||
|
|
||||||
export function MediaControlProvider({
|
|
||||||
children,
|
|
||||||
mediaItems,
|
|
||||||
}: {
|
|
||||||
children: ReactNode;
|
|
||||||
mediaItems?: MediaItem[];
|
|
||||||
}) {
|
|
||||||
const mediaMap = mediaItems?.reduce((pr, cr) => {
|
|
||||||
pr[cr.key] = cr;
|
|
||||||
return pr;
|
|
||||||
}, {} as MediaMap);
|
|
||||||
|
|
||||||
const getMedia = (key: string): string | null => {
|
|
||||||
if (mediaMap?.[key]) {
|
|
||||||
return mediaMap[key].photo;
|
|
||||||
}
|
|
||||||
console.warn(`Media key not found: ${key}`);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MediaControlContext.Provider value={{ m: getMedia }}>
|
|
||||||
{children}
|
|
||||||
</MediaControlContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useContext } from 'react';
|
|
||||||
|
|
||||||
import { MediaControlContext } from '../context/media-control.provider';
|
|
||||||
|
|
||||||
export function useMediaController() {
|
|
||||||
const context = useContext(MediaControlContext);
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error(
|
|
||||||
'useMediaController must be used within a MediaControlProvider',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof context.m !== 'function') {
|
|
||||||
throw new Error('Media function (m) is not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
@ -1,13 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { TooltipProvider } from '@radix-ui/react-tooltip';
|
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import { TextControlProvider } from '../language';
|
import { TextControlProvider } from '../language';
|
||||||
import { MediaControlProvider } from '../media/context/media-control.provider';
|
|
||||||
import { store } from '../store';
|
import { store } from '../store';
|
||||||
import { ThemeProvider } from '../theme/theme-provider';
|
import { ThemeProvider } from '../theme/theme-provider';
|
||||||
import { MediaItem } from '../types/media.type';
|
|
||||||
import { TextItem } from '../types/text.types';
|
import { TextItem } from '../types/text.types';
|
||||||
import { AosProvider } from './aos-provider';
|
import { AosProvider } from './aos-provider';
|
||||||
import { Toaster } from './toaster';
|
import { Toaster } from './toaster';
|
||||||
@ -15,32 +12,23 @@ import { Toaster } from './toaster';
|
|||||||
type ProvidersProps = {
|
type ProvidersProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
textItems: TextItem[];
|
textItems: TextItem[];
|
||||||
mediaItems: MediaItem[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Providers = ({
|
export const Providers = ({ children, textItems }: ProvidersProps) => {
|
||||||
children,
|
|
||||||
textItems,
|
|
||||||
mediaItems,
|
|
||||||
}: ProvidersProps) => {
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<TextControlProvider textItems={textItems}>
|
<TextControlProvider textItems={textItems}>
|
||||||
<MediaControlProvider mediaItems={mediaItems}>
|
<ThemeProvider
|
||||||
<ThemeProvider
|
attribute='class'
|
||||||
attribute='class'
|
defaultTheme='light'
|
||||||
defaultTheme='light'
|
enableSystem
|
||||||
enableSystem
|
disableTransitionOnChange
|
||||||
disableTransitionOnChange
|
>
|
||||||
>
|
<AosProvider>
|
||||||
<TooltipProvider>
|
{children}
|
||||||
<AosProvider>
|
<Toaster />
|
||||||
{children}
|
</AosProvider>
|
||||||
<Toaster />
|
</ThemeProvider>
|
||||||
</AosProvider>
|
|
||||||
</TooltipProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
</MediaControlProvider>
|
|
||||||
</TextControlProvider>
|
</TextControlProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
10
src/shared/shadcn-ui/conteiner.tsx
Normal file
10
src/shared/shadcn-ui/conteiner.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
interface ContainerProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
export default function Container({children}: ContainerProps) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-2.5 py-1">{children}</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { cn } from '@/shared/lib/utils';
|
|
||||||
|
|
||||||
const Textarea = React.forwardRef<
|
|
||||||
HTMLTextAreaElement,
|
|
||||||
React.ComponentProps<'textarea'>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<textarea
|
|
||||||
className={cn(
|
|
||||||
'border-input placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[60px] w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-sm focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
Textarea.displayName = 'Textarea';
|
|
||||||
|
|
||||||
export { Textarea };
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { cn } from '@/shared/lib/utils';
|
|
||||||
|
|
||||||
const TooltipProvider = TooltipPrimitive.Provider;
|
|
||||||
|
|
||||||
const Tooltip = TooltipPrimitive.Root;
|
|
||||||
|
|
||||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
||||||
|
|
||||||
const TooltipContent = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof TooltipPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
'bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 origin-[--radix-tooltip-content-transform-origin] overflow-hidden rounded-md px-3 py-1.5 text-xs',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
));
|
|
||||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
|
||||||
@ -1,548 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2025 TaylorDB
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface FileInformation {
|
|
||||||
fieldname: string;
|
|
||||||
originalname: string;
|
|
||||||
encoding: string;
|
|
||||||
mimetype: string;
|
|
||||||
destination: string;
|
|
||||||
filename: string;
|
|
||||||
path: string;
|
|
||||||
size: number;
|
|
||||||
format: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UploadResponse {
|
|
||||||
collectionName: string;
|
|
||||||
fileInformation: FileInformation;
|
|
||||||
metadata: {
|
|
||||||
thumbnails: any[];
|
|
||||||
clips: any[];
|
|
||||||
};
|
|
||||||
baseId: string;
|
|
||||||
storageAdaptor: string;
|
|
||||||
_id: string;
|
|
||||||
__v: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AttachmentColumnValue {
|
|
||||||
url: string;
|
|
||||||
fileType: string;
|
|
||||||
size: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Attachment {
|
|
||||||
public readonly collectionName: string;
|
|
||||||
public readonly fileInformation: FileInformation;
|
|
||||||
public readonly metadata: { thumbnails: any[]; clips: any[] };
|
|
||||||
public readonly baseId: string;
|
|
||||||
public readonly storageAdaptor: string;
|
|
||||||
public readonly _id: string;
|
|
||||||
|
|
||||||
constructor(data: UploadResponse) {
|
|
||||||
this.collectionName = data.collectionName;
|
|
||||||
this.fileInformation = data.fileInformation;
|
|
||||||
this.metadata = data.metadata;
|
|
||||||
this.baseId = data.baseId;
|
|
||||||
this.storageAdaptor = data.storageAdaptor;
|
|
||||||
this._id = data._id;
|
|
||||||
}
|
|
||||||
|
|
||||||
toColumnValue(): AttachmentColumnValue {
|
|
||||||
return {
|
|
||||||
url: this.fileInformation.path,
|
|
||||||
fileType: this.fileInformation.mimetype,
|
|
||||||
size: this.fileInformation.size,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type IsWithinOperatorValue =
|
|
||||||
| 'pastWeek'
|
|
||||||
| 'pastMonth'
|
|
||||||
| 'pastYear'
|
|
||||||
| 'nextWeek'
|
|
||||||
| 'nextMonth'
|
|
||||||
| 'nextYear'
|
|
||||||
| 'daysFromNow'
|
|
||||||
| 'daysAgo'
|
|
||||||
| 'currentWeek'
|
|
||||||
| 'currentMonth'
|
|
||||||
| 'currentYear';
|
|
||||||
|
|
||||||
type DefaultDateFilterValue =
|
|
||||||
| (
|
|
||||||
| 'today'
|
|
||||||
| 'tomorrow'
|
|
||||||
| 'yesterday'
|
|
||||||
| 'oneWeekAgo'
|
|
||||||
| 'oneWeekFromNow'
|
|
||||||
| 'oneMonthAgo'
|
|
||||||
| 'oneMonthFromNow'
|
|
||||||
)
|
|
||||||
| ['exactDay' | 'exactTimestamp', string]
|
|
||||||
| ['daysAgo' | 'daysFromNow', number];
|
|
||||||
|
|
||||||
type DateFilters = {
|
|
||||||
'=': DefaultDateFilterValue;
|
|
||||||
'!=': DefaultDateFilterValue;
|
|
||||||
'<': DefaultDateFilterValue;
|
|
||||||
'>': DefaultDateFilterValue;
|
|
||||||
'<=': DefaultDateFilterValue;
|
|
||||||
'>=': DefaultDateFilterValue;
|
|
||||||
isWithIn:
|
|
||||||
| IsWithinOperatorValue
|
|
||||||
| { value: 'daysAgo' | 'daysFromNow'; date: number };
|
|
||||||
isEmpty: boolean;
|
|
||||||
isNotEmpty: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DateAggregations = {
|
|
||||||
empty: number;
|
|
||||||
filled: number;
|
|
||||||
unique: number;
|
|
||||||
percentEmpty: number;
|
|
||||||
percentFilled: number;
|
|
||||||
percentUnique: number;
|
|
||||||
min: number | null;
|
|
||||||
max: number | null;
|
|
||||||
daysRange: number | null;
|
|
||||||
monthRange: number | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TextFilters = {
|
|
||||||
'=': string;
|
|
||||||
'!=': string;
|
|
||||||
caseEqual: string;
|
|
||||||
hasAnyOf: string[];
|
|
||||||
contains: string;
|
|
||||||
startsWith: string;
|
|
||||||
endsWith: string;
|
|
||||||
doesNotContain: string;
|
|
||||||
isEmpty: never;
|
|
||||||
isNotEmpty: never;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LinkFilters = {
|
|
||||||
hasAnyOf: number[];
|
|
||||||
hasAllOf: number[];
|
|
||||||
isExactly: number[];
|
|
||||||
'=': number;
|
|
||||||
hasNoneOf: number[];
|
|
||||||
contains: string;
|
|
||||||
doesNotContain: string;
|
|
||||||
isEmpty: never;
|
|
||||||
isNotEmpty: never;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SelectFilters<O extends readonly string[]> = {
|
|
||||||
hasAnyOf: O[number][];
|
|
||||||
hasAllOf: O[number][];
|
|
||||||
isExactly: O[number][];
|
|
||||||
'=': O[number];
|
|
||||||
hasNoneOf: O[number][];
|
|
||||||
contains: string;
|
|
||||||
doesNotContain: string;
|
|
||||||
isEmpty: never;
|
|
||||||
isNotEmpty: never;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LinkAggregations = {
|
|
||||||
empty: number;
|
|
||||||
filled: number;
|
|
||||||
percentEmpty: number;
|
|
||||||
percentFilled: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type NumberFilters = {
|
|
||||||
'=': number;
|
|
||||||
'!=': number;
|
|
||||||
'>': number;
|
|
||||||
'>=': number;
|
|
||||||
'<': number;
|
|
||||||
'<=': number;
|
|
||||||
hasAnyOf: number[];
|
|
||||||
hasNoneOf: number[];
|
|
||||||
isEmpty: never;
|
|
||||||
isNotEmpty: never;
|
|
||||||
};
|
|
||||||
|
|
||||||
type NumberAggregations = {
|
|
||||||
sum: number;
|
|
||||||
average: number;
|
|
||||||
median: number;
|
|
||||||
min: number | null;
|
|
||||||
max: number | null;
|
|
||||||
range: number;
|
|
||||||
standardDeviation: number;
|
|
||||||
histogram: Record<string, number>;
|
|
||||||
empty: number;
|
|
||||||
filled: number;
|
|
||||||
unique: number;
|
|
||||||
percentEmpty: number;
|
|
||||||
percentFilled: number;
|
|
||||||
percentUnique: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CheckboxFilters = {
|
|
||||||
'=': number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Column types
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export type ColumnType<
|
|
||||||
S,
|
|
||||||
U,
|
|
||||||
I,
|
|
||||||
R extends boolean,
|
|
||||||
F extends { [key: string]: any } = object,
|
|
||||||
A extends { [key: string]: any } = object,
|
|
||||||
> = {
|
|
||||||
raw: S;
|
|
||||||
insert: I;
|
|
||||||
update: U;
|
|
||||||
filters: F;
|
|
||||||
aggregations: A;
|
|
||||||
isRequired: R;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DateColumnType<R extends boolean> = ColumnType<
|
|
||||||
string,
|
|
||||||
string,
|
|
||||||
string,
|
|
||||||
R,
|
|
||||||
DateFilters,
|
|
||||||
DateAggregations
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type TextColumnType<R extends boolean> = ColumnType<
|
|
||||||
string,
|
|
||||||
string,
|
|
||||||
string,
|
|
||||||
R,
|
|
||||||
TextFilters
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type ALinkColumnType<
|
|
||||||
T extends string,
|
|
||||||
S,
|
|
||||||
U,
|
|
||||||
I,
|
|
||||||
R extends boolean,
|
|
||||||
F extends { [key: string]: any } = LinkFilters,
|
|
||||||
A extends LinkAggregations = LinkAggregations,
|
|
||||||
> = ColumnType<S, U, I, R, F, A> & {
|
|
||||||
linkedTo: T;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LinkColumnType<
|
|
||||||
T extends string,
|
|
||||||
R extends boolean,
|
|
||||||
> = ALinkColumnType<
|
|
||||||
T,
|
|
||||||
object,
|
|
||||||
number | number[] | { newIds: number[]; deletedIds: number[] },
|
|
||||||
number | number[],
|
|
||||||
R
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type AttachmentColumnType<R extends boolean> = ALinkColumnType<
|
|
||||||
'attachmentTable',
|
|
||||||
AttachmentColumnValue[],
|
|
||||||
Attachment[] | { newIds: number[]; deletedIds: number[] } | number[],
|
|
||||||
Attachment[] | number[],
|
|
||||||
R
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type NumberColumnType<R extends boolean> = ColumnType<
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
R,
|
|
||||||
NumberFilters,
|
|
||||||
NumberAggregations
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type CheckboxColumnType<R extends boolean> = ColumnType<
|
|
||||||
boolean,
|
|
||||||
boolean,
|
|
||||||
boolean,
|
|
||||||
R,
|
|
||||||
CheckboxFilters
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type AutoGeneratedNumberColumnType = ColumnType<
|
|
||||||
number,
|
|
||||||
never,
|
|
||||||
never,
|
|
||||||
false,
|
|
||||||
NumberFilters,
|
|
||||||
NumberAggregations
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type AutoGeneratedDateColumnType = ColumnType<
|
|
||||||
string,
|
|
||||||
never,
|
|
||||||
never,
|
|
||||||
false,
|
|
||||||
DateFilters,
|
|
||||||
DateAggregations
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type SingleSelectColumnType<
|
|
||||||
O extends readonly string[],
|
|
||||||
R extends boolean,
|
|
||||||
> = ALinkColumnType<
|
|
||||||
'selectTable',
|
|
||||||
O[number],
|
|
||||||
O[number] | O[number][],
|
|
||||||
O[number] | O[number][],
|
|
||||||
R,
|
|
||||||
SelectFilters<O>
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type TableRaws<T extends keyof TaylorDatabase> = {
|
|
||||||
[K in keyof TaylorDatabase[T]]: TaylorDatabase[T][K] extends ColumnType<
|
|
||||||
infer S,
|
|
||||||
any,
|
|
||||||
any,
|
|
||||||
infer R,
|
|
||||||
any,
|
|
||||||
any
|
|
||||||
>
|
|
||||||
? R extends true
|
|
||||||
? S
|
|
||||||
: S | undefined
|
|
||||||
: never;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TableInserts<T extends keyof TaylorDatabase> = {
|
|
||||||
[K in keyof TaylorDatabase[T]]: TaylorDatabase[T][K] extends ColumnType<
|
|
||||||
any,
|
|
||||||
infer I,
|
|
||||||
any,
|
|
||||||
infer R,
|
|
||||||
any,
|
|
||||||
any
|
|
||||||
>
|
|
||||||
? R extends true
|
|
||||||
? I
|
|
||||||
: I | undefined
|
|
||||||
: never;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TableUpdates<T extends keyof TaylorDatabase> = {
|
|
||||||
[K in keyof TaylorDatabase[T]]: TaylorDatabase[T][K] extends ColumnType<
|
|
||||||
any,
|
|
||||||
any,
|
|
||||||
infer U,
|
|
||||||
any,
|
|
||||||
any,
|
|
||||||
any
|
|
||||||
>
|
|
||||||
? U
|
|
||||||
: never;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SelectTable = {
|
|
||||||
id: AutoGeneratedNumberColumnType;
|
|
||||||
name: TextColumnType<true>;
|
|
||||||
color: TextColumnType<true>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AttachmentTable = {
|
|
||||||
id: AutoGeneratedNumberColumnType;
|
|
||||||
name: TextColumnType<true>;
|
|
||||||
metadata: TextColumnType<true>;
|
|
||||||
size: NumberColumnType<true>;
|
|
||||||
fileType: TextColumnType<true>;
|
|
||||||
url: TextColumnType<true>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CollaboratorsTable = {
|
|
||||||
id: AutoGeneratedNumberColumnType;
|
|
||||||
name: TextColumnType<true>;
|
|
||||||
emailAddress: TextColumnType<true>;
|
|
||||||
avatar: TextColumnType<true>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TaylorDatabase = {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Internal tables, these tables can not be queried directly.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
selectTable: SelectTable;
|
|
||||||
attachmentTable: AttachmentTable;
|
|
||||||
collaboratorsTable: CollaboratorsTable;
|
|
||||||
vakansii: VakansiiTable;
|
|
||||||
partnyory: PartnyoryTable;
|
|
||||||
azs: AzsTable;
|
|
||||||
akcii: AkciiTable;
|
|
||||||
istoriyaKompanii: IstoriyaKompaniiTable;
|
|
||||||
komanda: KomandaTable;
|
|
||||||
otzyvy: OtzyvyTable;
|
|
||||||
tekstovyjKontentSajta: TekstovyjKontentSajtaTable;
|
|
||||||
sertifikaty: SertifikatyTable;
|
|
||||||
mediaKontentSajta: MediaKontentSajtaTable;
|
|
||||||
blagotvoritelnyjFond: BlagotvoritelnyjFondTable;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const VakansiiTipOptions = ['Офис', 'Заправки'] as const;
|
|
||||||
export const VakansiiLokaciyaOptions = ['Душанбе'] as const;
|
|
||||||
|
|
||||||
type VakansiiTable = {
|
|
||||||
id: NumberColumnType<false>;
|
|
||||||
createdAt: AutoGeneratedDateColumnType;
|
|
||||||
updatedAt: AutoGeneratedDateColumnType;
|
|
||||||
zagolovok: TextColumnType<false>;
|
|
||||||
tip: SingleSelectColumnType<typeof VakansiiTipOptions, false>;
|
|
||||||
lokaciya: SingleSelectColumnType<typeof VakansiiLokaciyaOptions, false>;
|
|
||||||
tegi: LinkColumnType<'selectTable', false>;
|
|
||||||
};
|
|
||||||
type PartnyoryTable = {
|
|
||||||
id: NumberColumnType<false>;
|
|
||||||
createdAt: AutoGeneratedDateColumnType;
|
|
||||||
updatedAt: AutoGeneratedDateColumnType;
|
|
||||||
nazvanie: TextColumnType<false>;
|
|
||||||
izobrozhenie: AttachmentColumnType<false>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AzsChasyRabotyOptions = ['Круглосуточно'] as const;
|
|
||||||
export const AzsRegionOptions = [
|
|
||||||
'Душанбе',
|
|
||||||
'Бохтар',
|
|
||||||
'Худжанд',
|
|
||||||
'Регар',
|
|
||||||
'Вахдат',
|
|
||||||
'А.Джоми',
|
|
||||||
'Обикиик',
|
|
||||||
'Кулоб',
|
|
||||||
'Дахана',
|
|
||||||
'Ёвон',
|
|
||||||
'Панч',
|
|
||||||
'Исфара',
|
|
||||||
'Мастчох',
|
|
||||||
'Хисор',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
type AzsTable = {
|
|
||||||
id: NumberColumnType<false>;
|
|
||||||
createdAt: AutoGeneratedDateColumnType;
|
|
||||||
updatedAt: AutoGeneratedDateColumnType;
|
|
||||||
imya: TextColumnType<false>;
|
|
||||||
adress: TextColumnType<false>;
|
|
||||||
opisanie: TextColumnType<false>;
|
|
||||||
chasyRaboty: SingleSelectColumnType<typeof AzsChasyRabotyOptions, false>;
|
|
||||||
lat: TextColumnType<false>;
|
|
||||||
long: TextColumnType<false>;
|
|
||||||
avtomojka: CheckboxColumnType<false>;
|
|
||||||
dt: CheckboxColumnType<false>;
|
|
||||||
ai92: CheckboxColumnType<false>;
|
|
||||||
ai95: CheckboxColumnType<false>;
|
|
||||||
z100: CheckboxColumnType<false>;
|
|
||||||
propan: CheckboxColumnType<false>;
|
|
||||||
zaryadnayaStanciya: CheckboxColumnType<false>;
|
|
||||||
miniMarket: CheckboxColumnType<false>;
|
|
||||||
tualet: CheckboxColumnType<false>;
|
|
||||||
region: SingleSelectColumnType<typeof AzsRegionOptions, false>;
|
|
||||||
foto: AttachmentColumnType<false>;
|
|
||||||
};
|
|
||||||
type AkciiTable = {
|
|
||||||
id: NumberColumnType<false>;
|
|
||||||
createdAt: AutoGeneratedDateColumnType;
|
|
||||||
updatedAt: AutoGeneratedDateColumnType;
|
|
||||||
zagolovok: TextColumnType<false>;
|
|
||||||
opisanie: TextColumnType<false>;
|
|
||||||
do: DateColumnType<false>;
|
|
||||||
foto: AttachmentColumnType<false>;
|
|
||||||
};
|
|
||||||
type IstoriyaKompaniiTable = {
|
|
||||||
id: NumberColumnType<false>;
|
|
||||||
createdAt: AutoGeneratedDateColumnType;
|
|
||||||
updatedAt: AutoGeneratedDateColumnType;
|
|
||||||
zagolovok: TextColumnType<false>;
|
|
||||||
god: NumberColumnType<false>;
|
|
||||||
opisanie: TextColumnType<false>;
|
|
||||||
};
|
|
||||||
type KomandaTable = {
|
|
||||||
id: NumberColumnType<false>;
|
|
||||||
createdAt: AutoGeneratedDateColumnType;
|
|
||||||
updatedAt: AutoGeneratedDateColumnType;
|
|
||||||
polnoeImya: TextColumnType<false>;
|
|
||||||
foto: AttachmentColumnType<false>;
|
|
||||||
zvanie: TextColumnType<false>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const OtzyvyStatusOptions = ['Опубликовано', 'Не публиковать'] as const;
|
|
||||||
|
|
||||||
type OtzyvyTable = {
|
|
||||||
id: NumberColumnType<false>;
|
|
||||||
createdAt: AutoGeneratedDateColumnType;
|
|
||||||
updatedAt: AutoGeneratedDateColumnType;
|
|
||||||
polnoeImya: TextColumnType<false>;
|
|
||||||
otzyv: TextColumnType<false>;
|
|
||||||
rejting: NumberColumnType<false>;
|
|
||||||
status: SingleSelectColumnType<typeof OtzyvyStatusOptions, false>;
|
|
||||||
};
|
|
||||||
type TekstovyjKontentSajtaTable = {
|
|
||||||
id: NumberColumnType<false>;
|
|
||||||
createdAt: AutoGeneratedDateColumnType;
|
|
||||||
updatedAt: AutoGeneratedDateColumnType;
|
|
||||||
klyuchNeIzmenyat: TextColumnType<true>;
|
|
||||||
znachenie: TextColumnType<true>;
|
|
||||||
opisanie: LinkColumnType<'selectTable', false>;
|
|
||||||
};
|
|
||||||
type SertifikatyTable = {
|
|
||||||
id: NumberColumnType<false>;
|
|
||||||
createdAt: AutoGeneratedDateColumnType;
|
|
||||||
updatedAt: AutoGeneratedDateColumnType;
|
|
||||||
nazvanie: TextColumnType<false>;
|
|
||||||
opisanie: TextColumnType<false>;
|
|
||||||
dataVydachi: DateColumnType<false>;
|
|
||||||
dejstvitelenDo: DateColumnType<false>;
|
|
||||||
foto: AttachmentColumnType<false>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MediaKontentSajtaStranicaOptions = [
|
|
||||||
'Главная',
|
|
||||||
'О нас',
|
|
||||||
'Благотворительность',
|
|
||||||
'Общая',
|
|
||||||
'Клиенты',
|
|
||||||
'Программа лояльности',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
type MediaKontentSajtaTable = {
|
|
||||||
id: NumberColumnType<false>;
|
|
||||||
createdAt: AutoGeneratedDateColumnType;
|
|
||||||
updatedAt: AutoGeneratedDateColumnType;
|
|
||||||
mestopolozheniya: TextColumnType<false>;
|
|
||||||
klyuchNeIzmenyat: TextColumnType<true>;
|
|
||||||
foto: AttachmentColumnType<false>;
|
|
||||||
stranica: SingleSelectColumnType<
|
|
||||||
typeof MediaKontentSajtaStranicaOptions,
|
|
||||||
false
|
|
||||||
>;
|
|
||||||
};
|
|
||||||
type BlagotvoritelnyjFondTable = {
|
|
||||||
id: NumberColumnType<false>;
|
|
||||||
createdAt: AutoGeneratedDateColumnType;
|
|
||||||
updatedAt: AutoGeneratedDateColumnType;
|
|
||||||
zagolovok: TextColumnType<false>;
|
|
||||||
opisanie: TextColumnType<false>;
|
|
||||||
data: DateColumnType<false>;
|
|
||||||
lokaciya: TextColumnType<false>;
|
|
||||||
foto: AttachmentColumnType<false>;
|
|
||||||
};
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export interface MediaItem {
|
|
||||||
key: string; // _klyuchNeIzmenya
|
|
||||||
name: string; // _name
|
|
||||||
photo: string | null;
|
|
||||||
}
|
|
||||||
@ -4,49 +4,41 @@ import { Users } from 'lucide-react';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
import AboutCounter from '@/shared/components/about-counter';
|
import AboutCounter from '@/shared/components/about-counter';
|
||||||
import { Container } from '@/shared/components/container';
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { useMediaController } from '@/shared/media/hooks/use-media-controller';
|
|
||||||
|
|
||||||
export const AboutSection = () => {
|
export const AboutSection = () => {
|
||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
const { m } = useMediaController();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id='about'>
|
<section id='about' className='px-2 py-8 sm:py-16'>
|
||||||
<Container>
|
<div className='container mx-auto'>
|
||||||
<div className='text-justify'>
|
<div className='grid items-center gap-12 md:grid-cols-2'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
|
||||||
<Users className='h-6 w-6 text-red-600' />
|
|
||||||
</div>
|
|
||||||
<h2 className='mb-6 text-3xl font-bold tracking-tight sm:text-4xl'>
|
|
||||||
{t('home.about.title')}
|
|
||||||
</h2>
|
|
||||||
<p className='mb-3 text-gray-600 sm:mb-6'>
|
|
||||||
{t('home.about.description1')}
|
|
||||||
</p>
|
|
||||||
<p className='mb-3 text-gray-600 sm:mb-6'>
|
|
||||||
{t('home.about.description2')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className='my-4 grid items-center gap-6 sm:my-8 md:grid-cols-2 md:gap-12'>
|
|
||||||
<div>
|
<div>
|
||||||
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
|
<Users className='h-6 w-6 text-red-600' />
|
||||||
|
</div>
|
||||||
|
<h2 className='mb-6 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
|
{t('home.about.title')}
|
||||||
|
</h2>
|
||||||
|
<p className='mb-6 text-gray-600'>{t('home.about.description1')}</p>
|
||||||
|
<p className='mb-6 text-gray-600'>{t('home.about.description2')}</p>
|
||||||
|
|
||||||
<AboutCounter />
|
<AboutCounter />
|
||||||
<Features />
|
<Features />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className='relative h-[400px] overflow-hidden rounded-xl md:h-full'
|
className='relative h-[400px] overflow-hidden rounded-xl shadow-xl'
|
||||||
data-aos='zoom-in-down'
|
data-aos='zoom-in-down'
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={m('main.price-board') || ''}
|
src='/placeholder.svg?height=400&width=600'
|
||||||
alt='About our company'
|
alt='About our company'
|
||||||
fill
|
fill
|
||||||
className='w-full object-contain p-2.5'
|
className='object-cover'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,47 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
|
||||||
import { useMediaController } from '@/shared/media/hooks/use-media-controller';
|
|
||||||
import { AppStoreButtons } from '@/shared/components/app-store-buttons';
|
|
||||||
|
|
||||||
export const AppDownloadSection = () => {
|
|
||||||
const { t } = useTextController();
|
|
||||||
const { m } = useMediaController();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className='bg-gray-50 py-16 sm:py-24 overflow-hidden'>
|
|
||||||
<Container>
|
|
||||||
<div className='flex flex-col items-center text-center space-y-8'>
|
|
||||||
<div className='space-y-4'>
|
|
||||||
<h2 className='text-3xl sm:text-4xl font-bold tracking-tight text-gray-900'>
|
|
||||||
{t('common.name')} всегда с вами
|
|
||||||
</h2>
|
|
||||||
<p className='text-lg text-gray-600 max-w-2xl mx-auto'>
|
|
||||||
Заправляйтесь быстрее, копите баллы и следите за акциями в нашем мобильном приложении. Ваш верный помощник на дорогах Таджикистана.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AppStoreButtons className='justify-center' />
|
|
||||||
|
|
||||||
<div className='flex flex-wrap items-center justify-center gap-6 text-sm font-medium text-gray-500'>
|
|
||||||
<div className='flex items-center gap-1.5 transition-colors hover:text-red-600'>
|
|
||||||
<svg className='w-5 h-5 text-green-500' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
|
||||||
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M5 13l4 4L19 7' />
|
|
||||||
</svg>
|
|
||||||
Бонусная карта в телефоне
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center gap-1.5 transition-colors hover:text-red-600'>
|
|
||||||
<svg className='w-5 h-5 text-green-500' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
|
||||||
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M5 13l4 4L19 7' />
|
|
||||||
</svg>
|
|
||||||
История заправок
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -4,32 +4,25 @@ import { ChevronRight, Heart } from 'lucide-react';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { useMediaController } from '@/shared/media/hooks/use-media-controller';
|
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
|
|
||||||
export const CharitySection = () => {
|
export const CharitySection = () => {
|
||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
const { m } = useMediaController();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id='charity'>
|
<section id='charity' className='px-2 py-8 sm:py-16'>
|
||||||
<Container>
|
<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
|
||||||
className='relative order-2 h-[400px] w-full overflow-hidden rounded-xl shadow-xl md:order-1'
|
className='relative order-2 h-[400px] w-full overflow-hidden rounded-xl shadow-xl md:order-1'
|
||||||
data-aos='zoom-in'
|
data-aos='zoom-in'
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src='/placeholder.svg?height=400&width=600'
|
||||||
m('home.charity.banner') ||
|
|
||||||
'/placeholder.svg?height=400&width=600'
|
|
||||||
}
|
|
||||||
alt='Charity Foundation'
|
alt='Charity Foundation'
|
||||||
fill
|
fill
|
||||||
className='object-cover'
|
className='object-cover'
|
||||||
loader={({ src }) => src}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='order-1 md:order-2'>
|
<div className='order-1 md:order-2'>
|
||||||
@ -66,7 +59,7 @@ export const CharitySection = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,99 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Check } from 'lucide-react';
|
|
||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
|
||||||
import { useTextController } from '@/shared/language';
|
|
||||||
import { Card, CardContent } from '@/shared/shadcn-ui/card';
|
|
||||||
|
|
||||||
const loyaltyLevels = [
|
|
||||||
{
|
|
||||||
id: 'card-1',
|
|
||||||
borderColor: 'gray-400',
|
|
||||||
title: 'clients.loyalty.works.levels.card-1.title',
|
|
||||||
percent: 'clients.loyalty.works.levels.card-1.percent',
|
|
||||||
bonuses: [
|
|
||||||
'clients.loyalty.works.levels.card-1.bonus-1',
|
|
||||||
'clients.loyalty.works.levels.card-1.bonus-2',
|
|
||||||
'clients.loyalty.works.levels.card-1.bonus-3',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'card-2',
|
|
||||||
borderColor: 'yellow-500',
|
|
||||||
title: 'clients.loyalty.works.levels.card-2.title',
|
|
||||||
percent: 'clients.loyalty.works.levels.card-2.percent',
|
|
||||||
bonuses: [
|
|
||||||
'clients.loyalty.works.levels.card-2.bonus-1',
|
|
||||||
'clients.loyalty.works.levels.card-2.bonus-2',
|
|
||||||
'clients.loyalty.works.levels.card-2.bonus-3',
|
|
||||||
'clients.loyalty.works.levels.card-2.bonus-4',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'card-3',
|
|
||||||
borderColor: 'red-600',
|
|
||||||
title: 'clients.loyalty.works.levels.card-3.title',
|
|
||||||
percent: 'clients.loyalty.works.levels.card-3.percent',
|
|
||||||
bonuses: [
|
|
||||||
'clients.loyalty.works.levels.card-3.bonus-1',
|
|
||||||
'clients.loyalty.works.levels.card-3.bonus-2',
|
|
||||||
'clients.loyalty.works.levels.card-3.bonus-3',
|
|
||||||
'clients.loyalty.works.levels.card-3.bonus-4',
|
|
||||||
'clients.loyalty.works.levels.card-3.bonus-5',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const LoyaltyLevels = () => {
|
|
||||||
const { t } = useTextController();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<div className='mb-12 text-center'>
|
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
|
||||||
{t('clients.loyalty.works.levels.title')}
|
|
||||||
</h2>
|
|
||||||
<p className='mx-auto max-w-2xl text-gray-600'>
|
|
||||||
{t('clients.loyalty.works.levels.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='grid gap-8 md:grid-cols-3'>
|
|
||||||
{loyaltyLevels.map((level) => (
|
|
||||||
<Card
|
|
||||||
key={level.id}
|
|
||||||
data-aos='flip-left'
|
|
||||||
data-aos-duration='500'
|
|
||||||
className={`overflow-hidden border-t-4 border-t-${level.borderColor} transition-all hover:shadow-lg`}
|
|
||||||
>
|
|
||||||
<CardContent className='p-6'>
|
|
||||||
<h3 className='mb-4 text-center text-2xl font-bold'>
|
|
||||||
{t(level.title)}
|
|
||||||
</h3>
|
|
||||||
<div className='mb-6 text-center'>
|
|
||||||
<span className='text-4xl font-bold'>{t(level.percent)}</span>
|
|
||||||
<p className='text-sm text-gray-600'>
|
|
||||||
{t('clients.loyalty.works.levels.card.mark')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<ul className='space-y-2'>
|
|
||||||
{level.bonuses.map((bonus, index) => (
|
|
||||||
<li
|
|
||||||
key={`${level.id}-bonus-${index}`}
|
|
||||||
className='flex items-center'
|
|
||||||
>
|
|
||||||
<Check className='mr-2 h-5 w-5 text-green-500' />
|
|
||||||
<span>{t(bonus)}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LoyaltyLevels;
|
|
||||||
@ -3,9 +3,8 @@
|
|||||||
import { Percent } from 'lucide-react';
|
import { Percent } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { useMediaController } from '@/shared/media/hooks/use-media-controller';
|
import Container from '@/shared/shadcn-ui/conteiner';
|
||||||
|
|
||||||
interface Benefit {
|
interface Benefit {
|
||||||
title: string;
|
title: string;
|
||||||
@ -33,58 +32,50 @@ const benefits: Array<Benefit> = [
|
|||||||
|
|
||||||
export const BenefitsSection = () => {
|
export const BenefitsSection = () => {
|
||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
const { m } = useMediaController();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className='bg-gray-50'>
|
<Container>
|
||||||
<Container>
|
<section className='bg-gray-50 py-16'>
|
||||||
<div className='grid items-center gap-12 md:grid-cols-2'>
|
<div className='container mx-auto'>
|
||||||
<div
|
<div className='grid items-center gap-12 md:grid-cols-2'>
|
||||||
data-aos='fade-right'
|
<div data-aos='fade-right' data-aos-duration='4000' className='order-2 md:order-1'>
|
||||||
data-aos-duration='4000'
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
className='order-2 md:order-1'
|
<Percent className='h-6 w-6 text-red-600' />
|
||||||
>
|
</div>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<h2 className='mb-6 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
<Percent className='h-6 w-6 text-red-600' />
|
{t('clients.benefits.title')}
|
||||||
</div>
|
</h2>
|
||||||
<h2 className='mb-6 text-3xl font-bold tracking-tight sm:text-4xl'>
|
<p className='mb-6 text-gray-600'>
|
||||||
{t('clients.benefits.title')}
|
{t('clients.benefits.subtitle')}
|
||||||
</h2>
|
</p>
|
||||||
<p className='mb-6 text-gray-600'>
|
|
||||||
{t('clients.benefits.subtitle')}
|
<div className='space-y-4'>
|
||||||
</p>
|
{benefits.map(({ title, description }) => {
|
||||||
<div className='space-y-4'>
|
return (
|
||||||
{benefits.map(({ title, description }) => {
|
<div key={title} className='flex items-start'>
|
||||||
return (
|
<div className='mt-1 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-red-600'>
|
||||||
<div key={title} className='flex items-start'>
|
<span className='text-xs text-white'>✓</span>
|
||||||
<div className='mt-1 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-red-600'>
|
</div>
|
||||||
<span className='text-xs text-white'>✓</span>
|
<div className='ml-3'>
|
||||||
|
<h3 className='text-lg font-medium'>{title}</h3>
|
||||||
|
<p className='text-gray-600'>{description}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='ml-3'>
|
);
|
||||||
<h3 className='text-lg font-medium'>{title}</h3>
|
})}
|
||||||
<p className='text-gray-600'>{description}</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div data-aos='fade-up' className='relative order-1 h-[400px] overflow-hidden rounded-xl shadow-xl md:order-2'>
|
||||||
);
|
<Image
|
||||||
})}
|
src='/placeholder.svg?height=400&width=600&text=Преимущества+для+клиентов'
|
||||||
|
alt='Преимущества для клиентов'
|
||||||
|
fill
|
||||||
|
className='object-cover'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
data-aos='fade-up'
|
|
||||||
className='relative order-1 h-[400px] overflow-hidden rounded-xl shadow-xl md:order-2'
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={
|
|
||||||
m('clients.third-section.banner') ||
|
|
||||||
'/placeholder.svg?height=400&width=600&text=Преимущества+для+клиентов'
|
|
||||||
}
|
|
||||||
alt='Преимущества для клиентов'
|
|
||||||
fill
|
|
||||||
className='object-cover'
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</section>
|
||||||
</section>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
import { CreditCard, type LucideProps, Percent, Wallet } from 'lucide-react';
|
import { CreditCard, type LucideProps, Percent, Wallet } from 'lucide-react';
|
||||||
import { type ForwardRefExoticComponent, type RefAttributes } from 'react';
|
import { type ForwardRefExoticComponent, type RefAttributes } from 'react';
|
||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -12,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;
|
||||||
@ -50,42 +50,41 @@ export const ServicesOverviewSection = () => {
|
|||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<Container>
|
||||||
<Container>
|
<section className='py-16'>
|
||||||
<div className='mb-12 text-center'>
|
<div className='container mx-auto'>
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
<div className='mb-12 text-center'>
|
||||||
{t('clients.services.title')}
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
</h2>
|
{t('clients.services.title')}
|
||||||
<p className='mx-auto max-w-2xl text-gray-600'>
|
</h2>
|
||||||
{t('clients.services.subtitle')}
|
<p className='mx-auto max-w-2xl text-gray-600'>
|
||||||
</p>
|
{t('clients.services.subtitle')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 }) => {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={title}
|
||||||
|
className='overflow-hidden transition-all hover:shadow-lg'
|
||||||
|
>
|
||||||
|
<CardHeader className='pb-3'>
|
||||||
|
<div className='mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-red-100'>
|
||||||
|
<Icon className='h-6 w-6 text-red-600' />
|
||||||
|
</div>
|
||||||
|
<CardTitle>{title}</CardTitle>
|
||||||
|
<CardDescription>{description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='text-sm text-gray-600'>
|
||||||
|
<p>{contentText}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</section>
|
||||||
className='grid gap-3 md:grid-cols-2 md:gap-6 lg:grid-cols-3'
|
</Container>
|
||||||
>
|
|
||||||
{servicesOverview.map(({ description, Icon, contentText, title }) => {
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
data-aos='flip-left'
|
|
||||||
data-aos-duration='600'
|
|
||||||
key={title}
|
|
||||||
className='overflow-hidden transition-all hover:shadow-lg'
|
|
||||||
>
|
|
||||||
<CardHeader className='pb-3'>
|
|
||||||
<div className='mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-red-100'>
|
|
||||||
<Icon className='h-6 w-6 text-red-600' />
|
|
||||||
</div>
|
|
||||||
<CardTitle>{title}</CardTitle>
|
|
||||||
<CardDescription>{description}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className='text-sm text-gray-600'>
|
|
||||||
<p>{contentText}</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
</section>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,24 +1,30 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
import Link from 'next/link';
|
||||||
|
|
||||||
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';
|
||||||
|
|
||||||
export const CtaSection = () => {
|
export const CtaSection = () => {
|
||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className='bg-red-600 text-white'>
|
<section className='bg-red-600 px-2 py-8 text-white sm:py-16'>
|
||||||
<Container>
|
<div className='container mx-auto'>
|
||||||
<div className='flex flex-col items-center text-center'>
|
<div className='flex flex-col items-center text-center'>
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
{t('home.cta.title')}
|
{t('home.cta.title')}
|
||||||
</h2>
|
</h2>
|
||||||
<p className='mb-8 max-w-2xl'>{t('home.cta.description')}</p>
|
<p className='mb-8 max-w-2xl'>{t('home.cta.description')}</p>
|
||||||
<h4 className='text-xl font-bold'>
|
<div className='flex flex-col gap-4 sm:flex-row'>
|
||||||
{t('common.buttons.purchaseCardAtGasStations')}
|
<Link href='#'>
|
||||||
</h4>
|
<Button variant='secondary'>
|
||||||
|
{t('common.buttons.purchaseCardAtGasStations')}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,8 +4,6 @@ import { Fuel, Mail, MapPin, Phone } from 'lucide-react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { AppStoreButtons } from '@/shared/components/app-store-buttons';
|
|
||||||
import { Logo } from '@/shared/assets/logo';
|
|
||||||
|
|
||||||
export const Footer = () => {
|
export const Footer = () => {
|
||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
@ -14,14 +12,15 @@ export const Footer = () => {
|
|||||||
<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:gap-4 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'>
|
<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'>
|
||||||
<Logo/>
|
<Fuel className='h-6 w-6 text-red-500' />
|
||||||
|
<span className='text-xl font-bold'>{t('common.name')}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className='mb-4 text-gray-400'>{t('home.hero.description')}</p>
|
<p className='mb-4 text-gray-400'>{t('home.hero.description')}</p>
|
||||||
<div className='mb-6 flex space-x-4'>
|
<div className='flex space-x-4'>
|
||||||
<a href={t('social.facebook')} target='_blank' className='text-gray-400 hover:text-white'>
|
<a href='#' className='text-gray-400 hover:text-white'>
|
||||||
<svg
|
<svg
|
||||||
className='h-6 w-6'
|
className='h-6 w-6'
|
||||||
fill='currentColor'
|
fill='currentColor'
|
||||||
@ -35,7 +34,7 @@ export const Footer = () => {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a href={t('social.instagram')} target='_blank' className='text-gray-400 hover:text-white'>
|
<a href='#' className='text-gray-400 hover:text-white'>
|
||||||
<svg
|
<svg
|
||||||
className='h-6 w-6'
|
className='h-6 w-6'
|
||||||
fill='currentColor'
|
fill='currentColor'
|
||||||
@ -49,11 +48,20 @@ export const Footer = () => {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
<a href='#' className='text-gray-400 hover:text-white'>
|
||||||
|
<svg
|
||||||
|
className='h-6 w-6'
|
||||||
|
fill='currentColor'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
aria-hidden='true'
|
||||||
|
>
|
||||||
|
<path d='M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84' />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<AppStoreButtons className='mt-4' />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex md:justify-center'>
|
<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')}
|
||||||
@ -74,7 +82,7 @@ export const Footer = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex md:justify-center'>
|
<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')}
|
||||||
@ -92,7 +100,7 @@ export const Footer = () => {
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
href='/clients'
|
href='/clients/loyalty'
|
||||||
className='text-gray-400 hover:text-white'
|
className='text-gray-400 hover:text-white'
|
||||||
>
|
>
|
||||||
{t('common.navigation.clients')}
|
{t('common.navigation.clients')}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ export function MobileNav() {
|
|||||||
|
|
||||||
<Collapsible open={clientsOpen} onOpenChange={setClientsOpen}>
|
<Collapsible open={clientsOpen} onOpenChange={setClientsOpen}>
|
||||||
<CollapsibleTrigger className='data-[state=open]:animate-collapsible-down data-[state=closed]:animate-collapsible-up flex w-full items-center justify-between text-lg font-medium transition-all hover:text-red-600'>
|
<CollapsibleTrigger className='data-[state=open]:animate-collapsible-down data-[state=closed]:animate-collapsible-up flex w-full items-center justify-between text-lg font-medium transition-all hover:text-red-600'>
|
||||||
<Link href='/clients' onClick={() => setOpen(false)}><span>{t('common.navigation.clients')}</span></Link>
|
<span>{t('common.navigation.clients')}</span>
|
||||||
{clientsOpen ? (
|
{clientsOpen ? (
|
||||||
<ChevronDown className='h-5 w-5' />
|
<ChevronDown className='h-5 w-5' />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -3,73 +3,42 @@
|
|||||||
import { MapPin } from 'lucide-react';
|
import { MapPin } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
import { useMediaController } from '@/shared/media/hooks/use-media-controller';
|
|
||||||
import { AppStoreButtons } from '@/shared/components/app-store-buttons';
|
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
|
|
||||||
export const HeroSection = () => {
|
export const HeroSection = () => {
|
||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
const { m } = useMediaController();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const iframe = document.getElementById('xtraseats-iframe');
|
|
||||||
|
|
||||||
iframe?.addEventListener('load', () => {
|
|
||||||
window.addEventListener('message', function (event) {
|
|
||||||
// Optional: validate event.origin === "https://app.realpromo.io"
|
|
||||||
if (iframe && event.data.iframeHeight) {
|
|
||||||
if (iframe) {
|
|
||||||
iframe.style.height = event.data.iframeHeight + 10 + 'px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className='relative'>
|
<section className='relative'>
|
||||||
<div className='relative h-[550px] w-full overflow-hidden bg-black sm:h-[500px] xl:h-[650px]'>
|
<div className='relative h-[400px] w-full overflow-hidden sm:h-[500px] xl:h-[700px]'>
|
||||||
<div
|
<Image
|
||||||
style={{
|
src='/oriyo_bg.jpeg'
|
||||||
width: '100%',
|
alt='Gas Station Network'
|
||||||
height: '50%',
|
fill
|
||||||
position: 'absolute',
|
className='object-cover'
|
||||||
// top: -60,
|
priority
|
||||||
// right: 20,
|
/>
|
||||||
}}
|
<div className='absolute inset-0 flex items-center bg-gradient-to-r from-black/70 to-black/30 px-2'>
|
||||||
className='sm:!-top-16 sm:-right-40 sm:!h-[70vh] sm:!w-[100vh] md:!-top-10 md:!-right-30 xl:!top-0 xl:!right-0'
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={m('home.hero-section.banner') || '/oriyo_bg.jpeg'}
|
|
||||||
alt='Oriyo Station'
|
|
||||||
fill
|
|
||||||
className='object-cover sm:scale-110 md:scale-120 xl:scale-140'
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='absolute inset-0 flex items-end bg-gradient-to-r from-black/70 to-black/30 px-2 pb-12 sm:items-center sm:pb-0'>
|
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
<div className='max-w-lg space-y-4 text-white'>
|
<div className='max-w-lg space-y-4 text-white'>
|
||||||
<div className='animate-fade animate-duration-[3000ms] animate-ease-in-out'>
|
<div className='animate-fade animate-duration-[3000ms] animate-ease-in-out'>
|
||||||
<h1 className='text-5xl font-bold tracking-tight md:text-6xl'>
|
<h1 className='font-bold tracking-tight text-4xl md:text-6xl'>
|
||||||
{t('home.hero.title')}
|
{t('home.hero.title')}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<p className='text-gray-200 sm:text-lg'>
|
<p className='text-gray-200 sm:text-lg'>
|
||||||
{t('home.hero.description')}
|
{t('home.hero.description')}
|
||||||
</p>
|
</p>
|
||||||
<div className='mt-6 flex gap-2 sm:flex-row sm:gap-4'>
|
<div className='flex flex-col mt-6 gap-2 sm:flex-row sm:gap-4'>
|
||||||
<Link href='#stations'>
|
<Link href='#'>
|
||||||
<Button className='bg-red-600 hover:bg-red-700'>
|
<Button className='bg-red-600 hover:bg-red-700'>
|
||||||
{t('common.buttons.findStation')}{' '}
|
{t('common.buttons.findStation')}{' '}
|
||||||
<MapPin className='ml-2 h-4 w-4' />
|
<MapPin className='ml-2 h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href='about'>
|
<Link href='#'>
|
||||||
<Button variant='outline' className='animate-pulse'>
|
<Button variant='outline' className='animate-pulse'>
|
||||||
{t('common.buttons.learnMore')}
|
{t('common.buttons.learnMore')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import { Stations } from '@/app/api-utlities/@types';
|
|||||||
|
|
||||||
import { GasStationMap } from '@/features/map';
|
import { GasStationMap } from '@/features/map';
|
||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
|
||||||
interface MapSectionProps {
|
interface MapSectionProps {
|
||||||
@ -17,8 +16,8 @@ export const MapSection = ({ stations }: MapSectionProps) => {
|
|||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id='stations' className='bg-gray-50'>
|
<section id='stations' className='bg-gray-50 px-2 py-8 sm:py-16'>
|
||||||
<Container>
|
<div className='container mx-auto'>
|
||||||
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
<MapPin className='h-6 w-6 text-red-600' />
|
<MapPin className='h-6 w-6 text-red-600' />
|
||||||
@ -36,7 +35,7 @@ export const MapSection = ({ stations }: MapSectionProps) => {
|
|||||||
>
|
>
|
||||||
<GasStationMap stations={stations} />
|
<GasStationMap stations={stations} />
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,118 +0,0 @@
|
|||||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { cn } from '@/shared/lib/utils';
|
|
||||||
import { ButtonProps, buttonVariants } from '@/shared/shadcn-ui/button';
|
|
||||||
|
|
||||||
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
|
|
||||||
<nav
|
|
||||||
role='navigation'
|
|
||||||
aria-label='pagination'
|
|
||||||
className={cn('mx-auto flex w-full justify-center', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
Pagination.displayName = 'Pagination';
|
|
||||||
|
|
||||||
const PaginationContent = React.forwardRef<
|
|
||||||
HTMLUListElement,
|
|
||||||
React.ComponentProps<'ul'>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ul
|
|
||||||
ref={ref}
|
|
||||||
className={cn('flex flex-row items-center gap-1', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
PaginationContent.displayName = 'PaginationContent';
|
|
||||||
|
|
||||||
const PaginationItem = React.forwardRef<
|
|
||||||
HTMLLIElement,
|
|
||||||
React.ComponentProps<'li'>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<li ref={ref} className={cn('', className)} {...props} />
|
|
||||||
));
|
|
||||||
PaginationItem.displayName = 'PaginationItem';
|
|
||||||
|
|
||||||
type PaginationLinkProps = {
|
|
||||||
isActive?: boolean;
|
|
||||||
} & Pick<ButtonProps, 'size'> &
|
|
||||||
React.ComponentProps<'a'>;
|
|
||||||
|
|
||||||
const PaginationLink = ({
|
|
||||||
className,
|
|
||||||
isActive,
|
|
||||||
size = 'icon',
|
|
||||||
...props
|
|
||||||
}: PaginationLinkProps) => (
|
|
||||||
<a
|
|
||||||
aria-current={isActive ? 'page' : undefined}
|
|
||||||
className={cn(
|
|
||||||
buttonVariants({
|
|
||||||
variant: isActive ? 'outline' : 'ghost',
|
|
||||||
size,
|
|
||||||
}),
|
|
||||||
isActive && 'bg-accent pointer-events-none',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
PaginationLink.displayName = 'PaginationLink';
|
|
||||||
|
|
||||||
const PaginationPrevious = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
|
||||||
<PaginationLink
|
|
||||||
aria-label='Go to previous page'
|
|
||||||
size='default'
|
|
||||||
className={cn('gap-1 pl-2.5', className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronLeft className='h-4 w-4' />
|
|
||||||
<span>Предыдущая</span>
|
|
||||||
</PaginationLink>
|
|
||||||
);
|
|
||||||
PaginationPrevious.displayName = 'PaginationPrevious';
|
|
||||||
|
|
||||||
const PaginationNext = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
|
||||||
<PaginationLink
|
|
||||||
aria-label='Go to next page'
|
|
||||||
size='default'
|
|
||||||
className={cn('gap-1 pr-2.5', className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span>Следующая</span>
|
|
||||||
<ChevronRight className='h-4 w-4' />
|
|
||||||
</PaginationLink>
|
|
||||||
);
|
|
||||||
PaginationNext.displayName = 'PaginationNext';
|
|
||||||
|
|
||||||
const PaginationEllipsis = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'span'>) => (
|
|
||||||
<span
|
|
||||||
aria-hidden
|
|
||||||
className={cn('flex h-9 w-9 items-center justify-center', className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<MoreHorizontal className='h-4 w-4' />
|
|
||||||
<span className='sr-only'>More pages</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
PaginationEllipsis.displayName = 'PaginationEllipsis';
|
|
||||||
|
|
||||||
export {
|
|
||||||
Pagination,
|
|
||||||
PaginationContent,
|
|
||||||
PaginationEllipsis,
|
|
||||||
PaginationItem,
|
|
||||||
PaginationLink,
|
|
||||||
PaginationNext,
|
|
||||||
PaginationPrevious,
|
|
||||||
};
|
|
||||||
@ -1,16 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Handshake } from 'lucide-react';
|
import { Handshake } from 'lucide-react';
|
||||||
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { Partners } from '@/app/api-utlities/@types';
|
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';
|
||||||
|
|
||||||
import { PartnersSlider } from './partners-slider';
|
|
||||||
|
|
||||||
interface PartnersSectionProps {
|
interface PartnersSectionProps {
|
||||||
partners: Partners;
|
partners: Partners;
|
||||||
}
|
}
|
||||||
@ -18,19 +16,9 @@ interface PartnersSectionProps {
|
|||||||
export const PartnersSection = ({ partners }: PartnersSectionProps) => {
|
export const PartnersSection = ({ partners }: PartnersSectionProps) => {
|
||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
|
|
||||||
const [partnersFirstHalf, partnersSecondHalf] = useMemo(() => {
|
|
||||||
const length = partners.length;
|
|
||||||
const midPoint = Math.floor(length / 2);
|
|
||||||
|
|
||||||
const partnersFirstHalf = partners.slice(0, midPoint);
|
|
||||||
const partnersSecondHalf = partners.slice(midPoint);
|
|
||||||
|
|
||||||
return [partnersFirstHalf, partnersSecondHalf];
|
|
||||||
}, [partners]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id='partners' className='bg-gray-50 py-8 sm:py-16'>
|
<section id='partners' className='bg-gray-50 px-2 py-8 sm:py-16'>
|
||||||
<div className='mx-auto'>
|
<div className='container mx-auto'>
|
||||||
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
<Handshake className='h-6 w-6 text-red-600' />
|
<Handshake className='h-6 w-6 text-red-600' />
|
||||||
@ -43,17 +31,27 @@ export const PartnersSection = ({ partners }: PartnersSectionProps) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PartnersSlider
|
<div className='grid grid-cols-2 gap-4 sm:gap-8 md:grid-cols-4'>
|
||||||
partners={partnersFirstHalf}
|
{partners.map(({ id, name, poster }) => (
|
||||||
autoplaySpeed={3000}
|
<div
|
||||||
className='sm:mb-12'
|
key={id}
|
||||||
/>
|
className='flex h-32 flex-col items-center justify-center gap-0.5 rounded-lg bg-white p-6 shadow-md transition-transform hover:scale-105'
|
||||||
|
data-aos='flip-left'
|
||||||
<PartnersSlider
|
>
|
||||||
partners={partnersSecondHalf}
|
<Image
|
||||||
autoplaySpeed={4000}
|
src={
|
||||||
direction='rtl'
|
poster ??
|
||||||
/>
|
`/placeholder.svg?height=80&width=160&text=Partner ${id}`
|
||||||
|
}
|
||||||
|
alt={`Partner ${id}`}
|
||||||
|
width={160}
|
||||||
|
height={80}
|
||||||
|
className='max-h-16 w-auto'
|
||||||
|
/>
|
||||||
|
<h4 className='font-extralight'>{name}</h4>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='mt-12 text-center'>
|
<div className='mt-12 text-center'>
|
||||||
<h3 className='mb-4 text-xl font-bold'>
|
<h3 className='mb-4 text-xl font-bold'>
|
||||||
@ -62,11 +60,7 @@ export const PartnersSection = ({ partners }: PartnersSectionProps) => {
|
|||||||
<p className='mx-auto mb-6 max-w-2xl text-gray-600'>
|
<p className='mx-auto mb-6 max-w-2xl text-gray-600'>
|
||||||
{t('home.partners.becomePartnerText')}
|
{t('home.partners.becomePartnerText')}
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link href='#'>
|
||||||
href={`mailto:${t('common.partners.email')}?subject=${t('home.partners.becomePartnerTextEmailSubject')}`}
|
|
||||||
target='_blank'
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
>
|
|
||||||
<Button className='bg-red-600 hover:bg-red-700'>
|
<Button className='bg-red-600 hover:bg-red-700'>
|
||||||
{t('common.buttons.contactUs')}
|
{t('common.buttons.contactUs')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,75 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
// Import Swiper styles
|
|
||||||
import 'swiper/css';
|
|
||||||
import { Autoplay, FreeMode } from 'swiper/modules';
|
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
|
||||||
|
|
||||||
import { Partners } from '@/app/api-utlities/@types';
|
|
||||||
|
|
||||||
interface PartnersSliderProps {
|
|
||||||
partners: Partners;
|
|
||||||
autoplaySpeed?: number;
|
|
||||||
className?: string;
|
|
||||||
direction?: 'ltr' | 'rtl';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PartnersSlider({
|
|
||||||
partners,
|
|
||||||
autoplaySpeed = 3000,
|
|
||||||
className = '',
|
|
||||||
direction = 'ltr',
|
|
||||||
}: PartnersSliderProps) {
|
|
||||||
return (
|
|
||||||
<div className={`w-full ${className}`}>
|
|
||||||
<Swiper
|
|
||||||
modules={[Autoplay, FreeMode]} // Register Swiper modules
|
|
||||||
slidesPerView='auto'
|
|
||||||
spaceBetween={30}
|
|
||||||
loop={true}
|
|
||||||
freeMode={true}
|
|
||||||
speed={autoplaySpeed}
|
|
||||||
grabCursor={true}
|
|
||||||
autoplay={{
|
|
||||||
delay: 1000,
|
|
||||||
disableOnInteraction: false,
|
|
||||||
pauseOnMouseEnter: false,
|
|
||||||
reverseDirection: direction === 'rtl',
|
|
||||||
}}
|
|
||||||
allowTouchMove={true}
|
|
||||||
breakpoints={{
|
|
||||||
320: {
|
|
||||||
slidesPerView: 4,
|
|
||||||
spaceBetween: 5,
|
|
||||||
},
|
|
||||||
640: {
|
|
||||||
slidesPerView: 4,
|
|
||||||
spaceBetween: 20,
|
|
||||||
},
|
|
||||||
768: {
|
|
||||||
slidesPerView: 4,
|
|
||||||
spaceBetween: 30,
|
|
||||||
},
|
|
||||||
1024: {
|
|
||||||
slidesPerView: 5,
|
|
||||||
spaceBetween: 30,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{partners.map((partner) => (
|
|
||||||
<SwiperSlide key={partner.id}>
|
|
||||||
<div className='flex h-32 items-center justify-center px-4'>
|
|
||||||
<Image
|
|
||||||
src={partner.poster || '/placeholder.svg'}
|
|
||||||
alt={partner.name}
|
|
||||||
fill
|
|
||||||
className='w-auto scale-90 object-contain transition-transform hover:scale-105'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
))}
|
|
||||||
</Swiper>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -4,7 +4,6 @@ import { Gift } from 'lucide-react';
|
|||||||
|
|
||||||
import { Discounts } from '@/app/api-utlities/@types/index';
|
import { Discounts } from '@/app/api-utlities/@types/index';
|
||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
|
||||||
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';
|
||||||
|
|
||||||
@ -16,8 +15,8 @@ export const PromotionsSection = ({ discounts }: PromotionsSectionProps) => {
|
|||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id='promotions' className='bg-gray-50'>
|
<section id='promotions' className='bg-gray-50 px-2 py-8 sm:py-16'>
|
||||||
<Container>
|
<div className='container mx-auto'>
|
||||||
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
<div className='mb-12 flex flex-col items-center justify-center text-center'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
<Gift className='h-6 w-6 text-red-600' />
|
<Gift className='h-6 w-6 text-red-600' />
|
||||||
@ -30,7 +29,7 @@ export const PromotionsSection = ({ discounts }: PromotionsSectionProps) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<PromotionSlider discounts={discounts} />
|
<PromotionSlider discounts={discounts} />
|
||||||
</Container>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,65 +1,73 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useIntersectionObserver } from '@/shared/hooks/use-intersection-observer';
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
|
||||||
import AnimatedCounter from '../shared/components/animated-counter';
|
import AnimatedCounter from '../shared/components/animated-counter';
|
||||||
|
|
||||||
const stats = [
|
|
||||||
{
|
|
||||||
key: 'stations',
|
|
||||||
value: 'home.stats.stations.value',
|
|
||||||
suffix: 'home.stats.stations.suffix',
|
|
||||||
label: 'home.stats.stations',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'daily',
|
|
||||||
value: 'home.stats.daily.value',
|
|
||||||
suffix: 'home.stats.daily.suffix',
|
|
||||||
label: 'home.stats.daily',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'years',
|
|
||||||
value: 'home.stats.years.value',
|
|
||||||
suffix: '',
|
|
||||||
label: 'home.stats.years',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'mode',
|
|
||||||
value: 'home.stats.mode.value',
|
|
||||||
suffix: 'home.stats.mode.suffix',
|
|
||||||
label: 'home.stats.mode',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function StatsSection() {
|
export function StatsSection() {
|
||||||
const { t } = useTextController();
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const { sectionRef, isVisible } = useIntersectionObserver<HTMLDivElement>();
|
const sectionRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const toNumber = (value: string) => Number(t(value));
|
const { t } = useTextController();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const [entry] = entries;
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setIsVisible(true);
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
threshold: 0.1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sectionRef.current) {
|
||||||
|
observer.observe(sectionRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section ref={sectionRef} className='bg-red-600 text-white'>
|
<section
|
||||||
<Container>
|
ref={sectionRef}
|
||||||
|
className='bg-red-600 px-2 py-6 text-white sm:py-12'
|
||||||
|
>
|
||||||
|
<div className='container mx-auto'>
|
||||||
<div className='grid grid-cols-2 gap-4 text-center sm:gap-8 md:grid-cols-4'>
|
<div className='grid grid-cols-2 gap-4 text-center sm:gap-8 md:grid-cols-4'>
|
||||||
{stats.map(({ key, value, suffix, label }) => (
|
<div className='space-y-2'>
|
||||||
<div key={key} className='space-y-2'>
|
<h3 className='text-3xl font-bold'>
|
||||||
<h3 className='text-3xl font-bold'>
|
{isVisible ? <AnimatedCounter end={25} suffix='+' /> : '0+'}
|
||||||
{isVisible ? (
|
</h3>
|
||||||
<AnimatedCounter
|
<p className='text-sm text-white/80'>{t('home.stats.stations')}</p>
|
||||||
end={toNumber(value)}
|
</div>
|
||||||
suffix={t(suffix) || undefined}
|
<div className='space-y-2'>
|
||||||
/>
|
<h3 className='text-3xl font-bold'>
|
||||||
) : (
|
{isVisible ? <AnimatedCounter end={10000} suffix='+' /> : '0+'}
|
||||||
`0${t(suffix) || ''}`
|
</h3>
|
||||||
)}
|
<p className='text-sm text-white/80'>{t('home.stats.daily')}</p>
|
||||||
</h3>
|
</div>
|
||||||
<p className='text-sm text-white/80'>{t(label)}</p>
|
<div className='space-y-2'>
|
||||||
</div>
|
<h3 className='text-3xl font-bold'>
|
||||||
))}
|
{isVisible ? <AnimatedCounter end={15} /> : '0'}
|
||||||
|
</h3>
|
||||||
|
<p className='text-sm text-white/80'>{t('home.stats.years')}</p>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<h3 className='text-3xl font-bold'>
|
||||||
|
{isVisible ? <AnimatedCounter end={24} suffix='/7' /> : '0/7'}
|
||||||
|
</h3>
|
||||||
|
<p className='text-sm text-white/80'>{t('home.stats.mode')}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
184
src/widgets/transactions-table.tsx
Normal file
184
src/widgets/transactions-table.tsx
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { format, subMonths } from 'date-fns';
|
||||||
|
import { ru } from 'date-fns/locale';
|
||||||
|
import { CalendarIcon } from 'lucide-react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useFetchBonusTransactionsQuery } from '@/entities/bonus/api/bonus.api';
|
||||||
|
|
||||||
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
|
import { Calendar } from '@/shared/shadcn-ui/calendar';
|
||||||
|
import { Label } from '@/shared/shadcn-ui/label';
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@/shared/shadcn-ui/popover';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/shared/shadcn-ui/table';
|
||||||
|
|
||||||
|
export const TransactionsTable = () => {
|
||||||
|
const [startDate, setStartDate] = useState<Date>(subMonths(new Date(), 1));
|
||||||
|
const [endDate, setEndDate] = useState<Date>(new Date());
|
||||||
|
|
||||||
|
const { data, refetch } = useFetchBonusTransactionsQuery({
|
||||||
|
limit: 100,
|
||||||
|
page: 1,
|
||||||
|
start_date: format(startDate, 'yyyy-MM-dd'),
|
||||||
|
end_date: format(endDate, 'yyyy-MM-dd'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter transactions by date range
|
||||||
|
const filterTransactions = () => {
|
||||||
|
if (!startDate || !endDate) return;
|
||||||
|
|
||||||
|
refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
const { t } = useTextController();
|
||||||
|
|
||||||
|
useEffect(() => {}, [startDate, endDate]);
|
||||||
|
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='space-y-6'>
|
||||||
|
<div className='flex flex-col items-start justify-between gap-4 md:flex-row md:items-center'>
|
||||||
|
<h2 className='text-2xl font-bold'>
|
||||||
|
{t('corporate.transactions.title')}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className='flex w-full flex-col gap-4 md:w-auto md:flex-row'>
|
||||||
|
<div className='grid sm:grid-cols-2 gap-2'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<Label htmlFor='start-date'>
|
||||||
|
{t('corporate.transactions.dateFrom')}
|
||||||
|
</Label>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
className='w-full justify-start text-left font-normal'
|
||||||
|
>
|
||||||
|
<CalendarIcon className='mr-2 h-4 w-4' />
|
||||||
|
{startDate
|
||||||
|
? format(startDate, 'PP', { locale: ru })
|
||||||
|
: 'Выберите дату'}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className='w-auto p-0'>
|
||||||
|
<Calendar
|
||||||
|
mode='single'
|
||||||
|
selected={startDate}
|
||||||
|
onSelect={setStartDate}
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<Label htmlFor='end-date'>
|
||||||
|
{t('corporate.transactions.dateTo')}
|
||||||
|
</Label>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
className='w-full justify-start text-left font-normal'
|
||||||
|
>
|
||||||
|
<CalendarIcon className='mr-2 h-4 w-4' />
|
||||||
|
{endDate
|
||||||
|
? format(endDate, 'PP', { locale: ru })
|
||||||
|
: t('corporate.transactions.selectDate')}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className='w-auto p-0'>
|
||||||
|
<Calendar
|
||||||
|
mode='single'
|
||||||
|
selected={endDate}
|
||||||
|
onSelect={setEndDate}
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className='mt-auto bg-red-600 hover:bg-red-700'
|
||||||
|
onClick={filterTransactions}
|
||||||
|
>
|
||||||
|
{t('corporate.transactions.applyButton')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='rounded-md border'>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>
|
||||||
|
{t('corporate.transactions.tableHeaders.date')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>
|
||||||
|
{t('corporate.transactions.tableHeaders.station')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>
|
||||||
|
{t('corporate.transactions.tableHeaders.product')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className='text-right'>
|
||||||
|
{t('corporate.transactions.tableHeaders.quantity')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className='text-right'>
|
||||||
|
{t('corporate.transactions.tableHeaders.price')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className='text-right'>
|
||||||
|
{t('corporate.transactions.tableHeaders.total')}
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.transactions.length > 0 ? (
|
||||||
|
data.transactions.map((transaction) => (
|
||||||
|
<TableRow key={transaction.id}>
|
||||||
|
<TableCell>
|
||||||
|
{format(new Date(transaction.date_create), 'dd.MM.yyyy')}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{transaction.station}</TableCell>
|
||||||
|
<TableCell>{transaction.product_name}</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
{transaction.price_real}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
{transaction.amount} {t('corporate.currency')}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right font-medium'>
|
||||||
|
{transaction.sum_real} {t('corporate.currency')}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={6}
|
||||||
|
className='py-6 text-center text-gray-500'
|
||||||
|
>
|
||||||
|
{t('corporate.transactions.noTransactions')}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user