From 1c9fad7ce5dac22b22cd508253d5f3a0b8fc1fbd Mon Sep 17 00:00:00 2001 From: Mallya Date: Tue, 19 May 2026 22:40:08 +0530 Subject: [PATCH 1/4] fix: resolve onboarding PR conflicts and cleanup --- src/app/api/user/settings/route.ts | 77 ++++++++++------ src/app/dashboard/page.tsx | 20 +++- src/components/DashboardHeader.tsx | 141 ++++++++++++++++++++++------- src/components/OnboardingTour.tsx | 89 ++++++++++++++++++ 4 files changed, 260 insertions(+), 67 deletions(-) create mode 100644 src/components/OnboardingTour.tsx diff --git a/src/app/api/user/settings/route.ts b/src/app/api/user/settings/route.ts index 04e60932..4f6e5dfa 100644 --- a/src/app/api/user/settings/route.ts +++ b/src/app/api/user/settings/route.ts @@ -24,12 +24,14 @@ export async function GET(req: NextRequest) { // Fetch user from Supabase const { data, error } = await supabaseAdmin .from("users") - .select("id, github_login, is_public, leaderboard_opt_in") + .select( + "id, github_login, is_public, leaderboard_opt_in, seen_onboarding" + ) .eq("id", user.id) .single(); - if (error) { console.error("Error fetching user:", error); + return NextResponse.json( { error: "Failed to fetch user settings" }, { status: 500 } @@ -46,18 +48,23 @@ export async function PATCH(req: NextRequest) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - // Get user ID from Supabase - const user = await resolveAppUser(session.githubId, session.githubLogin); +const user = await resolveAppUser(session.githubId, session.githubLogin); - if (!user) { - return NextResponse.json( - { error: "User not found" }, - { status: 404 } - ); - } +if (!user) { + console.error("Error fetching user"); + + return NextResponse.json( + { error: "User not found" }, + { status: 404 } + ); +} + + let body: { + is_public?: boolean; + leaderboard_opt_in?: boolean; + seen_onboarding?: boolean; + }; - // Parse request body - let body: { is_public?: boolean; leaderboard_opt_in?: boolean }; try { body = await req.json(); } catch { @@ -67,49 +74,65 @@ export async function PATCH(req: NextRequest) { ); } - const { is_public, leaderboard_opt_in } = body; + const { + is_public, + leaderboard_opt_in, + seen_onboarding, + } = body; - if ( - typeof is_public !== "boolean" && - typeof leaderboard_opt_in !== "boolean" - ) { - return NextResponse.json( - { error: "At least one boolean setting is required" }, - { status: 400 } - ); - } + const updates: { + is_public?: boolean; + leaderboard_opt_in?: boolean; + seen_onboarding?: boolean; + } = {}; - const updates: { is_public?: boolean; leaderboard_opt_in?: boolean } = {}; if (typeof is_public === "boolean") { updates.is_public = is_public; } + if (typeof leaderboard_opt_in === "boolean") { updates.leaderboard_opt_in = leaderboard_opt_in; + + // If user opts into leaderboard, profile must be public if (leaderboard_opt_in) { updates.is_public = true; } } + if (typeof seen_onboarding === "boolean") { + updates.seen_onboarding = seen_onboarding; + } + + if (Object.keys(updates).length === 0) { + return NextResponse.json( + { error: "No valid fields to update" }, + { status: 400 } + ); + } + const { data: updated, error: updateError } = await supabaseAdmin .from("users") .update(updates) .eq("id", user.id) - .select("id, github_login, is_public, leaderboard_opt_in") + .select( + "id, github_login, is_public, leaderboard_opt_in, seen_onboarding" + ) .single(); if (updateError || !updated) { - console.error("Error updating settings:", updateError); + console.error("Failed to update settings:", updateError); + return NextResponse.json( { error: "Failed to update settings" }, { status: 500 } ); } - // Return updated user (only safe fields) return NextResponse.json({ id: updated.id, github_login: updated.github_login, is_public: updated.is_public, leaderboard_opt_in: updated.leaderboard_opt_in ?? false, + seen_onboarding: updated.seen_onboarding, }); -} +} \ No newline at end of file diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 333e6d73..a5b47aa7 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -60,21 +60,27 @@ export default async function DashboardPage() { {/* Row 1: Contribution graph + Streak + Friend Comparison */}
- +
+ +
- +
+ +
{/* Row 2: PR metrics, PR breakdown & Time Chart */}
- +
+ +
@@ -98,9 +104,13 @@ export default async function DashboardPage() { {/* Row 5: Top repos + Language breakdown + Goal tracker */}
- +
+ +
- +
+ +
); diff --git a/src/components/DashboardHeader.tsx b/src/components/DashboardHeader.tsx index effb33e5..a852f5ca 100644 --- a/src/components/DashboardHeader.tsx +++ b/src/components/DashboardHeader.tsx @@ -1,73 +1,144 @@ -"use client" +"use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } 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"; +import OnboardingTour from "@/components/OnboardingTour"; + +interface UserSettings { + is_public: boolean; + seen_onboarding: boolean; +} export default function DashboardHeader() { const { data: session } = useSession(); - const [isPublic, setIsPublic] = useState(null); + + const [settings, setSettings] = useState(null); + const [showTour, setShowTour] = useState(false); useEffect(() => { if (!session) { - setIsPublic(null); + setSettings(null); return; } async function loadSettings() { try { const res = await fetch("/api/user/settings"); + if (res.ok) { const data = await res.json(); - setIsPublic(data.is_public === true); + + setSettings(data); + + // Auto-start onboarding tour if not seen + if (!data.seen_onboarding) { + setShowTour(true); + } } else { - setIsPublic(false); + setSettings({ + is_public: false, + seen_onboarding: false, + }); } } catch (error) { console.error("Failed to load settings:", error); - setIsPublic(false); + + setSettings({ + is_public: false, + seen_onboarding: false, + }); } } loadSettings(); }, [session]); + const handleTourComplete = useCallback(async () => { + setShowTour(false); + + try { + await fetch("/api/user/settings", { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + seen_onboarding: true, + }), + }); + + setSettings((prev) => + prev + ? { + ...prev, + seen_onboarding: true, + } + : prev + ); + } catch (error) { + console.error("Failed to save onboarding state:", error); + } + }, []); + + const handleRestartTour = useCallback(() => { + setShowTour(true); + }, []); + return ( -
-
-
-

- Dashboard -

-

- Your coding activity at a glance -

-
+ <> + {showTour && ( + + )} + +
+
+
+

+ Dashboard +

-
- {isPublic === true && session?.githubLogin && ( - + Your coding activity at a glance +

+
+ +
+ + + {settings?.is_public && session?.githubLogin && ( + + Share Profile + + )} + + + + + +
-
- -
+ +
+ ); -} +} \ No newline at end of file diff --git a/src/components/OnboardingTour.tsx b/src/components/OnboardingTour.tsx new file mode 100644 index 00000000..f769b967 --- /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 b231519dc3d4a2da07aa4dac46fc1de7b02f7bb7 Mon Sep 17 00:00:00 2001 From: Mallya Date: Tue, 19 May 2026 20:38:24 +0530 Subject: [PATCH 2/4] fix: add driver.js dependency --- package-lock.json | 7 +++++++ package.json | 1 + 2 files changed, 8 insertions(+) diff --git a/package-lock.json b/package-lock.json index b9dff971..06bfd16a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@upstash/redis": "^1.38.0", "clsx": "^2.1.1", "date-fns": "^3.6.0", + "driver.js": "^1.4.0", "html-to-image": "^1.11.13", "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.7", @@ -2293,6 +2294,12 @@ "@types/trusted-types": "^2.0.7" } }, + "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 fd2359d8..0737870a 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "clsx": "^2.1.1", "date-fns": "^3.6.0", "html-to-image": "^1.11.13", + "driver.js": "^1.4.0", "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.7", "next": "^14.2.35", From 3dacde47080342bf738c4c8a16211f2a1ba5fa05 Mon Sep 17 00:00:00 2001 From: Mallya Date: Thu, 21 May 2026 01:23:13 +0530 Subject: [PATCH 3/4] fix: static CSS import, indentation fix, migration file, EOF newlines --- next.config.mjs | 3 ++- src/app/api/user/settings/route.ts | 2 +- src/app/layout.tsx | 1 + src/components/OnboardingTour.tsx | 17 ++--------------- src/types/css.d.ts | 4 ++++ .../20260520000000_add_seen_onboarding.sql | 1 + 6 files changed, 11 insertions(+), 17 deletions(-) create mode 100644 src/types/css.d.ts create mode 100644 supabase/migrations/20260520000000_add_seen_onboarding.sql diff --git a/next.config.mjs b/next.config.mjs index c9ddb84d..69ce87e2 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,5 +1,6 @@ /** @type {import("next").NextConfig} */ const nextConfig = { + transpilePackages: ["driver.js"], images: { remotePatterns: [ { @@ -10,4 +11,4 @@ const nextConfig = { }, }; -export default nextConfig; +export default nextConfig; \ No newline at end of file diff --git a/src/app/api/user/settings/route.ts b/src/app/api/user/settings/route.ts index 4f6e5dfa..5b06692d 100644 --- a/src/app/api/user/settings/route.ts +++ b/src/app/api/user/settings/route.ts @@ -135,4 +135,4 @@ if (!user) { leaderboard_opt_in: updated.leaderboard_opt_in ?? false, seen_onboarding: updated.seen_onboarding, }); -} \ No newline at end of file +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4ab1a645..bea88a19 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import Providers from "./providers"; +import "driver.js/dist/driver.css"; import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); diff --git a/src/components/OnboardingTour.tsx b/src/components/OnboardingTour.tsx index f769b967..c6563b43 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/src/types/css.d.ts b/src/types/css.d.ts new file mode 100644 index 00000000..fa9154c3 --- /dev/null +++ b/src/types/css.d.ts @@ -0,0 +1,4 @@ +declare module '*.css' { + const content: Record; + export default content; +} diff --git a/supabase/migrations/20260520000000_add_seen_onboarding.sql b/supabase/migrations/20260520000000_add_seen_onboarding.sql new file mode 100644 index 00000000..0ccb48cd --- /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 5741396142afd664432322ac4b366a1fd86edbe7 Mon Sep 17 00:00:00 2001 From: Mallya Date: Fri, 22 May 2026 00:41:02 +0530 Subject: [PATCH 4/4] 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 75d597f8..fefeb694 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; }