167 lines
6.2 KiB
TypeScript
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>
|
|
);
|
|
}
|