From b6d1443519299fcac5f256bb085a582c166e1fc9 Mon Sep 17 00:00:00 2001 From: bntvllnt <32437578+bntvllnt@users.noreply.github.com> Date: Sat, 25 Apr 2026 05:19:27 +0200 Subject: [PATCH 1/2] feat(ui): add progress tracker component --- .../progress-tracker-default.png | Bin 0 -> 83227 bytes packages/ui/src/components/index.ts | 19 + .../src/components/progress-tracker/index.ts | 19 + .../progress-tracker/progress-tracker.mdx | 49 ++ .../progress-tracker.stories.tsx | 114 ++++ .../progress-tracker.test.tsx | 131 +++++ .../progress-tracker/progress-tracker.tsx | 545 ++++++++++++++++++ .../progress-tracker.visual.tsx | 79 +++ 8 files changed, 956 insertions(+) create mode 100644 packages/ui/.snapshots/progress-tracker/progress-tracker.visual.tsx-chromium/progress-tracker-default.png create mode 100644 packages/ui/src/components/progress-tracker/index.ts create mode 100644 packages/ui/src/components/progress-tracker/progress-tracker.mdx create mode 100644 packages/ui/src/components/progress-tracker/progress-tracker.stories.tsx create mode 100644 packages/ui/src/components/progress-tracker/progress-tracker.test.tsx create mode 100644 packages/ui/src/components/progress-tracker/progress-tracker.tsx create mode 100644 packages/ui/src/components/progress-tracker/progress-tracker.visual.tsx diff --git a/packages/ui/.snapshots/progress-tracker/progress-tracker.visual.tsx-chromium/progress-tracker-default.png b/packages/ui/.snapshots/progress-tracker/progress-tracker.visual.tsx-chromium/progress-tracker-default.png new file mode 100644 index 0000000000000000000000000000000000000000..d08927bfed0840666db31f3d365760d0dc5b6d5d GIT binary patch literal 83227 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
&k10c1}Fe?>EcomgcQ0ksNo}y5P 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

