From 695c7a65a1e9ab3b95beeaf02a1e3b10bb16996b Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Tue, 24 Mar 2026 20:32:17 +0100
Subject: feat: client-side trip tracking with browser notifications (#151)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com>
---
 .../app/components/arrivals/ArrivalCard.tsx        | 43 +++++++++++++++++++++-
 1 file changed, 42 insertions(+), 1 deletion(-)

(limited to 'src/frontend/app/components/arrivals/ArrivalCard.tsx')

diff --git a/src/frontend/app/components/arrivals/ArrivalCard.tsx b/src/frontend/app/components/arrivals/ArrivalCard.tsx
index bdd20a5..9c68a97 100644
--- a/src/frontend/app/components/arrivals/ArrivalCard.tsx
+++ b/src/frontend/app/components/arrivals/ArrivalCard.tsx
@@ -1,4 +1,4 @@
-import { AlertTriangle, BusFront, LocateIcon } from "lucide-react";
+import { AlertTriangle, 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";
@@ -9,6 +9,8 @@ import "./ArrivalCard.css";
 interface ArrivalCardProps {
   arrival: Arrival;
   onClick?: () => void;
+  onTrack?: () => void;
+  isTracked?: boolean;
 }
 
 const AutoMarquee = ({ text }: { text: string }) => {
@@ -57,6 +59,8 @@ const AutoMarquee = ({ text }: { text: string }) => {
 export const ArrivalCard: React.FC<ArrivalCardProps> = ({
   arrival,
   onClick,
+  onTrack,
+  isTracked = false,
 }) => {
   const { t } = useTranslation();
   const {
@@ -287,6 +291,43 @@ export const ArrivalCard: React.FC<ArrivalCardProps> = ({
             </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();
+                e.stopPropagation();
+                onTrack();
+              }
+            }}
+            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>
+        )}
       </div>
     </Tag>
   );
-- 
cgit v1.3

