From 4032d528af2c5a503e3d3555393cf5642b0209da Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Fri, 12 Sep 2025 14:12:38 +0100 Subject: [PATCH] learning progression + completion tracking --- .../patterns/learning-progression.tsx | 65 +++++++++++++++++-- frontend/src/types/index.ts | 31 +++++++++ 2 files changed, 91 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/patterns/learning-progression.tsx b/frontend/src/components/patterns/learning-progression.tsx index 1b79a69..92c28db 100644 --- a/frontend/src/components/patterns/learning-progression.tsx +++ b/frontend/src/components/patterns/learning-progression.tsx @@ -1,7 +1,12 @@ +"use client"; + +import { useEffect, useState } from "react"; import Link from "next/link"; import { Badge } from "@/components/ui/badge"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { getDifficultyVariant, capitalize } from "@/lib/utils"; +import { isQuestionCompleted } from "@/lib/progress"; +import { CheckCircle, Star } from "lucide-react"; import type { LearningProgression as LearningProgressionType } from "@/types"; interface LearningProgressionProps { @@ -9,6 +14,24 @@ interface LearningProgressionProps { } export function LearningProgression({ progression }: LearningProgressionProps) { + const [completedSlugs, setCompletedSlugs] = useState>(new Set()); + + // Load completion status on mount (client-side only) + useEffect(() => { + const allQuestions = [ + ...progression.warmup, + ...progression.core, + ...progression.challenge, + ]; + const completed = new Set(); + for (const q of allQuestions) { + if (isQuestionCompleted(q.slug)) { + completed.add(q.slug); + } + } + setCompletedSlugs(completed); + }, [progression]); + const hasQuestions = progression.warmup.length > 0 || progression.core.length > 0 || @@ -16,6 +39,11 @@ export function LearningProgression({ progression }: LearningProgressionProps) { if (!hasQuestions) return null; + // Calculate completion stats per tier + const warmupCompleted = progression.warmup.filter(q => completedSlugs.has(q.slug)).length; + const coreCompleted = progression.core.filter(q => completedSlugs.has(q.slug)).length; + const challengeCompleted = progression.challenge.filter(q => completedSlugs.has(q.slug)).length; + return ( @@ -30,13 +58,18 @@ export function LearningProgression({ progression }: LearningProgressionProps) { 1 Warmup + {progression.warmup.length > 0 && ( + + {warmupCompleted}/{progression.warmup.length} + + )}

Start here to build foundational understanding.

{progression.warmup.map((q) => ( - + ))}
@@ -49,13 +82,18 @@ export function LearningProgression({ progression }: LearningProgressionProps) { 2 Core Practice + {progression.core.length > 0 && ( + + {coreCompleted}/{progression.core.length} + + )}

Master the pattern with these representative problems.

{progression.core.map((q) => ( - + ))}
@@ -68,13 +106,18 @@ export function LearningProgression({ progression }: LearningProgressionProps) { 3 Challenge + {progression.challenge.length > 0 && ( + + {challengeCompleted}/{progression.challenge.length} + + )}

Test your mastery with complex variations.

{progression.challenge.map((q) => ( - + ))}
@@ -87,15 +130,27 @@ export function LearningProgression({ progression }: LearningProgressionProps) { interface QuestionLinkProps { question: LearningProgressionType["warmup"][number]; + isCompleted: boolean; } -function QuestionLink({ question }: QuestionLinkProps) { +function QuestionLink({ question, isCompleted }: QuestionLinkProps) { return ( - {question.title} +
+ {isCompleted && ( + + )} + {question.title} + {question.is_optimal && ( + + + Optimal + + )} +
{question.leetcode_id && ( diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index ab7fb2d..77a0c38 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -52,6 +52,7 @@ export interface LearningQuestion { slug: string; difficulty: Difficulty; leetcode_id: number | null; + is_optimal: boolean; } export interface LearningProgression { @@ -118,6 +119,7 @@ export interface Explanation { time_complexity: string; space_complexity: string; complexity_explanation: string | null; + pattern_comparison: string | null; } export interface Solution { @@ -202,6 +204,35 @@ export interface HiddenTestOutput { output: unknown; } +// Progress Dashboard Types + +export interface QuestionProgress { + slug: string; + completedAt: string; + attempts: number; + timeSpentMs: number; + lastAttemptAt: string; + primaryPattern: string; + difficulty: Difficulty; + code: string; +} + +export interface ProgressData { + version: 1; + questions: Record; + patternsStudied: string[]; + totalTimeMs: number; + lastActiveAt: string; +} + +export interface ReviewCandidate { + slug: string; + score: number; + daysSinceReview: number; + difficulty: Difficulty; + pattern: string; +} + export interface SubmissionRequest { code: string; hidden_outputs: HiddenTestOutput[];