Add the Next.js web app and shared lesson content.

Move lessons out of the C sources into JSON so the web client can use the same material as the desktop app, and point release builds at desktop/.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
FBukovina 2026-09-17 16:06:15 +02:00
commit f7470d333c
113 changed files with 25817 additions and 9 deletions

View file

@ -0,0 +1,53 @@
'use client';
import Link from 'next/link';
import { useApp } from '@/app/providers';
import { TopBar } from '@/components/TopBar';
import { LockIcon } from '@/components/icons';
/** Only year 1 has content, matching `build_net_years_page` in the desktop app. */
const YEARS = [1, 2, 3, 4];
export function NetworkYearsScreen() {
const { t } = useApp();
return (
<div className="shell">
<TopBar title={t('net_years_title')} backHref="/subjects" />
<main className="page">
<div className="page__head">
<h1 className="page__title">{t('net_years_title')}</h1>
<p className="page__sub">{t('net_years_sub')}</p>
</div>
<div className="grid">
{YEARS.map((year) => {
const label = t(`net_year${year}`);
const locked = year !== 1;
if (locked) {
return (
<div className="card card--locked" key={year} aria-disabled>
<span className="row" style={{ color: 'var(--overlay)' }}>
<LockIcon size={22} />
</span>
<h2 className="card__title">{label}</h2>
<p className="card__body">{t('net_year_locked_sub')}</p>
</div>
);
}
return (
<Link className="card card--interactive" href={`/networks/year-${year}`} key={year}>
<span className="card__kicker">{year}</span>
<h2 className="card__title">{label}</h2>
</Link>
);
})}
</div>
</main>
</div>
);
}

View file

@ -0,0 +1,6 @@
import { NetworkYearsScreen } from './NetworkYearsScreen';
/** Year picker: year 1 has all 27 lessons, years 2-4 are placeholders. */
export default function NetworkYearsPage() {
return <NetworkYearsScreen />;
}

View file

@ -0,0 +1,23 @@
import { notFound } from 'next/navigation';
import { getLesson, getLessonIds } from '@/content';
import { LessonScreen } from '@/components/LessonScreen';
export async function generateStaticParams() {
const ids = await getLessonIds('networks');
return ids.map((lesson) => ({ lesson }));
}
export default async function NetworkLessonPage({
params,
}: {
params: Promise<{ lesson: string }>;
}) {
const { lesson: id } = await params;
try {
return <LessonScreen lesson={await getLesson('networks', id)} backHref="/networks/year-1" />;
} catch {
notFound();
}
}

View file

@ -0,0 +1,23 @@
import { notFound } from 'next/navigation';
import { getLessons, getPath } from '@/content';
import { PathScreen } from '@/components/PathScreen';
export default async function NetworkYear1Page() {
const path = await getPath('networks');
if (!path) notFound();
const lessons = await getLessons('networks');
const taskCounts = Object.fromEntries(lessons.map((l) => [l.id, l.tasks.length]));
return (
<PathScreen
path={path}
titleKey="net_years_title"
subtitleKey="net_sub"
backHref="/networks"
hrefBase="/networks/year-1"
taskCounts={taskCounts}
/>
);
}