aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/components/arrivals
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-12-23 12:59:52 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2025-12-23 13:00:16 +0100
commit87417c313b455ba0dee19708528cc8d0b830a276 (patch)
tree34b7a2d6bb97157a1d35f57be85b8ff6532865d2 /src/frontend/app/components/arrivals
parentbed48c3d7e49b1736d50ce42d92bb6c18cf02504 (diff)
Reimplement real time for Vitrasa
Diffstat (limited to 'src/frontend/app/components/arrivals')
-rw-r--r--src/frontend/app/components/arrivals/ArrivalCard.tsx136
-rw-r--r--src/frontend/app/components/arrivals/ArrivalList.tsx4
2 files changed, 121 insertions, 19 deletions
diff --git a/src/frontend/app/components/arrivals/ArrivalCard.tsx b/src/frontend/app/components/arrivals/ArrivalCard.tsx
index de4fcc7..5cfbaa3 100644
--- a/src/frontend/app/components/arrivals/ArrivalCard.tsx
+++ b/src/frontend/app/components/arrivals/ArrivalCard.tsx
@@ -1,3 +1,4 @@
+import { AlertTriangle, LocateIcon } from "lucide-react";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import LineIcon from "~/components/LineIcon";
@@ -6,15 +7,11 @@ import "./ArrivalCard.css";
interface ArrivalCardProps {
arrival: Arrival;
- reduced?: boolean;
}
-export const ArrivalCard: React.FC<ArrivalCardProps> = ({
- arrival,
- reduced,
-}) => {
+export const ReducedArrivalCard: React.FC<ArrivalCardProps> = ({ arrival }) => {
const { t } = useTranslation();
- const { route, headsign, estimate } = arrival;
+ const { route, headsign, estimate, delay, shift } = arrival;
const etaValue = estimate.minutes.toString();
const etaUnit = t("estimates.minutes", "min");
@@ -32,15 +29,73 @@ export const ArrivalCard: React.FC<ArrivalCardProps> = ({
}
}, [estimate.precision]);
+ const metaChips = useMemo(() => {
+ const chips: Array<{
+ label: string;
+ tone?: string;
+ kind?: "regular" | "gps" | "delay" | "warning";
+ }> = [];
+
+ // Delay chip
+ if (delay) {
+ const delta = Math.round(delay.minutes);
+ const absDelta = Math.abs(delta);
+
+ if (delta === 0) {
+ chips.push({
+ label: "OK",
+ tone: "delay-ok",
+ kind: "delay",
+ });
+ } else if (delta > 0) {
+ const tone =
+ delta <= 2
+ ? "delay-ok"
+ : delta <= 10
+ ? "delay-warn"
+ : "delay-critical";
+ chips.push({
+ label: `R${delta}`,
+ tone,
+ kind: "delay",
+ });
+ } else {
+ const tone = absDelta <= 2 ? "delay-ok" : "delay-early";
+ chips.push({
+ label: `A${absDelta}`,
+ tone,
+ kind: "delay",
+ });
+ }
+ }
+
+ // Shift chip
+ if (shift) {
+ chips.push({
+ label: `${shift.shiftName} ยท ${shift.shiftTrip}`,
+ kind: "regular",
+ });
+ }
+
+ // Precision chips
+ if (estimate.precision === "unsure") {
+ chips.push({
+ label: "!",
+ tone: "warning",
+ kind: "warning",
+ });
+ } else if (estimate.precision === "confident") {
+ chips.push({
+ label: "", // Just the icon for reduced
+ kind: "gps",
+ });
+ }
+
+ return chips;
+ }, [delay, shift, estimate.precision]);
+
return (
- <div
- 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
- ${reduced ? "reduced" : ""}
- `.trim()}
- >
+ <div className="flex-none flex items-center gap-2.5 min-h-12 rounded px-3 py-2.5 transition-all bg-slate-50 dark:bg-slate-800 shadow-sm">
<div className="shrink-0 min-w-[7ch]">
<LineIcon
line={route.shortName}
@@ -50,11 +105,58 @@ export const ArrivalCard: React.FC<ArrivalCardProps> = ({
/>
</div>
<div className="flex-1 min-w-0 flex flex-col gap-1">
- <strong
- className={`text-base overflow-hidden text-ellipsis line-clamp-2 leading-tight ${estimate.precision == "past" ? "line-through" : ""}`}
+ <span
+ className={`text-base font-medium overflow-hidden text-ellipsis line-clamp-2 leading-tight ${estimate.precision == "past" ? "line-through" : ""}`}
>
{headsign.destination}
- </strong>
+ </span>
+ {metaChips.length > 0 && (
+ <div className="flex items-center gap-1 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-slate-600 dark:text-slate-400";
+ }
+
+ return (
+ <span
+ key={`${chip.label}-${idx}`}
+ className={`text-xs px-2.5 py-0.5 rounded-full flex items-center justify-center gap-1 shrink-0 font-medium tracking-wide ${chipColourClasses}`}
+ >
+ {chip.kind === "gps" && (
+ <LocateIcon className="w-3 h-3 my-0.5 inline-block" />
+ )}
+ {chip.kind === "warning" && (
+ <AlertTriangle className="w-3 h-3 my-0.5 inline-block" />
+ )}
+ {chip.label}
+ </span>
+ );
+ })}
+ </div>
+ )}
</div>
<div
className={`
diff --git a/src/frontend/app/components/arrivals/ArrivalList.tsx b/src/frontend/app/components/arrivals/ArrivalList.tsx
index a1210d5..b2394fb 100644
--- a/src/frontend/app/components/arrivals/ArrivalList.tsx
+++ b/src/frontend/app/components/arrivals/ArrivalList.tsx
@@ -1,6 +1,6 @@
import React from "react";
import { type Arrival } from "../../api/schema";
-import { ArrivalCard } from "./ArrivalCard";
+import { ReducedArrivalCard } from "./ArrivalCard";
interface ArrivalListProps {
arrivals: Arrival[];
@@ -14,7 +14,7 @@ export const ArrivalList: React.FC<ArrivalListProps> = ({
return (
<div className="flex flex-col gap-3">
{arrivals.map((arrival, index) => (
- <ArrivalCard
+ <ReducedArrivalCard
key={`${arrival.route.shortName}-${index}`}
arrival={arrival}
reduced={reduced}