From ae8ce7b0062c6a5e3a1ecabc03861c856fbc8340 Mon Sep 17 00:00:00 2001 From: Mallya Date: Tue, 19 May 2026 22:40:08 +0530 Subject: [PATCH 1/7] fix: resolve onboarding PR conflicts and cleanup --- src/components/OnboardingTour.tsx | 89 +++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/components/OnboardingTour.tsx diff --git a/src/components/OnboardingTour.tsx b/src/components/OnboardingTour.tsx new file mode 100644 index 000000000..f769b967d --- /dev/null +++ b/src/components/OnboardingTour.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useEffect, useCallback } from "react"; +import { driver } from "driver.js"; + +interface OnboardingTourProps { + onComplete: () => void; +} + +// steps match the actual widget ids on the dashboard +const TOUR_STEPS = [ + { + element: "#widget-contribution-graph", + popover: { + title: "Contribution Graph", + description: "See your daily GitHub commit activity. Switch between 7, 14, 30, and 90 day views.", + }, + }, + { + element: "#widget-streak", + popover: { + title: "Streak Tracker", + description: "Your current commit streak — how many days in a row you've pushed code.", + }, + }, + { + element: "#widget-pr-metrics", + popover: { + title: "PR Analytics", + description: "Average review time, merge rate, and open vs closed pull request counts.", + }, + }, + { + element: "#widget-top-repos", + popover: { + title: "Top Repositories", + description: "Your most active repos ranked by commits. Click column headers to sort.", + }, + }, + { + element: "#widget-goals", + popover: { + title: "Weekly Goals", + description: "Set coding targets and track your progress automatically.", + }, + }, +]; + +export default function OnboardingTour({ onComplete }: OnboardingTourProps) { + const startTour = useCallback(() => { + const driverObj = driver({ + showProgress: true, + animate: true, + allowClose: true, + steps: TOUR_STEPS, + onDestroyStarted: () => { + // mark tour as seen whether user finishes or skips + onComplete(); + driverObj.destroy(); + }, + }); + + driverObj.drive(); + }, [onComplete]); + + // auto-start on mount + // inject driver.js styles once on mount + useEffect(() => { + const linkId = "driver-js-css"; + if (document.getElementById(linkId)) return; + const link = document.createElement("link"); + link.id = linkId; + link.rel = "stylesheet"; + link.href = "https://cdn.jsdelivr.net/npm/driver.js@1.3.1/dist/driver.css"; + document.head.appendChild(link); + return () => { + document.getElementById(linkId)?.remove(); + }; + }, []); + + // auto-start on mount + useEffect(() => { + // small delay so dashboard widgets have time to render first + const timer = setTimeout(startTour, 800); + return () => clearTimeout(timer); + }, [startTour]); + + return null; +} \ No newline at end of file From bd1ca2943b8d9fde2b683a3e592b0ef0c40e4443 Mon Sep 17 00:00:00 2001 From: Mallya Date: Thu, 21 May 2026 01:23:13 +0530 Subject: [PATCH 2/7] fix: static CSS import, indentation fix, migration file, EOF newlines --- src/components/OnboardingTour.tsx | 17 ++--------------- .../20260520000000_add_seen_onboarding.sql | 1 + 2 files changed, 3 insertions(+), 15 deletions(-) create mode 100644 supabase/migrations/20260520000000_add_seen_onboarding.sql diff --git a/src/components/OnboardingTour.tsx b/src/components/OnboardingTour.tsx index f769b967d..c6563b439 100644 --- a/src/components/OnboardingTour.tsx +++ b/src/components/OnboardingTour.tsx @@ -2,6 +2,7 @@ import { useEffect, useCallback } from "react"; import { driver } from "driver.js"; +import "driver.js/dist/driver.css"; interface OnboardingTourProps { onComplete: () => void; @@ -63,20 +64,6 @@ export default function OnboardingTour({ onComplete }: OnboardingTourProps) { driverObj.drive(); }, [onComplete]); - // auto-start on mount - // inject driver.js styles once on mount - useEffect(() => { - const linkId = "driver-js-css"; - if (document.getElementById(linkId)) return; - const link = document.createElement("link"); - link.id = linkId; - link.rel = "stylesheet"; - link.href = "https://cdn.jsdelivr.net/npm/driver.js@1.3.1/dist/driver.css"; - document.head.appendChild(link); - return () => { - document.getElementById(linkId)?.remove(); - }; - }, []); // auto-start on mount useEffect(() => { @@ -86,4 +73,4 @@ export default function OnboardingTour({ onComplete }: OnboardingTourProps) { }, [startTour]); return null; -} \ No newline at end of file +} diff --git a/supabase/migrations/20260520000000_add_seen_onboarding.sql b/supabase/migrations/20260520000000_add_seen_onboarding.sql new file mode 100644 index 000000000..0ccb48cd3 --- /dev/null +++ b/supabase/migrations/20260520000000_add_seen_onboarding.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN IF NOT EXISTS seen_onboarding BOOLEAN DEFAULT FALSE; From 3f1f96ad297d1a47e20e4a05d5d7fad4976af34e Mon Sep 17 00:00:00 2001 From: Mallya Date: Fri, 22 May 2026 00:41:02 +0530 Subject: [PATCH 3/7] fix: skip onboarding tour in E2E/webdriver environments --- src/components/OnboardingTour.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/OnboardingTour.tsx b/src/components/OnboardingTour.tsx index c6563b439..25e29f3d0 100644 --- a/src/components/OnboardingTour.tsx +++ b/src/components/OnboardingTour.tsx @@ -67,10 +67,11 @@ export default function OnboardingTour({ onComplete }: OnboardingTourProps) { // auto-start on mount useEffect(() => { - // small delay so dashboard widgets have time to render first - const timer = setTimeout(startTour, 800); - return () => clearTimeout(timer); - }, [startTour]); + // skip tour in test environments to avoid blocking E2E tests + if (typeof window !== "undefined" && window.navigator.webdriver) return; + const timer = setTimeout(startTour, 800); + return () => clearTimeout(timer); +}, [startTour]); return null; } From 0dd944113aa13d68199d6d8f1c872ef6a39043c8 Mon Sep 17 00:00:00 2001 From: Mallya Date: Sat, 23 May 2026 01:23:37 +0530 Subject: [PATCH 4/7] feat(onboarding): add first-time dashboard tour (#251) - Add OnboardingTour component using driver.js - Add Take Tour button in DashboardHeader - Extend settings PATCH to accept seen_onboarding field - Tour auto-starts for new users, skips in E2E environments --- src/app/api/user/settings/route.ts | 118 +++++++++++++++-------------- src/app/dashboard/page.tsx | 2 + src/components/DashboardHeader.tsx | 17 +++++ 3 files changed, 79 insertions(+), 58 deletions(-) diff --git a/src/app/api/user/settings/route.ts b/src/app/api/user/settings/route.ts index 572e8e14e..a7f51264f 100644 --- a/src/app/api/user/settings/route.ts +++ b/src/app/api/user/settings/route.ts @@ -11,10 +11,9 @@ import { cacheGet, cacheSet, cacheDelete } from "@/lib/metrics-cache"; export const dynamic = "force-dynamic"; async function fetchUserSettings(userId: string) { - // Tier 1: All columns const res1 = await supabaseAdmin .from("users") - .select("id, github_login, bio, is_public, public_since, show_weekly_goals, leaderboard_opt_in, pinned_repos, wakatime_api_key_encrypted, wakatime_api_key_iv, weekly_digest_opt_in, discord_webhook_url, timezone, webhook_url, discord_muted_until") + .select("id, github_login, bio, is_public, public_since, show_weekly_goals, leaderboard_opt_in, pinned_repos, wakatime_api_key_encrypted, wakatime_api_key_iv, weekly_digest_opt_in, discord_webhook_url, timezone, webhook_url, discord_muted_until, seen_onboarding") .eq("id", userId) .single(); @@ -66,10 +65,9 @@ async function fetchUserSettings(userId: string) { }; } - // Tier 2: Without bio, for deployments that have not run the latest migration. const res2 = await supabaseAdmin .from("users") - .select("id, github_login, is_public, public_since, show_weekly_goals, leaderboard_opt_in, pinned_repos, wakatime_api_key_encrypted, wakatime_api_key_iv, webhook_url") + .select("id, github_login, is_public, public_since, show_weekly_goals, leaderboard_opt_in, pinned_repos, wakatime_api_key_encrypted, wakatime_api_key_iv, webhook_url") .eq("id", userId) .single(); @@ -121,10 +119,9 @@ async function fetchUserSettings(userId: string) { }; } - // Tier 3: Without public_since and show_weekly_goals (added by migrations) const res3 = await supabaseAdmin .from("users") - .select("id, github_login, is_public, public_since, show_weekly_goals") + .select("id, github_login, is_public, public_since, show_weekly_goals") .eq("id", userId) .single(); @@ -139,6 +136,7 @@ async function fetchUserSettings(userId: string) { hasDiscordSettings: false, hasBio: false, hasDiscordMutedUntil: false, + hasWebhookUrl: false, leaderboard_opt_in: false, weekly_digest_opt_in: false, pinned_repos: [] as string[], @@ -146,6 +144,7 @@ async function fetchUserSettings(userId: string) { wakatime_api_key_iv: null, discord_webhook_url: null, timezone: "UTC", + webhook_url: null, discord_muted_until: null, }; } @@ -161,6 +160,7 @@ async function fetchUserSettings(userId: string) { hasDiscordSettings: false, hasBio: false, hasDiscordMutedUntil: false, + hasWebhookUrl: false, leaderboard_opt_in: false, weekly_digest_opt_in: false, pinned_repos: [] as string[], @@ -168,14 +168,14 @@ async function fetchUserSettings(userId: string) { wakatime_api_key_iv: null, discord_webhook_url: null, timezone: "UTC", + webhook_url: null, discord_muted_until: null, }; } - // Tier 4: Absolute minimum — columns guaranteed in every schema version const res4 = await supabaseAdmin .from("users") - .select("id, github_login, is_public") + .select("id, github_login, is_public") .eq("id", userId) .single(); @@ -190,6 +190,7 @@ async function fetchUserSettings(userId: string) { hasDiscordSettings: false, hasBio: false, hasDiscordMutedUntil: false, + hasWebhookUrl: false, leaderboard_opt_in: false, weekly_digest_opt_in: false, pinned_repos: [] as string[], @@ -197,6 +198,7 @@ async function fetchUserSettings(userId: string) { wakatime_api_key_iv: null, discord_webhook_url: null, timezone: "UTC", + webhook_url: null, discord_muted_until: null, }; } @@ -211,6 +213,7 @@ async function fetchUserSettings(userId: string) { hasDiscordSettings: false, hasBio: false, hasDiscordMutedUntil: false, + hasWebhookUrl: false, leaderboard_opt_in: false, weekly_digest_opt_in: false, pinned_repos: [] as string[], @@ -218,6 +221,7 @@ async function fetchUserSettings(userId: string) { wakatime_api_key_iv: null, discord_webhook_url: null, timezone: "UTC", + webhook_url: null, discord_muted_until: null, }; } @@ -231,14 +235,11 @@ export async function GET(req: NextRequest) { const user = await resolveAppUser(session.githubId, session.githubLogin); if (!user) { - return NextResponse.json( - { error: "Failed to fetch user settings" }, - { status: 500 } - ); + return NextResponse.json({ error: "Failed to fetch user settings" }, { status: 500 }); } const cacheKey = `settings:${user.id}`; - const SETTINGS_TTL = 5 * 60; // 5 minutes + const SETTINGS_TTL = 5 * 60; const cached = await cacheGet>(cacheKey, SETTINGS_TTL); if (cached) { @@ -267,13 +268,13 @@ export async function GET(req: NextRequest) { timezone: result.timezone, webhook_url: result.webhook_url ?? null, discord_muted_until: result.discord_muted_until ?? null, + seen_onboarding: (result.data as any).seen_onboarding ?? false, }; await cacheSet(cacheKey, response, SETTINGS_TTL); return NextResponse.json(response); } - export async function PATCH(req: NextRequest) { const session = await getServerSession(authOptions); @@ -282,24 +283,33 @@ export async function PATCH(req: NextRequest) { } const user = await resolveAppUser(session.githubId, session.githubLogin); - if (!user) { - return NextResponse.json( - { error: "User not found" }, - { status: 404 } - ); + return NextResponse.json({ error: "User not found" }, { status: 404 }); } - let body: { is_public?: boolean; show_weekly_goals?: boolean; leaderboard_opt_in?: boolean; weekly_digest_opt_in?: boolean; pinned_repos?: string[]; wakatime_api_key?: string; discord_webhook_url?: string | null; timezone?: string; bio?: string; webhook_url?: string | null; discord_muted_until?: string | null }; + let body: { + is_public?: boolean; + show_weekly_goals?: boolean; + leaderboard_opt_in?: boolean; + weekly_digest_opt_in?: boolean; + pinned_repos?: string[]; + wakatime_api_key?: string; + discord_webhook_url?: string | null; + timezone?: string; + bio?: string; + webhook_url?: string | null; + discord_muted_until?: string | null; + seen_onboarding?: boolean; + }; + try { body = await req.json(); } catch (e) { return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); } - const { is_public, show_weekly_goals, leaderboard_opt_in, weekly_digest_opt_in, pinned_repos, wakatime_api_key, discord_webhook_url, timezone, bio, webhook_url, discord_muted_until } = body; + const { is_public, show_weekly_goals, leaderboard_opt_in, weekly_digest_opt_in, pinned_repos, wakatime_api_key, discord_webhook_url, timezone, bio, webhook_url, discord_muted_until, seen_onboarding } = body; - // Retrieve supported columns first const settingsResult = await fetchUserSettings(user.id); if (settingsResult.error || !settingsResult.data) { console.error("Error fetching settings during PATCH:", settingsResult.error); @@ -307,7 +317,23 @@ export async function PATCH(req: NextRequest) { } const { hasLeaderboardOptIn, hasPinnedRepos, hasWakatimeKey, hasWeeklyDigestOptIn, hasDiscordSettings, hasBio, hasWebhookUrl, hasDiscordMutedUntil } = settingsResult; - const updates: { is_public?: boolean; public_since?: string | null; show_weekly_goals?: boolean; leaderboard_opt_in?: boolean; weekly_digest_opt_in?: boolean; pinned_repos?: string[]; wakatime_api_key_encrypted?: string | null; wakatime_api_key_iv?: string | null; discord_webhook_url?: string | null; timezone?: string; bio?: string; webhook_url?: string | null; discord_muted_until?: string | null } = {}; + + const updates: { + is_public?: boolean; + public_since?: string | null; + show_weekly_goals?: boolean; + leaderboard_opt_in?: boolean; + weekly_digest_opt_in?: boolean; + pinned_repos?: string[]; + wakatime_api_key_encrypted?: string | null; + wakatime_api_key_iv?: string | null; + discord_webhook_url?: string | null; + timezone?: string; + bio?: string; + webhook_url?: string | null; + discord_muted_until?: string | null; + seen_onboarding?: boolean; + } = {}; if (is_public !== undefined && is_public !== null && typeof is_public === "boolean") { updates.is_public = is_public; @@ -318,17 +344,13 @@ export async function PATCH(req: NextRequest) { } } - if ( - hasLeaderboardOptIn && - leaderboard_opt_in !== undefined && - leaderboard_opt_in !== null && - typeof leaderboard_opt_in === "boolean" - ) { + if (hasLeaderboardOptIn && leaderboard_opt_in !== undefined && leaderboard_opt_in !== null && typeof leaderboard_opt_in === "boolean") { updates.leaderboard_opt_in = leaderboard_opt_in; if (leaderboard_opt_in) { updates.is_public = true; } } + if (show_weekly_goals !== undefined && show_weekly_goals !== null && typeof show_weekly_goals === "boolean") { updates.show_weekly_goals = show_weekly_goals; } @@ -337,12 +359,7 @@ export async function PATCH(req: NextRequest) { updates.webhook_url = webhook_url; } - if ( - hasWeeklyDigestOptIn && - weekly_digest_opt_in !== undefined && - weekly_digest_opt_in !== null && - typeof weekly_digest_opt_in === "boolean" - ) { + if (hasWeeklyDigestOptIn && weekly_digest_opt_in !== undefined && weekly_digest_opt_in !== null && typeof weekly_digest_opt_in === "boolean") { updates.weekly_digest_opt_in = weekly_digest_opt_in; } @@ -353,23 +370,19 @@ export async function PATCH(req: NextRequest) { updates.pinned_repos = pinned_repos; } + if (typeof seen_onboarding === "boolean") { + updates.seen_onboarding = seen_onboarding; + } + if (!hasBio && bio !== undefined) { - return NextResponse.json( - { error: "Bio settings are not available until the latest database migration is applied" }, - { status: 400 } - ); + return NextResponse.json({ error: "Bio settings are not available until the latest database migration is applied" }, { status: 400 }); } if (hasBio && bio !== undefined) { const result = validateTextInput(bio, "Bio", 500); - if (!result.ok) { - return NextResponse.json( - { error: result.error }, - { status: 400 } - ); + return NextResponse.json({ error: result.error }, { status: 400 }); } - updates.bio = result.value; } @@ -394,7 +407,6 @@ export async function PATCH(req: NextRequest) { } } - // Handle Discord settings (only if the discord columns exist in the schema) if (hasDiscordSettings && discord_webhook_url !== undefined) { if (discord_webhook_url === "") { updates.discord_webhook_url = null; @@ -422,7 +434,6 @@ export async function PATCH(req: NextRequest) { } } - // If there are no updates (or none that are supported by the schema) if (Object.keys(updates).length === 0) { return NextResponse.json({ id: (settingsResult.data as any).id, @@ -442,7 +453,6 @@ export async function PATCH(req: NextRequest) { }); } - // Query only supported columns in the returning select statement const selectCols = ["id", "github_login", "is_public", "public_since", "show_weekly_goals"]; if (hasBio) selectCols.push("bio"); if (hasLeaderboardOptIn) selectCols.push("leaderboard_opt_in"); @@ -468,21 +478,13 @@ export async function PATCH(req: NextRequest) { return NextResponse.json({ error: "Failed to update settings" }, { status: 500 }); } - // Bust settings cache so next GET returns fresh data. await cacheDelete(`settings:${user.id}`); - // If is_public or leaderboard_opt_in changed, the cached leaderboard would - // show stale eligibility until it expires (up to 1 hour). Bust the cache - // immediately so the next request reflects the updated preference. - const leaderboardEligibilityChanged = - "is_public" in updates || "leaderboard_opt_in" in updates; - + const leaderboardEligibilityChanged = "is_public" in updates || "leaderboard_opt_in" in updates; if (leaderboardEligibilityChanged) { try { await clearLeaderboardCache(); } catch { - // Cache invalidation is best-effort — a failure must not prevent the - // settings response from reaching the client. console.error("[settings] Failed to invalidate leaderboard cache after visibility change"); } } @@ -503,4 +505,4 @@ export async function PATCH(req: NextRequest) { webhook_url: (updated as any).webhook_url ?? null, discord_muted_until: (updated as any).discord_muted_until ?? null, }); -} +} \ No newline at end of file diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 3c7a6a7d5..f2cd54f7e 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -10,6 +10,7 @@ import DashboardSSEProvider from "@/components/DashboardSSEProvider"; import StreakAtRiskBanner from "@/components/StreakAtRiskBanner"; import ThrottleBanner from "@/components/ThrottleBanner"; import CustomizableDashboard from "@/components/dashboard/CustomizableDashboard"; +import OnboardingTour from "@/components/OnboardingTour"; export default async function DashboardPage() { const session = await getServerSession(authOptions); @@ -19,6 +20,7 @@ export default async function DashboardPage() {
+ {/* Quick actions */}
diff --git a/src/components/DashboardHeader.tsx b/src/components/DashboardHeader.tsx index ac6ec6e5e..8cf468032 100644 --- a/src/components/DashboardHeader.tsx +++ b/src/components/DashboardHeader.tsx @@ -379,6 +379,23 @@ export default function DashboardHeader() { Share Profile )} +<<<<<<< HEAD +======= + + + + + + +>>>>>>> 410c25b (feat(onboarding): add first-time dashboard tour (#251))
)} From 9c878363910a8d2e6b216511bfb13acc252f0610 Mon Sep 17 00:00:00 2001 From: Mallya Date: Sat, 23 May 2026 01:35:34 +0530 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20remove=20onComplete=20prop=20?= =?UTF-8?q?=E2=80=94=20OnboardingTour=20handles=20completion=20internally?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/dashboard/page.tsx | 2 -- src/components/OnboardingTour.tsx | 35 +++++++++++++++++-------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index f2cd54f7e..3c7a6a7d5 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -10,7 +10,6 @@ import DashboardSSEProvider from "@/components/DashboardSSEProvider"; import StreakAtRiskBanner from "@/components/StreakAtRiskBanner"; import ThrottleBanner from "@/components/ThrottleBanner"; import CustomizableDashboard from "@/components/dashboard/CustomizableDashboard"; -import OnboardingTour from "@/components/OnboardingTour"; export default async function DashboardPage() { const session = await getServerSession(authOptions); @@ -20,7 +19,6 @@ export default async function DashboardPage() {
- {/* Quick actions */}
diff --git a/src/components/OnboardingTour.tsx b/src/components/OnboardingTour.tsx index 25e29f3d0..f4b8701b0 100644 --- a/src/components/OnboardingTour.tsx +++ b/src/components/OnboardingTour.tsx @@ -4,11 +4,6 @@ import { useEffect, useCallback } from "react"; import { driver } from "driver.js"; import "driver.js/dist/driver.css"; -interface OnboardingTourProps { - onComplete: () => void; -} - -// steps match the actual widget ids on the dashboard const TOUR_STEPS = [ { element: "#widget-contribution-graph", @@ -47,7 +42,19 @@ const TOUR_STEPS = [ }, ]; -export default function OnboardingTour({ onComplete }: OnboardingTourProps) { +async function markTourSeen() { + try { + await fetch("/api/user/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ seen_onboarding: true }), + }); + } catch { + // silent fail — not critical + } +} + +export default function OnboardingTour() { const startTour = useCallback(() => { const driverObj = driver({ showProgress: true, @@ -55,23 +62,19 @@ export default function OnboardingTour({ onComplete }: OnboardingTourProps) { allowClose: true, steps: TOUR_STEPS, onDestroyStarted: () => { - // mark tour as seen whether user finishes or skips - onComplete(); + markTourSeen(); driverObj.destroy(); }, }); driverObj.drive(); - }, [onComplete]); - + }, []); - // auto-start on mount useEffect(() => { - // skip tour in test environments to avoid blocking E2E tests - if (typeof window !== "undefined" && window.navigator.webdriver) return; - const timer = setTimeout(startTour, 800); - return () => clearTimeout(timer); -}, [startTour]); + if (typeof window !== "undefined" && window.navigator.webdriver) return; + const timer = setTimeout(startTour, 800); + return () => clearTimeout(timer); + }, [startTour]); return null; } From e6bf573c9f3fcb1340c49b7c4021757aac3a3b09 Mon Sep 17 00:00:00 2001 From: Mallya Date: Mon, 8 Jun 2026 16:25:40 +0530 Subject: [PATCH 6/7] fix: resolve conflicts, keep target validation guard (#929) --- src/components/GoalTracker.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/GoalTracker.tsx b/src/components/GoalTracker.tsx index 166266fc0..1c9860734 100644 --- a/src/components/GoalTracker.tsx +++ b/src/components/GoalTracker.tsx @@ -140,6 +140,11 @@ export function useGoalTracker() { if (e) e.preventDefault(); setCreating(true); setCreateError(null); + if (target <= 0) { + setCreateError("Target must be greater than 0."); + setCreating(false); + return; + } try { const result = await submitGoalWithRefresh({ From ca89dcac4bbf5e140a1f7f3dbf6571409965ab87 Mon Sep 17 00:00:00 2001 From: Mallya Date: Mon, 8 Jun 2026 16:47:36 +0530 Subject: [PATCH 7/7] fix(a11y): add aria-label to productivity badge, text is primary indicator (#952) --- package-lock.json | 7 ++++ package.json | 1 + src/components/CodingActivityInsightsCard.tsx | 35 +++++++++++-------- src/components/DashboardHeader.tsx | 17 --------- 4 files changed, 29 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5c52af4b5..7796c3bc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "clsx": "^2.1.1", "date-fns": "^3.6.0", "dompurify": "^3.1.6", + "driver.js": "^1.4.0", "fflate": "^0.8.3", "html-to-image": "^1.11.13", "idb-keyval": "^6.2.4", @@ -9759,6 +9760,12 @@ "node": ">=4" } }, + "node_modules/driver.js": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/driver.js/-/driver.js-1.4.0.tgz", + "integrity": "sha512-Gm64jm6PmcU+si21sQhBrTAM1JvUrR0QhNmjkprNLxohOBzul9+pNHXgQaT9lW84gwg9GMLB3NZGuGolsz5uew==", + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/package.json b/package.json index e4d89fcd6..5d544411c 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "clsx": "^2.1.1", "date-fns": "^3.6.0", "dompurify": "^3.1.6", + "driver.js": "^1.4.0", "fflate": "^0.8.3", "html-to-image": "^1.11.13", "idb-keyval": "^6.2.4", diff --git a/src/components/CodingActivityInsightsCard.tsx b/src/components/CodingActivityInsightsCard.tsx index f0d6f37ba..4887d9b3c 100644 --- a/src/components/CodingActivityInsightsCard.tsx +++ b/src/components/CodingActivityInsightsCard.tsx @@ -28,16 +28,21 @@ function formatCommitCount(count: number): string { function InsightRow({ label, value, + ariaLabel, }: { label: string; value: string; + ariaLabel?: string; }) { return (

{label}

-

+

{value}

@@ -176,7 +181,7 @@ export default function CodingActivityInsightsCard() { return []; } - const rows = [ + const rows: { label: string; value: string; ariaLabel?: string }[] = [ { label: "Most active", value: `${data.mostActiveHour.label} with ${formatCommitCount(data.mostActiveHour.count)}`, @@ -191,18 +196,19 @@ export default function CodingActivityInsightsCard() { }, { - label: "Productivity", - value: - data.productivityLevel === "Excellent" - ? "🟢 Excellent" - : data.productivityLevel === "Very Good" - ? "🔵 Very Good" - : data.productivityLevel === "Good" - ? "🟡 Good" - : data.productivityLevel === "Moderate" - ? "🟠 Moderate" - : "🔴 Low", - }, + label: "Productivity", + value: + data.productivityLevel === "Excellent" + ? "🟢 Excellent" + : data.productivityLevel === "Very Good" + ? "🔵 Very Good" + : data.productivityLevel === "Good" + ? "🟡 Good" + : data.productivityLevel === "Moderate" + ? "🟠 Moderate" + : "🔴 Low", + ariaLabel: `Productivity level: ${data.productivityLevel ?? "Low"}`, + }, { label: "Daily Average", @@ -349,6 +355,7 @@ export default function CodingActivityInsightsCard() { key={row.label} label={row.label} value={row.value} + ariaLabel={row.ariaLabel} /> ))}
diff --git a/src/components/DashboardHeader.tsx b/src/components/DashboardHeader.tsx index 8cf468032..ac6ec6e5e 100644 --- a/src/components/DashboardHeader.tsx +++ b/src/components/DashboardHeader.tsx @@ -379,23 +379,6 @@ export default function DashboardHeader() { Share Profile )} -<<<<<<< HEAD -======= - - - - - - ->>>>>>> 410c25b (feat(onboarding): add first-time dashboard tour (#251))
)}