Skip to content

Frontend

Florian Mataigne edited this page May 18, 2026 · 7 revisions

Frontend

Mockups (Figma)

Figma Design

Overview

The frontend is a Vite + React + TypeScript application. It uses Material UI for components, i18next for translations, and React Router v7 for client-side navigation.

At app startup, bootstrap.tsx renders an empty shell immediately for fast TTI, then main.tsx loads the full app during idle time. App.tsx restores the persisted theme mode from localStorage, creates the theme with createAppTheme, applies CssBaseline, and renders the router.

Key Responsibilities

  • Present localized site content and detail pages (productions, series, blogs, media files).
  • Provide search, filters, pagination, and list/grid view toggles for collection pages.
  • Centralize API access and error handling via shared services.
  • Expose a comprehensive design system (tokens, muiPalette, styles) for consistent UI across light/dark modes.
  • Handle language-aware routing with slug localization (EN /archive <-> NL /archief).
  • Normalize API errors and display them via i18n translation.
  • Support deferred loading (bootstrap shell -> idle main load) for performance.

Tech Stack

Core

  • Vite 8 for dev/build tooling with fast HMR
  • React 19 for UI composition
  • TypeScript 6 for type safety

UI and Styling

  • Material UI v9 (@mui/material, @mui/icons-material, @mui/system, @mui/x-date-pickers)
  • Emotion v11 (@emotion/react, @emotion/styled) - MUI's CSS-in-JS foundation

Routing and i18n

  • react-router-dom v7 for client-side routing with lazy code-splitting
  • i18next v26 + react-i18next v17 for multi-language support (EN, NL)

Data and Utility Libraries

  • axios v1 for API requests with custom interceptors
  • dompurify v3 for safe HTML sanitization
  • embla-carousel-react v8 + embla-carousel-wheel-gestures v8 for carousels
  • react-pdf v10 for PDF viewing (mocked in tests)
  • dayjs v1 for date manipulation
  • pdfjs-dist v5 for PDF rendering
  • react-icons v5 for icon sets
  • simple-icons v16 for brand/social icons

Styling System

Styling is intentionally split into layers:

Layer 0: Global CSS (src/index.css)

  • @font-face declarations (ABC Monument Grotesk: Regular, Medium, Bold, Light, Heavy)
  • CSS custom properties for runtime theming
  • Global CSS reset rules and base styles
  • Utility classes

Layer 1: Design Tokens (src/theme/tokens.ts)

Source of truth for all reusable values, exported as a single tokens constant. Key categories:

  • Colors - accent, series, light/dark mode palettes, neutral scale, overlays, media backgrounds, semantic colors
  • Spacing - xs (4px) to 3xl (64px), with numeric variants
  • Typography - fontFamily, weights, sizes, lineHeights
  • Shadows - subtle through xl, plus navbar and mediaControl
  • Transitions - fast (150ms) to verySlow (500ms)
  • Breakpoints - xs (0) to xl (1920px)
  • Z-index - base (0) to tooltip (1080)
  • Component-specific - navbar height, card dimensions, chip padding

Layer 2: MUI Theme Factory (src/theme/muiPalette.ts)

Converts tokens into a Material UI Theme supporting light/dark modes via createAppTheme(mode).

  • Palette - mode-aware, light mode and darkmode have different palattes.
  • Typography - h1–h6, body1, body2, button (mapped from tokens)
  • Breakpoints - parsed from tokens
  • Shape - borderRadius and spacing
  • Shadows - 25 entries
  • Component overrides - MuiButton, MuiCard, MuiPaper, MuiChip
  • TypeScript augmentation - via muiPalette.d.ts extends Palette + PaletteOptions with accent, and ButtonPropsColorOverrides with accent

Layer 3: Shared Styles (src/theme/styles.ts)

Reusable sx recipe factories:

  • createNavbarStyles() - returns a style-map specifically for the navigation bar
  • createCommonStyles(theme) - returns a mode-aware style map for common components that are reused in the codebase.
  • createHomePageStyles(theme) - returns a mode-aware style map for the landing page

Routing

Routes are defined in frontend/src/router.tsx with a language-first strategy:

Routing Strategy

  1. Language-prefixed URLs: all routes use /:lang/... where lang is nl or en
  2. Language inference: paths like /archive are inferred to specific languages (/archief -> NL, /archive -> EN)
  3. Fallback resolution: if no slug-specific inference, use browser i18n language or default (NL)
  4. Compatibility aliases: old routes like /productions, /producties redirect to canonical localized equivalents
  5. Deferred loading: bootstrap.tsx renders empty shell immediately; main.tsx loads full app via requestIdleCallback

