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>
101 lines
2.2 KiB
TypeScript
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) }}
|
|
/>
|
|
)
|
|
}
|