Jauge offset 62, onglet mensuel renommé, déduction 66% sur boutons libres, champs label noir/vert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-04-21 16:27:30 +02:00
parent 25fe91ea93
commit b8048d24f6
5 changed files with 68 additions and 6 deletions

52
CLAUDE.md Normal file
View file

@ -0,0 +1,52 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project overview
Static PHP site for the Index NGO donation/support campaign (`soutenir.index.ngo`). No build step — PHP files are served directly. The site has two language versions: French (`index.php`) and English (`en/index.php`).
## Architecture
### Content flow
All editorial content is managed via a **Kirby CMS** running in a separate repo (`/Users/adrienpayet/Documents/code/en-cours/index/main/`). The static site fetches content from the Kirby API and caches it locally:
```
Kirby CMS (main repo) → /support-content?lang=fr → includes/cache.php → index.php
```
- **`includes/config.php`** — API URL, token, cache TTL constants
- **`includes/cache.php`** — `getContent(lang)` function: serves cache if fresh (<5 min), otherwise fetches from Kirby API via curl, falls back to stale cache
- **`cache/`** — stores `content.fr.json` and `content.en.json`
### Kirby backend (separate repo)
- **Blueprint**: `/Users/adrienpayet/Documents/code/en-cours/index/main/site/blueprints/pages/support.yml` — defines all editable fields for the support page
- **API route**: `/Users/adrienpayet/Documents/code/en-cours/index/main/site/config/routes/support-content.php` — builds and returns the JSON response consumed by this site; uses `->inline()` on writer fields
When adding a new dynamic field: add it to the blueprint, expose it in the API route, then consume `$data['fieldName']` in the PHP templates.
### Donation gauge
- **`api/donorbox-proxy.php`** — server-side proxy to Donorbox API (Basic Auth). Paginates through all active plans to get `recurring_donors_count`. Caches result in `api/cache/donorbox_data.json` (5 min).
- **`assets/js/donorbox-gauge.js`** — calls the proxy, adds `RECURRING_DONORS_OFFSET` to the real count, updates the gauge CSS variable `--pourcent` and the displayed number. Also sets up 5-minute auto-refresh.
`RECURRING_DONORS_OFFSET` is defined at the top of `donorbox-gauge.js` and represents historic/offline supporters added to the live Donorbox count.
### Donation pad
**`assets/js/donation.js`** — IIFE that:
- Dynamically generates all donation buttons (amounts, after-tax calculations) on `DOMContentLoaded`
- Contains `TRANSLATIONS` object for FR/EN static strings in the donation pad
- Generates Donorbox redirect URLs with UTM passthrough
- Handles monthly/one-off tab switching
Static strings in the donation pad (e.g. "Avec déduction fiscale de 66 %") live in the `TRANSLATIONS` object in this file, not in Kirby.
## Key conventions
- **Writer fields in Kirby**: use `->inline()` in the API route to get inline HTML without block wrappers. For multiline text (e.g. a title with a `<br>`), use a `writer` field with `nodes: false` and `buttons: false` — this preserves line breaks. In the API route, use `->inline()` which converts newlines to `<br>` tags; do **not** use `nl2br()`.
- **`heroHeading` field**: rendered with `<?= $data['heroHeading'] ?>` (no `htmlspecialchars`) because it contains trusted HTML from the writer field.
- **Language**: `document.documentElement.lang` drives JS translations. FR page sets `lang="fr"`, EN page sets `lang="en"`.
- **Cache invalidation**: delete files in `cache/` to force a fresh fetch from Kirby.

View file

