Skip to content

Commit 1073960

Browse files
authored
Merge pull request #4 from Johnpii1/codex/add-fallback-avatar-color-hashing
Add deterministic fallback avatar colors by creator id
2 parents eebd021 + 0213bcd commit 1073960

6 files changed

Lines changed: 63 additions & 2 deletions

File tree

src/components/common/CreatorCard.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ const CreatorCard: React.FC<CreatorCardProps> = ({ creator, className }) => {
108108
<div className="relative mb-4 aspect-square overflow-hidden rounded-xl">
109109
<CreatorInitialsAvatar
110110
name={creator.title}
111+
creatorId={creator.id}
111112
imageSrc={creator.thumbnail}
112113
imageClassName="transition-transform duration-500 md:group-hover:scale-[1.03]"
113114
/>

src/components/common/CreatorInitialsAvatar.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { useState } from 'react';
22
import { cn } from '@/lib/utils';
3+
import { getFallbackAvatarColors } from '@/utils/avatarColor.util';
34

45
interface CreatorInitialsAvatarProps {
56
name: string;
7+
creatorId?: string | number | null;
68
imageSrc?: string;
79
className?: string;
810
imageClassName?: string;
@@ -24,20 +26,26 @@ const getInitials = (name: string) => {
2426

2527
const CreatorInitialsAvatar: React.FC<CreatorInitialsAvatarProps> = ({
2628
name,
29+
creatorId,
2730
imageSrc,
2831
className,
2932
imageClassName,
3033
}) => {
3134
const [hasError, setHasError] = useState(false);
3235
const initials = getInitials(name);
36+
const fallbackColors = getFallbackAvatarColors(creatorId);
3337

3438
if (!imageSrc || hasError) {
3539
return (
3640
<div
3741
className={cn(
38-
'flex size-full items-center justify-center bg-gradient-to-br from-slate-800 via-slate-700 to-amber-700 text-3xl font-black tracking-wide text-white/95',
42+
'flex size-full items-center justify-center text-3xl font-black tracking-wide',
3943
className
4044
)}
45+
style={{
46+
background: fallbackColors.background,
47+
color: fallbackColors.textColor,
48+
}}
4149
>
4250
<span aria-label={`${name} initials avatar`}>{initials}</span>
4351
</div>

src/components/common/CreatorProfileHeader.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import CreatorInitialsAvatar from '@/components/common/CreatorInitialsAvatar';
99
interface CreatorProfileHeaderProps {
1010
name: string;
1111
handle: string;
12+
creatorId?: string | number | null;
1213
avatarUrl?: string;
1314
isVerified?: boolean;
1415
className?: string;
@@ -17,6 +18,7 @@ interface CreatorProfileHeaderProps {
1718
const CreatorProfileHeader: React.FC<CreatorProfileHeaderProps> = ({
1819
name,
1920
handle,
21+
creatorId,
2022
avatarUrl,
2123
isVerified,
2224
className,
@@ -44,7 +46,7 @@ const CreatorProfileHeader: React.FC<CreatorProfileHeaderProps> = ({
4446
>
4547
<div className="flex flex-col gap-4 md:flex-row md:items-center md:gap-6">
4648
<div className="size-24 overflow-hidden rounded-2xl border-4 border-white/10 shadow-xl md:size-32">
47-
<CreatorInitialsAvatar name={name} imageSrc={avatarUrl} />
49+
<CreatorInitialsAvatar name={name} creatorId={creatorId} imageSrc={avatarUrl} />
4850
</div>
4951
<div className="space-y-1">
5052
<div className="flex items-center gap-2">
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { describe, expect, it } from 'vitest';
3+
import CreatorInitialsAvatar from '@/components/common/CreatorInitialsAvatar';
4+
import { getFallbackAvatarColors } from '@/utils/avatarColor.util';
5+
6+
describe('CreatorInitialsAvatar', () => {
7+
it('uses deterministic fallback colors for the same creator id', () => {
8+
const first = getFallbackAvatarColors('creator-123');
9+
const second = getFallbackAvatarColors('creator-123');
10+
const third = getFallbackAvatarColors('creator-456');
11+
12+
expect(first).toEqual(second);
13+
expect(first.background).not.toBe(third.background);
14+
expect(first.textColor).toBe('rgba(255, 255, 255, 0.95)');
15+
});
16+
17+
it('renders initials fallback with hashed background when image is missing', () => {
18+
render(<CreatorInitialsAvatar name="Alex Rivers" creatorId="creator-123" />);
19+
20+
const initials = screen.getByLabelText('Alex Rivers initials avatar');
21+
const avatar = initials.parentElement;
22+
const colors = getFallbackAvatarColors('creator-123');
23+
24+
expect(initials).toHaveTextContent('AR');
25+
expect(avatar).toHaveStyle({
26+
background: colors.background,
27+
color: colors.textColor,
28+
});
29+
});
30+
});

src/pages/LandingPage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ function LandingPage() {
479479
<CreatorProfileHeader
480480
name="Alex Rivers"
481481
handle="arivers"
482+
creatorId="arivers"
482483
isVerified={true}
483484
avatarUrl="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop"
484485
/>

src/utils/avatarColor.util.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const stringHash = (value: string) => {
2+
let hash = 0;
3+
for (let i = 0; i < value.length; i += 1) {
4+
hash = (hash * 31 + value.charCodeAt(i)) >>> 0;
5+
}
6+
return hash;
7+
};
8+
9+
export const getFallbackAvatarColors = (creatorId?: string | number | null) => {
10+
const normalizedId = String(creatorId ?? '').trim();
11+
const hash = stringHash(normalizedId || 'fallback-avatar');
12+
const baseHue = hash % 360;
13+
const accentHue = (baseHue + 28) % 360;
14+
15+
return {
16+
background: `linear-gradient(135deg, hsl(${baseHue} 65% 28%), hsl(${accentHue} 72% 36%))`,
17+
textColor: 'rgba(255, 255, 255, 0.95)',
18+
};
19+
};

0 commit comments

Comments
 (0)