profileShare

rasmusjy / splitapp-frontend-vue

Read-only snapshot

No repository description.

main default branch 96 files Expires Sep 13, 2026, 9:06 AM
main Snapshot head d4d8a12 1 commits
rasmusjy Initial commit: SplitApp Vue 3 frontend d4d8a12
README.md
View source

SplitApp — Vue 3 frontend

https://travel.rasmusj.com/

Frontend (separate client app) for the SplitApp travel planning and shared expense management system. Built with Vue 3 + TypeScript + Vite, talks to the ASP.NET Core backend over REST with JWT + refresh token authentication.

This repo satisfies the "Separate client app" requirements of the TalTech Personal Project assignment — see Assignment compliance below.

Features

  • Authentication — register, login, logout, JWT + refresh token flow with auto-refresh on 401
  • Trips — create, view, edit, delete trips with destination, dates, currency (views/trips)
  • Members & invitations — invite travel companions by token, accept/decline/revoke (views/members, views/invitations)
  • Expenses — record shared costs with four split methods (see below), select any trip member as the payer (not just the logged-in user), and see totals summed in the trip's default currency even when individual expenses were entered in other currencies (views/expenses, components/SplitMethodSelector.vue)
  • Budget categories — categorize expenses, track spent vs planned (views/budget-categories)
  • Wishlist — trip activities and destinations with group voting and completion tracking (views/wishlist)
  • Polls — group decision-making with single/multi vote and close-poll workflow (views/polls)
  • Settlements — real-time balance tracking with suggested payments preview; organizer finalizes the trip (Active → Finalizing) to lock in the settlement plan, then participants complete the mark-paid / confirm-paid workflow — trip auto-advances to Settled once every recipient has confirmed. The Mark Paid button appears only for the payer of a row and Confirm only for the payee; other viewers see an "Awaiting Confirmation" / "Pending" badge. Reopen is available to the organizer while the plan is not yet Completed (views/settlements)
  • Multi-currency support (services/CurrencyService.ts)

Expense split methods

The SplitMethodSelector component supports four validated split strategies, matching the backend's ESplitMethod enum:

Method Behavior
EqualAll Total divided equally among every trip member
EqualSubset User picks a subset of members; total divided equally among picked
ExactAmounts Per-member fixed amount input; must sum exactly to total
Percentages Per-member percentage input; must sum to exactly 100%

Each method is validated live and blocks the save button when invalid.

Tech stack

  • Vue 3.5 (Composition API) + TypeScript 6
  • Vite 8 (dev server + production build)
  • Vue Router 5 — nested routes with an auth navigation guard
  • Pinia 3 — auth store (JWT, refresh token, user name) with localStorage sync
  • Axios — shared httpClient.ts with request/response interceptors
  • Bootstrap 5 + custom coral/teal CSS variable theme
  • Vitest 4 + @vue/test-utils + MSW — unit & integration tests
  • Playwright — browser-based end-to-end tests
  • ESLint (flat config) + Oxlint + Prettier

Project structure

src/
├── components/                 # SplitMethodSelector, ToastContainer
├── composables/                # useToast — global toast system
├── directives/                 # v-animate — IntersectionObserver scroll animations
├── router/                     # vue-router config + auth guard
├── services/                   # 9 axios-based API clients, one per resource
│   ├── httpClient.ts           # shared axios instance with JWT interceptors
│   ├── AccountService.ts
│   ├── TripService.ts
│   ├── ExpenseService.ts
│   ├── BudgetCategoryService.ts
│   ├── CurrencyService.ts
│   ├── PollService.ts
│   ├── WishlistService.ts
│   ├── SettlementService.ts
│   └── InvitationService.ts
├── stores/
│   └── auth.ts                 # jwt, refreshToken, userName, isAuthenticated
├── types/                      # TypeScript contracts mirroring backend DTOs
├── utils/                      # formatCurrency, parseJwt
├── views/                      # Pages (HomeView, LoginView, RegisterView, trips/, expenses/, ...)
├── App.vue                     # Root layout (navbar + router-view + toasts)
└── main.ts
e2e/                            # Playwright tests
src/__tests__/
├── unit/                       # Vitest unit tests
├── integration/                # Vitest + MSW integration tests
└── vitest.setup.ts

Requirements

  • Node.js ^20.19.0 || >=22.12.0
  • npm
  • A running instance of the SplitApp backend (splitapp-backend-clean-onion) on the URL configured in .env

