Skip to content

Commit 74e6c0b

Browse files
author
VORTEX
committed
feat(ui): redesign app with modern learning interface
1 parent d0ed153 commit 74e6c0b

File tree

14 files changed

+512
-64
lines changed

14 files changed

+512
-64
lines changed

src/app/admin/page.tsx

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,40 @@
1+
import { Badge, Card, PageHeader } from "@/components/ui";
12
import { BRAND_NAME } from "@/lib/branding";
23
import { curriculum } from "@/lib/curriculum";
34

45
export default function AdminPage() {
56
return (
6-
<main className="p-8 max-w-5xl mx-auto space-y-4">
7-
<h1 className="text-3xl font-bold">{BRAND_NAME} Admin</h1>
8-
<p className="text-gray-600">Manage lessons/content snapshots.</p>
9-
<div className="overflow-auto border rounded">
10-
<table className="w-full text-sm">
11-
<thead>
12-
<tr className="bg-gray-100">
13-
<th className="p-2 text-left">Slug</th>
14-
<th className="p-2">Level</th>
15-
<th className="p-2">Sentences</th>
16-
</tr>
17-
</thead>
18-
<tbody>
19-
{curriculum.lessons.slice(0, 20).map((l) => (
20-
<tr key={l.slug} className="border-t">
21-
<td className="p-2">{l.slug}</td>
22-
<td className="p-2 text-center">{l.level}</td>
23-
<td className="p-2 text-center">{l.sentences.length}</td>
7+
<div className="space-y-6">
8+
<PageHeader
9+
title={`${BRAND_NAME} Admin`}
10+
subtitle="Beheer content, controleer leskwaliteit en houd curriculum-overzicht scherp."
11+
action={<Badge>Readonly demo</Badge>}
12+
/>
13+
14+
<Card>
15+
<div className="overflow-auto">
16+
<table className="w-full min-w-[560px] text-sm">
17+
<thead>
18+
<tr className="border-b border-slate-200 text-slate-500">
19+
<th className="p-3 text-left font-medium">Slug</th>
20+
<th className="p-3 text-left font-medium">Titel</th>
21+
<th className="p-3 text-center font-medium">Niveau</th>
22+
<th className="p-3 text-center font-medium">Zinnen</th>
2423
</tr>
25-
))}
26-
</tbody>
27-
</table>
28-
</div>
29-
</main>
24+
</thead>
25+
<tbody>
26+
{curriculum.lessons.slice(0, 20).map((l) => (
27+
<tr key={l.slug} className="border-b border-slate-100 last:border-b-0">
28+
<td className="p-3 font-mono text-xs text-slate-600">{l.slug}</td>
29+
<td className="p-3 text-slate-800">{l.title}</td>
30+
<td className="p-3 text-center">{l.level}</td>
31+
<td className="p-3 text-center">{l.sentences.length}</td>
32+
</tr>
33+
))}
34+
</tbody>
35+
</table>
36+
</div>
37+
</Card>
38+
</div>
3039
);
3140
}

src/app/globals.css

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
@import "tailwindcss";
22

33
:root {
4-
--background: #ffffff;
5-
--foreground: #171717;
4+
--background: #f8fafc;
5+
--foreground: #0f172a;
66
}
77

88
@theme inline {
@@ -12,15 +12,18 @@
1212
--font-mono: var(--font-geist-mono);
1313
}
1414

15-
@media (prefers-color-scheme: dark) {
16-
:root {
17-
--background: #0a0a0a;
18-
--foreground: #ededed;
19-
}
15+
* {
16+
box-sizing: border-box;
17+
}
18+
19+
html,
20+
body {
21+
margin: 0;
22+
padding: 0;
2023
}
2124

2225
body {
2326
background: var(--background);
2427
color: var(--foreground);
25-
font-family: Arial, Helvetica, sans-serif;
28+
font-family: var(--font-geist-sans), Inter, system-ui, sans-serif;
2629
}

src/app/layout.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Metadata } from "next";
22
import { Geist, Geist_Mono } from "next/font/google";
33
import { BRAND_DESCRIPTION, BRAND_NAME, BRAND_TAGLINE } from "@/lib/branding";
4+
import { AppShell } from "@/components/app-shell";
45
import "./globals.css";
56

