Skip to content

Commit 9f0b8c6

Browse files
feat(ui): app shell with tabbed dashboard, remove Edit Layout, refocus landing (#3531)
* fix: public profile not found, resume analysis 502, AI model errors, leaderboard rate limits - Public profile: the canonical-username redirect ran inside a try/catch. redirect() works by throwing, so the catch turned every mixed-case profile link (e.g. /u/Priyanshu-byte-coder) into "Profile Not Found". The page already redirects correctly after loading. - Resume: the contribution query asked for 50 repositories with per-commit diff stats in one request; GitHub answers 502 past its ~10s budget. Repositories are now paged 10 at a time and commits carry no diff stats (line totals come from merged pull requests). - AI features: Groq returned model_not_found for llama-3.3-70b-versatile on the production key, so personality, AI insights, resume wording and project tutor all failed (project tutor still used the long-retired llama3-8b-8192). A shared helper tries GROQ_MODEL, then falls back through available models and remembers the one that works. - Leaderboard: per-user stats paged up to 10 commit searches for streaks and hit GitHub's secondary rate limit. Stats now come from contribution data via GraphQL (streak from the contribution calendar, as on GitHub profiles), using each user's stored token. - SSE stream: connections now end at 4 minutes instead of being killed at the 300s function limit, which logged an error per connection. * fix(profile): QR code dialog could not be closed ShareProfileSection rendered ProfileQrModal unconditionally, so the dialog was always open and its close button only toggled state nothing read. It now renders only while open. * feat(ui): app shell with tabbed dashboard, remove Edit Layout, refocus landing Information architecture - Signed-in pages share an app shell: a slim sidebar (drawer on mobile) and a compact top bar with sync, shortcuts, notifications, account switcher and an account menu. The marketing navbar and footer no longer appear inside the app. - The single 34-widget dashboard is split into tabs, each its own route: Overview, Activity, Pull requests, Repositories, Goals, Community. Insights group: Year in Code (highlighted), Resume, Personality, Leaderboard. Repo health and repo comparison live under Repositories. - Overview is a summary: four week-at-a-glance numbers from one request, a Year in Code spotlight, commits chart, today's focus, AI mentor, personal records and recent activity. - Edit Layout (drag-and-drop, layout persistence, /api/user/dashboard-layout) is removed along with the @dnd-kit packages. Widgets keep their labelled regions and live summaries through a slim WidgetRegion. Declutter - Today's Focus is a compact card instead of a second hero with its own greeting; Resume and Personality use the standard page header instead of oversized banners. - Removed the Discussion Activity widget: it duplicated Community Discussions and read fields the API never returns, so it showed blanks. - Goals: one-line empty state instead of a second "Create Goal" call to action above the form. - Fixed "Merged (30dd)", "1 days to 7", truncated personal records and theme name, and the PR stat grid leaving a lone tile. - Avatars loaded from github.com/<login>.png are blocked by the CSP and rendered broken (leaderboard, account switcher, settings, rooms); they now use avatars.githubusercontent.com. Landing - One product tour organised like the app replaces three overlapping feature grids; adds a Year in Code section; drops the stat tiles that repeated the stats row. Tests: e2e specs visit the tab that now hosts each widget; the visual workflow can regenerate baselines on demand (update_snapshots input). * fix(ui): show a dash instead of an endless skeleton when weekly stats fail * ci(visual): regenerate every baseline when asked, not only failing ones * test(visual): baselines for the new app shell and landing page * chore(api): regenerate OpenAPI after removing the dashboard-layout route
1 parent 5769540 commit 9f0b8c6

60 files changed

Lines changed: 2972 additions & 3764 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/visual-regression.yml‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ on:
1515
- "playwright.visual.config.mjs"
1616
- ".github/workflows/visual-regression.yml"
1717
workflow_dispatch:
18+
inputs:
19+
update_snapshots:
20+
description: "Regenerate baselines and upload them as an artifact (for intentional UI changes)"
21+
type: boolean
22+
default: false
1823

1924
permissions:
2025
contents: read
@@ -64,8 +69,21 @@ jobs:
6469
run: pnpm run build
6570

6671
- name: Run visual regression tests
72+
if: ${{ !inputs.update_snapshots }}
6773
run: npx playwright test -c playwright.visual.config.mjs
6874

75+
- name: Regenerate baselines
76+
if: ${{ inputs.update_snapshots }}
77+
run: npx playwright test -c playwright.visual.config.mjs --update-snapshots=all
78+
79+
- name: Upload regenerated baselines
80+
if: ${{ inputs.update_snapshots }}
81+
uses: actions/upload-artifact@v7
82+
with:
83+
name: visual-baselines
84+
path: tests/snapshots/
85+
retention-days: 7
86+
6987
- name: Upload visual regression artifacts
7088
uses: actions/upload-artifact@v7
7189
if: failure()

‎e2e/dashboard-widgets.spec.js‎

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -229,16 +229,25 @@ test("dashboard widgets render with mocked metrics", async ({ page }) => {
229229
await expect(
230230
page.getByRole("heading", { name: "Dashboard", exact: true })
231231
).toBeVisible({ timeout: 30000 });
232-
await expect(page.getByRole("heading", { name: "Your Commits" }).first()).toBeVisible(
233-
{ timeout: 10000 }
232+
await expect(
233+
page.getByRole("heading", { name: "Your Commits" }).first()
234+
).toBeVisible({ timeout: 10000 });
235+
236+
await page.goto("/dashboard/pull-requests", { waitUntil: "load" });
237+
await expect(
238+
page.getByRole("heading", { level: 1, name: "Pull requests" })
239+
).toBeVisible({ timeout: 30000 });
240+
await expect(page.getByRole("heading", { name: "PR Analytics" })).toHaveCount(
241+
1
234242
);
235-
await expect(page.getByRole("heading", { name: "PR Analytics" })).toHaveCount(1);
236243
await expect(page.getByRole("heading", { name: "PR Analytics" })).toBeVisible(
237244
{ timeout: 10000 }
238245
);
246+
247+
await page.goto("/dashboard/goals", { waitUntil: "load" });
239248
await expect(
240249
page.getByRole("heading", { name: "Goals", exact: true }).first()
241-
).toBeVisible({ timeout: 10000 });
250+
).toBeVisible({ timeout: 30000 });
242251
await expect(page.getByText("Make 10 commits")).toBeVisible({
243252
timeout: 10000,
244253
});
@@ -279,9 +288,9 @@ test("goal form posts a new goal", async ({ page }) => {
279288
}
280289
});
281290

282-
await page.goto("/dashboard", { waitUntil: "load" });
291+
await page.goto("/dashboard/goals", { waitUntil: "load" });
283292
await expect(
284-
page.getByRole("heading", { name: "Dashboard", exact: true })
293+
page.getByRole("heading", { level: 1, name: "Goals" })
285294
).toBeVisible({ timeout: 30000 });
286295
await page.getByLabel("Goal title").fill("Ship one PR");
287296
await page.getByLabel("Target").fill("1");
@@ -349,9 +358,9 @@ function mockMetricResponse(url) {
349358
thisWeek: { opened: 3, merged: 2 },
350359
lastWeek: { opened: 1, merged: 1 },
351360
},
352-
issues: {
353-
thisWeek: { opened: 4, closed: 2 },
354-
lastWeek: { opened: 3, closed: 1 }
361+
issues: {
362+
thisWeek: { opened: 4, closed: 2 },
363+
lastWeek: { opened: 3, closed: 1 },
355364
},
356365
productivityScore: { current: 85, previous: 80 },
357366
activeDays: {
@@ -360,13 +369,9 @@ function mockMetricResponse(url) {
360369
},
361370
streak: 3,
362371
topRepo: "demo/repo",
363-
repoBreakdown: [
364-
{ repoName: "demo/devtrack", commits: 10 },
365-
],
366-
dailyCommits: [
367-
{ date: "2023-10-07", commits: 5 },
368-
],
369-
mostActiveDay: "2023-10-07"
372+
repoBreakdown: [{ repoName: "demo/devtrack", commits: 10 }],
373+
dailyCommits: [{ date: "2023-10-07", commits: 5 }],
374+
mostActiveDay: "2023-10-07",
370375
};
371376
}
372377
if (url.includes("/api/metrics/compare")) {

‎e2e/dashboard.spec.ts‎

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -244,17 +244,17 @@ async function injectMockSession(page: import("@playwright/test").Page) {
244244
thisWeek: { opened: 3, merged: 2 },
245245
lastWeek: { opened: 1, merged: 1 },
246246
},
247-
issues: {
248-
thisWeek: { opened: 5, closed: 0 },
249-
lastWeek: { opened: 3, closed: 0 }
247+
issues: {
248+
thisWeek: { opened: 5, closed: 0 },
249+
lastWeek: { opened: 3, closed: 0 },
250250
},
251251
productivityScore: { current: 88, previous: 75 },
252252
activeDays: { thisWeek: 5, lastWeek: 4 },
253253
streak: 7,
254254
topRepo: "demo/devtrack",
255255
repoBreakdown: [],
256256
dailyCommits: [],
257-
mostActiveDay: "2023-10-07"
257+
mostActiveDay: "2023-10-07",
258258
}),
259259
})
260260
);
@@ -267,7 +267,7 @@ async function injectMockSession(page: import("@playwright/test").Page) {
267267
activeCount: 12,
268268
growthTrend: 15,
269269
sponsors: [],
270-
sparklineData: []
270+
sparklineData: [],
271271
}),
272272
})
273273
);
@@ -394,19 +394,21 @@ test("[Dashboard E2E] Commits widget renders", async ({ page }) => {
394394
});
395395