brn=R|GPh3dwnDyCdl}?`1u) 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 P6k+gm^>bP0l+XkK+CAGB literal 0 HcmV?d00001 diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 8c076ec..f23cc3b 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -325,6 +325,25 @@ export { type SummaryProps, } from "./learning-objectives"; export { ProgressBar, type ProgressBarProps } from "./progress-bar"; +export { + ProgressTracker, + ProgressTrackerBadge, + type ProgressTrackerBadgeProps, + ProgressTrackerModule, + type ProgressTrackerModuleItem, + type ProgressTrackerModuleProps, + ProgressTrackerModules, + type ProgressTrackerModulesProps, + type ProgressTrackerModuleStatus, + ProgressTrackerOverview, + type ProgressTrackerOverviewProps, + type ProgressTrackerProps, + ProgressTrackerStat, + type ProgressTrackerStatProps, + ProgressTrackerStats, + type ProgressTrackerStatsProps, + useProgressTrackerContext, +} from "./progress-tracker"; export { CommonMistake, type CommonMistakeProps, diff --git a/packages/ui/src/components/progress-tracker/index.ts b/packages/ui/src/components/progress-tracker/index.ts new file mode 100644 index 0000000..2f9674f --- /dev/null +++ b/packages/ui/src/components/progress-tracker/index.ts @@ -0,0 +1,19 @@ +export { + ProgressTracker, + ProgressTrackerBadge, + type ProgressTrackerBadgeProps, + ProgressTrackerModule, + type ProgressTrackerModuleItem, + type ProgressTrackerModuleProps, + ProgressTrackerModules, + type ProgressTrackerModulesProps, + type ProgressTrackerModuleStatus, + ProgressTrackerOverview, + type ProgressTrackerOverviewProps, + type ProgressTrackerProps, + ProgressTrackerStat, + type ProgressTrackerStatProps, + ProgressTrackerStats, + type ProgressTrackerStatsProps, + useProgressTrackerContext, +} from "./progress-tracker"; diff --git a/packages/ui/src/components/progress-tracker/progress-tracker.mdx b/packages/ui/src/components/progress-tracker/progress-tracker.mdx new file mode 100644 index 0000000..779ea89 --- /dev/null +++ b/packages/ui/src/components/progress-tracker/progress-tracker.mdx @@ -0,0 +1,49 @@ +import { Canvas, Controls, Meta, Primary } from '@storybook/addon-docs/blocks' +import * as Stories from './progress-tracker.stories' + + + +# Progress Tracker + +Curriculum-level learning dashboard for modules, lessons, exercises, streaks, and earned skills. + + + +## Installation + +```bash +pnpm dlx shadcn@latest add https://ui.vllnt.com/r/progress-tracker.json +``` + +## Import + +```tsx +import { ProgressTracker } from '@vllnt/ui' +``` + +## Usage + +```tsx + + + + + + + + + + +``` + + + +## API Reference + + diff --git a/packages/ui/src/components/progress-tracker/progress-tracker.stories.tsx b/packages/ui/src/components/progress-tracker/progress-tracker.stories.tsx new file mode 100644 index 0000000..5539ae5 --- /dev/null +++ b/packages/ui/src/components/progress-tracker/progress-tracker.stories.tsx @@ -0,0 +1,114 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; + +import { + ProgressTracker, + ProgressTrackerModule, + ProgressTrackerModules, + ProgressTrackerOverview, + ProgressTrackerStat, + ProgressTrackerStats, + type ProgressTrackerProps, +} from "./progress-tracker"; + +const meta = { + args: { + modules: [ + { + badge: "Foundation", + completedExercises: 18, + completedLessons: 12, + currentLesson: "Final assessment", + description: "Build confidence with variables, functions, and arrays.", + exercises: 18, + lessons: 12, + progress: 1, + skills: ["Syntax", "Functions", "Arrays"], + status: "completed", + timeSpent: "8h 10m", + title: "JavaScript Basics", + }, + { + badge: "Now learning", + completedExercises: 5, + completedLessons: 3, + currentLesson: "Hooks in action", + description: "Move from JSX to reusable interactive interfaces.", + exercises: 12, + lessons: 8, + progress: 0.4, + skills: ["Components", "Hooks", "State"], + status: "in-progress", + timeSpent: "6h 20m", + title: "React Fundamentals", + }, + { + badge: "Next up", + completedExercises: 0, + completedLessons: 0, + currentLesson: "Unlock after React Fundamentals", + description: "Design resilient APIs and asynchronous application flows.", + exercises: 10, + lessons: 6, + progress: 0, + skills: ["REST", "Caching", "Error handling"], + status: "available", + timeSpent: "0h", + title: "API Design", + }, + { + badge: "Locked", + completedExercises: 0, + completedLessons: 0, + currentLesson: "Requires API Design", + description: "Bring everything together with deployment and observability.", + exercises: 14, + lessons: 9, + progress: 0, + skills: ["CI", "Monitoring", "Release"], + status: "locked", + timeSpent: "0h", + title: "Ship to production", + }, + ], + overallProgress: 0.65, + streak: 7, + title: "Frontend mastery path", + }, + component: ProgressTracker, + title: "Learning/ProgressTracker", +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +function ProgressTrackerStory(args: ProgressTrackerProps): React.ReactNode { + return ( + + + + + + + + + + {args.modules?.map((module) => ( + + ))} + + + ); +} + +export const Default: Story = { + render: (args) => , +}; + +export const CompactDashboard: Story = { + args: { + overallProgress: 0.82, + streak: 12, + title: "Bootcamp progress", + }, + render: (args) => , +}; diff --git a/packages/ui/src/components/progress-tracker/progress-tracker.test.tsx b/packages/ui/src/components/progress-tracker/progress-tracker.test.tsx new file mode 100644 index 0000000..7a840ee --- /dev/null +++ b/packages/ui/src/components/progress-tracker/progress-tracker.test.tsx @@ -0,0 +1,131 @@ +import { render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; + +import { + ProgressTracker, + ProgressTrackerBadge, + ProgressTrackerModule, + ProgressTrackerModules, + ProgressTrackerOverview, + ProgressTrackerStat, + ProgressTrackerStats, +} from "./progress-tracker"; + +const modules = [ + { + badge: "Core", + completedExercises: 18, + completedLessons: 12, + currentLesson: "Review quiz", + exercises: 18, + lessons: 12, + progress: 1, + skills: ["Syntax", "Variables"], + status: "completed" as const, + title: "JavaScript Basics", + }, + { + badge: "Active", + completedExercises: 4, + completedLessons: 3, + currentLesson: "Hooks in action", + exercises: 10, + lessons: 8, + progress: 0.4, + skills: ["Components", "Hooks"], + status: "in-progress" as const, + timeSpent: "6h 20m", + title: "React Fundamentals", + }, +]; + +afterEach(() => { + localStorage.clear(); +}); + +describe("ProgressTracker", () => { + it("renders overview metrics and top-level title", () => { + render( + + + , + ); + + expect(screen.getByText("Learning progress")).toBeVisible(); + expect( + screen.getByRole("progressbar", { name: /overall progress/i }), + ).toHaveAttribute("aria-valuenow", "65"); + expect(screen.getByText("15/20")).toBeVisible(); + expect(screen.getByText("22/28")).toBeVisible(); + }); + + it("renders modules with progress semantics and status labels", () => { + render( + + + + + + , + ); + + expect(screen.getByText("Completed")).toBeVisible(); + expect(screen.getByText("In progress")).toBeVisible(); + expect( + screen.getByRole("progressbar", { name: /react fundamentals progress/i }), + ).toHaveAttribute("aria-valuenow", "40"); + expect(screen.getByText("Current: Hooks in action")).toBeVisible(); + expect(screen.getByText("Hooks")).toBeVisible(); + }); + + it("uses Checklist persistence when checklist items and a persist key are provided", () => { + localStorage.setItem( + "checklist:react-fundamentals", + JSON.stringify(["intro", "components", "hooks"]), + ); + + render( + + + + + , + ); + + expect( + screen.getByRole("progressbar", { + name: /checklist-backed module progress/i, + }), + ).toHaveAttribute("aria-valuenow", "75"); + expect(screen.getByText("3/4")).toBeVisible(); + }); + + it("renders stat cards and custom badges", () => { + render( + + + + + + Consistency + , + ); + + expect(screen.getByText("Time Spent")).toBeVisible(); + expect(screen.getByText("24h")).toBeVisible(); + expect(screen.getByText("Consistency")).toBeVisible(); + }); +}); diff --git a/packages/ui/src/components/progress-tracker/progress-tracker.tsx b/packages/ui/src/components/progress-tracker/progress-tracker.tsx new file mode 100644 index 0000000..2ab8ccf --- /dev/null +++ b/packages/ui/src/components/progress-tracker/progress-tracker.tsx @@ -0,0 +1,545 @@ +"use client"; + +import * as React from "react"; + +import type { ReactNode } from "react"; + +import { cn } from "../../lib/utils"; +import { Badge } from "../badge"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "../card"; +import type { ChecklistItem } from "../checklist"; +import { ProgressBar } from "../progress-bar"; + +export type ProgressTrackerModuleStatus = + | "available" + | "completed" + | "in-progress" + | "locked"; + +export type ProgressTrackerModuleItem = { + badge?: string; + checklistItems?: ChecklistItem[]; + completedExercises?: number; + completedLessons?: number; + currentLesson?: string; + description?: string; + exercises?: number; + href?: string; + id?: string; + lessons: number; + persistKey?: string; + progress: number; + skills?: string[]; + status: ProgressTrackerModuleStatus; + timeSpent?: string; + title: string; +}; + +export type ProgressTrackerProps = React.HTMLAttributes & { + children?: ReactNode; + modules?: ProgressTrackerModuleItem[]; + overallProgress: number; + streak?: number; + title?: string; +}; + +type ProgressTrackerContextValue = { + modules: ProgressTrackerModuleItem[]; + overallProgress: number; + streak: number; + title?: string; +}; + +const ProgressTrackerContext = + React.createContext(null); + +function clampPercentage(value: number): number { + if (Number.isNaN(value)) return 0; + if (value <= 1) return Math.round(Math.max(0, value) * 100); + return Math.round(Math.min(Math.max(0, value), 100)); +} + +const STATUS_LABELS: Record = { + available: "Available", + completed: "Completed", + "in-progress": "In progress", + locked: "Locked", +}; + +const STATUS_CLASSES: Record = { + available: "border-secondary bg-secondary text-secondary-foreground", + completed: "border-primary/20 bg-primary text-primary-foreground", + "in-progress": "border-primary/20 bg-primary/10 text-primary", + locked: "border-border bg-muted text-muted-foreground", +}; + +function getStatusLabel(status: ProgressTrackerModuleStatus): string { + return STATUS_LABELS[status]; +} + +function getStatusClasses(status: ProgressTrackerModuleStatus): string { + return STATUS_CLASSES[status]; +} + +function readPersistedChecklistItems(persistKey?: string): string[] { + if (!persistKey || typeof window === "undefined") return []; + + try { + const saved = localStorage.getItem(`checklist:${persistKey}`); + if (!saved) return []; + const parsed = JSON.parse(saved) as unknown; + return Array.isArray(parsed) + ? parsed.filter((item): item is string => typeof item === "string") + : []; + } catch { + return []; + } +} + +function useChecklistProgress( + checklistItems: ChecklistItem[] = [], + persistKey?: string, +): null | { completedCount: number; progress: number; total: number } { + const total = checklistItems.length; + const [persistedIds, setPersistedIds] = React.useState(() => + readPersistedChecklistItems(persistKey), + ); + + React.useEffect(() => { + setPersistedIds(readPersistedChecklistItems(persistKey)); + }, [persistKey]); + + React.useEffect(() => { + if (!persistKey || typeof window === "undefined") return; + + const sync = (): void => { + setPersistedIds(readPersistedChecklistItems(persistKey)); + }; + + const intervalId = window.setInterval(sync, 1000); + window.addEventListener("storage", sync); + window.addEventListener("focus", sync); + + return () => { + window.clearInterval(intervalId); + window.removeEventListener("storage", sync); + window.removeEventListener("focus", sync); + }; + }, [persistKey]); + + if (!persistKey || total === 0) return null; + + const validIds = new Set(checklistItems.map((item) => item.id)); + const completedCount = persistedIds.filter((id) => validIds.has(id)).length; + + return { + completedCount, + progress: total > 0 ? Math.round((completedCount / total) * 100) : 0, + total, + }; +} + +function useProgressTrackerContext(): ProgressTrackerContextValue { + const context = React.useContext(ProgressTrackerContext); + + if (!context) { + throw new Error( + "ProgressTracker compound components must be used within .", + ); + } + + return context; +} + +function ProgressTrackerRoot({ + children, + className, + modules = [], + overallProgress, + streak = 0, + title = "Learning progress", + ...props +}: ProgressTrackerProps): React.ReactNode { + const contextValue = React.useMemo( + () => ({ + modules, + overallProgress: clampPercentage(overallProgress), + streak, + title, + }), + [modules, overallProgress, streak, title], + ); + + return ( + +

