feat(progress): spaced repetition dashboard
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { getProgressData } from "@/lib/progress";
|
||||
import { getReviewSuggestions } from "@/lib/spaced-repetition";
|
||||
import { getQuestions, getPatterns } from "@/lib/api";
|
||||
import {
|
||||
ProgressOverview,
|
||||
PatternProgress,
|
||||
ReviewSuggestions,
|
||||
ProgressExport,
|
||||
} from "@/components/progress";
|
||||
import type {
|
||||
ProgressData,
|
||||
Pattern,
|
||||
QuestionListItem,
|
||||
ReviewCandidate,
|
||||
} from "@/types";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
export default function ProgressPage() {
|
||||
const [progressData, setProgressData] = useState<ProgressData | null>(null);
|
||||
const [questions, setQuestions] = useState<QuestionListItem[]>([]);
|
||||
const [patterns, setPatterns] = useState<Pattern[]>([]);
|
||||
const [reviewCandidates, setReviewCandidates] = useState<ReviewCandidate[]>(
|
||||
[]
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const progress = getProgressData();
|
||||
setProgressData(progress);
|
||||
|
||||
const [questionsRes, patternsRes] = await Promise.all([
|
||||
getQuestions({ limit: 1000 }),
|
||||
getPatterns(),
|
||||
]);
|
||||
|
||||
setQuestions(questionsRes.items);
|
||||
setPatterns(patternsRes.items);
|
||||
|
||||
const candidates = getReviewSuggestions(progress, questionsRes.items);
|
||||
setReviewCandidates(candidates);
|
||||
} catch (error) {
|
||||
console.error("Failed to load progress data:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
const handleImportSuccess = () => {
|
||||
loadData();
|
||||
};
|
||||
|
||||
if (isLoading || !progressData) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-[var(--primary)]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const hasProgress = Object.keys(progressData.questions).length > 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Progress Dashboard</h1>
|
||||
<p className="text-[var(--muted-foreground)] mt-2">
|
||||
Track your learning journey and review questions using spaced
|
||||
repetition.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ProgressOverview
|
||||
progressData={progressData}
|
||||
totalQuestions={questions.length}
|
||||
/>
|
||||
|
||||
{!hasProgress && (
|
||||
<div className="rounded-lg border border-[var(--border)] bg-[var(--card)] p-8 text-center">
|
||||
<h2 className="text-lg font-semibold mb-2">Get Started</h2>
|
||||
<p className="text-[var(--muted-foreground)] mb-4">
|
||||
Complete some questions to start tracking your progress. Your data
|
||||
is stored locally in your browser.
|
||||
</p>
|
||||
<Link
|
||||
href="/questions"
|
||||
className="inline-flex items-center px-4 py-2 text-sm font-medium rounded bg-[var(--primary)] text-[var(--primary-foreground)] hover:opacity-90 transition-opacity"
|
||||
>
|
||||
Browse Questions
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasProgress && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<ReviewSuggestions
|
||||
candidates={reviewCandidates}
|
||||
questions={questions}
|
||||
/>
|
||||
<PatternProgress
|
||||
progressData={progressData}
|
||||
patterns={patterns}
|
||||
questions={questions}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ProgressExport onImportSuccess={handleImportSuccess} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user