Skip to content

Commit 3ebdfca

Browse files
authored
Merge pull request #153 from Johnpii1/issue-133
Issue 133
2 parents 9f359a2 + f5c1a7f commit 3ebdfca

8 files changed

Lines changed: 84 additions & 3 deletions

File tree

‎src/components/common/CreatorCard.tsx‎

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

‎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,
@@ -63,7 +65,7 @@ const CreatorProfileHeader: React.FC<CreatorProfileHeaderProps> = ({
6365
>
6466
<div className="flex flex-col gap-4 md:flex-row md:items-center md:gap-6">
6567
<div className="size-24 overflow-hidden rounded-2xl border-4 border-white/10 shadow-xl md:size-32">
66-
<CreatorInitialsAvatar name={name} imageSrc={avatarUrl} />
68+
<CreatorInitialsAvatar name={name} creatorId={creatorId} imageSrc={avatarUrl} />
6769
</div>
6870
<div className="space-y-1">
6971
<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/components/ui/dialog.test.tsx‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { describe, expect, it } from 'vitest';
3+
4+
import { Dialog, DialogContent } from './dialog';
5+
6+
describe('DialogContent', () => {
7+
it('adds mobile safe-area bottom inset padding and restores desktop spacing', () => {
8+
render(
9+
<Dialog open>
10+
<DialogContent>Dialog body</DialogContent>
11+
</Dialog>
12+
);
13+
14+
const content = screen.getByRole('dialog');
15+
expect(content.className).toContain(
16+
'pb-[calc(1.5rem+env(safe-area-inset-bottom))]'
17+
);
18+
expect(content.className).toContain('sm:pb-6');
19+
});
20+
});

‎src/components/ui/dialog.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ function DialogContent({
5959
<DialogPrimitive.Content
6060
data-slot="dialog-content"
6161
className={cn(
62-
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg',
62+
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 pb-[calc(1.5rem+env(safe-area-inset-bottom))] shadow-lg duration-200 outline-none sm:max-w-lg sm:pb-6',
6363
className
6464
)}
6565
{...props}

‎src/pages/LandingPage.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ function LandingPage() {
482482
<CreatorProfileHeader
483483
name="Alex Rivers"
484484
handle="arivers"
485+
creatorId="arivers"
485486
isVerified={true}
486487
avatarUrl="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop"
487488
/>

‎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)