feat: add favorites/bookmarks system for stops and routes#436
feat: add favorites/bookmarks system for stops and routes#436Amartuvshins0404 wants to merge 2 commits intoOneBusAway:developfrom
Conversation
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
aaronbrethorst
left a comment
There was a problem hiding this comment.
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
-
Pass
stopLat/stopLontoStopPageHeaderin both standalone stop pages. Neither/stops/[stopID]/+page.sveltenor/stops/[stopID]/schedule/+page.sveltepasses the new coordinate props, even though thestopobject haslatandlonavailable. This means any favorite added from these pages savescoords: null. When the user clicks that favorite from the Favorites tab,handleStopClickcallsmapProvider.flyTo(undefined, undefined, 20)andmapProvider.addMarkerwith undefined coordinates — which will crash or produce broken map behavior.Fix in
src/routes/stops/[stopID]/+page.svelteline 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.svelteline 151.
Important
-
Use
$derivedwith store auto-subscription instead of manual$effect+subscribe. BothFavoriteToggle.svelte(lines 13-19) andFavoritesList.svelte(lines 12-17) use a$statevariable updated inside an$effectwith manualsubscribe/unsubscribe. This project uses Svelte 5 with runes and the store auto-subscription$storeNamesyntax is cleaner and less error-prone. For example, inFavoriteToggle:// 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.
-
Remove the redundant
handleKeydownonFavoriteToggle's<button>. Native HTML<button>elements already fireclickevents on Enter and Space keypress — the customhandleKeydownhandler (lines 33-37) duplicates built-in browser behavior and can be safely removed along with theonkeydownbinding on line 44. -
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
removeFavoritewith the correct ID and should NOT triggeronStopClick/onRouteClick(thestopPropagationbehavior). - Clicking "Clear all favorites" should call
clearAll.
- Clicking the trash icon should call
Fit and Finish
- Use
??instead of||fordirectionandcoordsdefaults infavoritesStore.jsline 61-62.item.direction || nullwould coerce an empty string""tonull. 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
42964f0 to
2e1a5c3
Compare
|
Hi there, @aaronbrethorst. Appreciate to code review and feedback. Addressed the issue accordingly. |
Summary
recentTripsStore.jspatternChanges
New files
src/stores/favoritesStore.js— Store withaddFavorite,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 optionalstopLat/stopLonprops for coordinates.src/components/stops/StopModal.svelte— Added FavoriteToggle above StopPane inside the modal.src/locales/en.json— Addedtabs.favoritesandfavorites.*translation keys.Design decisions
recentTripsStore.js. No backend persistence needed. Can be extended later.get()from svelte/store for one-shot reads — used in toggle() instead of subscribe/unsubscribe to avoid double-fire inside callbacks.Tests
Verification
/stops/[stopID]→ star appears next to stop name in headernpm run lintpassesnpm run test— 55 files, 1187 tests, all passingScreenshots
Favorites tab (empty state)
Star toggle on stop page
Dark mode support