refactor: update SL departure and clock components to respect AlwaysOn mode

This commit is contained in:
cediackermann 2026-04-20 18:27:24 +02:00
parent 28c41577ec
commit f78b058303
4 changed files with 41 additions and 51 deletions

View file

@ -1,17 +1,5 @@
export const LineBadge = ({ designation, mode }: { designation: string; mode: string }) => { export const LineBadge = ({ designation }: { designation: string; mode?: string; groupOfLines?: string }) => (
const getColor = (mode: string) => { <span className="border border-white text-white px-2 py-0.5 font-bold min-w-[56px] text-center text-xl inline-block">
switch (mode) {
case 'METRO': return 'bg-sl-blue';
case 'BUS': return 'bg-sl-red';
case 'TRAM': return 'bg-sl-green';
case 'TRAIN': return 'bg-sl-orange';
default: return 'bg-gray-600';
}
};
return (
<span className={`${getColor(mode)} text-white px-3 py-1 rounded font-bold min-w-[60px] text-center text-xl inline-block`}>
{designation} {designation}
</span> </span>
); );
};

View file

@ -1,30 +1,40 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useAlwaysOn } from '../../contexts/AlwaysOnContext';
export const Clock = () => { export const Clock = () => {
const alwaysOn = useAlwaysOn();
const [time, setTime] = useState(new Date()); const [time, setTime] = useState(new Date());
useEffect(() => { useEffect(() => {
const timer = setInterval(() => setTime(new Date()), 1000); if (!alwaysOn) {
return () => clearInterval(timer); const id = setInterval(() => setTime(new Date()), 1000);
}, []); return () => clearInterval(id);
}
const timeStr = time.toLocaleTimeString('en-GB', { // Sync to the next full minute, then tick every 60 s
hour: '2-digit', let interval: ReturnType<typeof setInterval>;
minute: '2-digit', const now = new Date();
second: '2-digit', const msUntilNext = (60 - now.getSeconds()) * 1000 - now.getMilliseconds();
hour12: false const timeout = setTimeout(() => {
}); setTime(new Date());
interval = setInterval(() => setTime(new Date()), 60_000);
}, msUntilNext);
return () => { clearTimeout(timeout); clearInterval(interval); };
}, [alwaysOn]);
const timeStr = alwaysOn
? time.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', hour12: false })
: time.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
const dateStr = time.toLocaleDateString('en-GB', { const dateStr = time.toLocaleDateString('en-GB', {
weekday: 'long', weekday: 'long', day: 'numeric', month: 'long',
day: 'numeric',
month: 'long'
}); });
return ( return (
<div className="flex flex-col"> <div className="flex items-baseline gap-6">
<div className="text-8xl font-bold tracking-tight">{timeStr}</div> <div className={`text-7xl ${alwaysOn ? 'font-light' : 'font-bold'}`}>{timeStr}</div>
<div className="text-2xl text-gray-400 mt-2 font-medium">{dateStr}</div> <div className="text-2xl text-gray-400">{dateStr}</div>
</div> </div>
); );
}; };

View file

@ -1,20 +1,20 @@
import { LineBadge } from '../atoms/LineBadge'; import { LineBadge } from '../atoms/LineBadge';
import { Departure } from '../../types'; import { Departure } from '../../types';
import { useAlwaysOn } from '../../contexts/AlwaysOnContext';
export const DepartureItem = ({ departure }: { departure: Departure }) => { export const DepartureItem = ({ departure }: { departure: Departure }) => {
const alwaysOn = useAlwaysOn();
const depTime = new Date(departure.scheduled); const depTime = new Date(departure.scheduled);
const diffMin = Math.round((depTime.getTime() - Date.now()) / 60000); const diffMin = Math.round((depTime.getTime() - Date.now()) / 60000);
const timeText = diffMin <= 0 ? 'Now' : `${diffMin} min`; const timeText = diffMin <= 0 ? 'Now' : `${diffMin} min`;
return ( return (
<div className="flex justify-between items-center px-5 py-4 bg-surface rounded-xl text-3xl font-medium transition hover:bg-zinc-800"> <div className={`flex justify-between items-center py-3 border-b border-zinc-700 text-2xl ${alwaysOn ? 'font-light' : ''}`}>
<div className="flex items-center gap-6"> <div className="flex items-center gap-4">
<LineBadge designation={departure.line.designation} mode={departure.line.transport_mode} /> <LineBadge designation={departure.line.designation} mode={departure.line.transport_mode} groupOfLines={departure.line.group_of_lines} />
<span className="truncate max-w-[45vw]">{departure.destination}</span> <span>{departure.destination}</span>
</div> </div>
<span className="font-bold text-sl-green whitespace-nowrap ml-6"> <span className={alwaysOn ? 'font-normal' : 'font-bold'}>{timeText}</span>
{timeText}
</span>
</div> </div>
); );
}; };

View file

@ -2,19 +2,11 @@ import { DepartureItem } from '../molecules/DepartureItem';
import { Departure } from '../../types'; import { Departure } from '../../types';
export const DepartureList = ({ departures, loading }: { departures: Departure[]; loading: boolean }) => { export const DepartureList = ({ departures, loading }: { departures: Departure[]; loading: boolean }) => {
if (loading && departures.length === 0) return <p className="text-gray-500 text-xl mt-4">Loading...</p>;
if (!loading && departures.length === 0) return <p className="text-gray-500 text-xl mt-4">No departures.</p>;
return ( return (
<div className="flex flex-col gap-4 mt-8"> <div className="flex-1 overflow-hidden">
{loading && departures.length === 0 && ( {departures.map((dep, i) => (
<div className="flex justify-center p-12 text-gray-500 text-2xl animate-pulse">
Fetching departures...
</div>
)}
{!loading && departures.length === 0 && (
<div className="flex justify-center p-12 text-gray-400 text-2xl bg-surface rounded-xl">
No upcoming departures found.
</div>
)}
{departures.slice(0, 5).map((dep, i) => (
<DepartureItem key={`${dep.line.designation}-${i}`} departure={dep} /> <DepartureItem key={`${dep.line.designation}-${i}`} departure={dep} />
))} ))}
</div> </div>