+ {children} +
+ + ); +} + +export type ProgressTrackerOverviewProps = + React.HTMLAttributes & { + description?: string; + label?: string; + }; + +// eslint-disable-next-line max-lines-per-function +function ProgressTrackerOverview({ + className, + description = "Track completion across modules, lessons, and exercises.", + label = "Overall progress", + ...props +}: ProgressTrackerOverviewProps): React.ReactNode { + const { modules, overallProgress, streak, title } = + useProgressTrackerContext(); + const radius = 54; + const circumference = 2 * Math.PI * radius; + const offset = circumference - (overallProgress / 100) * circumference; + const completedModules = modules.filter( + (module) => module.status === "completed", + ).length; + const totalLessons = modules.reduce((sum, module) => sum + module.lessons, 0); + const completedLessons = modules.reduce( + (sum, module) => sum + (module.completedLessons ?? 0), + 0, + ); + const totalExercises = modules.reduce( + (sum, module) => sum + (module.exercises ?? 0), + 0, + ); + const completedExercises = modules.reduce( + (sum, module) => sum + (module.completedExercises ?? 0), + 0, + ); + + return ( + + +
+ + {label} + +
+ {title} + {description} +
+
+
+
+
+ +
+
+ + + + +
+ + {overallProgress}% + + + Complete + +
+
+

