Skip to main content
SmartMove is a single-page application (SPA) built with React 18 and deployed on Vercel. All transit data is fetched directly from the South Tyrol EFA API. User data is stored locally first and optionally synced to Supabase when the user is authenticated.

Stack overview

LayerTechnology
FrameworkReact 18 + React Router 7
Build toolVite 6
StylingTailwind CSS 4 + Radix UI primitives
MapsLeaflet + React-Leaflet
Auth & databaseSupabase (PostgreSQL + Auth + RLS)
AnimationsMotion (Framer Motion)
Transit dataEFA API (efa.sta.bz.it)
HostingVercel

Application layers

Browser
├── React SPA (React 18 + React Router 7)
│   ├── Components (screens, UI)
│   ├── i18n (hook-based, JSON translation files)
│   └── Services
│       ├── efa.ts         → EFA API (transit data)
│       ├── storage.ts     → localStorage + cloud sync
│       ├── supabase.ts    → Supabase client
│       └── notificationService.ts

├── localStorage           ← immediate source of truth

└── Supabase (PostgreSQL + Auth + RLS)
    └── cloud sync when authenticated

Persistence model

localStorage is the primary data store. Every read and write goes through storage.ts, which provides typed accessors for all persisted keys. When a user is authenticated, every write to localStorage also triggers an async upsert to Supabase via syncKeyToCloud(). On login, syncAllFromSupabase() pulls the latest cloud state into localStorage. This means:
  • The app works fully offline without an account
  • Cloud sync is additive — it never blocks the UI
  • localStorage is always the fast path for reads

Event-driven state

Components do not share React context or a global store for persisted data. Instead, storage.ts dispatches custom window events on every write:
window.dispatchEvent(new Event('recent-stops-changed'));
window.dispatchEvent(new Event('liked-routes-changed'));
window.dispatchEvent(new Event('theme-changed'));
Components add listeners with window.addEventListener and re-read from storage.ts when an event fires. This keeps components decoupled while still reacting to changes made by other parts of the app.

Routing

React Router 7 handles all client-side navigation via createBrowserRouter. The Layout component wraps all authenticated screens and renders the shared navigation shell.
PathComponentPurpose
/HomeScreenRoute search
/resultsResultsScreenConnection results
/detail/:idRouteDetailScreenFull trip detail
/r/:idRouteDetailScreenShared route link
/mapLiveMapScreenInteractive transit map
/accountAccountScreenUser profile and settings
/stoerungenServiceAlertsPageService disruptions
/loginLoginScreenAuthentication
/signupSignUpScreenRegistration

Transit data

All transit data comes from the South Tyrol EFA API at https://efa.sta.bz.it/apb. The efa.ts service layer handles stop autocomplete, connection search, departure boards, and service alerts. It normalizes API inconsistencies and maps transport modes to a consistent set:
  • RAIL — trains (SAD, Trenitalia)
  • BUS — buses (SASA, SAD coaches)
  • GONDOLA — cable cars and aerial tramways
  • WALK — walking segments
The EFA service also handles bilingual query translation automatically. When a user types a German place name, the service also searches the Italian variant (and vice versa) in parallel to maximize stop coverage.

i18n

The app ships with four locales: German (de), Italian (it), English (en), and Ladin (lad). A custom hook reads the active language from localStorage and provides typed t() translation lookups. EFA API queries adapt to the selected language — German and Italian are queried directly; Ladin and English fall back to German for transit data.

Data persistence

localStorage keys, window events, and Supabase sync in detail.

Transit API

EFA integration, stop search, and transport mode mapping.

Build docs developers (and LLMs) love