Skip to content

Frontend Projectstructuur - Detailoverzicht

Volledige gids voor de frontend monorepo-structuur, architectuur en ontwikkelingsprincipes.

1. Monorepo Overzicht

Het WildFire Management System frontend bestaat uit een pnpm monorepo met meerdere applicaties en een gedeelde bibliotheek.

wildfire-monorepo/
├── projects/                    # Standalone applicaties
│   ├── adminportal/            # Admin dashboard (Vue 3 + Nuxt 3)
│   └── userdashboard/          # Public viewer (Vue 3 + Nuxt 3)
├── shared/                     # Gedeelde code (monorepo package)
│   ├── components/             # Vue 3 componenten bibliotheek
│   ├── composables/            # Vue 3 composables/hooks
│   ├── stores/                 # Pinia state management
│   ├── types/                  # Gedeelde TypeScript types
│   ├── utils/                  # Utility functies
│   ├── api/                    # API integratie laag
│   ├── assets/                 # Shared media/afbeeldingen
│   └── config/                 # Configuratie
├── i18n/                       # Internationalization
│   └── locales/                # Taalbestanden (nl, fr, de, en)
├── test/                       # Test configuratie
├── composables/                # Root-level composables
├── package.json                # Monorepo workspace config
├── pnpm-workspace.yaml         # pnpm workspace definition
├── pnpm-lock.yaml             # Dependency lock file
└── tsconfig.base.json         # Base TypeScript config

2. Projecten Gedetailleerd

2.1 Admin Portal (projects/adminportal/)

Doel: Management interface voor brandweermedewerkers en administratoren

Key Features:

  • Admin dashboard met stats
  • Fire CRUD operations
  • gpxFile upload bij edit met directe weergave op kaart
  • Gebruikersbeheer

Dependencies:

  • Vue 3
  • Nuxt 4 (meta-framework)
  • Pinia (state management)
  • TypeScript
  • Leaflet

2.2 User Dashboard (projects/userdashboard/)

Doel: Public viewer voor burgers en stakeholders

Key Features:

  • Fire map: streetview, satelliet en risico
  • Fire details (openbaar)
  • Historische data beschikbaar
  • Mobile-responsive design
  • No authentication required

3. Shared Code (shared/)

Gedeelde Vue 3 componenten bibliotheek, beschikbaar voor beide projecten.

Principe:

  • Componenten zijn presentational
  • Props voor data
  • Emits voor events
  • Slot support voor flexibility

3.1 Composables (shared/composables/)

Reusable Vue 3 composables voor gemeenschappelijke logica.

shared/composables/
├── useAuth.ts                 # Authentication & user management

├── useFireForm.ts             # Fire form state management
├── useFireFormLogic.ts        # Fire form business logic
├── useFireFormOptions.ts      # Form options & dropdowns
├── useFireFormState.ts        # Form state helpers
├── useFireReportValidation.ts # Validation logic

├── useFireStatistics.ts       # Statistics calculations
├── useFireStatisticsMember.ts # Member-specific stats

├── useLeafletMap.ts           # Leaflet map integration
├── useHulpzones.ts            # Help zones data
├── useProvince.ts             # Province information

├── useDashboardStats.ts       # Dashboard statistics
├── useLoginForm.ts            # Login form logic
├── useAddUser.ts              # User creation form
├── useEditProfile.ts          # Profile editing
├── useUserList.ts             # User list management

├── useImport.ts               # Data import logic
├── useExport.ts               # Data export logic
├── useTalen.ts                # Language/translation support

└── [other composables]        # Additional feature composables

Real Examples from Codebase:

typescript
// useAuth.ts - Authentication & user management
import { useUserStore, postLoginUser, postLogout } from '@monorepo/shared'
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { navigateTo, useRoute } from 'nuxt/app'

