'use client'; /** * LegalDocumentPage — unified UI for privacy-policy, terms-of-service, * and any other markdown-backed legal document. * * Replaces two near-identical hub components (`PrivacyPolicyPage` + * `TermsOfServicePage`) that differed only in title, contact email, * and copy strings. Caller passes those as props. * * Markdown rendering: defaults to lib's `SimpleMarkdownRenderer` * (sufficient for plain-markdown legal docs). Embedders that need * richer markdown (embeds, video, OG previews) pass their own via * the `MarkdownRenderer` prop — same injection pattern as * `ReleaseDetailPage`. * * Endpoint configuration: forwarded to `useLegalDocs(docType, { apiEndpoint })`. */ import type { ComponentType } from 'react'; import { PageShell, PageLayout } from '../../ui'; import { RichMarkdownRenderer } from '../../ui/markdown'; import { useRouter } from '../../../embed-shims/next-navigation'; import { useLegalDocs, type LegalDocument } from './use-legal-docs'; import { formatLegalDate } from '../../../utils/format'; export interface LegalDocumentMarkdownRendererProps { content: string; sectionIds?: Array<{ id: string; title: string; level: number }>; demoteMarkdownH1ToH2?: boolean; } export interface LegalDocumentPageProps { /** Document type identifier — drives the default API endpoint * `/api/legal/` AND the error-log prefix. Common values: * `'privacy'`, `'terms'`. Embedders may use any string. */ docType: string; /** Heading text (e.g. "Privacy Policy", "Terms of Service"). */ title: string; /** Fallback subtitle shown when no `lastUpdated` date is available * (e.g. "Our privacy policy and data protection practices"). */ fallbackDescription: string; /** Email shown in the error + empty-state copy * (e.g. `'privacy@openframe.io'`, `'legal@openframe.io'`). */ contactEmail: string; /** Prompt shown above the contact link in the error state * (e.g. "For privacy-related questions, please contact:"). */ errorContactPrompt: string; /** Title for the error block (e.g. "Unable to load privacy policy"). */ errorTitle: string; /** Sentence shown when the API returns no document * (e.g. "Privacy policy content is not available at this time."). */ emptyStateMessage: string; /** SSR-prepared document, if available. */ initialData?: LegalDocument | null; /** SSR-prepared formatted "Last Updated" label. Stable across hydration. */ initialLastUpdatedLabel?: string | null; /** Override the default `/api/legal/` endpoint * (reverse-proxy embedders, alternate API paths). */ apiEndpoint?: string; /** Override the default markdown renderer. */ MarkdownRenderer?: ComponentType; /** Back-button config — same pattern as `DevSectionPage`. Pass `false` * to hide. Default `{ label: 'Back to home', href: '/' }`. */ backButton?: { label?: string; href?: string } | false; /** Render the standalone ``. Default true. Pass false when the host * layout already provides the page container — only the padding box renders, * avoiding a nested `
`. */ shell?: boolean; } export function LegalDocumentPage({ docType, title, fallbackDescription, contactEmail, errorContactPrompt, errorTitle, emptyStateMessage, initialData = null, initialLastUpdatedLabel = null, apiEndpoint, MarkdownRenderer = RichMarkdownRenderer, backButton, shell = true, }: LegalDocumentPageProps) { const router = useRouter(); const { data, isLoading, error } = useLegalDocs(docType, { initialData, apiEndpoint }); // Back-button config — mirrors DevSectionPage's `{ label: 'Back to home', // onClick: () => router.push('/') }`. Hide entirely when caller passes // `false` (e.g. embed-mode where the host owns navigation chrome). const backCfg = backButton === false ? undefined : { label: backButton?.label ?? 'Back to home', onClick: () => router.push(backButton?.href ?? '/'), }; const fallbackLastUpdatedLabel = data?.lastSynced != null ? formatLegalDate(data.lastSynced) : null; const effectiveLastUpdatedLabel = initialLastUpdatedLabel ?? fallbackLastUpdatedLabel; // Subtitle routes through the frozen `PageLayout` `TitleBlock` (text-h2 title // + subtitle) — unified header across all help-center pages. Shows the // last-updated date when known, else the fallback description. const subtitle = effectiveLastUpdatedLabel ? `Last Updated: ${effectiveLastUpdatedLabel}` : fallbackDescription; const inner = ( {data?.sourceFile && (

Source: {data.sourceFile}

)}
{isLoading ? ( // Loading skeleton matching Knowledge Hub pattern
) : error ? (

{errorTitle}

{error}

{errorContactPrompt}

{contactEmail}
) : data ? ( ) : (

{emptyStateMessage}

Please contact{' '} {contactEmail} {' '} for more information.

)}
); return shell ? {inner} :
{inner}
; }