Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# CHANGELOG

## 2026-11-07 - Make visible controls do what they look like (Phase 2)

- "View trends" in a crossing card's menu now opens `#trends`, the hourly pattern section, and scrolls to it. Before, it reloaded the same crossing page at the top. The menu skips its focus return for this link so the page does not jump back up.
- New pages start at the top. A small `ScrollToTop` runs only when the path changes; Back/forward keeps the browser's restoration, `#hash` links scroll to their target, and query-only changes (lane, map selection) keep their position.
- Stats tiles and the exchange-rate card no longer lift on hover, since they have no action.
- Verified in headless Chrome at 500px: View trends lands on the section; a deep-scrolled crossing page opens Best time at the top. Back position restoration was not compared against the previous build.

## 2026-11-07 - Stop overstating Walk or Drive, Analytics and the embed (Phase 1)

- Walk or Drive: the result no longer says "Walk." / "You save". It states the comparison it actually made (standard car lane vs a pedestrian lane plus an assumed 20 min for parking and the bridge), notes it applies only if you can use that lane and your trip works on foot, and reminds that Ready Lane needs an RFID document. Heading changed to "Comparison from reported waits, not a personal recommendation." EN/ES. Logic unchanged.
Expand Down
2 changes: 2 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import ScrollToTop from '@/components/ScrollToTop';
import Layout from '@/Layout';
import ErrorBoundary from '@/components/ErrorBoundary';
import Dashboard from '@/pages/Dashboard';
Expand Down Expand Up @@ -64,6 +65,7 @@ function LayoutRoutes() {
export default function App() {
return (
<BrowserRouter>
<ScrollToTop />
<ErrorBoundary>
<Suspense fallback={<RouteFallback />}>
<Routes>
Expand Down
21 changes: 21 additions & 0 deletions src/components/ScrollToTop.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useEffect, useRef } from 'react';
import { useLocation, useNavigationType } from 'react-router-dom';

// New pages start at the top. Back/forward (POP) keeps the browser's own
// scroll restoration, and links with a #hash let the page scroll to it.
// Only a pathname change counts, so query-only changes (lane, map
// selection) keep their position.
export default function ScrollToTop() {
const { pathname, hash } = useLocation();
const navigationType = useNavigationType();
const previous = useRef(pathname);

useEffect(() => {
if (previous.current === pathname) return;
previous.current = pathname;
if (navigationType === 'POP' || hash) return;
window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
}, [pathname, hash, navigationType]);

return null;
}
9 changes: 6 additions & 3 deletions src/components/dashboard/BorderCrossingCard.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import { DEFAULT_LANE, laneLabel, withLane } from '@/lib/laneContext';
import { availableLaneWait } from '@/lib/crossingAvailability';
Expand Down Expand Up @@ -121,6 +121,9 @@ export default function BorderCrossingCard({
selectedLane = DEFAULT_LANE,
}) {
const [showLanes, setShowLanes] = useState(false);
// Skip the menu's focus return when we are scrolling to #trends, or it
// pulls the page back up to the trigger.
const skipMenuFocusReturn = useRef(false);
const [aggregate, setAggregate] = useState(null);

const cardSlug = slug || crossing.slug || null;
Expand Down Expand Up @@ -298,10 +301,10 @@ export default function BorderCrossingCard({
<MoreHorizontal className="w-5 h-5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-44">
<DropdownMenuContent align="end" className="w-44" onCloseAutoFocus={(event) => { if (skipMenuFocusReturn.current) { event.preventDefault(); skipMenuFocusReturn.current = false; } }}>
{cardSlug ? (
<DropdownMenuItem asChild>
<Link to={withLane(`/crossing/${cardSlug}/`, selectedLane)} className="cursor-pointer" onClick={() => track('crossing-open', { slug: cardSlug, source: 'card', direction: selectedDirection })}>
<Link to={withLane(`/crossing/${cardSlug}/#trends`, selectedLane)} className="cursor-pointer" onClick={() => { skipMenuFocusReturn.current = true; track('crossing-open', { slug: cardSlug, source: 'card', direction: selectedDirection }); }}>
{language === 'en' ? 'View trends' : 'Ver tendencias'}
</Link>
</DropdownMenuItem>
Expand Down
4 changes: 2 additions & 2 deletions src/components/dashboard/ExchangeRateWidget.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default function ExchangeRateWidget({ exchangeRate, language, theme, comp
theme === 'dark'
? 'bg-gray-800/60 border-gray-700'
: 'bg-white border-slate-200'
} transition-all hover:shadow-md`}>
}`}>
<CardContent className="p-3">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-1">
Expand Down Expand Up @@ -72,7 +72,7 @@ export default function ExchangeRateWidget({ exchangeRate, language, theme, comp
theme === 'dark'
? 'bg-linear-to-br from-gray-800/50 to-gray-900/50 border-gray-700'
: 'bg-linear-to-br from-white/90 to-blue-50/50 border-slate-200'
} backdrop-blur-xs transition-all duration-300 hover:shadow-xl`}>
} backdrop-blur-xs`}>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-lg">
<div className="flex items-center gap-1">
Expand Down
4 changes: 2 additions & 2 deletions src/components/dashboard/StatsOverview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export default function StatsOverview({
theme === 'dark'
? 'bg-gray-800/60 border-gray-700'
: 'bg-white border-slate-200'
} hover:shadow-md transition-all`}>
}`}>
<CardContent className="p-2.5 flex items-start gap-2">
<div className={`p-1 rounded-md ${stat.bg} shrink-0`}>
<stat.icon className={`w-3.5 h-3.5 ${stat.color}`} />
Expand Down Expand Up @@ -151,7 +151,7 @@ export default function StatsOverview({
theme === 'dark'
? 'bg-gray-800/60 border-gray-700'
: 'bg-white/80 border-slate-200'
} backdrop-blur-xs hover:shadow-md transition-all`}>
} backdrop-blur-xs`}>
<CardContent className="p-3 min-h-[70px] sm:min-h-[80px] flex items-start gap-2">
<div className={`p-1.5 rounded-lg ${stat.bg} shrink-0`}>
<stat.icon className={`w-4 h-4 ${stat.color}`} />
Expand Down
9 changes: 5 additions & 4 deletions src/pages/CrossingDetail.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,10 +299,11 @@ export default function CrossingDetail() {
const { hash, search } = useLocation();
const loadedPort = crossing?.port_number;
useEffect(() => {
if (hash !== '#cameras' || !loadedPort) return;
const frame = requestAnimationFrame(() => document.getElementById('cameras')?.scrollIntoView());
const target = hash === '#cameras' || hash === '#trends' ? hash.slice(1) : null;
if (!target || !loadedPort) return;
const frame = requestAnimationFrame(() => document.getElementById(target)?.scrollIntoView());
return () => cancelAnimationFrame(frame);
}, [hash, loadedPort]);
}, [hash, loadedPort, aggregate]);

// Aggregate buckets are port-local; evaluate "today"/"now" in the port's
// timezone once the crossing is known.
Expand Down Expand Up @@ -587,7 +588,7 @@ export default function CrossingDetail() {
})()}

{aggregate && aggregate.by_hour && aggregate.by_hour.length > 0 && (
<section className="mb-6">
<section id="trends" className="mb-6 scroll-mt-20">
<div className="flex items-baseline justify-between gap-3 flex-wrap mb-2">
<h2 className="text-lg font-semibold text-slate-900 dark:text-white">
{language === 'en'
Expand Down
Loading