8.2 KiB
8.2 KiB
System Patterns — Strava Frontend
Architecture (SPA layering)
src/
main.ts - app bootstrap, axios instances, providers
App.vue - root component
router/ - vue-router config (lazy pages)
layouts/ - AppLayout (authenticated shell), AuthLayout, RouterBypass
pages/ - route components (workouts/, routes/, auth/, preferences/, admin/)
components/ - reusable UI (navbar/, sidebar/, icons/, app-layout-navigation/)
stores/ - Pinia stores (user-store, global-store)
i18n/ - vue-i18n setup + locales/*.json
services/ - axios helpers, vuestic-ui config (global-config, themes)
scss/ - global styles, vuestic-sass, icon fonts
server/ - SSR Express server (NOT type-checked by vue-tsc, excluded from tsconfig include)
index.ts - Express app: SSR routes, helpers, port 3001
template.ts - HTML shell with SEO meta, OG tags, JSON-LD, asset tags
api.ts - Axios client for backend API (getPublicWorkouts, getPublicWorkout)
nginx.conf - Reverse proxy: SSR routes → :3001, everything else → SPA static files
Dependency direction: pages -> components/stores/services. Pages own business logic; components are presentational. server/ is standalone (Express, runs via tsx).
API access pattern
- Two Axios instances created in
src/main.ts:axiosAuth(addsAuthorization: Bearer <token>fromlocalStorageon every request) andaxiosPublic. Both have a shared response error interceptor: 401 ->localStorage.clear()+ redirect tologinroute. - Instances are provided app-wide:
app.provide('axiosAuth', ...)/app.provide('axiosPublic', ...). Components consume withinject('axiosAuth') as AxiosInstance. - Base URL is a hardcoded
HOSTconstant insrc/main.ts(https://cycle-rider.ru; localhost variant commented out). All backend paths are under/api/v0. - Auth check on startup: if
localStorage.tokenexists, GET/api/v0/auth/check; on failure clear storage.
Data fetching pattern
- No central API layer: components/composable
.tsfiles (e.g.src/pages/workouts/components/GetWorkout.ts) callaxiosAuth.get(url)directly and map the response into view state. - Workout detail data shape:
{ workout, results: [{ timestamp, longitude, latitude, elevation, power, heart_rate, speed }, ...] }— mapped into Chart.js datasets + map line coordinates.
State management
- Pinia with options-style stores.
useUserStorehydrates fromlocalStoragekeysuser,profile,attachmentson store init (no persistence plugin).useGlobalStoreholds sidebar state. - Auth data lives in
localStorage(keys:token,user,profile,attachments).
Routing
- History-mode router (
createWebHistory), all pages lazy-imported. - Two layouts:
AppLayout(sidebar + navbar shell) for authenticated pages;AuthLayoutfor auth pages. Catch-all redirects toexplore(=pages/workouts/Feed.vue). - Legacy redirect:
/dashboard→explore. - Top-level route
workout_public_itemat/public/workouts/:id— NOT nested under AppLayout (accessible without auth, SSR-rendered). - No global auth guard — unauthenticated handling is done via the 401 interceptor.
SSR (Server-Side Rendering)
- Express server (
server/index.ts) on port 3001 (envSSR_PORT). - SSR routes:
GET /(landing, 302→/exploreiftokencookie present),GET /explore,GET /public/workouts/:id,GET /routes,GET /sitemap.xml,GET /robots.txt,GET /health. - HTML rendered via
renderTemplate()fromserver/template.ts— full SEO meta (title, description, canonical, OG, JSON-LD, yandex-verification). - Asset tags extracted from
dist/index.html(hashed filenames); falls back to dev script tag ifdist/absent. - API data fetched server-side via
server/api.ts(Axios toVITE_APP_API_URL||https://cycle-rider.ru). - Nginx proxies SSR routes to
:3001; all other paths servedist/index.html(SPA fallbacktry_files). SSR_BASE_URLenv controls canonical/OG URLs (defaulthttps://cycle-rider.ru).
Charts & maps
- Chart.js via
vue-chartjs(+chartjs-adapter-moment,chartjs-plugin-zoom,chartjs-chart-geofor elevation). Chart building lives insrc/pages/workouts/components/*.ts. - Yandex Maps via
vue-yandex-maps(createYmapswith a hardcoded API key inmain.ts). Route polyline drawn from workoutresultscoordinates. - Typing contract (since vue 3.5 strict checks):
vue-yandex-maps:settingscoordinates must be theLngLat = [lon, lat, alt?]tuple type — plainnumber[]refs failvue-tsc. Pattern:ref<LngLat>([lon, lat])+as LngLatcasts in templates (seepages/routes/Route.vue,pages/workouts/components/WorkoutItem.vue). - Custom chart controllers extend
chart.jscontrollers (LineWithLineControllerinLineWithLineChart.ts); the map-sync plugin is read fromchart.config.pluginsby id — use non-null assertions onchart.config.data!(do NOT reintroduce@ts-ignore, lint bans it).
Styling
- Vuestic UI (config in
src/services/vuestic-ui/global-config.ts, themes inthemes.ts, custom icons registered inicons-config/). - Tailwind CSS with Vuestic CSS variables as color tokens (
--va-primaryetc. intailwind.config.js). Custom font-size tokens:tag,regularSmall/Medium/Large. - Global SCSS in
src/scss/main.scss; icon fonts insrc/scss/icon-fonts/.
i18n
vue-i18nin composition mode (legacy: false), locale and fallback =ru. Locales auto-loaded fromsrc/i18n/locales/*.jsonviaimport.meta.glob+@intlify/unplugin-vue-i18n/viteplugin.
Error handling
- Central axios interceptor logs and re-throws; 401 triggers logout redirect. Components typically use
.then/.catchchains (callback style, not async/await) — keep the existing style when modifying.
Lint / type-check pipeline
- Flat ESLint config (
eslint.config.mjs, eslint 9 + typescript-eslint 8):tseslint.configs.recommended+pluginVue.configs["flat/essential"]. CRITICAL: the vue flat preset leaves espree as inner parser — the TS parser MUST be attached for**/*.vuevialanguageOptions.parserOptions.parser(from thetypescript-eslintCJS default import). - Override style: file-targeted blocks with justifying comments ONLY (no global rule disabling, no inline eslint-disable). Existing exceptions:
src/main.ts(any/unused — interceptor debt),src/pages/**/*.vue(multi-word names),Logout.vue(valid-template-root),workouts/components/WorkoutItem.vue(no-mutating-props — local-state extraction pending). ban-ts-commentis enforced:@ts-ignoreis not allowed; use@ts-expect-erroror proper typing (non-null assertions).no-unused-varshonorsargsIgnorePattern: "^_".yarn lintrunsprelint: prettier --write .— running lint auto-reformats ~30 files; commit the formatting as part of the change.
Build / deploy
- Vite 5 build with
vue-tsc 2 --noEmittype-check inyarn build. Dev and Docker both use yarn v1 (Docker:node:20-alpine,COPY package.json yarn.lock,yarn install --frozen-lockfile). - Docker multi-stage: Stage 1 (build) —
node:20-alpine, yarn install,yarn build. Stage 2 (runtime) —node:20-alpine+ nginx, copiesdist/to nginx html root, copiesserver/+node_modules/, CMD runsnpx tsx server/index.ts & nginx -g 'daemon off;'. nginx.conf: SSR proxy for/explore|/routes|/sitemap.xml|/robots.txt|/public/workouts/|/→:3001;/assets/30d immutable; SPA fallbacktry_files $uri /index.html.run.shlauncher for local:cd /app && npx tsx server/index.ts & nginx -g 'daemon off;'.serve -s ./distfor CI preview (static only, no SSR).tsxis independencies(not devDependencies) — needed in the runtime container.- ⚠️ Legacy
package-lock.jsonis STALE and unused (Docker no longer copies it) — do not runnpm installin this repo; removal is a separate cleanup task. - Version ceilings (learned the hard way): vue-tsc 2.x supports TypeScript up to 5.8 — TS 5.9 crashes it (
Search string not found: "supportedTSExtensions"); pin"typescript": "5.8"no-caret. sass ≥1.7x latest requires node ≥20.19 — verify local dev node before bumping. - yarn 1.22 has no
upalias — useyarn upgrade.