diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-12-24 17:53:32 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-12-24 17:53:32 +0100 |
| commit | 9ed46bea58dbb81ceada2a957fd1db653fb21e52 (patch) | |
| tree | f1cb09ad15571abbfae1c59105952330ec3a0502 /src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx | |
| parent | 4a866f5352a51916ddb9849b2d68213856196c9c (diff) | |
Implement new UI styles
Diffstat (limited to 'src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx')
| -rw-r--r-- | src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx | 467 |
1 files changed, 0 insertions, 467 deletions
diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx deleted file mode 100644 index 679345f..0000000 --- a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx +++ /dev/null @@ -1,467 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from "react"; -import Marquee from "react-fast-marquee"; -import { useTranslation } from "react-i18next"; -import LineIcon from "~components/LineIcon"; -import { type ConsolidatedCirculation } from "~routes/stops-$id"; - -import { AlertTriangle, LocateIcon } from "lucide-react"; -import "./ConsolidatedCirculationCard.css"; - -interface ConsolidatedCirculationCardProps { - estimate: ConsolidatedCirculation; - onMapClick?: () => void; - readonly?: boolean; - reduced?: boolean; - driver?: string; -} - -// Utility function to parse service ID and get the turn number -const parseServiceId = (serviceId: string): string => { - const parts = serviceId.split("_"); - if (parts.length === 0) return ""; - - const lastPart = parts[parts.length - 1]; - if (lastPart.length < 6) return ""; - - const last6 = lastPart.slice(-6); - const lineCode = last6.slice(0, 3); - const turnCode = last6.slice(-3); - - // Remove leading zeros from turn - const turnNumber = parseInt(turnCode, 10).toString(); - - // Parse line number with special cases - const lineNumber = parseInt(lineCode, 10); - let displayLine: string; - - switch (lineNumber) { - case 1: - displayLine = "C1"; - break; - case 3: - displayLine = "C3"; - break; - case 30: - displayLine = "N1"; - break; - case 33: - displayLine = "N4"; - break; - case 8: - displayLine = "A"; - break; - case 101: - displayLine = "H"; - break; - case 150: - displayLine = "REF"; - break; - case 500: - displayLine = "TUR"; - break; - case 201: - displayLine = "U1"; - break; - case 202: - displayLine = "U2"; - break; - default: - displayLine = `L${lineNumber}`; - } - - return `${displayLine}-${turnNumber}`; -}; - -const AutoMarquee = ({ text }: { text: string }) => { - const containerRef = useRef<HTMLDivElement>(null); - const [shouldScroll, setShouldScroll] = useState(false); - - useEffect(() => { - const el = containerRef.current; - if (!el) return; - - const checkScroll = () => { - // 9px per char for text-sm font-mono is a safe upper bound estimate - // (14px * 0.6 = 8.4px) - const charWidth = 9; - const availableWidth = el.offsetWidth; - const textWidth = text.length * charWidth; - - setShouldScroll(textWidth > availableWidth); - }; - - checkScroll(); - - const observer = new ResizeObserver(checkScroll); - observer.observe(el); - - return () => observer.disconnect(); - }, [text]); - - if (shouldScroll) { - return ( - <div ref={containerRef} className="w-full overflow-hidden"> - <Marquee speed={60} gradient={false}> - <div className="mr-64 text-sm font-mono">{text}</div> - </Marquee> - </div> - ); - } - - return ( - <div - ref={containerRef} - className="w-full overflow-hidden text-sm font-mono truncate" - > - {text} - </div> - ); -}; - -export const ConsolidatedCirculationCard: React.FC< - ConsolidatedCirculationCardProps -> = ({ estimate, onMapClick, readonly, reduced, driver }) => { - const { t } = useTranslation(); - - const formatDistance = (meters: number) => { - if (meters > 1024) { - return `${(meters / 1000).toFixed(1)} km`; - } - return `${meters} ${t("estimates.meters", "m")}`; - }; - - const getTripIdDisplay = (tripId: string): string => { - const parts = tripId.split("_"); - return parts.length > 1 ? parts[1] : tripId; - }; - - const etaMinutes = - estimate.realTime?.minutes ?? estimate.schedule?.minutes ?? null; - - let etaValue: string; - let etaUnit: string; - - if (etaMinutes === null) { - etaValue = "--"; - etaUnit = t("estimates.minutes", "min"); - } else { - const isRenfe = driver === "renfe"; - const isLongWait = etaMinutes > 60; - - if (isRenfe || isLongWait) { - const now = new Date(); - const arrivalTime = new Date(now.getTime() + etaMinutes * 60 * 1000); - etaValue = arrivalTime.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - hour12: false, - }); - etaUnit = ""; - } else { - etaValue = Math.max(0, Math.round(etaMinutes)).toString(); - etaUnit = t("estimates.minutes", "min"); - } - } - - const timeClass = useMemo(() => { - if (estimate.realTime && estimate.schedule?.running) { - return "time-running"; - } - if (estimate.realTime && !estimate.schedule) { - return "time-running"; - } - if (estimate.realTime && !estimate.schedule?.running) { - return "time-delayed"; - } - return "time-scheduled"; - }, [estimate.realTime, estimate.schedule]); - - const delayChip = useMemo(() => { - if (!estimate.schedule || !estimate.realTime) { - return null; - } - - const delta = Math.round( - estimate.realTime.minutes - estimate.schedule.minutes - ); - const absDelta = Math.abs(delta); - - // On time - if (delta === 0) { - return { - label: reduced ? "OK" : t("estimates.delay_on_time"), - tone: "delay-ok", - kind: "delay", - } as const; - } - - // Delayed - if (delta > 0) { - const tone = - delta <= 2 ? "delay-ok" : delta <= 10 ? "delay-warn" : "delay-critical"; - return { - label: reduced - ? `R${delta}` - : t("estimates.delay_positive", { - minutes: delta, - }), - tone, - kind: "delay", - } as const; - } - - // Early - const tone = absDelta <= 2 ? "delay-ok" : "delay-early"; - return { - label: reduced - ? `A${absDelta}` - : t("estimates.delay_negative", { - minutes: absDelta, - }), - tone, - kind: "delay", - } as const; - }, [estimate.schedule, estimate.realTime, t, reduced]); - - const metaChips = useMemo(() => { - const chips: Array<{ - label: string; - tone?: string; - kind?: "regular" | "gps" | "delay" | "warning"; - }> = []; - - if (delayChip) { - chips.push(delayChip); - } - - if (estimate.schedule && driver !== "renfe") { - chips.push({ - label: `${parseServiceId(estimate.schedule.serviceId)} · ${getTripIdDisplay( - estimate.schedule.tripId - )}`, - kind: "regular", - }); - } - - if (estimate.realTime && estimate.realTime.distance >= 0) { - chips.push({ - label: formatDistance(estimate.realTime.distance), - kind: "regular", - }); - } - - if (!reduced) { - if (estimate.currentPosition) { - if (estimate.isPreviousTrip) { - chips.push({ label: t("estimates.previous_trip"), kind: "gps" }); - } else { - chips.push({ label: t("estimates.bus_gps_position"), kind: "gps" }); - } - } - - if (driver !== "renfe") { - if (timeClass === "time-delayed") { - chips.push({ - label: reduced ? "!" : t("estimates.low_accuracy"), - tone: "warning", - kind: "warning", - }); - } - - if (timeClass === "time-scheduled") { - chips.push({ - label: reduced ? "⧗" : t("estimates.no_realtime"), - tone: "warning", - kind: "warning", - }); - } - } - } - - return chips; - }, [delayChip, estimate.schedule, estimate.realTime, timeClass, t, reduced]); - - // Check if bus has GPS position (live tracking) - const hasGpsPosition = !!estimate.currentPosition; - const isRenfe = driver === "renfe"; - const isClickable = hasGpsPosition; - const looksDisabled = !isClickable && !isRenfe; - - const Tag = readonly ? "div" : "button"; - const interactiveProps = readonly - ? {} - : { - onClick: isClickable ? onMapClick : undefined, - type: "button" as const, - disabled: !isClickable, - }; - - if (reduced) { - return ( - <Tag - className={` - flex-none flex items-center gap-2.5 min-h-12 - bg-(--message-background-color) border border-(--border-color) - rounded-xl px-3 py-2.5 transition-all - ${ - readonly - ? looksDisabled - ? "opacity-70 cursor-not-allowed" - : "" - : isClickable - ? "cursor-pointer hover:shadow-[0_4px_14px_rgba(0,0,0,0.08)] hover:border-(--button-background-color) hover:bg-[color-mix(in_oklab,var(--button-background-color)_5%,var(--message-background-color))] active:scale-[0.98]" - : looksDisabled - ? "opacity-70 cursor-not-allowed" - : "" - } - `.trim()} - {...interactiveProps} - > - <div className="shrink-0 min-w-[7ch]"> - <LineIcon line={estimate.line} mode="pill" /> - </div> - <div className="flex-1 min-w-0 flex flex-col gap-1"> - <strong className="text-base text-(--text-color) overflow-hidden text-ellipsis line-clamp-2 leading-tight"> - {driver === "renfe" && estimate.schedule?.tripId && ( - <span className="font-mono text-slate-500 mr-1.5 text-sm"> - {estimate.schedule.tripId} - </span> - )} - {driver === "renfe" ? estimate.route.toUpperCase() : estimate.route} - </strong> - {metaChips.length > 0 && ( - <div className="flex items-center gap-1.5 flex-wrap"> - {metaChips.map((chip, idx) => { - let chipColourClasses = ""; - switch (chip.tone) { - case "delay-ok": - chipColourClasses = - "bg-green-600/20 dark:bg-green-600/30 text-green-700 dark:text-green-300"; - break; - case "delay-warn": - chipColourClasses = - "bg-amber-600/20 dark:bg-yellow-600/30 text-amber-700 dark:text-yellow-300"; - break; - case "delay-critical": - chipColourClasses = - "bg-red-400/20 dark:bg-red-600/30 text-red-600 dark:text-red-300"; - break; - case "delay-early": - chipColourClasses = - "bg-blue-400/20 dark:bg-blue-600/30 text-blue-700 dark:text-blue-300"; - break; - case "warning": - chipColourClasses = - "bg-orange-400/20 dark:bg-orange-600/30 text-orange-700 dark:text-orange-300"; - break; - default: - chipColourClasses = - "bg-black/[0.06] dark:bg-white/[0.12] text-[var(--text-color)]"; - } - - return ( - <span - key={`${chip.label}-${idx}`} - className={`text-xs px-2 py-0.5 rounded-full flex items-center justify-center gap-1 shrink-0 ${chipColourClasses}`} - > - {chip.kind === "gps" && ( - <LocateIcon className="w-3 h-3 inline-block" /> - )} - {chip.kind === "warning" && ( - <AlertTriangle className="w-3 h-3 inline-block" /> - )} - {chip.label} - </span> - ); - })} - </div> - )} - </div> - <div - className={` - inline-flex items-center justify-center px-2 py-1.5 rounded-xl shrink-0 - ${ - timeClass === "time-running" - ? "bg-green-600/20 dark:bg-green-600/25 text-[#1a9e56] dark:text-[#22c55e]" - : timeClass === "time-delayed" - ? "bg-orange-600/20 dark:bg-orange-600/25 text-[#d06100] dark:text-[#fb923c]" - : "bg-blue-900/20 dark:bg-blue-600/25 text-[#0b3d91] dark:text-[#93c5fd]" - } - `.trim()} - > - <div className="flex flex-col items-center leading-none"> - <span className="text-lg font-bold leading-none">{etaValue}</span> - <span className="text-[0.65rem] uppercase tracking-wider mt-0.5 opacity-90"> - {etaUnit} - </span> - </div> - </div> - </Tag> - ); - } - - return ( - <Tag - className={`consolidated-circulation-card ${ - readonly - ? looksDisabled - ? "no-gps" - : "" - : isClickable - ? "has-gps" - : looksDisabled - ? "no-gps" - : "" - }`} - {...interactiveProps} - > - <> - <div className="card-row main"> - <div className="line-info"> - <LineIcon line={estimate.line} mode="pill" /> - </div> - <div className="route-info"> - <strong> - {driver === "renfe" && estimate.schedule?.tripId && ( - <span className="font-mono text-slate-500 mr-2 text-[0.9em]"> - {estimate.schedule.tripId} - </span> - )} - {driver === "renfe" - ? estimate.route.toUpperCase() - : estimate.route} - </strong> - {estimate.nextStreets && estimate.nextStreets.length > 0 && ( - <AutoMarquee text={estimate.nextStreets.join(" — ")} /> - )} - </div> - <div className={`eta-badge ${timeClass}`}> - <div className="eta-text"> - <span className="eta-value">{etaValue}</span> - <span className="eta-unit">{etaUnit}</span> - </div> - </div> - </div> - - {metaChips.length > 0 && ( - <div className="card-row meta"> - {metaChips.map((chip, idx) => ( - <span - key={`${chip.label}-${idx}`} - className={`meta-chip ${chip.tone ?? ""}`.trim()} - > - {chip.kind === "gps" && ( - <LocateIcon className="w-3 h-3 inline-block" /> - )} - {chip.kind === "warning" && ( - <AlertTriangle className="w-3 h-3 inline-block" /> - )} - {chip.label} - </span> - ))} - </div> - )} - </> - </Tag> - ); -}; |
