Files
Bardioc26 8c7aa86181 cleanup unused or orphaned files (#9)
* 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
2026-01-01 14:01:15 +01:00

8.5 KiB
Raw Permalink Blame History

description, applyTo
description applyTo
Instructions for writing Vue 3 components following project conventions and best practices **/*.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

<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:

<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:

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:

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:

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

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

<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:

// 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

<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)

<script>
export default {
  props: {
    character: Object,
    id: [String, Number]
  }
}
</script>

Events (Child → Parent)

<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.):

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:

<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

<select v-model="selected" :disabled="isLoading">
<input type="checkbox" v-model="option" :disabled="isLoading">

Form Validation

Validation Pattern

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:

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

<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