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
23 changes: 22 additions & 1 deletion e2e/dashboard-widgets.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,27 @@ test.beforeEach(async ({ page }) => {
body: "data: {}\n\n",
});
});

await page.route("**/api/user/dashboard-layout**", async (route) => {
if (route.request().method() === "GET") {
await route.fulfill({
contentType: "application/json",
body: JSON.stringify({ layout: null }),
});
} else {
await route.fulfill({
contentType: "application/json",
body: JSON.stringify({ ok: true }),
});
}
});

await page.route("**/api/daily-note**", async (route) => {
await route.fulfill({
contentType: "application/json",
body: JSON.stringify({ note: null }),
});
});
});
test("dashboard widgets render with mocked metrics", async ({ page }) => {
await page.goto("/dashboard", { waitUntil: "load" });
Expand Down Expand Up @@ -329,7 +350,7 @@ function mockMetricResponse(url) {
};
}
if (url.includes("/api/streak/freeze")) {
return { freezes: [] };
return { hasFreeze: false, freezeDate: null };
}
if (url.includes("/api/integrations/jira")) {
return null;
Expand Down
20 changes: 20 additions & 0 deletions e2e/notifications.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ test.beforeEach(async ({ page }) => {
});
});

await page.route("**/api/notifications**", async (route) => {
await route.fulfill({ contentType: "application/json", body: JSON.stringify({ notifications: [], unreadCount: 0 }) });
});

