feat(profile): a full identity system — name styles, decorations, effects, nameplates, tag, links, board
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s

The owner asked for profile customisation that is "completo e FODA", pointing
at Discord's profile editor. This is the whole thing, end to end, with a live
preview.

What a user can now set, and where it shows:

  - Display name style: 10 fonts × 9 effects (solid, gradient, neon, toon,
    pop, gummy, prism, glitch, fire) × two colours. Popout and full profile.
  - Avatar decoration: 10 (ring, neon, flames, stars, halo, vines, pixel,
    hearts, frost, crown). Inline SVG — no image assets, scales to any size.
  - Profile effect over the banner: 9 (sparkles, rain, snow, matrix,
    confetti, aurora, embers, bubbles, hearts). CSS particles with seeded
    positions so re-renders do not change the weather.
  - Nameplate behind the member-list row: 10 presets + custom two-colour.
  - Theme: two colours washed behind the card, and the banner fallback.
  - Pronouns, and a clan-style tag (emoji + 2–4 letters) beside the name,
    also on the member row.
  - Links, up to 5, with the brand icon picked from the host.
  - Board: favourite game, up to 5 in rotation, up to 10 wanted, a quote.
    Games come from Steam search through a small server proxy (cached,
    10 min) or are typed by hand; covers only load from Steam's CDN.
  - Badges, derived rather than stored: instance admin, server owner,
    founder (joined in the space's first month), veteran (a year), OG (six
    months), stylist. Nothing to grant, revoke or migrate.

Storage is one JSON column, users.profile_style (migration 0019), and one
normaliser in shared/profile.ts that both the server (on write and on read)
and the client (on render) run. It never throws: unknown ids fall back,
strings are cut to their limits, colours must be #rrggbb, link URLs must be
https, cover URLs must be a Steam CDN host. Kept out of the federation
durable-field set on purpose — peers on older builds do not know the field,
and it is cosmetic.

The editor is a new Settings > Profile tab. Its preview is the same
ProfileCard component the popout renders, with the draft style swapped in —
what you see while editing is, by construction, what others see.

Fonts are OFL builds self-hosted from public/fonts alongside DM Sans, latin
subsets only, ~120 KB in total.

