aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend/app')
-rw-r--r--src/frontend/app/api/schema.ts1
-rw-r--r--src/frontend/app/components/arrivals/ArrivalCard.tsx122
-rw-r--r--src/frontend/app/components/arrivals/ReducedArrivalCard.tsx46
-rw-r--r--src/frontend/app/routes/home.tsx2
-rw-r--r--src/frontend/app/routes/stops-$id.tsx3
5 files changed, 125 insertions, 49 deletions
diff --git a/src/frontend/app/api/schema.ts b/src/frontend/app/api/schema.ts
index 40358a6..4d34a44 100644
--- a/src/frontend/app/api/schema.ts
+++ b/src/frontend/app/api/schema.ts
@@ -66,6 +66,7 @@ export const ArrivalSchema = z.object({
currentPosition: PositionSchema.optional().nullable(),
vehicleInformation: VehicleInformationSchema.optional().nullable(),
operator: z.string().nullable(),
+ operation: z.enum(["pickup_dropoff", "pickup_only", "dropoff_only"]),
});
export const ArrivalEstimateSchema = z.object({
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>
);
diff --git a/src/frontend/app/routes/home.tsx b/src/frontend/app/routes/home.tsx
index e71c788..0a13fe6 100644
--- a/src/frontend/app/routes/home.tsx
+++ b/src/frontend/app/routes/home.tsx
@@ -240,8 +240,6 @@ export default function StopList() {
</ul>
</div>
)}
-
- {/*<ServiceAlerts />*/}
</>
)}
</div>
diff --git a/src/frontend/app/routes/stops-$id.tsx b/src/frontend/app/routes/stops-$id.tsx
index b3d7e86..2734895 100644
--- a/src/frontend/app/routes/stops-$id.tsx
+++ b/src/frontend/app/routes/stops-$id.tsx
@@ -10,6 +10,7 @@ import {
type StopArrivalsResponse,
} from "~/api/schema";
import { ArrivalList } from "~/components/arrivals/ArrivalList";
+import ServiceAlerts from "~/components/ServiceAlerts";
import { ErrorDisplay } from "~/components/ErrorDisplay";
import { PullToRefresh } from "~/components/PullToRefresh";
import RouteIcon from "~/components/RouteIcon";
@@ -229,6 +230,8 @@ export default function Estimates() {
</div>
)}
+ <ServiceAlerts selectorFilter={[`stop#${stopId}`]} />
+
<div className="estimates-list-container flex-1">
{dataLoading ? (
<>{/*TODO: New loading skeleton*/}</>