Skip to content

Commit 40400dd

Browse files
committed
showcase submissions
1 parent fe862ec commit 40400dd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+4553
-70
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@tanstack/react-start": "1.141.8",
4545
"@tanstack/react-table": "^8.21.3",
4646
"@types/d3": "^7.4.3",
47+
"@uploadthing/react": "^7.3.3",
4748
"@visx/hierarchy": "^2.10.0",
4849
"@visx/responsive": "^2.10.0",
4950
"@vitejs/plugin-react": "^4.3.3",
@@ -65,6 +66,7 @@
6566
"react": "^19.2.0",
6667
"react-colorful": "^5.6.1",
6768
"react-dom": "^19.2.0",
69+
"react-easy-crop": "^5.5.6",
6870
"react-instantsearch": "7",
6971
"rehype-autolink-headings": "^7.1.0",
7072
"rehype-callouts": "^2.1.2",
@@ -80,6 +82,7 @@
8082
"tailwind-merge": "^1.14.0",
8183
"unified": "^11.0.5",
8284
"unist-util-visit": "^5.0.0",
85+
"uploadthing": "^7.7.4",
8386
"vite-bundle-analyzer": "^1.2.1",
8487
"vite-tsconfig-paths": "^5.0.1",
8588
"zod": "^4.0.17",

pnpm-lock.yaml

