Pending course, rest ready for launch

This commit is contained in:
Marcelo
2026-03-15 13:52:11 +00:00
parent be4ca2ed78
commit 62b3cfe467
77 changed files with 6450 additions and 868 deletions

90
app/(protected)/practice/[slug]/page.tsx Executable file → Normal file
View File

@@ -1,17 +1,18 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useState, useTransition } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import ProgressBar from "@/components/ProgressBar";
import { getPracticeBySlug, mockPracticeModules } from "@/lib/data/mockPractice";
const attemptsKey = (slug: string) => `acve.practice-attempts.${slug}`;
import { getPracticeAttempts, submitPracticeAttempt } from "@/app/(protected)/practice/[slug]/actions";
type AttemptRecord = {
completedAt: string;
score: number;
total: number;
id: string;
scorePercent: number;
correctCount: number;
totalQuestions: number;
completedAt: Date;
};
export default function PracticeExercisePage() {
@@ -23,21 +24,16 @@ export default function PracticeExercisePage() {
const [score, setScore] = useState(0);
const [finished, setFinished] = useState(false);
const [selected, setSelected] = useState<number | null>(null);
const [selectedAnswers, setSelectedAnswers] = useState<number[]>([]);
const [attempts, setAttempts] = useState<AttemptRecord[]>([]);
const [isSaving, startTransition] = useTransition();
const loadAttempts = () => {
if (!practiceModule) return;
if (typeof window === "undefined") return;
const raw = window.localStorage.getItem(attemptsKey(practiceModule.slug));
if (!raw) {
setAttempts([]);
return;
}
try {
setAttempts(JSON.parse(raw) as AttemptRecord[]);
} catch {
setAttempts([]);
}
startTransition(async () => {
const result = await getPracticeAttempts(practiceModule.slug);
setAttempts(result as AttemptRecord[]);
});
};
useEffect(() => {
@@ -79,21 +75,23 @@ export default function PracticeExercisePage() {
};
const next = () => {
if (selected === null) return;
setSelectedAnswers((prev) => {
const nextAnswers = [...prev];
nextAnswers[index] = selected;
return nextAnswers;
});
if (index + 1 >= total) {
if (typeof window !== "undefined") {
const raw = window.localStorage.getItem(attemptsKey(practiceModule.slug));
const parsed = raw ? ((JSON.parse(raw) as AttemptRecord[]) ?? []) : [];
const nextAttempts = [
{
completedAt: new Date().toISOString(),
score,
total,
},
...parsed,
].slice(0, 5);
window.localStorage.setItem(attemptsKey(practiceModule.slug), JSON.stringify(nextAttempts));
setAttempts(nextAttempts);
}
const finalAnswers = [...selectedAnswers];
finalAnswers[index] = selected;
startTransition(async () => {
await submitPracticeAttempt({
slug: practiceModule.slug,
selectedAnswers: finalAnswers,
});
loadAttempts();
});
setFinished(true);
return;
}
@@ -106,6 +104,7 @@ export default function PracticeExercisePage() {
setIndex(0);
setScore(0);
setSelected(null);
setSelectedAnswers([]);
setFinished(false);
};
@@ -115,6 +114,7 @@ export default function PracticeExercisePage() {
setIndex(0);
setScore(0);
setSelected(null);
setSelectedAnswers([]);
};
if (finished) {
@@ -130,9 +130,6 @@ export default function PracticeExercisePage() {
<button className="acve-button-primary px-5 py-2 text-sm font-semibold hover:brightness-105" onClick={restart} type="button">
Retake quiz
</button>
<button className="rounded-md border border-slate-300 px-5 py-2 text-sm font-semibold text-slate-700" type="button">
Review answers (placeholder)
</button>
<Link className="rounded-md border border-slate-300 px-5 py-2 text-sm font-semibold text-slate-700" href="/practice">
Back to modules
</Link>
@@ -140,14 +137,15 @@ export default function PracticeExercisePage() {
</section>
<section className="acve-panel p-4">
<h2 className="text-base font-semibold text-slate-800">Attempt History (Mock)</h2>
<h2 className="text-base font-semibold text-slate-800">Attempt History</h2>
{attempts.length === 0 ? (
<p className="mt-2 text-sm text-slate-600">No attempts recorded yet.</p>
) : (
<ul className="mt-2 space-y-2">
{attempts.map((attempt) => (
<li key={attempt.completedAt} className="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-700">
Score {attempt.score}/{attempt.total} on {new Date(attempt.completedAt).toLocaleString()}
<li key={attempt.id} className="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-700">
Score {attempt.correctCount}/{attempt.totalQuestions} ({attempt.scorePercent}%) on{" "}
{new Date(attempt.completedAt).toLocaleString()}
</li>
))}
</ul>
@@ -175,7 +173,7 @@ export default function PracticeExercisePage() {
</div>
<div className="rounded-xl border border-slate-200 bg-white p-3">
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500">Difficulty</p>
<p className="mt-1 text-2xl font-semibold text-slate-900">Intermediate</p>
<p className="mt-1 text-2xl font-semibold text-slate-900">{practiceModule.difficulty ?? "Intermediate"}</p>
</div>
</div>
<div className="mt-5 flex flex-wrap items-center gap-2">
@@ -189,14 +187,15 @@ export default function PracticeExercisePage() {
</section>
<section className="acve-panel p-4">
<h2 className="text-base font-semibold text-slate-800">Attempt History (Mock)</h2>
<h2 className="text-base font-semibold text-slate-800">Attempt History</h2>
{attempts.length === 0 ? (
<p className="mt-2 text-sm text-slate-600">No attempts recorded yet for this module.</p>
) : (
<ul className="mt-2 space-y-2">
{attempts.map((attempt) => (
<li key={attempt.completedAt} className="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-700">
Score {attempt.score}/{attempt.total} on {new Date(attempt.completedAt).toLocaleString()}
<li key={attempt.id} className="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-700">
Score {attempt.correctCount}/{attempt.totalQuestions} ({attempt.scorePercent}%) on{" "}
{new Date(attempt.completedAt).toLocaleString()}
</li>
))}
</ul>
@@ -237,14 +236,15 @@ export default function PracticeExercisePage() {
</section>
<section className="acve-panel p-4">
<h2 className="text-lg font-semibold text-slate-800">Attempt History (Mock)</h2>
<h2 className="text-lg font-semibold text-slate-800">Attempt History</h2>
{attempts.length === 0 ? (
<p className="mt-2 text-sm text-slate-600">No attempts recorded yet.</p>
) : (
<ul className="mt-2 space-y-2">
{attempts.map((attempt) => (
<li key={attempt.completedAt} className="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-700">
Score {attempt.score}/{attempt.total} on {new Date(attempt.completedAt).toLocaleString()}
<li key={attempt.id} className="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-700">
Score {attempt.correctCount}/{attempt.totalQuestions} ({attempt.scorePercent}%) on{" "}
{new Date(attempt.completedAt).toLocaleString()}
</li>
))}
</ul>
@@ -285,7 +285,7 @@ export default function PracticeExercisePage() {
<button
className="acve-button-primary mt-6 px-6 py-2 text-sm font-semibold hover:brightness-105 disabled:opacity-50"
disabled={selected === null}
disabled={selected === null || isSaving}
onClick={next}
type="button"
>