diff --git a/ui/homepage/.eslintrc.json b/ui/homepage/.eslintrc.json new file mode 100644 index 0000000..cb6d4e4 --- /dev/null +++ b/ui/homepage/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "next/core-web-vitals" + ] +} diff --git a/ui/homepage/app/docs/[name]/page.tsx b/ui/homepage/app/docs/[name]/page.tsx index d42c4d5..9cace2e 100644 --- a/ui/homepage/app/docs/[name]/page.tsx +++ b/ui/homepage/app/docs/[name]/page.tsx @@ -1,172 +1,185 @@ import Link from 'next/link' +import { notFound } from 'next/navigation' +import type { Metadata } from 'next' +import { ExternalLink, FileText, Monitor, type LucideIcon } from 'lucide-react' + import Breadcrumbs, { Crumb } from '../../../components/download/Breadcrumbs' -import MarkdownPanel from '../../../components/download/MarkdownPanel' import { formatDate } from '../../../lib/format' -import listings from '../../../public/dl-index/all.json' -import type { DirListing } from '../../../types/download' +import { docResources, getDocResource } from '../resources' -interface DocFile { - path: string - href: string - title: string - segments: string[] - lastModified?: string +type ViewMode = 'pdf' | 'html' + +interface ViewOption { + id: ViewMode + label: string + description: string + url: string + icon: LucideIcon } -function formatSectionTitle(name: string): string { - return name - .split(/[-_]/g) - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(' ') +function normalizeViewParam(viewParam: string | string[] | undefined): ViewMode | undefined { + if (!viewParam) return undefined + const value = Array.isArray(viewParam) ? viewParam[0] : viewParam + if (value === 'pdf' || value === 'html') return value + return undefined } -function collectDocFiles(listing: DirListing, all: DirListing[], base = ''): DocFile[] { - const files: DocFile[] = [] - for (const entry of listing.entries) { - if (entry.type === 'file') { - const lower = entry.name.toLowerCase() - if (lower.endsWith('.md') || lower.endsWith('.mdx')) { - const relative = base ? `${base}/${entry.name}` : entry.name - const segments = relative - .split('/') - .map((segment) => segment.replace(/\.(md|mdx)$/i, '')) - files.push({ - path: relative, - href: entry.href, - title: formatSectionTitle(segments[segments.length - 1]), - segments, - lastModified: entry.lastModified, - }) - } - } else if (entry.type === 'dir') { - const child = all.find((l) => l.path === `${listing.path}${entry.name}/`) - if (child) { - const nextBase = base ? `${base}/${entry.name}` : entry.name - files.push(...collectDocFiles(child, all, nextBase)) - } - } - } - return files.sort((a, b) => a.path.localeCompare(b.path)) -} - -function buildCrumbs(section: string, selected?: DocFile): Crumb[] { - const crumbs: Crumb[] = [ +function buildBreadcrumbs(slug: string, docTitle: string): Crumb[] { + return [ { label: 'Docs', href: '/docs' }, - { label: formatSectionTitle(section), href: `/docs/${section}` }, + { label: docTitle, href: `/docs/${slug}` }, ] - - if (selected) { - const label = selected.segments.map((segment) => formatSectionTitle(segment)).join(' / ') - crumbs.push({ label, href: `/docs/${section}?file=${encodeURIComponent(selected.path)}` }) - } - - return crumbs -} - -function normalizeFileParam(fileParam: string | string[] | undefined): string | undefined { - if (!fileParam) return undefined - if (Array.isArray(fileParam)) { - return fileParam[0] - } - return fileParam } export function generateStaticParams() { - const allListings = listings as DirListing[] - const docsRoot = allListings.find((l) => l.path === 'docs/') - if (!docsRoot) return [] as { name: string }[] - return docsRoot.entries - .filter((entry) => entry.type === 'dir') - .map((entry) => ({ name: entry.name })) + return docResources.map((doc) => ({ name: doc.slug })) } -export const dynamic = 'force-dynamic' +export function generateMetadata({ params }: { params: { name: string } }): Metadata { + const doc = getDocResource(params.name) + if (!doc) { + return { title: 'Documentation' } + } + return { + title: `${doc.title} | Documentation`, + description: doc.description, + } +} export default function DocPage({ params, searchParams, }: { params: { name: string } - searchParams?: { file?: string | string[] } + searchParams?: { view?: string | string[] } }) { - const allListings = listings as DirListing[] - const docListing = allListings.find((l) => l.path === `docs/${params.name}/`) - - if (!docListing) { - return ( -
-
Documentation section not found.
-
- ) + const doc = getDocResource(params.name) + if (!doc) { + notFound() } - const docFiles = collectDocFiles(docListing, allListings) - if (docFiles.length === 0) { - return ( -
-
- This section does not contain any Markdown documents yet. -
-
- ) + const viewOptions: ViewOption[] = [] + if (doc.pdfUrl) { + viewOptions.push({ + id: 'pdf', + label: 'PDF', + description: 'Best for printing and full fidelity diagrams.', + url: doc.pdfUrl, + icon: FileText, + }) + } + if (doc.htmlUrl) { + viewOptions.push({ + id: 'html', + label: 'HTML', + description: 'Responsive reader mode optimised for browsers.', + url: doc.htmlUrl, + icon: Monitor, + }) } - const selectedPath = normalizeFileParam(searchParams?.file) - const selected = selectedPath ? docFiles.find((file) => file.path === selectedPath) : docFiles[0] - const active = selected ?? docFiles[0] - const crumbs = buildCrumbs(params.name, active) + const normalizedView = normalizeViewParam(searchParams?.view) + const activeView = viewOptions.find((view) => view.id === normalizedView) ?? viewOptions[0] + + const breadcrumbs = buildBreadcrumbs(doc.slug, doc.title) + const ActiveIcon = activeView?.icon return (
-
- -
- -
-
-

- {active.segments.map((segment) => formatSectionTitle(segment)).join(' / ')} -

- {active.lastModified && ( - - Updated {formatDate(active.lastModified)} - +
+ + +
+
+
+

+ {doc.category || 'Documentation'} {doc.version ? `• ${doc.version}` : ''} +

+

{doc.title}

+

{doc.description}

+ {doc.tags && doc.tags.length > 0 && ( +
+ {doc.tags.map((tag) => ( + + {tag} + + ))} +
)}
+
+ {doc.updatedAt && Updated {formatDate(doc.updatedAt)}} + {doc.estimatedMinutes && Approx. {doc.estimatedMinutes} minute read} +
- -
+ + {viewOptions.length > 0 && ( +
+ {viewOptions.map((view) => { + const isActive = activeView?.id === view.id + const Icon = view.icon + return ( + +
+ +
+
+
+ {view.label} + {isActive && Selected} +
+

{view.description}

+
+ + ) + })} +
+ )} + + {activeView && ( +
+ {ActiveIcon && ( + + + {activeView.label} view selected + + )} + + Open in new tab + +
+ )} + + +
+ {activeView ? ( +