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
4 changes: 4 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ const nextConfig = {
protocol: "https",
hostname: "github.githubassets.com",
},
{
protocol: "https",
hostname: "via.placeholder.com",
},
],
},
async headers() {
Expand Down
106 changes: 80 additions & 26 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import LazyWidget from "@/components/LazyWidget";
import LazyWidget from "@/components/LazyWidget";
import DiscussionsWidget from "@/components/DiscussionsWidget";
import CommunityMetrics from "@/components/CommunityMetrics";
import GoalTracker from "@/components/GoalTracker";
Expand All @@ -22,6 +22,7 @@ import PersonalRecords from "@/components/PersonalRecords";
import LocalCodingTime from "@/components/LocalCodingTime";
import CodingTimeWidget from "@/components/CodingTimeWidget";
import RecentActivity from "@/components/RecentActivity";
import FriendComparison from "@/components/FriendComparison";
import { authOptions } from "@/lib/auth";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
Expand Down Expand Up @@ -58,11 +59,6 @@ const CodingActivityInsightsCard = dynamic(
{ ssr: false, loading: () => <SkeletonCard /> },
);

const FriendComparison = dynamic(
() => import("@/components/FriendComparison"),
{ ssr: false, loading: () => <SkeletonCard /> },
);

const ActivityRingChart = dynamic(
() => import("@/components/ActivityRingChart"),
{ ssr: false, loading: () => <SkeletonCard /> },
Expand Down Expand Up @@ -108,24 +104,29 @@ export default async function DashboardPage() {
<DashboardHeader />

{/* Quick actions */}
<div className="mt-8 mb-8 flex flex-col sm:flex-row items-center justify-between gap-4">
{/* Left side actions */}
<div className="flex flex-wrap items-center gap-3 w-full sm:w-auto">
<Link
href="/wrapped"
className="inline-flex w-full sm:w-auto justify-center items-center gap-2 rounded-xl 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:scale-[1.02]"
>
Year in Code
</Link>
<Link
href="/dashboard/settings"
className="inline-flex w-full sm:w-auto justify-center items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-5 py-2.5 text-sm font-medium transition-all hover:bg-white/10 hover:scale-[1.02]"
>
Settings
</Link>
</div>
{/* Right side exports */}
<div className="w-full sm:w-auto">
<div className="mt-4 flex flex-wrap items-center gap-2 sm:gap-3">
<Link
href="/wrapped"
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"
>
✨ Year in Code
</Link>
<Link
href="/friend-compare"
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"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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" />
</svg>
Compare Friends
</Link>
<Link
href="/dashboard/settings"
className="secondary-button inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium"
>
Settings
</Link>
<div className="sm:ml-auto">
<ExportButton />
</div>
</div>
Expand Down Expand Up @@ -198,8 +199,60 @@ export default async function DashboardPage() {
<h2 className="text-2xl font-bold tracking-tight">Analytics & Repositories</h2>
</div>

{/* Repo Analytics Explorer spans full width */}
<div className="w-full overflow-hidden">
{/* Right: streak + coding time */}
<div className="flex flex-col gap-6">
<StreakTracker />
<LocalCodingTime />
<CodingTimeWidget />
</div>

{/* Repo analytics explorer — full width */}
<div className="mt-6">
<LazyWidget fallback={<SkeletonCard />}>
<RepoAnalyticsExplorer />
</LazyWidget>
</div>

{/* -- Row 2: PR metrics + Community metrics -- */}
<div id="pull-requests" className="mt-6 grid grid-cols-1 gap-6 scroll-mt-24 md:grid-cols-2">
<PRMetrics />
<CommunityMetrics />
</div>

{/* PR breakdown + commit time — 2-col so charts have room */}
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<LazyWidget fallback={<SkeletonCard />}>
<PRBreakdownChart />
</LazyWidget>
<LazyWidget fallback={<SkeletonCard />}>
<CommitTimeChart />
</LazyWidget>
</div>

{/* Activity ring — full width */}
<div className="mt-6">
<LazyWidget fallback={<SkeletonCard />}>
<ActivityRingChart />
</LazyWidget>
</div>

{/* Coding activity insights — full width */}
<div className="mt-6">
<LazyWidget fallback={<SkeletonCard />}>
<CodingActivityInsightsCard />
</LazyWidget>
</div>

{/* PR review trend — full width */}
<div className="mt-6">
<LazyWidget fallback={<SkeletonCard />}>
<PRReviewTrendChart />
</LazyWidget>
</div>

{/* -- Row 3: Issues (2/3) + CI analytics (1/3) -- */}
<div id="goals" className="mt-6 grid grid-cols-1 gap-6 scroll-mt-24 lg:grid-cols-3">
<div className="lg:col-span-2">
<LazyWidget fallback={<SkeletonCard />}>
<RepoAnalyticsExplorer />
</LazyWidget>
Expand Down Expand Up @@ -231,6 +284,7 @@ export default async function DashboardPage() {
</LazyWidget>
</div>
</div>
</div>
</section>

{/* 4. GOALS & INSIGHTS */}
Expand Down
127 changes: 127 additions & 0 deletions src/app/friend-compare/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"use client";

import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import { redirect } from "next/navigation";
import FriendComparison from "@/components/FriendComparison";
import dynamic from "next/dynamic";
import Link from "next/link";

const ContributionGraph = dynamic(
() => import("@/components/ContributionGraph"),
{ ssr: false }
);

export default function FriendComparePage() {
const { data: session, status } = useSession();
const [showCommitActivity, setShowCommitActivity] = useState(false);
const [compareUsername, setCompareUsername] = useState<string | null>(null);

useEffect(() => {
if (status === "unauthenticated") {
redirect("/");
}
}, [status]);

useEffect(() => {
const handleShowCommitActivity = (e: Event) => {
const customEvent = e as CustomEvent<{ username?: string }>;
const username = customEvent.detail?.username;
setCompareUsername(username || null);
setShowCommitActivity(true);
};

const handleClearCommitActivity = () => {
setShowCommitActivity(false);
setCompareUsername(null);
};

window.addEventListener("devtrack:show-commit-activity", handleShowCommitActivity as EventListener);
window.addEventListener("devtrack:clear-compare-user", handleClearCommitActivity);
return () => {
window.removeEventListener("devtrack:show-commit-activity", handleShowCommitActivity as EventListener);
window.removeEventListener("devtrack:clear-compare-user", handleClearCommitActivity);
};
}, []);

// When showCommitActivity becomes true, dispatch the compare event after a tick
useEffect(() => {
if (showCommitActivity && compareUsername) {
// Dispatch after the component has fully mounted (1000ms delay ensures dynamic import + listener setup)
const timer = setTimeout(() => {
window.dispatchEvent(
new CustomEvent("devtrack:compare-user", {
detail: { username: compareUsername },
})
);
// Scroll to the element
const element = document.getElementById("contribution-activity");
if (element) {
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - 100;
window.scrollTo({ top: offsetPosition, behavior: "smooth" });
}
}, 1000);
return () => clearTimeout(timer);
}
}, [showCommitActivity, compareUsername]);

// Auto-show commit activity if a friend was persisted on page refresh
useEffect(() => {
if (typeof window !== "undefined") {
try {
const persistedFriend = localStorage.getItem("devtrack:compare_username");
if (persistedFriend) {
setCompareUsername(persistedFriend);
setShowCommitActivity(true);
}
} catch {
// Silently fail if localStorage is not available
}
}
}, []);

if (status === "loading") {
return (
<div className="min-h-screen bg-[var(--background)] p-4 text-[var(--foreground)] transition-colors md:p-8 flex items-center justify-center">
<div className="text-center">
<div className="h-12 w-12 bg-[var(--card-muted)] rounded-lg animate-pulse mx-auto mb-4" />
<p className="text-[var(--muted-foreground)]">Loading...</p>
</div>
</div>
);
}

return (
<div className="min-h-screen bg-[var(--background)] p-4 text-[var(--foreground)] transition-colors md:p-8">
{/* Header */}
<div className="mb-8 max-w-6xl mx-auto">
<Link
href="/dashboard"
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)]"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Dashboard
</Link>
<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">
Friend Comparison
</h1>
<p className="mt-2 text-[var(--muted-foreground)]">
Compare your GitHub stats with friends and see how you stack up
</p>
</div>

{/* Main content */}
<div className="max-w-6xl mx-auto space-y-6">
<FriendComparison />

{/* Commit Activity Comparison - Only rendered when button is clicked */}
{showCommitActivity && (
<ContributionGraph />
)}
</div>
</div>
);
}
Loading
Loading