strava-frontend/.roo/memory-bank/systemPatterns.md

6.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

Dependency direction: pages -> components/stores/services. Pages own business logic; components are presentational.

API access pattern

  • Two Axios instances created in src/main.ts: axiosAuth (adds Authorization: Bearer <token> from localStorage on every request) and axiosPublic. Both have a shared response error interceptor: 401 -> localStorage.clear() + redirect to login route.
  • Instances are provided app-wide: app.provide('axiosAuth', ...) / app.provide('axiosPublic', ...). Components consume with inject('axiosAuth') as AxiosInstance.
  • Base URL is a hardcoded HOST constant in src/main.ts (https://cycle-rider.ru; localhost variant commented out). All backend paths are under /api/v0.
  • Auth check on startup: if localStorage.token exists, GET /api/v0/auth/check; on failure clear storage.

Data fetching pattern

  • No central API layer: components/composable .ts files (e.g. src/pages/workouts/components/GetWorkout.ts) call axiosAuth.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. useUserStore hydrates from localStorage keys user, profile, attachments on store init (no persistence plugin). useGlobalStore holds 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; AuthLayout for auth pages. Catch-all redirects to dashboard (= pages/workouts/Feed.vue).
  • No global auth guard — unauthenticated handling is done via the 401 interceptor.

Charts & maps

  • Chart.js via vue-chartjs (+ chartjs-adapter-moment, chartjs-plugin-zoom, chartjs-chart-geo for elevation). Chart building lives in src/pages/workouts/components/*.ts.
  • Yandex Maps via vue-yandex-maps (createYmaps with a hardcoded API key in main.ts). Route polyline drawn from workout results coordinates.
  • Typing contract (since vue 3.5 strict checks): vue-yandex-maps :settings coordinates must be the LngLat = [lon, lat, alt?] tuple type — plain number[] refs fail vue-tsc. Pattern: ref<LngLat>([lon, lat]) + as LngLat casts in templates (see pages/routes/Route.vue, pages/workouts/components/WorkoutItem.vue).
  • Custom chart controllers extend chart.js controllers (LineWithLineController in LineWithLineChart.ts); the map-sync plugin is read from chart.config.plugins by id — use non-null assertions on chart.config.data! (do NOT reintroduce @ts-ignore, lint bans it).

Styling

  • Vuestic UI (config in src/services/vuestic-ui/global-config.ts, themes in themes.ts, custom icons registered in icons-config/).
  • Tailwind CSS with Vuestic CSS variables as color tokens (--va-primary etc. in tailwind.config.js). Custom font-size tokens: tag, regularSmall/Medium/Large.
  • Global SCSS in src/scss/main.scss; icon fonts in src/scss/icon-fonts/.

i18n

  • vue-i18n in composition mode (legacy: false), locale and fallback = ru. Locales auto-loaded from src/i18n/locales/*.json via import.meta.glob + @intlify/unplugin-vue-i18n/vite plugin.

Error handling

  • Central axios interceptor logs and re-throws; 401 triggers logout redirect. Components typically use .then/.catch chains (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 **/*.vue via languageOptions.parserOptions.parser (from the typescript-eslint CJS 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-comment is enforced: @ts-ignore is not allowed; use @ts-expect-error or proper typing (non-null assertions).
  • no-unused-vars honors argsIgnorePattern: "^_".
  • yarn lint runs prelint: 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 --noEmit type-check in yarn build. Dev and Docker both use yarn v1 (Docker: node:22, COPY package.json yarn.lock, yarn install --frozen-lockfile).
  • nginx.conf for static serving in the image; run.sh launcher; serve -s ./dist for CI preview.
  • ⚠️ Legacy package-lock.json is STALE and unused (Docker no longer copies it) — do not run npm install in 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 — Docker (node:22) is fine; verify local dev node before bumping.
  • yarn 1.22 has no up alias — use yarn upgrade.