mirror of
https://github.com/luxfi/lps-docs.git
synced 2026-10-30 16:20:48 +00:00
@luxfi/lps-docs. No history, no remote, not on GitHub. Co-authored-by: Hanzo Dev <[email protected]>
354 lines
13 KiB
TypeScript
354 lines
13 KiB
TypeScript
import { source } from '@/lib/source';
|
|
import config from '@/rfc.config';
|
|
import { notFound } from 'next/navigation';
|
|
import ReactMarkdown from 'react-markdown';
|
|
import remarkGfm from 'remark-gfm';
|
|
import Link from 'next/link';
|
|
import { ArrowLeft, ArrowRight, ExternalLink, Calendar, User, Tag } from 'lucide-react';
|
|
import { DocsPage, DocsBody } from '@hanzo/docs/ui/page';
|
|
import { extractHeadings } from '@/lib/toc';
|
|
import { FilteredView } from './filtered-view';
|
|
|
|
const shortName = config.shortName;
|
|
const shortNameKey = shortName.toLowerCase();
|
|
|
|
// RFC Index/Overview Page Component
|
|
function RFCIndexPage() {
|
|
const categories = source.getCategorizedPages();
|
|
const stats = source.getStats();
|
|
const allPages = source.getAllPages();
|
|
|
|
return (
|
|
<>
|
|
{/* Client-side filtered view - reads URL params and shows/hides based on filter presence */}
|
|
<FilteredView allPages={allPages} shortName={shortName} />
|
|
|
|
{/* Default view - categories (hidden by default, shown when no filter) */}
|
|
<div className="max-w-4xl mx-auto hidden" id="rfc-index">
|
|
<h1 className="text-3xl font-bold mb-4">All {config.name}</h1>
|
|
<p className="text-muted-foreground mb-8">
|
|
Browse all {stats.total} proposals organized by category. Use the sidebar to navigate
|
|
or press <kbd className="px-2 py-0.5 rounded bg-accent text-xs font-mono">Ctrl+K</kbd> to search.
|
|
</p>
|
|
|
|
{/* Quick Stats */}
|
|
<div className="grid grid-cols-4 gap-4 mb-12 p-4 rounded-lg border border-border bg-card">
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold">{stats.total}</div>
|
|
<div className="text-xs text-muted-foreground">Total</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-green-500">{stats.byStatus['Final'] || 0}</div>
|
|
<div className="text-xs text-muted-foreground">Final</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-blue-500">{stats.byStatus['Review'] || 0}</div>
|
|
<div className="text-xs text-muted-foreground">Review</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-yellow-500">{stats.byStatus['Draft'] || 0}</div>
|
|
<div className="text-xs text-muted-foreground">Draft</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Categories */}
|
|
{categories.map((cat) => (
|
|
<section key={cat.name} className="mb-12">
|
|
<Link
|
|
href={`/docs/category/${cat.slug}`}
|
|
className="flex items-center gap-3 mb-4 group w-fit"
|
|
>
|
|
<h2 className="text-xl font-semibold group-hover:text-primary transition-colors">{cat.name}</h2>
|
|
<span className="text-xs text-muted-foreground px-2 py-1 rounded-full bg-accent group-hover:bg-primary/10 transition-colors">
|
|
{cat.rfcs.length} proposals
|
|
</span>
|
|
<ArrowRight className="size-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
</Link>
|
|
<p className="text-sm text-muted-foreground mb-4">{cat.description}</p>
|
|
<div className="space-y-2">
|
|
{cat.rfcs.map((rfc) => {
|
|
const rfcNum = rfc.data.frontmatter[shortNameKey];
|
|
return (
|
|
<Link
|
|
key={rfc.slug.join('/')}
|
|
href={`/docs/${rfc.slug.join('/')}`}
|
|
className="flex items-center gap-4 p-3 rounded-lg border border-border hover:border-foreground/20 hover:bg-accent/50 transition-colors group"
|
|
>
|
|
<span className="text-sm font-mono text-muted-foreground w-20 shrink-0">
|
|
{shortName}-{String(rfcNum).padStart(4, '0')}
|
|
</span>
|
|
<span className="flex-1 font-medium text-sm truncate group-hover:text-foreground">
|
|
{rfc.data.title}
|
|
</span>
|
|
{rfc.data.frontmatter.status && (
|
|
<span className={`text-xs px-2 py-0.5 rounded-full shrink-0 ${
|
|
rfc.data.frontmatter.status === 'Final' ? 'bg-green-500/10 text-green-500' :
|
|
rfc.data.frontmatter.status === 'Draft' ? 'bg-yellow-500/10 text-yellow-500' :
|
|
rfc.data.frontmatter.status === 'Review' ? 'bg-blue-500/10 text-blue-500' :
|
|
'bg-gray-500/10 text-gray-500'
|
|
}`}>
|
|
{rfc.data.frontmatter.status}
|
|
</span>
|
|
)}
|
|
<ArrowRight className="size-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
))}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Custom heading components with IDs for TOC linking
|
|
function createHeadingComponent(level: number) {
|
|
const HeadingComponent = ({ children, ...props }: any) => {
|
|
const text = typeof children === 'string' ? children : String(children);
|
|
const id = text
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9\s-]/g, '')
|
|
.replace(/\s+/g, '-')
|
|
.replace(/-+/g, '-');
|
|
|
|
const Tag = `h${level}` as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
return <Tag id={id} {...props}>{children}</Tag>;
|
|
};
|
|
HeadingComponent.displayName = `Heading${level}`;
|
|
return HeadingComponent;
|
|
}
|
|
|
|
// Custom code component that strips erroneous backticks from inline code
|
|
function CodeComponent({ children, className, ...props }: any) {
|
|
// Handle inline code (no className) - strip backticks if present
|
|
if (!className) {
|
|
let content = typeof children === 'string' ? children : String(children);
|
|
// Strip leading/trailing backticks that may appear due to GFM table parsing
|
|
content = content.replace(/^`+|`+$/g, '');
|
|
return <code {...props}>{content}</code>;
|
|
}
|
|
// Code blocks keep className for syntax highlighting
|
|
return <code className={className} {...props}>{children}</code>;
|
|
}
|
|
|
|
const markdownComponents = {
|
|
h2: createHeadingComponent(2),
|
|
h3: createHeadingComponent(3),
|
|
h4: createHeadingComponent(4),
|
|
code: CodeComponent,
|
|
};
|
|
|
|
// Author link component - parses "Name (@username)" format and links to GitHub
|
|
function AuthorLink({ author }: { author: string }) {
|
|
// Parse author string - formats: "Name (@username)", "@username", "Name"
|
|
const match = author.match(/@([a-zA-Z0-9_-]+)/);
|
|
const username = match ? match[1] : null;
|
|
|
|
if (username) {
|
|
// Extract display name (everything before the @username part)
|
|
const displayName = author.replace(`(@${username})`, '').replace(`@${username}`, '').trim();
|
|
const finalDisplay = displayName || `@${username}`;
|
|
|
|
return (
|
|
<a
|
|
href={`https://github.com/${username}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="font-medium hover:text-primary transition-colors"
|
|
>
|
|
{finalDisplay}
|
|
</a>
|
|
);
|
|
}
|
|
|
|
// No username found - link to repo org as fallback
|
|
const repoOrg = config.repoUrl.split('/').slice(-2, -1)[0] || 'github';
|
|
return (
|
|
<a
|
|
href={`https://github.com/${repoOrg}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="font-medium hover:text-primary transition-colors"
|
|
>
|
|
{author}
|
|
</a>
|
|
);
|
|
}
|
|
|
|
// Individual RFC Page Component
|
|
function RFCDetailPage({ page }: { page: any }) {
|
|
const { frontmatter } = page.data;
|
|
const tocItems = extractHeadings(page.data.content);
|
|
const rfcNum = frontmatter[shortNameKey];
|
|
|
|
return (
|
|
<DocsPage toc={tocItems}>
|
|
{/* Header */}
|
|
<div className="mb-6 pb-6 border-b border-border">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Link
|
|
href="/docs"
|
|
className="text-sm text-muted-foreground hover:text-foreground flex items-center gap-1"
|
|
>
|
|
<ArrowLeft className="size-3" />
|
|
All Proposals
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="flex items-start justify-between gap-4 mb-3">
|
|
<div>
|
|
<span className="text-xs font-mono text-muted-foreground">
|
|
{shortName}-{String(rfcNum).padStart(4, '0')}
|
|
</span>
|
|
<h1 className="text-2xl font-bold mt-1">{page.data.title}</h1>
|
|
</div>
|
|
{frontmatter.status && (
|
|
<span className={`text-xs px-2 py-1 rounded-full shrink-0 ${
|
|
frontmatter.status === 'Final' ? 'bg-green-500/10 text-green-500' :
|
|
frontmatter.status === 'Draft' ? 'bg-yellow-500/10 text-yellow-500' :
|
|
frontmatter.status === 'Review' ? 'bg-blue-500/10 text-blue-500' :
|
|
frontmatter.status === 'Last Call' ? 'bg-purple-500/10 text-purple-500' :
|
|
'bg-gray-500/10 text-gray-500'
|
|
}`}>
|
|
{frontmatter.status}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{page.data.description && (
|
|
<p className="text-sm text-muted-foreground">{page.data.description}</p>
|
|
)}
|
|
|
|
{/* Metadata Grid - Tighter */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-4 p-3 rounded-lg bg-card border border-border text-xs">
|
|
{frontmatter.type && (
|
|
<div>
|
|
<div className="text-muted-foreground mb-0.5">Type</div>
|
|
<Link
|
|
href={`/docs?type=${encodeURIComponent(frontmatter.type)}`}
|
|
className="font-medium hover:text-primary transition-colors"
|
|
>
|
|
{frontmatter.type}
|
|
</Link>
|
|
</div>
|
|
)}
|
|
{frontmatter.category && (
|
|
<div>
|
|
<div className="text-muted-foreground mb-0.5">Category</div>
|
|
<Link
|
|
href={`/docs/category/${frontmatter.category.toLowerCase().replace(/\s+/g, '-')}`}
|
|
className="font-medium hover:text-primary transition-colors"
|
|
>
|
|
{frontmatter.category}
|
|
</Link>
|
|
</div>
|
|
)}
|
|
{frontmatter.author && (
|
|
<div>
|
|
<div className="text-muted-foreground mb-0.5 flex items-center gap-1">
|
|
<User className="size-3" /> Author
|
|
</div>
|
|
<AuthorLink author={frontmatter.author} />
|
|
</div>
|
|
)}
|
|
{frontmatter.created && (
|
|
<div>
|
|
<div className="text-muted-foreground mb-0.5 flex items-center gap-1">
|
|
<Calendar className="size-3" /> Created
|
|
</div>
|
|
<div className="font-medium">{frontmatter.created}</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Tags */}
|
|
{frontmatter.tags && frontmatter.tags.length > 0 && (
|
|
<div className="flex items-center gap-2 mt-3">
|
|
<Tag className="size-3 text-muted-foreground" />
|
|
<div className="flex flex-wrap gap-1">
|
|
{frontmatter.tags.map((tag: string) => (
|
|
<Link
|
|
key={tag}
|
|
href={`/docs?tag=${encodeURIComponent(tag)}`}
|
|
className="text-xs px-2 py-0.5 rounded-full bg-accent text-muted-foreground hover:bg-primary/10 hover:text-primary transition-colors"
|
|
>
|
|
{tag}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* External Links */}
|
|
{frontmatter['discussions-to'] && (
|
|
<a
|
|
href={frontmatter['discussions-to']}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-1 mt-3 text-xs text-muted-foreground hover:text-foreground"
|
|
>
|
|
<ExternalLink className="size-3" />
|
|
Join Discussion
|
|
</a>
|
|
)}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<DocsBody>
|
|
<ReactMarkdown
|
|
remarkPlugins={[remarkGfm]}
|
|
components={markdownComponents}
|
|
>
|
|
{page.data.content}
|
|
</ReactMarkdown>
|
|
</DocsBody>
|
|
</DocsPage>
|
|
);
|
|
}
|
|
|
|
export default async function Page({
|
|
params,
|
|
}: {
|
|
params: Promise<{ slug?: string[] }>;
|
|
}) {
|
|
const { slug } = await params;
|
|
|
|
// Show index page if no slug
|
|
if (!slug || slug.length === 0) {
|
|
return <RFCIndexPage />;
|
|
}
|
|
|
|
const page = source.getPage(slug);
|
|
if (!page) notFound();
|
|
|
|
return <RFCDetailPage page={page} />;
|
|
}
|
|
|
|
export async function generateStaticParams() {
|
|
const params = source.generateParams();
|
|
// Add empty slug for index page
|
|
return [{ slug: [] }, ...params];
|
|
}
|
|
|
|
export async function generateMetadata({ params }: { params: Promise<{ slug?: string[] }> }) {
|
|
const { slug } = await params;
|
|
|
|
if (!slug || slug.length === 0) {
|
|
return {
|
|
title: 'All Proposals',
|
|
description: `Browse all ${config.name} - standards and improvements`,
|
|
};
|
|
}
|
|
|
|
const page = source.getPage(slug);
|
|
if (!page) return {};
|
|
|
|
const rfcNum = page.data.frontmatter[shortNameKey];
|
|
|
|
return {
|
|
title: `${shortName}-${rfcNum}: ${page.data.title}`,
|
|
description: page.data.description || `${config.name} ${rfcNum}`,
|
|
};
|
|
}
|