added more file type specific instructions
I hope that coding results will be better and more consistent
This commit is contained in:
+175
-14
@@ -1,14 +1,175 @@
|
||||
# general instructions
|
||||
- You are GitHub Copilot, an AI pair programmer.
|
||||
- You are designed to help write code faster and with less effort.
|
||||
- You can generate code snippets, complete code, and suggest improvements.
|
||||
- You can understand and write code in multiple programming languages.
|
||||
- You can help with debugging and fixing errors in code.
|
||||
- You can assist with writing tests and documentation.
|
||||
- You can learn from the context of the code you are working on.
|
||||
- You will NOT write example or demonstration code.
|
||||
- The GitHub repository is github.com:Bardioc26/bamort.git.
|
||||
- The main package is cmd/backend/main.go.
|
||||
- The main function is in cmd/backend/main.go.
|
||||
- You write tests ONLY in _test.go files.
|
||||
- You will NEVER create files for testing with a main() function!
|
||||
# Bamort Development Instructions
|
||||
|
||||
Bamort is a role-playing game character management system (MOAM replacement) with a Go backend and Vue.js frontend.
|
||||
|
||||
## Project Overview
|
||||
|
||||
- **Repository**: github.com:Bardioc26/bamort.git
|
||||
- **Architecture**: Monorepo with separate backend/frontend in Docker containers
|
||||
- **Backend**: Go 1.25 + Gin framework + GORM + MariaDB
|
||||
- **Frontend**: Vue 3 + Vite + i18n
|
||||
- **Development**: Docker Compose with live-reload (Air for Go, Vite HMR for Vue)
|
||||
|
||||
## Backend Architecture (`backend/`)
|
||||
|
||||
### Module Structure
|
||||
Each domain module follows this pattern (e.g., `character/`, `pdfrender/`, `equipment/`):
|
||||
```
|
||||
module/
|
||||
handlers.go # HTTP handlers (Gin controllers)
|
||||
routes.go # Route registration: RegisterRoutes(r *gin.RouterGroup)
|
||||
*_test.go # Tests with setupTestEnvironment(t)
|
||||
```
|
||||
|
||||
**Key conventions:**
|
||||
- **Entry point**: `cmd/main.go` registers all modules via `RegisterRoutes(protected)`
|
||||
- **Models**: `models/` contains GORM entities (e.g., `Char`, `SkFertigkeit`, `EqWaffe`)
|
||||
- **Database**: Shared `database.DB` instance, use `models.MigrateStructure(db)` for migrations
|
||||
- **Configuration**: `config.Cfg` loaded from env vars (see `config/config.go`)
|
||||
- `TEMPLATES_DIR` for PDF templates (default: `./templates`)
|
||||
- `ENVIRONMENT=test|development|production`
|
||||
- `DATABASE_TYPE=mysql|sqlite`
|
||||
|
||||
### Testing Requirements
|
||||
- **NEVER** create test files with `main()` functions
|
||||
- **ALWAYS** use `_test.go` suffix
|
||||
- **ALWAYS** call `setupTestEnvironment(t)` at start of each test:
|
||||
```go
|
||||
func setupTestEnvironment(t *testing.T) {
|
||||
original := os.Getenv("ENVIRONMENT")
|
||||
os.Setenv("ENVIRONMENT", "test")
|
||||
t.Cleanup(func() { /* restore */ })
|
||||
}
|
||||
```
|
||||
- Use `testutils.SetupTestDB()` for database tests (creates SQLite test DB)
|
||||
- Test character ID 18 (Fanjo Vetrani) exists in test database
|
||||
|
||||
### API Patterns
|
||||
- Protected routes under `/api` prefix require JWT authentication
|
||||
- Use `respondWithError(c, status, message)` for error responses
|
||||
- Route registration example:
|
||||
```go
|
||||
func RegisterRoutes(r *gin.RouterGroup) {
|
||||
group := r.Group("/module")
|
||||
group.GET("/list", ListHandler)
|
||||
group.POST("/create", CreateHandler)
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
```bash
|
||||
docker ps | grep bamort-backend-dev # Must be running before testing
|
||||
```
|
||||
|
||||
### Container Names & Ports
|
||||
- `bamort-backend-dev`: Go API at http://localhost:8180 (Air live-reload)
|
||||
- `bamort-frontend-dev`: Vue at http://localhost:5173 (Vite HMR)
|
||||
- `bamort-mariadb-dev`: MariaDB at localhost:3306
|
||||
- `bamort-phpmyadmin-dev`: http://localhost:8082
|
||||
|
||||
### Rebuild After Dockerfile Changes
|
||||
```bash
|
||||
cd /data/dev/bamort
|
||||
docker-compose -f docker/docker-compose.dev.yml build bamort-backend-dev
|
||||
docker-compose -f docker/docker-compose.dev.yml up -d bamort-backend-dev
|
||||
```
|
||||
|
||||
### View Logs
|
||||
```bash
|
||||
docker logs bamort-backend-dev --tail=50
|
||||
docker logs bamort-frontend-dev --tail=20
|
||||
```
|
||||
|
||||
## Testing Commands
|
||||
|
||||
### Backend Tests
|
||||
```bash
|
||||
cd /data/dev/bamort/backend
|
||||
go test -v ./pdfrender/ -run TestExportCharacterToPDF
|
||||
go test -v ./character/
|
||||
```
|
||||
|
||||
### Frontend
|
||||
- HMR auto-reloads on file save
|
||||
- Check browser console and `docker logs bamort-frontend-dev`
|
||||
|
||||
## PDF Rendering Module (`pdfrender/`)
|
||||
|
||||
- Uses `chromedp` for HTML→PDF (requires Chromium in Docker)
|
||||
- Templates in `templates/Default_A4_Quer/` (page1_stats.html, page2_play.html, etc.)
|
||||
- Continuation pages auto-generated for overflow (page1.2_stats.html pattern)
|
||||
- Test with character ID 18: generates 4 pages + continuations if needed
|
||||
- Template capacity defined in HTML comments: `<!-- MaxItems: 58 -->`
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Error Handling (Backend)
|
||||
```go
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
2. **NEVER** create test files with `main()` - use `_test.go`
|
||||
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`
|
||||
|
||||
@@ -0,0 +1,484 @@
|
||||
---
|
||||
description: 'Instructions for writing CSS following project conventions and best practices'
|
||||
applyTo: '**/*.css,**/*.vue'
|
||||
---
|
||||
|
||||
# CSS Development Instructions
|
||||
|
||||
Follow project-specific CSS conventions and modern best practices.
|
||||
|
||||
## Scoped Styles in Vue Components
|
||||
|
||||
### Always Use Scoped Styles
|
||||
```vue
|
||||
<style scoped>
|
||||
.component-class {
|
||||
/* Styles only apply to this component */
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
**Critical**: Use `scoped` attribute to prevent style conflicts between components.
|
||||
|
||||
## Layout Patterns
|
||||
|
||||
### Flexbox for Component Layouts
|
||||
Standard pattern for headers, modals, and lists:
|
||||
|
||||
```css
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px; /* Use gap instead of margin */
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
```
|
||||
|
||||
### Common Flex Patterns
|
||||
```css
|
||||
/* Horizontal layout with spacing */
|
||||
.horizontal-layout {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,342 @@
|
||||
---
|
||||
description: 'Instructions for writing JavaScript following project conventions and ES6+ best practices'
|
||||
applyTo: '**/*.js,**/*.mjs'
|
||||
---
|
||||
|
||||
# JavaScript Development Instructions
|
||||
|
||||
Follow ES6+ best practices and project-specific patterns for JavaScript code.
|
||||
|
||||
## Module System
|
||||
|
||||
### ES6 Modules
|
||||
Use ES6 import/export syntax:
|
||||
|
||||
```js
|
||||
// Named exports
|
||||
export const API = axios.create({ ... })
|
||||
export function helper() { ... }
|
||||
|
||||
// Default export
|
||||
export default {
|
||||
messages: { ... }
|
||||
}
|
||||
|
||||
// Imports
|
||||
import API from '../utils/api'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
```
|
||||
|
||||
## API Configuration (`utils/api.js`)
|
||||
|
||||
### Standard Axios Instance Pattern
|
||||
```js
|
||||
import axios from 'axios'
|
||||
|
||||
const API = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8180'
|
||||
})
|
||||
|
||||
// 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)
|
||||
)
|
||||
|
||||
// Response interceptor - handles 401
|
||||
API.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('token')
|
||||
// Optional: redirect to login
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default API
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- Single Axios instance for the entire app
|
||||
- Auto-adds Authorization header from localStorage
|
||||
- Auto-handles 401 responses
|
||||
- Uses Vite environment variables
|
||||
|
||||
## Pinia Store Pattern (`stores/`)
|
||||
|
||||
### Standard Store Structure
|
||||
```js
|
||||
import { defineStore } from 'pinia'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import de from '@/locales/de'
|
||||
import en from '@/locales/en'
|
||||
|
||||
export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: localStorage.getItem('language') || 'de',
|
||||
fallbackLocale: 'en',
|
||||
messages: { de, en }
|
||||
})
|
||||
|
||||
export const useLanguageStore = defineStore('language', {
|
||||
state: () => ({
|
||||
currentLanguage: localStorage.getItem('language') || 'de'
|
||||
}),
|
||||
actions: {
|
||||
setLanguage(lang) {
|
||||
this.currentLanguage = lang
|
||||
i18n.global.locale.value = lang
|
||||
localStorage.setItem('language', lang)
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Locale Files (`locales/de`, `locales/en`)
|
||||
|
||||
### Translation Object Structure
|
||||
**Important**: Locale files use `.js` extension and export objects (not `.json`):
|
||||
|
||||
```js
|
||||
// locales/de
|
||||
export default {
|
||||
char: 'Figur',
|
||||
menu: {
|
||||
Datasheet: 'Datenblatt',
|
||||
Skill: 'Fertigkeiten'
|
||||
},
|
||||
export: {
|
||||
selectTemplate: 'Vorlage wählen',
|
||||
exportPDF: 'PDF Export',
|
||||
exporting: 'Exportiere...',
|
||||
pleaseSelectTemplate: 'Bitte Vorlage auswählen'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Conventions:**
|
||||
- Nested objects for grouping related translations
|
||||
- camelCase for keys
|
||||
- Always add to both `de` and `en` files simultaneously
|
||||
- Keep structure identical between languages
|
||||
|
||||
## Async/Await Patterns
|
||||
|
||||
### Error Handling
|
||||
```js
|
||||
try {
|
||||
const response = await API.get('/api/endpoint')
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Operation failed:', error)
|
||||
throw error // or handle gracefully
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Parallel Requests
|
||||
```js
|
||||
const [characters, templates] = await Promise.all([
|
||||
API.get('/api/characters'),
|
||||
API.get('/api/pdf/templates')
|
||||
])
|
||||
```
|
||||
|
||||
## Browser APIs
|
||||
|
||||
### LocalStorage Usage
|
||||
```js
|
||||
// Save
|
||||
localStorage.setItem('token', response.data.token)
|
||||
localStorage.setItem('language', 'de')
|
||||
|
||||
// Retrieve
|
||||
const token = localStorage.getItem('token')
|
||||
const lang = localStorage.getItem('language') || 'de'
|
||||
|
||||
// Remove
|
||||
localStorage.removeItem('token')
|
||||
```
|
||||
|
||||
### Blob/File Handling
|
||||
```js
|
||||
// Create blob from response
|
||||
const blob = new Blob([response.data], { type: 'application/pdf' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
|
||||
// Open in new window
|
||||
const pdfWindow = window.open(url, '_blank')
|
||||
|
||||
// Clean up after use
|
||||
setTimeout(() => window.URL.revokeObjectURL(url), 10000)
|
||||
```
|
||||
|
||||
### URL Parameters
|
||||
```js
|
||||
// Build query string
|
||||
const params = new URLSearchParams({
|
||||
template: templateId,
|
||||
showUserName: 'true'
|
||||
})
|
||||
const url = `/api/export?${params.toString()}`
|
||||
|
||||
// Extract from params
|
||||
const queryParams = Object.fromEntries(params)
|
||||
```
|
||||
|
||||
## Event Handling
|
||||
|
||||
### Debouncing/Throttling
|
||||
For search inputs or resize events:
|
||||
|
||||
```js
|
||||
let debounceTimer
|
||||
function debounce(func, delay = 300) {
|
||||
return (...args) => {
|
||||
clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => func(...args), delay)
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const search = debounce(async (query) => {
|
||||
const results = await API.get(`/api/search?q=${query}`)
|
||||
}, 300)
|
||||
```
|
||||
|
||||
### Cleanup
|
||||
```js
|
||||
// Save timer ID for cleanup
|
||||
this.timer = setTimeout(() => { ... }, 5000)
|
||||
|
||||
// Clean up in component lifecycle
|
||||
beforeUnmount() {
|
||||
if (this.timer) clearTimeout(this.timer)
|
||||
}
|
||||
```
|
||||
|
||||
## Array/Object Operations
|
||||
|
||||
### Array Methods
|
||||
```js
|
||||
// Filter
|
||||
const learned = skills.filter(s => s.Fertigkeitswert > 0)
|
||||
|
||||
// Map
|
||||
const names = characters.map(c => c.name)
|
||||
|
||||
// Find
|
||||
const char = characters.find(c => c.id === 18)
|
||||
|
||||
// Some/Every
|
||||
const hasSkills = character.fertigkeiten.some(f => f.Fertigkeitswert > 0)
|
||||
```
|
||||
|
||||
### Object Destructuring
|
||||
```js
|
||||
// Response destructuring
|
||||
const { data, headers, status } = response
|
||||
|
||||
// Props destructuring
|
||||
const { character, template, showUserName = false } = options
|
||||
```
|
||||
|
||||
### Spread Operator
|
||||
```js
|
||||
// Merge objects
|
||||
const merged = { ...defaults, ...userOptions }
|
||||
|
||||
// Copy array
|
||||
const copy = [...originalArray]
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Loading State Management
|
||||
```js
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
data: null,
|
||||
error: null
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.loadData()
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.isLoading = true
|
||||
this.error = null
|
||||
try {
|
||||
const response = await API.get('/api/data')
|
||||
this.data = response.data
|
||||
} catch (error) {
|
||||
this.error = error.message
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Form Validation
|
||||
```js
|
||||
validateForm() {
|
||||
if (!this.selectedTemplate) {
|
||||
alert(this.$t('export.pleaseSelectTemplate'))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!this.validateForm()) return
|
||||
|
||||
// Proceed with submission
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use `const` by default**, `let` when reassignment needed, never `var`
|
||||
2. **Prefer arrow functions** for callbacks and short functions
|
||||
3. **Use template literals** for string interpolation
|
||||
4. **Handle promise rejections** with try/catch or .catch()
|
||||
5. **Clean up timers and intervals** in component lifecycle
|
||||
6. **Use optional chaining** `?.` for nested properties
|
||||
7. **Use nullish coalescing** `??` instead of `||` for default values
|
||||
8. **Keep functions small** and single-purpose
|
||||
9. **Document complex logic** with comments
|
||||
10. **Use meaningful variable names** - avoid single letters except loops
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
❌ Don't use `var` - use `const` or `let`
|
||||
❌ Don't ignore promise rejections
|
||||
❌ Don't mutate function parameters
|
||||
❌ Don't create memory leaks (clean up listeners, timers)
|
||||
❌ Don't use `eval()` or `new Function()`
|
||||
❌ Don't mix callbacks and promises
|
||||
❌ Don't forget to handle edge cases (null, undefined, empty arrays)
|
||||
❌ Don't use `==` - always use `===` for comparisons
|
||||
|
||||
## Environment Variables (Vite)
|
||||
|
||||
### Accessing Variables
|
||||
```js
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8180'
|
||||
const isDev = import.meta.env.DEV
|
||||
const isProd = import.meta.env.PROD
|
||||
```
|
||||
|
||||
**Convention**: Prefix all custom variables with `VITE_`
|
||||
@@ -0,0 +1,307 @@
|
||||
---
|
||||
description: 'Instructions for writing Vue 3 components following project conventions and best practices'
|
||||
applyTo: '**/*.vue'
|
||||
---
|
||||
|
||||
# Vue 3 Development Instructions
|
||||
|
||||
Follow Vue 3 best practices and project-specific conventions when writing components.
|
||||
|
||||
## Component Structure
|
||||
|
||||
### Standard Component Layout
|
||||
```vue
|
||||
<template>
|
||||
<!-- HTML template -->
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Component-specific styles */
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Component logic
|
||||
</script>
|
||||
```
|
||||
|
||||
**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() {
|
||||
// Method implementation
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## API Communication
|
||||
|
||||
### Using the API Utility
|
||||
Always use `API` from `utils/api.js` - it handles authentication automatically:
|
||||
|
||||
```js
|
||||
import API from '../utils/api'
|
||||
|
||||
// In methods:
|
||||
const response = await API.get(`/api/characters/${this.id}`)
|
||||
const data = await API.post('/api/characters', character)
|
||||
```
|
||||
|
||||
**Never** manually add Authorization headers - the interceptor handles this.
|
||||
|
||||
### 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)
|
||||
alert(this.$t('errors.loadFailed') + ': ' + (error.response?.data?.error || error.message))
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
### 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">
|
||||
```
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
### Popup Blocker Workaround
|
||||
Open windows **synchronously** before async operations:
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
**Critical**: `window.open()` must be called synchronously in the click handler, not after `await`.
|
||||
|
||||
## Common 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 style
|
||||
2. **Always use scoped styles** to avoid CSS conflicts
|
||||
3. **Name components with PascalCase** (e.g., `CharacterDetails.vue`)
|
||||
4. **Use meaningful prop names** and validate when possible
|
||||
5. **Handle errors gracefully** with user-friendly messages
|
||||
6. **Keep template logic simple** - move complex logic to methods
|
||||
7. **Clean up resources** in `beforeUnmount` if needed
|
||||
8. **Test with actual API** running in Docker container
|
||||
9. **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 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
|
||||
Reference in New Issue
Block a user