feat(frontend): add progress tracking
This commit is contained in:
26
frontend/src/components/questions/completion-indicator.tsx
Normal file
26
frontend/src/components/questions/completion-indicator.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { CheckCircle } from "lucide-react";
|
||||||
|
import { isQuestionCompleted } from "@/lib/progress";
|
||||||
|
|
||||||
|
interface CompletionIndicatorProps {
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CompletionIndicator({ slug }: CompletionIndicatorProps) {
|
||||||
|
const [completed, setCompleted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCompleted(isQuestionCompleted(slug));
|
||||||
|
}, [slug]);
|
||||||
|
|
||||||
|
if (!completed) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CheckCircle
|
||||||
|
className="h-4 w-4 text-green-500 flex-shrink-0"
|
||||||
|
aria-label="Completed"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { getDifficultyVariant, getDifficultyLabel, capitalize } from "@/lib/utils";
|
import { getDifficultyVariant, getDifficultyLabel, capitalize } from "@/lib/utils";
|
||||||
|
import { CompletionIndicator } from "./completion-indicator";
|
||||||
import type { QuestionListItem } from "@/types";
|
import type { QuestionListItem } from "@/types";
|
||||||
|
|
||||||
interface QuestionCardProps {
|
interface QuestionCardProps {
|
||||||
@@ -14,7 +15,10 @@ export function QuestionCard({ question }: QuestionCardProps) {
|
|||||||
className="block p-4 rounded-lg border border-[var(--border)] bg-[var(--card)] hover:border-[var(--primary)] transition-colors"
|
className="block p-4 rounded-lg border border-[var(--border)] bg-[var(--card)] hover:border-[var(--primary)] transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-4 mb-3">
|
<div className="flex items-start justify-between gap-4 mb-3">
|
||||||
<h3 className="font-semibold">{question.title}</h3>
|
<div className="flex items-center gap-2">
|
||||||
|
<CompletionIndicator slug={question.slug} />
|
||||||
|
<h3 className="font-semibold">{question.title}</h3>
|
||||||
|
</div>
|
||||||
<Badge
|
<Badge
|
||||||
variant={getDifficultyVariant(question.difficulty)}
|
variant={getDifficultyVariant(question.difficulty)}
|
||||||
aria-label={getDifficultyLabel(question.difficulty)}
|
aria-label={getDifficultyLabel(question.difficulty)}
|
||||||
|
|||||||
40
frontend/src/lib/progress.ts
Normal file
40
frontend/src/lib/progress.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const PROGRESS_KEY = "codetutor-progress";
|
||||||
|
const SOLUTIONS_KEY = "codetutor-solutions";
|
||||||
|
|
||||||
|
export function getCompletedQuestions(): Set<string> {
|
||||||
|
if (typeof window === "undefined") return new Set();
|
||||||
|
const stored = localStorage.getItem(PROGRESS_KEY);
|
||||||
|
return stored ? new Set(JSON.parse(stored)) : new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markQuestionCompleted(slug: string, code?: string): void {
|
||||||
|
const completed = getCompletedQuestions();
|
||||||
|
completed.add(slug);
|
||||||
|
localStorage.setItem(PROGRESS_KEY, JSON.stringify([...completed]));
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
saveSolution(slug, code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isQuestionCompleted(slug: string): boolean {
|
||||||
|
return getCompletedQuestions().has(slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveSolution(slug: string, code: string): void {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
const solutions = getSavedSolutions();
|
||||||
|
solutions[slug] = code;
|
||||||
|
localStorage.setItem(SOLUTIONS_KEY, JSON.stringify(solutions));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSavedSolution(slug: string): string | null {
|
||||||
|
if (typeof window === "undefined") return null;
|
||||||
|
const solutions = getSavedSolutions();
|
||||||
|
return solutions[slug] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSavedSolutions(): Record<string, string> {
|
||||||
|
const stored = localStorage.getItem(SOLUTIONS_KEY);
|
||||||
|
return stored ? JSON.parse(stored) : {};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user