396396
test("[Dashboard E2E] PR Analytics widget renders", async ({ page }) => {
397-
await page.goto("/dashboard", { waitUntil: "domcontentloaded" });
397+
await page.goto("/dashboard/pull-requests", {
398+
waitUntil: "domcontentloaded",
399+
});
398400
await expect(
399-
page.getByRole("heading", { name: "Dashboard", exact: true })
401+
page.getByRole("heading", { level: 1, name: "Pull requests" })
400402
).toBeVisible({ timeout: 30_000 });
401403
await scrollToWidget(page, "PR Analytics");
402404
});
403405

404406
test("[Dashboard E2E] Goals widget renders with mocked goal", async ({
405407
page,
406408
}) => {
407-
await page.goto("/dashboard", { waitUntil: "domcontentloaded" });
409+
await page.goto("/dashboard/goals", { waitUntil: "domcontentloaded" });
408410
await expect(
409-
page.getByRole("heading", { name: "Dashboard", exact: true })
411+
page.getByRole("heading", { level: 1, name: "Goals" })
410412
).toBeVisible({ timeout: 30_000 });
411413
await scrollToWidget(page, "Goals");
412414
await expect(page.getByText("Make 10 commits")).toBeVisible({
@@ -455,9 +457,9 @@ test("[Dashboard E2E] no uncaught console errors on dashboard load", async ({
455457
});
456458

457459
test("[Dashboard E2E] weekly summary widget renders", async ({ page }) => {
458-
await page.goto("/dashboard", { waitUntil: "domcontentloaded" });
460+
await page.goto("/dashboard/activity", { waitUntil: "domcontentloaded" });
459461
await expect(
460-
page.getByRole("heading", { name: "Dashboard", exact: true })
462+
page.getByRole("heading", { level: 1, name: "Activity" })
461463
).toBeVisible({ timeout: 30_000 });
462464
// Weekly summary section should appear somewhere on the dashboard.
463465
await expect(
@@ -504,23 +506,19 @@ test("[Dashboard E2E] analytics widgets render once without duplicate metric req
504506
if (request.method() !== "GET") return;
505507

506508
const url = request.url();
507-
if (
508-
TRACKED_METRICS_ENDPOINTS.some((endpoint) => url.includes(endpoint))
509-
) {
509+
if (TRACKED_METRICS_ENDPOINTS.some((endpoint) => url.includes(endpoint))) {
510510
requestUrls.push(url);
511511
}
512512
});
513513

514-
await page.goto("/dashboard", { waitUntil: "domcontentloaded" });
514+
await page.goto("/dashboard/pull-requests", {
515+
waitUntil: "domcontentloaded",
516+
});
515517
await expect(
516-
page.getByRole("heading", { name: "Dashboard", exact: true })
518+
page.getByRole("heading", { level: 1, name: "Pull requests" })
517519
).toBeVisible({ timeout: 30_000 });
518520

519521
const lazyAnalyticsWidgets = [
520-
{
521-
region: "Repository Analytics",
522-
heading: "Repo Analytics",
523-
},
524522
{
525523
region: "PR Breakdown",
526524
heading: "PR Breakdown",
@@ -531,7 +529,9 @@ test("[Dashboard E2E] analytics widgets render once without duplicate metric req
531529
},
532530
] as const;
533531

534-
await page.getByRole("region", { name: "PR Metrics" }).scrollIntoViewIfNeeded();
532+
await page
533+
.getByRole("region", { name: "PR Metrics" })
534+
.scrollIntoViewIfNeeded();
535535
await expect(
536536
page.getByRole("heading", { name: "PR Analytics", level: 2 })
537537
).toBeVisible({ timeout: 15_000 });
@@ -551,6 +551,17 @@ test("[Dashboard E2E] analytics widgets render once without duplicate metric req
551551
).toHaveCount(1);
552552
}
553553

554+
await page.goto("/dashboard/repositories", { waitUntil: "domcontentloaded" });
555+
await page
556+
.getByRole("region", { name: "Repository Analytics" })
557+
.scrollIntoViewIfNeeded();
558+
await expect(
559+
page.getByRole("heading", { name: "Repo Analytics", level: 2 })
560+
).toBeVisible({ timeout: 15_000 });
561+
await expect(
562+
page.getByRole("region", { name: "Repository Analytics" })
563+
).toHaveCount(1);
564+
554565
for (const endpoint of TRACKED_METRICS_ENDPOINTS) {
555566
const { min, max } = METRICS_REQUEST_BOUNDS[endpoint];
556567
await expect
@@ -559,8 +570,8 @@ test("[Dashboard E2E] analytics widgets render once without duplicate metric req
559570
const count = countMetricRequests(requestUrls, endpoint);
560571
return count >= min && count <= max;
561572
},
562-
{ timeout: 15_000 },
573+
{ timeout: 15_000 }
563574
)
564575
.toBe(true);
565576
}
566-
});
577+
});

‎e2e/goals.spec.ts‎

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -106,28 +106,35 @@ async function setupGoalsMocks(page: import("@playwright/test").Page) {
106106
await page.route("**/api/goals/sync**", (route) =>
107107
route.fulfill({
108108
contentType: "application/json",
109-
body: JSON.stringify({ ok: true, last_synced_at: new Date().toISOString() }),
109+
body: JSON.stringify({
110+
ok: true,
111+
last_synced_at: new Date().toISOString(),
112+
}),
110113
})
111114
);
112115

113116
await installDashboardApiMocks(page);
114117
}
115118

116119
async function openGoalsWidget(page: import("@playwright/test").Page) {
117-
await page.goto("/dashboard", { waitUntil: "domcontentloaded" });
120+
await page.goto("/dashboard/goals", { waitUntil: "domcontentloaded" });
118121
await expect(
119-
page.getByRole("heading", { name: "Dashboard", exact: true })
122+
page.getByRole("heading", { level: 1, name: "Goals" })
120123
).toBeVisible({ timeout: 30_000 });
121124
await scrollToWidget(page, "Goals");
122125
}
123126

124127
test("[Goals E2E] goals widget renders on dashboard", async ({ page }) => {
125-
page.on("console", msg => console.log("BROWSER CONSOLE:", msg.text()));
128+
page.on("console", (msg) => console.log("BROWSER CONSOLE:", msg.text()));
126129
await setupGoalsMocks(page);
127130

128131
await page.route(/\/api\/goals(\?|$)/, (route) => {
129132
if (route.request().method() === "POST") {
130-
return route.fulfill({ status: 201, contentType: "application/json", body: JSON.stringify({ ok: true }) });
133+
return route.fulfill({
134+
status: 201,
135+
contentType: "application/json",
136+
body: JSON.stringify({ ok: true }),
137+
});
131138
}
132139
return route.fulfill({
133140
contentType: "application/json",
@@ -173,8 +180,14 @@ test("[Goals E2E] creating a goal sends POST /api/goals with correct payload", a
173180
await page.getByLabel("Unit", { exact: true }).selectOption("prs");
174181
await page.getByRole("button", { name: "Create goal" }).click();
175182

176-
await expect.poll(() => goalPosts.filter(Boolean), { timeout: 10_000 }).toHaveLength(1);
177-
expect(goalPosts.find(Boolean)).toMatchObject({ title: "Ship one PR", target: 1, unit: "prs" });
183+
await expect
184+
.poll(() => goalPosts.filter(Boolean), { timeout: 10_000 })
185+
.toHaveLength(1);
186+
expect(goalPosts.find(Boolean)).toMatchObject({
187+
title: "Ship one PR",
188+
target: 1,
189+
unit: "prs",
190+
});
178191
});
179192

180193
test("[Goals E2E] newly created goal appears in the goals list", async ({
@@ -198,10 +211,17 @@ test("[Goals E2E] newly created goal appears in the goals list", async ({
198211
// Use regex so only /api/goals is matched — NOT /api/goals/sync or /api/goals/:id.
199212
await page.route(/\/api\/goals(\?|$)/, async (route) => {
200213
if (route.request().method() === "POST") {
201-
const body = route.request().postDataJSON() as Record<string, unknown> | null;
214+
const body = route.request().postDataJSON() as Record<
215+
string,
216+
unknown
217+
> | null;
202218
// Guard: /api/goals/sync POSTs (if leaked) have null body — skip them.
203219
if (!body) {
204-
return route.fulfill({ status: 201, contentType: "application/json", body: JSON.stringify({ ok: true }) });
220+
return route.fulfill({
221+
status: 201,
222+
contentType: "application/json",
223+
body: JSON.stringify({ ok: true }),
224+
});
205225
}
206226
goalsStore.push({
207227
id: `g-new-${Date.now()}`,
@@ -227,7 +247,9 @@ test("[Goals E2E] newly created goal appears in the goals list", async ({
227247

228248
await openGoalsWidget(page);
229249

230-
await expect(page.getByText("Existing Goal")).toBeVisible({ timeout: 10_000 });
250+
await expect(page.getByText("Existing Goal")).toBeVisible({
251+
timeout: 10_000,
252+
});
231253

232254
// Scroll goal form into view and wait for it to be interactive.
233255
const titleInput = page.getByLabel("Goal title");
@@ -241,7 +263,9 @@ test("[Goals E2E] newly created goal appears in the goals list", async ({
241263
await page.getByRole("button", { name: "Create goal" }).click();
242264

243265
// The new goal should appear without a page reload.
244-
await expect(page.getByText("Ship five PRs")).toBeVisible({ timeout: 10_000 });
266+
await expect(page.getByText("Ship five PRs")).toBeVisible({
267+
timeout: 10_000,
268+
});
245269
});
246270

247271
test("[Goals E2E] deleting a goal removes it from the list", async ({
@@ -264,7 +288,11 @@ test("[Goals E2E] deleting a goal removes it from the list", async ({
264288

265289
await page.route(/\/api\/goals(\?|$)/, async (route) => {
266290
if (route.request().method() === "POST") {
267-
return route.fulfill({ status: 201, contentType: "application/json", body: JSON.stringify({ ok: true }) });
291+
return route.fulfill({
292+
status: 201,
293+
contentType: "application/json",
294+
body: JSON.stringify({ ok: true }),
295+
});
268296
}
269297
return route.fulfill({
270298
contentType: "application/json",
@@ -294,15 +322,22 @@ test("[Goals E2E] deleting a goal removes it from the list", async ({
294322
});
295323

296324
await openGoalsWidget(page);
297-
await expect(page.getByText("Goal to Delete")).toBeVisible({ timeout: 10_000 });
325+
await expect(page.getByText("Goal to Delete")).toBeVisible({
326+
timeout: 10_000,
327+
});
298328

299329
// Click the delete button (trash icon) next to this goal.
300-
const goalRow = page.locator("li").filter({ hasText: "Goal to Delete" }).first();
330+
const goalRow = page
331+
.locator("li")
332+
.filter({ hasText: "Goal to Delete" })
333+
.first();
301334
await goalRow.getByRole("button", { name: /delete goal/i }).click();
302335

303336
// A confirmation modal appears — click "Permanently Delete" to confirm.
304337
await page.getByRole("button", { name: /permanently delete/i }).click();
305338

306339
// Goal should be gone.
307-
await expect(page.getByText("Goal to Delete", { exact: true })).not.toBeVisible({ timeout: 10_000 });
340+
await expect(
341+
page.getByText("Goal to Delete", { exact: true })
342+
).not.toBeVisible({ timeout: 10_000 });
308343
});

0 commit comments

Comments
 (0)