learning progression + completion tracking
This commit is contained in:
@@ -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)]">
|
||||
|
||||
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user