syncova-policies/components/markdown-renderer.tsx
Weapie 328f042307 Syncova Policies: Portal für DSGVO-Datenschutzerklärungen
Next.js 15 App Router mit öffentlicher Übersicht und geschütztem
Admin-Bereich zur Pflege von Datenschutzerklärungen nach Art. 12ff. DSGVO.

- Oberfläche auf Basis von shadcn/ui, Inter lokal eingebunden
- Prisma/SQLite, Auth.js mit Credentials-Provider und Rollen
- Massenimport der Merkblätter des Amtes Leezen aus PDF
- Docker-Image (standalone) und Gitea-Workflow zur Veröffentlichung

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 16:23:51 +02:00

101 lines
2.2 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import { Skeleton } from "@/components/ui/skeleton"
import { cn } from "@/lib/utils"
import { markdownToHtml, markdownToHtmlSync } from "@/lib/markdown"
interface MarkdownRendererProps {
content: string
className?: string
inline?: boolean
}
export function MarkdownRenderer({
content,
className,
inline = false,
}: MarkdownRendererProps) {
const [htmlContent, setHtmlContent] = useState<string>("")
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
let cancelled = false
async function processMarkdown() {
setIsLoading(true)
try {
const html = await markdownToHtml(content)
if (!cancelled) setHtmlContent(html)
} catch (error) {
console.error("Error rendering markdown:", error)
if (!cancelled) setHtmlContent(content)
} finally {
if (!cancelled) setIsLoading(false)
}
}
if (content) {
processMarkdown()
} else {
setHtmlContent("")
setIsLoading(false)
}
return () => {
cancelled = true
}
}, [content])
if (isLoading) {
return (
<div className={cn("space-y-2", className)}>
<Skeleton className="h-4 w-4/5" />
<Skeleton className="h-4 w-3/5" />
</div>
)
}
if (!htmlContent) {
return (
<p className={cn("text-sm text-muted-foreground italic", className)}>
Kein Inhalt verfügbar
</p>
)
}
const Component = inline ? "span" : "div"
return (
<Component
className={cn("markdown-content", className)}
dangerouslySetInnerHTML={{ __html: htmlContent }}
/>
)
}
// Synchronous version for cases where we can't use hooks
export function StaticMarkdownRenderer({
content,
className,
inline = false,
}: MarkdownRendererProps) {
if (!content) {
return (
<p className={cn("text-sm text-muted-foreground italic", className)}>
Kein Inhalt verfügbar
</p>
)
}
const Component = inline ? "span" : "div"
return (
<Component
className={cn("markdown-content", className)}
dangerouslySetInnerHTML={{ __html: markdownToHtmlSync(content) }}
/>
)
}