Skip to content

feat: add favorites/bookmarks system for stops and routes#436

Open
Amartuvshins0404 wants to merge 2 commits intoOneBusAway:developfrom
Amartuvshins0404:feat/favorites-system
Open

feat: add favorites/bookmarks system for stops and routes#436
Amartuvshins0404 wants to merge 2 commits intoOneBusAway:developfrom
Amartuvshins0404:feat/favorites-system

Conversation

@Amartuvshins0404
Copy link
Copy Markdown

@Amartuvshins0404 Amartuvshins0404 commented Mar 25, 2026

Summary

  • Add favorites feature allowing riders to save frequently-used stops and routes for one-tap access
  • localStorage-backed store following the existing recentTripsStore.js pattern
  • Star toggle button on stop pages and modals, dedicated Favorites tab in the search pane
  • Closes Favorites / Saved Stops and Routes #315

Changes

New files

  • src/stores/favoritesStore.js — Store with addFavorite, removeFavorite, isFavorite, clearAll. Deduplicates by type+entityId. Validates entries on load. Browser-guarded localStorage with try-catch on every operation.
  • src/components/favorites/FavoriteToggle.svelte — Star icon button (filled yellow when favorited, gray outline when not). Keyboard accessible (Enter + Space), ARIA labels via i18n.
  • src/components/favorites/FavoritesList.svelte — Renders saved favorites with name, direction, type icon. Per-item trash button to remove. Empty state message. "Clear all favorites" link.

Modified files

  • src/components/search/SearchPane.svelte — Added "Favorites" tab between "Stops and Stations" and "Plan a Trip". Renders FavoritesList with click handlers that navigate to the stop/route on the map.
  • src/components/stops/StopPageHeader.svelte — Added FavoriteToggle next to stop name in the heading. Added optional stopLat/stopLon props for coordinates.
  • src/components/stops/StopModal.svelte — Added FavoriteToggle above StopPane inside the modal.
  • src/locales/en.json — Added tabs.favorites and favorites.* translation keys.

Design decisions

  • localStorage only — mirrors recentTripsStore.js. No backend persistence needed. Can be extended later.
  • No max limit — unlike recent trips (capped at 3), favorites are user-curated and should not be pruned.
  • Dedup by type+entityId — saving the same stop twice updates the timestamp instead of creating a duplicate. A stop and route with the same entityId are kept as separate favorites.
  • get() from svelte/store for one-shot reads — used in toggle() instead of subscribe/unsubscribe to avoid double-fire inside callbacks.

Tests

  • 14 store tests: add stop/route, LIFO order, no max limit, dedup, remove by ID, isFavorite true/false, clearAll, missing optional fields, malformed data on load, corrupted JSON, localStorage quota exceeded
  • 7 FavoriteToggle tests: renders button, aria-label add/remove, calls addFavorite/removeFavorite, Enter key, Space key, missing optional props
  • 9 FavoritesList tests: empty state, renders list, shows/hides direction, onStopClick/onRouteClick callbacks, clear all visible/hidden, keyboard navigation
  • All 1187 tests pass (1157 existing + 30 new)

Verification

  1. Open the app → "Favorites" tab visible next to "Stops and Stations"
  2. Click Favorites tab → shows "No favorites yet" empty state
  3. Click any bus stop on map → modal opens with ☆ outline star
  4. Click the star → turns ★ yellow, stop appears in Favorites tab instantly
  5. Click star again → unfavorites, removed from list
  6. Navigate to /stops/[stopID] → star appears next to stop name in header
  7. Refresh browser → favorites persist from localStorage
  8. npm run lint passes
  9. npm run test — 55 files, 1187 tests, all passing

Screenshots

Favorites tab (empty state)

Favorites tab showing empty state with helper text

Star toggle on stop page

Before (unfavorited) After (favorited)
Stop page with unfavorited star outline Stop page with filled yellow star after favoriting

