From 3ebb062e99dbd8a63d5642d67ba4be753e61a34d Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Wed, 19 Nov 2025 22:58:40 +0100 Subject: feat: Enhance map attribution feature; improve styles and add toggle functionality in StopMapSheet component --- .../Stops/ConsolidatedCirculationCard.tsx | 141 +++++++++++-------- .../Stops/ConsolidatedCirculationList.css | 153 ++++++++++++--------- 2 files changed, 167 insertions(+), 127 deletions(-) (limited to 'src/frontend/app/components/Stops') diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx index 9733d89..f725b8c 100644 --- a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx @@ -1,4 +1,4 @@ -import { Clock } from "lucide-react"; +import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { type RegionConfig } from "~/config/RegionConfig"; import LineIcon from "~components/LineIcon"; @@ -88,25 +88,8 @@ export const ConsolidatedCirculationCard: React.FC< const formatDistance = (meters: number) => { if (meters > 1024) { return `${(meters / 1000).toFixed(1)} km`; - } else { - return `${meters} ${t("estimates.meters", "m")}`; - } - }; - - const getDelayText = (estimate: ConsolidatedCirculation): string | null => { - if (!estimate.schedule || !estimate.realTime) { - return null; - } - - const delay = estimate.realTime.minutes - estimate.schedule.minutes; - - if (delay >= -1 && delay <= 2) { - return "OK"; - } else if (delay > 2) { - return "R" + delay; - } else { - return "A" + Math.abs(delay); } + return `${meters} ${t("estimates.meters", "m")}`; }; const getTripIdDisplay = (tripId: string): string => { @@ -114,67 +97,105 @@ export const ConsolidatedCirculationCard: React.FC< return parts.length > 1 ? parts[1] : tripId; }; - const getTimeClass = (estimate: ConsolidatedCirculation): string => { + const etaMinutes = + estimate.realTime?.minutes ?? estimate.schedule?.minutes ?? null; + const etaValue = + etaMinutes === null ? "--" : Math.max(0, Math.round(etaMinutes)).toString(); + const 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"; - } else if (estimate.realTime && !estimate.schedule?.running) { + } + if (estimate.realTime && !estimate.schedule?.running) { return "time-delayed"; } - return "time-scheduled"; - }; + }, [estimate.realTime, estimate.schedule]); - const displayMinutes = - estimate.realTime?.minutes ?? estimate.schedule?.minutes ?? 0; - const timeClass = getTimeClass(estimate); - const delayText = getDelayText(estimate); + 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); + + if (delta === 0) { + return { + label: t("estimates.delay_on_time", "En hora (0 min)"), + tone: "delay-ok", + } as const; + } + + if (delta > 0) { + const tone = delta <= 2 ? "delay-ok" : delta <= 10 ? "delay-warn" : "delay-critical"; + return { + label: t("estimates.delay_positive", "Retraso de {{minutes}} min", { + minutes: delta, + }), + tone, + } as const; + } + + const tone = absDelta <= 2 ? "delay-ok" : "delay-early"; + return { + label: t("estimates.delay_negative", "Adelanto de {{minutes}} min", { + minutes: absDelta, + }), + tone, + } as const; + }, [estimate.schedule, estimate.realTime, t]); + + const metaChips = useMemo(() => { + const chips: Array<{ label: string; tone?: string }> = []; + if (delayChip) { + chips.push(delayChip); + } + if (estimate.schedule) { + chips.push({ + label: `${parseServiceId(estimate.schedule.serviceId)} ยท ${getTripIdDisplay( + estimate.schedule.tripId + )}`, + }); + } + if (estimate.realTime && estimate.realTime.distance >= 0) { + chips.push({ label: formatDistance(estimate.realTime.distance) }); + } + return chips; + }, [delayChip, estimate.schedule, estimate.realTime]); return (
-
+
- +
-
{estimate.route}
- -
-
- - {estimate.realTime - ? `${displayMinutes} ${t("estimates.minutes", "min")}` - : absoluteArrivalTime(displayMinutes)} -
-
- {estimate.schedule && ( - <> - {parseServiceId(estimate.schedule.serviceId)} ( - {getTripIdDisplay(estimate.schedule.tripId)}) - - )} - - {estimate.schedule && - estimate.realTime && - estimate.realTime.distance >= 0 && <> · } - - {estimate.realTime && estimate.realTime.distance >= 0 && ( - <>{formatDistance(estimate.realTime.distance)} - )} - - {estimate.schedule && - estimate.realTime && - estimate.realTime.distance >= 0 && <> · } - - {delayText} +
+
+ {etaValue} + {etaUnit}
+ {metaChips.length > 0 && ( +
+ {metaChips.map((chip, idx) => ( + + {chip.label} + + ))} +
+ )}
); }; diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationList.css b/src/frontend/app/components/Stops/ConsolidatedCirculationList.css index b79fc73..7e757fb 100644 --- a/src/frontend/app/components/Stops/ConsolidatedCirculationList.css +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationList.css @@ -14,25 +14,29 @@ .consolidated-circulation-card { flex: 0 0 auto; - display: flex; flex-direction: column; + gap: 0.5rem; background-color: var(--message-background-color); - border-radius: 8px; + border-radius: 12px; border: 1px solid var(--border-color); - overflow: hidden; + padding: 0.65rem 0.85rem; transition: box-shadow 0.2s ease; } .consolidated-circulation-card:hover { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); } -.consolidated-circulation-card .card-header { + +.consolidated-circulation-card .card-row { display: flex; align-items: center; - gap: 0.75rem; - padding: 0.875rem 1rem; + gap: 0.65rem; +} + +.consolidated-circulation-card .card-row.main { + min-height: 48px; } .consolidated-circulation-card .line-info { @@ -45,101 +49,116 @@ } .consolidated-circulation-card .route-info strong { - font-size: 0.95rem; + font-size: 1rem; color: var(--text-color); overflow: hidden; text-overflow: ellipsis; - white-space: nowrap; - display: block; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + line-height: 1.25; } -.consolidated-circulation-card .time-info { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 0.25rem; - flex-shrink: 0; +.consolidated-circulation-card .eta-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.3rem 0.45rem; + border-radius: 12px; } -.consolidated-circulation-card .arrival-time { +.consolidated-circulation-card .eta-text { display: flex; + flex-direction: column; align-items: center; - gap: 0.4rem; - font-size: 1.05rem; - font-weight: 600; + line-height: 1; } -.consolidated-circulation-card .arrival-time svg { - width: 18px; - height: 18px; - flex-shrink: 0; +.consolidated-circulation-card .eta-value { + font-size: 1.15rem; + font-weight: 700; } -/* Time color states */ -.consolidated-circulation-card .arrival-time.time-running, -.consolidated-circulation-card .arrival-time.time-running svg { - color: #22c55e; +.consolidated-circulation-card .eta-unit { + font-size: 0.65rem; + text-transform: uppercase; + letter-spacing: 0.08em; } -.consolidated-circulation-card .arrival-time.time-delayed, -.consolidated-circulation-card .arrival-time.time-delayed svg { - color: #ff6a00; +.consolidated-circulation-card .eta-badge.time-running { + background: rgba(34, 197, 94, 0.12); + color: #1a9e56; } -.consolidated-circulation-card .arrival-time.time-scheduled, -.consolidated-circulation-card .arrival-time.time-scheduled svg { - color: #0b3d91; /* dark blue */ +.consolidated-circulation-card .eta-badge.time-delayed { + background: rgba(255, 106, 0, 0.12); + color: #d06100; } -[data-theme="dark"] .consolidated-circulation-card .arrival-time.time-scheduled, -[data-theme="dark"] - .consolidated-circulation-card - .arrival-time.time-scheduled - svg { - color: #8fb4ff; /* lighten for dark backgrounds */ +.consolidated-circulation-card .eta-badge.time-scheduled { + background: rgba(11, 61, 145, 0.12); + color: #0b3d91; } -.consolidated-circulation-card .distance-info { - font-size: 0.75rem; - color: var(--subtitle-color); - text-align: right; +[data-theme="dark"] .consolidated-circulation-card .eta-badge.time-scheduled { + color: #8fb4ff; +} + +.consolidated-circulation-card .card-row.meta { + flex-wrap: wrap; + justify-content: flex-start; + gap: 0.4rem; } -.consolidated-circulation-card .card-footer { - padding: 0.5rem 1rem 0.75rem 1rem; - border-top: 1px solid var(--border-color); - background-color: rgba(0, 0, 0, 0.02); +.meta-chip { + font-size: 0.75rem; + padding: 0.2rem 0.55rem; + border-radius: 999px; + background: rgba(0, 0, 0, 0.03); + color: var(--subtitle-color); + border: 1px solid var(--border-color); } @media (prefers-color-scheme: dark) { - .consolidated-circulation-card .card-footer { - background-color: rgba(255, 255, 255, 0.03); + .meta-chip { + background: rgba(255, 255, 255, 0.05); } } -.consolidated-circulation-card .status-text { - font-size: 0.8rem; - color: var(--subtitle-color); - line-height: 1.4; - display: block; +.meta-chip.delay-ok { + background: rgba(34, 197, 94, 0.15); + border-color: rgba(34, 197, 94, 0.3); + color: #1a9e56; } -/* Responsive adjustments */ -@media (max-width: 480px) { - .consolidated-circulation-card .card-header { - gap: 0.5rem; - padding: 0.5rem 0.65rem; - } +.meta-chip.delay-warn { + background: rgba(251, 191, 36, 0.25); + border-color: rgba(251, 191, 36, 0.5); + color: #fbbf24; +} + +.meta-chip.delay-critical { + background: rgba(239, 68, 68, 0.2); + border-color: rgba(239, 68, 68, 0.35); + color: #b91c1c; +} - .consolidated-circulation-card .arrival-time { - font-size: 1rem; +.meta-chip.delay-early { + background: rgba(59, 130, 246, 0.17); + border-color: rgba(59, 130, 246, 0.3); + color: #1d4ed8; +} + +@media (max-width: 480px) { + .consolidated-circulation-card { + padding: 0.65rem 0.75rem; } - .consolidated-circulation-card .card-footer { - padding: 0.5rem 0.875rem 0.625rem 0.875rem; + .consolidated-circulation-card .card-row { + gap: 0.5rem; } - .consolidated-circulation-card .status-text { - font-size: 0.9rem; + .consolidated-circulation-card .eta-badge { + padding: 0.25rem 0.4rem; } } -- cgit v1.3