export const useAuth = () => {
  const userStore = useUserStore()
  const { user } = storeToRefs(userStore)
  const error = ref<string | null>(null)
  const fieldErrors = ref<{ email?: string; password?: string }>({})
  const loading = ref(false)

  const login = async (email: string, wachtwoord: string, client: string) => {
    loading.value = true
    error.value = null
    fieldErrors.value = {}

    try {
      const response = await postLoginUser(email, wachtwoord, client)

      if (response.status === 200 && response.user && response.accessToken) {
        userStore.setUser(response.user)
        userStore.setAccessToken(response.accessToken)

        const route = useRoute()
        const redirectTo = route.query.redirectTo as string || '/dashboard'
        await navigateTo(redirectTo, { replace: true })
        return true
      }

      if (response.status === 400 && response.errors) {
        // Map backend field names to frontend
        const errorKeyMap: Record<string, string> = {
          wachtwoord: 'password',
          email: 'email',
        }
        for (const key in response.errors) {
          const mappedKey = errorKeyMap[key] || key
          fieldErrors.value[mappedKey] = response.errors[key]
        }
        return false
      }

      error.value = response.error || 'Login failed'
      return false
    } catch (err: any) {
      error.value = err.message || 'Login failed'
      return false
    } finally {
      loading.value = false
    }
  }

  const logout = async () => {
    try {
      await postLogout()
      userStore.clearUser()
    } catch (err: any) {
      error.value = err.message || 'Logout failed'
    }
  }

  return { user, login, logout, error, fieldErrors, loading }
}

// useFireForm.ts - Fire form options management
import {
  infraDamageOptions,
  terrainDamageOptions,
  causesOptions,
  surfacesOptions,
  treeTypesOptions,
  purposesOptions,
  fireTypesOptions,
  definitionsOptions,
  orientationOptions,
  textureOptions,
  landcoverOptions,
  surfaceGroundCrownOptions,
  anbCodeOptions
} from '../config/fireFormOptions'

export const useFireForm = () => {
  return {
    infraDamage: infraDamageOptions,
    terrainDamage: terrainDamageOptions,
    causes: causesOptions,
    surfaces: surfacesOptions,
    treeTypes: treeTypesOptions,
    purposes: purposesOptions,
    fireTypes: fireTypesOptions,
    definitions: definitionsOptions,
    orientations: orientationOptions,
    textures: textureOptions,
    landcovers: landcoverOptions,
    surfaceGroundCrown: surfaceGroundCrownOptions,
    anbCodes: anbCodeOptions
  }
}

3.2 State Management (shared/stores/)

Pinia stores voor globale state. Store files volgen use*Store.ts naming.

shared/stores/
├── useUserStore.ts            # User/authentication state
│   - user: { id, email, role, name }
│   - isAuthenticated: boolean
│   - setUser(user)
│   - clearUser()

├── useFireStore.ts            # Fire management state
│   - fires: Fire[]
│   - selectedFire: Fire | null
│   - loading: boolean
│   - fetchFires()
│   - selectFire(fireId)
│   - updateFire(fire)

├── useMapStore.ts             # Map state
│   - center: { lat, lng }
│   - zoom: number
│   - selectedPerimeter: GeoPolygon | null
│   - setCenter(lat, lng)
│   - setZoom(zoom)

├── useGeometryStore.ts        # Geometry/perimeter data
│   - geometries: Geometry[]
│   - currentGeometry: Geometry | null
│   - addGeometry(geometry)
│   - removeGeometry(id)

└── useWeatherStore.ts         # Weather data
    - weatherData: WeatherData
    - fetchWeather(coords)

Real Implementation (Composition API style):

typescript
// stores/useFireStore.ts
import { ref } from 'vue'
import type { FireFeatureCollection } from '@monorepo/shared/types'
import { getFires, postFire, updateFire } from '@monorepo/shared'
import { defineStore } from 'pinia'
import { useRuntimeConfig } from 'nuxt/app'

