Implémente l'intégralité de la maquette « SLUC Business Club.dc.html » : - Site public : accueil (héro administrable, carrousel membres, agenda avec inscription en ligne, rencontres passées, demande d'adhésion), annuaire avec recherche/filtres/fiche détaillée, page association - Espace membre : connexion, édition de fiche avec aperçu direct, upload logo/photo, statut d'adhésion par saison, changement de mot de passe - Espace admin : tableau de bord, membres (validation par saison), rencontres (CRUD + inscrits + impression + export Excel), inscriptions, catégories, contenu du site Architecture : 3 conteneurs Docker Compose — PostgreSQL 16 (réseau interne, rôle applicatif restreint), API Express (non-root, read-only fs), nginx non privilégié (frontend React + reverse-proxy + CSP stricte). Sécurité : requêtes 100 % paramétrées, bcrypt + JWT httpOnly SameSite=Strict, vérification d'Origin (CSRF), validation zod, rate limiting, uploads vérifiés par octets magiques avec noms aléatoires, aucun secret committé. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o
53 lines
1.5 KiB
JavaScript
53 lines
1.5 KiB
JavaScript
async function request(path, options = {}) {
|
||
const res = await fetch(path, {
|
||
headers: options.body instanceof FormData ? {} : { 'Content-Type': 'application/json' },
|
||
credentials: 'same-origin',
|
||
...options,
|
||
body:
|
||
options.body instanceof FormData
|
||
? options.body
|
||
: options.body
|
||
? JSON.stringify(options.body)
|
||
: undefined,
|
||
});
|
||
let data = null;
|
||
try {
|
||
data = await res.json();
|
||
} catch {
|
||
/* non-JSON response */
|
||
}
|
||
if (!res.ok) {
|
||
const err = new Error(data?.error || `Erreur ${res.status}`);
|
||
err.status = res.status;
|
||
throw err;
|
||
}
|
||
return data;
|
||
}
|
||
|
||
export const api = {
|
||
get: (path) => request(path),
|
||
post: (path, body) => request(path, { method: 'POST', body }),
|
||
put: (path, body) => request(path, { method: 'PUT', body }),
|
||
del: (path) => request(path, { method: 'DELETE' }),
|
||
upload: (path, file) => {
|
||
const fd = new FormData();
|
||
fd.append('file', file);
|
||
return request(path, { method: 'POST', body: fd });
|
||
},
|
||
};
|
||
|
||
const MOIS = ['Jan', 'Fév', 'Mars', 'Avr', 'Mai', 'Juin', 'Juil', 'Août', 'Sept', 'Oct', 'Nov', 'Déc'];
|
||
|
||
export function dateParts(dateStr) {
|
||
const d = new Date(dateStr);
|
||
if (Number.isNaN(d.getTime())) return { jour: '--', mois: '' };
|
||
return { jour: String(d.getUTCDate()).padStart(2, '0'), mois: MOIS[d.getUTCMonth()] };
|
||
}
|
||
|
||
export function seasonLabel(d = new Date()) {
|
||
const y = d.getFullYear();
|
||
return d.getMonth() >= 8 ? `${y}–${y + 1}` : `${y - 1}–${y}`;
|
||
}
|
||
|
||
export const statutLabel = (s) => (s === 'confirmee' ? 'Confirmée' : 'En attente');
|