AI travel planner for road trips and city trips. Fill in a short wizard, Gemini returns a full itinerary (stops, times, map coordinates, lodging, fuel/tolls), then you edit, save, and share it.
- Next.js 16 (App Router), React 19, TypeScript
- Tailwind 4 + shadcn (
base-nova) - Leaflet maps (
leaflet+react-leaflet) - Google Gemini (
@google/genai, primarygemini-3.1-flash-litewith automatic fallback) - Supabase Auth + Postgres (
@supabase/ssr+@supabase/supabase-js) — login, saved trips, passkeys @react-pdf/rendererfor client-side PDF export (dynamically imported on click)- Italian and English via URL prefixes (
/it,/en) - Light/dark theme (cookie-persisted, OS-seeded on first visit)
- Road trips: multi-stop driving itineraries with distances, fuel costs, tolls, overnight stays. Optional "visit origin" toggle includes sightseeing in the origin city.
- City trips: walkable/public-transport itineraries within a single city.
- Editable results: reorder/remove stops (distances recompute live), replace a stop from 3 Gemini alternatives with undo toast + pinnable previous choice.
- Lazy stop details: on-demand description (Gemini) + real photo (Wikipedia, freely licensed) in a modal.
- Export to PDF: a printable, offline A4 itinerary (days, stops, substops, costs, toll breakdown, plus a black & white route map with territory outlines + numbered stops for road trips) generated client-side with
@react-pdf/renderer, saved asPackrOn - <Title>.pdf. - Sharing: every generated trip is stored in Supabase and shared with its public UUID route (
/{locale}/trip/{id}). Guest trips can be claimed by signing up. - Accounts & saved trips: email/password auth, passkeys (WebAuthn), username profile, saved-trip library with edit-overwrite, account deletion.
- SEO: locale sitemap with hreflang, canonical URLs, Open Graph/Twitter cards, JSON-LD, robots rules.
npm install
cp .env.example .env.local
# set GEMINI_API_KEY + Supabase vars (see below)
npm run devOpen http://localhost:3000. Middleware redirects / to /it or /en from the packron-locale cookie or Accept-Language.
| Command | Purpose |
|---|---|
npm run dev |
Dev server |
npm run build |
Production build |
npm run typecheck |
tsc --noEmit |
npm test |
Unit tests (Vitest) |
| Variable | Required | Notes |
|---|---|---|
GEMINI_API_KEY |
Yes, for generation | Also used for stop alternatives + descriptions |
GEMINI_MODEL |
No | Default gemini-3.1-flash-lite (fallback is built-in) |
NEXT_PUBLIC_CARTO_BASEMAPS_KEY |
No | Optional CARTO Voyager tiles; without it, maps use public OpenStreetMap tiles. Restrict the key to your site referrers |
NEXT_PUBLIC_SUPABASE_URL |
Yes, for auth/saved trips | Supabase project URL |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY |
Yes, for auth/saved trips | Public sb_publishable_… key (legacy NEXT_PUBLIC_SUPABASE_ANON_KEY still works as fallback). Never add a secret/service-role key |
NEXT_PUBLIC_SITE_URL |
Required in prod | Canonical URL used for sitemap, canonical URLs, and social metadata |
NEXT_PUBLIC_BOOKING_AFFILIATE_ID |
No | Omitted from Booking URLs until set to a real aid |
NEXT_PUBLIC_GETYOURGUIDE_PARTNER_ID |
No | Optional partner_id on GetYourGuide searches |
| Without the Supabase vars the app still builds and runs, but auth UI explains setup and trip generation/storage returns 503. |
- Create a project, then run all migrations in the SQL editor (in order):
supabase/migrations/20260911000000_create_saved_trips.sql(table + RLS)supabase/migrations/20260912000000_delete_own_account.sql(self-service account deletion)supabase/migrations/20261002000000_unify_trip_storage.sql(guest rows + public trip reads)
- Set
NEXT_PUBLIC_SUPABASE_URL+ key, andNEXT_PUBLIC_SITE_URLin prod. - Dashboard → Authentication → Providers → Email: disable email confirmation so signup returns an active session immediately. Set the Site URL to the production URL; this app does not send signup confirmation links.
- Optional, for passkeys: Authentication → Passkeys → enable, RP display name
PackrOn, RP ID = bare production domain. RP ID binds credentials, so localhost needs its own RP ID/project for testing.
Pages (all under /it + /en):
/— planner/how-it-works— how it works (legacy/come-funziona302-redirects here)/examples— example itineraries (legacy/esempi302-redirects here)/login,/signup— public auth entry points (in sitemap)/trips,/trips/[id]— private saved-trip library (never indexed)/trip/[id]— public itinerary by database UUID (never indexed)/account— private profile, passkeys, delete account (never indexed)
API:
POST /api/generate-trip— Gemini itinerary (required-fields check only; feasibility via Geminiimpossible_tripflag)POST /api/suggest-stops— three nearby alternatives (Gemini, mock fallback)POST /api/describe-stop— lazy description + Wikipedia photoGET /api/reverse-geocode— Nominatim, then BigDataCloudGET /api/trips,POST /api/trips— list / save (auth required)GET /api/trips/[id]— public trip read;PUT(auto-save edits) /PATCH(favourite flag) /DELETEremain owner-onlyPOST /api/trips/claim— attach a guest trip to the authenticated userDELETE /api/account— delete own account (confirmation by typing username)GET /auth/callback— Supabase PKCE code exchange (not locale-prefixed)
- Every generated itinerary is inserted into
saved_trips; authenticated generations include the server-derived user ID and guest generations keepuser_idnull. The trip UUID route is the same public URL used by the share action. - Edits in the trip view auto-save over the same row; the star button only toggles the
is_favoriteflag (runsupabase/migrations/20261005000000_trip_favorites.sql). My Trips lists every computed trip, with favourites in a separate top section. - Guest trip IDs are also retained in browser local storage for the Recent Trips list. The guest can claim an orphaned row by creating an account while viewing it; the claim endpoint accepts only null-owner rows and derives ownership from the session.
lib/gemini.ts,lib/gemini-prompt.ts,lib/gemini-schema.ts— model call + fallback, prompt rules, response schemalib/geocode.ts,lib/geo.ts— required-field check, haversine distances, live recomputelib/trips.ts— saved-trip validation and row summarieslib/supabase/— browser/server clients, session refreshlib/i18n.ts— all user-facing strings (it/en,MessageKeytype-safe)components/planner.tsx,components/result/— wizard + editable result viewcomponents/auth/,components/trips/— login, account, saved-trip UI
Architecture decisions and rationale live in AGENTS.md (single source of truth for contributors).
public/logo.png is part of the repo (it may be hidden from some workspace scans by ignore rules).