67
const geistSans = Geist({
@@ -26,7 +27,9 @@ export default function RootLayout({
2627
}>) {
2728
return (
2829
<html lang="en">
29-
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>
30+
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
31+
<AppShell>{children}</AppShell>
32+
</body>
3033
</html>
3134
);
3235
}

src/app/lesson/[slug]/page.tsx

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,58 @@
1+
import { Badge, Card, PageHeader } from "@/components/ui";
2+
import { LessonPractice } from "@/components/lesson-practice";
13
import { getLesson } from "@/lib/curriculum";
4+
import { fixSpanishSentence, toEnglish } from "@/lib/content-fix";
25
import { notFound } from "next/navigation";
3-
export default async function LessonPage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const lesson = getLesson(slug); if (!lesson) return notFound(); return (<main className="p-8 max-w-4xl mx-auto space-y-4"><h1 className="text-3xl font-bold">{lesson.title}</h1><p className="text-sm text-gray-500">Level {lesson.level} · Unit {lesson.unit}</p><div className="space-y-3">{lesson.exercises.map((ex, idx) => (<div key={idx} className="border rounded p-4 bg-white"><p className="text-xs text-gray-500 uppercase">{ex.type}</p><p className="font-medium">{ex.prompt}</p><p className="text-sm text-gray-600 mt-2">Answer: {ex.answer}</p></div>))}</div></main>); }
6+
7+
function normalizeExercise(type: string, prompt: string, answer: string) {
8+
const fixedPromptEs = fixSpanishSentence(prompt);
9+
const fixedAnswerEs = fixSpanishSentence(answer);
10+
11+
if (type === "translate_to_spanish") {
12+
return {
13+
type,
14+
prompt: toEnglish(fixedAnswerEs),
15+
answer: fixedAnswerEs,
16+
};
17+
}
18+
19+
if (type === "translate_to_english") {
20+
return {
21+
type,
22+
prompt: fixedPromptEs,
23+
answer: toEnglish(fixedPromptEs),
24+
};
25+
}
26+
27+
return {
28+
type,
29+
prompt: fixedPromptEs,
30+
answer: fixedAnswerEs,
31+
};
32+
}
33+
34+
export default async function LessonPage({ params }: { params: Promise<{ slug: string }> }) {
35+
const { slug } = await params;
36+
const lesson = getLesson(slug);
37+
if (!lesson) return notFound();
38+
39+
const exercises = lesson.exercises.map((ex) => normalizeExercise(ex.type, ex.prompt, ex.answer));
40+
41+
return (
42+
<div className="space-y-6">
43+
<PageHeader
44+
title={lesson.title}
45+
subtitle={`Level ${lesson.level} · Unit ${lesson.unit}`}
46+
action={<Badge tone="warning">Beginner mode actief</Badge>}
47+
/>
48+
49+
<Card className="bg-sky-50 border-sky-200">
50+
<p className="text-sm text-sky-800">
51+
Zinnen worden automatisch grammaticaal gecorrigeerd en waar nodig vertaald, zodat je als beginner sneller begrijpt waarom een antwoord klopt.
52+
</p>
53+
</Card>
54+
55+
<LessonPractice exercises={exercises} />
56+
</div>
57+
);
58+
}

src/app/page.tsx

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,58 @@
11
import Link from "next/link";
2+
import { Badge, Card, PageHeader, ProgressBar } from "@/components/ui";
23
import { BRAND_NAME } from "@/lib/branding";
34
import { curriculum } from "@/lib/curriculum";
45

6+
const quickActions = [
7+
{ href: "/lesson/lesson-1", title: "Start eerste les", desc: "Begin stap voor stap met basiszinnen." },
8+
{ href: "/review", title: "Review sessie", desc: "Herhaal woorden die je bijna vergeet." },
9+
{ href: "/practice", title: "Vrij oefenen", desc: "Train op snelheid en begrip." },
10+
{ href: "/vocab", title: "Bekijk vocab", desc: "Zoek en markeer nieuwe woorden." },
11+
];
12+
513
export default function Home() {
14+
const progress = 22;
15+
616
return (
7-
<main className="p-8 max-w-5xl mx-auto space-y-6">
8-
<h1 className="text-4xl font-bold">{BRAND_NAME} — Learn Spanish</h1>
9-
<p className="text-gray-600">Open-source Spanish training with production-focused reliability.</p>
17+
<div className="space-y-6">
18+
<PageHeader
19+
title={`Welkom bij ${BRAND_NAME}`}
20+
subtitle="Een minimal leeromgeving voor beginners: duidelijk, rustig en direct bruikbaar."
21+
action={<Badge tone="success">Streak 4 dagen</Badge>}
22+
/>
1023

11-
<div className="grid grid-cols-2 gap-4">
12-
<Link className="rounded bg-blue-600 text-white p-4" href="/lesson/lesson-1">
13-
Start Lesson
14-
</Link>
15-
<Link className="rounded bg-emerald-600 text-white p-4" href="/vocab">
16-
Vocabulary Browser
17-
</Link>
18-
<Link className="rounded bg-purple-600 text-white p-4" href="/admin">
19-
Admin Panel
20-
</Link>
21-
</div>
24+
<div className="grid gap-4 md:grid-cols-3">
25+
<Card className="md:col-span-2">
26+
<p className="text-sm text-slate-500">Je voortgang</p>
27+
<p className="mt-1 text-2xl font-semibold">A1 · Unit 1</p>
28+
<p className="mt-2 text-sm text-slate-600">Je bent goed bezig. Rond nog 3 oefeningen af om Unit 1 te voltooien.</p>
29+
<div className="mt-4">
30+
<ProgressBar value={progress} />
31+
<p className="mt-2 text-xs text-slate-500">{progress}% van je weekdoel</p>
32+
</div>
33+
<Link href="/lesson/lesson-1" className="mt-4 inline-flex rounded-xl bg-sky-600 px-4 py-2 text-sm font-medium text-white hover:bg-sky-700">
34+
Doorgaan met les
35+
</Link>
36+
</Card>
2237

23-
<div className="rounded border p-4">
24-
<h2 className="font-semibold">Curriculum Stats</h2>
25-
<p>
26-
{curriculum.meta.lessons} lessons · {curriculum.meta.sentences} sentences · {curriculum.meta.exerciseTypes.length} exercise
27-
types
28-
</p>
38+
<Card>
39+
<p className="text-sm text-slate-500">Curriculum</p>
40+
<ul className="mt-3 space-y-2 text-sm text-slate-700">
41+
<li>{curriculum.meta.lessons} lessen</li>
42+
<li>{curriculum.meta.sentences} voorbeeldzinnen</li>
43+
<li>{curriculum.meta.exerciseTypes.length} oefenvormen</li>
44+
</ul>
45+
</Card>
2946
</div>
30-
</main>
47+
48+
<section className="grid gap-3 md:grid-cols-2">
49+
{quickActions.map((item) => (
50+
<Link key={item.href} href={item.href} className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition hover:border-sky-300 hover:shadow">
51+
<p className="font-semibold text-slate-900">{item.title}</p>
52+
<p className="mt-1 text-sm text-slate-600">{item.desc}</p>
53+
</Link>
54+
))}
55+
</section>
56+
</div>
3157
);
3258
}

src/app/practice/page.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Badge, Card, ErrorState, PageHeader } from "@/components/ui";
2+
3+
const drills = ["Vertalen", "Luisteren", "Woordvolgorde", "Dictee"];
4+
5+
export default function PracticePage() {
6+
return (
7+
<div className="space-y-6">
8+
<PageHeader title="Practice" subtitle="Vrij trainen op een oefenvorm die bij je past." action={<Badge>4 modes</Badge>} />
9+
<div className="grid gap-3 md:grid-cols-2">
10+
{drills.map((drill) => (
11+
<Card key={drill}>
12+
<p className="font-semibold text-slate-900">{drill}</p>
13+
<p className="mt-1 text-sm text-slate-600">Korte sessie van 5 minuten met directe feedback.</p>
14+
</Card>
15+
))}
16+
</div>
17+
<ErrorState message="Audio-oefeningen worden later toegevoegd. Gebruik voorlopig Translate en Word Order." />
18+
</div>
19+
);
20+
}

