1564 lines
55 KiB
Markdown
1564 lines
55 KiB
Markdown
|
|
# BaMoRT Frontend — Comprehensive Code Analysis
|
|||
|
|
|
|||
|
|
_Generated: 2026-03-13_
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Table of Contents
|
|||
|
|
|
|||
|
|
1. [Project Setup](#1-project-setup)
|
|||
|
|
2. [Application Entry](#2-application-entry)
|
|||
|
|
3. [Router](#3-router)
|
|||
|
|
4. [Pinia Stores](#4-pinia-stores)
|
|||
|
|
5. [Views (src/views/)](#5-views-srcviews)
|
|||
|
|
6. [Components (src/components/)](#6-components-srccomponents)
|
|||
|
|
7. [API Layer](#7-api-layer)
|
|||
|
|
8. [i18n](#8-i18n)
|
|||
|
|
9. [Styling](#9-styling)
|
|||
|
|
10. [Business Logic in Frontend](#10-business-logic-in-frontend)
|
|||
|
|
11. [User Flows](#11-user-flows)
|
|||
|
|
12. [Component Hierarchy](#12-component-hierarchy)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Project Setup
|
|||
|
|
|
|||
|
|
### `package.json`
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
name: bamort-frontend
|
|||
|
|
version: 0.2.3
|
|||
|
|
type: module
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Runtime dependencies:**
|
|||
|
|
| Package | Version | Purpose |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `vue` | ^3.5.13 | Core framework (Options API) |
|
|||
|
|
| `vue-router` | ^4.5.0 | Client-side routing |
|
|||
|
|
| `pinia` | ^2.3.0 | State management |
|
|||
|
|
| `vue-i18n` | ^11.0.0 | Internationalisation (DE + EN) |
|
|||
|
|
| `axios` | ^1.7.9 | HTTP client |
|
|||
|
|
|
|||
|
|
**Dev dependencies:**
|
|||
|
|
| Package | Version | Purpose |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `vite` | ^6.0.1 | Build tool / dev server |
|
|||
|
|
| `@vitejs/plugin-vue` | ^5.2.1 | Vite Vue 3 plugin |
|
|||
|
|
| `vite-plugin-vue-devtools` | ^7.6.5 | Browser devtools integration |
|
|||
|
|
|
|||
|
|
No testing framework, no TypeScript compiler (`.ts` files exist only as type declaration stubs).
|
|||
|
|
|
|||
|
|
### `vite.config.js`
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
export default defineConfig({
|
|||
|
|
plugins: [vue(), vueDevTools()],
|
|||
|
|
resolve: {
|
|||
|
|
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) }
|
|||
|
|
},
|
|||
|
|
server: {
|
|||
|
|
host: ['bamort.trokan.de', '192.168.0.48', 'localhost', 'terra.local'],
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Key notes:
|
|||
|
|
- `@` alias maps to `src/`
|
|||
|
|
- Dev server explicitly bound to multiple hostnames (network-accessible)
|
|||
|
|
- Minification and source maps are commented out (disabled)
|
|||
|
|
- No Vite proxy configuration — all API calls go directly to the configured `VITE_API_URL`
|
|||
|
|
|
|||
|
|
### `index.html`
|
|||
|
|
|
|||
|
|
Single-page app root. Mounts `#app` and loads `src/main.js` as an ES module. Uses `favicon.png`.
|
|||
|
|
|
|||
|
|
### `src/version.js`
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
export const VERSION = '0.2.3'
|
|||
|
|
export const GIT_COMMIT = import.meta.env.VITE_GIT_COMMIT || 'unknown'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Exposes `getVersion()` and `getGitCommit()` consumed by `LandingView` and `SystemInfoView`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. Application Entry
|
|||
|
|
|
|||
|
|
**`src/main.js`**
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
import './assets/main.css'
|
|||
|
|
import { createApp } from "vue"
|
|||
|
|
import { createPinia } from 'pinia'
|
|||
|
|
import App from "./App.vue"
|
|||
|
|
import router from "./router"
|
|||
|
|
import { i18n } from './stores/languageStore'
|
|||
|
|
import UtilsPlugin from './utils/utilsPlugin'
|
|||
|
|
|
|||
|
|
const app = createApp(App)
|
|||
|
|
app.use(createPinia())
|
|||
|
|
app.use(router)
|
|||
|
|
app.use(i18n)
|
|||
|
|
app.use(UtilsPlugin)
|
|||
|
|
app.mount("#app")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Order of registration:
|
|||
|
|
1. Pinia (state store)
|
|||
|
|
2. Vue Router
|
|||
|
|
3. vue-i18n instance (imported from `languageStore.js`, not created here)
|
|||
|
|
4. `UtilsPlugin` (global properties: date helpers + dice rollers)
|
|||
|
|
|
|||
|
|
**`src/App.vue`**
|
|||
|
|
|
|||
|
|
Root component. Shows `<Menu />` only when the user is logged-in (detected via `localStorage.getItem('token')`). The main `<router-view />` is wrapped in a `<main>` that gets the CSS class `full-width` when the user is not logged in (centering forms on the page). Listens to `storage` and custom `auth-changed` window events to reactively update `loggedIn`.
|
|||
|
|
|
|||
|
|
### `UtilsPlugin` (`src/utils/utilsPlugin.js`)
|
|||
|
|
|
|||
|
|
Installs these global properties on the Vue app instance:
|
|||
|
|
|
|||
|
|
| Property | Source | Description |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `$formatDate` | `dateUtils.js` | Format ISO date to locale string |
|
|||
|
|
| `$formatDateTime` | `dateUtils.js` | Format ISO datetime |
|
|||
|
|
| `$formatRelativeDate` | `dateUtils.js` | Relative time (e.g. "2 hours ago") |
|
|||
|
|
| `$safeValue` | `dateUtils.js` | Return value or fallback |
|
|||
|
|
| `$capitalize` | `dateUtils.js` | Capitalize string |
|
|||
|
|
| `$rollDie` | `randomUtils.js` | Roll single die (1–N) |
|
|||
|
|
| `$rollDice` | `randomUtils.js` | Roll multiple dice → array |
|
|||
|
|
| `$rollDiceWithSum` | `randomUtils.js` | Roll + return sum |
|
|||
|
|
| `$rollNotation` | `randomUtils.js` | Parse RPG notation e.g. `"3d6+2"` |
|
|||
|
|
| `$randomBetween` | `randomUtils.js` | Random int in range |
|
|||
|
|
| `$randomChoice` | `randomUtils.js` | Pick random element from array |
|
|||
|
|
| `$shuffleArray` | `randomUtils.js` | Fisher-Yates shuffle |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Router
|
|||
|
|
|
|||
|
|
**`src/router/index.js`** — `createWebHistory` (HTML5 mode, no hash).
|
|||
|
|
|
|||
|
|
### Route Table
|
|||
|
|
|
|||
|
|
| Path | Name | Component | Auth? | Admin? |
|
|||
|
|
|---|---|---|---|---|
|
|||
|
|
| `/` | `Landing` | `LandingView` | — | — |
|
|||
|
|
| `/login` | `Login` | `LoginView` | — | — |
|
|||
|
|
| `/register` | `Register` | `RegisterView` | — | — |
|
|||
|
|
| `/forgot-password` | `ForgotPassword` | `ForgotPasswordView` | — | — |
|
|||
|
|
| `/reset-password` | `ResetPassword` | `ResetPasswordView` | — | — |
|
|||
|
|
| `/dashboard` | `Dashboard` | `DashboardView` | ✓ | — |
|
|||
|
|
| `/profile` | `UserProfile` | `UserProfileView` | ✓ | — |
|
|||
|
|
| `/users` | `UserManagement` | `UserManagementView` | ✓ | ✓ |
|
|||
|
|
| `/ausruestung/:characterId` | `Ausruestung` | `AusruestungView` | ✓ | — |
|
|||
|
|
| `/maintenance` | `Maintenance` | `MaintenanceView` | ✓ | — |
|
|||
|
|
| `/upload` | `FileUpload` | `FileUploadPage` | ✓ | — |
|
|||
|
|
| `/sponsors` | `Sponsors` | `SponsorsView` | — | — |
|
|||
|
|
| `/help` | `Help` | `HelpView` | — | — |
|
|||
|
|
| `/system-info` | `SystemInfo` | `SystemInfoView` | — | — |
|
|||
|
|
| `/character/:id` | `CharacterDetails` | `CharacterDetails` | ✓ | — |
|
|||
|
|
| `/character/create/:sessionId` | `CharacterCreation` | `CharacterCreation` | ✓ | — |
|
|||
|
|
|
|||
|
|
**Static imports** (loaded immediately — needed for unauthenticated pages): `LandingView`, `LoginView`, `RegisterView`, `ForgotPasswordView`, `ResetPasswordView`.
|
|||
|
|
|
|||
|
|
**Lazy-loaded** (code-split into separate chunks on first access): all other views and both character components.
|
|||
|
|
|
|||
|
|
Route params are passed as props for `CharacterDetails` (`id`) and `CharacterCreation` (`sessionId`).
|
|||
|
|
|
|||
|
|
### Navigation Guard
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
router.beforeEach(async (to, from, next) => {
|
|||
|
|
if (to.meta.requiresAuth && !isLoggedIn()) {
|
|||
|
|
next({ name: "Login" })
|
|||
|
|
} else if (to.meta.requiresAdmin) {
|
|||
|
|
const userStore = useUserStore()
|
|||
|
|
if (!userStore.currentUser) await userStore.fetchCurrentUser()
|
|||
|
|
if (!userStore.isAdmin) next({ name: "Dashboard" })
|
|||
|
|
else next()
|
|||
|
|
} else {
|
|||
|
|
next()
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`isLoggedIn()` simply checks `localStorage.getItem("token")`. The admin check lazy-loads the user profile if it hasn't been fetched yet.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Pinia Stores
|
|||
|
|
|
|||
|
|
### `userStore` (`src/stores/userStore.js`)
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
defineStore('user', {
|
|||
|
|
state: () => ({ currentUser: null, isLoading: false }),
|
|||
|
|
getters: {
|
|||
|
|
isAuthenticated: state => !!state.currentUser,
|
|||
|
|
userRole: state => state.currentUser?.role || 'standard',
|
|||
|
|
isAdmin: state => state.currentUser?.role === 'admin',
|
|||
|
|
isMaintainer: state => state.currentUser?.role === 'maintainer'
|
|||
|
|
|| state.currentUser?.role === 'admin',
|
|||
|
|
isStandardUser: state => !!state.currentUser
|
|||
|
|
},
|
|||
|
|
actions: {
|
|||
|
|
async fetchCurrentUser() { /* GET /api/user/profile */ },
|
|||
|
|
clearUser() { this.currentUser = null }
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- Fetches profile from `GET /api/user/profile`.
|
|||
|
|
- On successful fetch, also updates the i18n locale from `profile.preferred_language` and persists it to `localStorage`.
|
|||
|
|
- `currentUser` shape: `{ id, username, display_name, email, role, preferred_language }`.
|
|||
|
|
- Used by: `Menu.vue`, `CharacterDetails.vue`, `UserManagementView.vue`, router guard.
|
|||
|
|
|
|||
|
|
### `languageStore` (`src/stores/languageStore.js`)
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
export const i18n = createI18n({
|
|||
|
|
legacy: false,
|
|||
|
|
locale: localStorage.getItem('language') || 'de',
|
|||
|
|
fallbackLocale: 'en',
|
|||
|
|
messages: { de, en }
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
defineStore('language', {
|
|||
|
|
state: () => ({ currentLanguage: localStorage.getItem('language') || 'de' }),
|
|||
|
|
actions: {
|
|||
|
|
setLanguage(lang) {
|
|||
|
|
this.currentLanguage = lang
|
|||
|
|
i18n.global.locale.value = lang
|
|||
|
|
localStorage.setItem('language', lang)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Unusually, the `i18n` instance is **created and exported from this file**, then imported in `main.js`. Default language: `de`.
|
|||
|
|
|
|||
|
|
### `characterCreationStore` (`src/stores/characterCreation.js`)
|
|||
|
|
|
|||
|
|
Manages the multi-step character wizard session state in-memory (backed by server-side session).
|
|||
|
|
|
|||
|
|
**State:**
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
{
|
|||
|
|
sessionData: {
|
|||
|
|
basic_info: null,
|
|||
|
|
attributes: null,
|
|||
|
|
derived_values: null,
|
|||
|
|
skills: [],
|
|||
|
|
skills_meta: { totalUsedPoints: 0, selectedCategory: null },
|
|||
|
|
spells: [],
|
|||
|
|
equipment: null
|
|||
|
|
},
|
|||
|
|
currentStep: 1, // 1–5
|
|||
|
|
sessionId: null,
|
|||
|
|
isLoading: false,
|
|||
|
|
error: null
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Getters:** `characterClass`, `characterStand`, `characterRace`, `hasSelectedSkills`, `totalSkillPoints`, `isValid` (checks basic_info + attributes + derived_values are all non-null).
|
|||
|
|
|
|||
|
|
**Actions:** `initializeSession(sessionId?)`, `createNewSession()`, `loadSession(sessionId)` (TODO), `saveSession()` (TODO), `updateStepData(data)`, `goToStep(n)`, `nextStep()`, `previousStep()`, `clearSession()`.
|
|||
|
|
|
|||
|
|
Note: `saveSession()` and `loadSession()` are marked TODO — the actual session persistence is handled directly by `CharacterCreation.vue` via the API, bypassing this store.
|
|||
|
|
|
|||
|
|
### `counter` (`src/stores/counter.ts`)
|
|||
|
|
|
|||
|
|
A boilerplate Pinia counter store from Vue scaffolding. Not used in the application.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Views (`src/views/`)
|
|||
|
|
|
|||
|
|
Most views are thin wrappers that delegate to a single component.
|
|||
|
|
|
|||
|
|
### `LandingView.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Public landing page. Displays app logo, description, version info, and action links.
|
|||
|
|
|
|||
|
|
**Data fetched:** `GET ${VITE_API_URL}/api/public/version` (unauthenticated, uses raw `axios` not the `API` utility). Retries every 5 s up to 24 times if the backend is unreachable.
|
|||
|
|
|
|||
|
|
**Logic:**
|
|||
|
|
- `isBackendAvailable` computed: disables the login button until the backend responds.
|
|||
|
|
- Clears retry interval on `beforeUnmount`.
|
|||
|
|
- Shows frontend version from `version.js` (`0.2.3`) and backend version from API.
|
|||
|
|
|
|||
|
|
**Components used:** None (self-contained).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `LoginView.vue`
|
|||
|
|
|
|||
|
|
Thin wrapper: renders `<LoginForm />`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `RegisterView.vue`
|
|||
|
|
|
|||
|
|
Thin wrapper: renders `<RegisterForm />`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `ForgotPasswordView.vue`
|
|||
|
|
|
|||
|
|
Thin wrapper: renders `<ForgotPasswordForm />`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `ResetPasswordView.vue`
|
|||
|
|
|
|||
|
|
Thin wrapper: renders `<ResetPasswordForm />`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `DashboardView.vue`
|
|||
|
|
|
|||
|
|
Thin wrapper: renders `<CharacterList />`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `CharacterView.vue`
|
|||
|
|
|
|||
|
|
Thin wrapper (legacy): passes `$route.params.characterId` to `<CharacterDetails />` as `:characterId`. Route `/character/:id` uses `CharacterDetails` directly via lazy import; this view appears unused by the router.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `AusruestungView.vue`
|
|||
|
|
|
|||
|
|
Passes `$route.params.characterId` to `<AusruestungList />`. Legacy/simple standalone equipment list for a character — separate from the main character detail equipment tab.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `MaintenanceView.vue`
|
|||
|
|
|
|||
|
|
Thin wrapper: renders `<Maintenance />`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `UserProfileView.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Self-service profile management for the logged-in user.
|
|||
|
|
|
|||
|
|
**API calls:**
|
|||
|
|
- `GET /api/user/profile` (on created)
|
|||
|
|
- `PUT /api/user/display-name` — change display name (max 30 chars)
|
|||
|
|
- `PUT /api/user/language` — change preferred language (updates i18n locale + localStorage)
|
|||
|
|
- `PUT /api/user/email` — change email
|
|||
|
|
- `PUT /api/user/password` — change password (requires current + new + confirm)
|
|||
|
|
|
|||
|
|
**Validation:**
|
|||
|
|
- Display name: max 30 chars
|
|||
|
|
- Password: min 6 chars, must match confirmation
|
|||
|
|
- Email: must differ from current, type="email"
|
|||
|
|
- Duplicate email check via backend error message parsing
|
|||
|
|
|
|||
|
|
**Sections rendered:** User info display, Change Display Name, Change Language, Change Email, Change Password.
|
|||
|
|
|
|||
|
|
**After language update:** directly sets `this.$i18n.locale` and `localStorage.language`. Also updates `userStore.currentUser.display_name` after display name change.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `UserManagementView.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Admin panel for managing all users.
|
|||
|
|
|
|||
|
|
**Guard:** Route has `requiresAdmin: true`.
|
|||
|
|
|
|||
|
|
**API calls:**
|
|||
|
|
- `GET /api/users` — list all users
|
|||
|
|
- `PUT /api/users/:id/role` — change role (`standard` | `maintainer` | `admin`)
|
|||
|
|
- `DELETE /api/users/:id` — delete user
|
|||
|
|
- `PUT /api/users/:id/password` — admin password reset (no current password required)
|
|||
|
|
|
|||
|
|
**Dialogs (modal overlays):**
|
|||
|
|
1. Role change dialog — select from standard/maintainer/admin
|
|||
|
|
2. Delete confirmation dialog
|
|||
|
|
3. Password change dialog (min 6 chars, must confirm)
|
|||
|
|
|
|||
|
|
**Protection:** Buttons disabled for the current logged-in user's own row (cannot self-demote or self-delete). Detected via `currentUser.id === user.id` from `userStore`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `FileUploadPage.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Import character data from VTT (Virtual Tabletop) JSON or CSV files.
|
|||
|
|
|
|||
|
|
**API call:** `POST /api/importer/upload` with `multipart/form-data`.
|
|||
|
|
|
|||
|
|
**Validation:** Checks MIME types (`application/json`, `text/csv`). VTT file is required; CSV is optional.
|
|||
|
|
|
|||
|
|
**Note:** Manually adds `Authorization` header even though the API interceptor already does this — a redundancy.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `HelpView.vue`
|
|||
|
|
|
|||
|
|
Static content page. Dynamically loads FAQ items from i18n keys `help.faq1.question`/`.answer`, `help.faq2...`, etc., iterating until a key is not found using `$te()`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `SponsorsView.vue`
|
|||
|
|
|
|||
|
|
Static content page listing contributors and special thanks. Links to GitHub and Ko-fi.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `SystemInfoView.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Displays system/technology information and live backend status.
|
|||
|
|
|
|||
|
|
**API call:** `GET ${VITE_API_URL}/api/public/systeminfo` (uses raw `axios`). Returns `{ version, gitCommit, userCount, charCount, dbVersion }`.
|
|||
|
|
|
|||
|
|
**Technology stack display:** Hardcoded lists of Vue 3, Vite, Go 1.25, Gin, GORM, MariaDB, JWT, chromedp, Docker.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. Components (`src/components/`)
|
|||
|
|
|
|||
|
|
### `Menu.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Fixed top navigation bar (60px high, z-index 1000).
|
|||
|
|
|
|||
|
|
**Authentication-dependent rendering:**
|
|||
|
|
- Always visible: Home, Info dropdown (Help, Sponsors, System Info)
|
|||
|
|
- Logged-in only: Characters dropdown (Dashboard, Import Data)
|
|||
|
|
- Maintainer/Admin only: Admin dropdown (Maintenance, User Management)
|
|||
|
|
- Not logged-in only: Register link
|
|||
|
|
- Right side (logged-in): User icon dropdown (Profile, Logout)
|
|||
|
|
|
|||
|
|
**Hover dropdowns:** Implemented with `@mouseenter`/`@mouseleave` + 200 ms `setTimeout` delay to prevent premature closing.
|
|||
|
|
|
|||
|
|
**Logout flow:** calls `logout()` from `auth.js` (removes token), clears `userStore`, dispatches `auth-changed` window event, navigates to `/`.
|
|||
|
|
|
|||
|
|
**Auth change listener:** `window.addEventListener('auth-changed', ...)` — re-fetches user profile on login or clears on logout.
|
|||
|
|
|
|||
|
|
**Props/emits:** None.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `LoginForm.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Login form.
|
|||
|
|
|
|||
|
|
**API call:** `POST /login` with `{ username, password }`. On success: stores JWT in `localStorage`, calls `userStore.fetchCurrentUser()`, dispatches `auth-changed`, navigates to `/dashboard`.
|
|||
|
|
|
|||
|
|
**Error handling:** Displays "Invalid credentials" on any error.
|
|||
|
|
|
|||
|
|
**Note:** Hardcoded English labels — does not use `$t()` for the form labels (unlike most other components).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `RegisterForm.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Registration form.
|
|||
|
|
|
|||
|
|
**Validation:** Client-side password confirmation match check before API call.
|
|||
|
|
|
|||
|
|
**API call:** `POST /register` with `{ username, password, email }`.
|
|||
|
|
|
|||
|
|
**i18n:** Fully translated via `auth.*` keys.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `ForgotPasswordForm.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Request a password reset email.
|
|||
|
|
|
|||
|
|
**API call:** `POST /password-reset/request` with `{ email, redirect_url: window.location.origin }`.
|
|||
|
|
|
|||
|
|
**UX:** Shows success state (hides form, shows info text) on success without redirecting. Shows error message on failure.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `ResetPasswordForm.vue`
|
|||
|
|
|
|||
|
|
Handles the password-reset token link (not examined in detail — delegates to `<ResetPasswordForm />`).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `CharacterList.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Dashboard character listing with creation session management.
|
|||
|
|
|
|||
|
|
**API calls:**
|
|||
|
|
- `GET /api/characters` → `{ self_owned: [...], others: [...] }`
|
|||
|
|
- `GET /api/characters/create-sessions` → `{ sessions: [...] }`
|
|||
|
|
- `POST /api/characters/create-session` → `{ session_id }` — starts new character wizard
|
|||
|
|
- `DELETE /api/characters/create-session/:sessionId`
|
|||
|
|
|
|||
|
|
**Layout:**
|
|||
|
|
- "Create New Character" button (green dashed border)
|
|||
|
|
- `<CharacterCreationSessions>` sub-component (shows in-progress wizards)
|
|||
|
|
- Two column layout: owned characters | shared characters
|
|||
|
|
- Each character shows: name, race, class, grade, owner, public/private badge
|
|||
|
|
- Character names are `<router-link>` to `/character/:id`
|
|||
|
|
|
|||
|
|
**Props/emits:** None (standalone).
|
|||
|
|
|
|||
|
|
**Sub-components:** `CharacterCreationSessions`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `CharacterCreationSessions.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Shows active character creation sessions (drafts) on the dashboard.
|
|||
|
|
|
|||
|
|
**Props:** `sessions: Array`
|
|||
|
|
|
|||
|
|
**Emits:** `continue-session(sessionId)`, `delete-session(sessionId)`
|
|||
|
|
|
|||
|
|
**Renders:** Grid of cards, one per session. Shows name, step progress (e.g. "Step 2/5"), race/class, last updated, expires. Clicking a card card navigates via `continue-session` emit.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `CharacterDetails.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Main character management hub. Fetches character data and dynamically renders sub-views through a tab-like submenu.
|
|||
|
|
|
|||
|
|
**Props:** `id` (string, from route)
|
|||
|
|
|
|||
|
|
**API calls:**
|
|||
|
|
- `GET /api/characters/:id` (on created + after any edit via `refreshCharacter`)
|
|||
|
|
|
|||
|
|
**Computed:** `isOwner` — compares `character.user_id` with `userStore.currentUser.id`.
|
|||
|
|
|
|||
|
|
**Sub-views rendered via `<component :is="currentView">` with `character` and `isOwner` props:**
|
|||
|
|
1. `DatasheetView` (default)
|
|||
|
|
2. `SkillView`
|
|||
|
|
3. `WeaponView`
|
|||
|
|
4. `SpellView`
|
|||
|
|
5. `EquipmentView`
|
|||
|
|
6. `ExperianceView`
|
|||
|
|
7. `DeleteCharView`
|
|||
|
|
|
|||
|
|
**Additional dialogs:**
|
|||
|
|
- 📄 Export button → `ExportDialog`
|
|||
|
|
- 🌐/🔒 Visibility button (owner only) → `VisibilityDialog`
|
|||
|
|
|
|||
|
|
**Event handling:** `@character-updated` from sub-views triggers `refreshCharacter()` which reloads from API.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `DatasheetView.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Character stats and biographical information display with inline editing.
|
|||
|
|
|
|||
|
|
**Props:** `character: Object` (required)
|
|||
|
|
|
|||
|
|
**Emits:** `character-updated`
|
|||
|
|
|
|||
|
|
**Sub-components:** `ImageUploadCropper`
|
|||
|
|
|
|||
|
|
**Inline editing mechanism:** Double-click (`@dblclick`) on any displayed value activates an input/select in its place. On blur or Enter, `saveEdit(path)` / `saveProp(prop)` PATCHes the character via `PUT /api/characters/:id`. On Escape, `cancelEdit()` restores display.
|
|||
|
|
|
|||
|
|
**Stat fields (left column stat box):**
|
|||
|
|
|
|||
|
|
| Label key | Character path |
|
|||
|
|
|---|---|
|
|||
|
|
| `stats.beauty` | `eigenschaften.0.value` |
|
|||
|
|
| `stats.dexterity` | `eigenschaften.1.value` |
|
|||
|
|
| `stats.agility` | `eigenschaften.2.value` |
|
|||
|
|
| `stats.intelligence` | `eigenschaften.3.value` |
|
|||
|
|
| `stats.constitution` | `eigenschaften.4.value` |
|
|||
|
|
| `stats.charisma` | `eigenschaften.5.value` |
|
|||
|
|
| `stats.strength` | `eigenschaften.6.value` |
|
|||
|
|
| `stats.willpower` | `eigenschaften.7.value` |
|
|||
|
|
| `stats.spelltalent` | `eigenschaften.8.value` |
|
|||
|
|
| `stats.poisontolerance` | `git` |
|
|||
|
|
| `stats.movement` | `b.max` |
|
|||
|
|
| `stats.lifepoints` | `lp.max` |
|
|||
|
|
| `stats.staminapoints` | `ap.max` |
|
|||
|
|
| `stats.divinegrace` | `bennies.gg` |
|
|||
|
|
| `stats.fatesfavor` | `bennies.sg` |
|
|||
|
|
|
|||
|
|
**Character info fields (editable):** name, typ (class), gender (select), grad, rasse (select), origin (select), social_class (select), spezialisierung (select), alter, hand (select: rechts/links/beidhändig), groesse, gewicht, merkmale.groesse, merkmale.breite, merkmale.augenfarbe, merkmale.haarfarbe, glaube (multi-select), merkmale.sonstige.
|
|||
|
|
|
|||
|
|
**Select options:** loaded lazily via `GET /api/characters/:id/datasheet-options` (only when a select-type field is first double-clicked).
|
|||
|
|
|
|||
|
|
**`glaube` (beliefs) field:** Multi-select stored as comma-separated string; displayed and edited as array.
|
|||
|
|
|
|||
|
|
**Path traversal for saving:** `path.split('.')` reduces into nested character object, then `PUT /api/characters/:id` with full character payload.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `SkillView.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Display and manage character skills (Fertigkeiten + Waffenfertigkeiten) and innate skills.
|
|||
|
|
|
|||
|
|
**Props:** `character: Object` (required), `isOwner: Boolean`
|
|||
|
|
|
|||
|
|
**Emits:** `character-updated`
|
|||
|
|
|
|||
|
|
**Sub-components:** `SkillImproveDialog`, `SkillLearnDialog`
|
|||
|
|
|
|||
|
|
**Learning mode toggle:** 🎓 button visible to owner. When active:
|
|||
|
|
- Resources bar (EP + Gold) shown
|
|||
|
|
- Additional action buttons: 📚 Learn New, ➕ Add Skill
|
|||
|
|
- Each skill row shows ⬆️ Improve button
|
|||
|
|
|
|||
|
|
**Table structure:**
|
|||
|
|
- Regular skills: grouped by category (`character.categorizedskills`) — name, EW (Fertigkeitswert), bonus, PP (Praxispunkte), note
|
|||
|
|
- Weapon skills: from `character.waffenfertigkeiten`
|
|||
|
|
- Innate skills: from `character.InnateSkills` (right column, name + value only)
|
|||
|
|
|
|||
|
|
**PP (Praxispunkte) adjustment:** +/− buttons directly call `POST /api/characters/practice-points`.
|
|||
|
|
|
|||
|
|
**Inline skill editing:** Double-click on name, fertigkeitswert, or bemerkung → inline input; saved via `PUT /api/characters/:skill_id/skill`.
|
|||
|
|
|
|||
|
|
**Dialogs:**
|
|||
|
|
1. `SkillLearnDialog` — learn new skill (complex, see below)
|
|||
|
|
2. Improve Selection Dialog — simple modal; selects skill + PP count → `POST /api/characters/improve-skill`
|
|||
|
|
3. Add Skill Dialog — manually add skill by name/value → `POST /api/characters/:id/add-skill`
|
|||
|
|
4. `SkillImproveDialog` — detailed multi-level improvement dialog
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `SkillImproveDialog.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Detailed skill improvement dialog supporting multiple reward types and level sequences.
|
|||
|
|
|
|||
|
|
**Props:** `character: Object`, `skill: Object`, `isVisible: Boolean`, `learningType: 'improve'|'learn'|'spell'`
|
|||
|
|
|
|||
|
|
**Emits:** `close`, `skill-updated`
|
|||
|
|
|
|||
|
|
**Features:**
|
|||
|
|
- Loads available improvement levels + costs from `GET /api/characters/lerncost-newsystem` or similar
|
|||
|
|
- Loads reward types from API
|
|||
|
|
- Shows live resource tracking (EP remaining, Gold remaining, PP remaining)
|
|||
|
|
- Multi-level selection (click/deselect in sequence — must be consecutive)
|
|||
|
|
- Reward types: `default` (EP + Gold), `noGold` (EP only), `halveepnoGold` (half EP), `pp` (PP only), `mixed` (EP + PP)
|
|||
|
|
- Submits via `POST /api/characters/improve-skill-newsystem` or similar
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `SkillLearnDialog.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Learn a brand-new skill (not yet on the character).
|
|||
|
|
|
|||
|
|
**Features:**
|
|||
|
|
- Drag & drop (HTML5 native) or click-to-select skills from available list to "to learn" list
|
|||
|
|
- Category filter buttons (all categories shown as chips)
|
|||
|
|
- Search filter input
|
|||
|
|
- Sort by name or EP cost (ascending/descending toggle)
|
|||
|
|
- Reward type selector: Standard (EP + Gold) or Nur EP (no gold)
|
|||
|
|
- Live EP/Gold remaining display
|
|||
|
|
- Multi-skill batch learning (multiple skills can be queued)
|
|||
|
|
- Submits batch via `POST /api/characters/learn-skills` (or similar)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `SpellView.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Display character spells.
|
|||
|
|
|
|||
|
|
**Props:** `character: Object`, `isOwner: Boolean`
|
|||
|
|
|
|||
|
|
**Emits:** `character-updated`
|
|||
|
|
|
|||
|
|
**Sub-components:** `SpellLearnDialog`
|
|||
|
|
|
|||
|
|
**Table:** name, description, bonus, source.
|
|||
|
|
|
|||
|
|
**Learning:** Owner sees 🎓 button → opens `SpellLearnDialog`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `SpellLearnDialog.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Learn new spells with filtering, sorting, and multi-spell queuing.
|
|||
|
|
|
|||
|
|
**Features:**
|
|||
|
|
- School (Schule) filter buttons
|
|||
|
|
- Search term filter
|
|||
|
|
- Sort by name, EP cost, Gold cost
|
|||
|
|
- Reward type selector (loads from API)
|
|||
|
|
- Available/selected two-column layout
|
|||
|
|
- Per-spell affordability check
|
|||
|
|
- Batch spell learning queue with total cost summary
|
|||
|
|
- Submits via `POST /api/characters/learn-spells` (or similar)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `WeaponView.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Display and manage character weapons.
|
|||
|
|
|
|||
|
|
**Props:** `character: Object`, `isOwner: Boolean`
|
|||
|
|
|
|||
|
|
**Emits:** `character-updated`
|
|||
|
|
|
|||
|
|
**Table:** name (`*` suffix if magical), description, weight, value, amount, contained_in, bonus (anb/abwb).
|
|||
|
|
|
|||
|
|
**Add weapon dialog:**
|
|||
|
|
- Loads master weapon list from `GET /api/maintenance/weapons` (or similar)
|
|||
|
|
- Text search filter
|
|||
|
|
- Select + set amount
|
|||
|
|
- Submits via `POST /api/equipment` (or similar weapon endpoint)
|
|||
|
|
|
|||
|
|
**Edit weapon dialog:**
|
|||
|
|
- Inline form for: description, is_magical checkbox, amount, value, attack bonus (anb), defense bonus (abwb), damage bonus (schb)
|
|||
|
|
- Saves via `PUT /api/equipment/:id` (or similar)
|
|||
|
|
|
|||
|
|
**Delete weapon:** `DELETE /api/equipment/:id` with confirm dialog using `$t('weapon.confirmDelete')` with `{name}` interpolation.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `EquipmentView.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Display and manage character non-weapon equipment.
|
|||
|
|
|
|||
|
|
**Props:** `character: Object`, `isOwner: Boolean`
|
|||
|
|
|
|||
|
|
**Emits:** `character-updated`
|
|||
|
|
|
|||
|
|
**Table:** name, description, weight, value, amount, contained_in, bonus.
|
|||
|
|
|
|||
|
|
**Add equipment dialog:**
|
|||
|
|
- Loads master equipment from `GET /api/maintenance/equipment`
|
|||
|
|
- Text search filter
|
|||
|
|
- Select + set amount
|
|||
|
|
- Submits:`POST /api/equipment` with `{ character_id, name, beschreibung, gewicht, wert, anzahl, bonus, beinhaltet_in, contained_in, container_type }`
|
|||
|
|
|
|||
|
|
**Delete:** `DELETE /api/equipment/:id` with i18n confirm using `{name}` interpolation.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `ExperianceView.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Manage EP (experience points) and gold/wealth, with audit log.
|
|||
|
|
|
|||
|
|
**Props:** `character: Object`, `isOwner: Boolean`
|
|||
|
|
|
|||
|
|
**Sub-components:** `AuditLogView`
|
|||
|
|
|
|||
|
|
**Computed:** `totalWealthInGS` — currency conversion:
|
|||
|
|
```js
|
|||
|
|
goldstücke + Math.floor(silberstücke / 10) + Math.floor(kupferstücke / 10)
|
|||
|
|
```
|
|||
|
|
(1 GS = 10 SS = 10 KS — Midgard RPG currency system)
|
|||
|
|
|
|||
|
|
**EP management:** Add/remove EP inputs → `PUT /api/characters/:id/experience` or similar.
|
|||
|
|
|
|||
|
|
**Gold management:** Add/remove gold inputs → `PUT /api/characters/:id/wealth` or similar.
|
|||
|
|
|
|||
|
|
After both operations: calls `this.$refs.auditLog.loadAuditLog()` to refresh the audit log.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `AuditLogView.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Display change history (EP/gold transactions) for a character.
|
|||
|
|
|
|||
|
|
**Props:** `character: Object`
|
|||
|
|
|
|||
|
|
**API:** `GET /api/characters/:id/audit-log` with query params for field filter and date range.
|
|||
|
|
|
|||
|
|
**Filters:**
|
|||
|
|
- Field: all / experience_points / gold / silver / copper
|
|||
|
|
- Date range: all time / today / this week / this month / custom (from–to date inputs)
|
|||
|
|
- Group by date toggle
|
|||
|
|
|
|||
|
|
**Statistics section:** total_changes, ep_spent, ep_gained, gold_spent, gold_gained.
|
|||
|
|
|
|||
|
|
**Entry display:** Shows old → new value with difference (+/−), reason, optional notes. Color-coded: green for positive, red for negative changes.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `ExportDialog.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Multi-format character export modal.
|
|||
|
|
|
|||
|
|
**Props:** `characterId`, `showDialog: Boolean`
|
|||
|
|
|
|||
|
|
**Emits:** `update:showDialog`, `export-success`
|
|||
|
|
|
|||
|
|
**Computed:** `canExport` — false if no format selected, or PDF format but no template selected.
|
|||
|
|
|
|||
|
|
**On created:** loads templates from `GET /api/pdf/templates`.
|
|||
|
|
|
|||
|
|
**Export formats:**
|
|||
|
|
|
|||
|
|
| Format | API Call | Delivery |
|
|||
|
|
|---|---|---|
|
|||
|
|
| PDF | `GET /api/pdf/export/:id?template=X&showUserName=true` → returns `{ filename }`, then opens `GET /api/pdf/file/:filename` in new tab | Browser window popup |
|
|||
|
|
| VTT | `GET /api/importer/export/vtt/:id/file` (blob) | Programmatic download link |
|
|||
|
|
| BaMoRT (JSON) | `GET /api/transfer/download/:id` (blob) | Programmatic download link |
|
|||
|
|
|
|||
|
|
**PDF export note:** Gets filename from API first, then constructs full URL from `API.defaults.baseURL + /api/pdf/file/:filename`. Uses `window.open(url, '_blank')` synchronously (not before an async call) — avoids popup blockers per project convention.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `VisibilityDialog.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Change character visibility (private/public) and manage per-user sharing.
|
|||
|
|
|
|||
|
|
**Props:** `characterId`, `currentVisibility: Boolean`, `showDialog: Boolean`
|
|||
|
|
|
|||
|
|
**Emits:** `update:showDialog`, `visibility-updated(isPublic)`
|
|||
|
|
|
|||
|
|
**Watches:** `showDialog` — on open, loads available users and current shares.
|
|||
|
|
|
|||
|
|
**API calls:**
|
|||
|
|
- `GET /api/characters/:id/available-users` — users eligible to share with
|
|||
|
|
- `GET /api/characters/:id/shares` (implied) — currently shared users
|
|||
|
|
- `PUT /api/characters/:id/visibility` + share update
|
|||
|
|
|
|||
|
|
**Features:** Radio buttons for private/public. Searchable user list (filter by display name, username, email). Two-panel: "Add Users" | "Currently Shared With" with remove buttons.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `DeleteCharView.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Delete character confirmation panel (inside character detail submenu).
|
|||
|
|
|
|||
|
|
**Props:** `character: Object`, `isOwner: Boolean`
|
|||
|
|
|
|||
|
|
**Emits:** `deleted`, `cancel`
|
|||
|
|
|
|||
|
|
**Behavior:** Shows "not authorized" message for non-owners. For owners: confirmation text + "Yes, Delete" button → `DELETE /api/characters/:id` → navigates to `/dashboard`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `ImageUploadCropper.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Upload and crop a character portrait image.
|
|||
|
|
|
|||
|
|
**Props:** `characterId`
|
|||
|
|
|
|||
|
|
**Emits:** `image-updated`
|
|||
|
|
|
|||
|
|
**Features:**
|
|||
|
|
- File picker restricted to `image/*`
|
|||
|
|
- HTML5 Canvas cropping with mouse drag selection
|
|||
|
|
- Two crop shapes: rectangular or circular
|
|||
|
|
- Live preview canvas (400×400)
|
|||
|
|
- `POST /api/characters/:id/image` with FormData containing crop coordinates
|
|||
|
|
|
|||
|
|
**Implementation:** Uses `FileReader` → `Image` → two `<canvas>` elements (source + preview). Mouse events track drag selection rectangle. On save, crops the selection to a new canvas and uploads as FormData.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `Maintenance.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Master data management hub for game system admins/maintainers.
|
|||
|
|
|
|||
|
|
**API call:** `GET /api/maintenance` — loads all master data at once into `mdata` object.
|
|||
|
|
|
|||
|
|
**`mdata` shape:** `{ skills, skillcategories, weaponskills, spells, spellcategories, equipment, weapons }`
|
|||
|
|
|
|||
|
|
**Sub-views** (selected via bottom submenu):
|
|||
|
|
1. `maintenance/SkillView` (default)
|
|||
|
|
2. `maintenance/SpellView`
|
|||
|
|
3. `maintenance/EquipmentView`
|
|||
|
|
4. `maintenance/WeaponView`
|
|||
|
|
5. `maintenance/WeaponSkillView`
|
|||
|
|
6. `maintenance/BelieveView`
|
|||
|
|
7. `maintenance/GameSystemView`
|
|||
|
|
8. `maintenance/LitSourceView`
|
|||
|
|
9. `maintenance/MiscLookupView`
|
|||
|
|
10. `maintenance/SkillImprovementCostView`
|
|||
|
|
|
|||
|
|
All sub-views receive `mdata` as prop.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `maintenance/SkillView.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** CRUD for skill master data.
|
|||
|
|
|
|||
|
|
**Features:**
|
|||
|
|
- Full text search + multi-field filters (category, difficulty, improvable, innate, bonus property)
|
|||
|
|
- Sortable table (name, category columns)
|
|||
|
|
- Inline row editing (click row → edit form appears in table)
|
|||
|
|
- Create new skill inline
|
|||
|
|
- API: `POST /api/maintenance/skills`, `PUT /api/maintenance/skills/:id`, `DELETE /api/maintenance/skills/:id`
|
|||
|
|
|
|||
|
|
(Other maintenance views follow the same pattern for their respective domains.)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `CharacterCreation.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Multi-step character creation wizard.
|
|||
|
|
|
|||
|
|
**Props:** `sessionId: String` (required, from route)
|
|||
|
|
|
|||
|
|
**5-step wizard:**
|
|||
|
|
1. `CharacterBasicInfo` — name, origin, belief, gender, race, class, social class
|
|||
|
|
2. `CharacterAttributes` — 7 core attributes (1–100 range, roll support)
|
|||
|
|
3. `CharacterDerivedValues` — calculated + adjustable secondary stats
|
|||
|
|
4. `CharacterSkills` — point-buy skill selection with category budgets
|
|||
|
|
5. `CharacterSpells` — spell selection → finalize character
|
|||
|
|
|
|||
|
|
**API calls:**
|
|||
|
|
- `GET /api/characters/create-session/:sessionId` — load session state
|
|||
|
|
- `GET /api/characters/skill-categories` — available skill categories with point budgets
|
|||
|
|
- Step-specific save: `PUT /api/characters/create-session/:id/basic-info` etc.
|
|||
|
|
- `POST /api/characters/create` — finalize (step 5)
|
|||
|
|
- `DELETE /api/characters/create-session/:id` — delete draft
|
|||
|
|
|
|||
|
|
**Session persistence:** Each `handleNext(data)` saves the updated step data to the backend before incrementing `currentStep`. Session reloaded after each save to ensure consistent state.
|
|||
|
|
|
|||
|
|
**Expiry info:** Session expiry date shown at bottom. "Delete Draft" button available throughout.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `CharacterCreation/CharacterBasicInfo.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Step 1 of the wizard. Collects fundamental character identity.
|
|||
|
|
|
|||
|
|
**Fields:** Name (2–50 chars, required), Origin (select from API list), Belief/Glaube (typeahead search from API, minimum 2 chars to activate), Gender (male/female), Race (select), Class/Typ (select), Social Class (select or dice roll).
|
|||
|
|
|
|||
|
|
**Dice roll for social class:** 🎲 button (requires class to be selected first). Rolls `1d100` with class-specific modifier and maps to social class tier. Shows result in overlay that dismisses on click.
|
|||
|
|
|
|||
|
|
**Races:** loaded from backend (API call for character creation options).
|
|||
|
|
|
|||
|
|
**Emits:** `next(formData)`, `save(formData)`
|
|||
|
|
|
|||
|
|
**Validation:** `isValid` computed checks all required fields are non-empty.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `CharacterCreation/CharacterAttributes.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Step 2 — set the 7 base attributes (St, Gs, Gw, Ko, In, Zt, Au) each 1–100.
|
|||
|
|
|
|||
|
|
**Features:**
|
|||
|
|
- Individual roll buttons using `max(2d100)` formula per attribute (except Au)
|
|||
|
|
- 🎲 "Roll All" button
|
|||
|
|
- Race-specific Beauty (Au) restrictions: Elves ≥ 81, Gnomes/Dwarves ≤ 80
|
|||
|
|
- Shows total points sum and average
|
|||
|
|
- Disabled state display (shows "ENABLED"/"DISABLED" status indicator)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `CharacterCreation/CharacterDerivedValues.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Step 3 — calculate and optionally override secondary stats.
|
|||
|
|
|
|||
|
|
**Fields:** PA (personal charisma), WK (willpower), LP max (life points), AP max (adventure points), B max (movement), resistances, defense, bonuses, SG (fate's favor), GG (divine grace), GP (luck points).
|
|||
|
|
|
|||
|
|
**Dice formulas displayed in i18n keys:**
|
|||
|
|
- PA = 1d100 + 4×(In/10) − 20
|
|||
|
|
- WK = 1d100 + 2×(Ko/10 + In/10) − 20
|
|||
|
|
- LP = 1d3 + 7 + (Ko/10)
|
|||
|
|
- AP = 3d6 + class modifier
|
|||
|
|
- B = 1d6 + race modifier
|
|||
|
|
|
|||
|
|
**Recalculate button:** Calls backend to recalculate from attributes.
|
|||
|
|
|
|||
|
|
**Individual roll buttons** per derived value with tooltip showing formula.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `CharacterCreation/CharacterSkills.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Step 4 — point-buy skill selection with category budgets.
|
|||
|
|
|
|||
|
|
**Props:** `sessionData`, `skillCategories`
|
|||
|
|
|
|||
|
|
Each category has a point budget (`max_points`). Skills have costs. Emits `next`, `previous`, `save`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `CharacterCreation/CharacterSpells.vue`
|
|||
|
|
|
|||
|
|
**Purpose:** Step 5 — optional spell selection before finalization.
|
|||
|
|
|
|||
|
|
**Props:** `sessionData`, `skillCategories`
|
|||
|
|
|
|||
|
|
**Emits:** `previous`, `finalize(finalData)`, `save`
|
|||
|
|
|
|||
|
|
Finalize calls `POST /api/characters/create` with complete session data. On success navigates to the new character's detail view.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `AusruestungList.vue` (legacy)
|
|||
|
|
|
|||
|
|
**Purpose:** Simple standalone equipment list (old design). Loaded via `/ausruestung/:characterId` route.
|
|||
|
|
|
|||
|
|
**API:** `GET /ausruestung/:characterId` (note: no `/api` prefix — likely a legacy endpoint).
|
|||
|
|
|
|||
|
|
**Sub-component:** `AusruestungForm`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `AusruestungForm.vue` (legacy)
|
|||
|
|
|
|||
|
|
Simple form to add equipment name/quantity/weight. Posts to `/ausruestung` (no `/api` prefix).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `LanguageSwitcher.vue`
|
|||
|
|
|
|||
|
|
Marked as **"not used anymore"** in a comment in the template. A simple `<select>` that calls `languageStore.setLanguage()`. Language switching is now done via `UserProfileView`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### `HelloWorld.vue`, `TheWelcome.vue`, `WelcomeItem.vue`
|
|||
|
|
|
|||
|
|
**Vite/Vue scaffolding boilerplate** — not used in the application.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. API Layer
|
|||
|
|
|
|||
|
|
### `src/utils/api.js`
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
import axios from 'axios'
|
|||
|
|
|
|||
|
|
const API = axios.create({
|
|||
|
|
baseURL: import.meta.env.VITE_API_URL || 'https://bamort-api.trokan.de'
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// Request interceptor
|
|||
|
|
API.interceptors.request.use(config => {
|
|||
|
|
const token = localStorage.getItem('token')
|
|||
|
|
if (token) config.headers.Authorization = `Bearer ${token}`
|
|||
|
|
return config
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// Response interceptor
|
|||
|
|
API.interceptors.response.use(
|
|||
|
|
response => response,
|
|||
|
|
error => {
|
|||
|
|
if (error.response?.status === 401) {
|
|||
|
|
localStorage.removeItem('token')
|
|||
|
|
// redirect to /login is commented out
|
|||
|
|
}
|
|||
|
|
return Promise.reject(error)
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
export default API
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Key facts:**
|
|||
|
|
- Default base URL: `https://bamort-api.trokan.de` (production endpoint). In dev containers the env var overrides this to `http://localhost:8180`.
|
|||
|
|
- JWT is stored in and retrieved from `localStorage.getItem('token')`.
|
|||
|
|
- 401 handler removes the token but does **not** automatically redirect (commented out). Components handle 401 errors individually.
|
|||
|
|
- `API.defaults.baseURL` is used in `ExportDialog` to construct the PDF file download URL.
|
|||
|
|
|
|||
|
|
### `src/utils/auth.js`
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
export function isLoggedIn() { return !!localStorage.getItem("token") }
|
|||
|
|
export function logout() { localStorage.removeItem("token") }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Simple token-presence check. No JWT expiry validation, no PKCE, no refresh token mechanism.
|
|||
|
|
|
|||
|
|
### All API endpoints used by the frontend
|
|||
|
|
|
|||
|
|
| Method | Endpoint | Used by |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `POST` | `/login` | `LoginForm` |
|
|||
|
|
| `POST` | `/register` | `RegisterForm` |
|
|||
|
|
| `POST` | `/password-reset/request` | `ForgotPasswordForm` |
|
|||
|
|
| `GET` | `/api/public/version` | `LandingView` |
|
|||
|
|
| `GET` | `/api/public/systeminfo` | `SystemInfoView` |
|
|||
|
|
| `GET` | `/api/user/profile` | `userStore`, `UserProfileView` |
|
|||
|
|
| `PUT` | `/api/user/display-name` | `UserProfileView` |
|
|||
|
|
| `PUT` | `/api/user/language` | `UserProfileView` |
|
|||
|
|
| `PUT` | `/api/user/email` | `UserProfileView` |
|
|||
|
|
| `PUT` | `/api/user/password` | `UserProfileView` |
|
|||
|
|
| `GET` | `/api/users` | `UserManagementView` |
|
|||
|
|
| `PUT` | `/api/users/:id/role` | `UserManagementView` |
|
|||
|
|
| `DELETE` | `/api/users/:id` | `UserManagementView` |
|
|||
|
|
| `PUT` | `/api/users/:id/password` | `UserManagementView` |
|
|||
|
|
| `GET` | `/api/characters` | `CharacterList` |
|
|||
|
|
| `GET` | `/api/characters/:id` | `CharacterDetails` |
|
|||
|
|
| `PUT` | `/api/characters/:id` | `DatasheetView` |
|
|||
|
|
| `DELETE` | `/api/characters/:id` | `DeleteCharView` |
|
|||
|
|
| `GET` | `/api/characters/:id/datasheet-options` | `DatasheetView` |
|
|||
|
|
| `GET` | `/api/characters/:id/image` | `DatasheetView` (imageSrc) |
|
|||
|
|
| `POST` | `/api/characters/:id/image` | `ImageUploadCropper` |
|
|||
|
|
| `GET` | `/api/characters/:id/audit-log` | `AuditLogView` |
|
|||
|
|
| `GET` | `/api/characters/:id/available-users` | `VisibilityDialog` |
|
|||
|
|
| `POST` | `/api/characters/create-session` | `CharacterList` |
|
|||
|
|
| `GET` | `/api/characters/create-sessions` | `CharacterList` |
|
|||
|
|
| `GET` | `/api/characters/create-session/:id` | `CharacterCreation` |
|
|||
|
|
| `DELETE` | `/api/characters/create-session/:id` | `CharacterList`, `CharacterCreation` |
|
|||
|
|
| `GET` | `/api/characters/skill-categories` | `CharacterCreation` |
|
|||
|
|
| `POST` | `/api/characters/improve-skill` | `SkillView` |
|
|||
|
|
| `POST` | `/api/characters/practice-points` | `SkillView` |
|
|||
|
|
| `GET` | `/api/pdf/templates` | `ExportDialog` |
|
|||
|
|
| `GET` | `/api/pdf/export/:id` | `ExportDialog` |
|
|||
|
|
| `GET` | `/api/pdf/file/:filename` | `ExportDialog` |
|
|||
|
|
| `GET` | `/api/importer/export/vtt/:id/file` | `ExportDialog` |
|
|||
|
|
| `POST` | `/api/importer/upload` | `FileUploadPage` |
|
|||
|
|
| `GET` | `/api/transfer/download/:id` | `ExportDialog` |
|
|||
|
|
| `GET` | `/api/equipment` (maintenance) | `EquipmentView` |
|
|||
|
|
| `POST` | `/api/equipment` | `EquipmentView`, `WeaponView` |
|
|||
|
|
| `DELETE` | `/api/equipment/:id` | `EquipmentView`, `WeaponView` |
|
|||
|
|
| `PUT` | `/api/equipment/:id` | `WeaponView` |
|
|||
|
|
| `GET` | `/api/maintenance` | `Maintenance` |
|
|||
|
|
| `POST/PUT/DELETE` | `/api/maintenance/skills` | `maintenance/SkillView` |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. i18n
|
|||
|
|
|
|||
|
|
### Configuration
|
|||
|
|
|
|||
|
|
- Library: `vue-i18n` v11 (Composition API mode: `legacy: false`)
|
|||
|
|
- Default locale: `de` (German), persisted in `localStorage.language`
|
|||
|
|
- Fallback locale: `en`
|
|||
|
|
- Instance created in `languageStore.js` and exported as `i18n`
|
|||
|
|
|
|||
|
|
### Languages
|
|||
|
|
|
|||
|
|
Two supported languages: **German** (`de`) and **English** (`en`).
|
|||
|
|
|
|||
|
|
Both locales are JavaScript objects (not JSON), exported as ES module defaults from single files:
|
|||
|
|
- `src/locales/de` (no `.js` extension)
|
|||
|
|
- `src/locales/en` (no `.js` extension)
|
|||
|
|
|
|||
|
|
### Translation Key Structure
|
|||
|
|
|
|||
|
|
| Top-level key | Domain |
|
|||
|
|
|---|---|
|
|||
|
|
| `common` | Shared labels: loading, cancel, save, edit, back, next |
|
|||
|
|
| `auth` | Login/Registration form labels and messages |
|
|||
|
|
| `forgotPassword` | Password reset flow |
|
|||
|
|
| `import` | File import page |
|
|||
|
|
| `menu` | Navigation menu items |
|
|||
|
|
| `landing` | Landing page content |
|
|||
|
|
| `equipment` | Equipment table, dialogs, messages |
|
|||
|
|
| `skill` | Skill table headers and field labels |
|
|||
|
|
| `spell` | Spell table headers and field labels |
|
|||
|
|
| `weapon` | Weapon table, add/edit dialogs |
|
|||
|
|
| `weaponskill` | Weapon skill table |
|
|||
|
|
| `spells.learn` | Spell learning dialog |
|
|||
|
|
| `stats` | Abbreviated stat names (St, Gs, Gw, Ko, In, Zt, Au, pA, Wk...) |
|
|||
|
|
| `chars` / singular view names | View tab labels |
|
|||
|
|
| `characters.list` | Dashboard character list UI |
|
|||
|
|
| `characters.create` | Character wizard UI |
|
|||
|
|
| `characters.basicInfo` | Wizard step 1: basic info |
|
|||
|
|
| `characters.attributes` | Wizard step 2: attributes |
|
|||
|
|
| `characters.derivedValues` | Wizard step 3: derived values |
|
|||
|
|
| `characters.datasheet` | Datasheet edit hint |
|
|||
|
|
| `experience` | EP + wealth section |
|
|||
|
|
| `audit` | Audit log labels and filters |
|
|||
|
|
| `export` | Export dialog labels and messages |
|
|||
|
|
| `visibility` | Visibility/sharing dialog |
|
|||
|
|
| `userManagement` | Admin user management |
|
|||
|
|
| `profile` | User profile page |
|
|||
|
|
| `deleteChar` | Delete character authorization message |
|
|||
|
|
| `maintenance` | Maintenance section label |
|
|||
|
|
| `maintmenu` | Maintenance sub-menu items |
|
|||
|
|
| `believe` | Belief master data management |
|
|||
|
|
| `gamesystem` | Game system master data |
|
|||
|
|
| `litsource` | Literature source master data |
|
|||
|
|
| `misc` | Misc lookup master data |
|
|||
|
|
| `skillimprovement` | Skill improvement cost master data |
|
|||
|
|
| `help` | Help page content + FAQ keys |
|
|||
|
|
| `sponsors` | Sponsors page content |
|
|||
|
|
| `systemInfo` | System info page content |
|
|||
|
|
|
|||
|
|
**Note:** Some keys exist only partially in EN (e.g., `spells.learn` labels use German text in the EN locale — incomplete translation).
|
|||
|
|
|
|||
|
|
### Language switching
|
|||
|
|
|
|||
|
|
1. Via `UserProfileView` → `PUT /api/user/language` → updates `this.$i18n.locale` and `localStorage`
|
|||
|
|
2. Via `userStore.fetchCurrentUser()` → automatically applies `profile.preferred_language` if set
|
|||
|
|
3. The `LanguageSwitcher.vue` component exists but is marked unused.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. Styling
|
|||
|
|
|
|||
|
|
### Architecture
|
|||
|
|
|
|||
|
|
**Global-first approach:** All shared styles live in `src/assets/main.css` (imported in `main.js` as a side-effect import). Components use `<style>` (unscoped, contributing to global scope) or `<style scoped>` only for component-specific overrides.
|
|||
|
|
|
|||
|
|
Convention from vue.instructions.md: `<template>`, then `<style scoped>`, then `<script>` (template-first order, opposite of Vite default).
|
|||
|
|
|
|||
|
|
### CSS Files
|
|||
|
|
|
|||
|
|
**`src/assets/base.css`** — CSS custom properties (variables):
|
|||
|
|
```css
|
|||
|
|
:root {
|
|||
|
|
--vt-c-black-soft: #222222; /* nav background */
|
|||
|
|
--color-primary: #007bff; /* primary blue */
|
|||
|
|
--color-bg-secondary: #f8f9fa;
|
|||
|
|
--color-text-primary: #333;
|
|||
|
|
--color-text-secondary: #495057;
|
|||
|
|
--padding-xs/sm/md/lg /* 4/8/16/24px */
|
|||
|
|
--margin-xs/sm/md/lg /* 4/8/16/24px */
|
|||
|
|
--border-radius: 6px;
|
|||
|
|
--box-shadow-light: 0 2px 4px rgba(0,0,0,0.1);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
Dark mode support via `@media (prefers-color-scheme: dark)`.
|
|||
|
|
|
|||
|
|
**`src/assets/main.css`** — Application shell and shared component styles:
|
|||
|
|
|
|||
|
|
Key CSS classes defined globally:
|
|||
|
|
- `.top-nav` — fixed 60px navigation bar (black background)
|
|||
|
|
- `.main-content` — flex-grow content area with top+bottom padding for fixed nav+submenu
|
|||
|
|
- `.fullwidth-container` — full-width component wrapper with bottom padding for submenu
|
|||
|
|
- `.card` — white bordered box with hover animation
|
|||
|
|
- `.grid-container`, `.grid-2/3/4-columns` — responsive CSS grid layouts
|
|||
|
|
- `.page-header` — section header with blue 2px bottom border
|
|||
|
|
- `.section-header` — subsection header
|
|||
|
|
- `.submenu` — fixed bottom tab bar for character/maintenance view switching
|
|||
|
|
- `.cd-table`, `.cd-table-header` — data tables (green headers `#1da766`)
|
|||
|
|
- `.list-item`, `.list-container`, `.charlist` — character list cards
|
|||
|
|
- `.modal-overlay`, `.modal-content`, `.modal-header/body/footer` — modal dialogs
|
|||
|
|
- `.form-group`, `.form-control`, `.form-row` — form layout
|
|||
|
|
- `.btn`, `.btn-primary`, `.btn-secondary`, `.btn-danger`, `.btn-success` — button variants
|
|||
|
|
- `.badge`, `.badge-success`, `.badge-danger`, `.badge-warning`, `.badge-secondary` — status pills
|
|||
|
|
- `.loading`, `.empty-state` — feedback states
|
|||
|
|
|
|||
|
|
**Accent colour:** `#1da766` (green) used for table headers, learning mode buttons, active nav items.
|
|||
|
|
|
|||
|
|
**Primary colour:** `#007bff` (blue) used for page headers, primary buttons, links.
|
|||
|
|
|
|||
|
|
### Component-specific styles
|
|||
|
|
|
|||
|
|
Most components either have no `<style>` block or minimal overrides. Examples:
|
|||
|
|
- `SkillView.vue` — `@keyframes slideIn` for learning mode animation
|
|||
|
|
- `EquipmentView.vue` — `.modal-fullscreen` flex layout fix
|
|||
|
|
- `ForgotPasswordForm.vue` — local `.reset-*` classes for that form layout
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 10. Business Logic in Frontend
|
|||
|
|
|
|||
|
|
### Form Validation
|
|||
|
|
|
|||
|
|
| Location | Validation |
|
|||
|
|
|---|---|
|
|||
|
|
| `RegisterForm` | Password == confirmPassword (client), email type input |
|
|||
|
|
| `UserProfileView` | Display name max 30 chars, email != current, passwords min 6 chars + match |
|
|||
|
|
| `UserManagementView` | Passwords min 6 chars + match |
|
|||
|
|
| `CharacterBasicInfo` | All required fields non-empty (`isValid` computed) |
|
|||
|
|
| `CharacterAttributes` | Race-specific beauty restrictions (Elves ≥81, Gnomes/Dwarves ≤80) |
|
|||
|
|
| `FileUploadPage` | MIME type check for `.json`/`.csv` files |
|
|||
|
|
| `SkillImproveDialog` | Must select at least one level, must be able to afford it |
|
|||
|
|
| `SkillLearnDialog` | Skills must be affordable (EP + Gold check) |
|
|||
|
|
|
|||
|
|
### Data Transformation
|
|||
|
|
|
|||
|
|
**Character stat access (`DatasheetView.getStat`):**
|
|||
|
|
```js
|
|||
|
|
getStat(path) {
|
|||
|
|
return path.split('.').reduce((obj, key) => obj?.[key], this.character) ?? '-'
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
Deep path traversal on nested character object. Same pattern for saving via `pathParts` loop.
|
|||
|
|
|
|||
|
|
**Glaube (beliefs) encoding:** Multi-select array ↔ comma-separated string conversion in DatasheetView.
|
|||
|
|
|
|||
|
|
**Currency conversion (`ExperianceView`):**
|
|||
|
|
```js
|
|||
|
|
totalWealthInGS() {
|
|||
|
|
return goldstücke + Math.floor(silberstücke / 10) + Math.floor(kupferstücke / 10)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
Midgard RPG conversion: 1 GS = 10 SS = 10 KS.
|
|||
|
|
|
|||
|
|
**Skill level improvement cost computation:** Delegated to backend via API — frontend only displays what the server returns.
|
|||
|
|
|
|||
|
|
**Character creation dice rolls:**
|
|||
|
|
- `rollDie(100)` for attributes
|
|||
|
|
- Social class: `$rollDice(1, 100)` + class modifier → lookup table for result
|
|||
|
|
- Derived values: formulas like `1d100 + 4×(In/10) - 20` for PA; `1d3 + 7 + Ko/10` for LP
|
|||
|
|
|
|||
|
|
### Computed State
|
|||
|
|
|
|||
|
|
| Component | Computed | Logic |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `LandingView` | `isBackendAvailable` | backend version not in error states |
|
|||
|
|
| `ExportDialog` | `canExport` | format selected AND (if PDF) template selected |
|
|||
|
|
| `VisibilityDialog` | `filteredAvailableUsers` | exclude already-shared users, apply search query |
|
|||
|
|
| `ExperianceView` | `totalWealthInGS` | currency conversion formula |
|
|||
|
|
| `CharacterDetails` | `isOwner` | `character.user_id === userStore.currentUser.id` |
|
|||
|
|
| `SkillImproveDialog` | `remainingEP`, `remainingGold`, `remainingPP` | current resources minus selected cost |
|
|||
|
|
| `SkillLearnDialog` | `sortedFilteredSkills` | filter by category + search, sort by name/cost |
|
|||
|
|
| `CharacterBasicInfo` | `isValid` | all required fields filled |
|
|||
|
|
| `userStore` | `isAdmin`, `isMaintainer` | role string comparisons |
|
|||
|
|
|
|||
|
|
### Auth State Management
|
|||
|
|
|
|||
|
|
Auth state is managed entirely via `localStorage.token`. No reactive store for the token itself — components call `isLoggedIn()` directly at mount time. The `App.vue` listens to `storage` events (cross-tab) and a custom `auth-changed` event (same-tab) to update the `loggedIn` data property and show/hide the navigation menu.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 11. User Flows
|
|||
|
|
|
|||
|
|
### Login Flow
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
/ (Landing) → click "Zum Login"
|
|||
|
|
→ /login → LoginForm
|
|||
|
|
→ POST /login { username, password }
|
|||
|
|
→ JWT stored in localStorage.token
|
|||
|
|
→ userStore.fetchCurrentUser() (GET /api/user/profile)
|
|||
|
|
→ window.dispatchEvent('auth-changed')
|
|||
|
|
→ App.vue re-checks auth, shows Menu
|
|||
|
|
→ router.push('/dashboard')
|
|||
|
|
→ /dashboard → DashboardView → CharacterList
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Registration Flow
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
/register → RegisterForm
|
|||
|
|
→ validates passwords match
|
|||
|
|
→ POST /register { username, password, email }
|
|||
|
|
→ shows success badge
|
|||
|
|
→ user manually navigates to /login
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Password Reset Flow
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
/forgot-password → ForgotPasswordForm
|
|||
|
|
→ POST /password-reset/request { email, redirect_url }
|
|||
|
|
→ shows success state (no redirect)
|
|||
|
|
→ user receives email with link
|
|||
|
|
→ link leads to /reset-password?token=X
|
|||
|
|
→ ResetPasswordForm → POST /password-reset/confirm
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Character List / Dashboard Flow
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
/dashboard → CharacterList
|
|||
|
|
→ GET /api/characters → { self_owned, others }
|
|||
|
|
→ GET /api/characters/create-sessions → { sessions }
|
|||
|
|
→ renders owned + shared character links
|
|||
|
|
→ renders active creation session cards
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Character Creation Flow (New Character)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Dashboard → "Create New Character"
|
|||
|
|
→ POST /api/characters/create-session → { session_id }
|
|||
|
|
→ router.push('/character/create/:sessionId')
|
|||
|
|
|
|||
|
|
/character/create/:sessionId → CharacterCreation
|
|||
|
|
→ GET /api/characters/create-session/:id (load state)
|
|||
|
|
→ GET /api/characters/skill-categories (load skill budgets)
|
|||
|
|
|
|||
|
|
Step 1: CharacterBasicInfo
|
|||
|
|
→ fill name, origin, belief, gender, race, class, social class
|
|||
|
|
→ "Next: Attributes →"
|
|||
|
|
→ PUT /api/characters/create-session/:id/step1 (save)
|
|||
|
|
|
|||
|
|
Step 2: CharacterAttributes
|
|||
|
|
→ set 7 attributes (1–100), optionally roll dice
|
|||
|
|
→ "Next: Derived Values →"
|
|||
|
|
→ PUT /api/characters/create-session/:id/step2
|
|||
|
|
|
|||
|
|
Step 3: CharacterDerivedValues
|
|||
|
|
→ view/adjust calculated values, optionally recalculate
|
|||
|
|
→ "Next: Skills →"
|
|||
|
|
→ PUT /api/characters/create-session/:id/step3
|
|||
|
|
|
|||
|
|
Step 4: CharacterSkills
|
|||
|
|
→ select skills from category-budgeted list
|
|||
|
|
→ "Next: Spells →"
|
|||
|
|
→ PUT /api/characters/create-session/:id/step4
|
|||
|
|
|
|||
|
|
Step 5: CharacterSpells
|
|||
|
|
→ optionally select spells
|
|||
|
|
→ "Create Character" → POST /api/characters/create
|
|||
|
|
→ router.push('/character/:newId')
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Sessions can be resumed from the dashboard ("Continue Character Creation") or deleted.
|
|||
|
|
|
|||
|
|
### Character Management Flow
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
/character/:id → CharacterDetails
|
|||
|
|
→ GET /api/characters/:id
|
|||
|
|
|
|||
|
|
Tab: DatasheetView (default)
|
|||
|
|
→ displays stats + bio info
|
|||
|
|
→ double-click any field to edit inline
|
|||
|
|
→ blur/Enter → PUT /api/characters/:id
|
|||
|
|
|
|||
|
|
Tab: SkillView
|
|||
|
|
→ displays categorized skills + weapon skills + innate skills
|
|||
|
|
→ owner: PP adjustment (+/− buttons)
|
|||
|
|
→ owner: learning mode toggle (🎓)
|
|||
|
|
→ SkillLearnDialog: learn new skill
|
|||
|
|
→ SkillImproveDialog: improve existing skill
|
|||
|
|
→ inline add skill dialog
|
|||
|
|
|
|||
|
|
Tab: WeaponView
|
|||
|
|
→ displays weapons table
|
|||
|
|
→ owner: add weapon (search + select from master data)
|
|||
|
|
→ owner: edit weapon (magical, bonuses, value)
|
|||
|
|
→ owner: delete weapon
|
|||
|
|
|
|||
|
|
Tab: SpellView
|
|||
|
|
→ displays spells table
|
|||
|
|
→ owner: 🎓 → SpellLearnDialog
|
|||
|
|
|
|||
|
|
Tab: EquipmentView
|
|||
|
|
→ displays equipment table
|
|||
|
|
→ owner: add equipment (search + select from master data)
|
|||
|
|
→ owner: delete equipment
|
|||
|
|
|
|||
|
|
Tab: ExperianceView
|
|||
|
|
→ EP display + add/remove
|
|||
|
|
→ Gold display + add/remove
|
|||
|
|
→ AuditLogView (filterable change history)
|
|||
|
|
|
|||
|
|
Tab: DeleteCharView
|
|||
|
|
→ owner only: confirm + DELETE /api/characters/:id → /dashboard
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### PDF Export Flow
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
CharacterDetails → 📄 button → ExportDialog
|
|||
|
|
|
|||
|
|
Step 1: Load templates (GET /api/pdf/templates)
|
|||
|
|
Step 2: Select format (PDF / VTT / BaMoRT JSON)
|
|||
|
|
Step 3: If PDF → select template + optional showUserName checkbox
|
|||
|
|
Step 4: Click "Export"
|
|||
|
|
|
|||
|
|
PDF path:
|
|||
|
|
GET /api/pdf/export/:id?template=X → { filename }
|
|||
|
|
window.open(API.baseURL + /api/pdf/file/filename, '_blank')
|
|||
|
|
|
|||
|
|
VTT path:
|
|||
|
|
GET /api/importer/export/vtt/:id/file (blob)
|
|||
|
|
→ programmatic <a download> click
|
|||
|
|
|
|||
|
|
BaMoRT JSON path:
|
|||
|
|
GET /api/transfer/download/:id (blob)
|
|||
|
|
→ programmatic <a download> click
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Visibility / Sharing Flow
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
CharacterDetails → 🌐/🔒 button (owner only) → VisibilityDialog
|
|||
|
|
→ GET /api/characters/:id/available-users
|
|||
|
|
→ GET /api/characters/:id/shares (current)
|
|||
|
|
→ Toggle private/public radio
|
|||
|
|
→ Search + add users from available list
|
|||
|
|
→ Remove users from shared list
|
|||
|
|
→ "Save" → PUT /api/characters/:id/visibility + share update
|
|||
|
|
→ emits visibility-updated → CharacterDetails updates character.public
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Admin User Management Flow
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Menu Admin → "User Management" (admin only)
|
|||
|
|
→ /users → UserManagementView
|
|||
|
|
→ GET /api/users
|
|||
|
|
→ table of all users
|
|||
|
|
|
|||
|
|
Per user (excluding self):
|
|||
|
|
"Change Role" → modal → select role → PUT /api/users/:id/role
|
|||
|
|
"Change Password" → modal → new password → PUT /api/users/:id/password
|
|||
|
|
"Delete" → confirm modal → DELETE /api/users/:id
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 12. Component Hierarchy
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
App.vue
|
|||
|
|
├── Menu.vue [if logged in]
|
|||
|
|
└── <router-view> (main content)
|
|||
|
|
├── LandingView.vue [/]
|
|||
|
|
├── LoginView.vue [/login]
|
|||
|
|
│ └── LoginForm.vue
|
|||
|
|
├── RegisterView.vue [/register]
|
|||
|
|
│ └── RegisterForm.vue
|
|||
|
|
├── ForgotPasswordView.vue [/forgot-password]
|
|||
|
|
│ └── ForgotPasswordForm.vue
|
|||
|
|
├── ResetPasswordView.vue [/reset-password]
|
|||
|
|
│ └── ResetPasswordForm.vue
|
|||
|
|
├── DashboardView.vue [/dashboard] *auth*
|
|||
|
|
│ └── CharacterList.vue
|
|||
|
|
│ └── CharacterCreationSessions.vue
|
|||
|
|
├── UserProfileView.vue [/profile] *auth*
|
|||
|
|
├── UserManagementView.vue [/users] *admin*
|
|||
|
|
├── FileUploadPage.vue [/upload] *auth*
|
|||
|
|
├── MaintenanceView.vue [/maintenance] *auth*
|
|||
|
|
│ └── Maintenance.vue
|
|||
|
|
│ ├── maintenance/SkillView.vue (default)
|
|||
|
|
│ ├── maintenance/SpellView.vue
|
|||
|
|
│ ├── maintenance/EquipmentView.vue
|
|||
|
|
│ ├── maintenance/WeaponView.vue
|
|||
|
|
│ ├── maintenance/WeaponSkillView.vue
|
|||
|
|
│ ├── maintenance/BelieveView.vue
|
|||
|
|
│ ├── maintenance/GameSystemView.vue
|
|||
|
|
│ ├── maintenance/LitSourceView.vue
|
|||
|
|
│ ├── maintenance/MiscLookupView.vue
|
|||
|
|
│ └── maintenance/SkillImprovementCostView.vue
|
|||
|
|
├── AusruestungView.vue [/ausruestung/:characterId] *auth*
|
|||
|
|
│ └── AusruestungList.vue
|
|||
|
|
│ └── AusruestungForm.vue
|
|||
|
|
├── SponsorsView.vue [/sponsors]
|
|||
|
|
├── HelpView.vue [/help]
|
|||
|
|
├── SystemInfoView.vue [/system-info]
|
|||
|
|
├── CharacterDetails.vue [/character/:id] *auth*
|
|||
|
|
│ ├── ExportDialog.vue
|
|||
|
|
│ ├── VisibilityDialog.vue
|
|||
|
|
│ └── <dynamic component :is="currentView">
|
|||
|
|
│ ├── DatasheetView.vue (default)
|
|||
|
|
│ │ └── ImageUploadCropper.vue
|
|||
|
|
│ ├── SkillView.vue
|
|||
|
|
│ │ ├── SkillLearnDialog.vue
|
|||
|
|
│ │ └── SkillImproveDialog.vue
|
|||
|
|
│ ├── WeaponView.vue
|
|||
|
|
│ ├── SpellView.vue
|
|||
|
|
│ │ └── SpellLearnDialog.vue
|
|||
|
|
│ ├── EquipmentView.vue
|
|||
|
|
│ ├── ExperianceView.vue
|
|||
|
|
│ │ └── AuditLogView.vue
|
|||
|
|
│ └── DeleteCharView.vue
|
|||
|
|
└── CharacterCreation.vue [/character/create/:sessionId] *auth*
|
|||
|
|
├── CharacterCreation/CharacterBasicInfo.vue (step 1)
|
|||
|
|
├── CharacterCreation/CharacterAttributes.vue (step 2)
|
|||
|
|
├── CharacterCreation/CharacterDerivedValues.vue (step 3)
|
|||
|
|
├── CharacterCreation/CharacterSkills.vue (step 4)
|
|||
|
|
└── CharacterCreation/CharacterSpells.vue (step 5)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Component-to-Parent Emits Summary
|
|||
|
|
|
|||
|
|
| Component | Emits | Consumed by |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `CharacterCreationSessions` | `continue-session`, `delete-session` | `CharacterList` |
|
|||
|
|
| `ExportDialog` | `update:showDialog`, `export-success` | `CharacterDetails` |
|
|||
|
|
| `VisibilityDialog` | `update:showDialog`, `visibility-updated` | `CharacterDetails` |
|
|||
|
|
| `DatasheetView` | `character-updated` | `CharacterDetails` |
|
|||
|
|
| `SkillView` | `character-updated` | `CharacterDetails` |
|
|||
|
|
| `WeaponView` | `character-updated` | `CharacterDetails` |
|
|||
|
|
| `EquipmentView` | `character-updated` | `CharacterDetails` |
|
|||
|
|
| `SpellView` | `character-updated` | `CharacterDetails` |
|
|||
|
|
| `SkillLearnDialog` | `close`, `skill-learned` | `SkillView` |
|
|||
|
|
| `SkillImproveDialog` | `close`, `skill-updated` | `SkillView` |
|
|||
|
|
| `SpellLearnDialog` | `close`, `spell-learned` | `SpellView` |
|
|||
|
|
| `ImageUploadCropper` | `image-updated` | `DatasheetView` |
|
|||
|
|
| `DeleteCharView` | `deleted`, `cancel` | `CharacterDetails` |
|
|||
|
|
| `AusruestungForm` | `added` | `AusruestungList` |
|
|||
|
|
| `CharacterBasicInfo` | `next`, `save` | `CharacterCreation` |
|
|||
|
|
| `CharacterAttributes` | `next`, `previous`, `save` | `CharacterCreation` |
|
|||
|
|
| `CharacterDerivedValues` | `next`, `previous`, `save` | `CharacterCreation` |
|
|||
|
|
| `CharacterSkills` | `next`, `previous`, `save` | `CharacterCreation` |
|
|||
|
|
| `CharacterSpells` | `previous`, `finalize`, `save` | `CharacterCreation` |
|