This commit is contained in:
Marcelo
2026-02-17 00:07:00 +00:00
parent b7a86a2d1c
commit be4ca2ed78
92 changed files with 6850 additions and 1188 deletions

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

@@ -1,19 +1,49 @@
"use client";
import { useState } from "react";
import { useEffect, useState } 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}`;
type AttemptRecord = {
completedAt: string;
score: number;
total: number;
};
export default function PracticeExercisePage() {
const params = useParams<{ slug: string }>();
const practiceModule = getPracticeBySlug(params.slug);
const [started, setStarted] = useState(false);
const [index, setIndex] = useState(0);
const [score, setScore] = useState(0);
const [finished, setFinished] = useState(false);
const [selected, setSelected] = useState<number | null>(null);
const [attempts, setAttempts] = useState<AttemptRecord[]>([]);
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([]);
}
};
useEffect(() => {
loadAttempts();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [practiceModule?.slug]);
if (!practiceModule) {
return (
@@ -50,6 +80,20 @@ export default function PracticeExercisePage() {
const next = () => {
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);
}
setFinished(true);
return;
}
@@ -58,31 +102,120 @@ export default function PracticeExercisePage() {
};
const restart = () => {
setStarted(true);
setIndex(0);
setScore(0);
setSelected(null);
setFinished(false);
};
const start = () => {
setStarted(true);
setFinished(false);
setIndex(0);
setScore(0);
setSelected(null);
};
if (finished) {
return (
<div className="mx-auto max-w-3xl rounded-2xl border border-slate-300 bg-white p-8 text-center shadow-sm">
<h1 className="acve-heading text-5xl">Exercise complete</h1>
<p className="mt-2 text-3xl text-slate-700">
Final score: {score}/{total}
</p>
<button className="acve-button-primary mt-6 px-6 py-2 text-lg font-semibold hover:brightness-105" onClick={restart} type="button">
Restart
</button>
<div className="acve-page">
<section className="acve-panel p-6 text-center md:p-8">
<p className="acve-pill mx-auto mb-3 w-fit">Practice Results</p>
<h1 className="text-3xl font-semibold text-[#222a38] md:text-4xl">Exercise complete</h1>
<p className="mt-2 text-xl text-slate-700 md:text-2xl">
Final score: {score}/{total}
</p>
<div className="mt-5 flex flex-wrap items-center justify-center gap-2">
<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>
</div>
</section>
<section className="acve-panel p-4">
<h2 className="text-base font-semibold text-slate-800">Attempt History (Mock)</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>
))}
</ul>
)}
</section>
</div>
);
}
if (!started) {
return (
<div className="acve-page">
<section className="acve-panel acve-section-base">
<p className="acve-pill mb-3 w-fit">Practice Session</p>
<h1 className="text-3xl font-semibold leading-tight text-[#222a38] md:text-4xl">{practiceModule.title}</h1>
<p className="mt-2 max-w-3xl text-base leading-relaxed text-slate-600">{practiceModule.description}</p>
<div className="mt-4 grid gap-3 sm:grid-cols-3">
<div className="rounded-xl border border-slate-200 bg-white p-3">
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500">Questions</p>
<p className="mt-1 text-2xl font-semibold text-slate-900">{total}</p>
</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">Estimated time</p>
<p className="mt-1 text-2xl font-semibold text-slate-900">{Math.max(3, total * 2)} min</p>
</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>
</div>
</div>
<div className="mt-5 flex flex-wrap items-center gap-2">
<button className="acve-button-primary px-5 py-2 text-sm font-semibold hover:brightness-105" onClick={start} type="button">
Start practice
</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>
</div>
</section>
<section className="acve-panel p-4">
<h2 className="text-base font-semibold text-slate-800">Attempt History (Mock)</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>
))}
</ul>
)}
</section>
</div>
);
}
return (
<div className="space-y-6">
<section className="text-center">
<p className="acve-pill mx-auto mb-4 w-fit">Practice and Exercises</p>
<h1 className="acve-heading text-4xl md:text-6xl">Master Your Skills</h1>
<div className="acve-page">
<section className="acve-panel p-4">
<div className="mb-3 flex flex-wrap items-center justify-between gap-3">
<p className="text-base font-semibold text-slate-700">
{practiceModule.title} | Question {index + 1} / {total}
</p>
<p className="text-base font-semibold text-[#222a38]">Score: {score}/{total}</p>
</div>
<ProgressBar value={progress} />
</section>
<section className="grid gap-4 md:grid-cols-3">
@@ -95,28 +228,33 @@ export default function PracticeExercisePage() {
isActive ? "border-brand bg-white shadow-sm" : "border-slate-300 bg-white"
}`}
>
<div className="mb-2 text-3xl text-brand md:text-4xl">{moduleIndex === 0 ? "A/" : moduleIndex === 1 ? "[]" : "O"}</div>
<h2 className="text-2xl text-[#222a38] md:text-4xl">{module.title}</h2>
<p className="mt-2 text-lg text-slate-600 md:text-2xl">{module.description}</p>
<div className="mb-2 text-3xl text-brand">{moduleIndex === 0 ? "A/" : moduleIndex === 1 ? "[]" : "O"}</div>
<h2 className="text-xl text-[#222a38]">{module.title}</h2>
<p className="mt-2 text-sm text-slate-600">{module.description}</p>
</div>
);
})}
</section>
<section className="acve-panel p-4">
<div className="mb-3 flex items-center justify-between gap-4">
<p className="text-xl font-semibold text-slate-700">
Question {index + 1} / {total}
</p>
<p className="text-xl font-semibold text-[#222a38] md:text-2xl">Score: {score}/{total}</p>
</div>
<ProgressBar value={progress} />
<h2 className="text-lg font-semibold text-slate-800">Attempt History (Mock)</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>
))}
</ul>
)}
</section>
<section className="acve-panel p-6">
<div className="rounded-2xl bg-[#f6f6f8] px-6 py-10 text-center">
<p className="text-lg text-slate-500 md:text-2xl">Spanish Term</p>
<h2 className="acve-heading mt-2 text-3xl md:text-6xl">{current.prompt.replace("Spanish term: ", "")}</h2>
<div className="rounded-2xl bg-[#f6f6f8] px-6 py-8 text-center">
<p className="text-sm font-semibold uppercase tracking-wide text-slate-500">Prompt</p>
<h2 className="mt-2 text-3xl font-semibold text-[#222a38] md:text-4xl">{current.prompt.replace("Spanish term: ", "")}</h2>
</div>
<div className="mt-6 grid gap-3">
@@ -135,7 +273,7 @@ export default function PracticeExercisePage() {
return (
<button
key={choice}
className={`rounded-xl border px-4 py-3 text-left text-base text-slate-800 md:text-xl ${stateClass}`}
className={`rounded-xl border px-4 py-3 text-left text-base text-slate-800 ${stateClass}`}
onClick={() => pick(choiceIndex)}
type="button"
>
@@ -146,7 +284,7 @@ export default function PracticeExercisePage() {
</div>
<button
className="acve-button-primary mt-6 px-6 py-2 text-lg font-semibold hover:brightness-105 disabled:opacity-50"
className="acve-button-primary mt-6 px-6 py-2 text-sm font-semibold hover:brightness-105 disabled:opacity-50"
disabled={selected === null}
onClick={next}
type="button"