--- 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 ``` **Order matters**: Template, Style, Script (as seen throughout the codebase) ### Options API Pattern (Primary) Use Options API for consistency with existing codebase: ```vue ``` ## 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 ``` ### 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 ``` **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 ``` ### Events (Child → Parent) ```vue ``` ## 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 ``` ### Disabling Form Elements During Loading ```vue ``` ## 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('...') // 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 ``` ### 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