diff --git a/next.config.ts b/next.config.ts
index e9ffa30..f7d9caa 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,7 +1,15 @@
-import type { NextConfig } from "next";
+import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
- /* config options here */
+ images: {
+ remotePatterns: [
+ {
+ protocol: 'https',
+ hostname: 'media.bambooapp.ai',
+ pathname: '/files/**',
+ },
+ ],
+ },
};
export default nextConfig;
diff --git a/package.json b/package.json
index 6bedcc6..38833e9 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
+ "@pbe/react-yandex-maps": "^1.2.5",
"@radix-ui/react-collapsible": "^1.1.8",
"@radix-ui/react-dialog": "^1.1.11",
"@radix-ui/react-dropdown-menu": "^2.1.11",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 531d1c9..ed43332 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,6 +11,9 @@ importers:
'@hookform/resolvers':
specifier: ^5.0.1
version: 5.0.1(react-hook-form@7.56.1(react@19.1.0))
+ '@pbe/react-yandex-maps':
+ specifier: ^1.2.5
+ version: 1.2.5(react@19.1.0)
'@radix-ui/react-collapsible':
specifier: ^1.1.8
version: 1.1.8(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -526,6 +529,12 @@ packages:
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
engines: {node: '>=12.4.0'}
+ '@pbe/react-yandex-maps@1.2.5':
+ resolution: {integrity: sha512-cBojin5e1fPx9XVCAqHQJsCnHGMeBNsP0TrNfpWCrPFfxb30ye+JgcGr2mn767Gbr1d+RufBLRiUcX2kaiAwjQ==}
+ engines: {node: '>=16'}
+ peerDependencies:
+ react: ^16.x || ^17.x || ^18.x
+
'@pkgr/core@0.2.4':
resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
@@ -1115,6 +1124,9 @@ packages:
'@types/use-sync-external-store@0.0.6':
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
+ '@types/yandex-maps@2.1.29':
+ resolution: {integrity: sha512-nuibRWj3RU/9KXlCzTrRtDE+n6V9l7NbT9JakicqZ5OXIdwyb6blvV2Uwn6lB58WYm3DSUDP2I2AWlnWMc8z2w==}
+
'@typescript-eslint/eslint-plugin@8.30.1':
resolution: {integrity: sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3047,6 +3059,11 @@ snapshots:
'@nolyfill/is-core-module@1.0.39': {}
+ '@pbe/react-yandex-maps@1.2.5(react@19.1.0)':
+ dependencies:
+ '@types/yandex-maps': 2.1.29
+ react: 19.1.0
+
'@pkgr/core@0.2.4': {}
'@radix-ui/number@1.1.1': {}
@@ -3619,6 +3636,8 @@ snapshots:
'@types/use-sync-external-store@0.0.6': {}
+ '@types/yandex-maps@2.1.29': {}
+
'@typescript-eslint/eslint-plugin@8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
@@ -4194,7 +4213,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import-x@4.10.6(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-plugin-import@2.31.0)(eslint@9.25.0(jiti@2.4.2)))(eslint@9.25.0(jiti@2.4.2)):
+ eslint-module-utils@2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.25.0(jiti@2.4.2)):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -4236,7 +4255,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.25.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import-x@4.10.6(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-plugin-import@2.31.0)(eslint@9.25.0(jiti@2.4.2)))(eslint@9.25.0(jiti@2.4.2))
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.25.0(jiti@2.4.2))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
diff --git a/src/app/api-utlities/@types/index.ts b/src/app/api-utlities/@types/index.ts
index f650de7..aa1e81d 100644
--- a/src/app/api-utlities/@types/index.ts
+++ b/src/app/api-utlities/@types/index.ts
@@ -45,7 +45,7 @@ export type Station = Root<{
_propanCopy: boolean;
_zaryadnayaStanci: boolean;
_miniMarketCop: boolean;
- _region: Select;
+ _region: Select[];
_foto: Image[];
}>;
diff --git a/src/app/api-utlities/@types/main.ts b/src/app/api-utlities/@types/main.ts
new file mode 100644
index 0000000..881fcc3
--- /dev/null
+++ b/src/app/api-utlities/@types/main.ts
@@ -0,0 +1,18 @@
+import {
+ presentDiscounts,
+ presentJobs,
+ presentPartners,
+ presentStations,
+} from '../presenters';
+
+export type Partners = ReturnType;
+export type Jobs = ReturnType;
+export type Discounts = ReturnType;
+export type Stations = ReturnType;
+
+export type MainPageData = {
+ discounts: Discounts;
+ jobs: Jobs;
+ partners: Partners;
+ stations: Stations;
+};
diff --git a/src/app/api-utlities/presenters/index.ts b/src/app/api-utlities/presenters/index.ts
index a71189e..7fd3220 100644
--- a/src/app/api-utlities/presenters/index.ts
+++ b/src/app/api-utlities/presenters/index.ts
@@ -13,13 +13,15 @@ export const presentImage = (images: Image[]) =>
isEmpty(images) ? null : `${process.env.TAYLOR_MEDIA_URL}/${images[0].url}`;
export const presentPartners = (partners: Partner) =>
- partners.records.map((record) => ({
+ partners.records.map((record, index) => ({
+ id: index + 1,
name: record._name,
poster: presentImage(record._image),
}));
export const presentJobs = (jobs: Job) =>
- jobs.records.map((job) => ({
+ jobs.records.map((job, index) => ({
+ id: index + 1,
name: job._name,
tags: job._tags.map((tag) => tag.name),
location: !isEmpty(job._localtio) ? job._localtio[0].name : null,
@@ -27,7 +29,8 @@ export const presentJobs = (jobs: Job) =>
}));
export const presentDiscounts = (discounts: Discount) =>
- discounts.records.map((discount) => ({
+ discounts.records.map((discount, index) => ({
+ id: index + 1,
name: discount._name,
description: discount._opisanie,
expiresAt: discount._do,
@@ -35,7 +38,8 @@ export const presentDiscounts = (discounts: Discount) =>
}));
export const presentStations = (stations: Station) =>
- stations.records.map((station: any) => ({
+ stations.records.map((station, index) => ({
+ id: index + 1,
name: station._name,
description: station._opisanie,
address: station._adress,
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 95e56b6..9322edb 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -8,16 +8,23 @@ import { PromotionsSection } from '@/widgets/promotions-section';
import { StatsSection } from '@/widgets/stats-section';
import { VacanciesSection } from '@/widgets/vacancies-section';
-export default function Home() {
+import { MainPageData } from './api-utlities/@types/main';
+
+export default async function Home() {
+ const mainPageData = (await fetch(
+ `${process.env.NEXT_PUBLIC_BASE_URL}/api/pages/main`,
+ { method: 'GET' },
+ ).then((res) => res.json())) as MainPageData;
+
return (
-
+
-
-
-
+
+
+
diff --git a/src/features/map/model/index.ts b/src/features/map/model/index.ts
new file mode 100644
index 0000000..8d16b29
--- /dev/null
+++ b/src/features/map/model/index.ts
@@ -0,0 +1,4 @@
+export type Point = {
+ id: number;
+ coordinates: [number, number];
+};
diff --git a/src/features/map/ui/gas-station-map.tsx b/src/features/map/ui/gas-station-map.tsx
index ee81f87..76ce169 100644
--- a/src/features/map/ui/gas-station-map.tsx
+++ b/src/features/map/ui/gas-station-map.tsx
@@ -10,6 +10,8 @@ import {
} from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
+import { Stations } from '@/app/api-utlities/@types/main';
+
import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Badge } from '@/shared/shadcn-ui/badge';
import { Button } from '@/shared/shadcn-ui/button';
@@ -168,7 +170,13 @@ const allFilters = [
// Extract unique cities from stations
const allCities = [...new Set(stations.map((station) => station.city))].sort();
-export default function GasStationMap() {
+interface GasStationMapProps {
+ stations: Stations;
+}
+
+export default function GasStationMap({
+ stations: _stations,
+}: GasStationMapProps) {
const { t } = useTextController();
const mapRef = useRef(null);
const [activeFilters, setActiveFilters] = useState([]);
diff --git a/src/features/map/ui/yandex-map.tsx b/src/features/map/ui/yandex-map.tsx
new file mode 100644
index 0000000..20f0d7f
--- /dev/null
+++ b/src/features/map/ui/yandex-map.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import { Map, Placemark, YMaps } from '@pbe/react-yandex-maps';
+
+import { Point } from '../model';
+
+type YandexMapProps = {
+ points: Point[];
+};
+
+export const YandexMap = ({ points }: YandexMapProps) => {
+ return (
+
+
+
+ );
+};
diff --git a/src/shared/components/promotion-slider.tsx b/src/shared/components/promotion-slider.tsx
index b5bc7d4..fabe4be 100644
--- a/src/shared/components/promotion-slider.tsx
+++ b/src/shared/components/promotion-slider.tsx
@@ -5,6 +5,8 @@ import Image from 'next/image';
import Link from 'next/link';
import { useEffect, useState } from 'react';
+import { Discounts } from '@/app/api-utlities/@types/main';
+
import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Button } from '@/shared/shadcn-ui/button';
import { Card, CardContent } from '@/shared/shadcn-ui/card';
@@ -41,7 +43,11 @@ const promotions = [
},
];
-export default function PromotionSlider() {
+interface PromotionSliderProps {
+ discounts: Discounts;
+}
+
+export default function PromotionSlider({ discounts }: PromotionSliderProps) {
const [currentIndex, setCurrentIndex] = useState(0);
const [visibleItems, setVisibleItems] = useState(3);
@@ -85,7 +91,7 @@ export default function PromotionSlider() {
transform: `translateX(-${currentIndex * (100 / visibleItems)}%)`,
}}
>
- {promotions.map((promo) => (
+ {discounts.map((promo) => (
- {promo.title}
+ {promo.name}
{promo.description}
- Действует до: {promo.validUntil}
+ {promo.expiresAt
+ ? `Действует до: ${promo.expiresAt}`
+ : null}
-
-
+ {discounts.length > 3 && (
+ <>
+
+
+ >
+ )}
);
}
diff --git a/src/shared/language/context/text-control-provider.tsx b/src/shared/language/context/text-control-provider.tsx
index 0e3e4af..eb83eab 100644
--- a/src/shared/language/context/text-control-provider.tsx
+++ b/src/shared/language/context/text-control-provider.tsx
@@ -20,9 +20,9 @@ export function TextControlProvider({
textItems,
}: {
children: ReactNode;
- textItems: TextItem[];
+ textItems?: TextItem[];
}) {
- const textMap = textItems.reduce(
+ const textMap = textItems?.reduce(
(pr, cr) => {
pr[cr.key] = cr.value;
@@ -33,7 +33,7 @@ export function TextControlProvider({
// Translation function for flat structure
const t = (key: string): string => {
- if (textMap[key]) {
+ if (textMap?.[key]) {
return textMap[key];
}
diff --git a/src/widgets/map-section.tsx b/src/widgets/map-section.tsx
index e4e0afc..8bfa798 100644
--- a/src/widgets/map-section.tsx
+++ b/src/widgets/map-section.tsx
@@ -2,16 +2,30 @@
import { MapPin } from 'lucide-react';
+import { Stations } from '@/app/api-utlities/@types/main';
+
import { GasStationMap } from '@/features/map';
+import { Point } from '@/features/map/model';
+import { YandexMap } from '@/features/map/ui/yandex-map';
import { useTextController } from '@/shared/language/hooks/use-text-controller';
-export const MapSection = () => {
+interface MapSectionProps {
+ stations: Stations;
+}
+
+export const MapSection = ({ stations }: MapSectionProps) => {
const { t } = useTextController();
+ const points = stations.map((st) => ({
+ id: st.id,
+ coordinates: [st.latitude, st.longitude],
+ })) as Point[];
+
return (
+
@@ -27,7 +41,7 @@ export const MapSection = () => {
className='h-[500px] overflow-hidden rounded-xl border shadow-lg'
data-aos='fade-up'
>
-
+ {/* */}
diff --git a/src/widgets/partners-section.tsx b/src/widgets/partners-section.tsx
index 0b54893..dd51fd9 100644
--- a/src/widgets/partners-section.tsx
+++ b/src/widgets/partners-section.tsx
@@ -4,10 +4,16 @@ import { Handshake } from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link';
+import { Partners } from '@/app/api-utlities/@types/main';
+
import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Button } from '@/shared/shadcn-ui/button';
-export const PartnersSection = () => {
+interface PartnersSectionProps {
+ partners: Partners;
+}
+
+export const PartnersSection = ({ partners }: PartnersSectionProps) => {
const { t } = useTextController();
return (
@@ -26,20 +32,23 @@ export const PartnersSection = () => {
- {[1, 2, 3, 4, 5, 6, 7, 8].map((partner) => (
+ {partners.map(({ id, name, poster }) => (
-
Название
+ {name}
))}
diff --git a/src/widgets/promotions-section.tsx b/src/widgets/promotions-section.tsx
index f08bae3..39068b2 100644
--- a/src/widgets/promotions-section.tsx
+++ b/src/widgets/promotions-section.tsx
@@ -2,10 +2,16 @@
import { Gift } from 'lucide-react';
+import { Discounts } from '@/app/api-utlities/@types/main';
+
import PromotionSlider from '@/shared/components/promotion-slider';
import { useTextController } from '@/shared/language/hooks/use-text-controller';
-export const PromotionsSection = () => {
+interface PromotionsSectionProps {
+ discounts: Discounts;
+}
+
+export const PromotionsSection = ({ discounts }: PromotionsSectionProps) => {
const { t } = useTextController();
return (
@@ -22,7 +28,7 @@ export const PromotionsSection = () => {
{t('home.promotions.description')}
-
+
);
diff --git a/src/widgets/vacancies-section.tsx b/src/widgets/vacancies-section.tsx
index 1d08ffd..db81ee0 100644
--- a/src/widgets/vacancies-section.tsx
+++ b/src/widgets/vacancies-section.tsx
@@ -2,6 +2,8 @@
import { Briefcase } from 'lucide-react';
+import { Jobs } from '@/app/api-utlities/@types/main';
+
import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { cn } from '@/shared/lib/utils';
import { Badge } from '@/shared/shadcn-ui/badge';
@@ -14,9 +16,26 @@ import {
TabsTrigger,
} from '@/shared/shadcn-ui/tabs';
-export const VacanciesSection = () => {
+interface VacanciesSectionProps {
+ jobs: Jobs;
+}
+
+export const VacanciesSection = ({ jobs }: VacanciesSectionProps) => {
const { t } = useTextController();
+ const jobsByType = new Map();
+
+ jobs.forEach((job) => {
+ const existing = jobsByType.get(job.type) || [];
+ jobsByType.set(job.type, [...existing, job]);
+ });
+
+ const allVacancies = t('home.vacancies.all');
+ const officeVacancies = t('home.vacancies.office');
+ const stationsVacancies = t('home.vacancies.stations');
+
+ const jobsTabsTitle = [allVacancies, ...Array.from(jobsByType.keys())];
+
return (
@@ -32,57 +51,42 @@ export const VacanciesSection = () => {
-
+
- {t('home.vacancies.all')}
-
+
+ {t('home.vacancies.all')}
+
+
{t('home.vacancies.office')}
-
+
{t('home.vacancies.stations')}
-
- {[
- 'Оператор АЗС',
- 'Менеджер по продажам',
- 'Бухгалтер',
- 'Специалист по логистике',
- ].map((job, index) => (
+
+
+ {jobs.map((job, index) => (
))}
-
- {[
- 'Менеджер по продажам',
- 'Бухгалтер',
- 'Специалист по логистике',
- ].map((job, index) => (
-
- ))}
-
-
- {['Оператор АЗС', 'Заправщик', 'Менеджер станции'].map(
- (job, index) => (
+
+ {Array.from(jobsByType.entries()).map(([type, jobs]) => (
+
+ {jobs.map((job: Jobs[number], index: number) => (
- ),
- )}
-
+ ))}
+
+ ))}