Lazy-load heavy components & optimize bundle#3
Conversation
Introduce trip export/sharing, keyboard shortcuts, search history and multiple UI/UX improvements. Adds new components (TripExport, KeyboardShortcutsHelp, TripSelector, TripStats), several composables (useTripSharing, useKeyboardShortcuts, useSearchHistory, useCountryFlag, useWeather) and image/print assets. app.vue now handles importing shared trips from the URL, toggling dark mode, mobile map/list toggle, keyboard shortcut wiring, confetti on trip creation, and improved marker syncing. PlaceCard and PlaceList were enhanced with editable notes/time/cost, move/copy actions, category icons, expanded details and highlight/scroll behavior; DayTabs now shows day time totals and place counts. PlaceSearch gains recent-history selection, and TripExport supports copy/link/JSON download/import/print flows. Also minor styling/transitions, print stylesheet, map style-change handling, and updated tests/types to cover the new behavior.
Introduce a DayTimeline component and a list/timeline view toggle in the main UI; add a compact view toggle button and render DayTimeline when selected. Add persistent per-day customColors to Trip type and tripStore (getDayColor / setDayColor) and wire a color picker in DayTabs to choose day colors. Update map marker logic to accept a color resolver and make conicGradient accept a color function so markers, popups and route lines use custom day colors. Add star rating controls to PlaceCard and a new useVisitDuration composable to suggest visit durations by category. Minor UI tweaks: TripHeader gradient cycling, worldMap sizing fix, and minor icon/import updates.
Remove 4 unused PNG screenshots (~779 KB). Lazy-load WorldMap, dialog components, and canvas-confetti to split the main JS chunk and reduce initial parse/load time.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Resolve conflicts: keep lazy-loaded components and dark mode from feature branch, incorporate SpeedInsights and updated bundle limit from main.
|
To use Codex here, create an environment for this repo. |
Split CI into 4 parallel jobs: test, lint, typecheck, and bundle-size.
- Lower coverage thresholds to match actual coverage after new components - Add vue-tsc and typescript as devDependencies for nuxi typecheck - Use bun run for typecheck to resolve from local node_modules
Test files have pre-existing strict-mode issues (TS2532) that are handled by vitest separately. Excluding them from vue-tsc.
The lockfile is generated on macOS but CI runs on Linux. Combined with transient npm registry 403 errors, --frozen-lockfile causes flaky failures. Use plain bun install and pin bun-version: latest.
There was a problem hiding this comment.
Pull request overview
This PR focuses on reducing initial bundle size (lazy-loading heavy UI + dynamic imports) while expanding trip-planning functionality (multi-trip support, export/sharing, stats, timeline view) and tightening CI (lint/typecheck/bundle-size checks).
Changes:
- Lazy-load heavy components (e.g., Mapbox map + dialogs) and dynamically import
canvas-confetti. - Refactor trip state to support multiple trips in localStorage, plus new place fields and day color customization.
- Add export/sharing/import UI, new timeline/stats widgets, and expand CI with lint/typecheck/bundle-size checks.
Reviewed changes
Copilot reviewed 115 out of 118 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| vitest.config.ts | Formatting + lowers coverage thresholds. |
| types/trip.ts | Extends Trip/Place types (custom day colors + notes/time/cost/rating). |
| tests/unit/types/trip.test.ts | Formatting updates for DAY_COLORS tests. |
| tests/unit/stores/tripStore.test.ts | Updates store tests; adds localStorage cleanup. |
| tests/unit/composables/useMapboxSearch.test.ts | Formatting updates; fetch mock tests. |
| tests/unit/composables/useMapMarkers.helpers.test.ts | Formatting updates for marker helper tests. |
| tests/unit/composables/useImageUpload.test.ts | Formatting updates for image upload tests. |
| tests/setup.ts | Test polyfills + deterministic RAF/fetch defaults. |
| tests/perf/runtime-benchmarks.test.ts | Formatting updates for perf benchmarks. |
| tests/perf/check-bundle-size.mjs | Formatting updates; bundle-size check script. |
| tests/mocks/mapbox-gl.ts | Extends Mapbox mock API surface + formatting. |
| tests/integration/trip-flow.test.ts | Formatting updates for integration flow. |
| tests/integration/edit-trip-preserves-places.test.ts | Formatting updates for trip edit overlap tests. |
| tests/helpers/store-helper.ts | Formatting updates for Pinia test helper. |
| tests/helpers/fixtures.ts | Formatting updates for shared test fixtures. |
| tests/components/WorldMap.test.ts | Formatting updates for WorldMap component tests. |
| tests/components/TripHeader.test.ts | Updates tests for header UI changes (My Trips button, badge assertions). |
| tests/components/PlaceSearch.test.ts | Formatting updates for PlaceSearch tests. |
| tests/components/PlaceList.test.ts | Formatting updates for PlaceList tests. |
| tests/components/PlaceCard.test.ts | Updates tests for PlaceCard interaction changes (expand/collapse). |
| tests/components/DayTabs.test.ts | Formatting updates for DayTabs tests. |
| tailwind.config.js | Normalizes config formatting/quotes. |
| stores/tripStore.ts | Major store refactor: multi-trip storage, undo remove, move/duplicate, day colors, place metadata. |
| pages/index.vue | Adds minimal index page template. |
| package.json | Adds canvas-confetti, eslint scripts, typecheck deps (TS/vue-tsc). |
| nuxt.config.ts | Disables SSR, adds color-mode + eslint modules, enables pages. |
| lib/utils.ts | Formatting updates for cn helper. |
| eslint.config.mjs | Adds Nuxt flat ESLint config + rule customizations. |
| composables/useWeather.ts | New composable: fetches weather based on trip places. |
| composables/useVisitDuration.ts | New composable: suggests visit durations by category. |
| composables/useTripSharing.ts | New composable: encodes/decodes trips into URL hash. |
| composables/useSearchHistory.ts | New composable: localStorage-backed recent search history. |
| composables/useMapboxSearch.ts | Formatting updates for Mapbox search composable. |
| composables/useKeyboardShortcuts.ts | New composable: global keyboard shortcuts for navigation/actions. |
| composables/useImageUpload.ts | Formatting updates for image upload composable. |
| composables/useCountryFlag.ts | New composable: derives a country flag from addresses. |
| components/ui/sonner/index.ts | Formatting updates for UI barrel export. |
| components/ui/sonner/Sonner.vue | Formatting updates for Sonner toaster wrapper. |
| components/ui/separator/index.ts | Formatting updates for UI barrel export. |
| components/ui/separator/Separator.vue | Formatting updates for Separator wrapper. |
| components/ui/scroll-area/index.ts | Formatting updates for UI barrel exports. |
| components/ui/scroll-area/ScrollBar.vue | Formatting updates for ScrollBar wrapper. |
| components/ui/scroll-area/ScrollArea.vue | Formatting updates for ScrollArea wrapper. |
| components/ui/range-calendar/index.ts | Formatting updates for range-calendar exports. |
| components/ui/range-calendar/RangeCalendarPrevButton.vue | Formatting updates for range-calendar prev button. |
| components/ui/range-calendar/RangeCalendarNextButton.vue | Formatting updates for range-calendar next button. |
| components/ui/range-calendar/RangeCalendarHeading.vue | Formatting updates for range-calendar heading. |
| components/ui/range-calendar/RangeCalendarHeader.vue | Formatting updates for range-calendar header. |
| components/ui/range-calendar/RangeCalendarHeadCell.vue | Formatting updates for range-calendar head cell. |
| components/ui/range-calendar/RangeCalendarGridRow.vue | Formatting updates for range-calendar grid row. |
| components/ui/range-calendar/RangeCalendarGridHead.vue | Formatting updates for range-calendar grid head. |
| components/ui/range-calendar/RangeCalendarGridBody.vue | Formatting updates for range-calendar grid body. |
| components/ui/range-calendar/RangeCalendarGrid.vue | Formatting updates for range-calendar grid. |
| components/ui/range-calendar/RangeCalendarCellTrigger.vue | Formatting updates for range-calendar cell trigger. |
| components/ui/range-calendar/RangeCalendarCell.vue | Formatting updates for range-calendar cell. |
| components/ui/range-calendar/RangeCalendar.vue | Formatting updates for range-calendar root wrapper. |
| components/ui/popover/index.ts | Formatting updates for UI barrel exports. |
| components/ui/popover/PopoverTrigger.vue | Formatting updates for PopoverTrigger wrapper. |
| components/ui/popover/PopoverContent.vue | Formatting updates for PopoverContent wrapper. |
| components/ui/popover/Popover.vue | Formatting updates for Popover root wrapper. |
| components/ui/input/index.ts | Formatting updates for UI barrel export. |
| components/ui/input/Input.vue | Formatting updates for Input wrapper. |
| components/ui/dialog/index.ts | Formatting updates for UI barrel exports. |
| components/ui/dialog/DialogTrigger.vue | Formatting updates for DialogTrigger wrapper. |
| components/ui/dialog/DialogTitle.vue | Formatting updates for DialogTitle wrapper. |
| components/ui/dialog/DialogScrollContent.vue | Formatting updates for scrollable dialog content wrapper. |
| components/ui/dialog/DialogHeader.vue | Formatting updates for DialogHeader wrapper. |
| components/ui/dialog/DialogFooter.vue | Formatting updates for DialogFooter wrapper. |
| components/ui/dialog/DialogDescription.vue | Formatting updates for DialogDescription wrapper. |
| components/ui/dialog/DialogContent.vue | Formatting updates for DialogContent wrapper. |
| components/ui/dialog/DialogClose.vue | Formatting updates for DialogClose wrapper. |
| components/ui/dialog/Dialog.vue | Formatting updates for Dialog root wrapper. |
| components/ui/card/index.ts | Formatting updates for UI barrel exports. |
| components/ui/card/CardTitle.vue | Formatting updates for CardTitle wrapper. |
| components/ui/card/CardHeader.vue | Formatting updates for CardHeader wrapper. |
| components/ui/card/CardFooter.vue | Formatting updates for CardFooter wrapper. |
| components/ui/card/CardDescription.vue | Formatting updates for CardDescription wrapper. |
| components/ui/card/CardContent.vue | Formatting updates for CardContent wrapper. |
| components/ui/card/Card.vue | Formatting updates for Card wrapper. |
| components/ui/calendar/index.ts | Formatting updates for calendar exports. |
| components/ui/calendar/CalendarPrevButton.vue | Formatting updates for calendar prev button. |
| components/ui/calendar/CalendarNextButton.vue | Formatting updates for calendar next button. |
| components/ui/calendar/CalendarHeading.vue | Formatting updates for calendar heading. |
| components/ui/calendar/CalendarHeader.vue | Formatting updates for calendar header. |
| components/ui/calendar/CalendarHeadCell.vue | Formatting updates for calendar head cell. |
| components/ui/calendar/CalendarGridRow.vue | Formatting updates for calendar grid row. |
| components/ui/calendar/CalendarGridHead.vue | Formatting updates for calendar grid head. |
| components/ui/calendar/CalendarGridBody.vue | Formatting updates for calendar grid body. |
| components/ui/calendar/CalendarGrid.vue | Formatting updates for calendar grid. |
| components/ui/calendar/CalendarCellTrigger.vue | Formatting updates for calendar cell trigger. |
| components/ui/calendar/CalendarCell.vue | Formatting updates for calendar cell. |
| components/ui/calendar/Calendar.vue | Formatting updates for calendar root wrapper. |
| components/ui/button/index.ts | Formatting updates + button variants config. |
| components/ui/button/Button.vue | Formatting updates for Button wrapper. |
| components/ui/badge/index.ts | Formatting updates + badge variants config. |
| components/ui/badge/Badge.vue | Formatting updates for Badge wrapper. |
| components/WorldMap.vue | Adds map style switcher + persists style selection. |
| components/TripStats.vue | New component: trip stats + weather chip. |
| components/TripSetupDialog.vue | Dialog updates + image upload/preview UX tweaks. |
| components/TripSelector.vue | New dialog: select/switch/delete trips. |
| components/TripHeader.vue | Header enhancements: flag, progress bar, export, trip selector, gradient shuffle. |
| components/TripExport.vue | New dialog: export/share/import/print trip. |
| components/TripEditDialog.vue | Dialog updates + image upload/preview UX tweaks. |
| components/PlaceSearch.vue | Adds search history + exposes focus method + theme-aware dropdown. |
| components/PlaceList.vue | Adds highlight/scroll-to-place + undo toast + empty-state animation. |
| components/KeyboardShortcutsHelp.vue | New dialog listing shortcuts. |
| components/DayTimeline.vue | New timeline view for a day’s places. |
| components/DayTabs.vue | Adds per-day color picker + computed total estimated time. |
| assets/css/print.css | Adds print stylesheet hiding interactive/map UI. |
| app.vue | Main app refactor: lazy-loads, multi-view, sharing import, dark mode, shortcuts, mobile toggles. |
| .github/workflows/test.yml | CI split into test/lint/typecheck/bundle-size jobs. |
Comments suppressed due to low confidence (1)
tests/components/PlaceCard.test.ts:11
placeis declared as aPlace, but it’s missing newly required fields (notes,estimatedTime,cost,rating). This will fail TS typechecking and can hide missing-field runtime issues in tests. Add defaults for the new fields (or make them optional inPlaceif that’s intended).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| name: suggestion.name, | ||
| address: suggestion.place_formatted || feature.properties.full_address || '', | ||
| category: suggestion.feature_type || '', | ||
| coordinates: coords, |
There was a problem hiding this comment.
placeData is typed as Omit<Place, 'id' | 'order' | 'notes' | 'estimatedTime'> but Place now also requires cost and rating, so this object doesn’t satisfy the type and doesn’t match store.addPlace’s parameter type. Either include cost/rating defaults here or omit them in the Omit<> to align with addPlace.
| coordinates: coords, | |
| coordinates: coords, | |
| cost: 0, | |
| rating: 0, |
| store.addPlace({ | ||
| mapboxId: entry.mapboxId, | ||
| name: entry.name, | ||
| address: entry.address, | ||
| category: entry.category, | ||
| coordinates: entry.coordinates, | ||
| }); |
There was a problem hiding this comment.
The object passed to store.addPlace(...) when selecting from history is missing required Place fields (notes, estimatedTime, cost, rating) and doesn’t match addPlace’s parameter type. Provide defaults (or update typing) so this path typechecks and produces consistent place objects.
| store.addPlace({ | |
| mapboxId: entry.mapboxId, | |
| name: entry.name, | |
| address: entry.address, | |
| category: entry.category, | |
| coordinates: entry.coordinates, | |
| }); | |
| const placeData: Omit<Place, 'id' | 'order' | 'notes' | 'estimatedTime'> = { | |
| mapboxId: entry.mapboxId, | |
| name: entry.name, | |
| address: entry.address, | |
| category: entry.category, | |
| coordinates: entry.coordinates, | |
| }; | |
| store.addPlace(placeData); |
| order: i, | ||
| notes: '', | ||
| estimatedTime: '', | ||
| })), |
There was a problem hiding this comment.
decompress() creates Place objects without the newly required cost and rating fields, so the returned Trip doesn’t satisfy the current types and may break code that assumes these fields exist. Populate cost/rating with defaults (and optionally include them in the shared format if they should round-trip).
composables/useWeather.ts
Outdated
| const address = store.allPlaces[0].address; | ||
| const parts = address.split(','); | ||
| const city = parts.length > 1 ? parts[parts.length - 1].trim() : parts[0].trim(); | ||
|
|
||
| try { |
There was a problem hiding this comment.
The city parsing uses the last comma-separated segment of address; for typical strings like "Paris, France" this yields the country, not the city, producing incorrect weather lookups. Consider using the first segment (or second-to-last) and add a timeout implementation that doesn’t rely solely on AbortSignal.timeout for broader browser support.
| statements: 50, | ||
| branches: 40, | ||
| functions: 40, | ||
| lines: 50, |
There was a problem hiding this comment.
Coverage thresholds were reduced substantially (from 80% to 40–50%). If this is intentional, it should be justified in the PR (or scoped to specific directories/files). Otherwise, consider keeping the previous thresholds to avoid silently lowering the quality bar.
| export const SAMPLE_PLACE: Omit<Place, 'id' | 'order'> = { | ||
| mapboxId: 'poi.123', | ||
| name: 'Eiffel Tower', | ||
| address: 'Champ de Mars, Paris, France', | ||
| category: 'landmark', |
There was a problem hiding this comment.
SAMPLE_PLACE is typed as Omit<Place, 'id' | 'order'>, but Place now requires notes, estimatedTime, cost, and rating. This fixture no longer satisfies the type and will fail typechecking. Update the fixture type (omit the new fields) or add default values for the new required fields.
| return btoa(encodeURIComponent(JSON.stringify(data))); | ||
| } |
There was a problem hiding this comment.
compress() is base64-encoding JSON (plus URI encoding), which can easily exceed URL length limits as trips grow. Consider real compression (e.g., LZ-based) and/or a non-URL-based sharing mechanism; otherwise share links may fail unpredictably.
| toast.error('Invalid trip file format'); | ||
| return; | ||
| } | ||
| store.trip = data; |
There was a problem hiding this comment.
Importing via store.trip = data can desynchronize the store: the trip setter replaces the trip at currentTripId, but if data.id differs, currentTripId will point to no trip and the getter returns null. Import should add/replace by data.id and update currentTripId accordingly (or import as a new trip with a new id).
| store.trip = data; | |
| // Replace or add trip by its id and update the currentTripId to keep the store in sync | |
| const existingIndex = Array.isArray((store as any).trips) | |
| ? (store as any).trips.findIndex((t: Trip) => t.id === data.id) | |
| : -1; | |
| if (existingIndex !== -1) { | |
| (store as any).trips[existingIndex] = data; | |
| } else if (Array.isArray((store as any).trips)) { | |
| (store as any).trips.push(data); | |
| } | |
| (store as any).currentTripId = data.id; |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
This is the final PR Bugbot will review for you during this billing cycle
Your free Bugbot reviews will reset on March 7
Details
You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.
To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.
| - run: bun install --frozen-lockfile | ||
| with: | ||
| bun-version: latest | ||
| - run: bun install |
There was a problem hiding this comment.
CI drops frozen-lockfile, inconsistent with deploy workflow
Medium Severity
All four CI jobs changed bun install --frozen-lockfile to plain bun install, while deploy.yml still uses --frozen-lockfile. This means CI tests can silently pass with auto-resolved dependency versions that differ from the lockfile, but the deployment build will fail if the lockfile is out of sync. This inconsistency can cause PRs to pass CI yet break the deploy pipeline.


