adopting agent instructions
for a more consistent coding style
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# Bamort Development Instructions
|
||||
|
||||
READ THIS FILE CAREFULLY AND COMPLETE BEFORE STARTING DEVELOPMENT!
|
||||
|
||||
Bamort is a role-playing game character management system (MOAM replacement) with a Go backend and Vue.js frontend.
|
||||
|
||||
## Project Overview
|
||||
@@ -56,34 +58,6 @@ module/
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend Architecture (`frontend/`)
|
||||
|
||||
### Component Structure
|
||||
- **Views**: `src/views/` - Page-level components (CharacterView, DashboardView)
|
||||
- **Components**: `src/components/` - Reusable components (CharacterDetails, SkillView)
|
||||
- **Utils**: `src/utils/api.js` - Axios instance with JWT interceptor
|
||||
- **Locales**: `src/locales/de` and `src/locales/en` - i18n translations (JS objects, not JSON)
|
||||
- **Store**: Pinia stores in `src/stores/`
|
||||
|
||||
### API Communication
|
||||
- Use `API.get()`, `API.post()` from `utils/api.js` (auto-adds auth headers)
|
||||
- Base URL: `import.meta.env.VITE_API_URL` (defaults to `http://localhost:8180`)
|
||||
- Example:
|
||||
```js
|
||||
const response = await API.get(`/api/characters/${this.id}`)
|
||||
```
|
||||
|
||||
### Translation Pattern
|
||||
Add to both `locales/de` and `locales/en`:
|
||||
```js
|
||||
export default {
|
||||
export: {
|
||||
selectTemplate: 'Vorlage wählen', // DE
|
||||
exportPDF: 'PDF Export'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Docker Development Workflow
|
||||
|
||||
### Verify Containers
|
||||
@@ -141,21 +115,6 @@ if err != nil {
|
||||
}
|
||||
```
|
||||
|
||||
### Modal Dialogs (Frontend)
|
||||
```vue
|
||||
<div v-if="showDialog" class="modal-overlay" @click.self="showDialog = false">
|
||||
<div class="modal-content"><!-- content --></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Popup Blocker Workaround
|
||||
Open window **synchronously** before async calls:
|
||||
```js
|
||||
const newWindow = window.open('', '_blank')
|
||||
// ... then await API call ...
|
||||
newWindow.location.href = url
|
||||
```
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **NEVER** write example/demo code - only production code
|
||||
@@ -163,13 +122,11 @@ newWindow.location.href = url
|
||||
3. **ALWAYS** check `docker ps` before assuming containers are running
|
||||
4. **ALWAYS** use TDD: write failing test first, then implement
|
||||
5. **ALWAYS** use KISS principle: simplest solution that works
|
||||
6. **ALWAYS** add translations to both DE and EN locales
|
||||
7. **ALWAYS** use global CSS definition to ensure consistent style
|
||||
|
||||
## File-Specific Instructions
|
||||
|
||||
Load additional instructions for specific file types:
|
||||
- Go files: See `.github/instructions/go.instructions.md`
|
||||
- Vue files: See `.github/instructions/vue.instructions.md`
|
||||
- js files: See `.github/instructions/js.instructions.md`
|
||||
- css files: See `.github/instructions/css.instructions.md`
|
||||
- CSS files: See `.github/instructions/css.instructions.md`
|
||||
- JS files: See `.github/instructions/js.instructions.md`
|
||||
|
||||
@@ -5,480 +5,41 @@ applyTo: '**/*.css,**/*.vue'
|
||||
|
||||
# CSS Development Instructions
|
||||
|
||||
Follow project-specific CSS conventions and modern best practices.
|
||||
## Cascading Design & Style Hierarchy
|
||||
|
||||
## Scoped Styles in Vue Components
|
||||
**Central styles in `src/assets/main.css` are ALWAYS the preferred way to style.** They ensure a consistent design across all views and components.
|
||||
|
||||
### Always Use Scoped Styles
|
||||
```vue
|
||||
<style scoped>
|
||||
.component-class {
|
||||
/* Styles only apply to this component */
|
||||
}
|
||||
</style>
|
||||
```
|
||||
### Cascade Layers (top to bottom)
|
||||
1. **`src/assets/base.css`** — CSS variables, resets, root typography and color tokens
|
||||
2. **`src/assets/main.css`** — Global layout, nav, buttons, modals, forms, loading states, typography, spacing — **all shared UI patterns go here**
|
||||
3. **`<style scoped>` in `.vue` files** — Only for component-specific layout overrides that have no equivalent in `main.css`
|
||||
|
||||
**Critical**: Use `scoped` attribute to prevent style conflicts between components.
|
||||
### Rule: Central vs. Scoped CSS
|
||||
- **Use `main.css`** for: buttons, modals, forms, inputs, nav, loading spinners, typography, color tokens, flex layout utilities — anything reused in 2+ places
|
||||
- **Use `<style scoped>`** only when a style is truly unique to one component and cannot be expressed by an existing global class
|
||||
- **Never** duplicate a style from `main.css` in a scoped block
|
||||
|
||||
## Layout Patterns
|
||||
## Key CSS Techniques
|
||||
|
||||
### Flexbox for Component Layouts
|
||||
Standard pattern for headers, modals, and lists:
|
||||
- **Flexbox** for all layouts — use `gap` instead of margins between flex children
|
||||
- **CSS custom properties** (variables) from `base.css` — always reference via `var(--token-name)`
|
||||
- **Transitions** (`transition: all 0.2s ease`) on all interactive elements
|
||||
- **`@media` queries** in `main.css` for responsive breakpoints (mobile-first)
|
||||
- **`z-index` layers** defined centrally: modals `1000`, dropdowns `100`, tooltips `200`
|
||||
|
||||
```css
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px; /* Use gap instead of margin */
|
||||
}
|
||||
## Style Conventions
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
```
|
||||
- Class selectors only — no IDs, no inline styles, no `!important`
|
||||
- `rem`/`em` for font sizes; `px` only for borders and fixed-size icons
|
||||
- Max 3 levels of selector nesting
|
||||
- Always include `:hover`, `:focus`, `:disabled` states for interactive elements
|
||||
- `box-sizing: border-box` set globally — do not override
|
||||
|
||||
### Common Flex Patterns
|
||||
```css
|
||||
/* Horizontal layout with spacing */
|
||||
.horizontal-layout {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
## Anti-Patterns
|
||||
|
||||
/* Vertical centering */
|
||||
.centered {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Space between items */
|
||||
.space-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
```
|
||||
|
||||
## Modal Dialog Styling
|
||||
|
||||
### Standard Modal Pattern
|
||||
```css
|
||||
/* Overlay - covers entire viewport */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Modal content container */
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Modal sections */
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
position: relative; /* For loading overlays */
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 20px;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
```
|
||||
|
||||
## Button Styling
|
||||
|
||||
### Standard Button Styles
|
||||
```css
|
||||
/* Primary action button */
|
||||
.btn-primary,
|
||||
.btn-export {
|
||||
padding: 10px 20px;
|
||||
border: 1px solid #007bff;
|
||||
border-radius: 6px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #0056b3;
|
||||
border-color: #0056b3;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Cancel/secondary button */
|
||||
.btn-cancel {
|
||||
padding: 10px 20px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
background: #f8f9fa;
|
||||
color: #495057;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
background: #e9ecef;
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
|
||||
/* Icon-only button */
|
||||
.export-button-small {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border: 1px solid #007bff;
|
||||
border-radius: 8px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.export-button-small:hover {
|
||||
background: #0056b3;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
```
|
||||
|
||||
## Form Elements
|
||||
|
||||
### Input Styling
|
||||
```css
|
||||
.template-select,
|
||||
input[type="text"],
|
||||
input[type="email"] {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
color: #495057;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.template-select:focus,
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
input:disabled,
|
||||
select:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
background: #e9ecef;
|
||||
}
|
||||
```
|
||||
|
||||
### Checkbox Styling
|
||||
```css
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
```
|
||||
|
||||
## Loading Animations
|
||||
|
||||
### Spinner Animation
|
||||
```css
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #007bff;
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
```
|
||||
|
||||
### Loading Overlay
|
||||
```css
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.loading-overlay p {
|
||||
color: #007bff;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Color Scheme
|
||||
|
||||
### Standard Colors
|
||||
```css
|
||||
/* Primary */
|
||||
--primary: #007bff;
|
||||
--primary-hover: #0056b3;
|
||||
|
||||
/* Text */
|
||||
--text-primary: #333;
|
||||
--text-secondary: #495057;
|
||||
--text-muted: #666;
|
||||
|
||||
/* Backgrounds */
|
||||
--bg-light: #f8f9fa;
|
||||
--bg-gray: #e9ecef;
|
||||
|
||||
/* Borders */
|
||||
--border-light: #dee2e6;
|
||||
--border-dark: #adb5bd;
|
||||
|
||||
/* Semantic colors */
|
||||
--success: #28a745;
|
||||
--danger: #dc3545;
|
||||
--warning: #ffc107;
|
||||
```
|
||||
|
||||
Use these consistently across components for visual coherence.
|
||||
|
||||
## Spacing System
|
||||
|
||||
### Use Consistent Spacing
|
||||
```css
|
||||
/* Prefer these spacing values */
|
||||
gap: 8px; /* Tight spacing */
|
||||
gap: 10px; /* Default spacing */
|
||||
gap: 15px; /* Medium spacing */
|
||||
gap: 20px; /* Large spacing */
|
||||
|
||||
padding: 10px 12px; /* Inputs */
|
||||
padding: 20px; /* Modal sections */
|
||||
```
|
||||
|
||||
## Typography
|
||||
|
||||
### Font Sizing
|
||||
```css
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
p, span {
|
||||
font-size: 0.95rem;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
}
|
||||
```
|
||||
|
||||
### Font Weight
|
||||
```css
|
||||
font-weight: 400; /* Normal */
|
||||
font-weight: 500; /* Medium (buttons, labels) */
|
||||
font-weight: 600; /* Semibold (headings) */
|
||||
```
|
||||
|
||||
## Transitions and Animations
|
||||
|
||||
### Standard Transitions
|
||||
```css
|
||||
/* Buttons, interactive elements */
|
||||
transition: all 0.2s ease;
|
||||
|
||||
/* Background changes */
|
||||
transition: background 0.3s ease;
|
||||
|
||||
/* Transform animations */
|
||||
transition: transform 0.2s ease;
|
||||
```
|
||||
|
||||
### Hover Effects
|
||||
```css
|
||||
button:hover {
|
||||
transform: scale(1.02); /* Subtle scale */
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
```
|
||||
|
||||
## Responsive Design
|
||||
|
||||
### Mobile-First Approach
|
||||
```css
|
||||
/* Base styles (mobile) */
|
||||
.modal-content {
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
/* Tablet and up */
|
||||
@media (min-width: 768px) {
|
||||
.modal-content {
|
||||
width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop */
|
||||
@media (min-width: 1024px) {
|
||||
.modal-content {
|
||||
width: 700px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use `scoped`** on Vue component styles
|
||||
2. **Use flexbox for layouts** instead of floats or positioning
|
||||
3. **Use `gap` property** instead of margins for spacing
|
||||
4. **Keep selectors simple** - avoid deep nesting
|
||||
5. **Use relative units** (`rem`, `em`) for font sizes
|
||||
6. **Add transitions** for interactive elements
|
||||
7. **Use CSS variables** for repeated values
|
||||
8. **Keep z-index organized** (modals: 1000, dropdowns: 100, etc.)
|
||||
9. **Test hover states** for all interactive elements
|
||||
10. **Include disabled states** for buttons and inputs
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
❌ Don't use `!important` unless absolutely necessary
|
||||
❌ Don't use inline styles in templates - use classes
|
||||
❌ Don't use fixed pixel widths for responsive layouts
|
||||
❌ Don't nest selectors more than 3 levels deep
|
||||
❌ Don't use IDs for styling - use classes
|
||||
❌ Don't forget `:hover`, `:focus`, `:disabled` states
|
||||
❌ Don't use `position: absolute` unless necessary
|
||||
❌ Don't forget to test in different viewport sizes
|
||||
❌ Don't use vendor prefixes manually - use autoprefixer
|
||||
|
||||
## Common Component Patterns
|
||||
|
||||
### Close Button
|
||||
```css
|
||||
.close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
color: #333;
|
||||
}
|
||||
```
|
||||
|
||||
### Full-Height Container
|
||||
```css
|
||||
.character-details {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
```
|
||||
|
||||
### Submenu/Tabs
|
||||
```css
|
||||
.submenu {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin: 20px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.submenu button {
|
||||
padding: 10px 16px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
background: #f8f9fa;
|
||||
color: #495057;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.submenu button.active {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
```
|
||||
❌ Don't add a scoped style that duplicates a `main.css` rule
|
||||
❌ Don't use inline styles in Vue templates
|
||||
❌ Don't use `!important`
|
||||
❌ Don't use IDs as CSS selectors
|
||||
❌ Don't use floats or `position: absolute` for layout — use flexbox
|
||||
❌ Don't add vendor prefixes manually — autoprefixer handles this
|
||||
|
||||
@@ -5,6 +5,8 @@ applyTo: '**/*.go,**/go.mod,**/go.sum'
|
||||
|
||||
# Go Development Instructions
|
||||
|
||||
READ THIS FILE CAREFULLY AND COMPLETE BEFORE STARTING DEVELOPMENT!
|
||||
|
||||
Follow idiomatic Go practices and community standards when writing Go code. These instructions are based on [Effective Go](https://go.dev/doc/effective_go), [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments), and [Google's Go Style Guide](https://google.github.io/styleguide/go/).
|
||||
|
||||
## General Instructions
|
||||
@@ -266,10 +268,10 @@ Follow idiomatic Go practices and community standards when writing Go code. Thes
|
||||
|
||||
### Essential Tools
|
||||
|
||||
- `go fmt`: Format code
|
||||
- `go vet`: Find suspicious constructs
|
||||
- `golint` or `golangci-lint`: Additional linting
|
||||
- `go test`: Run tests
|
||||
- `go fmt`: Format code
|
||||
- `golint` or `golangci-lint`: Additional linting
|
||||
- `go mod`: Manage dependencies
|
||||
- `go generate`: Code generation
|
||||
|
||||
|
||||
@@ -5,394 +5,82 @@ applyTo: '**/*.vue, **/*.ts, **/*.js, **/*.scss'
|
||||
|
||||
# Vue 3 Development Instructions
|
||||
|
||||
Follow Vue 3 best practices and project-specific conventions when writing components.
|
||||
## Architecture & Code Organisation
|
||||
|
||||
## Component Structure
|
||||
### Folder Responsibilities
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `src/views/` | Page-level components mounted by the router — orchestrate layout and child components |
|
||||
| `src/components/` | Reusable UI components — self-contained, receive data via props, emit events upward |
|
||||
| `src/utils/` | Shared utility functions and the central API client — **always prefer these over local re-implementations** |
|
||||
| `src/stores/` | Pinia stores for global state (auth, language, etc.) |
|
||||
| `src/locales/` | i18n translation objects (`de` and `en`, `.js` files, not JSON) |
|
||||
| `src/assets/main.css` | Central styles — see css.instructions.md |
|
||||
|
||||
### Standard Component Layout
|
||||
```vue
|
||||
<template>
|
||||
<!-- HTML template -->
|
||||
</template>
|
||||
### Component vs. Inline Code
|
||||
- **Extract to a component** whenever UI logic would be repeated in more than one view, or when a dialog/panel is complex enough to have its own state
|
||||
- **Prefer a dedicated component** for modal dialogs over writing inline modal markup inside a view — keeps views clean and the dialog reusable
|
||||
- **Write utility functions in `src/utils/`** for any logic that could be used in more than one component (formatting, validation helpers, API wrappers)
|
||||
- **Local methods** are fine for view/component-specific logic that will never be reused
|
||||
|
||||
<style scoped>
|
||||
/* Component-specific styles */
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Component logic
|
||||
</script>
|
||||
### Component File Layout (order matters)
|
||||
```
|
||||
|
||||
**Order matters**: Template, Style, Script (as seen throughout the codebase)
|
||||
|
||||
### Options API Pattern (Primary)
|
||||
Use Options API for consistency with existing codebase:
|
||||
|
||||
```vue
|
||||
<script>
|
||||
export default {
|
||||
name: "ComponentName",
|
||||
props: ["id"],
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
// Initialization logic
|
||||
},
|
||||
methods: {
|
||||
async methodName() {
|
||||
// Use const/let, never var
|
||||
const response = await API.get('/api/endpoint')
|
||||
this.items = response.data
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template> → <style scoped> → <script>
|
||||
```
|
||||
Use **Options API** for consistency with the existing codebase (`data`, `created`, `methods`).
|
||||
|
||||
## API Communication
|
||||
|
||||
### Using the API Utility
|
||||
Always use `API` from `utils/api.js` - it handles authentication automatically:
|
||||
All HTTP calls go through `src/utils/api.js` — an Axios instance with JWT and 401-redirect interceptors.
|
||||
|
||||
```js
|
||||
import API from '../utils/api'
|
||||
- Import `API` from `utils/api.js`; **never** construct raw `axios` calls or add `Authorization` headers manually
|
||||
- Base URL is read from `import.meta.env.VITE_API_URL` (default `http://localhost:8180`)
|
||||
- Catch errors with `try/catch`; extract the message via `error.response?.data?.error ?? error.message` and show it to the user
|
||||
|
||||
// In methods:
|
||||
const response = await API.get(`/api/characters/${this.id}`)
|
||||
const data = await API.post('/api/characters', character)
|
||||
```
|
||||
## Modal Dialogs
|
||||
|
||||
**Never** manually add Authorization headers - the interceptor handles this.
|
||||
**Always implement modals as separate components** in `src/components/`, not as inline markup in a view.
|
||||
|
||||
### Environment Variables
|
||||
API base URL from Vite:
|
||||
```js
|
||||
import API from '../utils/api'
|
||||
// API uses: import.meta.env.VITE_API_URL || 'http://localhost:8180'
|
||||
```
|
||||
- The view renders `<MyDialog v-if="showDialog" @close="showDialog = false" />`
|
||||
- The dialog component handles its own internal state and emits `close` (and any result events) to the parent
|
||||
- Use global CSS classes `modal-overlay`, `modal-content`, `modal-header`, `modal-body`, `modal-footer`, `btn-primary`, `btn-cancel`, `close-button` — all defined in `main.css`, do not redefine them in scoped styles
|
||||
- Close on overlay click with `@click.self`; always include a `×` close button in the header
|
||||
|
||||
### API Configuration (`utils/api.js`)
|
||||
Standard Axios instance with interceptors:
|
||||
```js
|
||||
import axios from 'axios'
|
||||
## State & Reactivity
|
||||
|
||||
const API = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8180'
|
||||
})
|
||||
- **Pinia stores** (`src/stores/`) for state shared across multiple views (language, auth token, etc.)
|
||||
- **Component-local `data()`** for state that belongs only to that component
|
||||
- Always track async operations with an `isLoading` flag; disable interactive elements and show feedback while loading
|
||||
- Use `v-if` for infrequently toggled elements, `v-show` for frequent toggles; always use `:key` with `v-for`
|
||||
|
||||
// Request interceptor - adds auth token
|
||||
API.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
## Internationalization
|
||||
|
||||
// Response interceptor - handles 401
|
||||
API.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default API
|
||||
```
|
||||
|
||||
### Error Handling Pattern
|
||||
```js
|
||||
try {
|
||||
const response = await API.get(`/api/endpoint`)
|
||||
this.data = response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to load data:', error)
|
||||
const errorMsg = error.response?.data?.error ?? error.message
|
||||
alert(`${this.$t('errors.loadFailed')}: ${errorMsg}`)
|
||||
}
|
||||
```
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
### Using Translations
|
||||
```vue
|
||||
<template>
|
||||
<h2>{{ $t('char') }}: {{ character.name }}</h2>
|
||||
<button>{{ $t('export.exportPDF') }}</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Adding New Translations
|
||||
**ALWAYS** add to both `src/locales/de` and `src/locales/en`:
|
||||
|
||||
```js
|
||||
// src/locales/de
|
||||
export default {
|
||||
export: {
|
||||
selectTemplate: 'Vorlage wählen',
|
||||
exportPDF: 'PDF Export'
|
||||
}
|
||||
}
|
||||
|
||||
// src/locales/en
|
||||
export default {
|
||||
export: {
|
||||
selectTemplate: 'Select Template',
|
||||
exportPDF: 'Export PDF'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: Locale files use `.js` extension and export objects, not JSON.
|
||||
|
||||
## Modal Dialog Pattern
|
||||
|
||||
### Standard Modal Structure
|
||||
```vue
|
||||
<template>
|
||||
<!-- Trigger -->
|
||||
<button @click="showDialog = true">Open</button>
|
||||
|
||||
<!-- Modal -->
|
||||
<div v-if="showDialog" class="modal-overlay" @click.self="showDialog = false">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>{{ $t('modal.title') }}</h3>
|
||||
<button @click="showDialog = false" class="close-button">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Content -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button @click="showDialog = false" class="btn-cancel">{{ $t('cancel') }}</button>
|
||||
<button @click="handleSubmit" class="btn-primary">{{ $t('submit') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
showDialog: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**Key conventions:**
|
||||
- Use `@click.self` on overlay to close on outside click
|
||||
- Include close button (×) in header
|
||||
- Separate header, body, footer sections
|
||||
|
||||
## Component Communication
|
||||
|
||||
### Props (Parent → Child)
|
||||
```vue
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
character: Object,
|
||||
id: [String, Number]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Events (Child → Parent)
|
||||
```vue
|
||||
<template>
|
||||
<button @click="notifyParent">Update</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
notifyParent() {
|
||||
this.$emit('character-updated', this.character)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Parent component -->
|
||||
<template>
|
||||
<ChildComponent @character-updated="refreshCharacter" />
|
||||
</template>
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
### Pinia Store Pattern (`stores/`)
|
||||
For global state (language, auth, etc.):
|
||||
```js
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useLanguageStore = defineStore('language', {
|
||||
state: () => ({
|
||||
currentLanguage: localStorage.getItem('language') || 'de'
|
||||
}),
|
||||
actions: {
|
||||
setLanguage(lang) {
|
||||
this.currentLanguage = lang
|
||||
localStorage.setItem('language', lang)
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Loading States
|
||||
Always show feedback for async operations:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<button @click="submit" :disabled="isLoading">
|
||||
<span v-if="!isLoading">{{ $t('submit') }}</span>
|
||||
<span v-else>{{ $t('loading') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return { isLoading: false }
|
||||
},
|
||||
methods: {
|
||||
async submit() {
|
||||
this.isLoading = true
|
||||
try {
|
||||
await API.post('/api/endpoint', this.data)
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Disabling Form Elements During Loading
|
||||
```vue
|
||||
<select v-model="selected" :disabled="isLoading">
|
||||
<input type="checkbox" v-model="option" :disabled="isLoading">
|
||||
```
|
||||
|
||||
## Form Validation
|
||||
|
||||
### Validation Pattern
|
||||
```js
|
||||
methods: {
|
||||
validateForm() {
|
||||
if (!this.selectedTemplate) {
|
||||
alert(this.$t('export.pleaseSelectTemplate'))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
async submit() {
|
||||
if (!this.validateForm()) return
|
||||
|
||||
this.isLoading = true
|
||||
try {
|
||||
await API.post('/api/endpoint', this.data)
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- All user-visible strings use `$t('key')` — never hardcode text in templates
|
||||
- **Always add translations to both** `src/locales/de` and `src/locales/en` at the same time
|
||||
- Use nested keys that reflect the domain (e.g., `export.selectTemplate`)
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
### Popup Blocker Workaround
|
||||
Open windows **synchronously** before async operations:
|
||||
**Popup blocker**: `window.open()` must be called **synchronously** in the click handler — before any `await`. Open the window first, then perform the async API call and update `window.location.href` afterwards.
|
||||
|
||||
```js
|
||||
async exportToPDF() {
|
||||
// Open window FIRST (synchronously)
|
||||
const pdfWindow = window.open('', '_blank')
|
||||
if (!pdfWindow) {
|
||||
alert(this.$t('export.popupBlocked'))
|
||||
return
|
||||
}
|
||||
|
||||
// Show loading page
|
||||
pdfWindow.document.write('<html>...</html>')
|
||||
|
||||
// Then do async work
|
||||
const response = await API.get('/api/pdf/export')
|
||||
|
||||
// Update window with result
|
||||
pdfWindow.location.href = url
|
||||
}
|
||||
```
|
||||
## Code Conventions
|
||||
|
||||
**Critical**: `window.open()` must be called synchronously in the click handler, not after `await`.
|
||||
- `const` by default, `let` when reassignment is needed — never `var`
|
||||
- `===` for all comparisons, never `==`
|
||||
- Optional chaining `?.` and nullish coalescing `??` for safe property access
|
||||
- Keep template expressions simple — move logic to `methods`
|
||||
- PascalCase for component file names (`CharacterDetails.vue`)
|
||||
- Handle errors gracefully — with user-friendly messages
|
||||
- Test with actual API — running in Docker container
|
||||
- Check HMR reload — view logs: `docker logs bamort-frontend-dev`
|
||||
|
||||
## Common Patterns
|
||||
## Anti-Patterns
|
||||
|
||||
### Dynamic Component Loading
|
||||
```vue
|
||||
<template>
|
||||
<component :is="currentView" :character="character" @character-updated="refresh"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ViewA from './ViewA.vue'
|
||||
import ViewB from './ViewB.vue'
|
||||
|
||||
export default {
|
||||
components: { ViewA, ViewB },
|
||||
data() {
|
||||
return { currentView: 'ViewA' }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Conditional Rendering
|
||||
- Use `v-if` for elements that toggle rarely
|
||||
- Use `v-show` for frequent toggles
|
||||
- Use `v-for` with `:key` attribute always
|
||||
|
||||
### Event Modifiers
|
||||
- `@click.self` - only trigger if clicked element itself
|
||||
- `@submit.prevent` - prevent form submission
|
||||
- `@keyup.enter` - keyboard event handling
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use global CSS definition** to ensure consistent styling
|
||||
2. **Always use scoped styles** to avoid CSS conflicts
|
||||
3. **Name components with PascalCase** (e.g., `CharacterDetails.vue`)
|
||||
4. **Use `const` by default, `let` when needed** - never use `var`
|
||||
5. **Use optional chaining `?.` and nullish coalescing `??`** for safe property access
|
||||
6. **Handle errors gracefully** with user-friendly messages
|
||||
7. **Keep template logic simple** - move complex logic to methods
|
||||
8. **Clean up resources** in `beforeUnmount` if needed
|
||||
9. **Test with actual API** running in Docker container
|
||||
10. **Check HMR reload** - view logs: `docker logs bamort-frontend-dev`
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
❌ Don't use `v-if` and `v-for` on the same element
|
||||
❌ Don't mutate props directly
|
||||
❌ Don't use `var` - use `const` or `let`
|
||||
❌ Don't use `==` - always use `===` for comparisons
|
||||
❌ Don't forget to handle loading and error states
|
||||
❌ Don't use inline styles - use scoped CSS
|
||||
❌ Don't call API methods in template expressions
|
||||
❌ Don't forget translations - add to both DE and EN
|
||||
❌ Don't write inline modal markup in views — extract to a component
|
||||
❌ Don't duplicate API call setup — use `utils/api.js`
|
||||
❌ Don't duplicate utility logic — add it to `src/utils/` and import it
|
||||
❌ Don't use inline styles — use global CSS classes from `main.css`; scoped CSS only for genuinely unique local overrides
|
||||
❌ Don't use `v-if` and `v-for` on the same element
|
||||
❌ Don't mutate props directly
|
||||
❌ Don't forget translations — always update both `de` and `en`
|
||||
❌ Don't call API methods in template expressions
|
||||
|
||||
Reference in New Issue
Block a user