Feature Requests
Archived

[G9] Equipped badge showcase on the profile

Split out of [G5] Achievements screen + showcase (post_01m1f97znnexyrvzqmkdwn0eet) so G5 can ship the achievements page alone. G5 builds the page where badges are browsed; this ticket builds the one place a badge is worn.

Depends on: [G3] (catalogue + badge glyphs), [G5] (achievements screen — the natural place to pick what you equip).

Scope

  • Equipping. A user picks one badge (or a small fixed number — to settle here) among the achievements they have actually unlocked. Stored per user; unequipping is always possible.
  • Display on /app/profile. The equipped badge shows on the owner's own profile and on the profile as seen by other visitors.
  • Gating, non-negotiable — the showcase inherits the rules already in place, it does not invent new ones:

    • hidden entirely when GAMIFICATION_ENABLED is off;
    • hidden per the viewer/target relationship exactly like xp already is — reuse withXp()/fetchXpByUser (apps/api/src/gamification/xp-lookup.util.ts) and the hideProgression flag rather than adding a parallel visibility path;
    • a Figurant (GHOST) shows nothing, same as every other progression signal.
  • Zero-footprint when empty. If nothing is equipped, or the viewer isn't allowed to see it, the profile must render exactly as it does today — no empty slot, no placeholder, no reserved space. This is an explicit requirement, not a nicety.

Open questions to settle at design time

  • One badge or a small showcase of 2-3? A single one is stronger as an identity signal; several turn the profile into a second achievements page, which G5 already is.
  • Where exactly on the profile — next to the pseudonym (like the existing LevelBadge/StreakBadge chips), or as its own block near LevelCard?
  • Does the equipped badge travel with the author line on reviews/comments, like LevelBadge and StreakBadge already do? That's a much bigger surface — probably a separate decision, possibly a "no".
  • Does a tiered achievement equip as its current tier (ring colour follows the tier reached), and what happens visually when the user later reaches the next tier?

Out of scope

The achievements page itself (G5), the unlock bubble (G6), any cosmetic that isn't an already-unlocked achievement — paid or unlockable cosmetics are [B1].

3 Comments

Sign in to comment

L
Logan·1 month ago

Shipped in PR #194, merged to main 2026-09-05 as bdf75b8. Moved to complete.

Design settled at 2026-09-03 (see the comment above): 3 badges max, own scoped box per badge (no card, no tier text — the tier ring alone carries that), equipped from the achievements ladder rather than a separate picker, secret achievements never equippable even once unlocked, a tiered achievement equips as the tier it was equipped at (frozen, since each tier is already its own catalogue key). A mouse-tilt hover effect was added to the badge boxes for a "medallion" feel — extracted as a reusable Svelte action (use:tilt, apps/web/src/lib/actions/tilt.ts) rather than one-off code, following the existing longpress/portal/scrollLock action convention.

Shipped:

  • User.equippedBadgeKeys (up to MAX_EQUIPPED_BADGES = 3), with POST/DELETE /achievements/:key/equip validating: must be unlocked, never secret, never above the limit, idempotent unequip.
  • SocialProfileDto.equippedBadges, gated behind the exact same gamificationEnabled && xpVisible condition as xp — reused, not reimplemented, per the ticket's non-negotiable.
  • The showcase on /app/profile: zero footprint when nothing is equipped, no placeholder.
  • Equip/unequip control on each achievement's ladder row, disabled with an explicit "3 badges max" tooltip once the limit is reached, and a persistent pin indicator on a group's medallion so an equipped badge is recognisable without hovering.

A real QA pass on screenshots caught and fixed 4 issues before merge: a single-tier badge rendering uncolored (missing the established tier ?? "gold" fallback once unlocked), a too-discreet equip button with no hover/active/loading transitions, no glanceable "this one is equipped" state from outside the ladder, and no indication of why a control was disabled at the limit.

Also resolved along the way: a pre-existing knip debt (5 flagged unused exports — LEVEL_BASE/LEVEL_STEP/LEVEL_CAP_COST/XP_RULE_LIST/allAiredEpisodesWatched) that had been blocking CI without a decision — kept via knip's @public tag rather than deleted, since these are intentionally-exported constants with no current caller yet.

Not built: a tooltip with the achievement's unlock description on the ladder's tier label — tried, then reverted as unnecessary (the description is already one click away on the achievements page).

L
Logan·1 month ago

Constraint added after the 2026-09-03 design discussion: never auto-equip a badge, not even "the rarest one" as a default. The equip must always be an explicit user choice.

Reason: the catalogue shipped in G3 contains achievements that are privately revealing by design — double_life (being a Figurant while still writing), one_sided (following someone who doesn't follow back), no_favorites, night_owl. Taken together, 44 conditions read less like a trophy collection and more like a behavioural fingerprint. What makes the showcase safe is precisely that the user picks what they wear; an automatic default would silently broadcast something they never chose to reveal.

Same discussion settled that /app/achievements (G5) shows the viewer's own achievements only, and that exposing another user's full achievements page is deliberately not planned — it would first require a per-entry "publicly exposable" notion that doesn't exist. The showcase covers the real need (showing who you are) without that problem.