From eb38af7fa7cb7bb3fb1192137221ebb830f114ca Mon Sep 17 00:00:00 2001 From: BunyodL Date: Fri, 2 May 2025 05:54:47 +0500 Subject: [PATCH] feat: integrate yandex-maps --- package.json | 1 + pnpm-lock.yaml | 25 + public/map/oriyo-marker.png | Bin 0 -> 11512 bytes src/app/page.tsx | 2 +- src/features/map/ui/gas-station-map.tsx | 867 +++++++++++------------- src/features/map/ui/yandex-map.tsx | 25 +- src/shared/shadcn-ui/separator.tsx | 31 + src/widgets/map-section.tsx | 10 +- 8 files changed, 487 insertions(+), 474 deletions(-) create mode 100644 public/map/oriyo-marker.png create mode 100644 src/shared/shadcn-ui/separator.tsx diff --git a/package.json b/package.json index 7c466b9..44bb7bf 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-navigation-menu": "^1.2.10", "@radix-ui/react-popover": "^1.1.11", "@radix-ui/react-select": "^2.2.2", + "@radix-ui/react-separator": "^1.1.4", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.8", "@radix-ui/react-toast": "^1.2.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e7278d..dbc27a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@radix-ui/react-select': specifier: ^2.2.2 version: 2.2.2(@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) + '@radix-ui/react-separator': + specifier: ^1.1.4 + version: 1.1.4(@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) '@radix-ui/react-slot': specifier: ^1.2.0 version: 1.2.0(@types/react@19.1.2)(react@19.1.0) @@ -818,6 +821,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-separator@1.1.4': + resolution: {integrity: sha512-2fTm6PSiUm8YPq9W0E4reYuv01EE3aFSzt8edBiXqPHshF8N9+Kymt/k0/R+F3dkY5lQyB/zPtrP82phskLi7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.2.0': resolution: {integrity: sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==} peerDependencies: @@ -3328,6 +3344,15 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-separator@1.1.4(@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)': + dependencies: + '@radix-ui/react-primitive': 2.1.0(@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) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-slot@1.2.0(@types/react@19.1.2)(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) diff --git a/public/map/oriyo-marker.png b/public/map/oriyo-marker.png new file mode 100644 index 0000000000000000000000000000000000000000..4fa44f080604b188e957a0d9fb97c51e244a4e46 GIT binary patch literal 11512 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 Sz3SJy$#T-lQdJVBA^!)vw}B%7 literal 0 HcmV?d00001 diff --git a/src/app/page.tsx b/src/app/page.tsx index 9322edb..0cba606 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -12,7 +12,7 @@ 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`, + `${process.env.NEXT_PUBLIC_API_URL}/pages/main`, { method: 'GET' }, ).then((res) => res.json())) as MainPageData; diff --git a/src/features/map/ui/gas-station-map.tsx b/src/features/map/ui/gas-station-map.tsx index 76ce169..c86273e 100644 --- a/src/features/map/ui/gas-station-map.tsx +++ b/src/features/map/ui/gas-station-map.tsx @@ -8,13 +8,14 @@ import { List, MapPin, } from 'lucide-react'; -import { useEffect, useRef, useState } from 'react'; +import { useMemo, 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'; +import { Separator } from '@/shared/shadcn-ui/separator'; import { Tabs, TabsContent, @@ -22,443 +23,221 @@ import { TabsTrigger, } from '@/shared/shadcn-ui/tabs'; -// Sample data for gas stations -const stations = [ - { - id: 1, - name: 'АЗС Душанбе-Центр', - address: 'ул. Рудаки 150, Душанбе', - city: 'Душанбе', - coordinates: { x: 0.2, y: 0.3 }, - services: ['ДТ', 'АИ-92', 'АИ-95', 'Z-100 Power', 'Минимаркет', 'Туалет'], - }, - { - id: 2, - name: 'АЗС Худжанд', - address: 'ул. Ленина 45, Худжанд', - city: 'Худжанд', - coordinates: { x: 0.5, y: 0.2 }, - services: [ - 'ДТ', - 'АИ-92', - 'АИ-95', - 'Пропан', - 'Минимаркет', - 'Автомойка', - 'Туалет', - ], - }, - { - id: 3, - name: 'АЗС Куляб', - address: 'ул. Сомони 78, Куляб', - city: 'Куляб', - coordinates: { x: 0.7, y: 0.4 }, - services: ['ДТ', 'АИ-92', 'Пропан', 'Туалет'], - }, - { - id: 4, - name: 'АЗС Бохтар', - address: 'ул. Айни 23, Бохтар', - city: 'Бохтар', - coordinates: { x: 0.3, y: 0.6 }, - services: [ - 'ДТ', - 'АИ-92', - 'АИ-95', - 'Z-100 Power', - 'Минимаркет', - 'Зарядная станция', - 'Туалет', - ], - }, - { - id: 5, - name: 'АЗС Хорог', - address: 'ул. Горная 12, Хорог', - city: 'Хорог', - coordinates: { x: 0.6, y: 0.7 }, - services: ['ДТ', 'АИ-92', 'Автомойка', 'Туалет'], - }, - { - id: 6, - name: 'АЗС Истаравшан', - address: 'ул. Исмоили Сомони 34, Истаравшан', - city: 'Истаравшан', - coordinates: { x: 0.8, y: 0.8 }, - services: ['ДТ', 'АИ-92', 'АИ-95', 'Минимаркет', 'Туалет'], - }, - { - id: 7, - name: 'АЗС Пенджикент', - address: 'ул. Рудаки 56, Пенджикент', - city: 'Пенджикент', - coordinates: { x: 0.1, y: 0.9 }, - services: ['ДТ', 'АИ-92', 'АИ-95', 'Пропан', 'Минимаркет', 'Туалет'], - }, - { - id: 8, - name: 'АЗС Душанбе-Запад', - address: 'ул. Джами 23, Душанбе', - city: 'Душанбе', - coordinates: { x: 0.25, y: 0.35 }, - services: [ - 'ДТ', - 'АИ-92', - 'АИ-95', - 'Z-100 Power', - 'Пропан', - 'Минимаркет', - 'Автомойка', - 'Туалет', - ], - }, - { - id: 9, - name: 'АЗС Душанбе-Восток', - address: 'ул. Айни 78, Душанбе', - city: 'Душанбе', - coordinates: { x: 0.15, y: 0.25 }, - services: [ - 'ДТ', - 'АИ-92', - 'АИ-95', - 'Зарядная станция', - 'Минимаркет', - 'Туалет', - ], - }, - { - id: 10, - name: 'АЗС Гиссар', - address: 'ул. Центральная 12, Гиссар', - city: 'Гиссар', - coordinates: { x: 0.4, y: 0.4 }, - services: ['ДТ', 'АИ-92', 'Пропан', 'Туалет'], - }, - { - id: 11, - name: 'АЗС Вахдат', - address: 'ул. Сомони 45, Вахдат', - city: 'Вахдат', - coordinates: { x: 0.55, y: 0.45 }, - services: ['ДТ', 'АИ-92', 'АИ-95', 'Минимаркет', 'Туалет'], - }, - { - id: 12, - name: 'АЗС Турсунзаде', - address: 'ул. Ленина 34, Турсунзаде', - city: 'Турсунзаде', - coordinates: { x: 0.65, y: 0.55 }, - services: ['ДТ', 'АИ-92', 'АИ-95', 'Z-100 Power', 'Автомойка', 'Туалет'], - }, -]; - -// All available filters -const allFilters = [ - 'ДТ', - 'АИ-92', - 'АИ-95', - 'Z-100 Power', - 'Пропан', - 'Зарядная станция', - 'Минимаркет', - 'Автомойка', - 'Туалет', -]; - -// Extract unique cities from stations -const allCities = [...new Set(stations.map((station) => station.city))].sort(); +import { Point } from '../model'; +import { YandexMap } from './yandex-map'; +// Пропсы для компонента GasStationMap interface GasStationMapProps { stations: Stations; } -export default function GasStationMap({ - stations: _stations, -}: GasStationMapProps) { - const { t } = useTextController(); - const mapRef = useRef(null); - const [activeFilters, setActiveFilters] = useState([]); - const [activeCities, setActiveCities] = useState([]); - const [filteredStations, setFilteredStations] = useState(stations); - const [selectedStation, setSelectedStation] = useState(null); - const [isFilterOpen, setIsFilterOpen] = useState(false); - const [isStationListOpen, setIsStationListOpen] = useState(false); - const [activeFilterTab, setActiveFilterTab] = useState('cities'); +// Пропсы для панели фильтров +interface FilterPanelProps { + isOpen: boolean; + onClose: () => void; + activeFilters: string[]; + activeCities: string[]; + allCities: string[]; + allFilters: string[]; + activeFilterTab: string; + toggleFilter: (filter: string) => void; + toggleCity: (city: string) => void; + selectAllCities: () => void; + setActiveFilterTab: (tab: string) => void; + resetFilters: () => void; + resetCities: () => void; + t: (key: string) => string; +} - // Toggle service filter - const toggleFilter = (filter: string) => { - if (activeFilters.includes(filter)) { - setActiveFilters(activeFilters.filter((f) => f !== filter)); - } else { - setActiveFilters([...activeFilters, filter]); - } - }; - - // Toggle city filter - const toggleCity = (city: string) => { - if (activeCities.includes(city)) { - setActiveCities(activeCities.filter((c) => c !== city)); - } else { - setActiveCities([...activeCities, city]); - } - }; - - // Select all cities - const selectAllCities = () => { - if (activeCities.length === allCities.length) { - setActiveCities([]); - } else { - setActiveCities([...allCities]); - } - }; - - // Filter stations based on active filters and cities - useEffect(() => { - let filtered = stations; - - // Filter by services - if (activeFilters.length > 0) { - filtered = filtered.filter((station) => - activeFilters.every((filter) => station.services.includes(filter)), - ); - } - - // Filter by cities - if (activeCities.length > 0) { - filtered = filtered.filter((station) => - activeCities.includes(station.city), - ); - } - - setFilteredStations(filtered); - }, [activeFilters, activeCities]); - - useEffect(() => { - // This is a placeholder for a real map implementation - // In a real application, you would use a mapping library like Mapbox, Google Maps, or Leaflet - if (mapRef.current) { - const canvas = document.createElement('canvas'); - canvas.width = mapRef.current.clientWidth; - canvas.height = mapRef.current.clientHeight; - mapRef.current.appendChild(canvas); - - const ctx = canvas.getContext('2d'); - if (ctx) { - // Draw a simple map placeholder - ctx.fillStyle = '#f3f4f6'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - // Draw some roads - ctx.strokeStyle = '#d1d5db'; - ctx.lineWidth = 5; - - // Horizontal roads - for (let i = 1; i < 5; i++) { - ctx.beginPath(); - ctx.moveTo(0, canvas.height * (i / 5)); - ctx.lineTo(canvas.width, canvas.height * (i / 5)); - ctx.stroke(); - } - - // Vertical roads - for (let i = 1; i < 8; i++) { - ctx.beginPath(); - ctx.moveTo(canvas.width * (i / 8), 0); - ctx.lineTo(canvas.width * (i / 8), canvas.height); - ctx.stroke(); - } - - // Draw gas station markers - filteredStations.forEach((station) => { - const isSelected = selectedStation === station.id; - // Draw marker - ctx.fillStyle = isSelected ? '#3b82f6' : '#ef4444'; - ctx.beginPath(); - ctx.arc( - station.coordinates.x * canvas.width, - station.coordinates.y * canvas.height, - isSelected ? 12 : 10, - 0, - 2 * Math.PI, - ); - ctx.fill(); - - // Draw white border - ctx.strokeStyle = 'white'; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.arc( - station.coordinates.x * canvas.width, - station.coordinates.y * canvas.height, - isSelected ? 12 : 10, - 0, - 2 * Math.PI, - ); - ctx.stroke(); - }); - - // Add city names - ctx.fillStyle = '#1f2937'; - ctx.font = 'bold 16px Arial'; - ctx.fillText('Душанбе', canvas.width * 0.45, canvas.height * 0.15); - ctx.fillText('Худжанд', canvas.width * 0.2, canvas.height * 0.25); - ctx.fillText('Куляб', canvas.width * 0.7, canvas.height * 0.35); - ctx.fillText('Бохтар', canvas.width * 0.3, canvas.height * 0.55); - ctx.fillText('Хорог', canvas.width * 0.6, canvas.height * 0.65); - ctx.fillText('Истаравшан', canvas.width * 0.8, canvas.height * 0.75); - ctx.fillText('Пенджикент', canvas.width * 0.1, canvas.height * 0.85); - } - } - - return () => { - if (mapRef.current) { - while (mapRef.current.firstChild) { - mapRef.current.removeChild(mapRef.current.firstChild); - } - } - }; - }, [filteredStations, selectedStation]); +// Пропсы для панели списка станций +interface StationListPanelProps { + isOpen: boolean; + onClose: () => void; + stations: Stations; + selectedStation: number | null; + activeFilters: string[]; + activeCities: string[]; + setSelectedStation: (id: number | null) => void; + t: (key: string) => string; + filterToFieldMap: { [key: string]: keyof Stations[number] }; + allFilters: string[]; + resetFilters: () => void; + resetCities: () => void; +} +// Компонент панели фильтров +function FilterPanel({ + isOpen, + onClose, + activeFilters, + activeCities, + allCities, + allFilters, + activeFilterTab, + toggleFilter, + toggleCity, + selectAllCities, + setActiveFilterTab, + resetFilters, + resetCities, + t, +}: FilterPanelProps) { return ( -
- {/* Filter panel - slides from left */} -
-
-
- - {t('map.filters')} -
- -
- -
- - - {t('map.cities')} - {t('map.services')} - - - - - -
- {allCities.map((city) => ( - - ))} -
- - {activeCities.length > 0 && ( - - )} -
- - -
- {allFilters.map((filter) => ( - - ))} -
- {activeFilters.length > 0 && ( - - )} -
-
+
+
+
+ + {t('map.filters')}
+
- {/* Station list panel - slides from right */} -
-
- -
- {t('map.stationsList')} - {filteredStations.length} -
-
-
+ - {filteredStations.length > 0 ? ( -
- {filteredStations.map((station) => ( + + {t('map.cities')} + {t('map.services')} + + + + + +
+ {allCities.map((city) => ( + + ))} +
+
+ + +
+ {allFilters.map((filter) => ( + + ))} +
+
+ + + + {/* Кнопка сброса фильтров */} + {activeFilterTab === 'cities' + ? activeCities.length > 0 && ( + + ) + : activeFilters.length > 0 && ( + + )} + +
+
+ ); +} + +// Компонент панели списка станций +function StationListPanel({ + isOpen, + onClose, + stations, + selectedStation, + activeFilters, + activeCities, + setSelectedStation, + t, + filterToFieldMap, + allFilters, + resetCities, + resetFilters, +}: StationListPanelProps) { + return ( +
+
+ +
+ {t('map.stationsList')} + {stations.length} +
+
+
+ {stations.length > 0 ? ( +
+ {stations.map((station) => { + const services = allFilters.filter( + (filter) => station[filterToFieldMap[filter]], + ); + + return (
{station.address}

+ {station.workingHours && ( +

+ {t('map.workingHours')}: {station.workingHours} +

+ )} + {station.description && ( +

+ {station.description} +

+ )}
- - {station.city} - - {station.services.map((service) => ( + {station.region && ( + + {station.region} + + )} + {services.map((service) => ( ))}
+ {station.image && ( + {station.name} + )}
- ))} + ); + })} +
+ ) : ( +
+

{t('map.noStations')}

+
+ {activeFilters.length > 0 && ( + + )} + {activeCities.length > 0 && ( + + )}
- ) : ( -
-

{t('map.noStations')}

-
- {activeFilters.length > 0 && ( - - )} - {activeCities.length > 0 && ( - - )} -
-
- )} -
+
+ )}
+
+ ); +} + +// Главный компонент +export default function GasStationMap({ stations }: GasStationMapProps) { + const { t } = useTextController(); + const [activeFilters, setActiveFilters] = useState([]); + const [activeCities, setActiveCities] = useState([]); + const [selectedStation, setSelectedStation] = useState(null); + const [isFilterOpen, setIsFilterOpen] = useState(false); + const [isStationListOpen, setIsStationListOpen] = useState(false); + const [activeFilterTab, setActiveFilterTab] = useState('cities'); + + // Все доступные фильтры + const allFilters = [ + // 'ДТ', -> нет значения в интерфейсе - TODO: поправить + 'АИ-92', + 'АИ-95', + 'Z-100 Power', + 'Пропан', + 'Зарядная станция', + 'Минимаркет', + 'Автомойка', + 'Туалет', + ]; + + // Маппинг фильтров на поля Station + const filterToFieldMap: { [key: string]: keyof Stations[number] } = { + 'АИ-92': 'ai92', + 'АИ-95': 'ai95', + 'Z-100 Power': 'z100', + Пропан: 'propan', + 'Зарядная станция': 'electricCharge', + Минимаркет: 'miniMarket', + Автомойка: 'carWash', + Туалет: 'toilet', + }; + + // Мемоизация списка уникальных регионов + const allCities = useMemo(() => { + return [ + ...new Set( + stations + .map((station) => station.region) + .filter((region): region is string => region !== null), + ), + ].sort(); + }, [stations]); + + // Мемоизация фильтрованных станций + const filteredStations = useMemo(() => { + let filtered = stations; + + // Фильтрация по регионам (ИЛИ) + if (activeCities.length > 0) { + filtered = filtered.filter( + (station) => station.region && activeCities.includes(station.region), + ); + } + + // Фильтрация по услугам (И) + if (activeFilters.length > 0) { + filtered = filtered.filter((station) => + activeFilters.every((filter) => { + const field = filterToFieldMap[filter]; + return Boolean(station[field]) === true; + }), + ); + } + return filtered; + }, [activeFilters, activeCities, stations]); + + // Мемоизация точек для карты + const points = useMemo( + (): Point[] => + filteredStations.map((st) => ({ + id: st.id, + coordinates: [st.latitude, st.longitude], + })), + [filteredStations], + ); + + // Переключение фильтра услуг + const toggleFilter = (filter: string) => { + setActiveFilters((prev) => + prev.includes(filter) + ? prev.filter((f) => f !== filter) + : [...prev, filter], + ); + }; + + // Переключение фильтра региона + const toggleCity = (city: string) => { + setActiveCities((prev) => + prev.includes(city) ? prev.filter((c) => c !== city) : [...prev, city], + ); + }; + + // Выбор всех регионов + const selectAllCities = () => { + setActiveCities( + activeCities.length === allCities.length ? [] : [...allCities], + ); + }; + + // Сброс фильтров услуг + const resetFilters = () => { + setActiveFilters([]); + }; + + // Сброс фильтров регионов + const resetCities = () => { + setActiveCities([]); + }; + + return ( +
+ {/* Filter panel */} + setIsFilterOpen(false)} + activeFilters={activeFilters} + activeCities={activeCities} + allCities={allCities} + allFilters={allFilters} + activeFilterTab={activeFilterTab} + toggleFilter={toggleFilter} + toggleCity={toggleCity} + selectAllCities={selectAllCities} + setActiveFilterTab={setActiveFilterTab} + resetFilters={resetFilters} + resetCities={resetCities} + t={t} + /> + + {/* Station list panel */} + setIsStationListOpen(false)} + stations={filteredStations} + selectedStation={selectedStation} + activeFilters={activeFilters} + activeCities={activeCities} + setSelectedStation={setSelectedStation} + t={t} + filterToFieldMap={filterToFieldMap} + allFilters={allFilters} + resetFilters={resetFilters} + resetCities={resetCities} + /> {/* Map */}
-
+
{/* Control buttons */} @@ -568,7 +517,7 @@ export default function GasStationMap({ {t('map.ourStations')}

- {t('map.totalStations')}: {stations.length} + {t('map.totalStations')}: {filteredStations.length}

diff --git a/src/features/map/ui/yandex-map.tsx b/src/features/map/ui/yandex-map.tsx index 20f0d7f..93b667a 100644 --- a/src/features/map/ui/yandex-map.tsx +++ b/src/features/map/ui/yandex-map.tsx @@ -1,6 +1,7 @@ 'use client'; import { Map, Placemark, YMaps } from '@pbe/react-yandex-maps'; +import React from 'react'; import { Point } from '../model'; @@ -8,29 +9,43 @@ type YandexMapProps = { points: Point[]; }; +const mapCenter = [55.751574, 37.573856]; + export const YandexMap = ({ points }: YandexMapProps) => { return ( {points.map((point) => ( - + ))} diff --git a/src/shared/shadcn-ui/separator.tsx b/src/shared/shadcn-ui/separator.tsx new file mode 100644 index 0000000..1851e42 --- /dev/null +++ b/src/shared/shadcn-ui/separator.tsx @@ -0,0 +1,31 @@ +'use client'; + +import * as SeparatorPrimitive from '@radix-ui/react-separator'; +import * as React from 'react'; + +import { cn } from '@/shared/lib/utils'; + +const Separator = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = 'horizontal', decorative = true, ...props }, + ref, + ) => ( + + ), +); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/src/widgets/map-section.tsx b/src/widgets/map-section.tsx index 8bfa798..0f982c1 100644 --- a/src/widgets/map-section.tsx +++ b/src/widgets/map-section.tsx @@ -5,8 +5,6 @@ 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'; @@ -17,15 +15,9 @@ interface MapSectionProps { export const MapSection = ({ stations }: MapSectionProps) => { const { t } = useTextController(); - const points = stations.map((st) => ({ - id: st.id, - coordinates: [st.latitude, st.longitude], - })) as Point[]; - return (
-
@@ -41,7 +33,7 @@ export const MapSection = ({ stations }: MapSectionProps) => { className='h-[500px] overflow-hidden rounded-xl border shadow-lg' data-aos='fade-up' > - {/* */} +