await page.route("**/api/user/github-accounts", async (route) => {
await route.fulfill({
contentType: "application/json",
Expand Down Expand Up @@ -255,13 +259,29 @@ test.beforeEach(async ({ page }) => {
});
}

await page.route("**/api/streak/freeze**", async (route) => {
await route.fulfill({ contentType: "application/json", body: JSON.stringify({ hasFreeze: false, freezeDate: null }) });
});

await page.route("**/api/stream**", async (route) => {
await route.fulfill({
status: 200,
contentType: "text/event-stream",
body: "data: {}\n\n",
});
});

await page.route("**/api/user/dashboard-layout**", async (route) => {
if (route.request().method() === "GET") {
await route.fulfill({ contentType: "application/json", body: JSON.stringify({ layout: null }) });
} else {
await route.fulfill({ contentType: "application/json", body: JSON.stringify({ ok: true }) });
}
});

await page.route("**/api/daily-note**", async (route) => {
await route.fulfill({ contentType: "application/json", body: JSON.stringify({ note: null }) });
});
});

test("notification bell opens and closes drawer", async ({ page }) => {
Expand Down
31 changes: 17 additions & 14 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@ export default async function DashboardPage() {
<DashboardHeader />

{/* Quick actions */}
<div className="mt-8 mb-4 flex flex-col sm:flex-row items-center justify-between gap-4">
<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-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]"
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-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]"
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>
Expand All @@ -44,32 +44,35 @@ export default async function DashboardPage() {
</div>
</div>

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

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

<section className="mt-14">
<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-6 shadow-lg flex flex-col md:flex-row justify-between items-center gap-6">
<div className="space-y-2 max-w-xl">
{/* 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 py-0.5 rounded bg-violet-500/10 border border-violet-500/20">
<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)]">
<span className="text-xs text-[var(--muted-foreground)] font-medium">
AI Resume Generator
</span>
</div>

<h3 className="text-lg font-bold text-[var(--foreground)]">
<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-xs text-[var(--muted-foreground)] leading-relaxed">
<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.
Expand All @@ -78,10 +81,10 @@ export default async function DashboardPage() {

<Link
href="/dashboard/career-intelligence"
className="inline-flex items-center gap-1.5 rounded-lg bg-gradient-to-r from-violet-600 to-indigo-600 px-5 py-2.5 text-xs font-bold text-white shadow-md shadow-indigo-500/20 hover:scale-[1.03] transition-all whitespace-nowrap"
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-4 w-4" />
<ChevronRight className="h-5 w-5" />
</Link>
</div>
</section>
Expand Down
42 changes: 25 additions & 17 deletions src/components/dashboard/CustomizableDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,17 +204,17 @@ const SECTION_ANCHOR_IDS: Record<DashboardSectionId, string> = {
};

const SECTION_ACCENT_CLASSES: Record<DashboardSectionId, string> = {
overview: "bg-[var(--accent)] shadow-[0_0_15px_var(--accent)]",
activity: "bg-emerald-500 shadow-[0_0_15px_rgba(16,185,129,0.5)]",
analytics: "bg-blue-500 shadow-[0_0_15px_rgba(59,130,246,0.5)]",
goals: "bg-purple-500 shadow-[0_0_15px_rgba(168,85,247,0.5)]",
overview: "h-1 bg-gradient-to-r from-[var(--accent)] to-[var(--accent)]/60 rounded-full shadow-md",
activity: "h-1 bg-gradient-to-r from-emerald-500 to-emerald-500/60 rounded-full shadow-md",
analytics: "h-1 bg-gradient-to-r from-blue-500 to-blue-500/60 rounded-full shadow-md",
goals: "h-1 bg-gradient-to-r from-purple-500 to-purple-500/60 rounded-full shadow-md",
};

const SECTION_GRID_CLASSES: Record<DashboardSectionId, string> = {
overview: "grid grid-cols-1 xl:grid-cols-2 gap-6 w-full",
activity: "grid grid-cols-1 xl:grid-cols-3 gap-6 w-full",
analytics: "grid grid-cols-1 lg:grid-cols-2 gap-6 w-full",
goals: "grid grid-cols-1 xl:grid-cols-3 gap-6 w-full",
overview: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-6 w-full",
activity: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full",
analytics: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-6 w-full",
goals: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full",
};

const WIDGET_SPAN_CLASSES: Partial<Record<DashboardWidgetId, string>> = {
Expand Down Expand Up @@ -584,7 +584,7 @@ export default function CustomizableDashboard() {
};

return (
<div className="mt-14">
<div className="mt-10 px-0.5">
<DashboardLayoutToolbar
isEditing={isEditing}
hiddenWidgets={layout.hidden}
Expand All @@ -611,24 +611,32 @@ export default function CustomizableDashboard() {
<section
key={sectionId}
id={SECTION_ANCHOR_IDS[sectionId]}
className={`space-y-6 scroll-mt-28 ${
sectionId === "goals" ? "mb-12" : "mb-14"
className={`scroll-mt-28 ${
sectionId === "goals" ? "mb-16" : "mb-14"
}`}
>
<div className="flex items-center gap-3 border-b border-white/10 pb-4">
<div className="space-y-2 mb-6">
<div
className={`h-8 w-1.5 rounded-full ${SECTION_ACCENT_CLASSES[sectionId]}`}
className={`w-12 ${SECTION_ACCENT_CLASSES[sectionId]}`}
/>
<h2 className="text-2xl font-bold tracking-tight">
{DASHBOARD_SECTION_LABELS[sectionId]}
</h2>
<div>
<h2 className="text-3xl font-bold tracking-tight text-[var(--foreground)]">
{DASHBOARD_SECTION_LABELS[sectionId]}
</h2>
<p className="mt-1 text-sm text-[var(--muted-foreground)] font-medium">
{sectionId === "overview" && "Quick summary of your development profile"}
{sectionId === "activity" && "Your coding patterns and contributions"}
{sectionId === "analytics" && "In-depth analysis of your repositories and code"}
{sectionId === "goals" && "Track progress, milestones, and insights"}
</p>
</div>
</div>

<SortableContext
items={sectionWidgets}
strategy={rectSortingStrategy}
>
<div className={SECTION_GRID_CLASSES[sectionId]}>
<div className={`${SECTION_GRID_CLASSES[sectionId]} auto-rows-max`}>
{sectionWidgets.map((widgetId) => (
<SortableDashboardWidget
key={widgetId}
Expand Down
41 changes: 22 additions & 19 deletions src/components/dashboard/DashboardLayoutToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,38 +22,39 @@ export default function DashboardLayoutToolbar({
onShowWidget,
}: DashboardLayoutToolbarProps) {
return (
<div className="mb-6 rounded-xl border border-[var(--border)] bg-[var(--card)]/80 p-4 shadow-sm backdrop-blur">
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div>
<h2 className="text-sm font-semibold text-[var(--foreground)]">
Dashboard Layout
<div className="mb-8 rounded-xl border border-[var(--border)] bg-[var(--card)]/50 backdrop-blur-sm p-6 shadow-sm hover:shadow-md transition-shadow duration-300">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="space-y-1">
<h2 className="text-base font-semibold text-[var(--foreground)] flex items-center gap-2">
<SlidersHorizontal className="h-5 w-5 text-[var(--accent)]" aria-hidden="true" />
Customize Your Dashboard
</h2>
<p className="mt-1 text-xs text-[var(--muted-foreground)]">
Reorder widgets, hide unused cards, and reset the dashboard anytime.
<p className="text-sm text-[var(--muted-foreground)] leading-relaxed">
Reorder widgets by dragging, hide cards you don&apos;t need, and reset to default anytime.
</p>
</div>

<div className="flex flex-wrap items-center gap-2">
<div className="flex flex-wrap items-center gap-2 self-start md:self-auto">
{isEditing ? (
<button
type="button"
onClick={onReset}
className="inline-flex items-center gap-2 rounded-lg border border-[var(--border)] bg-[var(--control)] px-4 py-2 text-sm font-medium text-[var(--foreground)] transition hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-[var(--accent)]"
className="inline-flex items-center gap-2 rounded-lg border border-[var(--border)] bg-[var(--card)] px-4 py-2.5 text-sm font-medium text-[var(--foreground)] transition-all hover:bg-[var(--card)]/80 hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-[var(--accent)] active:scale-95"
>
<RotateCcw className="h-4 w-4" aria-hidden="true" />
Reset
Reset Layout
</button>
) : null}

<button
type="button"
onClick={() => onEditingChange(!isEditing)}
className="inline-flex items-center gap-2 rounded-lg border border-[var(--accent)] bg-[var(--accent)]/10 px-4 py-2 text-sm font-semibold text-[var(--accent)] transition hover:bg-[var(--accent)]/20 focus:outline-none focus:ring-2 focus:ring-[var(--accent)]"
className="inline-flex items-center gap-2 rounded-lg border border-[var(--accent)] bg-[var(--accent)]/10 px-4 py-2.5 text-sm font-semibold text-[var(--accent)] transition-all hover:bg-[var(--accent)]/20 hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-[var(--accent)] active:scale-95"
>
{isEditing ? (
<>
<Check className="h-4 w-4" aria-hidden="true" />
Done
Done Editing
</>
) : (
<>
Expand All @@ -66,28 +67,30 @@ export default function DashboardLayoutToolbar({
</div>

{isEditing ? (
<div className="mt-4 border-t border-[var(--border)] pt-4">
<h3 className="text-xs font-semibold uppercase tracking-wide text-[var(--muted-foreground)]">
Hidden widgets
<div className="mt-6 border-t border-[var(--border)] pt-6 space-y-3">
<h3 className="text-xs font-semibold uppercase tracking-wider text-[var(--muted-foreground)] flex items-center gap-2">
<Eye className="h-3.5 w-3.5" aria-hidden="true" />
Hidden Widgets ({hiddenWidgets.length})
</h3>

{hiddenWidgets.length > 0 ? (
<div className="mt-3 flex flex-wrap gap-2">
<div className="flex flex-wrap gap-2">
{hiddenWidgets.map((widgetId) => (
<button
key={widgetId}
type="button"
onClick={() => onShowWidget(widgetId)}
className="inline-flex items-center gap-2 rounded-full border border-[var(--border)] bg-[var(--control)] px-3 py-1.5 text-xs font-medium text-[var(--foreground)] transition hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-[var(--accent)]"
className="inline-flex items-center gap-2 rounded-full border border-[var(--border)] bg-[var(--card)]/70 px-3 py-2 text-xs font-medium text-[var(--foreground)] transition-all hover:bg-[var(--accent)]/20 hover:border-[var(--accent)]/50 hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-[var(--accent)] active:scale-95"
title={`Click to show ${DASHBOARD_WIDGET_LABELS[widgetId]}`}
>
<Eye className="h-3.5 w-3.5" aria-hidden="true" />
Show {DASHBOARD_WIDGET_LABELS[widgetId]}
</button>
))}
</div>
) : (
<p className="mt-2 text-xs text-[var(--muted-foreground)]">
No widgets are hidden.
<p className="text-xs text-[var(--muted-foreground)] bg-[var(--card)]/30 rounded-lg px-3 py-2 italic">
✓ All widgets are visible. No hidden widgets to restore.
</p>
)}
</div>
Expand Down
16 changes: 8 additions & 8 deletions src/components/dashboard/SortableDashboardWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,16 @@ export default function SortableDashboardWidget({
ref={setNodeRef}
style={style}
className={`relative min-w-0 ${className} ${
isDragging ? "opacity-70" : ""
}`}
isDragging ? "opacity-60 scale-95" : ""
} transition-all duration-150`}
>
{isEditing ? (
<div className="absolute right-3 top-3 z-20 flex items-center gap-2">
<div className="absolute right-2 top-2 z-20 flex items-center gap-1.5">
<button
type="button"
aria-label={`Drag widget: ${title}`}
title={`Drag ${title}`}
className="touch-none rounded-lg border border-[var(--border)] bg-[var(--card)]/95 p-2 text-[var(--muted-foreground)] shadow-sm backdrop-blur transition hover:text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--accent)]"
title={`Drag to reorder ${title}`}
className="touch-none rounded-lg border border-[var(--border)] bg-[var(--card)]/95 p-2 text-[var(--muted-foreground)] shadow-sm backdrop-blur transition-all hover:text-[var(--foreground)] hover:shadow-md hover:border-[var(--accent)]/50 focus:outline-none focus:ring-2 focus:ring-[var(--accent)] cursor-grab active:cursor-grabbing"
{...attributes}
{...listeners}
>
Expand All @@ -67,7 +67,7 @@ export default function SortableDashboardWidget({
aria-label={`Hide widget: ${title}`}
title={`Hide ${title}`}
onClick={() => onHide(id)}
className="rounded-lg border border-[var(--border)] bg-[var(--card)]/95 p-2 text-[var(--muted-foreground)] shadow-sm backdrop-blur transition hover:text-red-400 focus:outline-none focus:ring-2 focus:ring-[var(--accent)]"
className="rounded-lg border border-[var(--border)] bg-[var(--card)]/95 p-2 text-[var(--muted-foreground)] shadow-sm backdrop-blur transition-all hover:text-red-500 hover:shadow-md hover:border-red-500/50 focus:outline-none focus:ring-2 focus:ring-[var(--accent)]"
>
<EyeOff className="h-4 w-4" aria-hidden="true" />
</button>
Expand All @@ -77,8 +77,8 @@ export default function SortableDashboardWidget({
<div
className={
isEditing
? "rounded-xl outline outline-1 outline-dashed outline-[var(--accent)]/60 outline-offset-4 transition"
: undefined
? "rounded-xl outline outline-2 outline-dashed outline-[var(--accent)]/40 outline-offset-2 transition-all"
: "rounded-xl hover:shadow-lg transition-shadow duration-200"
}
>
{children}
Expand Down
Loading