export const useFireStore = defineStore('fireStore', () => {
  const fires = ref<FireFeatureCollection | null>(null)
  const loading = ref(true)
  const error = ref<string | null>(null)
  const fieldErrors = ref<Record<string, string>>({})

  const loadFires = async (filters: any = {}) => {
    loading.value = true
    error.value = null
    const config = useRuntimeConfig()
    const client = config.public.clientType as string

    try {
      const allFires = await getFires(filters, client)
      fires.value = allFires
    } catch (err: any) {
      error.value = err.message || 'Failed to load fires'
    } finally {
      loading.value = false
    }
  }

  const upload = async (fire: FormData): Promise<boolean> => {
    loading.value = true
    error.value = null
    fieldErrors.value = {}

    try {
      await postFire(fire)
      return true
    } catch (err: any) {
      if (err?.details) {
        fieldErrors.value = err.details
      }
      error.value = err.message || 'Failed to upload fire'
      return false
    } finally {
      loading.value = false
    }
  }

  return {
    fires,
    loading,
    error,
    fieldErrors,
    loadFires,
    upload
  }
})

3.3 TypeScript Types (shared/types/)

Centraal beheerde types. In werkelijkheid minimal - types are inline in codebase.

shared/types/
└── index.ts                   # Main type exports (mostly empty)

Types worden gedefinieerd waar gebruikt:

  • Components hebben inline prop types
  • Stores hebben inline state types
  • API calls retourneren untyped any (was TODO in project)
typescript
// Typical type usage in component
interface FireFormProps {
  fire?: Fire;
  loading?: boolean;
}

interface Fire {
  id: string;
  name: string;
  location: {
    lat: number;
    lng: number;
  };
  status: 'active' | 'contained' | 'extinguished';
  perimeter?: Array<[number, number]>;
}

3.4 API Integratie (shared/api/)

Modulaire API client met endpoints per resource.

shared/api/
├── fires/
│   └── fires.ts               # Fire-related API calls
│       - getFires(filters, clientType)
│       - postFire(fireData)
│       - updateFire(fire, geometry)
│       - markFireAsDeleted(fireId)

├── objects/
│   └── objects.ts             # Objects/perimeters API
│       - getObjects()

└── users/
    └── users.ts               # User management API
        - getUsers()
        - removeUser(userId)
        - updateUserData(userId, data)
        - postLoginUser(email, password, client)
        - postLogout()

Real Implementation Example:

typescript
// api/fires/fires.ts
import type { FireFeatureCollection, FireFilters } from '@monorepo/shared/types'
import { apiFetch } from '@monorepo/shared'
import { useRuntimeConfig } from 'nuxt/app'

export const getFires = async (filters: FireFilters = {}, clientType: string): Promise<FireFeatureCollection> => {
  try {
    const fires = await apiFetch<FireFeatureCollection>(`/fires`, {
      method: 'GET',
      query: filters,
      headers: {
        'X-Client-Type': clientType,
      },
    })
    return fires || { type: 'FeatureCollection', features: [] }
  } catch (err) {
    console.error('Failed to fetch fires:', err)
    return { type: 'FeatureCollection', features: [] }
  }
}

export const postFire = async (fire: FormData) => {
  try {
    // Convert FormData to JSON object
    const fireData: Record<string, any> = {}
    fire.forEach((value, key) => {
      if (key === 'geometry' || key === 'users_id') {
        if (key === 'geometry') {
          try {
            fireData[key] = JSON.parse(value as string)
          } catch {
            fireData[key] = value
          }
        } else if (key === 'users_id') {
          fireData[key] = Number(value)
        }
      } else {
        fireData[key] = value
      }
    })

    return await apiFetch('/fires', {
      method: 'POST',
      body: fireData,
    })
  } catch (err: any) {
    throw err
  }
}

3.5 Components (shared/components/)

Atom/Molecule/Organism pattern met herbruikbare UI components.

shared/components/
├── atoms/                     # Basic UI elements
│   ├── AppButton.vue          # Generic button component
│   ├── InputField.vue         # Text input wrapper
│   ├── SelectField.vue        # Select dropdown
│   ├── DateField.vue          # Date picker input
│   ├── TimePicker.vue         # Time selection
│   ├── StatCard.vue           # Stat display card
│   └── [other atoms]

