'use client'
import { useState, useEffect, useCallback, useMemo } from 'react'
import {
Search, BookOpen, AlertTriangle, Shield, Scale, Handshake,
Briefcase, MessageCircle, Building2, Database, ChevronRight,
ArrowLeft, ExternalLink, Tag, Clock, Globe, Gavel,
} from 'lucide-react'
import type { WikiCategory, WikiArticle, WikiSearchResult } from '@/lib/sdk/types'
// =============================================================================
// SIMPLE MARKDOWN RENDERER
// =============================================================================
function renderMarkdown(md: string): string {
let html = md
// Escape HTML
.replace(/&/g, '&')
.replace(//g, '>')
// Code blocks (``` ... ```)
html = html.replace(
/^```[\w]*\n([\s\S]*?)^```$/gm,
(_match, code: string) => `
${code.trimEnd()}`
)
// Tables (must be before other block elements)
html = html.replace(
/^(\|.+\|)\n(\|[\s:|-]+\|)\n((?:\|.+\|\n?)*)/gm,
(_match, header: string, _sep: string, body: string) => {
const ths = header.split('|').filter((c: string) => c.trim()).map((c: string) => `${c.trim()} | `).join('')
const rows = body.trim().split('\n').map((row: string) => {
const tds = row.split('|').filter((c: string) => c.trim()).map((c: string) => `${c.trim()} | `).join('')
return `${tds}
`
}).join('')
return ``
}
)
// Headers
html = html.replace(/^### (.+)$/gm, '$1
')
html = html.replace(/^## (.+)$/gm, '$1
')
// Bold
html = html.replace(/\*\*(.+?)\*\*/g, '$1')
// Unordered lists
html = html.replace(/^- (.+)$/gm, '$1')
html = html.replace(/((?:]*>.*<\/li>\n?)+)/g, '')
// Paragraphs (lines that aren't already HTML)
html = html.replace(/^(?!<[hultdp]|$)(.+)$/gm, '$1
')
return html
}
// =============================================================================
// ICON MAP
// =============================================================================
const ICON_MAP: Record> = {
Database,
Shield,
AlertTriangle,
Scale,
Handshake,
Briefcase,
MessageCircle,
Building2,
Clock,
Globe,
Gavel,
}
function CategoryIcon({ icon, className }: { icon: string; className?: string }) {
const Icon = ICON_MAP[icon] || BookOpen
return
}
// =============================================================================
// RELEVANCE BADGE
// =============================================================================
function RelevanceBadge({ relevance }: { relevance: string }) {
const config = {
critical: { bg: 'bg-red-100 text-red-800', label: 'Kritisch' },
important: { bg: 'bg-amber-100 text-amber-800', label: 'Wichtig' },
info: { bg: 'bg-blue-100 text-blue-800', label: 'Info' },
}[relevance] || { bg: 'bg-gray-100 text-gray-600', label: relevance }
return (
{config.label}
)
}
// =============================================================================
// WIKI PAGE
// =============================================================================
export default function WikiPage() {
const [categories, setCategories] = useState([])
const [articles, setArticles] = useState([])
const [selectedCategory, setSelectedCategory] = useState(null)
const [selectedArticle, setSelectedArticle] = useState(null)
const [searchQuery, setSearchQuery] = useState('')
const [searchResults, setSearchResults] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
// Load categories on mount
useEffect(() => {
async function load() {
try {
const res = await fetch('/api/sdk/v1/wiki?endpoint=categories')
if (!res.ok) throw new Error('Failed to load categories')
const data = await res.json()
const cats: WikiCategory[] = (data.categories || []).map((c: Record) => ({
id: c.id,
name: c.name,
description: c.description || '',
icon: c.icon || '',
sortOrder: c.sort_order ?? 0,
articleCount: c.article_count ?? 0,
}))
setCategories(cats)
// Load all articles
const artRes = await fetch('/api/sdk/v1/wiki?endpoint=articles')
if (artRes.ok) {
const artData = await artRes.json()
setArticles((artData.articles || []).map((a: Record) => ({
id: a.id,
categoryId: a.category_id,
categoryName: a.category_name,
title: a.title,
summary: a.summary,
content: a.content,
legalRefs: a.legal_refs || [],
tags: a.tags || [],
relevance: a.relevance || 'info',
sourceUrls: a.source_urls || [],
version: a.version || 1,
updatedAt: a.updated_at || '',
})))
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Fehler beim Laden')
} finally {
setLoading(false)
}
}
load()
}, [])
// Search handler
const handleSearch = useCallback(async (query: string) => {
setSearchQuery(query)
if (query.length < 2) {
setSearchResults(null)
return
}
try {
const res = await fetch(`/api/sdk/v1/wiki?endpoint=search&q=${encodeURIComponent(query)}`)
if (res.ok) {
const data = await res.json()
setSearchResults((data.results || []).map((r: Record) => ({
id: r.id,
title: r.title,
summary: r.summary,
categoryName: r.category_name,
relevance: r.relevance || 'info',
highlight: r.highlight || '',
})))
}
} catch {
// silently fail search
}
}, [])
// Debounced search
useEffect(() => {
const timer = setTimeout(() => {
if (searchQuery.length >= 2) handleSearch(searchQuery)
}, 300)
return () => clearTimeout(timer)
}, [searchQuery, handleSearch])
// Filtered articles for selected category
const filteredArticles = useMemo(() => {
if (!selectedCategory) return articles
return articles.filter(a => a.categoryId === selectedCategory)
}, [articles, selectedCategory])
// Select article from search result
const selectFromSearch = (id: string) => {
const article = articles.find(a => a.id === id)
if (article) {
setSelectedArticle(article)
setSelectedCategory(article.categoryId)
setSearchResults(null)
setSearchQuery('')
}
}
if (loading) {
return (
)
}
if (error) {
return (
)
}
return (
{/* Header */}
Compliance Wiki
Interne Wissensbasis — {articles.length} Artikel in {categories.length} Kategorien
{/* Search */}
setSearchQuery(e.target.value)}
className="w-full pl-9 pr-4 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
{/* Search results dropdown */}
{searchResults && searchResults.length > 0 && (
{searchResults.map(r => (
))}
)}
{searchResults && searchResults.length === 0 && searchQuery.length >= 2 && (
Keine Ergebnisse fuer "{searchQuery}"
)}
{/* Content: Two columns */}
{/* Left: Categories */}
{categories.map(cat => (
))}
{/* Right: Article list or detail */}
{selectedArticle ? (
/* Article detail view */
{selectedArticle.categoryName} · v{selectedArticle.version}
{selectedArticle.title}
{selectedArticle.summary}
{/* Content (rendered markdown) */}
{/* Legal References */}
{selectedArticle.legalRefs.length > 0 && (
Rechtsreferenzen
{selectedArticle.legalRefs.map(ref => (
{ref}
))}
)}
{/* Tags */}
{selectedArticle.tags.length > 0 && (
Tags
{selectedArticle.tags.map(tag => (
{tag}
))}
)}
{/* Source URLs */}
{selectedArticle.sourceUrls.length > 0 && (
Quellen
{selectedArticle.sourceUrls.map(url => (
{url.startsWith('http') ? (
{url}
) : (
{url}
)}
))}
)}
) : (
/* Article list view */
{selectedCategory
? categories.find(c => c.id === selectedCategory)?.name || 'Artikel'
: 'Alle Artikel'
}
({filteredArticles.length})
{selectedCategory && (
{categories.find(c => c.id === selectedCategory)?.description}
)}
{filteredArticles.map(article => (
))}
{filteredArticles.length === 0 && (
Keine Artikel in dieser Kategorie.
)}
)}
)
}