Route Components

  • ScrollToTop: scrolls to top on every pathname/search change via useLayoutEffect
  • LanguagePathRedirect: detects language from pathname, redirects to properly localized path
  • AliasDetailRedirect: redirects alias slug detail paths to canonical localized routes
  • LocalizedLayout: main layout with /:lang/* pattern - renders Navbar, Routes, Footer
  • Router: root component with BrowserRouter and NotificationProvider

Route Table

Route Component Notes
/:lang HomePage Landing page with hero, stats, cards
/en/archive, /nl/archief ProductionsPage Archive listing with filter panel
/en/productions, /nl/producties (redirect) Redirects to archive listing
/en/productions/:id, /nl/producties/:id ProductionDetailPage Production detail page
/en/series, /nl/reeksen SeriesPage Series listing
/en/series/:id, /nl/reeksen/:id SeriesDetailPage Series detail with productions timeline
/en/blogs, /nl/blogs BlogsPage Blog listing
/en/blogs/:id, /nl/blogs/:id BlogDetailPage Blog detail page
/en/media, /nl/media MediaFilesPage Media files listing
/en/media/:id, /nl/media/:id (redirect) Redirects to media listing with error alert
/en/*, /nl/* NotFoundPage 404 fallback
/ and unprefixed paths (redirect) Normalized to /:lang/... via LanguagePathRedirect

Router Features

  • Lazy-loads Navbar, Footer, and all route pages with Suspense
  • ScrollToTop scrolls to top on route changes
  • Renders NotificationProvider for global floating alerts
  • Supports redirect aliases for old slug names
  • AliasDetailRedirect redirects untranslated detail URLs

Data Access

Frontend API logic is grouped by feature under frontend/src/services/:

Shared Transport

  • Api.ts - shared Axios instance with baseURL: '/api/v1', Content-Type: application/json header, X-API-Key from process.env.PUBLIC_API_KEY; request interceptor adds Accept-Language from i18n; response interceptor rejects all errors through normalizeApiError()
  • ApiErrorMapper.ts - normalizes HTTP errors to typed ApiError with i18n-translated messages
  • ApiParams.ts - converts FilteredListOptions to backend query params (pageSize -> page_size, array values comma-separated, scalar values passed directly)
  • ApiTypes.ts - ApiError class (extends Error with status: number, message: string), PaginationOptions, CommonListFilters (search, ordering, external_id), FilteredListOptions<TFilters> (extends PaginationOptions with filters)

Error Handling

Common HTTP status codes map to i18n keys:

  • 400 -> apiErrors.status.400
  • 401, 403, 404, 429, 500 -> status-specific keys
  • Network errors (status 0) -> apiErrors.network
  • Unknown codes -> apiErrors.status.genericWithCode (with {status} placeholder)

Feature Clients

Each service folder contains <Domain>Options.ts (filter types) and <Domain>.ts (API functions):

Service Functions Filter type
blogs/ getBlog(id), getBlogs(options?) BlogFilters { production?, published?, slug?, title? }
events/ getEvent(id), getEvents(options?) EventFilters { production?, hall?, location?, starts_at_after?, starts_at_before?, ends_at_after?, ends_at_before? }
genres/ getGenre(id), getGenres(options?) GenreFilters { type?, vendor_id?, name? }
halls/ getHall(id), getHalls(options?) HallFilters { space?, location?, seat_selection?, open_seating?, name? }
languages/ getLanguage(code), getLanguages(options?) LanguageFilters { code?, name?, is_active? }
locations/ getLocation(id), getLocations(options?) LocationFilters { city?, country?, postal_code?, is_own_location?, name? }
media/ getMediaGalleries(options?), getMediaGallery(id), getMediaItems(options?), getMediaItem(id) MediaGalleryFilters { name? }, MediaItemFilters { gallery?, type?, file_format?, original_filename? }
media_files/ getMediaFiles(options?), getMediaFile(id) MediaFileFilters { file_type?, mime_type?, filename?, description? }
pricing/ getPrice(id), getPrices(options?), getPriceRank(id), getPriceRanks(options?) PriceFilters { type?, visibility?, membership?, cineville_box?, description? }, PriceRankFilters { position?, position_gte?, position_lte?, description? }
productions/ getProduction(id, include?), getProductions(options?), getLandingStats() ProductionFilters { attendance_mode?, performer_type?, uit_database_type?, genre?, tag?, has_media?, title?, artist_name?, first_event_start_after?, first_event_start_before? }
spaces/ getSpace(id), getSpaces(options?) SpaceFilters { location?, name? }. Internal type HallInSpaceResponse maps nested halls to set space: null.
tags/ getTag(id), getTags(options?) TagFilters { type?, source?, is_enabled?, name? }

API Client Details

const api = axios.create({
  baseURL: '/api/v1',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': API_KEY,
  },
})

api.interceptors.request.use((config) => {
  const lang = (i18n.resolvedLanguage || i18n.language || 'nl').split('-', 1)[0]
  config.headers.set('Accept-Language', lang)
  return config
})

api.interceptors.response.use(
  (response) => response,
  (error) => Promise.reject(normalizeApiError(error, axios.isAxiosError))
)

Types

All domain model types live in src/types/. Key types:

Type File Description
AppThemeMode Theme.ts 'light' | 'dark'
ModeToggleProps Theme.ts { mode: AppThemeMode; onToggleMode: () => void }
FloatingAlertSeverity FloatingAlertConfig.ts 'error' | 'warning' | 'info' | 'success'
FloatingAlertProps FloatingAlertConfig.ts Alert props (open, onClose, message, title, severity, position, etc.)
AttendanceMode Productions.ts 'offline' | 'online'
PerformerType Productions.ts 'group' | 'solo'
Production Productions.ts Full production model with events, related, genres, blogs
Blog Blogs.ts Full blog model (extends BlogCardData with body and productions)
Event Events.ts Event with production, hall, dates, prices
Tag Tags.ts Tag with multilingual name/excerpt/description
Genre Genres.ts Genre with type, name, vendor_id
MediaFile MediaFiles.ts File with type ('image'|'pdf'|'other'), mime_type, size
GenreAndTagChipContext GenreAndTagChip.ts 'search' | 'description' | 'series' | 'static'
GenreAndTagChipType GenreAndTagChip.ts 'genre' | 'seriesTag'
SearchSortTarget search/types.ts 'name' | 'date'
SearchSortDirection search/types.ts 'asc' | 'desc'
SearchViewMode search/types.ts 'list' | 'grid'

Installation

Prerequisites

  • Node.js LTS (v20+ recommended)
  • npm

Steps

cd frontend
npm ci

Use npm ci for exact version reproducibility (recommended for CI).

Available Scripts

Development

npm run dev

Starts Vite dev server at http://localhost:5173 with:

  • Fast HMR (hot module reloading)
  • Dev proxy for /api, /admin, /static, /media -> http://localhost:8000

Build

npm run build

Type-checks (tsc) and builds optimized production assets into frontend/dist/.

Preview

npm run preview

Previews the production build locally.

Testing

npm test              # run once
npm run test:watch   # watch mode

Linting and Formatting

npm run lint            # check all (ESLint, zero warnings)
npm run lint:fix        # auto-fix
npm run format:check    # Prettier check
npm run format:fix      # Prettier auto-format

Testing

Frontend tests use Jest 30 + jsdom + React Testing Library.

Test Setup

  • jest.config.cjs - ts-jest preset, jsdom environment, setup file setupTests.ts, maps react-pdf to mock
  • tsconfig.jest.json - TS config for tests (CommonJS module, Node resolution, includes test files)
  • src/setupTests.ts - Registers testing-library matchers, polyfills (TextEncoder, TextDecoder, ResizeObserver, IntersectionObserver, matchMedia), mocks axios, sets process.env.PUBLIC_API_KEY = 'test-api-key', suppresses XMLHttpRequest console.error

The test directory mirrors the source structure. See Frontend folder Structure.

Test Conventions

  • Use React Testing Library for component tests
  • Mock external modules in __mocks__/
  • Wrap with MemoryRouter, ThemeProvider, and i18n when needed
  • setupTests.ts provides global mocks for axios, IntersectionObserver, ResizeObserver, matchMedia

Example Component Test

import { render, screen } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import { ThemeProvider } from '@mui/material/styles'
import { createAppTheme } from '../theme/muiPalette'

test('renders correctly', () => {
  render(
    <MemoryRouter>
      <ThemeProvider theme={createAppTheme('light')}>
        <MyComponent />
      </ThemeProvider>
    </MemoryRouter>
  )
  expect(screen.getByText(/expected text/i)).toBeInTheDocument()
})

Configuration Files

package.json

Name: viernulvier-frontend, private, ES module. Key scripts: dev, build (tsc + vite), preview, lint (ESLint, zero warnings), lint:fix, format:check, format:fix, test, test:watch.

vite.config.ts

Configures:

  • React plugin for JSX/TSX
  • Environment variable loading from ../infrastructure (envDir, envPrefix: 'PUBLIC_')
  • Defines process.env.PUBLIC_API_KEY
  • Dev server proxy (/api, /admin, /static, /media -> http://localhost:8000)

TypeScript Configurations

  • tsconfig.json - App code (ES2020 target, ESNext module, Bundler resolution, react-jsx, strict mode)
  • tsconfig.node.json - Tooling only (Vite config): composite, ESNext, Bundler resolution
  • tsconfig.jest.json - Tests (CommonJS module, Node resolution, includes test files)

eslint.config.cjs

Flat config using:

  • @typescript-eslint/parser and plugin
  • eslint-plugin-react, eslint-plugin-react-hooks, eslint-plugin-react-refresh
  • eslint-plugin-import for import ordering
  • eslint-config-prettier and eslint-plugin-prettier
  • Enforces: consistent type imports (verbatimModuleSyntax), import ordering, MUI sx over inline style, strict React rules
  • Zero warnings policy (--max-warnings 0)

jest.config.cjs

  • ts-jest preset, jsdom environment
  • Setup file: setupTests.ts
  • Maps react-pdf to mock
  • Coverage collection from src/

.prettierrc.json

{
  "semi": false,
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 100,
  "tabWidth": 2,
  "useTabs": false,
  "bracketSpacing": true,
  "bracketSameLine": false,
  "arrowParens": "always",
  "endOfLine": "lf"
}

index.html

Entry point loads /src/bootstrap.tsx as module script. Contains meta description and title "Archive".

lighthouserc.json

Lighthouse CI config: static dist dir, 3 runs, assertions for performance (warn >= 1), accessibility (error >= 1), best-practices (error >= 1), SEO (warn >= 0.5).

HTML Support in Text Sections

Text sections (descriptions, excerpts, teaser text) support safe HTML rendering via HtmlText component.

How It Works

  • HtmlText component wraps sanitizeHtml utility from SanitizeHtml.ts
  • All HTML is sanitized with DOMPurify (prevents XSS)
  • Default rules: USE_PROFILES.html, FORBID_TAGS [br, script], ADD_TAGS [img], ADD_ATTR [alt, height, src, style, title, width], FORBID_ATTR [onblur, onclick, onerror, onfocus, onkeydown, onkeypress, onkeyup, onmouseleave, onmouseenter, onmouseover, onload]
  • Additional rules available: forbidImagesRule (removes images), sanitizeImagesStrictRule (removes style/width/height/align attributes), forbidEmbedsRule (removes iframes)
  • htmlToPlainText(): decodes HTML entities, replaces block tags with spaces, extracts textContent
  • Empty content shows optional fallback message

Components Using HTML Rendering

Component Field Notes
BlogGridCard excerpt Blog excerpt in grid
BlogListCard excerpt Blog excerpt in list
SeriesGridCard description Series description in grid
SeriesListCard description Series description in list
SeriesHeader description Series description on detail page
Description (production) teaser, description Production teaser and description

Usage Example

import HtmlText from '../components/HtmlText'

export const MyComponent = ({ content }) => {
  return <HtmlText html={content} fallback="No description available" />
}

Troubleshooting

Dev Server Port Conflict

npm run dev -- --port 3000

Test Environment Errors (browser APIs)

Check src/setupTests.ts for polyfills. If adding new browser APIs to components, add corresponding mocks for: TextEncoder, TextDecoder, ResizeObserver, IntersectionObserver, matchMedia, scrollTo.

ESLint Issues After Updates

npm run lint          # check (zero warnings policy)
npm run lint:fix      # auto-fix

Verify plugin compatibility in eslint.config.cjs.

Build or TS Errors

npm run build

Check tsconfig*.json consistency across configs.

Missing Translation Keys

Add to both:

  • src/locales/en/translation.json
  • src/locales/nl/translation.json

Keys are hierarchical (nested objects). Key sections: nav, landing, searchbar, apiErrors, genreChip, events, archive.home, productions, blogs.home, blog, notFound, footer, series, media.

API Proxy Issues in Dev

Verify vite.config.ts server proxy config. Default routes:

  • /api -> http://localhost:8000/api
  • /admin -> http://localhost:8000/admin
  • /static -> http://localhost:8000/static
  • /media -> http://localhost:8000/media

Ensure backend dev server runs on :8000.

Resources

Clone this wiki locally