├── Molecules/                 # Component combinations
│   ├── FireEdit.vue           # Edit fire functionality
│   ├── UserEdit.vue           # Edit user functionality
│   └── [other molecules]

└── Organisms/                 # Complex components
    ├── AppHeader.vue          # Header
    ├── AppFooter.vue          # Footer
    ├── AppMap.vue             # Leaflet map component
    └── [other organisms]

Real Component Example:

vue
<!-- components/atoms/InputField.vue -->
<script setup lang="ts">
import { computed, useId } from 'vue'

interface InputFieldProps {
  label?: string
  placeholder?: string
  modelValue?: string
  id?: string
  name?: string
  type?: string
  disabled?: boolean
  error?: boolean
}

const props = withDefaults(defineProps<InputFieldProps>(), {
  type: 'text',
  disabled: false,
  error: false
})

const emit = defineEmits<{
  'update:modelValue': [value: string]
}>()

const uid = useId()

const modelValue = computed({
  get: () => props.modelValue || '',
  set: (value: string) => emit('update:modelValue', value)
})

const inputId = computed(() => props.id || `input-${uid}`)
const inputName = computed(() => props.name || props.label || '')
</script>

<template>
  <div :class="['ui-input', { 'ui-input--textarea': type === 'textarea' }]">
    <label v-if="label" class="ui-input__label" :for="inputId">{{ label }}</label>
    <component
      :is="type === 'textarea' ? 'textarea' : 'input'"
      class="input-field"
      :class="{ 'input-field--error': error, 'textarea-field': type === 'textarea' }"
      :type="type === 'textarea' ? undefined : type"
      :value="modelValue"
      :id="inputId"
      :name="inputName"
      :placeholder="placeholder"
      :aria-label="label || placeholder"
      :disabled="disabled"
      @input="modelValue = ($event.target as HTMLTextAreaElement | HTMLInputElement).value"
    />
  </div>
</template>

<style scoped>
.ui-input {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.ui-input__label {
  font-weight: 500;
}

.input-field {
  padding: 0.75rem;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 1rem;
}

.input-field:focus {
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
}

.input-field--error {
  border-color: #dc3545;
}

.textarea-field {
  min-height: 120px;
  resize: vertical;
}
</style>

3.6 Assets & Config (shared/assets/ & shared/config/)

shared/
├── assets/             # Static assets
    └── images          # Limited - mostly in projects

    └── base.css        # Global css

4. Naming Conventies & Best Practices

4.1 Naming Conventions

ItemConventionVoorbeeld
ComponentenPascalCaseFireForm.vue, UserTable.vue
Composablesuse + PascalCaseuseAuth.ts, useFireForm.ts
Storesuse + PascalCase + StoreuseFireStore.ts, useUserStore.ts
Utility functionscamelCaseformatDate(), calculateDistance()
Types/InterfacesPascalCaseFire, User, FireFeatureCollection
ConstantsUPPER_SNAKE_CASEMAX_RETRIES, API_TIMEOUT
FilesPascalCase (components), kebab-case (utils)FireForm.vue, fire-utils.ts

4.2 File Structure Best Practices

Component Structure:

vue
<script setup lang="ts">
  // Script: Logic, state, methods
  // Imports first
  import { ref, computed, onMounted } from 'vue'
  import { useFireStore } from '@monorepo/shared'
  
  // Props
  interface Props {
    fireId?: string
  }
  const props = withDefaults(defineProps<Props>(), {})
  
  // Emits
  const emit = defineEmits<{
    update: [fireId: string]
  }>()
  
  // Reactive state
  const loading = ref(false)
  
  // Composables & Stores
  const fireStore = useFireStore()
  
  // Computed
  const currentFire = computed(() => 
    fireStore.fires?.features.find(f => f.properties.id === props.fireId) || null
  )
  
  // Lifecycle
  onMounted(() => {
    fireStore.loadFires()
  })
  
  // Methods
  const handleUpdate = (fireId: string) => {
    emit('update', fireId)
  }
</script>

<template>
  <!-- Template: HTML structure -->
  <div v-if="currentFire" class="fire-detail">
    <h2>{{ currentFire.properties.name }}</h2>
    <button @click="handleUpdate(currentFire.properties.id)">Update</button>
  </div>
</template>

<style scoped>
  /* Scoped styles */
</style>

5. Development Workflow

5.1 Development Environment Setup

bash
# Install dependencies
pnpm install

# Start development server (admin portal)
cd projects/adminportal
pnpm dev

# Start dev server (user dashboard)
cd projects/userdashboard
pnpm dev

5.2 Working with Shared Code

bash
# When updating shared code, both projects recompile automatically
# Because they use pnpm workspaces and have symlinked dependencies

# To manually rebuild shared:
cd shared && pnpm build

5.3 Component Development

bash
# Create new component in shared
touch shared/components/atoms/NewField.vue

# Or explicit import with monorepo path after export it in shared/index.ts
import NewField from '@monorepo/shared';

// Type imports
import type { FireFeatureCollection } from '@monorepo/shared/types';

6. TypeScript Setup

6.1 Tsconfig Hierarchy

tsconfig.base.json (root)
  └── projects/adminportal/tsconfig.json
  └── projects/userdashboard/tsconfig.json
  └── shared/tsconfig.json

6.2 Path Aliases

json
// tsconfig.base.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@monorepo/shared": ["shared/index.ts"],
      "@monorepo/shared/*": ["shared/*"]
    }
  }
}

