'use client' import { useEffect, useState } from 'react' import { motion, AnimatePresence } from 'framer-motion' import { ArrowRight, Train } from 'lucide-react' interface Arrival { route: string minutes: number park: string wheelchair: boolean } interface Direction { stopId: string short: string sub: string } const DIRECTIONS: Direction[] = [ { stopId: '16226', short: 'Лента', sub: 'в центр' }, { stopId: '16354', short: 'Дыбенко', sub: 'от центра' }, ] const ROUTES: { num: string; color: string; bg: string }[] = [ { num: '23', color: '#60a5fa', bg: 'linear-gradient(135deg, #3b82f6, #2563eb)' }, { num: '27', color: '#fbbf24', bg: 'linear-gradient(135deg, #f59e0b, #d97706)' }, { num: '39', color: '#c084fc', bg: 'linear-gradient(135deg, #a855f7, #7c3aed)' }, ] function formatMinutes(m: number): string { if (m <= 0) return 'сейчас' return `${m} мин` } function Cell({ arrivals, color }: { arrivals: Arrival[]; color: string }) { const sorted = [...arrivals].sort((a, b) => a.minutes - b.minutes).slice(0, 3) if (sorted.length === 0) { return (
) } const [first, ...rest] = sorted const imminent = first.minutes <= 2 return (
{/* Primary time */}
{first.minutes <= 0 ? 'сейчас' : first.minutes}
{first.minutes > 0 && (
мин
)}
{/* Divider */} {rest.length > 0 && (
)} {/* Next arrivals */} {rest.length > 0 && (
затем
{rest.map(r => formatMinutes(r.minutes)).join(' · ')}
)}
) } export default function TransportWidget() { const [data, setData] = useState>({}) const [loading, setLoading] = useState(true) useEffect(() => { let cancelled = false const load = async () => { try { const results = await Promise.all( DIRECTIONS.map(d => fetch(`/api/transport?stopId=${d.stopId}`) .then(r => r.json()) .then(j => ({ stopId: d.stopId, arrivals: (j.arrivals as Arrival[]) || [] })) .catch(() => ({ stopId: d.stopId, arrivals: [] as Arrival[] })) ) ) if (cancelled) return const map: Record = {} for (const r of results) map[r.stopId] = r.arrivals setData(map) } finally { if (!cancelled) setLoading(false) } } load() const t = setInterval(load, 30_000) return () => { cancelled = true; clearInterval(t) } }, []) return (
{/* Glow */}
{/* Header */}
Трамвай
Ул. Антонова-Овсеенко
{/* Column headers */}
{DIRECTIONS.map(d => (
{d.short}
{d.sub}
))}
{/* Rows: one per route */}
{ROUTES.map(route => (
{/* Route badge */}
{route.num}
{DIRECTIONS.map(d => { const arrivals = (data[d.stopId] || []).filter(a => a.route === route.num) return ( a.minutes).join(',')}`} initial={{ opacity: 0, y: 4 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.2 }} > ) })}
))}
{loading && Object.keys(data).length === 0 && (
Загрузка расписания...
)}
) }