feat: graceful avatar fallback with deterministic initials in TournamentParticipants#624
Open
pitah23 wants to merge 3 commits into
Open
feat: graceful avatar fallback with deterministic initials in TournamentParticipants#624pitah23 wants to merge 3 commits into
pitah23 wants to merge 3 commits into
Conversation
…to ProfileBio - Define MAX_BIO_LENGTH (280) in src/lib/validations/profile.ts as the single source of truth, shared by the Zod schema and all UI components - Update ProfileBio.tsx with a live character counter (currentLength / MAX), warning style (text-destructive) at ≥90% capacity, aria-live announcement, and inline Zod-driven error that blocks Save when the limit is exceeded - Update profile/edit/page.tsx to import MAX_BIO_LENGTH from the shared location, removing the local 500-char constant that mismatched the server - Add accessible label/id to the bio textarea in the edit page - Fix broken import path in profile-edit.test.tsx and add useSearchParams mock - Add profile-bio.test.tsx with 16 tests covering counter updates, 90% warning threshold, submission blocking, successful save, and constant/schema consistency
- Add storageKey="theme" to ThemeProvider so next-themes writes to and reads from the "theme" localStorage key (defaultTheme="system", enableSystem, and suppressHydrationWarning were already in place) - Fix both ThemeToggle components to use resolvedTheme for the toggle decision, which correctly handles "system" mode (previously used the raw theme value, so system+dark-OS would not toggle to light) - Add mounted guard to both ThemeToggle components: SSR renders a stable placeholder button (no Sun/Moon icons, no onClick) so users never see a flash of the wrong icon before client hydration - Wire ThemeSelector to useTheme so its active-mode highlight reflects the persisted next-themes state instead of the stale settings prop; mode selection now calls setTheme (persistence) and onUpdate (app state) together; add same mounted guard for hydration safety Tests (27) in theme-persistence.test.tsx: - ThemeProvider forwards storageKey, defaultTheme, enableSystem correctly - localStorage: selected theme stored under "theme", restored on remount, falls back to system when no preference is present - ThemeToggle (both): light↔dark toggle, system+OS-pref toggle, SSR output verified via renderToStaticMarkup (placeholder, no onclick) - ThemeSelector: active mode from useTheme not settings, setTheme+onUpdate called together, correct highlight for all three modes - Cross-component sync: ThemeToggle and ThemeSelector reflect same state
…entParticipants
Replace the static gradient placeholder in TournamentParticipants with a
ParticipantAvatar component that tries to load the real avatar image first
and falls back to an initials-based placeholder only when needed.
What changed:
- Extract ParticipantAvatar component that renders <img> when avatarUrl is
non-empty, and a styled div with the first letter of the username when
avatarUrl is null/empty or the image fails to load
- Add hashUsername (djb2/unsigned-32-bit) + getAvatarColor to map each
username to a stable slot in a 16-color Tailwind palette; all color class
names appear as string literals so Tailwind JIT includes them
- Image load failures set imgFailed state once via onError; the img is then
unmounted so the error callback cannot fire again, preventing repeated
re-renders from repeated failures
- Every <img> carries alt={username}; the initials placeholder carries
role="img" aria-label={username} so screen readers get an equivalent label
in both states
- Add "use client" directive since the component now uses useState
Tests (36) in tournament-participants.test.tsx:
- hashUsername: determinism, distinct values, non-negative int, edge cases
- getAvatarColor: stable per username, always in palette, distributed across names
- ParticipantAvatar valid image: img rendered, src and alt correct, no initials shown
- ParticipantAvatar missing avatar: null/undefined/empty → placeholder + initials
- ParticipantAvatar initials: uppercase first letter, correct character
- ParticipantAvatar accessibility: aria-label on placeholder, alt on img
- ParticipantAvatar image failure: onError → placeholder shown, img removed,
correct initial, aria-label preserved
- ParticipantAvatar color: palette class applied, same class across renders,
matches getAvatarColor output
- TournamentParticipants integration: renders without crash, heading, one
avatar per participant, slot counts, full/available states
|
@pitah23 is attempting to deploy a commit to the paul joseph's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
@pitah23 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
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.
Closes #548
Summary
TournamentParticipantspreviously rendered every avatar as a hardcoded gradient div with the player's first letter — ignoring theavatarURL field entirely. This PR replaces that with aParticipantAvatarcomponent that:Changes
TournamentParticipants.tsx"use client"useStateAVATAR_COLORS(exported)hashUsername(exported)Math.imulfor 32-bit integer arithmetic,>>> 0for unsigned result — deterministic, no floating-point driftgetAvatarColor(exported)AVATAR_COLORSslot viahashUsername(username) % 16ParticipantAvatar(exported)<img src={avatarUrl} alt={username}>when URL is non-empty; ononErrorsetsimgFailed = true, unmounting the img and switching to the initials div; placeholder carriesrole="img" aria-label={username}<ParticipantAvatar>instead of the old static div__tests__/tournament-participants.test.tsx— 36 testsSee test plan below.
How the fallback works
Once
imgFailedistruethe<img>element is removed from the DOM, so the browser'serrorevent cannot fire again — preventing any re-render loop.Accessibility
<img alt="username"><div role="img" aria-label="username">Both states satisfy WCAG 1.1.1 (Non-text Content) with an equivalent text alternative.
Deterministic color assignment
A given username will receive the same color forever, without any runtime storage or server coordination.
Tests
hashUsername(5)getAvatarColor(4)AVATAR_COLORSParticipantAvatar— valid image (4)<img>whenavatarUrlis providedsrcmatches the provided URLaltattribute equals the usernameParticipantAvatar— missing avatar (4)nullURL → initials placeholder withrole="img"undefinedURL → initials placeholder<img>element in the DOM when URL is nullParticipantAvatar— initials (3)ParticipantAvatar— accessibility (3)role="img"andaria-labelmatching username<img>hasaltequal to username, tag isIMGgetByRole('img', { name: username })resolves for placeholderParticipantAvatar— image load failure (4)fireEvent.error→ initials placeholder appears<img>is removed from DOM after failure (no repeated error)role="img" aria-labelpreserved after failureParticipantAvatar— deterministic color (3)getAvatarColoroutputTournamentParticipantsintegration (6)Test plan
avatarURLsapi.dicebear.com— avatars switch to coloured initials on failure, no console errors about infinite re-renders<img>tags — each has a non-emptyaltattribute equal to the player's username