src/app/profile/page.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Badge, Card, PageHeader } from "@/components/ui";
2+
3+
export default function ProfilePage() {
4+
return (
5+
<div className="space-y-6">
6+
<PageHeader title="Profile" subtitle="Jouw leerprofiel, voorkeuren en dagelijkse doelen." action={<Badge tone="success">Actief</Badge>} />
7+
<div className="grid gap-4 md:grid-cols-2">
8+
<Card>
9+
<p className="text-sm text-slate-500">Doel</p>
10+
<p className="mt-1 font-semibold">15 minuten per dag</p>
11+
<p className="mt-1 text-sm text-slate-600">Focus op basisgesprekken en reiszinnen.</p>
12+
</Card>
13+
<Card>
14+
<p className="text-sm text-slate-500">Voorkeur</p>
15+
<p className="mt-1 font-semibold">NL → ES vertaling</p>
16+
<p className="mt-1 text-sm text-slate-600">Langzaam tempo, extra context bij fouten.</p>
17+
</Card>
18+
</div>
19+
</div>
20+
);
21+
}

src/app/review/page.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Link from "next/link";
2+
import { Badge, Card, EmptyState, PageHeader, ProgressBar } from "@/components/ui";
3+
4+
export default function ReviewPage() {
5+
const due = 12;
6+
return (
7+
<div className="space-y-6">
8+
<PageHeader title="Review" subtitle="Herhaal slim met focus op zwakke woorden." action={<Badge tone="warning">{due} kaarten te doen</Badge>} />
9+
<Card>
10+
<p className="text-sm text-slate-500">SRS queue</p>
11+
<p className="mt-1 text-xl font-semibold">Vandaag: {due} kaarten</p>
12+
<div className="mt-3"><ProgressBar value={35} /></div>
13+
<Link href="/lesson/lesson-1" className="mt-4 inline-flex rounded-xl bg-sky-600 px-4 py-2 text-sm font-medium text-white hover:bg-sky-700">Start review</Link>
14+
</Card>
15+
<EmptyState title="Geen gemiste sessies" description="Je ligt op schema. Kom later terug voor nieuwe review-kaarten." />
16+
</div>
17+
);
18+
}

