Skip to content

Feat/shortline fix 251#441

Open
Hitanshi7556 wants to merge 3 commits intoOneBusAway:developfrom
Hitanshi7556:feat/shortline-fix-251
Open

Feat/shortline fix 251#441
Hitanshi7556 wants to merge 3 commits intoOneBusAway:developfrom
Hitanshi7556:feat/shortline-fix-251

Conversation

@Hitanshi7556
Copy link
Copy Markdown

Overview
This PR implements short-line detection for route schedules to address issue #251.

Problem
When viewing a route schedule, if some trips end at an intermediate stop (short-line) instead of the final destination, this wasn't indicated in any way. For example, Route 120 at stop 12434 has trips ending at both Fashion Valley and Kearny Mesa, but there was no visual distinction.

Solution

  • Detect short-lines by comparing each trip's stopHeadsign with the main route headsign
  • Highlight short-line times with amber background and † marker
  • Show tooltip on hover: "Ends at [destination] — does not continue to [final destination]"
  • Display legend only when route has short-lines

Changes

  • src/routes/stops/[stopID]/schedule/+page.svelte: Added short-line detection logic in groupStopTimesByHour()
  • src/components/schedule-for-stop/RouteScheduleTable.svelte: Updated UI with amber styling and hover tooltips
  • src/locales/en.json: Added i18n translations
  • src/routes/stops/__tests__/schedule-shortline.test.js: Added unit tests (4 tests, all passing)

Testing

  • Unit tests pass for short-line detection logic
  • Tested with mock data showing mixed destinations
  • Tooltips appear instantly on hover

i used mock data to test the feature :
AFTER
image