Setup

  1. Install dependencies:

    npm install
    
  2. Create a .env file at the project root:

    VITE_API_BASE_URL=http://localhost:90/api/v1/
    

    Use http://localhost:5086/api/v1/ if you run the backend locally via dotnet run instead of Docker.

  3. Start the backend (separate repo):

    cd ../splitapp-backend-clean-onion
    docker compose up -d
    
  4. Run the frontend:

    npm run dev
    

    http://localhost:5173

Scripts

npm run dev             # Vite dev server (http://localhost:5173)
npm run build           # Type-check + production build
npm run preview         # Preview the production bundle
npm run test:unit       # Vitest unit + integration tests (watch mode)
npm run test:unit -- --run   # One-off CI-style run
npm run test:e2e        # Playwright end-to-end tests (needs backend running)
npm run test:e2e:ui     # Playwright interactive UI mode
npm run lint            # Oxlint + ESLint with --fix
npm run type-check      # vue-tsc strict type check
npm run format          # Prettier

Testing

The project has three testing layers — 39 tests in 7 files, all green against the phase 3 modular monolith backend.

Layer Files Tests Backend? Runtime
Unit 4 31 No (pure functions / isolated component) ~0.4s
Integration 1 4 No (MSW mocks the network at the HTTP layer) ~0.5s
E2E 2 4 Yes (real Chromium, real backend) ~11s

Unit (npm run test:unit)

Vitest + jsdom. Each file targets one tightly-scoped piece of the codebase:

File What it covers Why it matters
formatCurrency.spec.ts sign / symbol / decimals / thousands separators every page renders amounts — a regression here visually breaks the whole app
parseJwt.spec.ts base64url decoding (+/-_), ASP.NET nameidentifier claim with sub fallback wrong user-id parsing silently mis-attributes expenses to the wrong user
auth-store.spec.ts Pinia store: localStorage hydration, isAuthenticated reactivity, logout(), watcher-based sync broken sync drifts store and localStorage apart → user sees stale identity after reload
SplitMethodSelector.spec.ts all four split methods, live validation, edit-mode existingSplits pre-load most complex business logic on the front — invalid splits would let the user save a malformed expense

Integration (npm run test:unit, in the same suite)

File What it covers
token-refresh.spec.ts the most security-sensitive glue in the app — httpClient's 401 → refresh → retry pipeline, end to end. MSW intercepts at the network layer so axios behaves exactly as in production. Three scenarios: (1) successful refresh + replay, (2) refresh fails → logout + redirect to /login, (3) no refresh token present → straight to logout.

End-to-end (npm run test:e2e)

Playwright drives a real Chromium browser. The webServer config auto-starts npm run dev for you, so you only need the backend reachable.

File What it covers
auth.spec.ts login as seed user alice@taltech.ee, logout, wrong-password stays on /login, protected routes redirect anonymous users to /login
trip-crud.spec.ts positive happy flow — login → create a uniquely-named trip → see it in the list → open Edit → rename → verify the rename persists. Delete is intentionally skipped because backend DeleteBehavior.Restrict blocks deleting a trip that has any participant (even the auto-added Organizer) — that's a backend issue, not a frontend one.

How to run locally:

E2E hits the backend via whatever URL is in .env:

# Option A — against deployed phase 3 backend (no local docker needed)
# .env already points at https://travel.rasmusj.com/api/v1/
npm run test:e2e

# Option B — against a local backend
cd ../splitapp-backend-clean-onion && docker compose up -d   # backend on :90
# Override .env locally with VITE_API_BASE_URL=http://localhost:90/api/v1/
cd ../splitapp-frontend-vue
npm run test:e2e           # headless
npm run test:e2e:ui        # interactive UI mode for debugging

Note: the trip-crud test creates a real trip in whichever backend you point at. Against prod, that's a real row in the deployed database (it does not clean up).

Docker

The project ships with a multi-stage Dockerfile (Node build → Nginx serve) and a docker-compose.yml with two profiles.

Production (Nginx)

docker compose up --build

http://localhost:91

The backend URL is injected as a build arg (VITE_API_BASE_URL) so the same image can target different environments:

# Override at build time for a real deploy
VITE_API_BASE_URL=https://api.mydomain.com/api/v1/ docker compose up --build

Nginx is configured with an SPA fallback (try_files $uri $uri/ /index.html) so Vue Router's history mode works on direct URLs.

Port allocation

Service Host port Notes
Frontend (prod, Nginx) 91 separate from backend
Backend API (Docker) 90 rasmju-csweb-phase3
Backend API (dotnet run) 5086 / 7040 http / https
PostgreSQL 5432

CI/CD

