learning progression + completion tracking

This commit is contained in:
2025-09-12 14:12:38 +01:00
parent 2ff6da13b3
commit 9ac071764e
2 changed files with 91 additions and 5 deletions

View File

@@ -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<Set<string>>(new Set());
// Load completion status on mount (client-side only)
useEffect(() => {
const allQuestions = [
...progression.warmup,
...progression.core,
...progression.challenge,
];
const completed = new Set<string>();
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 (
<Card>
<CardHeader>
@@ -30,13 +58,18 @@ export function LearningProgression({ progression }: LearningProgressionProps) {
1
</span>
Warmup
{progression.warmup.length > 0 && (
<span className="ml-auto text-xs font-normal text-[var(--muted-foreground)]">
{warmupCompleted}/{progression.warmup.length}
</span>
)}
</h4>
<p className="text-sm text-[var(--muted-foreground)] mb-3">
Start here to build foundational understanding.
</p>
<div className="space-y-2">
{progression.warmup.map((q) => (
<QuestionLink key={q.id} question={q} />
<QuestionLink key={q.id} question={q} isCompleted={completedSlugs.has(q.slug)} />
))}
</div>
</div>
@@ -49,13 +82,18 @@ export function LearningProgression({ progression }: LearningProgressionProps) {
2
</span>
Core Practice
{progression.core.length > 0 && (
<span className="ml-auto text-xs font-normal text-[var(--muted-foreground)]">
{coreCompleted}/{progression.core.length}
</span>
)}
</h4>
<p className="text-sm text-[var(--muted-foreground)] mb-3">
Master the pattern with these representative problems.
</p>
<div className="space-y-2">
{progression.core.map((q) => (
<QuestionLink key={q.id} question={q} />
<QuestionLink key={q.id} question={q} isCompleted={completedSlugs.has(q.slug)} />
))}
</div>
</div>
@@ -68,13 +106,18 @@ export function LearningProgression({ progression }: LearningProgressionProps) {
3
</span>
Challenge
{progression.challenge.length > 0 && (
<span className="ml-auto text-xs font-normal text-[var(--muted-foreground)]">
{challengeCompleted}/{progression.challenge.length}
</span>
)}
</h4>
<p className="text-sm text-[var(--muted-foreground)] mb-3">
Test your mastery with complex variations.
</p>
<div className="space-y-2">
{progression.challenge.map((q) => (
<QuestionLink key={q.id} question={q} />
<QuestionLink key={q.id} question={q} isCompleted={completedSlugs.has(q.slug)} />
))}
</div>
</div>
@@ -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 (
<Link
href={`/questions/${question.slug}`}
className="flex items-center justify-between p-3 rounded-lg border border-[var(--border)] bg-[var(--card)] hover:border-[var(--primary)] transition-colors"
>
<span className="font-medium">{question.title}</span>
<div className="flex items-center gap-2">
{isCompleted && (
<CheckCircle className="h-4 w-4 text-green-500 flex-shrink-0" />
)}
<span className="font-medium">{question.title}</span>
{question.is_optimal && (
<span className="flex items-center gap-1 text-xs text-amber-500" title="This pattern is optimal for this problem">
<Star className="h-3 w-3 fill-current" />
Optimal
</span>
)}
</div>
<div className="flex items-center gap-2">
{question.leetcode_id && (
<span className="text-xs text-[var(--muted-foreground)]">

View File

@@ -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<string, QuestionProgress>;
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[];