Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .agents/rules/code-style.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Code Style Rules

## Language & Runtime
- TypeScript (strict mode) targeting `esnext`.
- Node 22 via `mise`. Package manager: `pnpm 10.20`.

## Formatting (Prettier)
| Setting | Value |
|---|---|
| `printWidth` | 100 |
| `tabWidth` | 2 |
| `useTabs` | false |
| `singleQuote` | true |
| `semi` | false |
| `trailingComma` | all |
| `arrowParens` | avoid |
| `bracketSpacing` | true |
| `endOfLine` | auto |

Run: `pnpm prettier`

## EditorConfig
- UTF-8, LF line endings, 2-space indent, trim trailing whitespace, insert final newline.

## TypeScript Conventions
- `strict: true` with all implicit checks enabled (`noImplicitAny`, `noImplicitReturns`, `noImplicitThis`, `noUnusedLocals`, `noImplicitOverride`, `noUncheckedIndexedAccess`, `strictNullChecks`).
- Use `Readonly<>` wrappers on model/data types (see `src/models/`).
- Prefer `type` aliases over `interface` for data shapes; use `interface` for stateful contracts (e.g., `State`).
- No semicolons at end of statements.
- Use single quotes for strings.
- Use trailing commas everywhere.
- Use arrow functions with omitted parens for single parameters (`x => x`).

## CSS Conventions
- Plain CSS with PostCSS (`postcss-preset-env` stage 0).
- CSS custom properties defined in `src/styles/vars.css`.
- Component styles co-located as `<component>.css`, imported via barrel `index.ts`.
- No CSS modules or CSS-in-JS; class names are plain global strings.
- Use `var(--token)` references for colors, spacing, typography, and borders.

## File Organisation
- Barrel exports via `index.ts` per component directory.
- Components follow `src/components/<name>/` pattern with `<name>.ts`, `<name>.css`, `<name>.test.ts`, `index.ts`.
- Models in `src/models/` as standalone type-only files.
- DOM utilities in `src/dom/`.
21 changes: 21 additions & 0 deletions .agents/rules/security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Security Rules

## General
- This is a frontend-only application with no backend server or authentication.
- API data is served as static JSON files from `public/api/` via Vite's dev server.

## XSS Considerations
- The custom `html` tagged template literal in `src/dom/html.ts` performs **no sanitisation** -- it concatenates strings directly. Any user-controlled or external data interpolated into templates is injected as raw HTML via `innerHTML`.
- **Do not** interpolate untrusted input into `html` templates without first escaping HTML entities.

## Dependencies
- Keep dependencies minimal. The project has zero production dependencies; all packages are `devDependencies`.
- Run `pnpm audit` periodically to check for known vulnerabilities.

## Secrets
- No secrets, API keys, or tokens are used in this project.
- Do not commit `.env` files or secrets to the repository.

## Data
- All data (`survey.json`, `demographics.json`, `respondents.json`) is mock/synthetic data with no PII.
<!-- TODO: human please fill in if production data handling rules apply -->
27 changes: 27 additions & 0 deletions .agents/rules/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Testing Rules

## Framework
- **Vitest 4.x** with `happy-dom` environment.
- Config: `vitest.config.ts`.

## Test File Conventions
- Co-locate tests with source: `src/components/<name>/<name>.test.ts`.
- Test files match pattern `**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}`.
- Import from `vitest`: `describe`, `it`, `expect`.

## Running Tests
```sh
pnpm test # watch mode (default vitest behaviour)
```

## Writing Tests
- Render components by calling the component function with a `State` object, then invoking the returned render function on a DOM element (e.g., `document.body`).
- Use mock JSON data from `public/api/` (`survey.json`, `demographics.json`, `respondents.json`) imported directly.
- Use `toMatchInlineSnapshot()` for full HTML output assertions.
- Use `toHaveLength()` for element count checks.
- Simulate user interaction via `dispatchEvent(new MouseEvent('click'))`.
- Assert state changes by importing `getState()` from `src/store.ts`.

