- app/(app)/products/[id]/docs/[folder]/[slug]/page.tsx: server-route
die doc laadt (scope-checked via productAccessFilter), frontmatter
parseert, en op basis van ?edit=1 viewer of editor toont. Fallback
voor unparseable frontmatter toont errors + raw content in <pre>.
- product-doc-viewer.tsx: server-component met frontmatter-kop
(title + status-badge + audience/applies_to/last_updated meta) en
body via <Markdown> (XSS-safe).
- product-doc-editor.tsx: client-wrapper rond MarkdownDocEditor met
parseProductDocMd validator + updateProductDocAction + cancelHref.
- delete-product-doc-button.tsx: AlertDialog confirm + delete-action
+ DemoTooltip + redirect-na-success. Disabled in demo.
- Edit-knop conditioneel verborgen bij disabled folder (T-1071 voegt
banner toe); delete blijft altijd zichtbaar voor cleanup.
- Button met `render={<Link/>}` ipv asChild (CLAUDE.md hardstop
base-ui pattern).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
78 lines
2.3 KiB
TypeScript
78 lines
2.3 KiB
TypeScript
// Doc-viewer voor Product Docs. Server-component. Rendert een
|
|
// frontmatter-kop (title + status-badge + meta) gevolgd door de markdown-
|
|
// body via components/markdown.tsx (XSS-safe; iframe/script disabled).
|
|
//
|
|
// Frontmatter wordt door de pagina geparseerd en als losse props
|
|
// doorgegeven — voorkomt dat de viewer parsing-fouten moet afhandelen.
|
|
|
|
import { debugProps } from '@/lib/debug'
|
|
import { Markdown } from '@/components/markdown'
|
|
import { ProductDocStatusBadge } from './product-doc-status-badge'
|
|
|
|
interface Props {
|
|
title: string
|
|
status: string
|
|
body: string
|
|
audience?: string | string[] | undefined
|
|
applies_to?: string | string[] | undefined
|
|
lastUpdated?: string | undefined
|
|
}
|
|
|
|
function renderList(value: string | string[] | undefined): string | null {
|
|
if (!value) return null
|
|
return Array.isArray(value) ? value.join(', ') : value
|
|
}
|
|
|
|
export function ProductDocViewer({
|
|
title,
|
|
status,
|
|
body,
|
|
audience,
|
|
applies_to,
|
|
lastUpdated,
|
|
}: Props) {
|
|
const audienceLabel = renderList(audience)
|
|
const appliesToLabel = renderList(applies_to)
|
|
|
|
return (
|
|
<article
|
|
className="space-y-4"
|
|
{...debugProps(
|
|
'product-doc-viewer',
|
|
'ProductDocViewer',
|
|
'components/product-docs/product-doc-viewer.tsx',
|
|
)}
|
|
>
|
|
<header className="space-y-2 pb-3 border-b border-border">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<h1 className="text-xl font-semibold">{title}</h1>
|
|
<ProductDocStatusBadge status={status} />
|
|
</div>
|
|
{(audienceLabel || appliesToLabel || lastUpdated) && (
|
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
|
{audienceLabel && (
|
|
<span>
|
|
<span className="uppercase tracking-wide">Audience:</span>{' '}
|
|
{audienceLabel}
|
|
</span>
|
|
)}
|
|
{appliesToLabel && (
|
|
<span>
|
|
<span className="uppercase tracking-wide">Applies to:</span>{' '}
|
|
{appliesToLabel}
|
|
</span>
|
|
)}
|
|
{lastUpdated && (
|
|
<span>
|
|
<span className="uppercase tracking-wide">Bijgewerkt:</span>{' '}
|
|
{lastUpdated}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</header>
|
|
|
|
<Markdown>{body}</Markdown>
|
|
</article>
|
|
)
|
|
}
|