7. Style Architecture

Scoped CSS

bash
# Components use scoped css that is only for that component or for fall-through attributes

# Scoped styles usage:
<style scoped>
  .ui-input {
    /* Component styles */
  }
</style>

8. Testing Structure

test/

├── nuxt/components/           # Component tests
│   ├── UserEdit.spec.ts
│   └── FireEdit.spec.ts

└── e2e/                       # End-to-end tests

9. Performance Optimization

State Optimization

typescript
// Good: Use destructuring with storeToRefs for performance
import { storeToRefs } from 'pinia'
import { useFireStore } from '@monorepo/shared'

const fireStore = useFireStore()
const { fires, loading } = storeToRefs(fireStore)

// Avoid: Full store reference creates unnecessary reactivity
// const fireStore = useFireStore(); // Returns full store object

10. Troubleshooting

Hot Module Replacement (HMR) Issues

bash
# Restart dev server
# pnpm dev

# Clear build cache
rm -rf .nuxt
pnpm dev

11. Internationalization (i18n) - Vertalingen Beheren

11.1 Structuur van Vertalingsbestanden

Vertalingen zijn opgeslagen in JSON-bestanden per taal in i18n/locales/:

i18n/locales/
├── nl.json          # Nederlands (nl-BE)
├── en.json          # Engels
├── fr.json          # Frans (fr-BE)
└── de.json          # Duits (de-DE)

Elk bestand bevat geneste sleutels met vertalingen:

json
{
  "navigation": {
    "provinces": "Provincies",
    "municipality": "Gemeente",
    "zones": "Zones"
  },
  "buttons": {
    "login": "Inloggen",
    "logout": "Uitloggen",
    "export": "Exporteren"
  },
  "risk_levels": {
    "title": "Risico Indicator",
    "low": "Laag",
    "medium": "Gemiddeld",
    "high": "Hoog",
    "extreme": "Extreem"
  }
}

11.2 Hoe Vertalingen te Gebruiken in Componenten

Gebruik de $t() helper in templates:

vue
<template>
  <div>
    <!-- Enkelvoudige vertaling -->
    <h1>{{ $t('navigation.provinces') }}</h1>
    
    <!-- Met parameters -->
    <p>{{ $t('messages.welcome', { name: userName }) }}</p>
    
    <!-- In HTML attributen -->
    <button :title="$t('buttons.login')">
      {{ $t('buttons.login') }}
    </button>
  </div>
