oriyo_next/src/features/map/ui/gas-station-map.tsx
2025-05-01 23:25:37 +05:00

577 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import {
Check,
ChevronLeft,
ChevronRight,
Filter,
List,
MapPin,
} from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { Stations } from '@/app/api-utlities/@types';
import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Badge } from '@/shared/shadcn-ui/badge';
import { Button } from '@/shared/shadcn-ui/button';
import {
Tabs,
TabsContent,
TabsList,
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();
interface GasStationMapProps {
stations: Stations;
}
export default function GasStationMap({
stations: _stations,
}: GasStationMapProps) {
const { t } = useTextController();
const mapRef = useRef<HTMLDivElement>(null);
const [activeFilters, setActiveFilters] = useState<string[]>([]);
const [activeCities, setActiveCities] = useState<string[]>([]);
const [filteredStations, setFilteredStations] = useState(stations);
const [selectedStation, setSelectedStation] = useState<number | null>(null);
const [isFilterOpen, setIsFilterOpen] = useState(false);
const [isStationListOpen, setIsStationListOpen] = useState(false);
const [activeFilterTab, setActiveFilterTab] = useState('cities');
// 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]);
return (
<div className='relative h-full w-full'>
{/* Filter panel - slides from left */}
<div
className={`absolute top-0 bottom-0 left-0 z-20 transform bg-white shadow-lg transition-transform duration-300 ${
isFilterOpen ? 'translate-x-0' : '-translate-x-full'
}`}
style={{ width: '300px' }}
>
<div className='flex items-center justify-between border-b border-gray-200 p-4'>
<div className='flex items-center gap-2'>
<Filter className='h-5 w-5 text-red-600' />
<span className='font-medium'>{t('map.filters')}</span>
</div>
<Button
variant='ghost'
size='sm'
onClick={() => setIsFilterOpen(false)}
>
<ChevronLeft className='h-5 w-5' />
</Button>
</div>
<div className='p-4'>
<Tabs
value={activeFilterTab}
onValueChange={setActiveFilterTab}
className='w-full'
>
<TabsList className='mb-4 grid w-full grid-cols-2'>
<TabsTrigger value='cities'>{t('map.cities')}</TabsTrigger>
<TabsTrigger value='services'>{t('map.services')}</TabsTrigger>
</TabsList>
<TabsContent value='cities' className='mt-0'>
<Button
variant='outline'
size='sm'
className='mb-3 flex w-full items-center justify-between'
onClick={selectAllCities}
>
<span>{t('map.allCities')}</span>
{activeCities.length === allCities.length && (
<Check className='h-4 w-4 text-green-600' />
)}
</Button>
<div className='flex max-h-[calc(100vh-250px)] flex-col gap-2 overflow-y-auto'>
{allCities.map((city) => (
<Button
key={city}
variant={
activeCities.includes(city) ? 'default' : 'outline'
}
size='sm'
className={`justify-between ${activeCities.includes(city) ? 'bg-red-600 hover:bg-red-700' : ''}`}
onClick={() => toggleCity(city)}
>
<span>{city}</span>
{activeCities.includes(city) && (
<Check className='ml-2 h-4 w-4' />
)}
</Button>
))}
</div>
{activeCities.length > 0 && (
<Button
variant='link'
className='mt-4 p-0 text-red-600'
onClick={() => setActiveCities([])}
>
{t('common.buttons.resetFilters')}
</Button>
)}
</TabsContent>
<TabsContent value='services' className='mt-0'>
<div className='flex flex-wrap gap-2'>
{allFilters.map((filter) => (
<Button
key={filter}
variant={
activeFilters.includes(filter) ? 'default' : 'outline'
}
size='sm'
className={
activeFilters.includes(filter)
? 'bg-red-600 hover:bg-red-700'
: ''
}
onClick={() => toggleFilter(filter)}
>
{filter}
</Button>
))}
</div>
{activeFilters.length > 0 && (
<Button
variant='link'
className='mt-4 p-0 text-red-600'
onClick={() => setActiveFilters([])}
>
{t('common.buttons.resetFilters')}
</Button>
)}
</TabsContent>
</Tabs>
</div>
</div>
{/* Station list panel - slides from right */}
<div
className={`absolute top-0 right-0 bottom-0 z-20 transform bg-white shadow-lg transition-transform duration-300 ${
isStationListOpen ? 'translate-x-0' : 'translate-x-full'
}`}
style={{ width: '350px' }}
>
<div className='flex items-center justify-between border-b border-gray-200 p-4'>
<Button
variant='ghost'
size='sm'
onClick={() => setIsStationListOpen(false)}
>
<ChevronRight className='h-5 w-5' />
</Button>
<div className='flex items-center gap-2'>
<span className='font-medium'>{t('map.stationsList')}</span>
<Badge>{filteredStations.length}</Badge>
</div>
</div>
<div
className='overflow-y-auto'
style={{ height: 'calc(100% - 60px)' }}
>
{filteredStations.length > 0 ? (
<div className='p-2'>
{filteredStations.map((station) => (
<div
key={station.id}
className={`mb-2 cursor-pointer rounded-lg border p-3 transition-colors ${
selectedStation === station.id
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:bg-gray-50'
}`}
onClick={() => setSelectedStation(station.id)}
>
<div className='flex items-start justify-between'>
<h4 className='font-medium'>{station.name}</h4>
<ChevronRight className='h-4 w-4 text-gray-400' />
</div>
<p className='mb-2 text-sm text-gray-500'>
{station.address}
</p>
<div className='flex flex-wrap gap-1'>
<Badge className='mb-1 border-blue-200 bg-blue-100 text-blue-800'>
{station.city}
</Badge>
{station.services.map((service) => (
<Badge
key={service}
variant='outline'
className={
activeFilters.includes(service)
? 'border-red-200 bg-red-100 text-red-800'
: ''
}
>
{service}
</Badge>
))}
</div>
</div>
))}
</div>
) : (
<div className='p-4 text-center text-gray-500'>
<p>{t('map.noStations')}</p>
<div className='mt-2 flex justify-center gap-2'>
{activeFilters.length > 0 && (
<Button
variant='link'
className='text-red-600'
onClick={() => setActiveFilters([])}
>
{t('common.buttons.resetFilters')}
</Button>
)}
{activeCities.length > 0 && (
<Button
variant='link'
className='text-red-600'
onClick={() => setActiveCities([])}
>
{t('map.allCities')}
</Button>
)}
</div>
</div>
)}
</div>
</div>
{/* Map */}
<div className='h-full w-full'>
<div ref={mapRef} className='h-full w-full'></div>
</div>
{/* Control buttons */}
<div className='absolute top-4 left-4 z-10'>
<Button
variant='default'
size='sm'
className='border border-gray-200 bg-white text-gray-800 shadow-md hover:bg-gray-100'
onClick={() => setIsFilterOpen(true)}
>
<Filter className='h-4 w-4 text-red-600 sm:mr-2' />
<span className='hidden sm:flex'>{t('map.filters')}</span>
{(activeFilters.length > 0 || activeCities.length > 0) && (
<Badge className='ml-2 bg-red-600' variant='default'>
{activeFilters.length + activeCities.length}
</Badge>
)}
</Button>
</div>
<div className='absolute top-4 right-4 z-10'>
<Button
variant='default'
size='sm'
className='border border-gray-200 bg-white text-gray-800 shadow-md hover:bg-gray-100'
onClick={() => setIsStationListOpen(true)}
>
<List className='h-4 w-4 text-red-600 sm:mr-2' />
<span className='hidden sm:flex'>{t('map.stationsList')}</span>
<Badge className='ml-2 bg-red-600' variant='default'>
{filteredStations.length}
</Badge>
</Button>
</div>
<div className='absolute bottom-4 left-4 rounded-lg bg-white p-3 shadow-lg'>
<div className='flex items-center gap-2 text-sm font-medium'>
<MapPin className='h-5 w-5 text-red-600' />
<span>{t('map.ourStations')}</span>
</div>
<p className='mt-1 text-xs text-gray-500'>
{t('map.totalStations')}: {stations.length}
</p>
</div>
</div>
);
}