Dengue Dashboard Frontend
A Vue 3 single-page application for visualizing and managing dengue epidemiological data, built with TypeScript and following modern frontend best practices.
Table of Contents
- Technology Stack
- PWA Setup
- Architecture Overview
- Project Structure
- Coding Standards
- Developer Guide
- Testing
- IDE Setup
Technology Stack
Core Framework
| Technology | Purpose | Why? |
|---|---|---|
| Vue 3 | UI Framework | Composition API provides better TypeScript support, code reusability through composables, and improved performance |
| TypeScript | Type Safety | Catches errors at compile time, better IDE support, self-documenting code |
| Vite | Build Tool | Lightning-fast HMR, native ES modules, optimized production builds |
| pnpm | Package Manager | Faster installs, disk space efficient, strict dependency resolution |
State Management & Data Fetching
| Technology | Purpose | Why? |
|---|---|---|
| Pinia | State Management | Official Vue store, excellent TypeScript support, devtools integration |
| Pinia Colada | Async State/Caching | Declarative queries with built-in caching, deduplication, and stale-while-revalidate |
| Axios | HTTP Client | Interceptors for auth, request/response transformation, better error handling |
UI & Visualization
| Technology | Purpose | Why? |
|---|---|---|
| Naive UI | Component Library | Comprehensive Vue 3 components, TypeScript support, customizable themes |
| ApexCharts | Charts & Graphs | Interactive charts, responsive, extensive chart types |
| AG Grid | Data Tables | High-performance grid, sorting, filtering, accessibility |
| Leaflet | Maps | Lightweight, mobile-friendly, extensive plugin ecosystem |
| ECharts | Advanced Visualizations | Complex data visualizations when needed |
Internationalization & Accessibility
| Technology | Purpose | Why? |
|---|---|---|
| Vue I18n | Translations | Runtime language switching, pluralization, date/number formatting |
| @vueuse/head | SEO/Meta | Dynamic document head management for accessibility and SEO |
Testing
| Technology | Purpose | Why? |
|---|---|---|
| Vitest | Unit Tests | Native Vite integration, Jest-compatible API, fast execution |
| Cypress | E2E Tests | Real browser testing, time-travel debugging, network stubbing |
| cypress-axe | A11y Testing | Automated accessibility audits during E2E tests |
Code Quality
| Technology | Purpose | Why? |
|---|---|---|
| ESLint | Linting | Catches code issues, enforces consistent style |
| Prettier | Formatting | Consistent code formatting across the team |
| vue-tsc | Type Checking | Full TypeScript checking for .vue files |
PWA Setup
The application is configured as a Progressive Web App (PWA) using vite-plugin-pwa, enabling offline functionality, installability, and improved performance through intelligent caching.
Configuration
The PWA is configured in vite.config.ts with the following key features:
| Feature | Value | Description |
|---|---|---|
| Register Type | autoUpdate | Service worker updates automatically without user prompt |
| Display Mode | standalone | App runs in its own window without browser UI |
| Theme Color | #1d73d8 | Blue theme matching the dashboard design |
| Start URL | / | App opens at the root path when launched |
Caching Strategy
The service worker implements different caching strategies based on resource type:
| Resource Type | Strategy | Cache Duration | Purpose |
|---|---|---|---|
API Calls (/api/*) | NetworkFirst | 7 days | Fresh data preferred, falls back to cache when offline |
| Pages (HTML) | NetworkFirst | 30 days | Always try to get latest version, cache for offline |
| Assets (JS, CSS) | StaleWhileRevalidate | 30 days | Serve cached immediately, update in background |
| Media (images, fonts) | CacheFirst | 1 year | Static assets rarely change, prioritize cache |
Caching Strategies Explained
- NetworkFirst: Tries network first, falls back to cache if offline. Best for dynamic content like API responses.
- StaleWhileRevalidate: Serves cached content immediately while fetching updates in the background. Best for assets that update occasionally.
- CacheFirst: Serves from cache if available, only fetches from network if not cached. Best for static assets.
Precached Assets
The following assets are automatically cached during service worker installation:
favicon.icorobots.txtimg/icons/logo.png- All
*.js,*.css,*.html,*.ico,*.png,*.svg,*.woff,*.woff2files
Web App Manifest
The manifest (manifest.webmanifest) is auto-generated with:
{
"name": "Dengue Dashboard",
"short_name": "Dengue",
"description": "Dashboard for Dengue surveillance and monitoring",
"theme_color": "#1d73d8",
"background_color": "#ffffff",
"display": "standalone",
"start_url": "/",
"icons": [
{ "src": "/img/icons/logo.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/img/icons/logo.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
]
}Service Worker Registration
The service worker is registered in src/registerServiceWorker.ts and provides lifecycle hooks:
| Event | Description |
|---|---|
ready | App is being served from cache |
registered | Service worker successfully registered |
cached | Content cached for offline use |
updatefound | New content is being downloaded |
updated | New content available (refresh needed) |
offline | No internet connection, running offline |
Development vs Production
| Environment | Behavior |
|---|---|
| Development | Service worker enabled with type: 'module' for easier debugging |
| Production | Full PWA functionality with optimized caching |
Testing PWA Locally
- Run the production build:
pnpm build - Preview the build:
pnpm preview - Open Chrome DevTools → Application tab → Service Workers
- Test offline mode using the "Offline" checkbox in DevTools
Adding New Cached Routes
To add caching for a new API pattern, add a new entry to the runtimeCaching array in vite.config.ts:
{
urlPattern: /^https:\/\/your-pattern\.*/i,
handler: 'NetworkFirst', // or 'CacheFirst', 'StaleWhileRevalidate'
options: {
cacheName: 'your-cache-name',
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 7, // 7 days
},
cacheableResponse: {
statuses: [0, 200],
},
},
}Architecture Overview
The application follows a layered architecture pattern that separates concerns:
┌─────────────────────────────────────────────────────────────┐
│ Views │
│ (Pages that compose components) │
├─────────────────────────────────────────────────────────────┤
│ Components │
│ (Atoms → Molecules → Organisms pattern) │
├─────────────────────────────────────────────────────────────┤
│ Composables │
│ (Reusable reactive logic & business rules) │
├──────────────────────┬──────────────────────────────────────┤
│ Queries │ Stores │
│ (Server state) │ (Client state) │
├──────────────────────┴──────────────────────────────────────┤
│ API │
│ (HTTP calls to backend) │
├─────────────────────────────────────────────────────────────┤
│ Instances │
│ (Configured libraries: axios, i18n) │
└─────────────────────────────────────────────────────────────┘Data Flow
- API Layer → Raw HTTP calls to the backend
- Queries Layer → Wraps API calls with Pinia Colada for caching/state management
- Composables → Business logic that combines queries and provides computed data
- Components → Consume composables and render UI
- Stores → Client-side state (auth, user preferences, UI state)
Project Structure
src/
├── api/ # API functions (HTTP calls only)
│ ├── infection.ts # Infection-related endpoints
│ ├── demography.ts # Demography endpoints
│ └── ...
│
├── queries/ # Pinia Colada query definitions
│ ├── infections.ts # Caching/state for infection data
│ └── ...
│
├── composables/ # Reusable composition functions
│ ├── useInfections.ts # Combines infection queries
│ ├── useMap.ts # Map-related logic
│ ├── charts/ # Chart configuration composables
│ └── ...
│
├── stores/ # Pinia stores (client state)
│ ├── authStore.ts # Authentication state
│ ├── userStore.ts # User preferences
│ └── ...
│
├── components/ # Vue components (Atomic Design)
│ ├── atoms/ # Basic building blocks
│ │ ├── SelectLocation.vue
│ │ ├── FileInput.vue
│ │ └── ...
│ ├── molecules/ # Combinations of atoms
│ │ ├── ChartComponent.vue
│ │ ├── FileUpload.vue
│ │ └── ...
│ └── organisms/ # Complex UI sections
│ ├── DashboardHeader.vue
│ ├── DashboardMap.vue
│ └── ...
│
├── views/ # Page-level components
│ ├── HomeView.vue
│ ├── MapView.vue
│ ├── admin/ # Admin-specific pages
│ ├── data_engineer/ # Data engineer pages
│ └── error/ # Error pages
│
├── types/ # TypeScript type definitions
│ ├── infection.ts
│ ├── users.ts
│ └── ...
│
├── services/ # Business logic services
│ ├── authService.ts
│ ├── userService.ts
│ └── ...
│
├── instances/ # Configured library instances
│ ├── myAxios.ts # Axios with interceptors
│ ├── i18n.ts # Vue I18n configuration
│ └── myFetch.ts # Fetch wrapper
│
├── router/ # Vue Router configuration
│ └── index.ts
│
├── locales/ # Translation files
│ ├── en.json
│ └── es.json
│
├── constants/ # Application constants
├── enums/ # TypeScript enums
├── helper/ # Utility functions
├── assets/ # Static assets (CSS, images)
└── __tests__/ # Unit tests
├── components/
└── composables/Coding Standards
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Components | PascalCase | ChartComponent.vue |
| Composables | camelCase with use prefix | useInfections.ts |
| Stores | camelCase with Store suffix | authStore.ts |
| Types/Interfaces | PascalCase (no I prefix) | Infection, User |
| API functions | camelCase, verb-first | getInfections(), createUser() |
| Constants | SCREAMING_SNAKE_CASE | API_BASE_URL |
TypeScript Rules
- No
Iprefix for interfaces (enforced by ESLint) - Unused variables must be prefixed with
_ - Strict mode enabled - no implicit
any
Code Formatting (Prettier)
{
singleQuote: true,
semi: false,
tabWidth: 4,
useTabs: true,
vueIndentScriptAndStyle: true
}Component Structure
Follow this order in .vue files:
<script setup lang="ts">
// 1. Imports
// 2. Props/Emits definitions
// 3. Composables & stores
// 4. Refs & reactive state
// 5. Computed properties
// 6. Functions
// 7. Lifecycle hooks
</script>
<template>
<!-- Template content -->
</template>
<style scoped lang="scss">
/* Scoped styles */
</style>Developer Guide
Adding a New Feature
Follow these steps when implementing a new feature:
- Define types in
src/types/for any new data structures - Create API functions in
src/api/for backend communication - Add queries in
src/queries/if data needs caching - Create composables in
src/composables/for business logic - Build components following Atomic Design (atoms → molecules → organisms)
- Create/update views to compose your components
- Add translations in
src/locales/en.jsonandsrc/locales/es.json - Write tests in
src/__tests__/
Adding a New Component
1. Determine the component level:
- Atom: Single-purpose, no dependencies on other components (buttons, inputs, selects)
- Molecule: Combines atoms into a functional unit (form groups, cards)
- Organism: Complex sections with business logic (headers, data tables, maps)
2. Create the component file:
<!-- src/components/molecules/MyComponent.vue -->
<script setup lang="ts">
// Define props with defaults
interface Props {
title: string
data: SomeType[]
loading?: boolean
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
})
// Define emits
const emit = defineEmits<{
select: [item: SomeType]
close: []
}>()
</script>
<template>
<div class="my-component">
<!-- Use semantic HTML -->
<!-- Include ARIA attributes for accessibility -->
</div>
</template>
<style scoped lang="scss">
.my-component {
// Component styles
}
</style>3. Add unit tests:
// src/__tests__/components/molecules/MyComponent.spec.ts
import { mount } from '@vue/test-utils'
import MyComponent from '@/components/molecules/MyComponent.vue'
describe('MyComponent', () => {
it('renders title correctly', () => {
const wrapper = mount(MyComponent, {
props: { title: 'Test', data: [] }
})
expect(wrapper.text()).toContain('Test')
})
})Adding a New API Endpoint
1. Define types (src/types/myFeature.ts):
export interface MyData {
id: number
name: string
// ...
}
export interface MyDataFilters {
search?: string
page?: number
}2. Create API function (src/api/myFeature.ts):
import { myAxios } from '@/instances/myAxios'
import type { MyData, MyDataFilters } from '@/types/myFeature'
export async function getMyData(filters?: MyDataFilters): Promise<MyData[]> {
return await myAxios
.get<MyData[]>('/my-endpoint', { params: filters })
.then((res) => res.data)
}
export async function getMyDataById(id: number): Promise<MyData> {
return await myAxios
.get<MyData>(`/my-endpoint/${id}`)
.then((res) => res.data)
}3. Create query (src/queries/myFeature.ts):
import { defineQuery, useQuery } from '@pinia/colada'
import { getMyData } from '@/api/myFeature'
export const useMyDataQuery = defineQuery(() => {
const { data, ...rest } = useQuery({
key: () => ['myData'],
query: () => getMyData(),
staleTime: 1000 * 60 * 60, // 1 hour
gcTime: 1000 * 60 * 30, // 30 minutes cache
refetchOnMount: false,
refetchOnWindowFocus: false,
})
return { data, ...rest }
})4. Create composable (src/composables/useMyFeature.ts):
import { useMyDataQuery } from '@/queries/myFeature'
import { computed } from 'vue'
export const useMyFeature = () => {
const { data, isLoading, error } = useMyDataQuery()
const processedData = computed(() => {
// Add business logic here
return data.value?.map(item => ({ ...item, processed: true }))
})
return {
data,
processedData,
isLoading,
error,
}
}Adding Translations
Add keys to both locale files:
// src/locales/en.json
{
"myFeature": {
"title": "My Feature",
"description": "This is my feature",
"actions": {
"save": "Save",
"cancel": "Cancel"
}
}
}// src/locales/es.json
{
"myFeature": {
"title": "Mi Función",
"description": "Esta es mi función",
"actions": {
"save": "Guardar",
"cancel": "Cancelar"
}
}
}Use in components:
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<h1>{{ t('myFeature.title') }}</h1>
<button>{{ t('myFeature.actions.save') }}</button>
</template>Testing
Unit Tests (Vitest)
pnpm test:unit # Run with UI and watch mode
pnpm test:ci # Run once with coverage (CI)- Tests live in
src/__tests__/, mirroring the source structure - Use
@vue/test-utilsfor component testing - Coverage reports output to
reports/coverage/
E2E Tests (Cypress)
pnpm cypress:open # Interactive mode
pnpm cypress:run # Headless mode
pnpm test:e2e:ci # CI mode (Chrome headless)- Tests live in
cypress/e2e/ - Includes accessibility testing with
cypress-axe
Writing Testable Code
- Extract business logic into composables (easier to test)
- Keep components focused (single responsibility)
- Use dependency injection via props/provide-inject
- Mock API calls at the axios instance level
IDE Setup
Recommended: VS Code
- Install Vue - Official (Volar)
- Disable Vetur if installed (conflicts with Volar)
- Enable "Take Over Mode" for better TypeScript support:
- Open Command Palette → "Extensions: Show Built-in Extensions"
- Find "TypeScript and JavaScript Language Features"
- Right-click → "Disable (Workspace)"
Recommended Extensions
- Vue - Official (Volar)
- ESLint
- Prettier
- TypeScript Vue Plugin
Type Checking
TypeScript cannot handle .vue imports natively. We use vue-tsc for type checking:
pnpm type-checkThis is automatically run during pnpm build.