+ {completedModules} of {modules.length} modules completed. +

+
+ +
+
+
Modules
+
+ {completedModules}/{modules.length} +
+
+
+
Lessons
+
+ {completedLessons}/{totalLessons} +
+
+
+
Exercises
+
+ {completedExercises}/{totalExercises} +
+
+
+
Momentum
+
+ {streak} day{streak === 1 ? "" : "s"} +
+
+
+
+
+ ); +} + +export type ProgressTrackerModulesProps = React.HTMLAttributes; + +function ProgressTrackerModules({ + children, + className, + ...props +}: ProgressTrackerModulesProps): React.ReactNode { + return ( +
+ {children} +
+ ); +} + +export type ProgressTrackerModuleProps = React.HTMLAttributes & + ProgressTrackerModuleItem; + +// eslint-disable-next-line max-lines-per-function +function ProgressTrackerModule({ + badge, + checklistItems, + className, + completedExercises, + completedLessons = 0, + currentLesson, + description, + exercises, + href, + id, + lessons, + persistKey, + progress, + skills = [], + status, + timeSpent, + title, + ...props +}: ProgressTrackerModuleProps): React.ReactNode { + const checklistProgress = useChecklistProgress(checklistItems, persistKey); + const resolvedLessons = checklistProgress?.completedCount ?? completedLessons; + const resolvedLessonTotal = checklistProgress?.total || lessons; + const progressPercent = + checklistProgress?.progress ?? clampPercentage(progress); + const progressValue = Math.min(resolvedLessons, resolvedLessonTotal); + const safeExerciseTotal = exercises ?? 0; + const safeExerciseComplete = Math.min( + completedExercises ?? 0, + safeExerciseTotal, + ); + const card = ( + + +
+
+ {title} + {description ? ( + {description} + ) : null} +
+ + {getStatusLabel(status)} + +
+
+ {badge ? {badge} : null} + {currentLesson ? ( + Current: {currentLesson} + ) : null} + {timeSpent ? {timeSpent} : null} +
+
+ +
+
+ +
+
+ + Lessons:{" "} + + {resolvedLessons}/{resolvedLessonTotal} + + + + Exercises:{" "} + + {safeExerciseComplete}/{safeExerciseTotal} + + +
+
+ {skills.length > 0 ? ( +
+ {skills.map((skill) => ( + {skill} + ))} +
+ ) : null} +
+
+ ); + + if (!href) return card; + + return ( + + {card} + + ); +} + +export type ProgressTrackerStatsProps = React.HTMLAttributes; + +function ProgressTrackerStats({ + children, + className, + ...props +}: ProgressTrackerStatsProps): React.ReactNode { + return ( +
+ {children} +
+ ); +} + +export type ProgressTrackerStatProps = React.HTMLAttributes & { + label: string; + value: ReactNode; +}; + +function ProgressTrackerStat({ + className, + label, + value, + ...props +}: ProgressTrackerStatProps): React.ReactNode { + return ( + + + {label} + {value} + + + ); +} + +export type ProgressTrackerBadgeProps = React.HTMLAttributes; + +function ProgressTrackerBadge({ + children, + className, + ...props +}: ProgressTrackerBadgeProps): React.ReactNode { + return ( + + {children} + + ); +} + +const ProgressTracker = Object.assign(ProgressTrackerRoot, { + Badge: ProgressTrackerBadge, + Module: ProgressTrackerModule, + Modules: ProgressTrackerModules, + Overview: ProgressTrackerOverview, + Stat: ProgressTrackerStat, + Stats: ProgressTrackerStats, +}); + +export { + ProgressTracker, + ProgressTrackerBadge, + ProgressTrackerModule, + ProgressTrackerModules, + ProgressTrackerOverview, + ProgressTrackerStat, + ProgressTrackerStats, + useProgressTrackerContext, +}; diff --git a/packages/ui/src/components/progress-tracker/progress-tracker.visual.tsx b/packages/ui/src/components/progress-tracker/progress-tracker.visual.tsx new file mode 100644 index 0000000..7b99d0b --- /dev/null +++ b/packages/ui/src/components/progress-tracker/progress-tracker.visual.tsx @@ -0,0 +1,79 @@ +import { expect, test } from "@playwright/experimental-ct-react"; + +import { + ProgressTracker, + ProgressTrackerModule, + ProgressTrackerModules, + ProgressTrackerOverview, + ProgressTrackerStat, + ProgressTrackerStats, +} from "./progress-tracker"; + +const modules = [ + { + badge: "Foundation", + completedExercises: 18, + completedLessons: 12, + currentLesson: "Final assessment", + description: "Build confidence with variables, functions, and arrays.", + exercises: 18, + lessons: 12, + progress: 1, + skills: ["Syntax", "Functions", "Arrays"], + status: "completed" as const, + timeSpent: "8h 10m", + title: "JavaScript Basics", + }, + { + badge: "Now learning", + completedExercises: 5, + completedLessons: 3, + currentLesson: "Hooks in action", + description: "Move from JSX to reusable interactive interfaces.", + exercises: 12, + lessons: 8, + progress: 0.4, + skills: ["Components", "Hooks", "State"], + status: "in-progress" as const, + timeSpent: "6h 20m", + title: "React Fundamentals", + }, + { + badge: "Next up", + completedExercises: 0, + completedLessons: 0, + currentLesson: "Unlock after React Fundamentals", + description: "Design resilient APIs and asynchronous application flows.", + exercises: 10, + lessons: 6, + progress: 0, + skills: ["REST", "Caching", "Error handling"], + status: "available" as const, + title: "API Design", + }, +]; + +test.describe("ProgressTracker Visual", () => { + test("default", async ({ mount, page }) => { + await mount( +
+ + + + + + + + + + {modules.map((module) => ( + + ))} + + +
, + ); + + await expect(page).toHaveScreenshot("progress-tracker-default.png"); + }); +}); From 41318dd6473fa34d9ba39851f2cbb8c04ca354bb Mon Sep 17 00:00:00 2001 From: bntvllnt <32437578+bntvllnt@users.noreply.github.com> Date: Sat, 25 Apr 2026 07:33:21 +0200 Subject: [PATCH 2/2] fix(ui): sync progress tracker checklist state --- .../ui/src/components/checklist/checklist.tsx | 7 ++ packages/ui/src/components/checklist/index.ts | 1 + .../progress-tracker.test.tsx | 103 ++++++++++++++- .../progress-tracker/progress-tracker.tsx | 118 +++++++++++++++--- 4 files changed, 212 insertions(+), 17 deletions(-) diff --git a/packages/ui/src/components/checklist/checklist.tsx b/packages/ui/src/components/checklist/checklist.tsx index f7b06a3..fe5a94d 100644 --- a/packages/ui/src/components/checklist/checklist.tsx +++ b/packages/ui/src/components/checklist/checklist.tsx @@ -4,6 +4,8 @@ import { useState } from "react"; import { cn } from "../../lib/utils"; +export const CHECKLIST_PROGRESS_EVENT = "vllnt:checklist-progress-change"; + export type ChecklistItem = { description?: string; id: string; @@ -159,6 +161,11 @@ export function Checklist({ `checklist:${persistKey}`, JSON.stringify([...newChecked]), ); + window.dispatchEvent( + new CustomEvent(CHECKLIST_PROGRESS_EVENT, { + detail: { persistKey }, + }), + ); } catch { /* skip */ } diff --git a/packages/ui/src/components/checklist/index.ts b/packages/ui/src/components/checklist/index.ts index 607adc7..05e6bd1 100644 --- a/packages/ui/src/components/checklist/index.ts +++ b/packages/ui/src/components/checklist/index.ts @@ -1,5 +1,6 @@ export { Checklist, + CHECKLIST_PROGRESS_EVENT, type ChecklistItem, type ChecklistProps, } from "./checklist"; diff --git a/packages/ui/src/components/progress-tracker/progress-tracker.test.tsx b/packages/ui/src/components/progress-tracker/progress-tracker.test.tsx index 7a840ee..ca76a2e 100644 --- a/packages/ui/src/components/progress-tracker/progress-tracker.test.tsx +++ b/packages/ui/src/components/progress-tracker/progress-tracker.test.tsx @@ -1,6 +1,8 @@ -import { render, screen } from "@testing-library/react"; +import { act, render, screen } from "@testing-library/react"; import { afterEach, describe, expect, it } from "vitest"; +import { CHECKLIST_PROGRESS_EVENT } from "../checklist"; + import { ProgressTracker, ProgressTrackerBadge, @@ -60,11 +62,17 @@ describe("ProgressTracker", () => { }); it("renders modules with progress semantics and status labels", () => { + const [firstModule, secondModule] = modules; + + if (!firstModule || !secondModule) { + throw new Error("Expected seeded modules for this test"); + } + render( - - + + , ); @@ -128,4 +136,93 @@ describe("ProgressTracker", () => { expect(screen.getByText("24h")).toBeVisible(); expect(screen.getByText("Consistency")).toBeVisible(); }); + + it("keeps locked modules non-interactive even when href is provided", () => { + render( + + + + + , + ); + + expect( + screen.queryByRole("link", { name: /locked module/i }), + ).not.toBeInTheDocument(); + expect(screen.getByText("Locked module")).toBeVisible(); + }); + + it("updates overview totals and module progress from same-tab checklist persistence events", () => { + render( + + + + + + , + ); + + expect(screen.getAllByText("0/4")).toHaveLength(2); + + act(() => { + localStorage.setItem( + "checklist:react-fundamentals", + JSON.stringify(["intro", "components", "hooks"]), + ); + window.dispatchEvent( + new CustomEvent(CHECKLIST_PROGRESS_EVENT, { + detail: { persistKey: "react-fundamentals" }, + }), + ); + }); + + expect(screen.getAllByText("3/4")).toHaveLength(2); + expect( + screen.getByRole("progressbar", { + name: /checklist-backed module progress/i, + }), + ).toHaveAttribute("aria-valuenow", "75"); + }); }); diff --git a/packages/ui/src/components/progress-tracker/progress-tracker.tsx b/packages/ui/src/components/progress-tracker/progress-tracker.tsx index 2ab8ccf..37c3bcb 100644 --- a/packages/ui/src/components/progress-tracker/progress-tracker.tsx +++ b/packages/ui/src/components/progress-tracker/progress-tracker.tsx @@ -13,7 +13,7 @@ import { CardHeader, CardTitle, } from "../card"; -import type { ChecklistItem } from "../checklist"; +import { CHECKLIST_PROGRESS_EVENT, type ChecklistItem } from "../checklist"; import { ProgressBar } from "../progress-bar"; export type ProgressTrackerModuleStatus = @@ -102,6 +102,44 @@ function readPersistedChecklistItems(persistKey?: string): string[] { } } +function areStringArraysEqual(left: string[], right: string[]): boolean { + if (left.length !== right.length) return false; + + return left.every((value, index) => value === right[index]); +} + +function getChecklistPersistKey(event?: Event): null | string { + if (!(event instanceof CustomEvent)) return null; + + const detail: unknown = event.detail; + if (typeof detail !== "object" || detail === null) return null; + if (!("persistKey" in detail)) return null; + + const { persistKey } = detail; + return typeof persistKey === "string" ? persistKey : null; +} + +function getResolvedLessonProgress(module: ProgressTrackerModuleItem): { + completedLessons: number; + totalLessons: number; +} { + if (!module.persistKey || !module.checklistItems?.length) { + return { + completedLessons: module.completedLessons ?? 0, + totalLessons: module.lessons, + }; + } + + const validIds = new Set(module.checklistItems.map((item) => item.id)); + const persistedIds = readPersistedChecklistItems(module.persistKey); + const completedLessons = persistedIds.filter((id) => validIds.has(id)).length; + + return { + completedLessons, + totalLessons: module.checklistItems.length, + }; +} + function useChecklistProgress( checklistItems: ChecklistItem[] = [], persistKey?: string, @@ -110,28 +148,39 @@ function useChecklistProgress( const [persistedIds, setPersistedIds] = React.useState(() => readPersistedChecklistItems(persistKey), ); + const setPersistedIdsIfChanged = React.useCallback((nextIds: string[]) => { + setPersistedIds((currentIds) => + areStringArraysEqual(currentIds, nextIds) ? currentIds : nextIds, + ); + }, []); React.useEffect(() => { - setPersistedIds(readPersistedChecklistItems(persistKey)); - }, [persistKey]); + setPersistedIdsIfChanged(readPersistedChecklistItems(persistKey)); + }, [persistKey, setPersistedIdsIfChanged]); React.useEffect(() => { if (!persistKey || typeof window === "undefined") return; - const sync = (): void => { - setPersistedIds(readPersistedChecklistItems(persistKey)); + const sync = (event?: Event): void => { + const eventPersistKey = getChecklistPersistKey(event); + if (eventPersistKey && eventPersistKey !== persistKey) return; + + setPersistedIdsIfChanged(readPersistedChecklistItems(persistKey)); + }; + const syncEventListener: EventListener = (event) => { + sync(event); }; - const intervalId = window.setInterval(sync, 1000); window.addEventListener("storage", sync); window.addEventListener("focus", sync); + window.addEventListener(CHECKLIST_PROGRESS_EVENT, syncEventListener); return () => { - window.clearInterval(intervalId); window.removeEventListener("storage", sync); window.removeEventListener("focus", sync); + window.removeEventListener(CHECKLIST_PROGRESS_EVENT, syncEventListener); }; - }, [persistKey]); + }, [persistKey, setPersistedIdsIfChanged]); if (!persistKey || total === 0) return null; @@ -204,17 +253,59 @@ function ProgressTrackerOverview({ }: ProgressTrackerOverviewProps): React.ReactNode { const { modules, overallProgress, streak, title } = useProgressTrackerContext(); + const trackedPersistKeys = React.useMemo( + () => modules.map((module) => module.persistKey).filter(Boolean), + [modules], + ); + const [, forceChecklistRefresh] = React.useState(0); + + React.useEffect(() => { + if (trackedPersistKeys.length === 0 || typeof window === "undefined") { + return; + } + + const trackedKeys = new Set(trackedPersistKeys); + const sync = (event?: Event): void => { + const eventPersistKey = getChecklistPersistKey(event); + if (eventPersistKey && !trackedKeys.has(eventPersistKey)) return; + + forceChecklistRefresh((version) => version + 1); + }; + const syncEventListener: EventListener = (event) => { + sync(event); + }; + + window.addEventListener("storage", sync); + window.addEventListener("focus", sync); + window.addEventListener(CHECKLIST_PROGRESS_EVENT, syncEventListener); + + return () => { + window.removeEventListener("storage", sync); + window.removeEventListener("focus", sync); + window.removeEventListener(CHECKLIST_PROGRESS_EVENT, syncEventListener); + }; + }, [trackedPersistKeys]); + const radius = 54; const circumference = 2 * Math.PI * radius; const offset = circumference - (overallProgress / 100) * circumference; const completedModules = modules.filter( (module) => module.status === "completed", ).length; - const totalLessons = modules.reduce((sum, module) => sum + module.lessons, 0); - const completedLessons = modules.reduce( - (sum, module) => sum + (module.completedLessons ?? 0), - 0, + const lessonTotals = modules.reduce( + (totals, module) => { + const resolvedProgress = getResolvedLessonProgress(module); + + return { + completedLessons: + totals.completedLessons + resolvedProgress.completedLessons, + totalLessons: totals.totalLessons + resolvedProgress.totalLessons, + }; + }, + { completedLessons: 0, totalLessons: 0 }, ); + const totalLessons = lessonTotals.totalLessons; + const completedLessons = lessonTotals.completedLessons; const totalExercises = modules.reduce( (sum, module) => sum + (module.exercises ?? 0), 0, @@ -452,11 +543,10 @@ function ProgressTrackerModule({ ); - if (!href) return card; + if (!href || status === "locked") return card; return (