Configured via .gitlab-ci.yml:

  • deploy stage — on merges to main, runs docker compose -p rasmju-js-a7 up --build --remove-orphans --detach on a shared runner (deploys the Nginx production image)
  • Tests run locally before pushing — there is intentionally no test stage in the pipeline. Run npm run test:unit -- --run and npm run test:e2e on your machine before merging.

The frontend is hosted on a separate URL from the backend and relies on the backend's permissive CORS policy (AllowAnyOrigin in Program.cs).

Architecture notes

Authentication flow

  1. LoginViewAccountService.loginAsync → backend returns { jwt, refreshToken, firstName, lastName }
  2. Auth store saves them; watchers mirror values to localStorage
  3. Every outgoing request picks up Authorization: Bearer <jwt> from the request interceptor
  4. On 401, the response interceptor:
    • calls AccountService.refreshTokenAsync(jwt, refreshToken)
    • on success: updates the store, replays the original request transparently
    • on failure: calls logoutAsync, clears the store, navigates to /login
  5. logout() tears down store state and removes localStorage entries

Nested routing

/trips/:tripId acts as a parent route whose DetailView provides isOrganizer, currentUserId, tripStatus, and tripCurrencySymbol to child routes via Vue's provide/inject. Children use these to:

  • Hide organizer-only actions (budget category CRUD, finalize/reopen trip)
  • Hide creator-only actions (edit/delete own wishlist items and expenses); the edit/delete slot in the expense list reserves layout space even when hidden so amounts stay aligned in a single column
  • Lock expense CRUD whenever trip status is not Active (i.e. Finalizing, Settled, or Archived)
  • Show suggested payment previews while trip is Active, and the real settlement plan once trip enters Finalizing or Settled; the header badge distinguishes the two states (warning hourglass vs. lock icon)
  • Hide payment action buttons from users who cannot act on a row — Mark Paid only for the payer, Confirm only for the payee (matching the BLL guards so the UI never surfaces a click that would silently 403)

Currency conversion

Expenses can be recorded in any currency (EUR, USD, GBP, SEK, NOK). To keep conversion rules in a single place, the frontend does not implement any currency math — it relies on the backend. The ExpenseDto returned by GET /api/v1/expenses/trip/{tripId} includes an amountInTripCurrency field that the backend pre-computes (SplitApp.Modules.Expenses.Application.CurrencyConverter in the modular monolith). List totals (expenses/IndexView.vue, trip dashboard) sum this field with e.amount as a fallback. Per-expense rows still display the original amount + currency symbol so the user sees what was actually entered.

Error handling

Services return an IResultObject<T> wrapper ({ data } | { errors: string[] }) instead of throwing. Views inspect .errors and show toast messages via the useToast composable.

Assignment compliance

This repository targets the "Separate client app" portion of the TalTech Personal Project Assignment 3.

Requirement Status Where
Written in a chosen technology (react/angular/vue/blazor/…) Vue 3 + TypeScript
Uses own backend REST API VITE_API_BASE_URL=http://localhost:90/api/v1/
JWT + refresh token authentication services/httpClient.ts, services/AccountService.ts, stores/auth.ts
Login / logout LoginView.vue, RegisterView.vue, App.vue
CRUD on ≥3 entities ✅ (5) Trips, Expenses, BudgetCategories, Wishlist, Polls
CI/CD deploy — client hosted on a separate URL from backend .gitlab-ci.yml, separate Docker container on port 91
CORS handling Backend CorsAllowAll policy; frontend on separate origin
Bonus: full unit + integration + e2e test coverage Vitest + MSW + Playwright — 39 tests, all green against phase 3 backend

Seed users

The backend seeds five demo users on first startup (if DataInitialization:SeedIdentity=true). Password for all of them is Kala.12345, which is in the source on purpose: this is a demo and the data is invented.

The administrator is separate. It is seeded only when SEED_ADMIN_PASSWORD is set on the backend, and there is no default, so without that variable there is no admin account at all.

Email Role
user@taltech.ee user
alice@taltech.ee user
bob@taltech.ee user
charlie@taltech.ee user
diana@taltech.ee user

Four example trips are also seeded (Barcelona Weekend, London Business Trip, Summer Cabin Getaway, NYC Adventure), each with expenses, polls and wishlist items.

Related documentation

  • YLEVAADE.md — comprehensive system overview in Estonian, covering both frontend and backend
  • splitapp-backend-clean-onion — backend repository (ASP.NET Core 10 + PostgreSQL)

Recommended IDE

VS Code + Vue (Official) (disable Vetur).