Dark mode support

Favorites tab in dark mode

Add a complete favorites feature allowing riders to save frequently-used
stops and routes for one-tap access. Implements localStorage-backed store
mirroring the existing recentTripsStore pattern, with star toggle UI on
stop pages and modals, and a dedicated Favorites tab in the search pane.

Closes OneBusAway#315
Copy link
Copy Markdown
Member

@aaronbrethorst aaronbrethorst left a comment

Choose a reason for hiding this comment

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

Hey Amartuvshin, this is a really well-put-together feature — the store follows the recentTripsStore.js pattern nicely, the test coverage at 30 tests is thorough (especially the localStorage edge cases), and the PR description with screenshots and verification steps is excellent. Before we can merge this, I will need you to make a couple changes:

Critical

  1. Pass stopLat/stopLon to StopPageHeader in both standalone stop pages. Neither /stops/[stopID]/+page.svelte nor /stops/[stopID]/schedule/+page.svelte passes the new coordinate props, even though the stop object has lat and lon available. This means any favorite added from these pages saves coords: null. When the user clicks that favorite from the Favorites tab, handleStopClick calls mapProvider.flyTo(undefined, undefined, 20) and mapProvider.addMarker with undefined coordinates — which will crash or produce broken map behavior.

    Fix in src/routes/stops/[stopID]/+page.svelte line 46:

    <StopPageHeader stopName={stop.name} stopId={stop.id} stopDirection={stop.direction} stopLat={stop.lat} stopLon={stop.lon} />

    And similarly in src/routes/stops/[stopID]/schedule/+page.svelte line 151.

Important

  1. Use $derived with store auto-subscription instead of manual $effect + subscribe. Both FavoriteToggle.svelte (lines 13-19) and FavoritesList.svelte (lines 12-17) use a $state variable updated inside an $effect with manual subscribe/unsubscribe. This project uses Svelte 5 with runes and the store auto-subscription $storeName syntax is cleaner and less error-prone. For example, in FavoriteToggle:

    // Replace the $state + $effect + subscribe pattern with:
    let isFav = $derived($favorites.some((f) => f.type === type && f.entityId === entityId));

    And in FavoritesList:

    let items = $derived($favorites);

    This eliminates the risk of subscription leaks and is more idiomatic.

  2. Remove the redundant handleKeydown on FavoriteToggle's <button>. Native HTML <button> elements already fire click events on Enter and Space keypress — the custom handleKeydown handler (lines 33-37) duplicates built-in browser behavior and can be safely removed along with the onkeydown binding on line 44.

  3. Add tests for the remove button and "Clear all" button in FavoritesList. Both user actions are implemented but untested:

    • Clicking the trash icon should call removeFavorite with the correct ID and should NOT trigger onStopClick/onRouteClick (the stopPropagation behavior).
    • Clicking "Clear all favorites" should call clearAll.

Fit and Finish

  1. Use ?? instead of || for direction and coords defaults in favoritesStore.js line 61-62. item.direction || null would coerce an empty string "" to null. While unlikely to matter in practice, ?? is the correct operator for "nullish, not falsy" semantics.

Thanks again, and I look forward to merging this change.

- Pass stopLat/stopLon to StopPageHeader in both standalone stop pages
  so favorites saved from these pages include coordinates
- Replace manual $effect + subscribe with $derived($favorites) in
  FavoriteToggle and FavoritesList for idiomatic Svelte 5
- Remove redundant handleKeydown on FavoriteToggle button (native
  buttons already handle Enter/Space)
- Add tests for remove button (with stopPropagation), and clear all
- Use ?? instead of || for direction/coords defaults in favoritesStore
@Amartuvshins0404
Copy link
Copy Markdown
Author

Hi there, @aaronbrethorst.

Appreciate to code review and feedback. Addressed the issue accordingly.

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.

Favorites / Saved Stops and Routes

2 participants