Verified: web build, 517 web tests (11 new: normaliser bounds and fallbacks,
link icon detection, badge derivation), server typecheck, 804 server tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 23:36:23 -03:00
co-authored by Claude Fable 5.1
parent 872066e5be
commit ef9c73501c
44 changed files with 6441 additions and 116 deletions
+109
View File
@@ -1032,6 +1032,115 @@ export const en = {
'system.pinned.seeAll': 'See all pinned messages.',
'system.preview.pinned': '{actor} pinned a message',
'join.page.back': 'Back to Resenhacord',
// Profile identity — editor, catalogs, badges, widgets
'settings.tab.profile': 'Profile',
'profile.tab.board': 'Board',
'profile.links': 'Links',
'profile.viewFull': 'View Full Profile',
'profile.editor.title': 'Profile',
'profile.editor.subtitle': 'Make it yours. Everything here shows on your profile card, the full profile and the member list.',
'profile.editor.preview': 'Preview',
'profile.editor.preview.hint': 'This is exactly what others see when they click your name.',
'profile.editor.name': 'Display name style',
'profile.editor.name.hint': 'Font, effect and colours for your name on your profile.',
'profile.editor.font': 'Font',
'profile.editor.effect': 'Effect',
'profile.editor.colors': 'Colours',
'profile.editor.decoration': 'Avatar decoration',
'profile.editor.profileEffect': 'Profile effect',
'profile.editor.nameplate': 'Nameplate',
'profile.editor.nameplate.hint': 'The background behind your row in the member list.',
'profile.editor.theme': 'Theme',
'profile.editor.theme.hint': 'Two colours washed behind your card and used when you have no banner.',
'profile.editor.theme.none': 'Default',
'profile.editor.identity': 'Identity',
'profile.editor.pronouns': 'Pronouns',
'profile.editor.pronouns.placeholder': 'he/him, she/her, they/them…',
'profile.editor.tag': 'Tag',
'profile.editor.tag.hint': '2–4 letters shown in a chip beside your name, with an optional emoji.',
'profile.editor.links': 'Links',
'profile.editor.links.hint': 'Up to 5. The icon is picked from the site automatically.',
'profile.editor.linkLabel': 'Label',
'profile.editor.addLink': 'Add link',
'profile.editor.board': 'Board',
'profile.editor.board.hint': 'Games and a quote, shown on the Board tab of your full profile.',
'profile.editor.searchGame': 'Search a game on Steam, or type a title and press Enter',
'profile.editor.searching': 'Searching…',
'profile.editor.useTyped': 'Use "{name}" as typed',
'profile.editor.gameNote': 'Why this one? (optional)',
'profile.editor.gameTags': 'Tags, comma-separated (up to 3)',
'profile.editor.quote': 'Quote',
'profile.editor.quote.placeholder': 'A line that says something about you…',
'profile.editor.quote.author': 'Who said it (optional)',
'profile.editor.remove': 'Remove',
'profile.editor.save': 'Save',
'profile.editor.saving': 'Saving...',
'profile.editor.saved': 'Profile updated',
'profile.editor.saveFailed': 'Could not save the profile',
'profile.editor.discard': 'Discard',
'profile.editor.resetAll': 'Reset everything',
'profile.font.default': 'Default',
'profile.font.display': 'Display',
'profile.font.comic': 'Comic',
'profile.font.script': 'Script',
'profile.font.pixel': 'Pixel',
'profile.font.future': 'Future',
'profile.font.serif': 'Serif',
'profile.font.mono': 'Mono',
'profile.font.round': 'Round',
'profile.font.marker': 'Marker',
'profile.effect.solid': 'Solid',
'profile.effect.gradient': 'Gradient',
'profile.effect.neon': 'Neon',
'profile.effect.toon': 'Toon',
'profile.effect.pop': 'Pop',
'profile.effect.gummy': 'Gummy',
'profile.effect.prism': 'Prism',
'profile.effect.glitch': 'Glitch',
'profile.effect.fire': 'Fire',
'profile.decoration.none': 'None',
'profile.decoration.ring': 'Ring',
'profile.decoration.neon': 'Neon',
'profile.decoration.flames': 'Flames',
'profile.decoration.stars': 'Stars',
'profile.decoration.halo': 'Halo',
'profile.decoration.vines': 'Vines',
'profile.decoration.pixel': 'Pixel',
'profile.decoration.hearts': 'Hearts',
'profile.decoration.frost': 'Frost',
'profile.decoration.crown': 'Crown',
'profile.profileEffect.none': 'None',
'profile.profileEffect.sparkles': 'Sparkles',
'profile.profileEffect.rain': 'Rain',
'profile.profileEffect.snow': 'Snow',
'profile.profileEffect.matrix': 'Matrix',
'profile.profileEffect.confetti': 'Confetti',
'profile.profileEffect.aurora': 'Aurora',
'profile.profileEffect.embers': 'Embers',
'profile.profileEffect.bubbles': 'Bubbles',
'profile.profileEffect.hearts': 'Hearts',
'profile.nameplate.none': 'None',
'profile.nameplate.sunset': 'Sunset',
'profile.nameplate.ocean': 'Ocean',
'profile.nameplate.neon': 'Neon',
'profile.nameplate.forest': 'Forest',
'profile.nameplate.royal': 'Royal',
'profile.nameplate.brasil': 'Brasil',
'profile.nameplate.cyber': 'Cyber',
'profile.nameplate.blossom': 'Blossom',
'profile.nameplate.midnight': 'Midnight',
'profile.nameplate.custom': 'Custom',
'profile.badge.admin': 'Instance admin',
'profile.badge.owner': 'Server owner',
'profile.badge.founder': 'Founding member — joined in the first month',
'profile.badge.veteran': 'Veteran — over a year here',
'profile.badge.og': 'OG — over six months here',
'profile.badge.stylist': 'Stylist — customised profile',
'profile.widgets.collection': 'Game Collection',
'profile.widgets.favoriteGame': 'Favorite Game',
'profile.widgets.rotation': 'Games in Rotation',
'profile.widgets.wishlist': 'Want to Play',
'profile.widgets.empty': 'Nothing on the board yet.',
} as const;
export type TranslationKey = keyof typeof en;