Compare commits
No commits in common. "dev" and "feat-get-media" have entirely different histories.
dev
...
feat-get-m
16
Dockerfile
16
Dockerfile
@ -1,4 +1,4 @@
|
|||||||
FROM node:20-alpine AS builder
|
FROM node:18-alpine AS builder
|
||||||
|
|
||||||
# Enable corepack and install pnpm
|
# Enable corepack and install pnpm
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
@ -7,20 +7,12 @@ RUN corepack enable && corepack prepare pnpm@latest --activate
|
|||||||
ENV CI=true
|
ENV CI=true
|
||||||
|
|
||||||
WORKDIR /app
|
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 . .
|
COPY . .
|
||||||
|
|
||||||
# Build the application
|
# Install dependencies and build
|
||||||
RUN pnpm build
|
RUN pnpm install && pnpm build
|
||||||
|
|
||||||
FROM node:20-alpine AS runner
|
FROM node:18-alpine AS runner
|
||||||
|
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
|
||||||
|
|||||||
@ -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/**',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
23
package.json
23
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",
|
||||||
@ -25,25 +25,23 @@
|
|||||||
"@radix-ui/react-toast": "^1.2.11",
|
"@radix-ui/react-toast": "^1.2.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.6",
|
"@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",
|
||||||
@ -55,6 +53,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 +63,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 +79,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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2380
pnpm-lock.yaml
generated
2380
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,15 +1,20 @@
|
|||||||
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';
|
||||||
|
|
||||||
|
import { makeStore } from '@/shared/store';
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'О нас',
|
title: "О нас",
|
||||||
description:
|
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;
|
||||||
|
|
||||||
|
|||||||
@ -22,90 +22,90 @@ 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;
|
_avtomojkaCopy: boolean;
|
||||||
z100: boolean;
|
_ai92Copy: boolean;
|
||||||
propan: boolean;
|
_ai95Copy: boolean;
|
||||||
zaryadnayaStanciya: boolean;
|
_z100Copy: boolean;
|
||||||
dt: boolean;
|
_propanCopy: boolean;
|
||||||
miniMarket: boolean;
|
_zaryadnayaStanci: boolean;
|
||||||
tualet: boolean;
|
_miniMarketCop: 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<{
|
export type MediaResponse = Root<{
|
||||||
mestopolozheniya: string;
|
_name: string;
|
||||||
foto: Image[];
|
_foto: Image[];
|
||||||
klyuchNeIzmenyat: string;
|
_klyuchNeIzmenya: 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<{
|
export type Charity = Root<{
|
||||||
zagolovok: string;
|
_name: string;
|
||||||
opisanie: string;
|
_opisanie: string;
|
||||||
data: string;
|
_data: string;
|
||||||
lokaciya: string;
|
_lokaciya: string;
|
||||||
foto: Image[];
|
_foto: Image[];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type Certificate = Root<{
|
export type Certificate = Root<{
|
||||||
nazvanie: string;
|
_name: string;
|
||||||
opisanie: string;
|
_opisanie: string;
|
||||||
dataVydachi: string;
|
_dataVydachi: string;
|
||||||
dejstvitelenDo: string;
|
_dejstvitelenDo: string;
|
||||||
foto: Image[];
|
_foto: Image[];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type TeamMembers = ReturnType<typeof presentTeamMembers>;
|
export type TeamMembers = ReturnType<typeof presentTeamMembers>;
|
||||||
|
|||||||
@ -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 +0,0 @@
|
|||||||
export class AuthorizationError extends Error {}
|
|
||||||
@ -25,102 +25,102 @@ 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,
|
dt: station._avtomojkaCopy || false,
|
||||||
z100: station.z100 || false,
|
z100: station._ai95Copy || false,
|
||||||
propan: station.propan || false,
|
propan: station._z100Copy || false,
|
||||||
electricCharge: station.zaryadnayaStanciya || false,
|
electricCharge: station._propanCopy || false,
|
||||||
miniMarket: station.miniMarket || false,
|
miniMarket: station._zaryadnayaStanci || false,
|
||||||
toilet: station.tualet || false,
|
toilet: station._miniMarketCop || 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) => {
|
export const presentMedia = (media: MediaResponse) => {
|
||||||
return media.records.map((record) => ({
|
return media.records.map((record) => ({
|
||||||
key: record.klyuchNeIzmenyat,
|
key: record._klyuchNeIzmenya,
|
||||||
name: record.mestopolozheniya,
|
name: record._name,
|
||||||
photo: presentImage(record.foto),
|
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) =>
|
export const presentCharities = (charities: Charity) =>
|
||||||
charities.records.map((charity, index) => ({
|
charities.records.map((charity, index) => ({
|
||||||
id: index + 1,
|
id: index + 1,
|
||||||
name: charity.zagolovok,
|
name: charity._name,
|
||||||
description: charity.opisanie,
|
description: charity._opisanie,
|
||||||
date: charity.data,
|
date: charity._data,
|
||||||
location: charity.lokaciya,
|
location: charity._lokaciya,
|
||||||
image: presentImage(charity.foto),
|
image: presentImage(charity._foto),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const presentCertificates = (certificates: Certificate) =>
|
export const presentCertificates = (certificates: Certificate) =>
|
||||||
certificates.records.map((certificate, index) => ({
|
certificates.records.map((certificate, index) => ({
|
||||||
id: index + 1,
|
id: index + 1,
|
||||||
name: certificate.nazvanie,
|
name: certificate._name,
|
||||||
description: certificate.opisanie,
|
description: certificate._opisanie,
|
||||||
issuedAt: certificate.dataVydachi,
|
issuedAt: certificate._dataVydachi,
|
||||||
validUntil: certificate.dejstvitelenDo,
|
validUntil: certificate._dejstvitelenDo,
|
||||||
image: presentImage(certificate.foto),
|
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 copy.ts
Normal file
13
src/app/api-utlities/requests/about-us-page.request copy.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import {
|
||||||
|
historyRequest,
|
||||||
|
reviewsRequest,
|
||||||
|
stationsWithImageRequest,
|
||||||
|
teamRequest,
|
||||||
|
} from './common';
|
||||||
|
|
||||||
|
export const aboutUsPageRequest = {
|
||||||
|
...teamRequest,
|
||||||
|
...historyRequest,
|
||||||
|
...stationsWithImageRequest,
|
||||||
|
...reviewsRequest,
|
||||||
|
};
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { certificatesRequest } from './common';
|
||||||
|
|
||||||
|
export const certificatesPageRequest = {
|
||||||
|
...certificatesRequest,
|
||||||
|
};
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { charityRequest } from './common';
|
||||||
|
|
||||||
|
export const charityPageRequest = {
|
||||||
|
...charityRequest,
|
||||||
|
};
|
||||||
@ -1,29 +1,29 @@
|
|||||||
import { EnumType, VariableType } from 'json-to-graphql-query';
|
import { EnumType, VariableType } 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,
|
_avtomojkaCopy: true, // DT
|
||||||
miniMarket: true,
|
_zaryadnayaStanci: true, // miniMarket
|
||||||
tualet: true,
|
_miniMarketCop: true, // toilet
|
||||||
region: {
|
_region: {
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
foto: {
|
_foto: {
|
||||||
url: true,
|
url: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -31,13 +31,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 +46,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 +73,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 +84,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,70 +101,70 @@ 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 = {
|
export const mediaRequest = {
|
||||||
mediaKontentSajta: {
|
_mediaKontentS: {
|
||||||
records: {
|
records: {
|
||||||
mestopolozheniya: true,
|
_name: true,
|
||||||
foto: {
|
_foto: {
|
||||||
id: true,
|
id: true,
|
||||||
url: true,
|
url: true,
|
||||||
},
|
},
|
||||||
klyuchNeIzmenyat: true,
|
_klyuchNeIzmenya: 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: {
|
__args: {
|
||||||
filtersSet: {
|
filtersSet: {
|
||||||
conjunction: new EnumType('and'),
|
conjunction: new EnumType('and'),
|
||||||
filtersSet: [
|
filtersSet: [
|
||||||
{
|
{
|
||||||
field: new EnumType('status'),
|
field: new EnumType('_status'),
|
||||||
operator: 'contains',
|
operator: 'contains',
|
||||||
value: 'Опубликовано',
|
value: 'Опубликовано',
|
||||||
},
|
},
|
||||||
@ -174,21 +173,21 @@ export const reviewsRequest = {
|
|||||||
},
|
},
|
||||||
records: {
|
records: {
|
||||||
id: true,
|
id: true,
|
||||||
polnoeImya: true,
|
_name: true,
|
||||||
otzyv: true,
|
_otzyv: true,
|
||||||
rejting: true,
|
_rejting: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const charityRequest = {
|
export const charityRequest = {
|
||||||
blagotvoritelnyjFond: {
|
_blagotvoriteln: {
|
||||||
records: {
|
records: {
|
||||||
zagolovok: true,
|
_name: true,
|
||||||
opisanie: true,
|
_opisanie: true,
|
||||||
data: true,
|
_data: true,
|
||||||
lokaciya: true,
|
_lokaciya: true,
|
||||||
foto: {
|
_foto: {
|
||||||
url: true,
|
url: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -196,13 +195,13 @@ export const charityRequest = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const certificatesRequest = {
|
export const certificatesRequest = {
|
||||||
sertifikaty: {
|
_sertifikaty: {
|
||||||
records: {
|
records: {
|
||||||
nazvanie: true,
|
_name: true,
|
||||||
opisanie: true,
|
_opisanie: true,
|
||||||
dataVydachi: true,
|
_dataVydachi: true,
|
||||||
dejstvitelenDo: true,
|
_dejstvitelenDo: true,
|
||||||
foto: {
|
_foto: {
|
||||||
url: true,
|
url: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -213,7 +212,7 @@ export const createReviewMutation = {
|
|||||||
__variables: {
|
__variables: {
|
||||||
review: 'TableOtzyvyMutationParameters',
|
review: 'TableOtzyvyMutationParameters',
|
||||||
},
|
},
|
||||||
otzyvy: {
|
_otzyvy: {
|
||||||
createRecord: {
|
createRecord: {
|
||||||
__args: {
|
__args: {
|
||||||
records: [new VariableType('review')],
|
records: [new VariableType('review')],
|
||||||
|
|||||||
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,
|
||||||
|
};
|
||||||
@ -2,7 +2,6 @@ import { RequestCookie } from 'next/dist/compiled/@edge-runtime/cookies';
|
|||||||
import { NextRequest } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
|
|
||||||
import oriyoClient from '@/app/api-utlities/clients/oriyo.client';
|
import oriyoClient from '@/app/api-utlities/clients/oriyo.client';
|
||||||
import { AuthorizationError } from '@/app/api-utlities/errors/authorization.error';
|
|
||||||
|
|
||||||
import { authorizationMiddleware } from '../../middlewares/auth.middleware';
|
import { authorizationMiddleware } from '../../middlewares/auth.middleware';
|
||||||
import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
|
import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
|
||||||
@ -17,10 +16,6 @@ const routeHandler = async (req: NextRequest, requestCookie: RequestCookie) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (oriyoResponse.status === 401) {
|
|
||||||
throw new AuthorizationError();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response(JSON.stringify(oriyoResponse.data), {
|
return new Response(JSON.stringify(oriyoResponse.data), {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,7 +3,6 @@ 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/clients/oriyo.client';
|
||||||
import { AuthorizationError } from '@/app/api-utlities/errors/authorization.error';
|
|
||||||
import { getParams } from '@/app/api-utlities/utilities/get-params';
|
import { getParams } from '@/app/api-utlities/utilities/get-params';
|
||||||
|
|
||||||
import { authorizationMiddleware } from '../../middlewares/auth.middleware';
|
import { authorizationMiddleware } from '../../middlewares/auth.middleware';
|
||||||
@ -35,25 +34,6 @@ const routeHandler = async (req: NextRequest, requestCookie: RequestCookie) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (oriyoResponse.status === 404)
|
|
||||||
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) {
|
|
||||||
throw new AuthorizationError();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oriyoResponse.data.error) throw oriyoResponse.data;
|
if (oriyoResponse.data.error) throw oriyoResponse.data;
|
||||||
|
|
||||||
return new Response(JSON.stringify(oriyoResponse.data), {
|
return new Response(JSON.stringify(oriyoResponse.data), {
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
import { revalidatePath } from 'next/cache';
|
import { revalidateTag } from 'next/cache';
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { FetchTags } from '@/shared/api/tags';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
revalidatePath('/', 'layout');
|
revalidateTag(FetchTags.TAYLOR);
|
||||||
revalidatePath('/', 'page');
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Failed to drop cache', detail: err },
|
{ error: 'Failed to revalidate', detail: err },
|
||||||
{ status: 500 },
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ 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/clients/oriyo.client';
|
||||||
import { AuthorizationError } from '@/app/api-utlities/errors/authorization.error';
|
|
||||||
import { getParams } from '@/app/api-utlities/utilities/get-params';
|
import { getParams } from '@/app/api-utlities/utilities/get-params';
|
||||||
|
|
||||||
import { authorizationMiddleware } from '../../middlewares/auth.middleware';
|
import { authorizationMiddleware } from '../../middlewares/auth.middleware';
|
||||||
@ -35,24 +34,6 @@ const routeHandler = async (req: NextRequest, requestCookie: RequestCookie) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
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;
|
if (oriyoResponse.data.error) throw oriyoResponse.data;
|
||||||
|
|
||||||
return new Response(JSON.stringify(oriyoResponse.data), {
|
return new Response(JSON.stringify(oriyoResponse.data), {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
|
import { has } from 'lodash';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { AuthorizationError } from '@/app/api-utlities/errors/authorization.error';
|
|
||||||
|
|
||||||
export const authorizationMiddleware =
|
export const authorizationMiddleware =
|
||||||
(handler: Function, authorizationTokenKey: string) =>
|
(handler: Function, authorizationTokenKey: string) =>
|
||||||
async (req: NextRequest, ...args: any[]) => {
|
async (req: NextRequest, ...args: any[]) => {
|
||||||
@ -17,7 +16,7 @@ export const authorizationMiddleware =
|
|||||||
try {
|
try {
|
||||||
return await handler(req, requestedToken, ...args);
|
return await handler(req, requestedToken, ...args);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AuthorizationError) {
|
if (has(error, 'code') && error.code === 401) {
|
||||||
const response = NextResponse.json(
|
const response = NextResponse.json(
|
||||||
{ message: 'Authorization session was timed out' },
|
{ message: 'Authorization session was timed out' },
|
||||||
{ status: 401 },
|
{ status: 401 },
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { NextRequest } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { requestTaylor } from '@/app/api-utlities/clients/taylor.client';
|
import { requestTaylor } from '@/app/api-utlities/clients/taylor.client';
|
||||||
import { createReviewMutation } from '@/app/api-utlities/requests/common';
|
import { createReviewMutation } from '@/app/api-utlities/requests/common';
|
||||||
@ -14,9 +15,9 @@ export const POST = async (req: NextRequest) => {
|
|||||||
{ mutation: createReviewMutation },
|
{ mutation: createReviewMutation },
|
||||||
{
|
{
|
||||||
review: {
|
review: {
|
||||||
polnoeImya: validatedRequest.name,
|
_name: validatedRequest.name,
|
||||||
otzyv: validatedRequest.reviewMessage,
|
_otzyv: validatedRequest.reviewMessage,
|
||||||
rejting: validatedRequest.rating,
|
_rejting: validatedRequest.rating,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { CharityPage } from '@/pages-templates/charity';
|
import { CharityPage } from '@/pages-templates/charity';
|
||||||
|
|
||||||
import { fetchCharityPageContent } from '@/features/pages/services/pages.service';
|
import { mainPageApi } from '@/features/pages/api/pages.api';
|
||||||
|
|
||||||
|
import { makeStore } from '@/shared/store';
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Благотворительность',
|
title: 'Благотворительность',
|
||||||
@ -9,9 +11,13 @@ export const metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function Charity() {
|
export default async function Charity() {
|
||||||
const data = await fetchCharityPageContent();
|
const store = makeStore();
|
||||||
|
|
||||||
if (!data) return null;
|
const { data, isLoading, error } = await store.dispatch(
|
||||||
|
mainPageApi.endpoints.fetchCharityPageContent.initiate(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading || !data) return null;
|
||||||
|
|
||||||
return <CharityPage content={data} />;
|
return <CharityPage content={data} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { CertificatesPage } from '@/pages-templates/clients/certificates';
|
import { CertificatesPage } from '@/pages-templates/clients/certificates';
|
||||||
|
|
||||||
import { fetchCertificatesPageContent } from '@/features/pages/services/pages.service';
|
import { mainPageApi } from '@/features/pages/api/pages.api';
|
||||||
|
|
||||||
|
import { makeStore } from '@/shared/store';
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Сертификаты',
|
title: 'Сертификаты',
|
||||||
@ -9,9 +11,13 @@ export const metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function Certificates() {
|
export default async function Certificates() {
|
||||||
const data = await fetchCertificatesPageContent();
|
const store = makeStore();
|
||||||
|
|
||||||
if (!data) return null;
|
const { data, isLoading, error } = await store.dispatch(
|
||||||
|
mainPageApi.endpoints.fetchCertificatesPageContent.initiate(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading || !data) return null;
|
||||||
|
|
||||||
return <CertificatesPage content={data} />;
|
return <CertificatesPage content={data} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { Inter } from 'next/font/google';
|
import { Inter } from 'next/font/google';
|
||||||
|
|
||||||
import {
|
import { textControlApi } from '@/shared/language/api/text-control.api';
|
||||||
fetchMediaContent,
|
import { mediaControlApi } from '@/shared/media/api/media-control.api';
|
||||||
fetchTextContent,
|
|
||||||
} from '@/features/pages/services/pages.service';
|
|
||||||
|
|
||||||
import { Providers } from '@/shared/providers/providers';
|
import { Providers } from '@/shared/providers/providers';
|
||||||
|
import { makeStore } from '@/shared/store';
|
||||||
import { MediaItem } from '@/shared/types/media.type';
|
import { MediaItem } from '@/shared/types/media.type';
|
||||||
import { TextItem } from '@/shared/types/text.types';
|
import { TextItem } from '@/shared/types/text.types';
|
||||||
|
|
||||||
@ -31,11 +29,17 @@ 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(),
|
// Запрос текстов
|
||||||
fetchMediaContent(),
|
const textResponse = await store.dispatch(
|
||||||
]);
|
textControlApi.endpoints.fetchText.initiate(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Запрос медиа
|
||||||
|
const mediaResponse = await store.dispatch(
|
||||||
|
mediaControlApi.endpoints.fetchMedia.initiate(),
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang='ru'
|
lang='ru'
|
||||||
@ -45,8 +49,8 @@ export default async function RootLayout({
|
|||||||
>
|
>
|
||||||
<body className={`${inter.className} min-w-2xs antialiased`}>
|
<body className={`${inter.className} min-w-2xs antialiased`}>
|
||||||
<Providers
|
<Providers
|
||||||
textItems={textItems as TextItem[]}
|
textItems={textResponse.data as TextItem[]}
|
||||||
mediaItems={mediaItems as MediaItem[]}
|
mediaItems={mediaResponse.data as MediaItem[]}
|
||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -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,11 @@
|
|||||||
|
import {
|
||||||
|
TransactionRequest,
|
||||||
|
TransactionResponse,
|
||||||
|
} from '@/entities/transactions/model/types';
|
||||||
|
|
||||||
import { baseAPI } from '@/shared/api/base-api';
|
import { baseAPI } from '@/shared/api/base-api';
|
||||||
|
|
||||||
import { ClientInfo } from '../model/types/bonus-client-info.type';
|
import { ClientInfo } from '../model/types/bonus-client-info.type';
|
||||||
import {
|
|
||||||
BonusTransactionRequest,
|
|
||||||
BonusTransactionResponse,
|
|
||||||
} from '../model/types/bonus-transactions.type';
|
|
||||||
|
|
||||||
export const bonusApi = baseAPI.injectEndpoints({
|
export const bonusApi = baseAPI.injectEndpoints({
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
@ -16,8 +17,8 @@ export const bonusApi = baseAPI.injectEndpoints({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
fetchBonusTransactions: builder.query<
|
fetchBonusTransactions: builder.query<
|
||||||
BonusTransactionResponse,
|
TransactionResponse,
|
||||||
BonusTransactionRequest
|
TransactionRequest
|
||||||
>({
|
>({
|
||||||
query: (request) => {
|
query: (request) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
|
import {
|
||||||
|
TransactionRequest,
|
||||||
|
TransactionResponse,
|
||||||
|
} from '@/entities/transactions/model/types';
|
||||||
|
|
||||||
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) => ({
|
||||||
@ -16,8 +17,8 @@ export const corporateApi = baseAPI.injectEndpoints({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
fetchCorporateTransactions: builder.query<
|
fetchCorporateTransactions: builder.query<
|
||||||
CorporateTransactionResponse,
|
TransactionResponse,
|
||||||
CorporateTransactionRequest
|
TransactionRequest
|
||||||
>({
|
>({
|
||||||
query: (request) => {
|
query: (request) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -1,22 +1,24 @@
|
|||||||
export interface BonusTransactionResponse {
|
export interface TransactionResponse {
|
||||||
transactions: BonusTransaction[];
|
transactions: Transaction[];
|
||||||
|
card_id: string;
|
||||||
current_page: number;
|
current_page: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
total_records: number;
|
total_records: number;
|
||||||
total_pages: number;
|
total_pages: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BonusTransaction {
|
export interface Transaction {
|
||||||
id: number;
|
id: number;
|
||||||
date_create: string;
|
date_create: string;
|
||||||
station?: string;
|
station?: string;
|
||||||
|
station_name?: string;
|
||||||
product_name: string;
|
product_name: string;
|
||||||
amount: string;
|
amount: string;
|
||||||
price_real: string;
|
price_real: string;
|
||||||
sum_real: string;
|
sum_real: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BonusTransactionRequest {
|
export interface TransactionRequest {
|
||||||
start_date?: string;
|
start_date?: string;
|
||||||
end_date?: string;
|
end_date?: string;
|
||||||
page: number;
|
page: number;
|
||||||
@ -5,7 +5,6 @@ import {
|
|||||||
YMaps,
|
YMaps,
|
||||||
} from '@pbe/react-yandex-maps';
|
} from '@pbe/react-yandex-maps';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import { useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
import { Point } from '../model';
|
import { Point } from '../model';
|
||||||
|
|
||||||
@ -22,29 +21,6 @@ export const YandexMap = ({
|
|||||||
selectedStation,
|
selectedStation,
|
||||||
handleMapStationClick,
|
handleMapStationClick,
|
||||||
}: YandexMapProps) => {
|
}: 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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<YMaps
|
<YMaps
|
||||||
query={{
|
query={{
|
||||||
@ -66,9 +42,6 @@ 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;
|
const isSelectedStation = selectedStation === point.id;
|
||||||
|
|||||||
99
src/features/pages/api/pages.api.ts
Normal file
99
src/features/pages/api/pages.api.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { jsonToGraphQLQuery } from 'json-to-graphql-query';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AboutUsPageData,
|
||||||
|
CertificatesPageData,
|
||||||
|
CharityPageData,
|
||||||
|
MainPageData,
|
||||||
|
} from '@/app/api-utlities/@types/pages';
|
||||||
|
import {
|
||||||
|
presentCertificates,
|
||||||
|
presentCharities,
|
||||||
|
presentDiscounts,
|
||||||
|
presentHistoryItems,
|
||||||
|
presentJobs,
|
||||||
|
presentPartners,
|
||||||
|
presentReviews,
|
||||||
|
presentStations,
|
||||||
|
presentTeamMembers,
|
||||||
|
} from '@/app/api-utlities/presenters';
|
||||||
|
import { aboutUsPageRequest } from '@/app/api-utlities/requests/about-us-page.request copy';
|
||||||
|
import { certificatesPageRequest } from '@/app/api-utlities/requests/certificates-page.request';
|
||||||
|
import { charityPageRequest } from '@/app/api-utlities/requests/charity-page.request copy';
|
||||||
|
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.query<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),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
fetchCharityPageContent: builder.query<CharityPageData, void>({
|
||||||
|
query: () => ({
|
||||||
|
url: '',
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
query: jsonToGraphQLQuery({ query: charityPageRequest }),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
transformResponse: (response: any) => {
|
||||||
|
return {
|
||||||
|
charities: presentCharities(response.data._blagotvoriteln),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
fetchCertificatesPageContent: builder.query<CertificatesPageData, void>({
|
||||||
|
query: () => ({
|
||||||
|
url: '',
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
query: jsonToGraphQLQuery({ query: certificatesPageRequest }),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
transformResponse: (response: any) => {
|
||||||
|
return {
|
||||||
|
certificates: presentCertificates(response.data._sertifikaty),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
@ -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,6 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
export function proxy(req: NextRequest) {
|
export function middleware(req: NextRequest) {
|
||||||
const url = req.nextUrl.clone();
|
const url = req.nextUrl.clone();
|
||||||
const path = url.pathname;
|
const path = url.pathname;
|
||||||
|
|
||||||
@ -8,10 +8,9 @@ import {
|
|||||||
useFetchCorporateTransactionsQuery,
|
useFetchCorporateTransactionsQuery,
|
||||||
useFetchMyCorporateInfoQuery,
|
useFetchMyCorporateInfoQuery,
|
||||||
} from '@/entities/corporate/api/corporate.api';
|
} from '@/entities/corporate/api/corporate.api';
|
||||||
import { CorporateTransactionRequest } from '@/entities/corporate/model/types/corporate-transactions.type';
|
import { TransactionRequest } from '@/entities/transactions/model/types';
|
||||||
|
|
||||||
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,26 +19,21 @@ 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';
|
||||||
|
|
||||||
export function CorporateDashboard() {
|
export function CorporateDashboard() {
|
||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
|
|
||||||
const { data } = useFetchMyCorporateInfoQuery({});
|
const { data, isLoading } = useFetchMyCorporateInfoQuery({});
|
||||||
|
|
||||||
const [request, setTransactionFetchRequest] =
|
const [request, setTransactionFetchRequest] = useState<TransactionRequest>({
|
||||||
useState<CorporateTransactionRequest>({
|
limit: 10,
|
||||||
limit: 10,
|
page: 1,
|
||||||
page: 1,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const { data: transactionsResponse, refetch } =
|
||||||
data: transactionsResponse,
|
useFetchCorporateTransactionsQuery(request);
|
||||||
refetch,
|
|
||||||
isFetching,
|
|
||||||
} = useFetchCorporateTransactionsQuery(request);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refetch();
|
refetch();
|
||||||
@ -165,66 +159,12 @@ export function CorporateDashboard() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TransactionsTable
|
{transactionsResponse && (
|
||||||
isLoading={isFetching}
|
<TransactionsTable
|
||||||
renderHeaders={() => (
|
data={transactionsResponse}
|
||||||
<TableRow>
|
onChange={setTransactionFetchRequest}
|
||||||
<TableHead>
|
/>
|
||||||
{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>
|
||||||
|
|||||||
@ -8,11 +8,10 @@ import {
|
|||||||
useFetchBonusTransactionsQuery,
|
useFetchBonusTransactionsQuery,
|
||||||
useFetchMyBonusInfoQuery,
|
useFetchMyBonusInfoQuery,
|
||||||
} from '@/entities/bonus/api/bonus.api';
|
} from '@/entities/bonus/api/bonus.api';
|
||||||
import { BonusTransactionRequest } from '@/entities/bonus/model/types/bonus-transactions.type';
|
import { TransactionRequest } from '@/entities/transactions/model/types';
|
||||||
|
|
||||||
import Loader from '@/shared/components/loader';
|
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,26 +20,21 @@ 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';
|
||||||
|
|
||||||
export function CustomerDashboard() {
|
export function CustomerDashboard() {
|
||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
|
|
||||||
const [request, setTransactionFetchRequest] =
|
const [request, setTransactionFetchRequest] = useState<TransactionRequest>({
|
||||||
useState<BonusTransactionRequest>({
|
limit: 10,
|
||||||
limit: 10,
|
page: 1,
|
||||||
page: 1,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const { data, isLoading } = useFetchMyBonusInfoQuery({});
|
const { data, isLoading } = useFetchMyBonusInfoQuery({});
|
||||||
|
|
||||||
const {
|
const { data: transactionsResponse, refetch } =
|
||||||
data: transactionsResponse,
|
useFetchBonusTransactionsQuery(request);
|
||||||
refetch,
|
|
||||||
isFetching: isTransactionLoading,
|
|
||||||
} = useFetchBonusTransactionsQuery(request);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refetch();
|
refetch();
|
||||||
@ -154,59 +148,12 @@ export function CustomerDashboard() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TransactionsTable
|
{transactionsResponse && (
|
||||||
isLoading={isTransactionLoading}
|
<TransactionsTable
|
||||||
data={
|
data={transactionsResponse}
|
||||||
transactionsResponse || {
|
onChange={setTransactionFetchRequest}
|
||||||
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>
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import AnimatedCounter from '@/shared/components/animated-counter';
|
|||||||
import { Container } from '@/shared/components/container';
|
import { Container } from '@/shared/components/container';
|
||||||
import { Review } from '@/shared/components/review';
|
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';
|
||||||
|
|
||||||
@ -26,7 +25,6 @@ export interface AboutPageProps {
|
|||||||
|
|
||||||
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 +33,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
|
||||||
@ -106,10 +102,7 @@ export default function AboutPage({ content }: AboutPageProps) {
|
|||||||
className='relative h-[500px] overflow-hidden rounded-xl shadow-xl'
|
className='relative h-[500px] overflow-hidden rounded-xl shadow-xl'
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src='/placeholder.svg?height=500&width=600&text=Главный+офис'
|
||||||
m('about.second-section.banner') ||
|
|
||||||
'/placeholder.svg?height=500&width=600&text=Главный+офис'
|
|
||||||
}
|
|
||||||
alt={t('about.overview.imageAlt')}
|
alt={t('about.overview.imageAlt')}
|
||||||
fill
|
fill
|
||||||
className='object-cover'
|
className='object-cover'
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import { CharityPageData } from '@/app/api-utlities/@types/pages';
|
|||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
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 {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@ -30,7 +29,7 @@ export interface CharityPageProps {
|
|||||||
|
|
||||||
export function CharityPage({ content }: CharityPageProps) {
|
export function CharityPage({ content }: CharityPageProps) {
|
||||||
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,14 +37,12 @@ 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=Благотворительный+фонд+Ориё'
|
||||||
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 data-aos='fade-down' data-aos-duration='800'>
|
||||||
@ -107,10 +104,7 @@ export function CharityPage({ content }: CharityPageProps) {
|
|||||||
className='relative h-[500px] overflow-hidden rounded-xl shadow-xl'
|
className='relative h-[500px] overflow-hidden rounded-xl shadow-xl'
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src='/placeholder.svg?height=500&width=600&text=Наша+миссия'
|
||||||
m('charity.second-section.banner') ||
|
|
||||||
'/placeholder.svg?height=500&width=600&text=Наша+миссия'
|
|
||||||
}
|
|
||||||
alt={t('charity.mission.imageAlt')}
|
alt={t('charity.mission.imageAlt')}
|
||||||
fill
|
fill
|
||||||
className='object-cover'
|
className='object-cover'
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import Image from 'next/image';
|
|||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
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';
|
||||||
@ -12,7 +11,6 @@ import { CtaSection } from '@/widgets/cta-section';
|
|||||||
|
|
||||||
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,14 +19,12 @@ 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 className='py-0'>
|
||||||
|
|||||||
@ -16,6 +16,8 @@ export function LoyaltyPage() {
|
|||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
const { m } = useMediaController();
|
const { m } = useMediaController();
|
||||||
|
|
||||||
|
const loyaltyProgrammMedia = m('page.loyaly.banner1');
|
||||||
|
|
||||||
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'>
|
||||||
@ -23,14 +25,12 @@ 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 data-aos='fade-down' data-aos-duration='800'>
|
||||||
@ -111,7 +111,7 @@ export function LoyaltyPage() {
|
|||||||
className='relative h-[400px] overflow-hidden rounded-xl shadow-xl'
|
className='relative h-[400px] overflow-hidden rounded-xl shadow-xl'
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={m('loyalty.second-section.banner') || ProgrammImg}
|
src={loyaltyProgrammMedia?.photo || ProgrammImg}
|
||||||
alt='Программа лояльности'
|
alt='Программа лояльности'
|
||||||
fill
|
fill
|
||||||
className='w-full object-contain p-2.5'
|
className='w-full object-contain p-2.5'
|
||||||
|
|||||||
@ -155,10 +155,7 @@ export default function LoginPage() {
|
|||||||
<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:info@oriyo.tj' className='text-red-600 hover:underline'>
|
||||||
href={`mailto:${t('auth.loginForm.contactUs.mail')}`}
|
|
||||||
className='text-red-600 hover:underline'
|
|
||||||
>
|
|
||||||
{t('auth.contactLink')}
|
{t('auth.contactLink')}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -6,7 +6,6 @@ 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: {
|
next: {
|
||||||
tags: [FetchTags.TAYLOR],
|
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 || '',
|
|
||||||
});
|
|
||||||
@ -7,11 +7,12 @@ import { useMediaController } from '../media/hooks/use-media-controller';
|
|||||||
|
|
||||||
export const Logo = () => {
|
export const Logo = () => {
|
||||||
const { m } = useMediaController();
|
const { m } = useMediaController();
|
||||||
|
const logo = m('logo');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link className='flex items-center gap-2' href={'/'}>
|
<Link className='flex items-center gap-2' href={'/'}>
|
||||||
<Image
|
<Image
|
||||||
src={m('logo') || '/logo-new.png'}
|
src={logo?.photo || '/logo-new.png'}
|
||||||
alt='oriyo-logo'
|
alt='oriyo-logo'
|
||||||
width={110}
|
width={110}
|
||||||
height={40}
|
height={40}
|
||||||
|
|||||||
@ -1,66 +1,77 @@
|
|||||||
'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}
|
ref={sectionRef}
|
||||||
className='my-4 grid grid-cols-1 gap-6 text-center md:my-8'
|
className='my-4 grid grid-cols-1 gap-6 text-center md:my-8'
|
||||||
>
|
>
|
||||||
{stats.map((stat, index) => (
|
<div className='transform rounded-lg bg-white p-3 shadow-md transition-transform hover:scale-105 sm:p-6'>
|
||||||
<div
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
key={index}
|
<Users className='h-6 w-6 text-red-600' />
|
||||||
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,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);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
@ -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);
|
|
||||||
};
|
|
||||||
@ -17,7 +17,7 @@ export const mediaControlApi = taylorAPI.injectEndpoints({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
transformResponse: (response: any) => {
|
transformResponse: (response: any) => {
|
||||||
return presentMedia(response.data.mediaKontentSajta);
|
return presentMedia(response.data._mediaKontentS);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { MediaItem } from '@/shared/types/media.type';
|
|||||||
export type MediaMap = Record<string, MediaItem>;
|
export type MediaMap = Record<string, MediaItem>;
|
||||||
|
|
||||||
type MediaControlContextType = {
|
type MediaControlContextType = {
|
||||||
m: (key: string) => string | null;
|
m: (key: string) => MediaItem | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MediaControlContext = createContext<
|
export const MediaControlContext = createContext<
|
||||||
@ -26,12 +26,12 @@ export function MediaControlProvider({
|
|||||||
return pr;
|
return pr;
|
||||||
}, {} as MediaMap);
|
}, {} as MediaMap);
|
||||||
|
|
||||||
const getMedia = (key: string): string | null => {
|
const getMedia = (key: string): MediaItem | undefined => {
|
||||||
if (mediaMap?.[key]) {
|
if (mediaMap?.[key]) {
|
||||||
return mediaMap[key].photo;
|
return mediaMap[key];
|
||||||
}
|
}
|
||||||
console.warn(`Media key not found: ${key}`);
|
console.warn(`Media key not found: ${key}`);
|
||||||
return null;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -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>;
|
|
||||||
};
|
|
||||||
@ -6,11 +6,9 @@ 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 { 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'>
|
||||||
@ -39,7 +37,7 @@ export const AboutSection = () => {
|
|||||||
data-aos='zoom-in-down'
|
data-aos='zoom-in-down'
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={m('main.price-board') || ''}
|
src='/clients/loyatly/oriyo-price-board.png'
|
||||||
alt='About our company'
|
alt='About our company'
|
||||||
fill
|
fill
|
||||||
className='w-full object-contain p-2.5'
|
className='w-full object-contain p-2.5'
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -13,6 +13,8 @@ export const CharitySection = () => {
|
|||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
const { m } = useMediaController();
|
const { m } = useMediaController();
|
||||||
|
|
||||||
|
const charityMedia = m('home.charity.banner');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id='charity'>
|
<section id='charity'>
|
||||||
<Container>
|
<Container>
|
||||||
@ -23,8 +25,7 @@ export const CharitySection = () => {
|
|||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
m('home.charity.banner') ||
|
charityMedia?.photo || '/placeholder.svg?height=400&width=600'
|
||||||
'/placeholder.svg?height=400&width=600'
|
|
||||||
}
|
}
|
||||||
alt='Charity Foundation'
|
alt='Charity Foundation'
|
||||||
fill
|
fill
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import Image from 'next/image';
|
|||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
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';
|
|
||||||
|
|
||||||
interface Benefit {
|
interface Benefit {
|
||||||
title: string;
|
title: string;
|
||||||
@ -33,7 +32,6 @@ 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'>
|
<section className='bg-gray-50'>
|
||||||
@ -74,10 +72,7 @@ export const BenefitsSection = () => {
|
|||||||
className='relative order-1 h-[400px] overflow-hidden rounded-xl shadow-xl md:order-2'
|
className='relative order-1 h-[400px] overflow-hidden rounded-xl shadow-xl md:order-2'
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src='/placeholder.svg?height=400&width=600&text=Преимущества+для+клиентов'
|
||||||
m('clients.third-section.banner') ||
|
|
||||||
'/placeholder.svg?height=400&width=600&text=Преимущества+для+клиентов'
|
|
||||||
}
|
|
||||||
alt='Преимущества для клиентов'
|
alt='Преимущества для клиентов'
|
||||||
fill
|
fill
|
||||||
className='object-cover'
|
className='object-cover'
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
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';
|
||||||
|
|
||||||
export const CtaSection = () => {
|
export const CtaSection = () => {
|
||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
@ -14,9 +17,9 @@ export const CtaSection = () => {
|
|||||||
{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'>
|
<h4 className='text-xl font-bold'>
|
||||||
{t('common.buttons.purchaseCardAtGasStations')}
|
{t('common.buttons.purchaseCardAtGasStations')}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -4,7 +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';
|
import { Logo } from '@/shared/assets/logo';
|
||||||
|
|
||||||
export const Footer = () => {
|
export const Footer = () => {
|
||||||
@ -20,7 +19,7 @@ export const Footer = () => {
|
|||||||
<Logo/>
|
<Logo/>
|
||||||
</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={t('social.facebook')} target='_blank' className='text-gray-400 hover:text-white'>
|
||||||
<svg
|
<svg
|
||||||
className='h-6 w-6'
|
className='h-6 w-6'
|
||||||
@ -50,7 +49,6 @@ export const Footer = () => {
|
|||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<AppStoreButtons className='mt-4' />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex md:justify-center'>
|
<div className='flex md:justify-center'>
|
||||||
|
|||||||
@ -3,31 +3,16 @@
|
|||||||
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 { 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();
|
const { m } = useMediaController();
|
||||||
|
|
||||||
useEffect(() => {
|
const banner = m('home.hero-section.banner');
|
||||||
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'>
|
||||||
@ -43,7 +28,7 @@ export const HeroSection = () => {
|
|||||||
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'
|
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
|
<Image
|
||||||
src={m('home.hero-section.banner') || '/oriyo_bg.jpeg'}
|
src={banner?.photo || '/oriyo_bg.jpeg'}
|
||||||
alt='Oriyo Station'
|
alt='Oriyo Station'
|
||||||
fill
|
fill
|
||||||
className='object-cover sm:scale-110 md:scale-120 xl:scale-140'
|
className='object-cover sm:scale-110 md:scale-120 xl:scale-140'
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { GasStationMap } from '@/features/map';
|
|||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
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';
|
||||||
|
|
||||||
interface MapSectionProps {
|
interface MapSectionProps {
|
||||||
stations: Stations;
|
stations: Stations;
|
||||||
@ -15,6 +16,9 @@ interface MapSectionProps {
|
|||||||
|
|
||||||
export const MapSection = ({ stations }: MapSectionProps) => {
|
export const MapSection = ({ stations }: MapSectionProps) => {
|
||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
|
const { m } = useMediaController();
|
||||||
|
|
||||||
|
const stationsMedia = m('stations.main');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id='stations' className='bg-gray-50'>
|
<section id='stations' className='bg-gray-50'>
|
||||||
|
|||||||
@ -1,63 +1,69 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
import { Container } from '@/shared/components/container';
|
||||||
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 ref={sectionRef} className='bg-red-600 text-white'>
|
||||||
<Container>
|
<Container>
|
||||||
<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>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
294
src/widgets/transactions-table.tsx
Normal file
294
src/widgets/transactions-table.tsx
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { ru } from 'date-fns/locale';
|
||||||
|
import { CalendarIcon } from 'lucide-react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TransactionRequest,
|
||||||
|
TransactionResponse,
|
||||||
|
} from '@/entities/transactions/model/types';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
} from './pagination';
|
||||||
|
|
||||||
|
export interface TransactionsTableProps {
|
||||||
|
onChange: (request: TransactionRequest) => void;
|
||||||
|
data: TransactionResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TransactionsTable = ({
|
||||||
|
data,
|
||||||
|
onChange,
|
||||||
|
}: TransactionsTableProps) => {
|
||||||
|
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
|
||||||
|
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||||
|
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
if (page < 1 || page > data.total_pages) return;
|
||||||
|
setCurrentPage(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPageNumbers = () => {
|
||||||
|
const pages = [];
|
||||||
|
const maxVisiblePages = 5; // Maximum number of visible pages
|
||||||
|
const halfVisible = Math.floor(maxVisiblePages / 2);
|
||||||
|
|
||||||
|
let startPage = Math.max(1, currentPage - halfVisible);
|
||||||
|
let endPage = Math.min(data.total_pages, currentPage + halfVisible);
|
||||||
|
|
||||||
|
if (currentPage <= halfVisible) {
|
||||||
|
endPage = Math.min(data.total_pages, maxVisiblePages);
|
||||||
|
} else if (currentPage + halfVisible >= data.total_pages) {
|
||||||
|
startPage = Math.max(1, data.total_pages - maxVisiblePages + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterTransactions = () => {
|
||||||
|
if (!startDate || !endDate) return;
|
||||||
|
setCurrentPage(1); // Reset to the first page when applying filters
|
||||||
|
};
|
||||||
|
|
||||||
|
const { t } = useTextController();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChange({
|
||||||
|
limit: itemsPerPage,
|
||||||
|
page: currentPage,
|
||||||
|
...(startDate ? { start_date: format(startDate, 'yyyy-MM-dd') } : {}),
|
||||||
|
...(endDate ? { end_date: format(endDate, 'yyyy-MM-dd') } : {}),
|
||||||
|
});
|
||||||
|
}, [startDate, endDate, itemsPerPage, currentPage]);
|
||||||
|
|
||||||
|
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 gap-2 sm:grid-cols-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 || transaction.station_name}
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{data.transactions.length > 0 && (
|
||||||
|
<div className='flex flex-col items-center justify-between gap-4 sm:flex-row'>
|
||||||
|
<div className='text-sm text-gray-500'>
|
||||||
|
Показано {data.transactions.length} из {data.total_records} операций
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Pagination>
|
||||||
|
<PaginationContent>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationPrevious
|
||||||
|
onClick={() => handlePageChange(currentPage - 1)}
|
||||||
|
className={
|
||||||
|
currentPage === 1 ? 'pointer-events-none opacity-50' : ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
|
||||||
|
{getPageNumbers().map((page, index) => (
|
||||||
|
<PaginationItem key={index}>
|
||||||
|
<PaginationLink
|
||||||
|
isActive={currentPage === page}
|
||||||
|
onClick={() => handlePageChange(page as number)}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationNext
|
||||||
|
onClick={() => handlePageChange(currentPage + 1)}
|
||||||
|
className={
|
||||||
|
currentPage === data.total_pages
|
||||||
|
? 'pointer-events-none opacity-50'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<span className='text-sm text-gray-500'>
|
||||||
|
{t('transactions.entries')}
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
className='rounded border p-1 text-sm'
|
||||||
|
value={itemsPerPage}
|
||||||
|
onChange={(e) => {
|
||||||
|
setItemsPerPage(Number(e.target.value));
|
||||||
|
setCurrentPage(1); // Reset to first page when changing items per page
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value={5}>5</option>
|
||||||
|
<option value={10}>10</option>
|
||||||
|
<option value={20}>20</option>
|
||||||
|
<option value={50}>50</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1 +0,0 @@
|
|||||||
export { TransactionsTable } from "./ui/transactions-table";
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
import { ChangeEvent } from 'react';
|
|
||||||
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Pagination,
|
|
||||||
PaginationContent,
|
|
||||||
PaginationItem,
|
|
||||||
PaginationLink,
|
|
||||||
PaginationNext,
|
|
||||||
PaginationPrevious,
|
|
||||||
} from '../../pagination';
|
|
||||||
|
|
||||||
interface TablePaginationProps {
|
|
||||||
currentPage: number;
|
|
||||||
itemsPerPage: number;
|
|
||||||
totalPages: number;
|
|
||||||
totalOperations?: number;
|
|
||||||
transactionsQuantity: number;
|
|
||||||
itemsPerPageOptions: number[];
|
|
||||||
onPageChange: (page: number) => void;
|
|
||||||
onItemsPerPageChange: (e: ChangeEvent<HTMLSelectElement>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TransactionsTablePagination = ({
|
|
||||||
currentPage,
|
|
||||||
itemsPerPage,
|
|
||||||
totalPages,
|
|
||||||
totalOperations = 0,
|
|
||||||
itemsPerPageOptions,
|
|
||||||
transactionsQuantity,
|
|
||||||
onPageChange,
|
|
||||||
onItemsPerPageChange,
|
|
||||||
}: TablePaginationProps) => {
|
|
||||||
const { t } = useTextController();
|
|
||||||
|
|
||||||
const getPageNumbers = () => {
|
|
||||||
const pages = [];
|
|
||||||
const maxVisiblePages = 5; // Maximum number of visible pages
|
|
||||||
const halfVisible = Math.floor(maxVisiblePages / 2);
|
|
||||||
|
|
||||||
let startPage = Math.max(1, currentPage - halfVisible);
|
|
||||||
let endPage = Math.min(totalPages, currentPage + halfVisible);
|
|
||||||
|
|
||||||
if (currentPage <= halfVisible) {
|
|
||||||
endPage = Math.min(totalPages, maxVisiblePages);
|
|
||||||
} else if (currentPage + halfVisible >= totalPages) {
|
|
||||||
startPage = Math.max(1, totalPages - maxVisiblePages + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = startPage; i <= endPage; i++) {
|
|
||||||
pages.push(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
return pages;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{transactionsQuantity > 0 && (
|
|
||||||
<div className='flex flex-col items-center justify-between gap-4 sm:flex-row'>
|
|
||||||
<div className='text-sm text-gray-500'>
|
|
||||||
Показано {transactionsQuantity} из {totalOperations} операций
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Pagination>
|
|
||||||
<PaginationContent>
|
|
||||||
<PaginationItem>
|
|
||||||
<PaginationPrevious
|
|
||||||
onClick={() => onPageChange(currentPage - 1)}
|
|
||||||
className={
|
|
||||||
currentPage === 1 ? 'pointer-events-none opacity-50' : ''
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PaginationItem>
|
|
||||||
|
|
||||||
{getPageNumbers().map((page, index) => (
|
|
||||||
<PaginationItem key={index}>
|
|
||||||
<PaginationLink
|
|
||||||
isActive={currentPage === page}
|
|
||||||
onClick={() => onPageChange(page as number)}
|
|
||||||
>
|
|
||||||
{page}
|
|
||||||
</PaginationLink>
|
|
||||||
</PaginationItem>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<PaginationItem>
|
|
||||||
<PaginationNext
|
|
||||||
onClick={() => onPageChange(currentPage + 1)}
|
|
||||||
className={
|
|
||||||
currentPage === totalPages
|
|
||||||
? 'pointer-events-none opacity-50'
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PaginationItem>
|
|
||||||
</PaginationContent>
|
|
||||||
</Pagination>
|
|
||||||
|
|
||||||
<div className='flex items-center gap-2'>
|
|
||||||
<span className='text-sm text-gray-500'>
|
|
||||||
{t('transactions.entries')}
|
|
||||||
</span>
|
|
||||||
<select
|
|
||||||
className='rounded border p-1 text-sm'
|
|
||||||
value={itemsPerPage}
|
|
||||||
onChange={onItemsPerPageChange}
|
|
||||||
>
|
|
||||||
{itemsPerPageOptions.map((option) => (
|
|
||||||
<option key={option} value={option}>
|
|
||||||
{option}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
import { format } from 'date-fns';
|
|
||||||
import { ru } from 'date-fns/locale';
|
|
||||||
import { CalendarIcon, Printer, X } from 'lucide-react';
|
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
|
||||||
|
|
||||||
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';
|
|
||||||
|
|
||||||
interface TransactionsTableHeaderProps {
|
|
||||||
startDate: Date | undefined;
|
|
||||||
setStartDate: Dispatch<SetStateAction<Date | undefined>>;
|
|
||||||
endDate: Date | undefined;
|
|
||||||
setEndDate: Dispatch<SetStateAction<Date | undefined>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TransactionsTableHeader = ({
|
|
||||||
startDate,
|
|
||||||
setStartDate,
|
|
||||||
endDate,
|
|
||||||
setEndDate,
|
|
||||||
}: TransactionsTableHeaderProps) => {
|
|
||||||
const { t } = useTextController();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col items-start justify-between gap-4 md:flex-row md:items-center'>
|
|
||||||
<div className='flex items-center justify-between max-md:w-full'>
|
|
||||||
<h2 className='text-2xl font-bold'>
|
|
||||||
{t('corporate.transactions.title')}
|
|
||||||
</h2>
|
|
||||||
<Button
|
|
||||||
size={'icon'}
|
|
||||||
variant={'outline'}
|
|
||||||
onClick={() => window.print()}
|
|
||||||
className='flex md:hidden'
|
|
||||||
>
|
|
||||||
<Printer />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex w-full flex-col justify-center gap-4 md:w-auto md:flex-row'>
|
|
||||||
<Button
|
|
||||||
size={'icon'}
|
|
||||||
variant={'outline'}
|
|
||||||
onClick={() => window.print()}
|
|
||||||
className='hidden md:flex'
|
|
||||||
>
|
|
||||||
<Printer />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className='grid gap-2 sm:grid-cols-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>
|
|
||||||
<Button variant='ghost' onClick={() => setStartDate(undefined)}>
|
|
||||||
<X className='mr-2 h-4 w-4' />
|
|
||||||
</Button>
|
|
||||||
</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>
|
|
||||||
<Button variant='ghost' onClick={() => setEndDate(undefined)}>
|
|
||||||
<X className='mr-2 h-4 w-4' />
|
|
||||||
</Button>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
import { format } from 'date-fns';
|
|
||||||
import { ChangeEvent, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import TableLoadingOverlay from '@/shared/components/table-loading-overlay';
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from '@/shared/shadcn-ui/table';
|
|
||||||
|
|
||||||
import { TransactionsTablePagination } from './table-pagination';
|
|
||||||
import { TransactionsTableHeader } from './transactions-table-header';
|
|
||||||
|
|
||||||
export interface TransactionRequest {
|
|
||||||
start_date?: string;
|
|
||||||
end_date?: string;
|
|
||||||
page: number;
|
|
||||||
limit: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransactionsTableProps<T> {
|
|
||||||
data: {
|
|
||||||
transactions: T[];
|
|
||||||
total_pages: number;
|
|
||||||
total_records: number;
|
|
||||||
};
|
|
||||||
isLoading: boolean;
|
|
||||||
onChange: (request: TransactionRequest) => void;
|
|
||||||
renderHeaders: () => React.ReactNode;
|
|
||||||
renderRow: (transaction: T, index: number) => React.ReactNode;
|
|
||||||
itemsPerPageOptions?: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TransactionsTable = <T,>({
|
|
||||||
data,
|
|
||||||
isLoading,
|
|
||||||
onChange,
|
|
||||||
renderHeaders,
|
|
||||||
renderRow,
|
|
||||||
itemsPerPageOptions = [5, 10, 20, 50],
|
|
||||||
}: TransactionsTableProps<T>) => {
|
|
||||||
const [startDate, setStartDate] = useState<Date | undefined>(
|
|
||||||
new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
|
||||||
);
|
|
||||||
const [endDate, setEndDate] = useState<Date | undefined>(new Date());
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(itemsPerPageOptions[0]);
|
|
||||||
|
|
||||||
const handlePageChange = (page: number) => {
|
|
||||||
if (page < 1 || page > data.total_pages) return;
|
|
||||||
setCurrentPage(page);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleItemsPerPageChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
|
||||||
setItemsPerPage(Number(e.target.value));
|
|
||||||
setCurrentPage(1); // Reset to first page when changing items per page
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterTransactions = () => {
|
|
||||||
if (!startDate || !endDate) return;
|
|
||||||
setCurrentPage(1); // Reset to the first page when applying filters
|
|
||||||
};
|
|
||||||
|
|
||||||
const { t } = useTextController();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onChange({
|
|
||||||
limit: itemsPerPage,
|
|
||||||
page: currentPage,
|
|
||||||
...(startDate ? { start_date: format(startDate, 'yyyy-MM-dd') } : {}),
|
|
||||||
...(endDate ? { end_date: format(endDate, 'yyyy-MM-dd') } : {}),
|
|
||||||
});
|
|
||||||
}, [startDate, endDate, itemsPerPage, currentPage]);
|
|
||||||
|
|
||||||
if (!data) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='relative space-y-6'>
|
|
||||||
<TableLoadingOverlay isLoading={isLoading} />
|
|
||||||
<TransactionsTableHeader
|
|
||||||
startDate={startDate}
|
|
||||||
setStartDate={setStartDate}
|
|
||||||
endDate={endDate}
|
|
||||||
setEndDate={setEndDate}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='relative rounded-md border'>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>{renderHeaders()}</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{data.transactions.length > 0 ? (
|
|
||||||
data.transactions.map((transaction, index) =>
|
|
||||||
renderRow(transaction, index),
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={6}
|
|
||||||
className='py-6 text-center text-gray-500'
|
|
||||||
>
|
|
||||||
{t('corporate.transactions.no-data')}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
<TransactionsTablePagination
|
|
||||||
currentPage={currentPage}
|
|
||||||
itemsPerPage={itemsPerPage}
|
|
||||||
itemsPerPageOptions={itemsPerPageOptions}
|
|
||||||
totalPages={data.total_pages}
|
|
||||||
transactionsQuantity={data.transactions.length}
|
|
||||||
totalOperations={data.total_records}
|
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onItemsPerPageChange={handleItemsPerPageChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Briefcase } from 'lucide-react';
|
import { Briefcase } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { Jobs } from '@/app/api-utlities/@types/index';
|
import { Jobs } from '@/app/api-utlities/@types/index';
|
||||||
|
|
||||||
@ -17,6 +16,7 @@ import {
|
|||||||
TabsList,
|
TabsList,
|
||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from '@/shared/shadcn-ui/tabs';
|
} from '@/shared/shadcn-ui/tabs';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
interface VacanciesSectionProps {
|
interface VacanciesSectionProps {
|
||||||
jobs: Jobs;
|
jobs: Jobs;
|
||||||
@ -131,7 +131,7 @@ const Vacancy = ({ jobTitle, location, tags }: VacancyProps) => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Link href={`mailto:${t('home.vacancies.vacancy.applyToMail')}`}>
|
<Link href='mailto:info@oriyo.tj'>
|
||||||
<Button variant='outline' size='sm'>
|
<Button variant='outline' size='sm'>
|
||||||
{t('common.buttons.apply')}
|
{t('common.buttons.apply')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "ES2017",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@ -15,45 +11,23 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"types": [
|
"types": ["node"],
|
||||||
"node"
|
|
||||||
],
|
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "next"
|
"name": "next"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": ["./src/*"],
|
||||||
"./src/*"
|
"@/entities/*": ["./src/entities/*"],
|
||||||
],
|
"@/features/*": ["./src/features/*"],
|
||||||
"@/entities/*": [
|
"@/shared/*": ["./src/shared/*"],
|
||||||
"./src/entities/*"
|
"@/widgets/*": ["./src/widgets/*"],
|
||||||
],
|
"@/pages-templates/*": ["./src/pages-templates/*"]
|
||||||
"@/features/*": [
|
|
||||||
"./src/features/*"
|
|
||||||
],
|
|
||||||
"@/shared/*": [
|
|
||||||
"./src/shared/*"
|
|
||||||
],
|
|
||||||
"@/widgets/*": [
|
|
||||||
"./src/widgets/*"
|
|
||||||
],
|
|
||||||
"@/pages-templates/*": [
|
|
||||||
"./src/pages-templates/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
"next-env.d.ts",
|
"exclude": ["node_modules"],
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx",
|
|
||||||
".next/types/**/*.ts",
|
|
||||||
".next/dev/types/**/*.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user