added more file type specific instructions

I hope that coding results will be better and more consistent
This commit is contained in:
2025-12-21 08:39:29 +01:00
parent c3d6af29e7
commit 2af477397e
10 changed files with 1457 additions and 44 deletions
+175 -14
View File
@@ -1,14 +1,175 @@
# general instructions # Bamort Development Instructions
- You are GitHub Copilot, an AI pair programmer.
- You are designed to help write code faster and with less effort. Bamort is a role-playing game character management system (MOAM replacement) with a Go backend and Vue.js frontend.
- You can generate code snippets, complete code, and suggest improvements.
- You can understand and write code in multiple programming languages. ## Project Overview
- You can help with debugging and fixing errors in code.
- You can assist with writing tests and documentation. - **Repository**: github.com:Bardioc26/bamort.git
- You can learn from the context of the code you are working on. - **Architecture**: Monorepo with separate backend/frontend in Docker containers
- You will NOT write example or demonstration code. - **Backend**: Go 1.25 + Gin framework + GORM + MariaDB
- The GitHub repository is github.com:Bardioc26/bamort.git. - **Frontend**: Vue 3 + Vite + i18n
- The main package is cmd/backend/main.go. - **Development**: Docker Compose with live-reload (Air for Go, Vite HMR for Vue)
- The main function is in cmd/backend/main.go.
- You write tests ONLY in _test.go files. ## Backend Architecture (`backend/`)
- You will NEVER create files for testing with a main() function!
### 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`
+484
View File
@@ -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;
}
```
+342
View File
@@ -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_`
+307
View File
@@ -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">&times;</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
-15
View File
@@ -1,15 +0,0 @@
# 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!
- If you want to ensure that the docker container is already runnung execute "docker ps" and find "bamort-backend-dev"
+8 -2
View File
@@ -65,7 +65,6 @@
- Fully tested and working end-to-end - Fully tested and working end-to-end
- **VERIFIED: 5 continuation pages generated for 50 skills, saved to /tmp/bamort_continuation_test/** - **VERIFIED: 5 continuation pages generated for 50 skills, saved to /tmp/bamort_continuation_test/**
## TODO (Remaining)
* ✅ 1. create API endpoint for listing available export templates * ✅ 1. create API endpoint for listing available export templates
* Endpoint: GET /api/pdf/templates * Endpoint: GET /api/pdf/templates
@@ -92,10 +91,17 @@
* ✅ Error handling with user-friendly alerts * ✅ Error handling with user-friendly alerts
* Status: ✅ Deployed with HMR, ready for testing * Status: ✅ Deployed with HMR, ready for testing
## TODO (Remaining)
* 1. create a directory xporttemp in the backend
* 2. save the PDF to file during the ExportCharacterToPDF call and return the filename charname+timestamp.pdf. Make shure the filename contains no spaces or special chars that might disturb the download
* 3. create an API endpoint to load the file from xporttemp
* 4. create a maintenance endpoint to clean up the xporttemp directory. remove all files that are older than 7 days
* 5. change the frontend to get the PDF from the new API endpoint
### Later ### Later
* continuation of lists does not work as expected but good enough for a first shot * continuation of lists does not work as expected but good enough for a first shot
* generalize handling so that only on set of functions can handle ALL kinds of templates. Needs massive refactoring * generalize handling so that only on set of functions can handle ALL kinds of templates. Needs massive refactoring
* currently the template fetched for rendering is set to Default_A4_Quer * currently the template fetched for rendering is set to Default_A4_Quer
* remove inline css as far as possible
* make pdf download popup an own view
+14
View File
@@ -5,6 +5,20 @@ FROM golang:1.25-alpine
# Install necessary packages for CGO and SQLite # Install necessary packages for CGO and SQLite
RUN apk add --no-cache gcc musl-dev sqlite-dev RUN apk add --no-cache gcc musl-dev sqlite-dev
# Install Chromium for PDF rendering
RUN apk add --no-cache \
chromium \
chromium-chromedriver \
nss \
freetype \
harfbuzz \
ca-certificates \
ttf-freefont
# Set Chrome path for chromedp
ENV CHROME_BIN=/usr/bin/chromium-browser \
CHROME_PATH=/usr/bin/chromium-browser
# Install Air für Live-Reloading (kompatible Version für Go 1.23) # Install Air für Live-Reloading (kompatible Version für Go 1.23)
#RUN go install github.com/cosmtrek/air@v1.49.0 #RUN go install github.com/cosmtrek/air@v1.49.0
RUN go install github.com/air-verse/air@latest RUN go install github.com/air-verse/air@latest
+118 -10
View File
@@ -18,9 +18,13 @@
<button @click="showExportDialog = false" class="close-button">&times;</button> <button @click="showExportDialog = false" class="close-button">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div v-if="isExporting" class="loading-overlay">
<div class="spinner"></div>
<p>{{ $t('export.generating') }}</p>
</div>
<div class="form-group"> <div class="form-group">
<label>{{ $t('export.selectTemplate') }}:</label> <label>{{ $t('export.selectTemplate') }}:</label>
<select v-model="selectedTemplate" class="template-select"> <select v-model="selectedTemplate" class="template-select" :disabled="isExporting">
<option value="">{{ $t('export.pleaseSelectTemplate') }}</option> <option value="">{{ $t('export.pleaseSelectTemplate') }}</option>
<option v-for="template in templates" :key="template.id" :value="template.id"> <option v-for="template in templates" :key="template.id" :value="template.id">
{{ template.name }} {{ template.name }}
@@ -29,13 +33,13 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="checkbox-label"> <label class="checkbox-label">
<input type="checkbox" v-model="showUserName"> <input type="checkbox" v-model="showUserName" :disabled="isExporting">
{{ $t('export.showUserName') }} {{ $t('export.showUserName') }}
</label> </label>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button @click="showExportDialog = false" class="btn-cancel"> <button @click="showExportDialog = false" class="btn-cancel" :disabled="isExporting">
{{ $t('export.cancel') }} {{ $t('export.cancel') }}
</button> </button>
<button @click="exportToPDF" class="btn-export" :disabled="!selectedTemplate || isExporting"> <button @click="exportToPDF" class="btn-export" :disabled="!selectedTemplate || isExporting">
@@ -170,6 +174,43 @@
.modal-body { .modal-body {
padding: 20px; padding: 20px;
position: relative;
}
.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;
border-radius: 0 0 8px 8px;
}
.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 p {
color: #007bff;
font-weight: 500;
margin: 0;
} }
.form-group { .form-group {
@@ -368,30 +409,97 @@ export default {
return return
} }
// Open window IMMEDIATELY (synchronously) to avoid popup blocker
const pdfWindow = window.open('', '_blank')
if (!pdfWindow) {
alert(this.$t('export.popupBlocked'))
return
}
// Show loading page in the new window
pdfWindow.document.write(`
<html>
<head>
<title>${this.$t('export.generating')}</title>
<style>
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-family: Arial, sans-serif;
background: #f5f5f5;
}
.loading-container {
text-align: center;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
h2 {
color: #333;
margin: 0 0 10px 0;
}
p {
color: #666;
margin: 0;
}
</style>
</head>
<body>
<div class="loading-container">
<div class="spinner"></div>
<h2>${this.$t('export.generating')}</h2>
<p>${this.$t('export.pleaseWait')}</p>
</div>
</body>
</html>
`)
this.isExporting = true this.isExporting = true
try { try {
const params = { template: this.selectedTemplate } // Build URL parameters
const params = new URLSearchParams({
template: this.selectedTemplate
})
if (this.showUserName) { if (this.showUserName) {
params.showUserName = true params.append('showUserName', 'true')
} }
// Fetch PDF from API
const response = await API.get(`/api/pdf/export/${this.id}`, { const response = await API.get(`/api/pdf/export/${this.id}`, {
params, params: Object.fromEntries(params),
responseType: 'blob' responseType: 'blob'
}) })
// Create blob URL and open in new tab // Create object URL from blob
const blob = new Blob([response.data], { type: 'application/pdf' }) const blob = new Blob([response.data], { type: 'application/pdf' })
const url = window.URL.createObjectURL(blob) const url = window.URL.createObjectURL(blob)
window.open(url, '_blank')
// Clean up blob URL after a delay // Replace loading page with PDF
setTimeout(() => window.URL.revokeObjectURL(url), 100) pdfWindow.location.href = url
// Clean up blob URL after some time
setTimeout(() => window.URL.revokeObjectURL(url), 10000)
// Close dialog on success // Close dialog on success
this.showExportDialog = false this.showExportDialog = false
} catch (error) { } catch (error) {
console.error('Failed to export PDF:', error) console.error('Failed to export PDF:', error)
pdfWindow.close()
alert(this.$t('export.exportFailed') + ': ' + (error.response?.data?.error || error.message)) alert(this.$t('export.exportFailed') + ': ' + (error.response?.data?.error || error.message))
} finally { } finally {
this.isExporting = false this.isExporting = false
+4 -1
View File
@@ -386,6 +386,9 @@ export default {
exportFailed: 'PDF Export fehlgeschlagen', exportFailed: 'PDF Export fehlgeschlagen',
showUserName: 'Benutzername anzeigen', showUserName: 'Benutzername anzeigen',
cancel: 'Abbrechen', cancel: 'Abbrechen',
export: 'Exportieren' export: 'Exportieren',
generating: 'PDF wird generiert...',
pleaseWait: 'Bitte warten, dies kann einen Moment dauern',
popupBlocked: 'Popup wurde blockiert. Bitte erlauben Sie Popups für diese Seite.'
} }
} }
+4 -1
View File
@@ -386,6 +386,9 @@ export default {
exportFailed: 'PDF export failed', exportFailed: 'PDF export failed',
showUserName: 'Show username', showUserName: 'Show username',
cancel: 'Cancel', cancel: 'Cancel',
export: 'Export' export: 'Export',
generating: 'Generating PDF...',
pleaseWait: 'Please wait, this may take a moment',
popupBlocked: 'Popup was blocked. Please allow popups for this site.'
} }
} }