diff --git a/package-lock.json b/package-lock.json index 5c52af4b..7796c3bc 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 e4d89fcd..5d544411 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/GoalTracker.tsx b/src/components/GoalTracker.tsx index 166266fc..1c986073 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({ diff --git a/src/components/OnboardingTour.tsx b/src/components/OnboardingTour.tsx new file mode 100644 index 00000000..f4b8701b --- /dev/null +++ b/src/components/OnboardingTour.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { useEffect, useCallback } from "react"; +import { driver } from "driver.js"; +import "driver.js/dist/driver.css"; + +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.", + }, + }, +]; + +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, + animate: true, + allowClose: true, + steps: TOUR_STEPS, + onDestroyStarted: () => { + markTourSeen(); + driverObj.destroy(); + }, + }); + + driverObj.drive(); + }, []); + + useEffect(() => { + if (typeof window !== "undefined" && window.navigator.webdriver) return; + const timer = setTimeout(startTour, 800); + return () => clearTimeout(timer); + }, [startTour]); + + return null; +} 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;