Easy-to-manage touch screens
무료 오픈소스 터치스크린으로 USB 설정 가이드, 드래그‑드롭 편집기, 로컬 제어를 제공하며 Home Assistant용으로 제작되었습니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Travel tracker under the Batch Apps umbrella at batchport.batch-apps.com. Track trips, destinations, and experiences with an interactive world map and stats dashboard.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
BatchPort is a personal travel tracker and installable progressive web app built on an interactive 3D globe. It lets you log every trip, destination, and experience, visualize journeys as great-circle arcs on a spinning globe, explore stats through a dedicated dashboard with charts, and maintain a bucket list of countries and places you want to visit. BatchPort is part of the Batch Apps umbrella (batch-apps.com), lives at batchport.batch-apps.com, and is invite-only. A public read-only demo runs at /demo, and any user can generate a shareable profile at /share/[slug].README 전문 보기 ↗BatchPort is a personal travel tracker and installable progressive web app built on an interactive 3D globe. It lets you log every trip, destination, and experience, visualize journeys as great-circle arcs on a spinning globe, explore stats through a dedicated dashboard with charts, and maintain a bucket list of countries and places you want to visit. BatchPort is part of the Batch Apps umbrella (batch-apps.com), lives at batchport.batch-apps.com, and is invite-only. A public read-only demo runs at /demo, and any user can generate a shareable profile at /share/[slug].
The floating control cluster is ranked by how often a control is actually reached for, capped at four buttons plus the search icon on the fullest surface and fewer everywhere else.
npm run cleanup-orphan-photos./api/search, session-scoped so RLS is the access boundary/api/weather/visit and cached in geocode_cache under the weather_visit provider for a year (past observations do not change)date_taken), the experiences logged that day with their ratings, and the observed weather line on the slide that opens each stop. Days with nothing in them are skipped, which is what keeps it a storyRatings say how good something was. They cannot say what the trip was about: a five star museum and a five star gelato are both five stars, and only one of them belongs on a share card. Curation is how you say which.
It is built around the three slots where a curated choice actually shows up, so what you are choosing, and what it will replace, is on screen rather than implied. A "Curate" action on the trip page, next to Story and Share, opens the panel.
Every picker shows the candidates together, with a position badge on the ones you have chosen, so first and second are distinguishable rather than both just "picked". Multi-item slots reorder by dragging or with the arrow buttons beside each one. Clearing a slot puts it back on automatic. Choices save as you make them.
Photos are shown at their real shape rather than cropped into a grid of identical squares, because shape is most of what you are choosing between: a portrait phone photo fills a story slide, and a wide landscape shot on the same slide is shown whole over a blurred blow-up of itself. Seeing which is which before you pick is the point. Nothing extra is downloaded to do it, and the picks you have made keep the same shapes in the row above, so the row reads as the same photographs.
Every candidate also carries a preview control that shows the photo exactly as a slide will draw it, at the shape a slide has on the screen you are using: filled edge to edge, or shown whole with bars, whichever it will actually be. It is the story's own image component doing the work rather than an imitation of it, so what the preview shows is what the slide does. The ones that will get bars are marked on the tile as well, so a gallery of forty does not have to be opened one at a time.
Preview the story. Composition problems are easiest to see in sequence, so the panel opens the real story, full screen, reflecting the choices you have just made rather than the ones the server last confirmed. Closing it comes straight back to the panel with everything where you left it, so the loop is adjust, watch, adjust.
A full-screen, paced recap of one year, opened from a card on the dashboard, a button on the stats page, and the same card on /demo and /share/[slug]. It reads as a sequence of single ideas rather than a dump of numbers: the year and a photograph from it, the distance covered with the same playful framing the stats page uses, the year drawing itself on a map, the trips, the best-rated moments with their photos, one or two things the data actually says, a scoreboard, and a closing slide about what is next.
The year card. A shareable image of the year through the same pipeline as the trip share cards: square and 9:16, both 2160px on the short edge. Where a trip card leads with a photograph, the year card leads with the map, because a year has no single cover and picking one trip's photo to stand for twelve months would be a claim the data does not support. The map is framed to the year rather than to the planet, so a year spent in Iceland fills the panel instead of scattering four dots across an empty ocean. Its routes are drawn in the same three transport languages the globe uses, so a year of trains looks like a year of trains. Under it: the countries, the headline numbers, and the year's best-rated moments. Offered from the recap's closing slide, and read-only like everything else here, so the demo and share surfaces can generate one too.
date_part filter Postgres cannot index, two queries ask for the handful of concrete anniversary dates in a 25 year look-back, and the context lookups that follow run only for rows that actually matcheduser_settings.home_geom via the EWKT conventionutc_offset_seconds the existing Open-Meteo climate lookup already returns (no new API, no new cache provider)Two exportable images, both drawn on a canvas from projected geometry rather than captured from the map, so neither is a screenshot and neither has a resolution ceiling set by the screen.
Map poster. An "Export poster" action on the stats page opens a dialog with a live preview drawn by the export's own code path, so what downloads is what was on screen. The choices are deliberately few: framing (a Robinson world map, or a globe centred on the traveller's own centre of gravity), theme (Midnight, the app's palette, or Paper, a warm off-white made to be printed), an optional stat block (countries, continents, trips, distance), and a title and subtitle that default to something sensible. Orientation follows the framing rather than being a third thing to choose: a world map is a landscape object and prints 16 x 12 inches, a globe is a square one and prints 12 x 16. Both are the same 12 inches on the short edge, so the type is the same physical size and the two hang as a pair.
The flat map crops the empty polar bands, widened automatically to cover any stop that falls outside them, which is what lets it run the full width of the page instead of floating in the middle of it. The globe framing says out loud how many stops fall on the hidden hemisphere and points at the world map, rather than quietly dropping them.
Output is a PNG at 300 DPI (4800 x 3600 px), or a PDF that states the physical page size so a print shop does not have to guess. Browsers cap canvas area and disagree about where (Safari stops at 16.7 megapixels, under a 300 DPI poster), so the dialog probes what this device can actually allocate, steps down through a ladder of DPIs, and reports the size it achieved instead of promising one it did not.
Per-trip share cards. A "Share" action next to the story, on the trip page, on the read-only demo and share surfaces, and on the story's closing slide. The card is the trip's cover photograph with the trip name and dates, the countries it crossed, the numbers (stops, countries, distance), the trip's best-rated experiences, and a small circular map of the route. Square for a feed, 9:16 for a story, both at 2160px on the short edge. Download always; where the browser supports sharing files, the OS share sheet as well.
The route map is an inset, not an overlay: a little under a third of the card's width, pushed into the top-right corner on a tighter margin than the text so it reads as anchored there rather than floating near it, with a soft drop shadow so it sits on the picture rather than looking stuck to it. Its arcs carry the same styling the globe gives them, so a ferry hop reads as a ferry hop on the card too. It is framed to the trip rather than to the planet, so a week in Europe fills the disc with the five countries it actually crossed instead of scattering five dots across a hemisphere; a trip too wide to fit falls back to the full hemisphere, and a trip in one city stops zooming before the coastline disappears. The title knows where the inset is, and on the rare card where a long name would run under it the name takes the column beside it instead.
The places line names countries, not cities, and never truncates. Countries are fewer and say more ("Netherlands, Germany, Czechia, Austria, Hungary" beats three city names and a "+8"). Seven of them fit on one line, because every size is tried on a single line before a second one is allowed, and where a wrap is genuinely needed the two lines are balanced by width rather than packing the first and orphaning whatever is left. A single-country trip shows its stops instead, since the country name alone would say almost nothing.
The foot of the card lists up to three best-rated experiences, name on the left and rating flush right, which fills the width where a single starred line left it looking unfinished. Fewer than three shows what exists, long names shrink to fit, and a trip nobody rated drops the block entirely rather than leaving a gap where it would have been.
Everything the card draws comes from the same trip payload the story already builds, so it costs no extra query on any surface. A cover that will not load is a designed state, not a broken one: the card falls back to its own gradient and says so.
Attribution. The compositions use no basemap tiles at all, only the bundled Natural Earth country outlines, so no tile provider's terms apply and there is nothing to attribute for the map beyond the credit the poster carries anyway. Where a share card's cover came from Wikimedia Commons, the licence's attribution condition does not stop applying because the image was redrawn onto a canvas, so the card prints the credit line.
/api/export?format=json|geojson, downloaded with a dated filename; the builders take no user parameter, so a request can only ever return the caller's own dataBatchPort works with no connection, which is when a travel app matters most: abroad, on a plane, standing somewhere you planned to be.
Reading offline. Opening the app with no signal lands on a saved copy of your account: every trip, its stops and experiences, the plan checklists with their day assignments, journal entries, and the globe with its country fills and arcs. The copy is a snapshot refreshed whenever you open the app online, held in IndexedDB, so it covers trips you never happened to open rather than only the pages you visited. The dark basemap and the country outlines are files that ship with the app, so the map draws with nothing to reach.
Writing offline. Four things a traveller does in the field keep working: checking a planned experience off, writing a journal entry, logging a new experience (including from nearby mode), and marking a bucket list item completed. Each one is queued on the device with a visible pending marker, and sent when you reconnect. A queued write is never dropped: it leaves the queue by succeeding or by you discarding it, a failed send keeps its error and stays in the list, and if the browser refuses to store it you are told it was not saved rather than told it was.
What needs a connection. Deleting anything, reordering, creating or editing trips and stops, assigning plan days, recording transport legs, settings, and photo uploads all refuse offline with a message naming what needs the connection, rather than queueing a change with conflict semantics nobody can predict. Discovery, weather, search of new places, attractions, and POI lookup simply have nothing to show. Nothing spins forever.
Conflict policy is last write wins, per row, stated in the pending panel.
A calm indicator, not a banner. A small chip in the nav appears when you are offline or something is waiting, and nothing at all otherwise. Tapping it lists every pending write, any that failed with why, and a retry.
Per-trip "Available offline". Every trip is already readable offline; this toggle stores that trip's photo thumbnails too, capped, removable, with the approximate size shown. It does not pre-download map tiles: MapTiler's terms allow keeping tiles you have already looked at and prohibit bulk download, so the map caches as you pan and the toggle says so rather than implying more.
Service worker hygiene. Caches are versioned and cleaned on activate. Navigations are network-first and never cached, so a deploy cannot leave a stale page on screen. Server actions, photo uploads, the session refresh, and the auth callback are passed straight through, untouched.
| Layer | Technology |
|---|---|
| Framework | Next.js 16.2.9 (App Router), TypeScript strict, React 19.2.4 |
| UI | shadcn/ui (Radix primitives), Tailwind CSS v4, Lucide icons, Sonner toasts |
| Database | Supabase (shared Batch Apps project), batchport schema, PostGIS |
| Map | MapLibre GL JS 5.24.0, native GeoJSON layers for pins and arcs, Protomaps PMTiles dark basemap |
| Charts | Recharts 3.9.0 |
| Geocoding | Photon (typeahead and POI), Nominatim (reverse), GeoNames countries.geojson (bundled) |
| Photos | Supabase Storage (batchport bucket), Wikimedia Commons API (Wikidata P18 property) |
| Resend | |
| Offline | Service worker (Cache Storage), IndexedDB snapshot and write queue |
| Hosting | Vercel |
All tables live in the batchport Postgres schema. Row-level security scopes every table to the authenticated user. A shared is_shared() function in Supabase allows anonymous SELECT when a user has public_share_enabled = true in user_settings or is_demo = true.
trips
destinations (ordered by order_index within a trip)
experiences (category, rating, optional coordinates)
trips: Top-level travel event with name, status (planned/ongoing/completed), start_date, end_date, notes, and cover_photo_id. start_date and end_date are maintained from the trip's destinations whenever any of them carry a date (earliest arrival to latest departure); they are only a user-entered value on a trip whose stops are undated. They stay stored rather than derived on read because the SQL stats views read them directly.
destinations: A stop within a trip. Stores a PostGIS geom geography(Point,4326) column; latitude and longitude are generated columns derived from geom and must never be inserted or updated directly. Also holds country_code, admin_region, arrival_date, departure_date, order_index, cover_photo_id, and notes. order_index is the route sequence every other feature reads, and it is renumbered to match arrival-date order on every destination write, so dating a stop reorders the route rather than leaving two competing notions of "next".
experiences: An activity at a destination. Has category_id, rating (smallint 1-10; each unit is half a star, so 10 = five full stars), visited_date, notes, an optional geom geography(Point,4326) set from POI search, and featured_rank (null = not featured, 1 = first) scoped to the owning trip.
categories: Static reference data: slug, label, icon, color, sort_order.
countries: Static reference data: code (ISO 3166-1 alpha-2), name. Used for the bucket list country dropdown and country name display.
bucket_list: Country or place items with type ("country" or "place"), country_code, place_name, optional geom (for place type), priority, target_date, fulfilled_trip_id, and fulfilled_at.
photos: Owner_type (trip/destination/experience), owner_id, source (upload/wikimedia/url), storage_path (for uploads), external_url (for wikimedia and url sources), attribution, order_index, and featured_rank (null = not featured, 1 = first) scoped to the owning trip.
geocode_cache: Cached API responses keyed by provider (photon, photon_poi, nominatim, wikimedia, the discover_* family, weather_visit) and query_norm. TTL: 30 days for geocoding responses, 90 days for Wikimedia and discovery responses, 365 days for observed weather (past observations are immutable).
journal_entries: One freeform entry per (user_id, trip_id, entry_date). Body text plus timestamps, with a unique constraint on the three-column key. Deliberately carries no destination_id: which stop a day belongs to is already determined by the destination arrival/departure ranges, so it is derived on read rather than stored where it could drift.
transport_legs: How one stop was reached from the one before it. Belongs to the ARRIVING destination, one row per stop (unique on destination_id), which is why it stores no origin: the order is already destinations.order_index, and a stored copy of it would drift the first time a stop moved. The leg on a trip's first stop is the journey out from home. Fields: mode (flight/train/bus/car/ferry/bike/walk/other, the only required one), carrier, duration_minutes, an optional distance_km override, and notes. trip_id is denormalized so a trip's or a user's legs read in one query.
user_settings: Per-user configuration: public_share_enabled, public_slug, is_demo, and the home location (home_geom geography(Point,4326) plus the home_name and home_country_code labels).
All stats come from SQL views and an RPC function in the batchport schema. The server Supabase client is already schema-scoped, so view names resolve without an explicit prefix.
| Name | Returns |
|---|---|
v_user_travel_summary | countries_visited, world_pct, continents_visited, total_trips, total_destinations, total_experiences, days_traveling |
v_experiences_by_category | slug, label, color, experience_count, avg_rating_stars |
v_country_frequency | country_code, country_name, trips, destinations |
v_yearly_breakdown | year, trips, countries, new_countries |
v_bucket_completion | total, fulfilled, completion_pct |
v_travel_extremes | northernmost_lat, southernmost_lat, easternmost_lng, westernmost_lng |
f_distance_traveled(p_user_id) | Total distance traveled in kilometers (RPC call via supabase.rpc) |
The one metric derived outside this layer is the distance-by-mode breakdown, which folds recorded transport legs against the same consecutive-stop great-circle distances the globe draws (getTransportBreakdown in lib/transport-data.ts). It has no view because it is a join of two small tables the page already needs, and it never changes the totals above.
Set these in .env.local (gitignored; never commit this file). See .env.local.example for the full list.
| Variable | Scope | Purpose |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | public | Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY | public | Supabase anon key |
NEXT_PUBLIC_APP_URL | public | Canonical app URL for auth redirects and absolute links (e.g. https://batchport.batch-apps.com) |
NEXT_PUBLIC_PMTILES_URL | public | Base URL for the PMTiles dark basemap; leave empty to use the bundled dark style |
NEXT_PUBLIC_MAPTILER_KEY | public | MapTiler API key (free tier) enabling the detailed basemap styles (streets, satellite, terrain) in the globe style switcher; leave empty to ship only the keyless dark default |
SUPABASE_SERVICE_ROLE_KEY | server only | Privileged Supabase access for admin operations (invites, geocode_cache writes, Storage cleanup) |
RESEND_API_KEY | server only | Resend API key for transactional email |
BATCHPORT_ADMIN_EMAIL | server only | Email address that receives access request notifications |
APPROVAL_SECRET | server only | 32-byte hex secret for signing HMAC approval/deny tokens (generate with: openssl rand -hex 32) |
NOMINATIM_USER_AGENT | server only | Contact string sent as User-Agent to Nominatim and Wikimedia APIs (e.g. BatchPort/1.0 ([email protected])) |
The app shell still renders when NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY are absent; the session refresh in src/proxy.ts is skipped and the app stays signed out.
npm run dev # Start the dev server at http://localhost:3000
npm run build # Production build (type-checks the whole project)
npm run start # Serve the production build
npm run lint # Run ESLint
npm run seed # Import seed trips for a user (requires the dev server running at localhost:3000)
# Wipe the public demo account and rebuild it from the fictional showcase
# dataset. Refuses to run without --reset, and refuses any user id other than
# the demo account. Leaves user_settings intact. Requires the dev server.
npm run seed-demo -- --reset
# Rebuild the landing hero's static fallback (src/lib/mock-travel-data.ts) from
# the demo fixture. Re-run it whenever scripts/demo-dataset.ts changes.
npm run generate-mock-globe
npm run backfill-photos # Backfill Wikimedia cover photos for existing destinations
npm run backfill-thumbnails # Generate gallery thumbnails for photos uploaded before thumbnails existed
npm run backfill-exif # Backfill date_taken and GPS coordinates from stored originals
npm run seed-countries # Populate the countries reference table
npm run seed-cities # Populate the cities table from GeoNames cities15000 (powers Discovery top cities)
npm run setup-shares # Initialize user_settings rows for existing users
# Bring existing trips' stored start_date/end_date and destinations.order_index
# in line with their stops. The app syncs these on every destination write and
# derives the same answer on read, so the UI is already correct; this is for the
# SQL stats views, which read the stored columns directly. Run once after
# deploying derived trip dates. Idempotent.
npm run resync-trip-schedules -- --dry-run
npm run resync-trip-schedules
# Deterministic checks for the Year in Travel derivation: which years are
# offered, how a year slices, what counts as planned, what a thin year
# produces, and that the year's photograph resolves to one of its trips'
# heroes. Pure functions against fixtures, no database and no dev server.
npm run check-year-recap
# Deterministic checks for the curation model: rank ordering, the caps
# (including a stop's, derived from its own day slides), the two photo slots
# and their backwards compatibility, what each slot holds and what it falls
# back to, and what the story, share card, and recap actually select,
# including the uncurated path on every one of them.
# Pure functions against fixtures, no database and no dev server.
npm run check-curation
# Deterministic checks for transport arc families on the drawn maps: the mode
# to family mapping and its dash patterns, that both exported cards style each
# hop by the mode on its arriving stop, that the poster stays uniform, and that
# an unannotated trip or year draws exactly the blue route it always did.
npm run check-map-arcs
# Deterministic checks for day-to-stay resolution: which stop owns a day, the
# boundary between back-to-back stays, gaps and nesting, and above all a trip
# that visits the same city twice, whose two stays must never merge.
npm run check-stays
# Deterministic checks for the recap map slide's playback clock: that every
# speed reaches the end of a real multi-trip year, that changing speed
# mid-flight rescales what is left rather than truncating it, that skip lands
# on the finished year, and that rebuilding the canvas mid-playback resumes.
npm run check-year-map-playback
# Find photo rows whose owner entity no longer exists. Report only:
npm run cleanup-orphan-photos -- --dry-run
# Delete the rows and their upload Storage objects (idempotent):
npm run cleanup-orphan-photos
src/
app/
(app)/ Authenticated route group (session required)
layout.tsx App shell: AppNav header + optional demo read-only banner
actions.ts signOut server action
dashboard/
page.tsx Main dashboard: globe, stats overview, trip list, bucket preview
stats/page.tsx Full stats dashboard with all charts and extremes
bucket-list/page.tsx Bucket list management board
settings/page.tsx Share settings (public toggle, slug)
trips/
new/page.tsx New trip form
[id]/page.tsx Trip detail: destination list, cover banner, photo section
[id]/edit/page.tsx Edit trip form
[id]/destinations/
new/page.tsx Add destination form
[destId]/page.tsx Destination detail: experiences and photos
[destId]/edit/page.tsx Edit destination form
auth/
callback/route.ts PKCE code exchange after Supabase invite
setup-password/page.tsx Password setup page for new invite recipients
demo/page.tsx Sessionless public demo (SharedProfileView)
share/[slug]/page.tsx Public share profile with OG/Twitter metadata
api/
approve-access/route.ts HMAC-verified invite trigger (GET)
deny-access/route.ts HMAC-verified denial email (GET)
request-access/route.ts Access request handler: validates input, emails admin (POST)
geocode/
search/route.ts Photon typeahead proxy with geocode_cache
lookup/route.ts Nominatim reverse lookup proxy with geocode_cache
poi/route.ts Photon POI search proxy with geocode_cache
weather/
visit/route.ts Observed daily weather for a coordinate and past date range
photos/
wikimedia/route.ts Wikimedia metadata lookup (server-side)
wikimedia/proxy/route.ts Image proxy to avoid CORS when displaying Wikimedia photos
offline/page.tsx Offline shell (public, renders from the IndexedDB snapshot)
api/offline/snapshot/route.ts The whole account in one document, for offline reads
layout.tsx Root layout: Inter font, dark class, Toaster, ServiceWorkerRegister
manifest.ts Web app manifest served at /manifest.webmanifest
globals.css Tailwind v4 tokens including --brand color (#2563EB)
components/
app-nav.tsx Top navigation with sign-out and mobile hamburger
location-search.tsx Geocoding typeahead component
poi-search.tsx POI search component for experience dialog
rating-input.tsx Half-star rating input (1-10 scale)
rating-display.tsx Read-only half-star rating display
category-icon.tsx Category icon renderer
map/
globe.tsx MapLibre GL JS globe: map lifecycle, interaction, mode wiring
globe-types.ts Public data shapes (destinations, arcs, bucket places, selection)
globe-sources.ts Pure GeoJSON builders and popup HTML helpers
globe-layers.ts Every runtime overlay layer (fills, arcs, pins) plus sky
basemaps.ts Basemap catalog, style resolution, per-style overlay themes
map-controls.tsx Floating control cluster: mode buttons + settings popover
dashboard-globe.tsx Globe wrapper with stats overlay and country drill-down panel
country-drilldown.tsx Side panel listing a country's destinations grouped by trip
replay-controls.tsx Replay mode overlay: readout and transport bar
unlocated-photos-modal.tsx Off-map grid of photos with no resolvable location
use-replay.ts Replay timeline engine
use-photo-mode.ts Photo map mode: clustering, markers, enter/exit
use-attractions.ts Wikipedia geosearch attraction markers
use-nearby.ts Nearby mode: device fix, you-are-here marker, enter/exit
use-detail-basemap.ts Detailed-basemap loan for the street-level modes
nearby-panel.tsx Nearby status card: context, checkoff prompt, actions
log-here-sheet.tsx Compact sheet that logs an experience at your coordinates
use-globe-fullscreen.ts Fullscreen state with panel-first Escape handling
map-utils.ts Brand colour, country match filters, feature bounds
map.css MapLibre popup dark theme styles
discover/
discovery-host.tsx Page-level discovery panel host and context
discovery-panel.tsx Country, city, and POI views
globe-search.tsx Collapsible search overlay for the globe
climate-line.tsx Monthly climate line for a city
country-facts.tsx Currency, languages, driving side, plug and voltage
trips/
dashboard-trip-card.tsx Trip card for the dashboard list
dashboard-trips.tsx Trip list section with Add Trip link
trip-form.tsx Trip create/edit form
delete-trip-button.tsx Confirmed delete with alert dialog
status-badge.tsx Planned/ongoing/completed badge
transport-leg.tsx Leg row between stops, plus the mode entry sheet
destinations/
destination-form.tsx Destination create/edit form with location search
delete-destination-button.tsx Confirmed delete
experiences/
experience-dialog.tsx Modal dialog for create/edit experiences with POI search
experiences-section.tsx Destination page section listing all experiences
weather/
visit-weather.tsx "While you were here" observed-weather line and day expander
photos/
photo-upload.tsx File picker with client-side resize and Storage upload
photo-gallery.tsx Grid gallery with lightbox, feature toggle, and cover actions
slide-image.tsx Full-screen photo: full-quality swap and the cover/contain fit rule
photo-banner.tsx Full-width cover photo banner with overlay
cover-photo-picker.tsx Pick or change the cover from existing photos
destination-photos.tsx Destination page photo section
trip-photos.tsx Trip page photo section (untagged + tagged by destination)
stats/
stats-grid.tsx Summary stat cards
yearly-chart.tsx Year-by-year bar chart
category-chart.tsx Experience count and rating by category
country-chart.tsx Visits per country bar chart
travel-map-stats.tsx Travel extremes display (N/S/E/W)
transport-breakdown.tsx Distance by transport mode, with the CO2e estimate
bucket-progress.tsx Bucket list completion bar and counts
chart-card.tsx Shared chart wrapper card
stat-card.tsx Individual stat number card
scoreboard.tsx Scoreboard lead (number and unit, fitted) and supporting strip
bucket-list/
bucket-list-board.tsx Full bucket list page with add/edit/fulfill
bucket-item-card.tsx Individual bucket item card
bucket-item-dialog.tsx Create/edit dialog with country or place selection
country-combobox.tsx Country picker for bucket items
fulfill-dialog.tsx Mark item fulfilled with trip selection
poster/
poster-export.tsx Poster dialog: live preview, options, PNG/PDF download
year/
year-recap-launcher.tsx Entry card and button, hidden when there is no year
year-recap.tsx The full-screen paced recap and its slide views
year-map-slide.tsx The year animating on a canvas via the replay engine
year-share-card.tsx Year card dialog and its launcher
share/
trip-share-card.tsx Per-trip share card dialog and its launcher
shared-profile-view.tsx Shared layout: globe, stats, trip list (used by /demo and /share/[slug])
share-globe.tsx Globe instance for public share (read-only, no drilldown links)
shared-trip-card.tsx Read-only trip card for share view
settings/
share-settings-form.tsx Public share toggle and slug form
auth/
landing-actions.tsx Request access and sign-in buttons on the landing page
ui/ shadcn/ui primitives (button, card, dialog, input, popover, etc.)
date-range-picker.tsx Single-calendar start/end range picker over YYYY-MM-DD strings
service-worker-register.tsx Registers public/sw.js and keeps it fresh across deploys
offline/
offline-shell.tsx Offline trip list and globe, read from IndexedDB
offline-trip.tsx Offline trip view with queued checkoff and journal writes
offline-status.tsx Offline chip and the pending write queue panel
trip-offline-toggle.tsx Per-trip photo thumbnail caching
lib/
types.ts Domain types mirroring the Postgres schema
constants.ts DEMO_USER_ID constant
utils.ts cn() Tailwind class merge helper
current-user.ts requireUser(): resolves auth session and returns supabase + user
demo.ts isDemoUser() and the shared read-only message
demo-guard.ts isDemoBlocked() helper for server actions
format.ts formatDateRange(), flagEmoji()
geo.ts pointEwkt() and haversineKm() shared helpers
revalidate.ts revalidateAppData(): app-wide cache purge after mutations
action-result.ts ActionResult type ({ ok: true } | { error: string })
trips.ts Trip data layer (getTrip, getTripOptions, create, update, delete)
trip-dates.ts Trip range derivation and chronological stop ordering (pure)
trip-schedule.ts Writes the derived range and order_index back after a stop changes
stays.ts Which stay owns a calendar day, and the boundary rule (pure)
destinations.ts Destination data layer (create sends EWKT geom)
experiences.ts Experience data layer and getCategories()
bucket-list.ts Bucket list data layer and autoFulfillBucketItems()
bucket-format.ts Bucket item display formatting helpers
curation.ts Curation model, slots, and comparators (pure, client-safe)
curation-slots.ts The three slots of one trip and their automatic answers (pure)
bucket-hero.ts Bucket list hero image lookup and session cache (client)
photos.ts Photo helpers: resizeImage, uploadPhoto, getPhotoUrl (client-safe)
photos-data.ts Server photo reads and autoPopulateDestinationCover()
photo-cleanup.ts Shared photo deletion: rows, Storage, cover pointers, owner lists
photo-map-data.ts Photo map mode data layer: coordinate resolution per photo
map-data.ts Globe data layer: getMapData() fetching destinations, arcs, bucket codes
replay.ts Replay timeline construction from destinations
year-recap.ts Year in Travel: year list, slicing, insights, slides (pure)
motion.ts prefers-reduced-motion check, shared by the animated surfaces
discover.ts Discovery data layer: countries, cities, POIs, climate
day-plan.ts Day assignment helpers for planned experiences
stats-data.ts Stats data layer: all SQL view queries and f_distance_traveled RPC
stats-format.ts Stats number formatting helpers
share-data.ts Public share data layer: getSharedProfile(), getProfileTrips()
share-settings.ts getShareSettings() read side using admin client
weather.ts Observed weather at time of visit (ERA5 archive, cached)
nearby.ts Nearby proximity helpers and radii (client-safe, pure)
nearby-data.ts Planned experiences with coordinates, for the checkoff prompt
transport.ts Transport modes, arc families, formatting, distance fold (pure)
transport-data.ts Transport server reads and the distance-by-mode breakdown
geocode.ts Geocoding cache helpers: readCache, writeCache, parsePhoton, parseNominatim
wikimedia.ts Wikimedia P18 lookup: getWikimediaPhoto() with 90-day cache
access-token.ts HMAC-SHA256 token generation and verification for invite links
poster/
projection.ts Robinson and orthographic projections, path clipping (pure)
countries.ts Country outline loader, cached per page
theme.ts Poster palettes (Midnight and Paper)
draw-map.ts Country fills, arcs, and pins painted into a fitted frame
poster.ts The printable poster: layout, render, PNG/PDF
poster-data.ts Poster inputs assembled from map data and stats
share-card.ts Per-trip share card, derived from a StoryTrip
card-parts.ts Places line, highlights row, stat tiles shared by both cards
year-card.ts Year card, derived from a YearRecap
year-map.ts The recap's animated map: cached base plus a per-frame overlay
canvas.ts Canvas plumbing: DPI probe, fonts, CORS images, download, share
pdf.ts One-page PDF wrapper stating the physical page size
landing-data.ts Landing hero globe data: cached anon read of the demo account
mock-travel-data.ts GENERATED static fallback for the landing hero (see generate-mock-globe.ts)
offline/
types.ts Offline snapshot shape (client-safe)
queue-types.ts Queued write vocabulary, labelling, coalescing (pure)
constants.ts Cache names, staleness, per-trip photo bounds
db.ts IndexedDB wrapper (meta and queue stores)
queue.ts The write queue store and the FIFO replay loop
snapshot.ts Snapshot fetch, storage, and staleness
trip-cache.ts Per-trip photo cache warm and removal
use-offline.ts Online status, queue, and connection guard hooks
forget.ts Wipes offline storage on sign-out
actions/
trips.ts Trip server actions
destinations.ts Destination server actions (triggers Wikimedia + bucket auto-fulfill)
experiences.ts Experience server actions
photos.ts Photo record server actions (insert, setCover, retag, delete + Storage cleanup)
curation.ts Curation slot writes: trip hero, a stop's photos, the highlights
bucket-list.ts Bucket list server actions
transport.ts Transport leg server actions (upsert by stop, delete)
share-settings.ts Share settings server action
offline.ts Replay of one queued offline write (idempotent)
utils/supabase/
client.ts Browser Supabase client (batchport schema)
server.ts Server Supabase client (batchport schema, async cookies())
public.ts Sessionless anon client (batchport schema, no cookies; usable inside a cache scope)
admin.ts Service-role client (not schema-scoped; must call .schema("batchport") per query)
proxy.ts Session refresh and route protection (Next.js 16 proxy convention)
public/
sw.js Service worker: precache, offline fallback, runtime caches
data/countries.geojson Country polygons for the globe (ISO_A2_EH property for code matching)
styles/dark-style.json MapLibre dark base style (used when PMTiles is not configured)
icons/ PWA icons: 48x48 through 512x512, maskable 192x192 and 512x512
scripts/
generate-icons.mjs Regenerates placeholder PWA icon PNGs
seed-trips.ts Imports travel history for a user (idempotent per trip name + user_id)
seed-demo.ts Resets the demo account and reseeds the fictional showcase dataset (--reset required)
demo-dataset.ts The demo showcase fixture (trips, destinations, experiences, bucket items)
generate-mock-globe.ts Regenerates src/lib/mock-travel-data.ts, the landing hero's static fallback
check-year-recap.ts Asserts the Year in Travel slicing and edge cases against fixtures
check-curation.ts Asserts the curation model, its slots, and every surface that selects from them
check-map-arcs.ts Asserts transport arc families on the exported cards, and the poster's uniform arcs
check-stays.ts Asserts which stay owns a day, including a trip that visits one city twice
check-year-map-playback.ts Asserts the recap map slide's clock: every speed, mid-flight changes, skip, rebuilds
backfill-photos.ts Backfills Wikimedia cover photos for existing destinations
backfill-thumbnails.ts Generates {storage_path}_thumb thumbnails and sets photos.thumb_path
backfill-exif.ts Backfills date_taken and GPS from stored originals
fix-gps-signs.ts Repairs EXIF GPS hemisphere signs on affected photos
cleanup-orphan-photos.ts Finds and removes photo rows whose owner no longer exists (--dry-run)
seed-countries.ts Populates the countries reference table
seed-cities.ts Populates the cities table from GeoNames cities15000
setup-share-settings.ts Creates user_settings rows for existing users
resync-trip-schedules.ts One-off: aligns stored trip dates and order_index with each trip's stops
sql/ One-off migrations to run in the Supabase SQL editor
The geocode_cache.provider check constraint must allow every provider the app
writes: photon, photon_poi, nominatim, wikimedia, discover_country,
discover_cities, discover_city, discover_poi, discover_climate, and
discover_geo. If the constraint is stale, cache writes fail silently and
every lookup hits the upstream API. Run
scripts/sql/2026-07-22-discover-geo-provider.sql in the Supabase SQL editor
to widen it (it supersedes the provider constraint from
2026-07-20-planner-completion.sql, which superseded
2026-07-18-discover-poi-provider.sql and
2026-07-15-geocode-cache-providers.sql). Note that
2026-07-20-planner-completion.sql also adds the experiences.planned_day
column (below), which the 07-22 file does not replace.
Planned trips are excluded from visited countries and stats in the app data
layer, but the SQL stats views also need the planned filter applied. See
scripts/sql/2026-07-16-stats-exclude-planned-trips.sql for the per-view
change to run in the Supabase SQL editor.
The trip planner needs the experiences.status column ('planned' | 'done')
and the matching stats-view filters. Run
scripts/sql/2026-07-18-experience-status.sql in the Supabase SQL editor.
Until it runs, the app degrades: experience writes retry without the status
key, reads treat a missing status as 'done', and the stats layer subtracts
planned experiences app-side.
Day planning needs the experiences.planned_day column (integer; day 1 = the
destination's arrival date, null = unassigned). It is added by
scripts/sql/2026-07-20-planner-completion.sql alongside the climate cache
provider. Until it runs, day-assignment writes fail with a friendly error,
reads treat a missing value as unassigned, and the plan block still renders as
a flat list. Climate lines and country practical facts (currency, languages,
driving side, plug/voltage from Wikidata) need no schema change; climate uses
the Open-Meteo ERA5 archive and caches under the discover_climate provider,
and the facts ride along in the existing discover_country aggregate (its
cache key was bumped so stale pre-facts payloads refresh).
The home location needs the home_name and home_country_code label columns
on user_settings (and home_geom, if the column is missing). Run
scripts/sql/2026-07-29-home-location.sql in the Supabase SQL editor. Until it
runs the feature degrades: saving a home retries without the label columns, so
the point is still stored and every distance feature works, but Settings shows
coordinates instead of a city name.
The observed-weather line needs the weather_visit cache provider allowed on
geocode_cache. Run scripts/sql/2026-08-01-weather-visit-provider.sql in the
Supabase SQL editor (it supersedes 2026-07-22-discover-geo-provider.sql,
widening the same constraint). Until it runs the line still renders correctly,
but nothing caches: every view re-hits the Open-Meteo archive.
The travel journal needs the journal_entries table, its RLS policies (which
include an is_shared() SELECT so the story renders on /demo and /share), and
its updated_at trigger. Run scripts/sql/2026-08-02-journal.sql in the
Supabase SQL editor. Until it runs the app degrades: the journal section
renders read-only with a one-line note, saving reports "Journaling is not set
up on this database yet" rather than pretending to have saved, and the story
view simply has no journal text to interleave. The trip story and On This Day
need no migration of their own.
Transport legs need the transport_legs table, its RLS policies (including an
is_shared() SELECT so legs render on /demo and /share), and its updated_at
trigger. Run scripts/sql/2026-08-03-transport-legs.sql in the Supabase SQL
editor. Until it runs the app degrades cleanly: the trip page leaves the leg
rows out entirely rather than offering a control that cannot save, arcs on the
globe keep their original styling, the stats page shows no distance breakdown,
and npm run seed-demo still seeds a complete demo account without legs.
Curation needs two migrations, in order. First
scripts/sql/2026-08-07-featured-curation.sql, which adds a featured_rank smallint column to both batchport.experiences and batchport.photos plus
the two partial indexes that serve it. Then
scripts/sql/2026-08-08-curation-slots.sql, which adds
batchport.photos.featured_slot text ('hero' or 'stop') so a photo's rank
says which of the two photo slots it holds, and its index. Run both in the
Supabase SQL editor.
Until they run the app degrades cleanly: every surface falls back to ratings
and the existing photo order exactly as it did before curation existed, photo
reads retry without the columns, and saving a slot reports "Curation is not set
up on this database yet" rather than pretending to have saved. A photo row
carrying a rank but no slot (anything curated before the second migration)
reads as a stop pick, which is what a bare rank meant then, so nothing that was
already curated changes. npm run seed-demo also still seeds a complete demo
account, just with nothing curated.
Nearby mode needs no migration. It reads the destinations the globe already has
and the planned experiences that carry a geom, so it works as soon as the
experience status column exists (2026-07-18-experience-status.sql); before
that every experience reads as done and the checkoff prompt simply never fires.
Global search works with no migration at all. When the tables grow, run
scripts/sql/2026-07-29-search-indexes.sql to add the pg_trgm GIN indexes that
serve its unanchored ILIKE matching; without them the same rows come back via a
sequential scan.
Clone the repository and install dependencies:
npm install
Copy the environment template and fill in real values:
cp .env.local.example .env.local
In the Supabase dashboard, expose the batchport schema under Settings, API, Exposed schemas. All Supabase clients in this app are scoped to that schema.
Run the seed scripts after your database migrations are applied:
npm run seed-countries # Populate the countries reference table
npm run setup-shares # Initialize user_settings rows for existing users
Start the dev server:
npm run dev
Open http://localhost:3000. Unauthenticated visitors land on the rotating-globe landing page.
.env.local.example in the Vercel project settings under Environment Variables.https://batchport.batch-apps.com/auth/setup-password (and the http://localhost:3000 equivalent for local or preview use). Missing this entry causes Supabase to strip the redirectTo parameter and silently break the invite flow.prefers-reduced-motion. Advancing is always yours: swipe, arrow keys, click zones, or the buttons.