Skip to content

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

Core Framework

TechnologyPurposeWhy?
Vue 3UI FrameworkComposition API provides better TypeScript support, code reusability through composables, and improved performance
TypeScriptType SafetyCatches errors at compile time, better IDE support, self-documenting code
ViteBuild ToolLightning-fast HMR, native ES modules, optimized production builds
pnpmPackage ManagerFaster installs, disk space efficient, strict dependency resolution

State Management & Data Fetching

TechnologyPurposeWhy?
PiniaState ManagementOfficial Vue store, excellent TypeScript support, devtools integration
Pinia ColadaAsync State/CachingDeclarative queries with built-in caching, deduplication, and stale-while-revalidate
AxiosHTTP ClientInterceptors for auth, request/response transformation, better error handling

UI & Visualization

TechnologyPurposeWhy?
Naive UIComponent LibraryComprehensive Vue 3 components, TypeScript support, customizable themes
ApexChartsCharts & GraphsInteractive charts, responsive, extensive chart types
AG GridData TablesHigh-performance grid, sorting, filtering, accessibility
LeafletMapsLightweight, mobile-friendly, extensive plugin ecosystem
EChartsAdvanced VisualizationsComplex data visualizations when needed

Internationalization & Accessibility

TechnologyPurposeWhy?
Vue I18nTranslationsRuntime language switching, pluralization, date/number formatting
@vueuse/headSEO/MetaDynamic document head management for accessibility and SEO

Testing

TechnologyPurposeWhy?
VitestUnit TestsNative Vite integration, Jest-compatible API, fast execution
CypressE2E TestsReal browser testing, time-travel debugging, network stubbing
cypress-axeA11y TestingAutomated accessibility audits during E2E tests

Code Quality

TechnologyPurposeWhy?
ESLintLintingCatches code issues, enforces consistent style
PrettierFormattingConsistent code formatting across the team
vue-tscType CheckingFull 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:

FeatureValueDescription
Register TypeautoUpdateService worker updates automatically without user prompt
Display ModestandaloneApp runs in its own window without browser UI
Theme Color#1d73d8Blue 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 TypeStrategyCache DurationPurpose
API Calls (/api/*)NetworkFirst7 daysFresh data preferred, falls back to cache when offline
Pages (HTML)NetworkFirst30 daysAlways try to get latest version, cache for offline
Assets (JS, CSS)StaleWhileRevalidate30 daysServe cached immediately, update in background
Media (images, fonts)CacheFirst1 yearStatic 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.ico
  • robots.txt
  • img/icons/logo.png
  • All *.js, *.css, *.html, *.ico, *.png, *.svg, *.woff, *.woff2 files

Web App Manifest

The manifest (manifest.webmanifest) is auto-generated with:

json
{
  "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:

EventDescription
readyApp is being served from cache
registeredService worker successfully registered
cachedContent cached for offline use
updatefoundNew content is being downloaded
updatedNew content available (refresh needed)
offlineNo internet connection, running offline

Development vs Production

EnvironmentBehavior
DevelopmentService worker enabled with type: 'module' for easier debugging
ProductionFull PWA functionality with optimized caching

Testing PWA Locally

  1. Run the production build: pnpm build
  2. Preview the build: pnpm preview
  3. Open Chrome DevTools → Application tab → Service Workers
  4. 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:

typescript
{
  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

  1. API Layer → Raw HTTP calls to the backend
  2. Queries Layer → Wraps API calls with Pinia Colada for caching/state management
  3. Composables → Business logic that combines queries and provides computed data
  4. Components → Consume composables and render UI
  5. 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

TypeConventionExample
ComponentsPascalCaseChartComponent.vue
ComposablescamelCase with use prefixuseInfections.ts
StorescamelCase with Store suffixauthStore.ts
Types/InterfacesPascalCase (no I prefix)Infection, User
API functionscamelCase, verb-firstgetInfections(), createUser()
ConstantsSCREAMING_SNAKE_CASEAPI_BASE_URL

TypeScript Rules

  • No I prefix for interfaces (enforced by ESLint)
  • Unused variables must be prefixed with _
  • Strict mode enabled - no implicit any

Code Formatting (Prettier)

javascript
{
  singleQuote: true,
  semi: false,
  tabWidth: 4,
  useTabs: true,
  vueIndentScriptAndStyle: true
}

Component Structure

Follow this order in .vue files:

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

  1. Define types in src/types/ for any new data structures
  2. Create API functions in src/api/ for backend communication
  3. Add queries in src/queries/ if data needs caching
  4. Create composables in src/composables/ for business logic
  5. Build components following Atomic Design (atoms → molecules → organisms)
  6. Create/update views to compose your components
  7. Add translations in src/locales/en.json and src/locales/es.json
  8. 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:

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

typescript
// 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):

typescript
export interface MyData {
  id: number
  name: string
  // ...
}

export interface MyDataFilters {
  search?: string
  page?: number
}

2. Create API function (src/api/myFeature.ts):

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

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

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

json
// src/locales/en.json
{
  "myFeature": {
    "title": "My Feature",
    "description": "This is my feature",
    "actions": {
      "save": "Save",
      "cancel": "Cancel"
    }
  }
}
json
// src/locales/es.json
{
  "myFeature": {
    "title": "Mi Función",
    "description": "Esta es mi función",
    "actions": {
      "save": "Guardar",
      "cancel": "Cancelar"
    }
  }
}

Use in components:

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

bash
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-utils for component testing
  • Coverage reports output to reports/coverage/

E2E Tests (Cypress)

bash
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

  1. Install Vue - Official (Volar)
  2. Disable Vetur if installed (conflicts with Volar)
  3. 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)"
  • Vue - Official (Volar)
  • ESLint
  • Prettier
  • TypeScript Vue Plugin

Type Checking

TypeScript cannot handle .vue imports natively. We use vue-tsc for type checking:

bash
pnpm type-check

This is automatically run during pnpm build.