8c7aa86181
* changed vue.instructions to be used for *.vue, *.ts, *.js, *.scss * remove unneeded files or files that may conflict with some peoples' protection meanings * ToDos as the popped up during cleaning
399 lines
8.5 KiB
Markdown
399 lines
8.5 KiB
Markdown
---
|
||
description: 'Instructions for writing Vue 3 components following project conventions and best practices'
|
||
applyTo: '**/*.vue, **/*.ts, **/*.js, **/*.scss'
|
||
---
|
||
|
||
# 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() {
|
||
// Use const/let, never var
|
||
const response = await API.get('/api/endpoint')
|
||
this.items = response.data
|
||
}
|
||
}
|
||
}
|
||
</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.
|
||
|
||
### Environment Variables
|
||
API base URL from Vite:
|
||
```js
|
||
import API from '../utils/api'
|
||
// API uses: import.meta.env.VITE_API_URL || 'http://localhost:8180'
|
||
```
|
||
|
||
### API Configuration (`utils/api.js`)
|
||
Standard Axios instance with interceptors:
|
||
```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')
|
||
}
|
||
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
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 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 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
|