Lines changed: 256 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/auth/auth.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export class AuthService implements IAuthService {
106106
email: user.email,
107107
name: user.name,
108108
image: user.image,
109+
oauthImage: user.oauthImage,
109110
displayUsername: user.displayUsername,
110111
capabilities,
111112
adsDisabled: user.adsDisabled,

src/auth/oauth.server.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export class OAuthService implements IOAuthService {
6060
const updates: {
6161
email?: string
6262
name?: string
63-
image?: string
63+
oauthImage?: string
6464
updatedAt?: Date
6565
} = {}
6666

@@ -70,8 +70,9 @@ export class OAuthService implements IOAuthService {
7070
if (profile.name && user.name !== profile.name) {
7171
updates.name = profile.name
7272
}
73-
if (profile.image && user.image !== profile.image) {
74-
updates.image = profile.image
73+
// Always update oauthImage from provider (it may have changed)
74+
if (profile.image && user.oauthImage !== profile.image) {
75+
updates.oauthImage = profile.image
7576
}
7677

7778
if (Object.keys(updates).length > 0) {
@@ -101,6 +102,7 @@ export class OAuthService implements IOAuthService {
101102
const updates: {
102103
name?: string
103104
image?: string
105+
oauthImage?: string
104106
updatedAt?: Date
105107
} = {}
106108

@@ -110,6 +112,10 @@ export class OAuthService implements IOAuthService {
110112
if (profile.image && !existingUser.image) {
111113
updates.image = profile.image
112114
}
115+
// Always update oauthImage from provider
116+
if (profile.image && existingUser.oauthImage !== profile.image) {
117+
updates.oauthImage = profile.image
118+
}
113119

114120
if (Object.keys(updates).length > 0) {
115121
updates.updatedAt = new Date()
@@ -124,6 +130,7 @@ export class OAuthService implements IOAuthService {
124130
email: profile.email,
125131
name: profile.name,
126132
image: profile.image,
133+
oauthImage: profile.image,
127134
displayUsername: profile.name,
128135
capabilities: [],
129136
})

src/auth/repositories.server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export class DrizzleUserRepository implements IUserRepository {
5050
email: string
5151
name?: string
5252
image?: string
53+
oauthImage?: string
5354
displayUsername?: string
5455
capabilities?: Capability[]
5556
}): Promise<DbUser> {
@@ -59,6 +60,7 @@ export class DrizzleUserRepository implements IUserRepository {
5960
email: data.email,
6061
name: data.name,
6162
image: data.image,
63+
oauthImage: data.oauthImage,
6264
displayUsername: data.displayUsername,
6365
capabilities: data.capabilities || [],
6466
})
@@ -76,7 +78,8 @@ export class DrizzleUserRepository implements IUserRepository {
7678
data: Partial<{
7779
email: string
7880
name: string
79-
image: string
81+
image: string | null
82+
oauthImage: string
8083
displayUsername: string
8184
capabilities: Capability[]
8285
adsDisabled: boolean
@@ -111,6 +114,7 @@ export class DrizzleUserRepository implements IUserRepository {
111114
email: user.email,
112115
name: user.name,
113116
image: user.image,
117+
oauthImage: user.oauthImage,
114118
displayUsername: user.displayUsername,
115119
capabilities: user.capabilities as Capability[],
116120
adsDisabled: user.adsDisabled,

src/auth/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ export type Capability =
1616
| 'builder'
1717
| 'feed'
1818
| 'moderate-feedback'
19+
| 'moderate-showcases'
1920

2021
export const VALID_CAPABILITIES: readonly Capability[] = [
2122
'admin',
2223
'disableAds',
2324
'builder',
2425
'feed',
2526
'moderate-feedback',
27+
'moderate-showcases',
2628
] as const
2729

2830
// ============================================================================
@@ -65,6 +67,7 @@ export interface AuthUser {
6567
email: string
6668
name: string | null
6769
image: string | null
70+
oauthImage: string | null
6871
displayUsername: string | null
6972
capabilities: Capability[]
7073
adsDisabled: boolean | null
@@ -80,6 +83,7 @@ export interface DbUser {
8083
email: string
8184
name: string | null
8285
image: string | null
86+
oauthImage: string | null
8387
displayUsername: string | null
8488
capabilities: Capability[]
8589
adsDisabled: boolean | null
@@ -105,6 +109,7 @@ export interface IUserRepository {
105109
email: string
106110
name?: string
107111
image?: string
112+
oauthImage?: string
108113
displayUsername?: string
109114
capabilities?: Capability[]
110115
}): Promise<DbUser>
@@ -113,7 +118,8 @@ export interface IUserRepository {
113118
data: Partial<{
114119
email: string
115120
name: string
116-
image: string
121+
image: string | null
122+
oauthImage: string
117123
displayUsername: string
118124
capabilities: Capability[]
119125
adsDisabled: boolean

src/components/Avatar.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { User } from 'lucide-react'
2+
import { twMerge } from 'tailwind-merge'
3+
4+
type AvatarSize = '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
5+
6+
const sizeClasses: Record<AvatarSize, { container: string; text: string }> = {
7+
'2xs': { container: 'w-4 h-4', text: 'text-[8px]' },
8+
xs: { container: 'w-6 h-6', text: 'text-xs' },
9+
sm: { container: 'w-8 h-8', text: 'text-sm' },
10+
md: { container: 'w-10 h-10', text: 'text-base' },
11+
lg: { container: 'w-14 h-14', text: 'text-lg' },
12+
xl: { container: 'w-20 h-20', text: 'text-xl' },
13+
}
14+
15+
interface AvatarProps {
16+
image?: string | null
17+
oauthImage?: string | null
18+
name?: string | null
19+
email?: string | null
20+
size?: AvatarSize
21+
className?: string
22+
}
23+
24+
function getInitials(name?: string | null, email?: string | null): string {
25+
if (name) {
26+
const parts = name.trim().split(/\s+/)
27+
if (parts.length >= 2) {
28+
return `${parts[0]![0]}${parts[parts.length - 1]![0]}`.toUpperCase()
29+
}
30+
return name.slice(0, 2).toUpperCase()
31+
}
32+
if (email) {
33+
return email.slice(0, 2).toUpperCase()
34+
}
35+
return ''
36+
}
37+
38+
export function Avatar({
39+
image,
40+
oauthImage,
41+
name,
42+
email,
43+
size = 'md',
44+
className = '',
45+
}: AvatarProps) {
46+
const displayImage = image || oauthImage
47+
const initials = getInitials(name, email)
48+
const { container, text } = sizeClasses[size]
49+
50+
if (displayImage) {
51+
return (
52+
<img
53+
src={displayImage}
54+
alt={name || email || 'User avatar'}
55+
className={twMerge(container, 'rounded-full object-cover', className)}
56+
/>
57+
)
58+
}
59+
60+
if (initials) {
61+
return (
62+
<div
63+
className={twMerge(
64+
container,
65+
text,
66+
'rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center font-medium text-gray-600 dark:text-gray-300',
67+
className,
68+
)}
69+
>
70+
{initials}
71+
</div>
72+
)
73+
}
74+
75+
return (
76+
<div
77+
className={twMerge(
78+
container,
79+
'rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center text-gray-500 dark:text-gray-400',
80+
className,
81+
)}
82+
>
83+
<User className="w-1/2 h-1/2" />
84+
</div>
85+
)
86+
}

0 commit comments

Comments
 (0)