Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ jobs:
if: failure() && github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: Post failure comment
Expand Down Expand Up @@ -163,16 +164,25 @@ jobs:
`[Full CI log](https://github.com/${owner}/${repo}/actions/runs/${runId})`,
].join('\n');

// Check if we already commented on this PR for this run to avoid duplicates
const { data: comments } = await github.rest.issues.listComments({
owner, repo, issue_number: prNumber,
});
const alreadyCommented = comments.some(
c => c.user.login === 'github-actions[bot]' &&
c.body.includes(`actions/runs/${runId}`)
);
if (alreadyCommented) return;

await github.rest.issues.createComment({
owner, repo, issue_number: prNumber, body,
});
try {
// Check if we already commented on this PR for this run to avoid duplicates
const { data: comments } = await github.rest.issues.listComments({
owner, repo, issue_number: prNumber,
});
const alreadyCommented = comments.some(
c => c.user.login === 'github-actions[bot]' &&
c.body.includes(`actions/runs/${runId}`)
);
if (alreadyCommented) return;

await github.rest.issues.createComment({
owner, repo, issue_number: prNumber, body,
});
} catch (error) {
if (error?.status === 403) {
core.warning('Skipping CI failure comment because the workflow token cannot write to this PR.');
return;
}

throw error;
}
344 changes: 195 additions & 149 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 10 additions & 8 deletions src/app/(app)/dashboard/github-prs-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ export function GitHubPRsPanel({ prs, claimedPrUrls, githubHandle }: Props) {

return (
<section>
<div className="mb-6 flex items-center justify-between border-b border-[#2d333b] pb-4">
<div className="mb-4 flex flex-col gap-3 border-b border-[#2d333b] pb-3 md:mb-6 md:flex-row md:items-center md:justify-between md:gap-0 md:pb-4">
<h2 className="text-[11px] uppercase tracking-widest text-zinc-500">MY PRS</h2>
<div className="flex items-center gap-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
<select
value={filter}
onChange={(e) => setFilter(e.target.value as Filter)}
className="cursor-pointer appearance-none border border-zinc-700 bg-[#1c2128] px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest text-zinc-300 focus:border-[#10b981] focus:outline-none"
className="w-full cursor-pointer appearance-none border border-zinc-700 bg-[#1c2128] px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest text-zinc-300 focus:border-[#10b981] focus:outline-none sm:w-auto"
>
<option value="open">Open</option>
<option value="closed">Closed</option>
Expand All @@ -50,16 +50,18 @@ export function GitHubPRsPanel({ prs, claimedPrUrls, githubHandle }: Props) {
No {filter} PRs.
</div>
) : (
<div className="space-y-6">
<div className="space-y-4 sm:space-y-6">
{filtered.map((pr) => (
<div key={pr.id} className="border-b border-[#2d333b] pb-6 last:border-0">
<div key={pr.id} className="border-b border-[#2d333b] pb-4 last:border-0 sm:pb-6">
<Link href={pr.url} target="_blank" rel="noopener noreferrer">
<h3 className="mb-1 text-[15px] text-white hover:underline">{pr.title}</h3>
<h3 className="mb-1 line-clamp-2 text-sm text-white hover:underline sm:text-base">
{pr.title}
</h3>
</Link>
<div className="mb-3 text-[11px] uppercase tracking-widest text-zinc-500">
<div className="mb-2 text-[11px] uppercase tracking-widest text-zinc-500 sm:mb-3">
#{pr.number} · {pr.repo_full_name} · {formatDate(pr.github_created_at)}
</div>
<div className="flex items-center gap-3">
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
<StateBadge state={pr.state} />
{claimedSet.has(pr.url) && (
<span className="border border-purple-700 bg-purple-900/30 px-2 py-0.5 text-[10px] uppercase tracking-widest text-purple-300">
Expand Down
238 changes: 231 additions & 7 deletions src/app/(app)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ export default async function DashboardPage() {
.maybeSingle();

return (
<div className="min-h-screen bg-[#111318] p-12 font-mono text-white">
<div className="min-h-screen bg-[#111318] px-4 py-8 font-mono text-white sm:px-6 md:px-12 md:py-12">
<div className="mx-auto max-w-6xl">
<LevelUpBanner />
{/* Header */}
<header className="mb-12 flex flex-col justify-between gap-6 border-b border-[#2d333b] pb-6 md:flex-row md:items-end">
<header className="mb-8 flex flex-col justify-between gap-4 border-b border-[#2d333b] pb-4 md:mb-12 md:gap-6 md:pb-6 lg:flex-row lg:items-end">
<div>
<div className="mb-4 text-[11px] uppercase tracking-widest text-zinc-500">
<div className="mb-2 text-[11px] uppercase tracking-widest text-zinc-500 md:mb-4">
01 / DASHBOARD
</div>
<h1 className="font-serif text-4xl text-white">
<h1 className="font-serif text-2xl text-white sm:text-3xl md:text-4xl">
Welcome back, {profile?.github_handle ?? 'Contributor'}.
</h1>
</div>
Expand All @@ -57,12 +57,185 @@ export default async function DashboardPage() {

{/* Stats Row */}
<Suspense fallback={<StatsSkeleton />}>
<div className="mb-12 grid grid-cols-1 gap-6 sm:grid-cols-2 md:mb-16 md:gap-12 lg:grid-cols-4">
{/* Level Progress */}
<div>
<div className="mb-3 text-[11px] uppercase tracking-widest text-zinc-500 md:mb-4">
LEVEL PROGRESS
</div>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
<div className="border border-zinc-700 px-3 py-2 font-serif text-lg sm:text-xl text-zinc-300">
L{level}
</div>
<div className="flex-1">
<div className="mb-2 h-1.5 w-full overflow-hidden bg-[#1c2128]">
<div
className="h-full bg-[#10b981]"
style={{ width: `${levelProgressPct(xp, level)}%` }}
/>
</div>
<div className="text-[10px] uppercase tracking-widest text-zinc-500">
{xp.toLocaleString()} / {(xp + needed).toLocaleString()} XP TO L{nextLevel}
</div>
</div>
</div>
</div>

{/* Total Merges */}
<div>
<div className="mb-3 text-[11px] uppercase tracking-widest text-zinc-500 md:mb-4">
TOTAL MERGES
</div>
<div className="flex items-end gap-2">
<span className="font-serif text-3xl leading-none sm:text-4xl">
{(merges ?? 0).toString().padStart(2, '0')}
</span>
<TrendingUp className="mb-1 h-4 w-4 text-[#10b981]" />
</div>
</div>

{/* Mentor Points */}
<div>
<div className="mb-3 text-[11px] uppercase tracking-widest text-zinc-500 md:mb-4">
MENTOR POINTS
</div>
<div className="flex items-end gap-2">
<span className="font-serif text-3xl leading-none sm:text-4xl">
{mentorPoints.toLocaleString()}
</span>
<Box className="mb-1 h-5 w-5 text-zinc-400" />
</div>
</div>

{/* Current Streak */}
<div>
<div className="mb-3 text-[11px] uppercase tracking-widest text-zinc-500 md:mb-4">
CURRENT STREAK
</div>
<div className="flex items-end gap-2">
<span className="font-serif text-3xl leading-none sm:text-4xl">
{(streak ?? 0).toString().padStart(2, '0')}
</span>
<span className="mb-1 text-[10px] uppercase tracking-widest text-zinc-500">
DAYS 🔥
</span>
</div>
</div>
</div>
<StatsRow userId={user.id} profile={profile} />
</Suspense>

{/* Main Columns */}
<div className="grid grid-cols-1 gap-16 lg:grid-cols-2">
<div className="grid grid-cols-1 gap-12 md:gap-16 lg:grid-cols-2">
{/* Left Column */}
<div className="space-y-12 md:space-y-16">
<section>
<div className="mb-4 flex flex-col items-start justify-between gap-2 border-b border-[#2d333b] pb-3 md:mb-6 md:flex-row md:items-center md:gap-0 md:pb-4">
<h2 className="text-[11px] uppercase tracking-widest text-zinc-500">
ACTIVE ISSUES
</h2>
<Link
href="/issues"
className="flex items-center gap-2 text-[11px] uppercase tracking-widest text-zinc-400 hover:text-white"
>
BROWSE MORE <ArrowRight className="h-3 w-3" />
</Link>
</div>

{recs.length > 0 ? (
<RecCards recs={recs} />
) : (
<div className="py-4 text-sm text-zinc-500">
No recommendations yet. Check back soon.
</div>
)}
</section>

<section>
<div className="mb-4 border-b border-[#2d333b] pb-3 md:mb-6 md:pb-4">
<h2 className="text-[11px] uppercase tracking-widest text-zinc-500">
YOUR MENTEES
</h2>
</div>
<div className="space-y-4">
{enrichedMentees && enrichedMentees.length > 0 ? (
enrichedMentees.map((mentee: any) => (
<div
key={mentee.id}
className="flex flex-col gap-3 border-b border-[#2d333b] pb-4 sm:flex-row sm:items-center sm:justify-between"
>
<div className="flex items-center gap-3 sm:gap-4">
<div className="flex h-10 w-10 items-center justify-center border border-zinc-800 bg-[#1c2128] text-xs uppercase text-zinc-500">
{mentee.github_handle.substring(0, 2)}
</div>
<div>
<div className="text-xs font-bold uppercase tracking-widest text-zinc-200">
{mentee.github_handle}
</div>
<div className="text-sm text-zinc-400">Help Request: {mentee.status}</div>
</div>
</div>
<Link
href={mentee.pr_url || '#'}
className="w-full border border-zinc-700 px-4 py-2 text-center text-[10px] uppercase tracking-widest text-zinc-300 transition-colors hover:bg-zinc-800 sm:w-auto"
>
REVIEW DRAFT
</Link>
</div>
))
) : (
<div className="py-4 text-[11px] uppercase tracking-widest text-zinc-500">
No active mentees assigned to you.
</div>
)}
</div>
</section>
</div>

{/* Right Column */}
<div className="space-y-12 md:space-y-16">
<GitHubPRsPanel
prs={prs}
claimedPrUrls={claimedPrUrls}
githubHandle={profile?.github_handle ?? ''}
/>

<section>
<div className="mb-4 flex items-center justify-between border-b border-[#2d333b] pb-3 md:mb-6 md:pb-4">
<h2 className="text-[11px] uppercase tracking-widest text-zinc-500">
LEADERBOARD SNAPSHOT
</h2>
<span className="text-[11px] uppercase tracking-widest text-zinc-500">GLOBAL</span>
</div>

<div className="text-xs uppercase tracking-widest">
{leaders && leaders.length > 0 ? (
leaders.map((leader, index) => {
const isMe = leader.github_handle === profile?.github_handle;
return (
<div
key={leader.github_handle}
className={`flex justify-between border-b border-[#2d333b] py-3 md:py-3.5 ${isMe ? '-mx-2 bg-[#3b0764]/40 px-2 text-purple-300 md:-mx-3 md:px-3' : 'text-zinc-400'}`}
>
<div className="flex gap-3 md:gap-5">
<span className={`w-6 ${isMe ? 'opacity-50' : 'text-zinc-600'}`}>
{(index + 1).toString().padStart(2, '0')}
</span>
<span className="truncate">
{leader.github_handle} {isMe && '(YOU)'}
</span>
</div>
<span className="ml-2">{leader.xp.toLocaleString()} XP</span>
</div>
);
})
) : (
<div className="py-4 text-[11px] uppercase tracking-widest text-zinc-500">
BE THE FIRST ON THE BOARD — MERGE A PR TO EARN XP
</div>
)}
</div>
</section>
<div className="space-y-16">
<Suspense fallback={<RecsSkeleton />}>
<ActiveIssuesSection />
Expand All @@ -85,9 +258,9 @@ export default async function DashboardPage() {
</div>

{/* Footer */}
<footer className="mt-24 flex justify-between border-t border-[#2d333b] pt-8 text-[10px] uppercase tracking-widest text-zinc-600">
<footer className="mt-16 flex flex-col gap-4 border-t border-[#2d333b] pt-6 text-[10px] uppercase tracking-widest text-zinc-600 md:mt-24 md:flex-row md:justify-between md:pt-8">
<span>©{new Date().getFullYear()} ARCH_06 / SYSTEM_v1.0</span>
<div className="flex gap-6">
<div className="flex gap-4 sm:gap-6">
<Link href="#" className="transition-colors hover:text-zinc-400">
TERMS
</Link>
Expand All @@ -114,3 +287,54 @@ function NotConfigured() {
</div>
);
}

function StatsSkeleton() {
return (
<div className="mb-12 grid grid-cols-1 gap-6 sm:grid-cols-2 md:mb-16 md:gap-12 lg:grid-cols-4">
{/* Level Progress Skeleton */}
<div>
<div className="mb-3 text-[11px] uppercase tracking-widest text-zinc-500 md:mb-4">
LEVEL PROGRESS
</div>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
<div className="h-11 w-12 animate-pulse border border-zinc-700 bg-zinc-800" />
<div className="flex-1">
<div className="mb-2 h-1.5 w-full animate-pulse bg-zinc-800" />
<div className="h-3 w-3/4 animate-pulse bg-zinc-800" />
</div>
</div>
</div>

{/* Total Merges Skeleton */}
<div>
<div className="mb-3 text-[11px] uppercase tracking-widest text-zinc-500 md:mb-4">TOTAL MERGES</div>
<div className="flex items-end gap-2">
<div className="h-8 w-16 animate-pulse rounded bg-zinc-800 sm:h-9" />
<div className="mb-1 h-4 w-4 animate-pulse rounded bg-zinc-800" />
</div>
</div>

{/* Mentor Points Skeleton */}
<div>
<div className="mb-3 text-[11px] uppercase tracking-widest text-zinc-500 md:mb-4">
MENTOR POINTS
</div>
<div className="flex items-end gap-2">
<div className="h-8 w-24 animate-pulse rounded bg-zinc-800 sm:h-9" />
<div className="mb-1 h-5 w-5 animate-pulse rounded bg-zinc-800" />
</div>
</div>

{/* Current Streak Skeleton */}
<div>
<div className="mb-3 text-[11px] uppercase tracking-widest text-zinc-500 md:mb-4">
CURRENT STREAK
</div>
<div className="flex items-end gap-2">
<div className="h-8 w-16 animate-pulse rounded bg-zinc-800 sm:h-9" />
<div className="mb-1 h-4 w-12 animate-pulse rounded bg-zinc-800" />
</div>
</div>
</div>
);
}
Loading
Loading