Skip to content
Draft
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
210 changes: 103 additions & 107 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import DiscussionsWidget from "@/components/DiscussionsWidget";
import CommunityMetrics from "@/components/CommunityMetrics";
import GoalTracker from "@/components/GoalTracker";
import DashboardHeader from "@/components/DashboardHeader";
import DashboardHeader, { DashboardSyncProvider } from "@/components/DashboardHeader";
import StreakTracker from "@/components/StreakTracker";
import TopRepos from "@/components/TopRepos";
import PinnedRepos from "@/components/PinnedRepos";
Expand Down Expand Up @@ -106,132 +106,128 @@ export default async function DashboardPage() {
return (
<DashboardSSEProvider>
<div className="min-h-screen bg-[var(--background)] p-4 text-[var(--foreground)] transition-colors md:p-8">
<DashboardHeader />
<div className="mb-6 flex justify-end items-center gap-2">
<Link
href="/wrapped"
className="rounded-lg border border-[var(--accent)] bg-[var(--accent-soft)] px-4 py-2 text-sm font-semibold text-[var(--accent)] hover:opacity-90 transition-opacity min-w-[140px] flex items-center justify-center"
>
Year in Code
</Link>
<Link
href="/dashboard/settings"
className="secondary-button flex min-w-[140px] items-center justify-center rounded-xl px-4 py-2 text-sm font-medium"
>
Settings
</Link>
<ExportButton />
</div>
<StreakAtRiskBanner />

<div className="mb-6 mt-6">
<Link href="/wrapped">
<div className="relative overflow-hidden rounded-xl bg-gradient-to-r from-purple-600 via-pink-600 to-fuchsia-600 p-6 shadow-lg transition-transform hover:scale-[1.01]">
<div className="relative z-10 flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-white">Your Year in Code is here! ✨</h2>
<p className="mt-1 text-white/90">Discover your top languages, longest streaks, and coding habits of the year.</p>
</div>
<div className="rounded-full bg-white px-6 py-2 font-bold text-purple-600">
View Wrapped
<DashboardSyncProvider>
<DashboardHeader />
</DashboardSyncProvider>
<div className="mb-6 flex justify-end items-center gap-2">
<Link
href="/wrapped"
className="rounded-lg border border-[var(--accent)] bg-[var(--accent-soft)] px-4 py-2 text-sm font-semibold text-[var(--accent)] hover:opacity-90 transition-opacity min-w-[140px] flex items-center justify-center"
>
Year in Code
</Link>
<Link
href="/dashboard/settings"
className="secondary-button flex min-w-[140px] items-center justify-center rounded-xl px-4 py-2 text-sm font-medium"
>
Settings
</Link>
<ExportButton />
</div>
<StreakAtRiskBanner />

<div className="mb-6 mt-6">
<Link href="/wrapped">
<div className="relative overflow-hidden rounded-xl bg-gradient-to-r from-purple-600 via-pink-600 to-fuchsia-600 p-6 shadow-lg transition-transform hover:scale-[1.01]">
<div className="relative z-10 flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-white">Your Year in Code is here! ✨</h2>
<p className="mt-1 text-white/90">Discover your top languages, longest streaks, and coding habits of the year.</p>
</div>
<div className="rounded-full bg-white px-6 py-2 font-bold text-purple-600">
View Wrapped
</div>
</div>
<div className="absolute -right-10 -top-10 h-40 w-40 rounded-full bg-white/20 blur-3xl"></div>
<div className="absolute -bottom-10 -left-10 h-40 w-40 rounded-full bg-black/20 blur-3xl"></div>
</div>
<div className="absolute -right-10 -top-10 h-40 w-40 rounded-full bg-white/20 blur-3xl"></div>
<div className="absolute -bottom-10 -left-10 h-40 w-40 rounded-full bg-black/20 blur-3xl"></div>
</div>
</Link>
</div>
</Link>
</div>

<div className="mb-6">
<WeeklySummaryCard />
</div>
<div className="mb-6">
<WeeklySummaryCard />
</div>

<div className="mb-6">
<AIMentorWidget />
</div>
<div className="mb-6">
<AIMentorWidget />
</div>

<div className="mb-6">
<PersonalRecords />
</div>
<div className="mb-6">
<PersonalRecords />
</div>

{/* Row 1: Contribution graph + Streak + Local Coding Time */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<ContributionGraph />
<div className="mt-6">
<ContributionHeatmap />
</div>
<div className="mt-6">
<FriendComparison />
</div>
<div className="mt-6">
<RepoAnalyticsExplorer />
{/* Row 1: Contribution graph + Streak + Local Coding Time */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<ContributionGraph />
<div className="mt-6">
<ContributionHeatmap />
</div>
</div>
</div>

<div>
<StreakTracker />
<LocalCodingTime />
<div className="mt-6">
<CodingTimeWidget />
<div className="flex flex-col gap-6">
<StreakTracker />
<LocalCodingTime />
<div className="mt-6">
<CodingTimeWidget />
</div>
</div>
</div>
</div>

{/* Row 2: PR metrics, community metrics, PR breakdown & Time Chart */}
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
<PRMetrics />
<CommunityMetrics />
<PRBreakdownChart />
<CommitTimeChart />
</div>
{/* Row 2b: Activity Ring Chart */}
<div className="mt-6">
<ActivityRingChart />
</div>
{/* Row 2: PR metrics, community metrics, PR breakdown & Time Chart */}
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
<PRMetrics />
<CommunityMetrics />
<PRBreakdownChart />
<CommitTimeChart />
</div>
{/* Row 2b: Activity Ring Chart */}
<div className="mt-6">
<ActivityRingChart />
</div>

<div className="mt-6">
<CodingActivityInsightsCard />
</div>
<div className="mt-6">
<CodingActivityInsightsCard />
</div>

<div className="mt-6">
<PRReviewTrendChart />
</div>
<div className="mt-6">
<PRReviewTrendChart />
</div>

{/* Row 3: Issue metrics + CI analytics */}
<div className="mt-6 grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<IssueMetrics />
{/* Row 3: Issue metrics + CI analytics */}
<div className="mt-6 grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<IssueMetrics />
</div>
<CIAnalytics />
</div>
{/* Row 3b: Discussion activity */}
<div className="mt-6">
<DiscussionsWidget />
</div>
<CIAnalytics />
</div>
{/* Row 3b: Discussion activity */}
<div className="mt-6">
<DiscussionsWidget />
</div>

{/* Row 4: Pinned repositories */}
<div className="mt-6">
<PinnedRepos />
</div>
{/* Row 4: Pinned repositories */}
<div className="mt-6">
<PinnedRepos />
</div>

{/* Row 5: Inactive repository reminder */}
<div className="mt-6">
<InactiveRepositoriesCard />
</div>
{/* Row 5: Inactive repository reminder */}
<div className="mt-6">
<InactiveRepositoriesCard />
</div>

{/* Row 6: Top repos + Language breakdown + Goal tracker */}
<div className="mt-6 grid grid-cols-1 lg:grid-cols-3 gap-6">
<TopRepos />
<LanguageBreakdown />
<GoalTracker />
</div>
{/* Row 6: Top repos + Language breakdown + Goal tracker */}
<div className="mt-6 grid grid-cols-1 lg:grid-cols-3 gap-6">
<TopRepos />
<LanguageBreakdown />
<GoalTracker />
</div>

{/* Row 7: Recent GitHub activity */}
<div className="mt-6">
<RecentActivity />
{/* Row 7: Recent GitHub activity */}
<div className="mt-6">
<RecentActivity />
</div>
</div>
</div>
</DashboardSSEProvider>
);
}
98 changes: 97 additions & 1 deletion src/components/DashboardHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,94 @@
"use client";

import NotificationBell from "@/components/NotificationBell";
import { useEffect, useState } from "react";
import {
createContext,
ReactNode,
useContext,
useEffect,
useLayoutEffect,
useMemo,
useState,
} from "react";
import { useSession } from "next-auth/react";
import AccountToggle from "@/components/AccountToggle";
import SignOutButton from "@/components/SignOutButton";
import ThemeToggle from "@/components/ThemeToggle";
import UserAvatar from "@/components/UserAvatar";
import KeyboardShortcuts from "@/components/KeyboardShortcuts";

type DashboardSyncContextValue = {
lastSynced: Date | null;
};

const DashboardSyncContext = createContext<DashboardSyncContextValue>({
lastSynced: null,
});

function getRequestPath(input: RequestInfo | URL): string {
if (typeof input === "string") {
return input.startsWith("http") ? new URL(input).pathname : input;
}

if (input instanceof URL) {
return input.pathname;
}

return new URL(input.url).pathname;
}

function isDashboardDataRequest(input: RequestInfo | URL): boolean {
const requestPath = getRequestPath(input);

return (
requestPath.startsWith("/api/metrics/") ||
requestPath === "/api/goals" ||
requestPath.startsWith("/api/goals/") ||
requestPath.startsWith("/api/streak/") ||
requestPath === "/api/user/github-accounts" ||
requestPath.startsWith("/api/badge/")
);
}

export function DashboardSyncProvider({ children }: { children: ReactNode }) {
const [lastSynced, setLastSynced] = useState<Date | null>(null);

useLayoutEffect(() => {
const originalFetch = window.fetch;

window.fetch = async (...args) => {
const response = await originalFetch(...args);

if (response.ok && isDashboardDataRequest(args[0])) {
setLastSynced(new Date());
}

return response;
};

return () => {
window.fetch = originalFetch;
};
}, []);

const value = useMemo(() => ({ lastSynced }), [lastSynced]);

return (
<DashboardSyncContext.Provider value={value}>
{children}
</DashboardSyncContext.Provider>
);
}

function useDashboardSync() {
return useContext(DashboardSyncContext);
}

export default function DashboardHeader() {
const { data: session } = useSession();
const [isPublic, setIsPublic] = useState<boolean | null>(null);
const { lastSynced } = useDashboardSync();
const [now, setNow] = useState(() => Date.now());

useEffect(() => {
if (!session) {
Expand All @@ -38,6 +115,20 @@ export default function DashboardHeader() {
loadSettings();
}, [session]);

useEffect(() => {
if (!lastSynced) return;

const interval = setInterval(() => {
setNow(Date.now());
}, 60000);

return () => clearInterval(interval);
}, [lastSynced]);

const minutesAgo = lastSynced
? Math.floor((now - lastSynced.getTime()) / 60000)
: null;

return (
<header className="mb-8 rounded-3xl border border-[var(--border)] bg-[var(--card)]/95 p-5 shadow-[var(--shadow-soft)] backdrop-blur-md transition-all duration-300 hover:shadow-[var(--shadow-medium)] md:p-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-5">
Expand All @@ -53,6 +144,11 @@ export default function DashboardHeader() {
>
coding activity at a glance
</p>
{minutesAgo !== null && (
<p className="mt-1 text-xs text-[var(--muted-foreground)]">
{minutesAgo <= 0 ? "Synced just now" : `Synced ${minutesAgo} min ago`}
</p>
)}
</div>

{/* Right Section */}
Expand Down
Loading
Loading