aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/components
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2026-04-04 15:44:41 +0200
committerAriel Costas Guerrero <ariel@costas.dev>2026-04-04 15:44:41 +0200
commit97908d274ee12eb2301fadd5fc445d0f79479a56 (patch)
tree04eee0ad547cc68047011dea82549dcad4a0d0d8 /src/frontend/app/components
parent1b4f4a674ac533c0b51260ba35ab91dd2cf9486d (diff)
Enhance arrival and transit functionality with new vehicle operation logic and transit kind classification
Diffstat (limited to 'src/frontend/app/components')
-rw-r--r--src/frontend/app/components/arrivals/ArrivalCard.tsx122
-rw-r--r--src/frontend/app/components/arrivals/ReducedArrivalCard.tsx46
2 files changed, 121 insertions, 47 deletions
diff --git a/src/frontend/app/components/arrivals/ArrivalCard.tsx b/src/frontend/app/components/arrivals/ArrivalCard.tsx
index ec14492..827599e 100644
--- a/src/frontend/app/components/arrivals/ArrivalCard.tsx
+++ b/src/frontend/app/components/arrivals/ArrivalCard.tsx
@@ -1,4 +1,11 @@
-import { AlertTriangle, BusFront, LocateIcon, Navigation } from "lucide-react";
+import {
+ AlertTriangle,
+ ArrowDownRightSquare,
+ ArrowUpRightSquare,
+ BusFront,
+ LocateIcon,
+ Navigation,
+} from "lucide-react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import Marquee from "react-fast-marquee";
import { useTranslation } from "react-i18next";
@@ -71,10 +78,10 @@ export const ArrivalCard: React.FC<ArrivalCardProps> = ({
shift,
vehicleInformation,
operator,
+ operation,
} = arrival;
const etaValue = estimate.minutes.toString();
- const etaUnit = t("estimates.minutes", "min");
const timeClass = useMemo(() => {
switch (estimate.precision) {
@@ -93,9 +100,27 @@ export const ArrivalCard: React.FC<ArrivalCardProps> = ({
const chips: Array<{
label: string;
tone?: string;
- kind?: "regular" | "gps" | "delay" | "warning" | "vehicle";
+ kind?:
+ | "regular"
+ | "gps"
+ | "delay"
+ | "warning"
+ | "vehicle"
+ | "pickup"
+ | "dropoff";
}> = [];
+ if (operation !== "pickup_dropoff") {
+ chips.push({
+ label:
+ operation === "pickup_only"
+ ? t("journey.pickup_only", "Solo subida")
+ : t("journey.dropoff_only", "Solo bajada"),
+ tone: operation === "pickup_only" ? "pickup" : "dropoff",
+ kind: operation === "pickup_only" ? "pickup" : "dropoff",
+ });
+ }
+
// Badge/Shift info as a chip
if (headsign.badge) {
chips.push({
@@ -154,14 +179,6 @@ export const ArrivalCard: React.FC<ArrivalCardProps> = ({
});
}
- if (estimate.precision === "scheduled") {
- chips.push({
- label: t("estimates.no_realtime"),
- tone: "warning",
- kind: "warning",
- });
- }
-
// Vehicle information if available
if (vehicleInformation) {
let label = vehicleInformation.identifier;
@@ -240,6 +257,14 @@ export const ArrivalCard: React.FC<ArrivalCardProps> = ({
{metaChips.map((chip, idx) => {
let chipColourClasses = "";
switch (chip.tone) {
+ case "pickup":
+ chipColourClasses =
+ "bg-green-600/10 dark:bg-green-600/20 text-green-700 dark:text-green-300";
+ break;
+ case "dropoff":
+ chipColourClasses =
+ "bg-orange-400/10 dark:bg-orange-600/20 text-orange-700 dark:text-orange-300";
+ break;
case "delay-ok":
chipColourClasses =
"bg-green-600/10 dark:bg-green-600/20 text-green-700 dark:text-green-300";
@@ -279,47 +304,56 @@ export const ArrivalCard: React.FC<ArrivalCardProps> = ({
{chip.kind === "vehicle" && (
<BusFront className="w-3 h-3 inline-block" />
)}
+ {chip.kind === "pickup" && (
+ <ArrowUpRightSquare className="w-3 h-3 inline-block" />
+ )}
+ {chip.kind === "dropoff" && (
+ <ArrowDownRightSquare className="w-3 h-3 inline-block" />
+ )}
+
{chip.label}
</span>
);
})}
- {onTrack && estimate.precision !== "past" && (
- // Use a <span> instead of a <button> here because this element can
- // be rendered inside a <button> (when isClickable=true), and nested
- // <button> elements are invalid HTML.
- <span
- role="button"
- tabIndex={0}
- onClick={(e) => {
- e.stopPropagation();
- onTrack();
- }}
- onKeyDown={(e) => {
- if (e.key === "Enter" || e.key === " ") {
- e.preventDefault();
+ {onTrack &&
+ estimate.precision !== "past" &&
+ estimate.precision !== "scheduled" && (
+ // Use a <span> instead of a <button> here because this element can
+ // be rendered inside a <button> (when isClickable=true), and nested
+ // <button> elements are invalid HTML.
+ <span
+ role="button"
+ tabIndex={0}
+ onClick={(e) => {
e.stopPropagation();
onTrack();
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ e.stopPropagation();
+ onTrack();
+ }
+ }}
+ aria-label={
+ isTracked
+ ? t("journey.stop_tracking", "Detener seguimiento")
+ : t("journey.track_bus", "Seguir este autobús")
}
- }}
- aria-label={
- isTracked
- ? t("journey.stop_tracking", "Detener seguimiento")
- : t("journey.track_bus", "Seguir este autobús")
- }
- aria-pressed={isTracked}
- className={`ml-auto text-xs px-2.5 py-0.5 rounded-full flex items-center gap-1 shrink-0 font-medium tracking-wide transition-colors cursor-pointer select-none ${
- isTracked
- ? "bg-blue-600 text-white hover:bg-blue-700"
- : "bg-black/[0.04] dark:bg-white/[0.08] text-slate-500 dark:text-slate-400 hover:bg-blue-100 dark:hover:bg-blue-900/30 hover:text-blue-600 dark:hover:text-blue-400"
- }`}
- >
- <Navigation className="w-3 h-3" />
- {isTracked
- ? t("journey.tracking", "Siguiendo")
- : t("journey.track", "Seguir")}
- </span>
- )}
+ aria-pressed={isTracked}
+ className={`ml-auto text-xs px-2.5 py-0.5 rounded-full flex items-center gap-1 shrink-0 font-medium tracking-wide transition-colors cursor-pointer select-none ${
+ isTracked
+ ? "bg-blue-600 text-white hover:bg-blue-700"
+ : "bg-black/[0.04] dark:bg-white/[0.08] text-slate-500 dark:text-slate-400 hover:bg-blue-100 dark:hover:bg-blue-900/30 hover:text-blue-600 dark:hover:text-blue-400"
+ }`}
+ >
+ <Navigation className="w-3 h-3" />
+ {isTracked
+ ? t("journey.tracking", "Siguiendo")
+ : t("journey.track", "Seguir")}
+ </span>
+ )}
</div>
</Tag>
);
diff --git a/src/frontend/app/components/arrivals/ReducedArrivalCard.tsx b/src/frontend/app/components/arrivals/ReducedArrivalCard.tsx
index 19cc8d9..6046ffc 100644
--- a/src/frontend/app/components/arrivals/ReducedArrivalCard.tsx
+++ b/src/frontend/app/components/arrivals/ReducedArrivalCard.tsx
@@ -1,4 +1,10 @@
-import { AlertTriangle, BusFront, LocateIcon } from "lucide-react";
+import {
+ AlertTriangle,
+ ArrowDownRightSquare,
+ ArrowUpRightSquare,
+ BusFront,
+ LocateIcon,
+} from "lucide-react";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import RouteIcon from "~/components/RouteIcon";
@@ -23,10 +29,10 @@ export const ReducedArrivalCard: React.FC<ArrivalCardProps> = ({
shift,
vehicleInformation,
operator,
+ operation,
} = arrival;
const etaValue = estimate.minutes.toString();
- const etaUnit = t("estimates.minutes", "min");
const timeClass = useMemo(() => {
switch (estimate.precision) {
@@ -45,9 +51,27 @@ export const ReducedArrivalCard: React.FC<ArrivalCardProps> = ({
const chips: Array<{
label: string;
tone?: string;
- kind?: "regular" | "gps" | "delay" | "warning" | "vehicle";
+ kind?:
+ | "regular"
+ | "gps"
+ | "delay"
+ | "warning"
+ | "vehicle"
+ | "pickup"
+ | "dropoff";
}> = [];
+ if (operation !== "pickup_dropoff") {
+ chips.push({
+ label:
+ operation === "pickup_only"
+ ? t("journey.pickup_only", "Solo subida")
+ : t("journey.dropoff_only", "Solo bajada"),
+ tone: operation === "pickup_only" ? "pickup" : "dropoff",
+ kind: operation === "pickup_only" ? "pickup" : "dropoff",
+ });
+ }
+
if (operator) {
chips.push({
label: operator,
@@ -151,6 +175,7 @@ export const ReducedArrivalCard: React.FC<ArrivalCardProps> = ({
headsign.badge,
vehicleInformation,
operator,
+ operation,
]);
const isClickable = !!onClick && estimate.precision !== "past";
@@ -184,6 +209,14 @@ export const ReducedArrivalCard: React.FC<ArrivalCardProps> = ({
{metaChips.map((chip, idx) => {
let chipColourClasses = "";
switch (chip.tone) {
+ case "pickup":
+ chipColourClasses =
+ "bg-green-600/10 dark:bg-green-600/20 text-green-700 dark:text-green-300";
+ break;
+ case "dropoff":
+ chipColourClasses =
+ "bg-orange-400/10 dark:bg-orange-600/20 text-orange-700 dark:text-orange-300";
+ break;
case "delay-ok":
chipColourClasses =
"bg-green-600/10 dark:bg-green-600/20 text-green-700 dark:text-green-300";
@@ -223,6 +256,13 @@ export const ReducedArrivalCard: React.FC<ArrivalCardProps> = ({
{chip.kind === "vehicle" && (
<BusFront className="w-3 h-3 my-0.5 inline-block" />
)}
+ {/** I tried imitating the tachograph symbols for loading/unloading, but "bottom right" was better distinguished compared to "bottom left" */}
+ {chip.kind === "pickup" && (
+ <ArrowUpRightSquare className="w-3 h-3 my-0.5 inline-block" />
+ )}
+ {chip.kind === "dropoff" && (
+ <ArrowDownRightSquare className="w-3 h-3 my-0.5 inline-block" />
+ )}
{chip.label}
</span>
);