Summary
all-features.png,dark-mode.png,wave1-initial.png,wave1-trip-created.png) — ~779 KB of dead assets committed in b5f3383 but never referencedLazyWorldMap) to splitmapbox-gl(~1.5 MB) into a separate async chunkLazyTripSetupDialog,LazyTripEditDialog,LazyTripExport,LazyKeyboardShortcutsHelp,LazyTripSelector) to defer ~300 KBcanvas-confettiso it's only fetched on trip creationTest plan
bun run test— 17 test files, 143 tests all passingbun run lint— 0 errors (9 pre-existing warnings)v-ifgate)LazyprefixNote
Medium Risk
Moderate risk because it changes the main app shell behavior (lazy-loaded components, new view modes, URL import, marker click handling) and CI enforcement; failures would primarily affect UX/rendering rather than data integrity or security.
Overview
CI now runs more than tests. The GitHub Actions workflow is renamed to
CI, switches tobun install(no frozen lockfile), and adds separatelint,typecheck, andbundle-sizejobs.The main app UI is reworked to improve load time and add interaction.
app.vuenow lazy-loads heavy components (map and dialogs), dynamically importscanvas-confettion trip creation, adds dark-mode toggling, mobile list/map switching, list vs timeline view, keyboard shortcuts, and imports shared trips from a#share=URL hash. Marker syncing is updated to support selected-day styling/click callbacks, and a newassets/css/print.cssis added and globally imported to provide print-friendly output.Written by Cursor Bugbot for commit ad5613c. This will update automatically on new commits. Configure here.