@ -14,11 +14,13 @@
afterTax: 'Soit {amount}&#8239;€ après impôts', afterTax: 'Soit {amount}&#8239;€ après impôts',
perMonth: '€/mois', perMonth: '€/mois',
withTaxReduction: 'Avec 66&#8239;% de déduction fiscale', withTaxReduction: 'Avec 66&#8239;% de déduction fiscale',
chooseAmount: 'Choisissez votre montant',
}, },
en: { en: {
afterTax: 'That is {amount}&#8239;€ after tax', afterTax: 'That is {amount}&#8239;€ after tax',
perMonth: '€/month', perMonth: '€/month',
withTaxReduction: 'With 66&#8239;% tax deduction', withTaxReduction: 'With 66&#8239;% tax deduction',
chooseAmount: 'Choose your amount',
}, },
}; };
@ -115,6 +117,10 @@
window.open(generateDonorboxUrl(amount, false), '_blank'); window.open(generateDonorboxUrl(amount, false), '_blank');
}); });
} else { } else {
button.innerHTML = `
<p class="bold">${translate('chooseAmount')}</p>
<p class="small">${translate('withTaxReduction')}</p>
`;
button.addEventListener('click', () => { button.addEventListener('click', () => {
window.open(generateDonorboxUrl(null, false), '_blank'); window.open(generateDonorboxUrl(null, false), '_blank');
}); });
@ -144,6 +150,10 @@
window.open(generateDonorboxUrl(amount, true), '_blank'); window.open(generateDonorboxUrl(amount, true), '_blank');
}); });
} else { } else {
button.innerHTML = `
<p class="bold">${translate('chooseAmount')}</p>
<p class="small">${translate('withTaxReduction')}</p>
`;
button.addEventListener('click', () => { button.addEventListener('click', () => {
window.open(generateDonorboxUrl(null, true), '_blank'); window.open(generateDonorboxUrl(null, true), '_blank');
}); });

View file

@ -2,7 +2,7 @@ const DONORBOX_CONFIG = {
proxyUrl: '/api/donorbox-proxy.php', proxyUrl: '/api/donorbox-proxy.php',
}; };
const RECURRING_DONORS_OFFSET = 98; const RECURRING_DONORS_OFFSET = 62;
const GOAL_SUPPORTERS = 500; const GOAL_SUPPORTERS = 500;
async function fetchDonorboxData() { async function fetchDonorboxData() {

View file

@ -70,8 +70,8 @@ $data = getContent('en');
</div> </div>
<p class="hero-heading"> <p class="hero-heading">
<?= htmlspecialchars($data['heroObjectiveLabel'] ?? '') ?> <?= htmlspecialchars($data['gaugeBlackLabel'] ?? '') ?>
<?php if (!empty($data['heroObjectiveDate'])): ?><br /><strong><?= htmlspecialchars($data['heroObjectiveDate']) ?></strong><?php endif; ?> <?php if (!empty($data['gaugeGreenLabel'])): ?><br /><strong><?= htmlspecialchars($data['gaugeGreenLabel']) ?></strong><?php endif; ?>
</p> </p>
</section> </section>

View file

@ -70,8 +70,8 @@ $data = getContent('fr');
</div> </div>
<p class="hero-heading"> <p class="hero-heading">
<?= htmlspecialchars($data['heroObjectiveLabel'] ?? '') ?> <?= htmlspecialchars($data['gaugeBlackLabel'] ?? '') ?>
<?php if (!empty($data['heroObjectiveDate'])): ?><br /><strong><?= htmlspecialchars($data['heroObjectiveDate']) ?></strong><?php endif; ?> <?php if (!empty($data['gaugeGreenLabel'])): ?><br /><strong><?= htmlspecialchars($data['gaugeGreenLabel']) ?></strong><?php endif; ?>
</p> </p>
</section> </section>
@ -131,7 +131,7 @@ $data = getContent('fr');
<div class="col-right"> <div class="col-right">
<section id="section__donation"> <section id="section__donation">
<nav class="nav--tabs"> <nav class="nav--tabs">
<button class="nav--tabs__btn is-selected" data-tab="monthly">Je donne tous les mois</button> <button class="nav--tabs__btn is-selected" data-tab="monthly">Je deviens Soutien</button>
<button class="nav--tabs__btn" data-tab="one-off">Je donne une fois</button> <button class="nav--tabs__btn" data-tab="one-off">Je donne une fois</button>
</nav> </nav>