feat(03-03): expanded module view with dynamic routing and API client

- Create [moduleSlug] page that loads module component via MODULE_REGISTRY whitelist
- Only registered slugs trigger dynamic imports; unknown slugs show not-found state (T-03-09)
- Create api.ts with getActiveModules and getModuleBySlug utility functions
- Back-link navigation from expanded view to category page
This commit is contained in:
2026-06-19 13:38:25 +02:00
parent a191628fa5
commit de63b10749
2 changed files with 165 additions and 0 deletions
@@ -0,0 +1,98 @@
'use client';
import { loadModuleComponent, MODULE_REGISTRY } from '@/lib/module-loader';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { useParams } from 'next/navigation';
/**
* Expanded module view — renders the full module UI via lazy loading.
*
* Per D-05b: expanded/full view when user opens a specific module.
* Uses loadModuleComponent to get the dynamically imported component.
*
* Security (T-03-09): Only slugs whitelisted in MODULE_REGISTRY are loaded.
* Arbitrary slugs from URL params result in a not-found state — no
* arbitrary imports are ever triggered.
*/
export default function ExpandedModulePage() {
const params = useParams<{ category: string; moduleSlug: string }>();
const { category, moduleSlug } = params;
const t = useTranslations('modules');
// Only load from the whitelist registry (T-03-09)
const isRegistered = moduleSlug in MODULE_REGISTRY;
const ModuleComponent = isRegistered
? loadModuleComponent(moduleSlug)
: null;
// Not-found state for unknown/unregistered slugs
if (!ModuleComponent) {
return (
<div className="mx-auto max-w-2xl space-y-6 p-6">
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="rounded-lg bg-muted p-4 mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className="text-muted-foreground"
>
<circle cx="12" cy="12" r="10" />
<path d="m15 9-6 6" />
<path d="m9 9 6 6" />
</svg>
</div>
<h2 className="text-xl font-semibold mb-2">{t('notFound')}</h2>
<p className="text-sm text-muted-foreground mb-6">
{t('notFoundDescription')}
</p>
<Link
href={`/modules/${category}`}
className="text-sm font-medium text-primary hover:underline"
>
{t('backToCategory')}
</Link>
</div>
</div>
);
}
// Render the lazily loaded module component
return (
<div className="space-y-4">
{/* Back navigation */}
<div className="px-6 pt-4">
<Link
href={`/modules/${category}`}
className="inline-flex items-center gap-1.5 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m12 19-7-7 7-7" />
<path d="M19 12H5" />
</svg>
{t('backToCategory')}
</Link>
</div>
{/* Module content — loaded on demand (MOD-04) */}
<ModuleComponent />
</div>
);
}