diff --git a/src/app/components/Avatar.tsx b/src/app/components/Avatar.tsx new file mode 100644 index 0000000..83a3f5d --- /dev/null +++ b/src/app/components/Avatar.tsx @@ -0,0 +1,49 @@ +import {useReducer} from 'react'; + +// Module-level cache shared by every Avatar instance for the life of the +// page. The same user often appears in many places at once (member stacks, +// team panels, admin lists), so once we know a URL is broken we never want +// to flash a fallback again. +const failedAvatarUrls = new Set(); + +export interface AvatarProps { + displayName: string; + avatarUrl?: string | null; + className?: string; +} + +export function Avatar({displayName, avatarUrl, className}: AvatarProps) { + const [, forceRender] = useReducer((count: number) => count + 1, 0); + const failed = Boolean(avatarUrl) && failedAvatarUrls.has(avatarUrl!); + const showImage = Boolean(avatarUrl) && !failed; + + return ( + + {showImage ? ( + { + failedAvatarUrls.add(avatarUrl!); + forceRender(); + }} + /> + ) : ( + initials(displayName) + )} + + ); +} + +function initials(value: string) { + return value + .split(/\s+/) + .slice(0, 2) + .map((part) => part[0]) + .join('') + .toUpperCase(); +} diff --git a/src/app/components/ProjectCard.tsx b/src/app/components/ProjectCard.tsx index 3c1edb2..a12f7c8 100644 --- a/src/app/components/ProjectCard.tsx +++ b/src/app/components/ProjectCard.tsx @@ -1,11 +1,13 @@ import {Link} from 'wouter'; import type {ProjectSummary} from '../../shared/projects'; +import {Avatar} from './Avatar'; import {Markdown} from './Markdown'; interface ProjectListMember { id: string; displayName: string; + avatarUrl: string | null; } export function ProjectCard({ @@ -139,20 +141,13 @@ function MemberStack({ aria-label={members.map(({displayName}) => displayName).join(', ')} > {members.slice(0, 4).map((member) => ( - - {initials(member.displayName)} - + ))} {members.length > 4 && +{members.length - 4}} ); } - -function initials(value: string) { - return value - .split(/\s+/) - .slice(0, 2) - .map((part) => part[0]) - .join('') - .toUpperCase(); -} diff --git a/src/app/routes/ProjectDetailsPage.tsx b/src/app/routes/ProjectDetailsPage.tsx index 1222567..35fa964 100644 --- a/src/app/routes/ProjectDetailsPage.tsx +++ b/src/app/routes/ProjectDetailsPage.tsx @@ -3,6 +3,7 @@ import {useQuery} from '@tanstack/react-query'; import {Link, useLocation, useParams} from 'wouter'; import {QueryState} from '../components/AppLayout'; +import {Avatar} from '../components/Avatar'; import {Markdown} from '../components/Markdown'; import {ProjectVoting} from '../components/ProjectVoting'; import {useBallotStatus} from '../queries/administration'; @@ -138,7 +139,10 @@ export function ProjectDetailsPage() {