V2 + V3 : rôles, points, journal, podium et recadrage photo
V2 — authentification et administration :
- Authentification par email + mot de passe (email confirmé, un compte
par email), abandon de l'email interne dérivé du pseudo.
- Rôles public/judge sur profiles, section Administration (juges
uniquement, vérifiée côté serveur) pour gérer les membres.
- Verrou de pseudo : modifiable une fois par son propriétaire puis figé,
contournable par un juge.
- RLS étendue par un trigger BEFORE UPDATE (enforce_profile_update) pour
verrouiller les colonnes sensibles (role, points, pseudo_locked, pseudo
figé) — la RLS seule ne peut pas exprimer une règle par colonne.
V3 — points, journal, podium, progression, photo :
- RPC award_points() (SECURITY DEFINER) : seul point d'écriture de la
colonne points, vérifie le rôle juge côté serveur, delta signé sans
plancher à 0, trace chaque opération dans points_log.
- Leaderboard temps réel (Supabase Realtime) avec podium top 3
(égalités gérées), flèches de progression (previous_rank), et
contrôles de points juges avec confirmation explicite (plus de
debounce auto) avant envoi.
- Page /journal ("le crieur") : fil live et public des attributions de
points.
- Recadrage photo carré + compression client (react-easy-crop + canvas)
au signup et sur le profil.
- Passe de polish visuel : cartes/boutons cohérents, lien actif dans la
nav, podium retravaillé.
schema.sql, README.md et CLAUDE.md mis à jour en conséquence (schéma
idempotent, instructions SMTP/rôles/migration, conventions RLS+trigger
documentées pour les futures colonnes sensibles).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
export type RankedProfile<T extends { points: number }> = T & { rank: number };
|
||||
|
||||
// Classement "compétition" : les ex-aequo partagent le même rang, et le
|
||||
// rang suivant saute en conséquence (1, 2, 2, 4 — pas 1, 2, 2, 3).
|
||||
export function computeRanks<T extends { points: number }>(profiles: T[]): RankedProfile<T>[] {
|
||||
const sorted = [...profiles].sort((a, b) => b.points - a.points);
|
||||
let lastPoints: number | null = null;
|
||||
let lastRank = 0;
|
||||
|
||||
return sorted.map((profile, index) => {
|
||||
if (lastPoints === null || profile.points !== lastPoints) {
|
||||
lastRank = index + 1;
|
||||
lastPoints = profile.points;
|
||||
}
|
||||
return { ...profile, rank: lastRank };
|
||||
});
|
||||
}
|
||||
|
||||
export type Progress = { direction: "up" | "down" | "same" | "none"; amount: number };
|
||||
|
||||
export function computeProgress(currentRank: number, previousRank: number | null): Progress {
|
||||
if (previousRank == null) {
|
||||
return { direction: "none", amount: 0 };
|
||||
}
|
||||
const amount = previousRank - currentRank;
|
||||
if (amount > 0) return { direction: "up", amount };
|
||||
if (amount < 0) return { direction: "down", amount: -amount };
|
||||
return { direction: "same", amount: 0 };
|
||||
}
|
||||
Reference in New Issue
Block a user