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 Link from "next/link";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
import { getDifficultyVariant, capitalize } from "@/lib/utils";
|
import { getDifficultyVariant, capitalize } from "@/lib/utils";
|
||||||
|
import { isQuestionCompleted } from "@/lib/progress";
|
||||||
|
import { CheckCircle, Star } from "lucide-react";
|
||||||
import type { LearningProgression as LearningProgressionType } from "@/types";
|
import type { LearningProgression as LearningProgressionType } from "@/types";
|
||||||
|
|
||||||
interface LearningProgressionProps {
|
interface LearningProgressionProps {
|
||||||
@@ -9,6 +14,24 @@ interface LearningProgressionProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LearningProgression({ progression }: 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 =
|
const hasQuestions =
|
||||||
progression.warmup.length > 0 ||
|
progression.warmup.length > 0 ||
|
||||||
progression.core.length > 0 ||
|
progression.core.length > 0 ||
|
||||||
@@ -16,6 +39,11 @@ export function LearningProgression({ progression }: LearningProgressionProps) {
|
|||||||
|
|
||||||
if (!hasQuestions) return null;
|
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 (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -30,13 +58,18 @@ export function LearningProgression({ progression }: LearningProgressionProps) {
|
|||||||
1
|
1
|
||||||
</span>
|
</span>
|
||||||
Warmup
|
Warmup
|
||||||
|
{progression.warmup.length > 0 && (
|
||||||
|
<span className="ml-auto text-xs font-normal text-[var(--muted-foreground)]">
|
||||||
|
{warmupCompleted}/{progression.warmup.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-[var(--muted-foreground)] mb-3">
|
<p className="text-sm text-[var(--muted-foreground)] mb-3">
|
||||||
Start here to build foundational understanding.
|
Start here to build foundational understanding.
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{progression.warmup.map((q) => (
|
{progression.warmup.map((q) => (
|
||||||
<QuestionLink key={q.id} question={q} />
|
<QuestionLink key={q.id} question={q} isCompleted={completedSlugs.has(q.slug)} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,13 +82,18 @@ export function LearningProgression({ progression }: LearningProgressionProps) {
|
|||||||
2
|
2
|
||||||
</span>
|
</span>
|
||||||
Core Practice
|
Core Practice
|
||||||
|
{progression.core.length > 0 && (
|
||||||
|
<span className="ml-auto text-xs font-normal text-[var(--muted-foreground)]">
|
||||||
|
{coreCompleted}/{progression.core.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-[var(--muted-foreground)] mb-3">
|
<p className="text-sm text-[var(--muted-foreground)] mb-3">
|
||||||
Master the pattern with these representative problems.
|
Master the pattern with these representative problems.
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{progression.core.map((q) => (
|
{progression.core.map((q) => (
|
||||||
<QuestionLink key={q.id} question={q} />
|
<QuestionLink key={q.id} question={q} isCompleted={completedSlugs.has(q.slug)} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,13 +106,18 @@ export function LearningProgression({ progression }: LearningProgressionProps) {
|
|||||||
3
|
3
|
||||||
</span>
|
</span>
|
||||||
Challenge
|
Challenge
|
||||||
|
{progression.challenge.length > 0 && (
|
||||||
|
<span className="ml-auto text-xs font-normal text-[var(--muted-foreground)]">
|
||||||
|
{challengeCompleted}/{progression.challenge.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-[var(--muted-foreground)] mb-3">
|
<p className="text-sm text-[var(--muted-foreground)] mb-3">
|
||||||
Test your mastery with complex variations.
|
Test your mastery with complex variations.
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{progression.challenge.map((q) => (
|
{progression.challenge.map((q) => (
|
||||||
<QuestionLink key={q.id} question={q} />
|
<QuestionLink key={q.id} question={q} isCompleted={completedSlugs.has(q.slug)} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,15 +130,27 @@ export function LearningProgression({ progression }: LearningProgressionProps) {
|
|||||||
|
|
||||||
interface QuestionLinkProps {
|
interface QuestionLinkProps {
|
||||||
question: LearningProgressionType["warmup"][number];
|
question: LearningProgressionType["warmup"][number];
|
||||||
|
isCompleted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function QuestionLink({ question }: QuestionLinkProps) {
|
function QuestionLink({ question, isCompleted }: QuestionLinkProps) {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={`/questions/${question.slug}`}
|
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"
|
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">
|
<div className="flex items-center gap-2">
|
||||||
{question.leetcode_id && (
|
{question.leetcode_id && (
|
||||||
<span className="text-xs text-[var(--muted-foreground)]">
|
<span className="text-xs text-[var(--muted-foreground)]">
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export interface LearningQuestion {
|
|||||||
slug: string;
|
slug: string;
|
||||||
difficulty: Difficulty;
|
difficulty: Difficulty;
|
||||||
leetcode_id: number | null;
|
leetcode_id: number | null;
|
||||||
|
is_optimal: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LearningProgression {
|
export interface LearningProgression {
|
||||||
@@ -118,6 +119,7 @@ export interface Explanation {
|
|||||||
time_complexity: string;
|
time_complexity: string;
|
||||||
space_complexity: string;
|
space_complexity: string;
|
||||||
complexity_explanation: string | null;
|
complexity_explanation: string | null;
|
||||||
|
pattern_comparison: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Solution {
|
export interface Solution {
|
||||||
@@ -202,6 +204,35 @@ export interface HiddenTestOutput {
|
|||||||
output: unknown;
|
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 {
|
export interface SubmissionRequest {
|
||||||
code: string;
|
code: string;
|
||||||
hidden_outputs: HiddenTestOutput[];
|
hidden_outputs: HiddenTestOutput[];
|
||||||
|
|||||||
Reference in New Issue
Block a user