- Create favoritesStore.js with CRUD operations (add, remove, toggle)
- Implement FavoriteButton.svelte component with reactive store subscription
- Integrate FavoriteButton into StopItem and RouteItem components
- Add FavoriteButton to modal headers via ModalPane component
- Update StopModal to pass favoriteId and favoriteType props
- Fix test selectors for nested button scenarios
- All tests passing (1163 tests)
- Features: star icon toggles instantly, favorites persist across reloads
- Detect short-line trips via stopHeadsign comparison
- Show amber background + † marker for short-lines
- Add instant hover tooltip showing destination info
- Only display legend when route has short-lines
- Add unit tests for detection logic
- Add i18n translations (en.json)
Copilot AI review requested due to automatic review settings March 25, 2026 16:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR primarily adds short-line detection and presentation to the stop schedule view (issue #251) by tagging trips whose stopHeadsign differs from the route’s primary headsign and surfacing that difference in the schedule UI. It also introduces a favorites feature (store + UI buttons) and some repo/dev artifacts.

Changes:

  • Add short-line detection in stop schedule grouping and expose isShortLine/stopHeadsign to the UI.
  • Update the schedule table UI to highlight short-line times with a marker and tooltip/legend behavior.
  • Add a new favorites store and UI buttons (Stop/Route items and modal header), plus related test updates.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/routes/stops/[stopID]/schedule/+page.svelte Adds main headsign plumbing and sets isShortLine/stopHeadsign when grouping stop times.
src/components/schedule-for-stop/RouteScheduleTable.svelte Highlights short-line times and shows tooltip/legend for short-line trips.
src/locales/en.json Adds i18n strings intended for short-line legend/tooltip text.
src/routes/stops/tests/schedule-shortline.test.js Adds unit tests for short-line detection logic (currently via a test-local helper).
src/stores/favoritesStore.js Introduces a persisted favorites store backed by localStorage.
src/components/favorites/FavoriteButton.svelte Adds a reusable favorites toggle button component.
src/components/StopItem.svelte Adds favorites button to stop list items.
src/components/RouteItem.svelte Adds favorites button to route list items.
src/components/navigation/ModalPane.svelte Adds optional favorites button in the modal header.
src/components/stops/StopModal.svelte Wires stop modal to show a favorites control via ModalPane.
src/components/stops/tests/StopItem.test.js Updates tests to account for additional button(s) in StopItem.
src/components/tests/RouteItem.test.js Updates tests to account for additional button(s) in RouteItem.
PHASE_1_AUDIT.md Adds an architecture/audit document.
.vscode/tasks.json Adds a VS Code build task config.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/StopItem.svelte Outdated
Comment on lines 7 to 23
</div>
</div>
<FavoriteButton id={stop.id} type="stop" ariaLabel={`Add ${stop.name} to favorites`} />
</button>
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StopItem renders a <button> that contains FavoriteButton, which itself renders another <button>. Nested buttons are invalid HTML and can break click/keyboard behavior and accessibility. Restructure so the favorite control is a sibling of the primary button (e.g., wrap both in a container, or change the outer element to a non-button with appropriate role/handlers).

Copilot uses AI. Check for mistakes.
Comment on lines 18 to +21
<div class="text-normal flex-1 self-center font-semibold">{title}</div>
{#if favoriteId}
<FavoriteButton id={favoriteId} type={favoriteType} />
{/if}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ModalPane renders FavoriteButton without an ariaLabel, so many panes will expose a generic "Add to favorites" label. This is ambiguous for assistive tech; consider passing a specific label (e.g., derived from title) or adding a favoriteAriaLabel prop that callers can set.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +22
function getTooltip(stopTime) {
if (!stopTime.isShortLine || !stopTime.stopHeadsign) return null;
return `Ends at ${stopTime.stopHeadsign} — does not continue to ${schedule.mainHeadsign}`;
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The short-line tooltip/legend strings are currently hardcoded (getTooltip() and the legend text), but this PR adds i18n keys (schedule_for_stop.ends_at_short_line, schedule_for_stop.short_line_legend). To avoid untranslated UI and dead strings, use $t(...) with interpolation for both tooltip and legend (or remove the unused keys).

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +78
<span
class="relative cursor-help rounded bg-amber-100 px-2 text-amber-900 dark:bg-amber-900 dark:text-amber-100"
on:mouseenter={() => (hoveredTime = `${hour}_${index}`)}
on:mouseleave={() => (hoveredTime = null)}
>
{extractMinutes(stopTime.arrivalTime)}<sup class="text-amber-600 dark:text-amber-300">†</sup>
{#if hoveredTime === `${hour}_${index}`}
<div
class="absolute bottom-full left-0 z-50 mb-2 whitespace-nowrap rounded bg-gray-900 px-3 py-1 text-xs text-white shadow-lg dark:bg-gray-700"
>
{getTooltip(stopTime)}
<div class="absolute top-full left-2 h-0 w-0 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"
></div>
</div>
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The legend text says "tap/hover for details", but the UI only shows details on mouseenter/mouseleave. On touch devices there is no hover, so the tooltip will never appear. Add a click/tap toggle (and ideally keyboard focus handling) for short-line markers to match the stated interaction.

Copilot uses AI. Check for mistakes.
Comment on lines +2 to +25

/**
* Test helper to simulate the groupStopTimesByHour function
* This tests the short-line detection logic
*/
function groupStopTimesByHour(stopTimes, mainHeadsign) {
const grouped = {};
for (let stopTime of stopTimes) {
const date = new Date(stopTime.arrivalTime);
const hour = date.getHours();
if (!grouped[hour]) grouped[hour] = [];

// Detect short-line: if stopHeadsign differs from main headsign, it's a short-line
const isShortLine = !!(stopTime.stopHeadsign && stopTime.stopHeadsign !== mainHeadsign);

grouped[hour].push({
arrivalTime: stopTime.arrivalTime,
stopHeadsign: stopTime.stopHeadsign,
isShortLine: isShortLine
});
}
return grouped;
}

Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests re-implement groupStopTimesByHour() inside the test file instead of exercising the production logic from +page.svelte. As written, the tests can pass even if the real short-line detection breaks. Consider extracting the grouping/short-line detection into a shared module (e.g., $lib/scheduleUtils) and importing it in both the page and the tests.

Suggested change
/**
* Test helper to simulate the groupStopTimesByHour function
* This tests the short-line detection logic
*/
function groupStopTimesByHour(stopTimes, mainHeadsign) {
const grouped = {};
for (let stopTime of stopTimes) {
const date = new Date(stopTime.arrivalTime);
const hour = date.getHours();
if (!grouped[hour]) grouped[hour] = [];
// Detect short-line: if stopHeadsign differs from main headsign, it's a short-line
const isShortLine = !!(stopTime.stopHeadsign && stopTime.stopHeadsign !== mainHeadsign);
grouped[hour].push({
arrivalTime: stopTime.arrivalTime,
stopHeadsign: stopTime.stopHeadsign,
isShortLine: isShortLine
});
}
return grouped;
}
import { groupStopTimesByHour } from '../+page.svelte';

Copilot uses AI. Check for mistakes.
Comment thread src/components/RouteItem.svelte Outdated
Comment on lines 23 to 36
{getDisplayRouteName(route)}
</div>
<FavoriteButton id={route.id} type="route" ariaLabel={`Add ${getDisplayRouteName(route)} to favorites`} />
</button>
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RouteItem is a <button> and now contains FavoriteButton (another <button>), creating nested interactive elements. This is invalid HTML and can cause inconsistent click/keyboard behavior. Refactor so the favorite button is not nested inside the main button (make them siblings in a wrapper, or change the outer element).

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +28
<button
type="button"
onclick={handleToggle}
aria-label={ariaLabel}
class="favorite-btn inline-flex items-center justify-center rounded p-2 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700"
title={isFavorited ? 'Remove from favorites' : 'Add to favorites'}
>
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FavoriteButton keeps the same aria-label regardless of whether the item is already favorited. For screen readers, the label should reflect the current action/state (e.g., "Remove … from favorites" when isFavorited is true). Consider deriving the label from isFavorited or accepting separate add/remove labels.

Copilot uses AI. Check for mistakes.
Comment on lines +145 to +147
12<sup class="text-amber-600 dark:text-amber-300">†</sup>
</span>
Ends before final destination — tap/hover for details
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The legend text at the bottom is hardcoded and duplicates the new i18n string (schedule_for_stop.short_line_legend). Also the example "12†" is a magic value that may confuse users. Consider rendering just the marker (†) with localized legend text via $t(...).

Suggested change
12<sup class="text-amber-600 dark:text-amber-300">†</sup>
</span>
Ends before final destination — tap/hover for details
<sup class="text-amber-600 dark:text-amber-300">†</sup>
</span>
{$isLoading ? '' : $t('schedule_for_stop.short_line_legend')}

Copilot uses AI. Check for mistakes.
Comment thread src/stores/favoritesStore.js Outdated
Comment on lines +1 to +6
import { writable } from 'svelte/store';
import { browser } from '$app/environment';

const STORAGE_KEY = 'wayfinder_favorites';

function createFavoritesStore() {
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

favoritesStore adds a full favorites/bookmarks feature (store + UI hooks elsewhere), but the PR title/description focus on short-line schedule highlighting (#251). If favorites is intended here, please update the PR description/scope; otherwise consider splitting favorites-related changes into a separate PR to keep review and rollback safer.

Copilot uses AI. Check for mistakes.
Comment on lines +81 to +87
isFavorite: (id, type = 'stop') => {
let result = false;
subscribe((favorites) => {
result = favorites.some((fav) => fav.id === id && fav.type === type);
})();
return result;
},
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isFavorite() subscribes to the store just to compute a synchronous boolean, which creates a transient subscription on every call. This can be inefficient and is easy to misuse in render paths. Consider exposing a derived store per id/type, or (if you need sync reads) using get() from svelte/store inside this module.

Copilot uses AI. Check for mistakes.
@Hitanshi7556
Copy link
Copy Markdown
Author

Resolved all comments, PTAL @aaronbrethorst
Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants