Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
122 commits
Select commit Hold shift + click to select a range
9fd00f5
feat(): add hash router, i18next i18n (en/fr/es), and page views
ffillouxdev Apr 13, 2026
43609b3
fix(): replace hash router with History API and add i18n localized URLs
ffillouxdev Apr 13, 2026
5494b44
feat(): add header and bottom navbar with language selector
ffillouxdev Apr 15, 2026
712d134
feat(): desktop header with inline navbar matching mockups
ffillouxdev Apr 15, 2026
d597b23
refactor(): extract header into src/components/header.ts
ffillouxdev Apr 15, 2026
44a0fa8
style(): update header layout and center page titles
ffillouxdev Apr 15, 2026
d8d8868
fix(): light gray hr separator and fix localized URLs in header
ffillouxdev Apr 15, 2026
e85b62e
feat(): add offline PWA toggle in header dropdown menu
ffillouxdev Apr 15, 2026
75ec0b0
feat(): add mobile bottom navbar matching mockups
ffillouxdev Apr 15, 2026
68ac8d7
fix(): update home title to symptom location question
ffillouxdev Apr 15, 2026
2433aeb
feat(): add call-to-action component with book reference to home and …
ffillouxdev Apr 15, 2026
16d451b
fix(): move call-to-action outside main and add bottom padding for mo…
ffillouxdev Apr 15, 2026
5ee1e4e
fix(): wire view functions to router so CTA and page content render c…
ffillouxdev Apr 15, 2026
8365505
fix(): remove all main content except call-to-action on home and cata…
ffillouxdev Apr 15, 2026
5d136ee
feat(): update call-to-action with green text and book link button
ffillouxdev Apr 15, 2026
a832426
style(): left-align CTA with flex layout, image placeholder, and blac…
ffillouxdev Apr 15, 2026
c0836b6
style(): move image placeholder to the right in CTA
ffillouxdev Apr 15, 2026
7d9af51
style(): replace max-w-2xl with w-full in call-to-action
ffillouxdev Apr 15, 2026
ebacaef
style(): set max-w-5xl on all main elements
ffillouxdev Apr 15, 2026
885503d
style(): replace max-w-7xl with max-w-5xl in header
ffillouxdev Apr 15, 2026
0090a76
style(): add main-bg.png as background image on all main elements
ffillouxdev Apr 15, 2026
c2e3090
style: push main to bottom of page and replace CTA placeholder with b…
ffillouxdev Apr 16, 2026
ef3581f
style: replace min-h-[80vh] with md:h-[93vh] in home main
ffillouxdev Apr 16, 2026
7c232ad
style: set md:h-[93vh] on all main elements and use natural image dim…
ffillouxdev Apr 16, 2026
e362f82
style: resize CTA book image to thumbnail size per mockup
ffillouxdev Apr 16, 2026
f7e7fa6
style: standardize main classes across all pages
ffillouxdev Apr 16, 2026
3454da5
feat: add search input and disease list structure to catalogue page
ffillouxdev Apr 16, 2026
c8a9e87
style: add mt-2 to all mains, restore h1 to home, update catalogue se…
ffillouxdev Apr 16, 2026
08bc646
style: make main full width on mobile with px-4, max-w-5xl on tablet/…
ffillouxdev Apr 16, 2026
d923e5d
style: increase padding on mains from md:px-6 to md:px-28, remove pad…
ffillouxdev Apr 16, 2026
d80310b
i18n: add translations for catalogue search, disease columns, and CTA…
ffillouxdev Apr 16, 2026
9064365
style: apply h-[calc(100vh-4.5rem)] to all screen sizes and add green…
ffillouxdev Apr 16, 2026
d5044d1
feat: add questionnaire system with breadcrumb, question buttons and …
ffillouxdev Apr 16, 2026
b881e08
feat(): implement questionnaire with breadcrumb navigation on home
ffillouxdev Apr 16, 2026
4f7d8f8
feat(): populate catalogue with diseases and pathogens from identific…
ffillouxdev Apr 16, 2026
c1a457d
style(): update catalogue column headers to match source document naming
ffillouxdev Apr 16, 2026
1e3fb81
style(): remove max-w-md constraint on questionnaire buttons container
ffillouxdev Apr 16, 2026
8a5a64f
style(): add text truncation to breadcrumb and catalogue pathogen column
ffillouxdev Apr 16, 2026
af4b261
style(): add table-fixed to prevent content overflow beyond table border
ffillouxdev Apr 16, 2026
b693a7a
style(): add responsive width constraints to breadcrumb
ffillouxdev Apr 16, 2026
6946865
feat(): add result item to breadcrumb when reaching a disease
ffillouxdev Apr 16, 2026
3a8e7de
feat(): use disease name in breadcrumb instead of full option text
ffillouxdev Apr 16, 2026
5aa1870
feat(): truncate question labels in breadcrumb at punctuation or max …
ffillouxdev Apr 16, 2026
9b66171
Simplify breadcrumb display for disease results
ffillouxdev Apr 17, 2026
b4115e1
Replace height with min-height for main containers
ffillouxdev Apr 17, 2026
eeede37
Add max-height to catalogue table for scrollable area
ffillouxdev Apr 17, 2026
4e71cfb
feat(): shared disease result component for home and catalogue
ffillouxdev Apr 17, 2026
8b8ffd9
refactor(): collapsible geographical zones, improve catalogue result …
ffillouxdev Apr 17, 2026
fb326d4
refactor(): fix styling details and background consistency
ffillouxdev Apr 17, 2026
59708ce
refactor(): move warning above geographical zones details
ffillouxdev Apr 17, 2026
34470ea
refactor(): improve catalogue table size on mobile
ffillouxdev Apr 17, 2026
8833dad
refactor(): revert text enlargement, keep vertical padding increase
ffillouxdev Apr 17, 2026
645a624
refactor(): rename catalogue.ts to catalog.ts, translate column headers
ffillouxdev Apr 17, 2026
a6d9ad1
refactor(): reduce column header height on mobile
ffillouxdev Apr 17, 2026
8db4326
refactor(): keep (Syn. = Synonymy*) in English across all languages
ffillouxdev Apr 17, 2026
b9a5b37
refactor(): translate synonym abbreviation by language
ffillouxdev Apr 17, 2026
4e3af5a
refactor(): keep book title in English across all languages
ffillouxdev Apr 17, 2026
3d4ad7c
refactor(): update geographical warning with email and font-semibold
ffillouxdev Apr 17, 2026
f0d8287
refactor(): fix email typo and emphasize with bold and larger font
ffillouxdev Apr 17, 2026
4202142
refactor(): add back font-semibold to warning paragraph
ffillouxdev Apr 17, 2026
d973cff
refactor(): hide carousel navigation when 0 or 1 image
ffillouxdev Apr 17, 2026
f76bf86
Add filename/copyright attribution to image carousel counter
ffillouxdev Apr 17, 2026
8419b6a
Remove /datas directory (confidential data should not be published)
ffillouxdev Apr 17, 2026
5f83d02
Configure Tailwind CSS v3 with local build
ffillouxdev Apr 17, 2026
f6e88ed
Fix HMR WebSocket configuration for Vite dev server
ffillouxdev Apr 17, 2026
6e16125
Fix service worker returning undefined for uncached resources
ffillouxdev Apr 17, 2026
1ba3bb6
fix(tailwind): add postcss config to process @tailwind directives
ffillouxdev Apr 17, 2026
0e92293
feat(data): add other_causes category for non-disease items
ffillouxdev Apr 20, 2026
9c279aa
feat(sw): improve service worker caching strategy and add image preca…
ffillouxdev Apr 20, 2026
c2511cd
fix(catalogue): make list view robust in mobile landscape orientation
ffillouxdev Apr 20, 2026
e66f2ac
fix(catalogue): ensure list view has minimum height on all orientations
ffillouxdev Apr 20, 2026
04ed425
fix(catalogue): improve layout for mobile landscape orientation
ffillouxdev Apr 20, 2026
06fd96e
chore(catalogue): remove debug background class
ffillouxdev Apr 20, 2026
88417c8
fix(catalogue): use h-screen for portrait height
ffillouxdev Apr 20, 2026
cab5bb0
fix(catalogue): scope landscape/portrait heights to mobile only
ffillouxdev Apr 20, 2026
5eb9294
fix(catalogue): use height-based media query for landscape mobile
ffillouxdev Apr 20, 2026
2c25da7
feat(legal): add copyright notice for CIRAD content and images
ffillouxdev Apr 20, 2026
fb7b974
feat(router): add back button to not found page
ffillouxdev Apr 20, 2026
ae4d910
docs: add README and ARCHITECTURE documentation
ffillouxdev Apr 20, 2026
c880810
perf: add lazy loading to images and create useT() helper
ffillouxdev Apr 20, 2026
c96ae24
fix(catalogue): correct class name when toggling main height on resul…
ffillouxdev Apr 21, 2026
2406560
fix: remove unused i18next default imports
ffillouxdev Apr 21, 2026
e08d86c
fix(preview): expose SPA server on 0.0.0.0:5174
ffillouxdev Apr 21, 2026
eaeeaa0
feat: format carousel filename, update contact phrase and email, fix …
ffillouxdev Apr 21, 2026
29bdae3
feat: show formatted filename below single-image diseases
ffillouxdev Apr 21, 2026
4e4b68d
fix: remove trailing dot in carousel filename format
ffillouxdev Apr 21, 2026
baa6315
feat(breadcrumb): increase size to text-xs sm:text-sm, bold current i…
ffillouxdev Apr 21, 2026
f33ca1a
feat(pathogen): render *italic* markers from JSON, apply in catalogue…
ffillouxdev Apr 23, 2026
bdc5faf
fix(carousel): center prev/next arrows on image frame, not caption
ffillouxdev Apr 23, 2026
23735e3
feat(questionnaire): add prev question button above breadcrumb
ffillouxdev Apr 23, 2026
3afbaa4
feat(about): add /about page accessible from the More menu
ffillouxdev Apr 23, 2026
3bf4e40
fix(menu): close dropdown only on outside click, add image download p…
ffillouxdev Apr 23, 2026
d2853db
fix(carousel): add image counter, robust photographer extension cleanup
ffillouxdev May 4, 2026
550e78a
feat(catalog): exclude no convincing result from catalog list
ffillouxdev May 4, 2026
3e08a34
fix(readme): remove reference to deleted CONTRIBUTING.md
ffillouxdev May 18, 2026
41e1728
docs(readme): remove outdated browser support section
ffillouxdev May 18, 2026
0dc9ca1
feat(branding): rename app to CaneDr
ffillouxdev May 21, 2026
b4b4ec0
feat(assets): add CIRAD SVG logo, update favicon, process main-bg to …
ffillouxdev May 21, 2026
838b567
feat(fonts): integrate Frutiger LT Pro for headings and Myriad Pro fo…
ffillouxdev May 21, 2026
d26bc18
feat(about): rewrite content with official text, add credits with Git…
ffillouxdev May 21, 2026
2bcdd18
feat(ui): add CIRAD corner badge on home, white cards on policy pages…
ffillouxdev May 21, 2026
b81dccc
feat(catalog): add CIRAD badge in CTA, hide pathogen column when filt…
ffillouxdev May 21, 2026
f3a8120
fix(result): darken geo-zone separators, add contact email to no-imag…
ffillouxdev May 21, 2026
7aa11eb
fix(catalog): replace pathogen header with × when only other causes v…
ffillouxdev May 21, 2026
fc0443b
fix(result): center no-image message in photo frame, add line break b…
ffillouxdev May 21, 2026
13edbd8
feat(favicon): replace SVG favicon with favicon.ico
ffillouxdev May 21, 2026
77c1699
perf: precache vite bundles for offline, render header once, harden r…
ffillouxdev May 21, 2026
a4b026f
docs: refresh architecture and readme after perf refactor
ffillouxdev May 21, 2026
aa09113
ci: add github actions, dependabot, and post-build smoke test
ffillouxdev May 21, 2026
10f69d3
fix(router): re-render header inside each view to keep active link in…
ffillouxdev May 21, 2026
f215703
fix(result): shorten result_<name> label by stripping parentheticals
ffillouxdev May 21, 2026
c94e323
ci: add SonarCloud analysis, fix FOUC, add contact section
ffillouxdev Jun 1, 2026
dde5731
ci(sonar): add workflow_dispatch trigger
ffillouxdev Jun 1, 2026
6abb69d
feat(i18n): translate result prefix — Result/Résultat/Resultado
ffillouxdev Jun 1, 2026
3e420e9
fix(sonar): fix all 19 SonarCloud issues
ffillouxdev Jun 1, 2026
8eca473
ci(sonar): exclude i18n.ts from duplication analysis
ffillouxdev Jun 1, 2026
9d335d1
ci(sonar): disable quality gate blocking — free plan has no custom QG
ffillouxdev Jun 1, 2026
b86a891
ci(sonar): continue-on-error so QG never blocks CI
ffillouxdev Jun 1, 2026
8171dbf
ci(sonar): pass qualitygate.wait=false as action arg to suppress QG c…
ffillouxdev Jun 1, 2026
87b9249
ci(sonar): exclude all files from coverage (no tests in project)
ffillouxdev Jun 1, 2026
c16ad0f
ci(sonar): exclude repetitive views from duplication check
ffillouxdev Jun 1, 2026
9ae7e02
fix(sonar): remove ReDoS-vulnerable regex, split into two safe steps
ffillouxdev Jun 1, 2026
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
27 changes: 27 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
version: 2
updates:
# npm dependencies
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
labels:
- dependencies
commit-message:
prefix: chore(deps)
include: scope

# GitHub Actions workflow updates
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 3
labels:
- dependencies
- ci
commit-message:
prefix: chore(ci)
47 changes: 47 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: CI

on:
push:
branches: [main, claude]
pull_request:
branches: [main]

jobs:
build:
name: Type-check & build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Build (tsc + vite)
run: npm run build

- name: Verify build artefacts
run: npm run verify

audit:
name: npm audit (critical only)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Audit
run: npm audit --audit-level=critical
32 changes: 32 additions & 0 deletions .github/workflows/sonar.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: SonarCloud

on:
push:
branches: [main, claude]
pull_request:
branches: [main]
workflow_dispatch:

jobs:
sonar:
name: Analyze
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- run: npm ci

- uses: SonarSource/sonarcloud-github-action@master
continue-on-error: true
with:
args: -Dsonar.qualitygate.wait=false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
31 changes: 11 additions & 20 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

# Dependencies
node_modules
package-lock.json

# Build output
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
# Confidential assets
public/datas/

# IDE
.vscode
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.swp
CLAUDE.md
203 changes: 203 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# Architecture Overview

D-CAS 2.0 is a client-side, framework-free single-page application built with TypeScript, Vite, and Tailwind CSS. This document describes how the system is organized and how its key features work.

## Directory Structure

```
src/
├── main.ts # Entry point: renders header into #header-mount,
│ # initializes offline support & router
├── router.ts # Client-side router & route definitions
├── layout.ts # Re-exports header / bindHeaderEvents / initOffline
├── style.css # Tailwind directives
├── i18n.ts # Translation resources (EN/FR/ES) & language detection
├── views/ # Page-level components (return HTML strings — no header)
│ ├── home.ts # Interactive questionnaire
│ ├── catalog.ts # Disease list with search & sorting
│ ├── about.ts # About page (credits, CIRAD info)
│ ├── privacy.ts # Privacy policy
│ └── legal.ts # Legal notice & copyright
├── components/ # Reusable UI pieces (return HTML strings)
│ ├── header.ts # Navigation, language selector, offline toggle
│ ├── questionnaire.ts # Question tree navigation & disease results
│ ├── disease_result.ts # Disease detail view with carousel
│ ├── call_to_action.ts # CTA for sugarcane diseases book
│ ├── cirad_corner.ts # CIRAD corner logo (home page)
│ ├── breadcrumb.ts # Navigation breadcrumb
│ └── question_button.ts # Styled question button
├── data/
│ └── key-loader.ts # Loads & caches disease JSON from public/datas/
└── vite-env.d.ts # Vite type definitions
```

`index.html` has two mount points: `#header-mount` (header rendered once at boot, re-rendered only on language change) and `#app` (route content, swapped on navigation).

## Routing

**Client-side router** in `router.ts` with three-language URL aliases. Each route may declare an optional `init` callback for post-render wiring:

```ts
type Route = {
path: string
titleKey: string
view: () => string
init?: () => void
}

const routes: Route[] = [
{ path: '/', titleKey: 'home.title', view: homeView, init: initQuestionnaire },
{ path: '/catalogue', titleKey: 'catalogue.title', view: catalogueView, init: initCatalogue },
{ path: '/catalog', titleKey: 'catalogue.title', view: catalogueView, init: initCatalogue },
{ path: '/catalogo', titleKey: 'catalogue.title', view: catalogueView, init: initCatalogue },
// ... etc for about, privacy, legal
]
```

**Navigation flow:**
1. User clicks an internal `<a>` tag.
2. Router intercepts the click (skipping `target="_blank"`, `download`, modifier keys, `mailto:`, `tel:` and absolute URLs), prevents default, calls `navigateTo()`.
3. `navigateTo()` pushes state to history and re-renders the main content (`app.innerHTML = route.view()`).
4. `route.init?.()` runs after the view is in the DOM.
5. `window.popstate` listener handles back/forward buttons.

The header is **not** part of `route.view()` — it lives in its own `#header-mount` and is only re-rendered when the language changes.

## Internationalization (i18n)

**Language detection** in `i18n.ts`:
1. Browser's language detector (`i18next-browser-languagedetector`) reads `navigator.language`.
2. Falls back to English if user's language is not supported.

**Translation resources:**
- Single object with three languages (EN, FR, ES).
- Nested keys: `t('catalogue.diseaseNameColumn')`.
- All visible text must be added here — never hardcoded in views.

**Changing language:**
- User selects from dropdown in header.
- `i18next.changeLanguage()` updates the language.
- `main.ts`'s `refreshHeader()` re-renders the header into `#header-mount`, then `router.render()` re-renders the current route's main content.
- Service Worker is notified to precache the selected language's image assets.

**URL paths are localized:**
```
/catalogue (English)
/catalogo (Spanish)
/catalogue (French, same as English by convention)
```

The header component (`header.ts`) maps these paths based on current language via `lp(key)` helper.

## Data Loading & Caching

**Disease data** is loaded from JSON files in `public/datas/`:

```
identification-key.json (English)
cle-identification.json (French)
clave-de-identificacion.json (Spanish)
diseases-img/ (Disease photographs)
```

**Key loader** (`data/key-loader.ts`):
- Single fetch per session (cached in memory).
- If a translated key file is empty, falls back to English.
- Used by both questionnaire and catalog views.

## Offline Support (Service Worker)

**Service Worker** in `public/sw.js`:

1. **App shell** (HTML, identification keys, favicon) — precached at install time.
2. **Vite bundles** (hashed JS/CSS) — at install time, the SW fetches `/.vite/manifest.json` (generated by `build.manifest: true` in `vite.config.ts`) and precaches every entry's `file`, `css`, and `assets`. This guarantees the app works fully offline on first visit.
3. **Disease images** — precached on demand when the user picks a language (or after initial SW registration). Workers run in parallel (6 concurrent fetches) with an in-progress guard per language so rapid language toggles don't queue duplicate runs.
4. **Runtime strategy** — images (`/datas/diseases-img/*` and `/assets/*.{png,jpg,jpeg,webp,svg,gif,ico}`) use cache-first; everything else uses network-first with cache fallback.
5. **Cache busting** — bump `CACHE_NAME` in `sw.js` when the caching strategy changes. Old caches are deleted on activation.

**Image precaching progress:**
- SW broadcasts `{ type: 'precache-progress', done, total }` to all clients every 10 images.
- Header dropdown shows the progress bar and "Downloading 45/120" / "Ready for offline use ✓".

**User can toggle offline mode** via header switch (`offline-toggle`). This registers or unregisters the SW and wipes its caches.

## Component Patterns

All views and components follow the same pattern:

```ts
export function myComponent(): string {
const t = i18next.t.bind(i18next)

return /*html*/`
<div class="...">
${t('my.key')}
</div>
`
}

export function initMyComponent(): void {
// Set up event listeners
document.querySelector('.my-class')?.addEventListener('click', ...)
}
```

**Why this pattern?**
- No JSX, no virtual DOM, no build complexity.
- Easy to modify styling (Tailwind utilities in the HTML template).
- Event listeners bound explicitly (no magic).
- Type-safe (TypeScript strict mode).

## Adding a New Page

1. **Create view** in `src/views/mypage.ts`:
```ts
export function myPageView(): string { return `...` }
export function initMyPage(): void { /* listeners */ }
```

2. **Add translations** to `src/i18n.ts` (EN, FR, ES).

3. **Register route** in `src/router.ts` (use the `init?` field to wire post-render listeners):
```ts
{ path: '/mypage', titleKey: 'mypage.title', view: myPageView, init: initMyPage }
```

4. **Add navigation link** in `src/components/header.ts` (if needed).

## Key Technical Decisions

| Decision | Rationale |
| -------- | --------- |
| No framework | Minimal bundle, easy to modify, direct DOM control |
| TypeScript strict | Catch errors at build time, no `any` escapes |
| Tailwind CSS | Responsive design, utility-first, zero unused CSS |
| Service Worker | Full offline support for field use |
| i18next | Standard i18n library, easy to add languages |
| String HTML templates | No build step for views, fast iteration |
| Client-side router | No backend needed, static hosting compatible |

## Performance Considerations

- **Initial load** — ~200KB gzipped (Vite optimizes JS, Tailwind purges unused classes).
- **Disease images** — Downloaded on-demand by Service Worker, cached for offline use.
- **Catalog search** — Linear scan of disease list (fast enough for <200 diseases).
- **Mobile landscape** — Special height handling (`max-md:landscape:h-[200vh]`) to prevent table clipping.

## Confidential Assets

`public/datas/` is **gitignored** and must be provided by CIRAD. Without it:
- Questionnaire loads empty data (shows "Downloading...").
- Catalog shows empty list.
- Images are not available.

The app degrades gracefully; it doesn't crash.

---

**Last updated:** May 2026
**Maintained by:** [ffillouxdev](https://github.com/ffillouxdev)
Loading
Loading