Files
supabase/apps/www/components/Contribute/UserProfile.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

151 lines
5.2 KiB
TypeScript

import Link from 'next/link'
import { notFound } from 'next/navigation'
import { Badge, Button } from 'ui'
import { DiscordIcon, GitHubIcon, RedditIcon } from '~/components/Contribute/Icons'
import { getUserActivity } from '~/data/contribute'
import type { ThreadRow } from '~/types/contribute'
function ThreadCard({ thread }: { thread: ThreadRow }) {
return (
<Link
href={`/contribute/t/${thread.id}`}
className="border border-border rounded-lg p-4 bg-surface-75 hover:bg-surface-100 transition-colors min-w-0"
>
<div className="grid gap-2">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
{thread.channel === 'discord' && <DiscordIcon className="h-4 w-4 text-[#5865F2]" />}
{thread.channel === 'reddit' && <RedditIcon className="h-4 w-4 text-[#FF4500]" />}
{thread.channel === 'github' && <GitHubIcon className="h-4 w-4 text-foreground" />}
<span className="capitalize">{thread.channel}</span>
<span>•</span>
<span>{thread.posted}</span>
</div>
<h3 className="text-foreground font-medium">{thread.title}</h3>
{thread.summary && (
<p className="text-sm text-foreground-light line-clamp-2">{thread.summary}</p>
)}
{thread.product_areas.filter((area) => area !== 'Other').length > 0 && (
<div className="flex flex-wrap gap-1">
{thread.product_areas
.filter((area) => area !== 'Other')
.slice(0, 3)
.map((area) => (
<Badge key={area} variant="default">
{area}
</Badge>
))}
{thread.product_areas.filter((area) => area !== 'Other').length > 3 && (
<Badge variant="default">
+{thread.product_areas.filter((area) => area !== 'Other').length - 3}
</Badge>
)}
</div>
)}
</div>
</Link>
)
}
interface Reply {
id: string
author: string | null
content: string | null
ts: string | null
external_activity_url: string | null
thread_key: string | null
}
function ReplyCard({ reply, thread }: { reply: Reply; thread?: ThreadRow }) {
const platformName = thread?.channel
? thread.channel.charAt(0).toUpperCase() + thread.channel.slice(1)
: 'platform'
return (
<div className="border border-border rounded-lg p-4 bg-surface-75 min-w-0">
<div className="grid gap-2">
{thread && (
<Link
href={`/contribute/t/${thread.id}`}
className="text-foreground-light text-sm hover:text-foreground transition-colors"
>
<span className="font-medium">Reply to:</span> {thread.title}
</Link>
)}
<p className="text-foreground text-sm line-clamp-3">{reply.content}</p>
<div className="flex items-center justify-between gap-2">
{reply.ts && (
<span className="text-xs text-muted-foreground">
{new Date(reply.ts).toLocaleString()}
</span>
)}
{reply.external_activity_url && (
<Button asChild type="default" size="tiny">
<a
href={reply.external_activity_url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2"
>
View on {platformName}
</a>
</Button>
)}
</div>
</div>
</div>
)
}
export async function UserProfile({ username }: { username: string }) {
const { threads, replies, replyThreads, stats } = await getUserActivity(username)
if (threads.length === 0 && replies.length === 0) {
notFound()
}
return (
<div className="border border-border rounded-lg p-8 bg-surface-200">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl text-foreground mb-2">{username}</h1>
<div className="flex items-center gap-3 text-foreground-light">
<div className="flex items-center gap-2">
<span>{stats.threadCount}</span>
<span>{stats.threadCount === 1 ? 'thread' : 'threads'}</span>
</div>
<span>/</span>
<div className="flex items-center gap-2">
<span>{stats.replyCount}</span>
<span>{stats.replyCount === 1 ? 'reply' : 'replies'}</span>
</div>
</div>
</div>
{/* Threads Section */}
{threads.length > 0 && (
<div className="mb-8">
<h2 className="text-xl text-foreground mb-4">Threads created</h2>
<div className="grid gap-3">
{threads.map((thread) => (
<ThreadCard key={thread.id} thread={thread} />
))}
</div>
</div>
)}
{/* Replies Section */}
{replies.length > 0 && (
<div>
<h2 className="text-xl text-foreground mb-4">Recent replies</h2>
<div className="grid gap-3">
{replies.map((reply) => {
const thread = replyThreads.find((t) => t.thread_key === reply.thread_key)
return <ReplyCard key={reply.id} reply={reply} thread={thread} />
})}
</div>
</div>
)}
</div>
)
}