Skip to content

Commit decb9fc

Browse files
Merge pull request #1663 from narang24/feat/friend-comparison-enhancement
feat(friend-comparison): add dedicated comparison page and enhanced the component
2 parents ed0008c + e35a9c8 commit decb9fc

4 files changed

Lines changed: 442 additions & 94 deletions

File tree

next.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ const nextConfig = {
145145
protocol: "https",
146146
hostname: "github.githubassets.com",
147147
},
148+
{
149+
protocol: "https",
150+
hostname: "via.placeholder.com",
151+
},
148152
],
149153
},
150154
async headers() {

src/app/dashboard/page.tsx

Lines changed: 191 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,87 @@
1+
import LazyWidget from "@/components/LazyWidget";
2+
import DiscussionsWidget from "@/components/DiscussionsWidget";
3+
import CommunityMetrics from "@/components/CommunityMetrics";
4+
import GoalTracker from "@/components/GoalTracker";
15
import TodayFocusHero from "@/components/TodayFocusHero";
26
import DashboardHeader from "@/components/DashboardHeader";
37
import ExportButton from "@/components/ExportButton";
48
import Link from "next/link";
9+
import PersonalRecords from "@/components/PersonalRecords";
10+
import LocalCodingTime from "@/components/LocalCodingTime";
11+
import CodingTimeWidget from "@/components/CodingTimeWidget";
12+
import RecentActivity from "@/components/RecentActivity";
13+
import FriendComparison from "@/components/FriendComparison";
514
import { ChevronRight } from "lucide-react";
615
import { authOptions } from "@/lib/auth";
716
import { getServerSession } from "next-auth";
817
import { redirect } from "next/navigation";
918
import DashboardSSEProvider from "@/components/DashboardSSEProvider";
19+
20+
const SkeletonCard = () => (
21+
<div
22+
role="status"
23+
aria-busy="true"
24+
aria-live="polite"
25+
className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm"
26+
>
27+
<div className="h-6 w-48 bg-[var(--card-muted)] rounded mb-4 animate-pulse" />
28+
<div className="h-40 bg-[var(--card-muted)] rounded animate-pulse" />
29+
</div>
30+
);
31+
32+
const ContributionGraphSkeleton = () => (
33+
<div className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm">
34+
<h2 className="text-lg font-semibold text-[var(--foreground)]">Your Commits</h2>
35+
<div className="mt-3 h-40 rounded bg-[var(--card-muted)] animate-pulse" />
36+
</div>
37+
);
38+
39+
const PRMetricsSkeleton = () => (
40+
<div className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm">
41+
<h2 className="text-lg font-semibold text-[var(--card-foreground)]">PR Analytics</h2>
42+
<div className="mt-3 h-40 rounded bg-[var(--card-muted)] animate-pulse" />
43+
</div>
44+
);
45+
46+
const CodingActivityInsightsCard = dynamic(
47+
() => import("@/components/CodingActivityInsightsCard"),
48+
{ ssr: false, loading: () => <SkeletonCard /> },
49+
);
50+
51+
const ActivityRingChart = dynamic(
52+
() => import("@/components/ActivityRingChart"),
53+
{ ssr: false, loading: () => <SkeletonCard /> },
54+
);
55+
56+
const ContributionGraph = dynamic(
57+
() => import("@/components/ContributionGraph"),
58+
{ ssr: false, loading: () => <ContributionGraphSkeleton /> },
59+
);
60+
61+
const ContributionHeatmap = dynamic(
62+
() => import("@/components/ContributionHeatmap"),
63+
{ ssr: false, loading: () => <SkeletonCard /> },
64+
);
65+
66+
const PRMetrics = dynamic(() => import("@/components/PRMetrics"), {
67+
ssr: false,
68+
loading: () => <PRMetricsSkeleton />,
69+
});
70+
71+
const PRBreakdownChart = dynamic(
72+
() => import("@/components/PRBreakdownChart"),
73+
{ ssr: false, loading: () => <SkeletonCard /> },
74+
);
75+
76+
const CommitTimeChart = dynamic(
77+
() => import("@/components/CommitTimeChart"),
78+
{ ssr: false, loading: () => <SkeletonCard /> },
79+
);
80+
81+
const PRReviewTrendChart = dynamic(
82+
() => import("@/components/PRReviewTrendChart"),
83+
{ ssr: false, loading: () => <SkeletonCard /> },
84+
);
1085
import StreakAtRiskBanner from "@/components/StreakAtRiskBanner";
1186
import ThrottleBanner from "@/components/ThrottleBanner";
1287
import CustomizableDashboard from "@/components/dashboard/CustomizableDashboard";
@@ -22,25 +97,29 @@ export default async function DashboardPage() {
2297
<DashboardHeader />
2398

2499
{/* Quick actions */}
25-
<div className="mt-10 mb-6 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
26-
{/* Left side actions */}
27-
<div className="flex flex-wrap items-center gap-3 w-full sm:w-auto">
28-
<Link
29-
href="/wrapped"
30-
className="inline-flex w-full sm:w-auto justify-center items-center gap-2 rounded-lg border border-[var(--accent)] bg-[var(--accent)]/10 px-5 py-2.5 text-sm font-semibold text-[var(--accent)] shadow-sm shadow-[var(--accent)]/20 transition-all hover:bg-[var(--accent)]/20 hover:shadow-md hover:scale-[1.02] active:scale-95"
31-
>
32-
Year in Code
33-
</Link>
34-
35-
<Link
36-
href="/dashboard/settings"
37-
className="inline-flex w-full sm:w-auto justify-center items-center gap-2 rounded-lg border border-[var(--border)] bg-[var(--card)]/60 px-5 py-2.5 text-sm font-medium transition-all hover:bg-[var(--card)]/80 hover:shadow-sm hover:scale-[1.02] active:scale-95"
38-
>
39-
Settings
40-
</Link>
41-
</div>
42-
43-
<div className="w-full sm:w-auto">
100+
<div className="mt-4 flex flex-wrap items-center gap-2 sm:gap-3">
101+
<Link
102+
href="/wrapped"
103+
className="inline-flex items-center gap-2 rounded-lg border border-[var(--accent)] bg-[var(--accent-soft)] px-4 py-2 text-sm font-semibold text-[var(--accent)] transition-opacity hover:opacity-90"
104+
>
105+
✨ Year in Code
106+
</Link>
107+
<Link
108+
href="/friend-compare"
109+
className="inline-flex items-center gap-2 rounded-lg border border-[var(--accent)] bg-[var(--accent-soft)] px-4 py-2 text-sm font-semibold text-[var(--accent)] transition-opacity hover:opacity-90"
110+
>
111+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
112+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
113+
</svg>
114+
Compare Friends
115+
</Link>
116+
<Link
117+
href="/dashboard/settings"
118+
className="secondary-button inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium"
119+
>
120+
Settings
121+
</Link>
122+
<div className="sm:ml-auto">
44123
<ExportButton />
45124
</div>
46125
</div>
@@ -73,12 +152,100 @@ export default async function DashboardPage() {
73152
Generate an ATS-Friendly CV Backed by Your Real Code
74153
</h3>
75154

76-
<p className="text-sm text-[var(--muted-foreground)] leading-relaxed">
77-
Analyze your GitHub contributions, merged PRs, and lines of code
78-
changed to automatically generate professional bullet points for
79-
your target roles.
80-
</p>
155+
{/* Right: streak + coding time */}
156+
<div className="flex flex-col gap-6">
157+
<StreakTracker />
158+
<LocalCodingTime />
159+
<CodingTimeWidget />
160+
</div>
161+
162+
{/* Repo analytics explorer — full width */}
163+
<div className="mt-6">
164+
<LazyWidget fallback={<SkeletonCard />}>
165+
<RepoAnalyticsExplorer />
166+
</LazyWidget>
167+
</div>
168+
169+
{/* -- Row 2: PR metrics + Community metrics -- */}
170+
<div id="pull-requests" className="mt-6 grid grid-cols-1 gap-6 scroll-mt-24 md:grid-cols-2">
171+
<PRMetrics />
172+
<CommunityMetrics />
173+
</div>
174+
175+
{/* PR breakdown + commit time — 2-col so charts have room */}
176+
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
177+
<LazyWidget fallback={<SkeletonCard />}>
178+
<PRBreakdownChart />
179+
</LazyWidget>
180+
<LazyWidget fallback={<SkeletonCard />}>
181+
<CommitTimeChart />
182+
</LazyWidget>
183+
</div>
184+
185+
{/* Activity ring — full width */}
186+
<div className="mt-6">
187+
<LazyWidget fallback={<SkeletonCard />}>
188+
<ActivityRingChart />
189+
</LazyWidget>
190+
</div>
191+
192+
{/* Coding activity insights — full width */}
193+
<div className="mt-6">
194+
<LazyWidget fallback={<SkeletonCard />}>
195+
<CodingActivityInsightsCard />
196+
</LazyWidget>
197+
</div>
198+
199+
{/* PR review trend — full width */}
200+
<div className="mt-6">
201+
<LazyWidget fallback={<SkeletonCard />}>
202+
<PRReviewTrendChart />
203+
</LazyWidget>
204+
</div>
205+
206+
{/* -- Row 3: Issues (2/3) + CI analytics (1/3) -- */}
207+
<div id="goals" className="mt-6 grid grid-cols-1 gap-6 scroll-mt-24 lg:grid-cols-3">
208+
<div className="lg:col-span-2">
209+
<LazyWidget fallback={<SkeletonCard />}>
210+
<RepoAnalyticsExplorer />
211+
</LazyWidget>
212+
</div>
213+
214+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full">
215+
<div className="flex flex-col gap-6 w-full overflow-hidden">
216+
<PRMetrics />
217+
<LazyWidget fallback={<SkeletonCard />}>
218+
<PRBreakdownChart />
219+
</LazyWidget>
220+
<LazyWidget fallback={<SkeletonCard />}>
221+
<PRReviewTrendChart />
222+
</LazyWidget>
223+
<LazyWidget fallback={<SkeletonCard />}>
224+
<DiscussionsWidget />
225+
</LazyWidget>
226+
</div>
227+
<div className="flex flex-col gap-6 w-full overflow-hidden">
228+
<CommunityMetrics />
229+
<LazyWidget fallback={<SkeletonCard />}>
230+
<PinnedReposWidget />
231+
</LazyWidget>
232+
<LazyWidget fallback={<SkeletonCard />}>
233+
<TopRepos />
234+
</LazyWidget>
235+
<LazyWidget fallback={<SkeletonCard />}>
236+
<InactiveRepositoriesCard />
237+
</LazyWidget>
81238
</div>
239+
</div>
240+
</div>
241+
</section>
242+
243+
{/* 4. GOALS & INSIGHTS */}
244+
<section id="goals" className="mt-14 space-y-6 scroll-mt-28 mb-12">
245+
<div className="flex items-center gap-3 border-b border-white/10 pb-4">
246+
<div className="h-8 w-1.5 rounded-full bg-purple-500 shadow-[0_0_15px_rgba(168,85,247,0.5)]"></div>
247+
<h2 className="text-2xl font-bold tracking-tight">Goals & Insights</h2>
248+
</div>
82249

83250
<Link
84251
href="/dashboard/career-intelligence"

src/app/friend-compare/page.tsx

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { useSession } from "next-auth/react";
5+
import { redirect } from "next/navigation";
6+
import FriendComparison from "@/components/FriendComparison";
7+
import dynamic from "next/dynamic";
8+
import Link from "next/link";
9+
10+
const ContributionGraph = dynamic(
11+
() => import("@/components/ContributionGraph"),
12+
{ ssr: false }
13+
);
14+
15+
export default function FriendComparePage() {
16+
const { data: session, status } = useSession();
17+
const [showCommitActivity, setShowCommitActivity] = useState(false);
18+
const [compareUsername, setCompareUsername] = useState<string | null>(null);
19+
20+
useEffect(() => {
21+
if (status === "unauthenticated") {
22+
redirect("/");
23+
}
24+
}, [status]);
25+
26+
useEffect(() => {
27+
const handleShowCommitActivity = (e: Event) => {
28+
const customEvent = e as CustomEvent<{ username?: string }>;
29+
const username = customEvent.detail?.username;
30+
setCompareUsername(username || null);
31+
setShowCommitActivity(true);
32+
};
33+
34+
const handleClearCommitActivity = () => {
35+
setShowCommitActivity(false);
36+
setCompareUsername(null);
37+
};
38+
39+
window.addEventListener("devtrack:show-commit-activity", handleShowCommitActivity as EventListener);
40+
window.addEventListener("devtrack:clear-compare-user", handleClearCommitActivity);
41+
return () => {
42+
window.removeEventListener("devtrack:show-commit-activity", handleShowCommitActivity as EventListener);
43+
window.removeEventListener("devtrack:clear-compare-user", handleClearCommitActivity);
44+
};
45+
}, []);
46+
47+
// When showCommitActivity becomes true, dispatch the compare event after a tick
48+
useEffect(() => {
49+
if (showCommitActivity && compareUsername) {
50+
// Dispatch after the component has fully mounted (1000ms delay ensures dynamic import + listener setup)
51+
const timer = setTimeout(() => {
52+
window.dispatchEvent(
53+
new CustomEvent("devtrack:compare-user", {
54+
detail: { username: compareUsername },
55+
})
56+
);
57+
// Scroll to the element
58+
const element = document.getElementById("contribution-activity");
59+
if (element) {
60+
const elementPosition = element.getBoundingClientRect().top;
61+
const offsetPosition = elementPosition + window.pageYOffset - 100;
62+
window.scrollTo({ top: offsetPosition, behavior: "smooth" });
63+
}
64+
}, 1000);
65+
return () => clearTimeout(timer);
66+
}
67+
}, [showCommitActivity, compareUsername]);
68+
69+
// Auto-show commit activity if a friend was persisted on page refresh
70+
useEffect(() => {
71+
if (typeof window !== "undefined") {
72+
try {
73+
const persistedFriend = localStorage.getItem("devtrack:compare_username");
74+
if (persistedFriend) {
75+
setCompareUsername(persistedFriend);
76+
setShowCommitActivity(true);
77+
}
78+
} catch {
79+
// Silently fail if localStorage is not available
80+
}
81+
}
82+
}, []);
83+
84+
if (status === "loading") {
85+
return (
86+
<div className="min-h-screen bg-[var(--background)] p-4 text-[var(--foreground)] transition-colors md:p-8 flex items-center justify-center">
87+
<div className="text-center">
88+
<div className="h-12 w-12 bg-[var(--card-muted)] rounded-lg animate-pulse mx-auto mb-4" />
89+
<p className="text-[var(--muted-foreground)]">Loading...</p>
90+
</div>
91+
</div>
92+
);
93+
}
94+
95+
return (
96+
<div className="min-h-screen bg-[var(--background)] p-4 text-[var(--foreground)] transition-colors md:p-8">
97+
{/* Header */}
98+
<div className="mb-8 max-w-6xl mx-auto">
99+
<Link
100+
href="/dashboard"
101+
className="inline-flex items-center gap-2 px-3 py-2 mb-4 text-sm font-medium rounded-lg bg-[var(--control)] text-[var(--foreground)] hover:bg-[var(--card)] transition-colors border border-[var(--border)] hover:border-[var(--accent)] hover:text-[var(--accent)]"
102+
>
103+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
104+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
105+
</svg>
106+
Dashboard
107+
</Link>
108+
<h1 className="text-3xl md:text-4xl font-extrabold bg-gradient-to-r from-[var(--foreground)] via-[var(--foreground)] to-[var(--accent)] bg-clip-text text-transparent">
109+
Friend Comparison
110+
</h1>
111+
<p className="mt-2 text-[var(--muted-foreground)]">
112+
Compare your GitHub stats with friends and see how you stack up
113+
</p>
114+
</div>
115+
116+
{/* Main content */}
117+
<div className="max-w-6xl mx-auto space-y-6">
118+
<FriendComparison />
119+
120+
{/* Commit Activity Comparison - Only rendered when button is clicked */}
121+
{showCommitActivity && (
122+
<ContributionGraph />
123+
)}
124+
</div>
125+
</div>
126+
);
127+
}

0 commit comments

Comments
 (0)