## Coverage
<!-- TODO: human please fill in -->
No coverage thresholds are currently configured.
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,9 @@ eslint-data
########
/dist/*
/config.json
/.cache-loader
/.cache-loader

# AI agent local files #
#####################
CLAUDE.local.md
.claude/settings.local.json
56 changes: 56 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# AGENTS.md

## Project Summary
Attest Frontend Technical Test -- a vanilla TypeScript SPA that displays survey results with demographic filtering. Built with Vite, Vitest, PostCSS, and no framework.

## Quick Reference
| Command | Purpose |
|---|---|
| `pnpm start` | Dev server at http://localhost:5173 |
| `pnpm build` | Typecheck + production build |
| `pnpm test` | Run unit tests (Vitest, watch mode) |
| `pnpm typecheck` | TypeScript type checking (`tsc --noEmit`) |
| `pnpm prettier` | Format all files |

## Architecture
See `docs/ARCHITECTURE.md` for full details.

### Key Paths
| Path | Purpose |
|---|---|
| `src/main.ts` | App entry point; fetches data, subscribes to store |
| `src/store.ts` | Global state and pub/sub |
| `src/dom/` | Minimal HTML templating and rendering utilities |
| `src/components/filters/` | Demographic filter sidebar |
| `src/components/survey/` | Survey results display |
| `src/models/` | TypeScript type definitions (Demographic, Respondent, Survey) |
| `src/styles/` | Global CSS and design tokens |
| `public/api/` | Static mock JSON data |

## Coding Standards
See `.agents/rules/code-style.md` for formatting, TypeScript, and CSS conventions.

Key points:
- Strict TypeScript. No semicolons. Single quotes. Trailing commas.
- Prettier-formatted (run `pnpm prettier`).
- CSS custom properties from `src/styles/vars.css`. No CSS modules.
- Components are pure functions: `(state: State) => (el: Element | null) => void`.

## Testing
See `.agents/rules/testing.md` for full testing guidelines.

- Vitest with happy-dom.
- Tests co-located with components as `<name>.test.ts`.
- Mock data imported from `public/api/*.json`.

## Security
See `.agents/rules/security.md` for security considerations.

- The `html` template utility does **not** sanitise input. Never interpolate untrusted data.
- Zero production dependencies.

## Documentation Maintenance
When a code change invalidates any section of this file, `README.md`, or `docs/ARCHITECTURE.md`:
1. Flag the outdated section in your response.
2. Propose an update with the exact edit.
3. Apply the update only after confirmation.
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@AGENTS.md
@.agents/rules/code-style.md
@.agents/rules/testing.md
@.agents/rules/security.md
@docs/ARCHITECTURE.md


### Response Preferences
- Be concise. Prefer code over prose.
- When uncertain about architecture, read `docs/ARCHITECTURE.md` before assuming.
- When a change would invalidate any section of `AGENTS.md` or `README.md`,
flag it and offer to update them per the instructions in AGENTS.md.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,15 @@ You can use [`mise`](https://mise.jdx.dev/) to setup environment

## How to run?

- Open up the Dev Server on `https://localhost:5173` with `pnpm start`
- Open up the Dev Server on `http://localhost:5173` with `pnpm start`
- To retrieve the survey data request `http://localhost:5173/api/survey.json`
- To retrieve the demographics data request `http://localhost:5173/api/demographics.json`
- To retrieve the respondents data request `http://localhost:5173/api/respondents.json`
- Run unit tests `pnpm test` (these will only pass upon successfully completing task 3)
- Run type checking `pnpm typecheck`

## Documentation

- [`AGENTS.md`](./AGENTS.md) -- AI agent instructions and project quick reference.
- [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) -- System architecture, data flow, and directory structure.
- [`.agents/rules/`](./.agents/rules/) -- Code style, testing, and security conventions.
101 changes: 101 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Architecture

## Overview

A vanilla TypeScript single-page application that displays survey results with demographic filtering. No framework -- rendering is done with a minimal custom tagged-template-literal + `innerHTML` approach.

## Tech Stack
| Layer | Tool |
|---|---|
| Language | TypeScript 5.9 (strict) |
| Bundler | Vite (via `rolldown-vite`) |
| CSS | PostCSS with `postcss-preset-env` stage 0 |
| Testing | Vitest 4 + happy-dom |
| Formatting | Prettier |
| Runtime | Node 22 (managed by `mise`) |
| Package Manager | pnpm 10.20 |

## Directory Structure

```
.
├── index.html # SPA entry point
├── public/
│ ├── api/
│ │ ├── survey.json # Mock survey definition (questions + answers)
│ │ ├── demographics.json # Mock demographic categories and options
│ │ └── respondents.json # Mock respondent data (segmentation + responses)
│ └── assets/
│ └── logo.svg
├── src/
│ ├── main.ts # App bootstrap: subscribes to store, fetches data
│ ├── store.ts # Global state + pub/sub (patchState / subscribe)
│ ├── dom/
│ │ ├── html.ts # Tagged template literal for HTML string building
│ │ ├── render.ts # Renders HTML string into a DOM element via innerHTML
│ │ └── index.ts # Barrel export
│ ├── components/
│ │ ├── filters/
│ │ │ ├── filters.ts # Demographic filter sidebar component
│ │ │ ├── filters.css
│ │ │ ├── filters.test.ts
│ │ │ └── index.ts
│ │ └── survey/
│ │ ├── survey.ts # Survey results display component
│ │ ├── survey.css
│ │ ├── survey.test.ts
│ │ └── index.ts
│ ├── models/
│ │ ├── demographic.ts # Demographic, DemographicOption types
│ │ ├── respondent.ts # Respondent, RespondentSegmentation, RespondentResponses types
│ │ └── survey.ts # Survey, SurveyQuestion, SurveyQuestionAnswer types
│ └── styles/
│ ├── vars.css # CSS custom properties (design tokens)
│ └── main.css # Global styles, page layout grid
├── vite.config.ts
├── vitest.config.ts
├── tsconfig.json
├── prettier.config.js
├── .postcssrc.js
├── .editorconfig
└── .mise.toml
```

## Data Flow

```
index.html
└─ src/main.ts (bootstrap)
├─ fetch /api/survey.json ──► patchState({ survey })
├─ fetch /api/demographics.json ──► patchState({ demographics })
├─ fetch /api/respondents.json ──► patchState({ respondents })
└─ subscribe(callback)
├─ renderSurvey(state) ──► survey(state)(element)
└─ renderFilters(state) ──► filters(state)(element)
```

### State Management (`src/store.ts`)

A minimal pub/sub store:
- **`State`** holds: `survey`, `demographics`, `respondents`, `selectedDemographics`, `selectedQuestionAnswers`.
- **`patchState(partial)`** merges partial state via `Object.assign` and notifies subscribers.
- **`subscribe(fn)`** registers a callback invoked on every state change.
- **`getState()`** returns current state reference.

### Rendering (`src/dom/`)

- `html` -- tagged template literal that concatenates strings (no escaping, no virtual DOM).
- `render(htmlString, effects?)` -- returns a function `(el) => void` that sets `el.innerHTML` and optionally runs side effects (e.g., attaching event listeners).

### Components

Components are **pure functions**: `(state: State) => (el: Element | null) => void`.

- **`filters`** -- renders demographic options in a sidebar. Clicking an option calls `toggleSelectedDemographicOption` which patches `selectedDemographics` in the store.
- **`survey`** -- renders survey questions and answer bars. Filters respondents by `selectedDemographics` to show filtered counts and percentages. A "marker" bar shows the unfiltered baseline; a "track" bar shows the filtered value.

### Domain Model

- **`Survey`** -- has a title and a map of `SurveyQuestion`s, each with a map of `SurveyQuestionAnswer`s.
- **`Demographic`** -- has an id, display name, and a map of `DemographicOption`s.
- **`Respondent`** -- has segmentation (which demographic options they belong to) and responses (which answers they selected per question).