Files
supabase/apps/www/components/Contribute/LeaderboardContent.tsx
0b8db9adc1 Feat(www)/contribute (#40966)
* Rough contribute ui

* Clean up filter area

* Tweak query with new threads schema

* Unify table and apply some ui nits

* Fix types issues

* Setup types

* FilterS

* Fix filters

* Make search global

* Add icons for table rows

* Add single thread view

* Loading state for threads

* Fix icon colors

* Add leaderboard

* Fix conversation text overflow and add op badge

* Rename leaderboard

* Turn of leaderboard

* Formatting

* Remove other from product areas

* And from stacks

* Prettier types

* Start contribute/about

* About cards

* Add avatar component

* Polish up about page

* Nudges

* Add application form

* Add squad form

* Change to last 30 days instead of 24hrs

* Responsive nudges

* Format components for conversation view

* Re use ui components on about page

* Fix prettier errors

* Filter nudges

* chore(www): improve contribute UI (#41481)

* hero tweaks

* better table

* wrap post time

* nip and tuck

* use table component

* use table component

* Add menu item

* Alan/polish contribute filtering UI (#41504)

* polishing tabs

* nits in tables

* nit in filtering

* nits

* ship it

* fix selection style

* nit search bar

* Add view for authors

* Format author page

* Add contribute menu item

* Tidy up about page

* More about page edits

* Update

* basics

* copy cleanup

* improvements to thread table

* threads improvements

* remove extraneous div

* thread improvements

* chore(www): improve contribute UI (#41605)

* basics

* improvements to thread table

* threads improvements

* remove extraneous div

* thread improvements

* Add feature flag for contribute page

* Add feature flag for contribute page

* Fix ts error

* Thread view nudges

* Undo env change

* Cleanup

* Cleanup

* Typos

---------

Co-authored-by: Tomas Pozo <tomaspozogarzon@gmail.com>
Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com>
Co-authored-by: Alan Daniel <stylesshjs@gmail.com>
2026-01-07 14:48:23 -03:30

171 lines
4.4 KiB
TypeScript

'use client'
import { cn } from 'ui'
import type { LeaderboardRow } from '~/types/contribute'
function getInitials(name: string | null): string {
if (!name) return '?'
const parts = name.trim().split(/\s+/)
if (parts.length >= 2) {
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
}
return name.substring(0, 2).toUpperCase()
}
function getAvatarColor(name: string | null): string {
if (!name) return 'bg-surface-300'
const colors = [
'bg-blue-500',
'bg-purple-500',
'bg-pink-500',
'bg-red-500',
'bg-orange-500',
'bg-yellow-500',
'bg-green-500',
'bg-teal-500',
'bg-cyan-500',
'bg-indigo-500',
]
let hash = 0
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash)
}
return colors[Math.abs(hash) % colors.length]
}
function Avatar({ name, size = 'md' }: { name: string | null; size?: 'sm' | 'md' | 'lg' }) {
const initials = getInitials(name)
const sizeClasses = {
sm: 'h-8 w-8 text-xs',
md: 'h-12 w-12 text-base',
lg: 'h-20 w-20 text-2xl',
}
return (
<div
className={cn(
'rounded-full flex items-center justify-center text-white font-semibold',
getAvatarColor(name),
sizeClasses[size]
)}
>
{initials}
</div>
)
}
function TopThreeCard({
rank,
name,
score,
isFirst,
}: {
rank: number
name: string | null
score: number
isFirst: boolean
}) {
return (
<div
className={cn(
'flex flex-col items-center p-6 rounded-lg border border-border bg-surface-200',
isFirst && 'scale-105'
)}
>
<div className="mb-4">
<Avatar name={name} size="lg" />
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-foreground mb-1">{name || 'Anonymous'}</h3>
<div className="text-3xl font-bold text-foreground">{score.toLocaleString()}</div>
<div className="text-sm text-foreground-lighter mt-1">Replies</div>
</div>
</div>
)
}
function LeaderboardRowItem({
rank,
name,
score,
}: {
rank: number
name: string | null
score: number
}) {
return (
<div className="flex items-center gap-4 py-4 border-b border-border last:border-0">
<div className="text-lg font-semibold text-foreground w-12">{rank}</div>
<Avatar name={name} size="sm" />
<div className="flex-1">
<div className="text-foreground font-medium">{name || 'Anonymous'}</div>
</div>
<div className="text-right">
<div className="text-lg font-semibold text-foreground">{score.toLocaleString()}</div>
<div className="text-xs text-foreground-lighter">Replies</div>
</div>
</div>
)
}
export function LeaderboardContent({ leaderboard }: { leaderboard: LeaderboardRow[] }) {
if (!leaderboard || leaderboard.length === 0) {
return (
<div className="border border-border rounded-lg p-8 text-center text-muted-foreground">
No leaderboard data available
</div>
)
}
const topThree = leaderboard.slice(0, 3)
const rest = leaderboard.slice(3)
return (
<div className="w-full">
{/* Top 3 Cards */}
{topThree.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
{topThree.length >= 2 && (
<TopThreeCard
rank={2}
name={topThree[1]?.author ?? null}
score={topThree[1]?.reply_count ?? 0}
isFirst={false}
/>
)}
{topThree.length >= 1 && (
<TopThreeCard
rank={1}
name={topThree[0]?.author ?? null}
score={topThree[0]?.reply_count ?? 0}
isFirst={true}
/>
)}
{topThree.length >= 3 && (
<TopThreeCard
rank={3}
name={topThree[2]?.author ?? null}
score={topThree[2]?.reply_count ?? 0}
isFirst={false}
/>
)}
</div>
)}
{/* Rest of the leaderboard */}
{rest.length > 0 && (
<div className="border border-border rounded-lg bg-surface-200 p-6">
{rest.map((entry, index) => (
<LeaderboardRowItem
key={entry.author || index}
rank={index + 4}
name={entry.author}
score={entry.reply_count}
/>
))}
</div>
)}
</div>
)
}