✨ Add XP leaderboard: global/friends, month/year (G7) - #190
Merged
Conversation
Ranks by summed XP over a calendar month or year, gated behind both SOCIAL_ENABLED and GAMIFICATION_ENABLED (new GamificationFeatureGuard, same 404-not-403 pattern as SocialFeatureGuard). Lives in SocialModule rather than GamificationModule to avoid a circular import (Social already imports Gamification for AchievementService), and leans on a new FollowService.listFriendIds() for the friends scope. Ranking happens in JS rather than a SQL window function (no raw query exists elsewhere in this codebase): ties share a rank and the next distinct rank skips ahead by the tie's size, older account wins a tie. Top 100 shown, viewer's own row returned separately when it falls outside that cut. A PRIVATE row's real avatar only shows to a viewer who follows them — everyone else sees the same identicon a public account without a photo would show, so a leaderboard row never signals that an account is private. Frontend: new /app/leaderboard page (tabs, month/year filter, skeleton, animate:flip), a crown nav entry between Stats and Feed (gamification flag added alongside the existing social one across every nav skin), a "Voir le classement" link in the profile's XP card, and "Nouveau" badges on both the achievements and leaderboard page titles. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
The approved mockup ("Bobine") had a gold/silver/bronze gradient tint
+ colored rank number on the top 3, and a left accent stripe on your
own row — both were lost between the mockup and the actual page, which
shipped with a plain flat list. Reuses the existing tier-gold/silver/
bronze tokens (same ones the achievement medallions use) rather than
inventing new colors. Divider switched to dashed to match the
"pellicule" look too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The XP figure was one flat string ("4 820 XP") — split it into a bold,
larger number and a small dim "xp" unit, matching the mockup. Row
hover also had no transition (background snapped instantly); added a
150ms color transition.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A brand-new account got to ~level 2.8 the moment it added its very first item: WORK_ADDED (2 XP) + DOMAIN_STARTED (100 XP, a one-off per-domain milestone) = 102 XP, comfortably past the old level 2 threshold (52 XP). Rather than shrink DOMAIN_STARTED, raised LEVEL_BASE 40 -> 100 (LEVEL_STEP unchanged), so level 2 now costs 112 XP — the single biggest freebie in the game no longer levels you up by itself. LEVEL_CAP_LEVEL/XP_AT_CAP_LEVEL recomputed to keep the quadratic and capped branches meeting exactly at the boundary (now 75 -> 76 instead of 80 -> 81), and level.util.spec.ts's thresholds updated to match — all 11 cases still pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ader for consistency
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ranks by summed XP over a calendar month or year, in two scopes: global and friends. Gated behind both
SOCIAL_ENABLEDandGAMIFICATION_ENABLED(newGamificationFeatureGuard, same 404-not-403 pattern asSocialFeatureGuard) — a leaderboard needs XP to exist and other users to rank against, so either flag being off hides the whole surface.Ranking happens in plain JS rather than a SQL window function (no raw query exists anywhere else in this codebase): ties share a rank and the next distinct rank skips ahead by the tie's size (1, 2, 2, 4), older account wins a tie. Top 100 shown; the viewer's own row is returned separately when it falls outside that cut, never duplicated. A PRIVATE row's real avatar only shows to a viewer who follows them — everyone else sees the same identicon a public account without a photo would show, so a row never signals that an account is private.
Frontend: new
/app/leaderboardpage (tabs, month/year filter, loading skeleton,animate:flip), a crown nav entry between Stats and Feed (added agamificationgate alongside the existingsocialone across every nav skin — rail, bottom bar, filmstrip, launcher sheet), a "Voir le classement" link in the profile's XP card (own profile only), and "Nouveau" badges on both the achievements and leaderboard page titles.Test plan
LeaderboardService(tie ranking, Top 100 cutoff, private-avatar masking, GHOST/hideProgressionexclusion, friends scope, period boundaries) — all passing.pnpm --recursive check(pre-push hook) passes clean acrosspackages/shared,apps/api,apps/web./app/leaderboard(tabs, period filter, own-row highlight, deep link to/app/u/[username]) and the new profile card link, both themes, mobile viewport.Related
[G7] on Quackback's internal roadmap.
Checklist
frandenmessage catalogspnpm build:packagerun (packages/shared changed)schema.prismachanged — n/a, no schema change--no-verify— hooks ran clean🤖 Generated with Claude Code