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
2 changes: 1 addition & 1 deletion e2e/dashboard-widgets.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ function mockMetricResponse(url) {
};
}
if (url.includes("/api/metrics/languages")) {
return { languages: [{ language: "TypeScript", count: 12 }] };
return { languages: [{ name: "TypeScript", bytes: 12000, percentage: 100 }] };
}
if (url.includes("/api/metrics/streak")) {
return {
Expand Down
14 changes: 10 additions & 4 deletions e2e/dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ async function injectMockSession(page: import("@playwright/test").Page) {
route.fulfill({
contentType: "application/json",
body: JSON.stringify({
languages: [{ language: "TypeScript", count: 20 }],
languages: [{ name: "TypeScript", bytes: 120000, percentage: 100 }],
}),
})
);
Expand Down Expand Up @@ -379,7 +379,13 @@ test("[Dashboard E2E] no uncaught console errors on dashboard load", async ({
!e.includes("at RootLayout") &&
!e.includes("react-dev-overlay") &&
!e.includes("Failed to load resource") &&
!e.includes("Warning: ") && // Catch React warnings that get printed as errors
!e.includes("Warning: ") &&
!e.includes("sw.js") &&
!e.includes("ServiceWorker") &&
!e.includes("worker-src") &&
!e.includes("_vercel/") &&
!e.includes("429") &&
!e.includes("Too Many Requests") &&
e.trim() !== "div" &&
e.trim() !== "span" &&
e.trim() !== "p"
Expand All @@ -392,8 +398,8 @@ test("[Dashboard E2E] weekly summary widget renders", async ({ page }) => {
await expect(
page.getByRole("heading", { name: "Dashboard", exact: true })
).toBeVisible({ timeout: 30_000 });
// Weekly summary section should appear somewhere on the dashboard.
// WeeklySummaryCard renders an <h2> with the text "This Week".
await expect(
page.getByRole("heading", { name: "This Week" }).first()
page.getByRole("heading", { name: "This Week", exact: true }).first()
).toBeVisible({ timeout: 10_000 });
});
312 changes: 238 additions & 74 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,99 +1,263 @@
import TodayFocusHero from "@/components/TodayFocusHero";
import LazyWidget from "@/components/LazyWidget";
import DiscussionsWidget from "@/components/DiscussionsWidget";
import CommunityMetrics from "@/components/CommunityMetrics";
import GoalTracker from "@/components/GoalTracker";
import DashboardHeader from "@/components/DashboardHeader";
import StreakTracker from "@/components/StreakTracker";
import TopRepos from "@/components/TopRepos";
import PinnedReposWidget from "@/components/PinnedReposWidget";
import InactiveRepositoriesCard from "@/components/InactiveRepositoriesCard";
import LanguagesCard from "@/components/LanguagesCard";
import CIAnalytics from "@/components/CIAnalytics";
import IssueMetrics from "@/components/IssueMetrics";
import StreakAtRiskBanner from "@/components/StreakAtRiskBanner";
import RepoAnalyticsExplorer from "@/components/repo-analytics/RepoAnalyticsExplorer";
import dynamic from "next/dynamic";
import WeeklySummaryCard from "@/components/WeeklySummaryCard";
import { AIMentorWidget } from "@/components/AIMentorWidget";
import ExportButton from "@/components/ExportButton";
import Link from "next/link";
import { ChevronRight } from "lucide-react";
import PersonalRecords from "@/components/PersonalRecords";
import LocalCodingTime from "@/components/LocalCodingTime";
import CodingTimeWidget from "@/components/CodingTimeWidget";
import RecentActivity from "@/components/RecentActivity";
import { authOptions } from "@/lib/auth";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import DashboardSSEProvider from "@/components/DashboardSSEProvider";
import StreakAtRiskBanner from "@/components/StreakAtRiskBanner";
import ThrottleBanner from "@/components/ThrottleBanner";
import CustomizableDashboard from "@/components/dashboard/CustomizableDashboard";
import MilestonePlanner from "@/components/MilestonePlanner";

const SkeletonCard = () => (
<div
role="status"
aria-busy="true"
aria-live="polite"
className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm"
>
<div className="h-6 w-48 bg-[var(--card-muted)] rounded mb-4 animate-pulse" />
<div className="h-40 bg-[var(--card-muted)] rounded animate-pulse" />
</div>
);

const ContributionGraphSkeleton = () => (
<div className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm">
<h2 className="text-lg font-semibold text-[var(--foreground)]">Your Commits</h2>
<div className="mt-3 h-40 rounded bg-[var(--card-muted)] animate-pulse" />
</div>
);

const PRMetricsSkeleton = () => (
<div className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm">
<h2 className="text-lg font-semibold text-[var(--card-foreground)]">PR Analytics</h2>
<div className="mt-3 h-40 rounded bg-[var(--card-muted)] animate-pulse" />
</div>
);

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

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

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

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

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

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

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

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

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

export default async function DashboardPage() {
const session = await getServerSession(authOptions);
if (!session) redirect("/");

return (
<DashboardSSEProvider>
<div className="min-h-screen bg-[var(--background)] px-4 py-8 text-[var(--foreground)] transition-colors sm:px-6 lg:px-8 max-w-[1600px] mx-auto">
<div className="min-h-screen bg-[var(--background)] p-4 text-[var(--foreground)] transition-colors md:p-8">
<DashboardHeader />

{/* Quick actions */}
<div className="mt-10 mb-6 flex flex-col sm:flex-row items-start sm: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-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"
>
Year in Code
</Link>

<Link
href="/dashboard/settings"
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"
>
Settings
</Link>
</div>

{/* Action bar */}
<div className="mb-6 flex flex-wrap items-stretch justify-center gap-2 sm:justify-end">
<Link
href="/wrapped"
className="flex min-w-0 flex-1 items-center justify-center rounded-lg border border-[var(--accent)] bg-[var(--accent-soft)] px-4 py-2 text-center text-sm font-semibold text-[var(--accent)] transition-opacity hover:opacity-90 sm:min-w-[140px] sm:flex-none"
>
Year in Code
</Link>
<Link
href="/dashboard/settings"
className="secondary-button flex min-w-0 flex-1 items-center justify-center rounded-xl px-4 py-2 text-center text-sm font-medium sm:min-w-[140px] sm:flex-none"
>
Settings
</Link>
<div className="w-full sm:w-auto">
<ExportButton />
</div>
</div>

{/* Info Banners */}
<div className="space-y-3 mb-8">
<ThrottleBanner />
<StreakAtRiskBanner />
</div>

{/* Today Focus Section */}
<section className="mt-10 mb-10">
<TodayFocusHero userName={session.user?.name ?? null} />
</section>

{/* Featured Section */}
<section className="mt-10 mb-12">
<div className="relative overflow-hidden rounded-xl border border-[var(--border)] bg-gradient-to-r from-violet-950/20 via-indigo-950/10 to-transparent p-8 shadow-lg hover:shadow-xl transition-shadow flex flex-col md:flex-row justify-between items-start md:items-center gap-8">
<div className="space-y-3 max-w-xl flex-1">
<div className="flex items-center gap-2">
<span className="text-[10px] uppercase font-bold text-violet-400 tracking-wider px-2.5 py-1 rounded bg-violet-500/10 border border-violet-500/20">
New Feature
</span>
<span className="text-xs text-[var(--muted-foreground)] font-medium">
AI Resume Generator
</span>
</div>

<h3 className="text-xl font-bold text-[var(--foreground)] leading-tight">
Generate an ATS-Friendly CV Backed by Your Real Code
</h3>

<p className="text-sm text-[var(--muted-foreground)] leading-relaxed">
Analyze your GitHub contributions, merged PRs, and lines of code
changed to automatically generate professional bullet points for
your target roles.
</p>
</div>

<Link
href="/dashboard/career-intelligence"
className="inline-flex items-center gap-2 rounded-lg bg-gradient-to-r from-violet-600 to-indigo-600 px-6 py-3 text-sm font-bold text-white shadow-md shadow-indigo-500/20 hover:shadow-lg hover:scale-[1.03] transition-all whitespace-nowrap active:scale-95"
>
Build Resume
<ChevronRight className="h-5 w-5" />
</Link>
<StreakAtRiskBanner />

{/* Weekly summary — full width */}
<div className="mt-6">
<WeeklySummaryCard />
</div>

{/* Personal records + AI mentor side by side */}
<div className="mt-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
<PersonalRecords />
<AIMentorWidget />
</div>

{/* ── Row 1: Contribution graph (2/3) + Streak sidebar (1/3) ── */}
<div className="mt-6 grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left: contribution graph + heatmap */}
<div className="lg:col-span-2 flex flex-col gap-6">
<ContributionGraph />
<LazyWidget fallback={<SkeletonCard />}>
<ContributionHeatmap />
</LazyWidget>
</div>

{/* Right: streak + coding time */}
<div className="flex flex-col gap-6">
<StreakTracker />
<LocalCodingTime />
<CodingTimeWidget />
</div>
</section>
<section className="mt-8">
<MilestonePlanner />
</section>
<CustomizableDashboard />
</div>

{/* Friend comparison — full width, below the fold */}
<div className="mt-6">
<LazyWidget fallback={<SkeletonCard />}>
<FriendComparison />
</LazyWidget>
</div>

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

{/* ── Row 2: PR metrics + Community metrics ── */}
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<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 className="mt-6 grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<LazyWidget fallback={<SkeletonCard />}>
<IssueMetrics />
</LazyWidget>
</div>
<LazyWidget fallback={<SkeletonCard />}>
<CIAnalytics />
</LazyWidget>
</div>

{/* Discussions — full width */}
<div className="mt-6">
<LazyWidget fallback={<SkeletonCard />}>
<DiscussionsWidget />
</LazyWidget>
</div>

{/* Pinned spotlight repos — full width */}
<div className="mt-6">
<LazyWidget fallback={<SkeletonCard />}>
<PinnedReposWidget />
</LazyWidget>
</div>

{/* Inactive repo reminder — full width */}
<div className="mt-6">
<LazyWidget fallback={<SkeletonCard />}>
<InactiveRepositoriesCard />
</LazyWidget>
</div>

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

{/* Recent activity — full width */}
<div className="mt-6">
<LazyWidget fallback={<SkeletonCard />}>
<RecentActivity />
</LazyWidget>
</div>
</div>
</DashboardSSEProvider>
);
}
}
2 changes: 1 addition & 1 deletion src/components/LanguageBreakdown.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"use client";
"use client";

import { useEffect, useState } from "react";
import { useAccount } from "@/components/AccountContext";
Expand Down
Loading
Loading