src/app/statistics/page.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Badge, Card, PageHeader, ProgressBar } from "@/components/ui";
2+
3+
export default function StatisticsPage() {
4+
return (
5+
<div className="space-y-6">
6+
<PageHeader title="Statistics" subtitle="Bekijk je leertrend en consistentie." action={<Badge>Laatste 7 dagen</Badge>} />
7+
<div className="grid gap-4 md:grid-cols-3">
8+
<Card>
9+
<p className="text-sm text-slate-500">Studietijd</p>
10+
<p className="mt-1 text-2xl font-semibold">83 min</p>
11+
</Card>
12+
<Card>
13+
<p className="text-sm text-slate-500">Correct rate</p>
14+
<p className="mt-1 text-2xl font-semibold">78%</p>
15+
<div className="mt-2"><ProgressBar value={78} /></div>
16+
</Card>
17+
<Card>
18+
<p className="text-sm text-slate-500">Streak</p>
19+
<p className="mt-1 text-2xl font-semibold">4 dagen</p>
20+
</Card>
21+
</div>
22+
</div>
23+
);
24+
}

src/app/vocab/page.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
1+
import { Badge, Card, PageHeader } from "@/components/ui";
12
import { BRAND_NAME } from "@/lib/branding";
23
import { curriculum } from "@/lib/curriculum";
34

45
export default function VocabPage() {
56
return (
6-
<main className="p-8 max-w-5xl mx-auto">
7-
<h1 className="text-3xl font-bold mb-4">{BRAND_NAME} Vocabulary Browser</h1>
8-
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
9-
{curriculum.vocabulary.map((word) => (
10-
<span key={word} className="rounded border px-2 py-1 text-sm">
11-
{word}
12-
</span>
13-
))}
14-
</div>
15-
</main>
7+
<div className="space-y-6">
8+
<PageHeader
9+
title={`${BRAND_NAME} Vocabulary`}
10+
subtitle="Snel woorden browsen en herkennen op niveau A1/A2."
11+
action={<Badge tone="success">{curriculum.vocabulary.length} woorden</Badge>}
12+
/>
13+
14+
<Card>
15+
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-5">
16+
{curriculum.vocabulary.map((word) => (
17+
<span key={word} className="rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-700">
18+
{word}
19+
</span>
20+
))}
21+
</div>
22+
</Card>
23+
</div>
1624
);
1725
}

0 commit comments

Comments
 (0)