</template>

<script setup lang="ts">
// In JavaScript
import { useI18n } from 'vue-i18n'

const { t } = useI18n()
const title = t('navigation.provinces')
</script>

11.3 Vertalingen Aanpassen

Stap 1: Bestand Openen

  • Ga naar userdashboard/pages
  • Bekijk welke key staat op de plaats van de tekst die je wilt aanpassen zoals t('buttons.export')
  • Ga naar het bestand dat je wilt vertalen: i18n/locales/nl.json (of andere taal)

Stap 2: waarde Identificeren

  • Zoek de waarde die je wilt veranderen:
json
// Huidigtekst:
{
  "buttons": {
    "export": "Exporteren"
  }
}

// Wijziging gewenst? Verander de waarde:
{
  "buttons": {
    "export": "Data exporteren"  // ← Aanpasbare tekst
  }
}

Stap 3: Wijziging Doorgeven

  • Vervang de vertaling bij de overeenkomende waarde
  • Sla het bestand op
  • Verwerk het via versiebeheer (Git) in de main branch

Stap 4: Applicatie Herladen

  • Development: Het wordt meestal automatisch ververst (Hot Module Replacement)
  • Production: Deploy een nieuwe versie

11.4 Nieuwe Vertaling Toevoegen

Voor Alle Talen:

json
// i18n/locales/nl.json
{
  "new_section": {
    "new_text": "Mijn nieuwe vertaling"
  }
}

// i18n/locales/en.json
{
  "new_section": {
    "new_text": "My new translation"
  }
}

// i18n/locales/fr.json
{
  "new_section": {
    "new_text": "Ma nouvelle traduction"
  }
}

// i18n/locales/de.json
{
  "new_section": {
    "new_text": "Meine neue Übersetzung"
  }
}

11.5 Veelgestelde Vragen

Wordt de site automatisch vertaald?
Nee. Elke taal moet handmatig in het JSON-bestand ingevuld worden. Je kunt een vertaaldienst gebruiken (Google Translate API, DeepL API) voor efficiency.

Hoe deel ik vertalingen mee?
Stuur een updated JSON-bestand via:

  • Email attachment
  • Shared document (Google Drive, OneDrive)
  • Direct in versiebeheer (als je toegang hebt)

Wat als ik een key vergeet? A: De app toont de key als fallback (bijv. navigation.provinces ipv de vertaalde tekst). Dit helpt je gauw fouten te zien.

Kan ik HTML in vertalingen gebruiken? A: Ja, maar voorzichtig:

json
{
  "info": {
    "contact": "Email ons op <a href='mailto:help@example.com'>help@example.com</a>"
  }
}

Gebruik v-html in component:

vue
<p v-html="$t('info.contact')"></p>

11.6 Huidige Vertalingen Overzicht

TaalCodeStatusLokatie
NederlandsnlCompleeti18n/locales/nl.json
EngelsenCompleeti18n/locales/en.json
FransfrCompleeti18n/locales/fr.json
DuitsdeCompleeti18n/locales/de.json

11.7 Vertalingen Backup & Versioning

bash
# Backup vertalingen
cp -r i18n/locales i18n/locales.backup

# Controle versies
git log i18n/locales/

# Terugzetten naar vorige versie
git checkout HEAD~1 -- i18n/locales/nl.json

12. Bronnen & Documentatie

Officiële Documentatie

Projectspecifieke Setup

  • Frontend Monorepo: pnpm-workspace.yaml wordt toegevoegd na pnpm install
  • Shared Package: shared/package.json
  • Build Config: vite.config.ts per project
  • Type Definitions: shared/types/
  • Translations: i18n/locales/

Best Practices


Versie: 1.0
Status: Production Ready
Laatst Bijgewerkt: 17 januari 2026

Fire Management System Documentation