Files
codetutor/frontend/src/components/patterns/learning-progression.tsx
T

167 lines
6.2 KiB
TypeScript

"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 {
progression: LearningProgressionType;
}
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 ||
progression.challenge.length > 0;
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>
<CardTitle>Learning Path</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
{progression.warmup.length > 0 && (
<div>
<h4 className="font-semibold text-[var(--difficulty-easy)] mb-3 flex items-center gap-2">
<span className="w-6 h-6 rounded-full bg-[var(--difficulty-easy-bg)] flex items-center justify-center text-sm">
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} isCompleted={completedSlugs.has(q.slug)} />
))}
</div>
</div>
)}
{progression.core.length > 0 && (
<div>
<h4 className="font-semibold text-[var(--difficulty-medium)] mb-3 flex items-center gap-2">
<span className="w-6 h-6 rounded-full bg-[var(--difficulty-medium-bg)] flex items-center justify-center text-sm">
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} isCompleted={completedSlugs.has(q.slug)} />
))}
</div>
</div>
)}
{progression.challenge.length > 0 && (
<div>
<h4 className="font-semibold text-[var(--difficulty-hard)] mb-3 flex items-center gap-2">
<span className="w-6 h-6 rounded-full bg-[var(--difficulty-hard-bg)] flex items-center justify-center text-sm">
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} isCompleted={completedSlugs.has(q.slug)} />
))}
</div>
</div>
)}
</div>
</CardContent>
</Card>
);
}
interface QuestionLinkProps {
question: LearningProgressionType["warmup"][number];
isCompleted: boolean;
}
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"
>
<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)]">
#{question.leetcode_id}
</span>
)}
<Badge variant={getDifficultyVariant(question.difficulty)}>
{capitalize(question.difficulty)}
</Badge>
</div>
</Link>
);
}