This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Benjamin Admin bd70b59c5e fix(admin-v2): Restore HEAD SDK files for compatibility with new pages
Restore the SDK context, types, and component files to the HEAD version
since newer pages (company-profile, import) depend on these API changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 10:18:39 +01:00

465 lines
15 KiB
TypeScript

'use client'
import React, { useState, useEffect, useRef, useMemo } from 'react'
import { useRouter } from 'next/navigation'
import { useSDK, SDK_STEPS, CommandType, CommandHistory, downloadExport } from '@/lib/sdk'
// =============================================================================
// TYPES
// =============================================================================
interface Suggestion {
id: string
type: CommandType
label: string
description: string
shortcut?: string
icon: React.ReactNode
action: () => void | Promise<void>
}
// =============================================================================
// ICONS
// =============================================================================
const icons = {
navigation: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
),
action: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
),
search: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
),
generate: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
),
help: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
}
// =============================================================================
// COMMAND BAR
// =============================================================================
interface CommandBarProps {
onClose: () => void
}
export function CommandBar({ onClose }: CommandBarProps) {
const router = useRouter()
const { state, dispatch, goToStep } = useSDK()
const inputRef = useRef<HTMLInputElement>(null)
const [query, setQuery] = useState('')
const [selectedIndex, setSelectedIndex] = useState(0)
const [isLoading, setIsLoading] = useState(false)
// Focus input on mount
useEffect(() => {
inputRef.current?.focus()
}, [])
// Generate suggestions based on query
const suggestions = useMemo((): Suggestion[] => {
const results: Suggestion[] = []
const lowerQuery = query.toLowerCase()
// Navigation suggestions
SDK_STEPS.forEach(step => {
const matchesName = step.name.toLowerCase().includes(lowerQuery) ||
step.nameShort.toLowerCase().includes(lowerQuery)
const matchesDescription = step.description.toLowerCase().includes(lowerQuery)
if (!query || matchesName || matchesDescription) {
results.push({
id: `nav-${step.id}`,
type: 'NAVIGATION',
label: `Gehe zu ${step.name}`,
description: step.description,
icon: icons.navigation,
action: () => {
goToStep(step.id)
onClose()
},
})
}
})
// Action suggestions
const actions: Suggestion[] = [
{
id: 'action-new-usecase',
type: 'ACTION',
label: 'Neuen Anwendungsfall erstellen',
description: 'Startet die Anwendungsfall-Erfassung',
icon: icons.action,
action: () => {
goToStep('use-case-assessment')
onClose()
},
},
{
id: 'action-export-pdf',
type: 'ACTION',
label: 'Als PDF exportieren',
description: 'Exportiert Compliance-Bericht als PDF',
icon: icons.action,
action: async () => {
setIsLoading(true)
try {
await downloadExport(state, 'pdf')
} catch (error) {
console.error('PDF export failed:', error)
} finally {
setIsLoading(false)
onClose()
}
},
},
{
id: 'action-export-zip',
type: 'ACTION',
label: 'Als ZIP exportieren',
description: 'Exportiert alle Daten und Dokumente als ZIP-Archiv',
icon: icons.action,
action: async () => {
setIsLoading(true)
try {
await downloadExport(state, 'zip')
} catch (error) {
console.error('ZIP export failed:', error)
} finally {
setIsLoading(false)
onClose()
}
},
},
{
id: 'action-export-json',
type: 'ACTION',
label: 'Als JSON exportieren',
description: 'Exportiert den kompletten State als JSON',
icon: icons.action,
action: async () => {
try {
await downloadExport(state, 'json')
} catch (error) {
console.error('JSON export failed:', error)
} finally {
onClose()
}
},
},
{
id: 'action-validate',
type: 'ACTION',
label: 'Checkpoint validieren',
description: 'Validiert den aktuellen Schritt',
icon: icons.action,
action: () => {
// Trigger validation
onClose()
},
},
]
actions.forEach(action => {
if (!query || action.label.toLowerCase().includes(lowerQuery)) {
results.push(action)
}
})
// Generate suggestions
const generateSuggestions: Suggestion[] = [
{
id: 'gen-dsfa',
type: 'GENERATE',
label: 'DSFA generieren',
description: 'Generiert eine Datenschutz-Folgenabschätzung',
icon: icons.generate,
action: () => {
goToStep('dsfa')
onClose()
},
},
{
id: 'gen-tom',
type: 'GENERATE',
label: 'TOMs generieren',
description: 'Generiert technische und organisatorische Maßnahmen',
icon: icons.generate,
action: () => {
goToStep('tom')
onClose()
},
},
{
id: 'gen-vvt',
type: 'GENERATE',
label: 'VVT generieren',
description: 'Generiert das Verarbeitungsverzeichnis',
icon: icons.generate,
action: () => {
goToStep('vvt')
onClose()
},
},
{
id: 'gen-cookie',
type: 'GENERATE',
label: 'Cookie Banner generieren',
description: 'Generiert Cookie-Consent-Banner Code',
icon: icons.generate,
action: () => {
goToStep('cookie-banner')
onClose()
},
},
]
generateSuggestions.forEach(suggestion => {
if (!query || suggestion.label.toLowerCase().includes(lowerQuery)) {
results.push(suggestion)
}
})
// Help suggestions
const helpSuggestions: Suggestion[] = [
{
id: 'help-docs',
type: 'HELP',
label: 'Dokumentation öffnen',
description: 'Öffnet die SDK-Dokumentation',
icon: icons.help,
action: () => {
window.open('/docs/sdk', '_blank')
onClose()
},
},
{
id: 'help-next',
type: 'HELP',
label: 'Was muss ich als nächstes tun?',
description: 'Zeigt den nächsten empfohlenen Schritt',
icon: icons.help,
action: () => {
// Show contextual help
onClose()
},
},
]
helpSuggestions.forEach(suggestion => {
if (!query || suggestion.label.toLowerCase().includes(lowerQuery)) {
results.push(suggestion)
}
})
return results.slice(0, 10)
}, [query, state, goToStep, onClose])
// Keyboard navigation
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
setSelectedIndex(prev => Math.min(prev + 1, suggestions.length - 1))
break
case 'ArrowUp':
e.preventDefault()
setSelectedIndex(prev => Math.max(prev - 1, 0))
break
case 'Enter':
e.preventDefault()
if (suggestions[selectedIndex]) {
executeSuggestion(suggestions[selectedIndex])
}
break
case 'Escape':
onClose()
break
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [suggestions, selectedIndex, onClose])
// Reset selected index when query changes
useEffect(() => {
setSelectedIndex(0)
}, [query])
const executeSuggestion = async (suggestion: Suggestion) => {
// Add to history
const historyEntry: CommandHistory = {
id: `${Date.now()}`,
query: suggestion.label,
type: suggestion.type,
timestamp: new Date(),
success: true,
}
dispatch({ type: 'ADD_COMMAND_HISTORY', payload: historyEntry })
try {
await suggestion.action()
} catch (error) {
console.error('Command execution failed:', error)
}
}
const getTypeLabel = (type: CommandType): string => {
switch (type) {
case 'NAVIGATION':
return 'Navigation'
case 'ACTION':
return 'Aktion'
case 'SEARCH':
return 'Suche'
case 'GENERATE':
return 'Generieren'
case 'HELP':
return 'Hilfe'
default:
return type
}
}
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/50 transition-opacity"
onClick={onClose}
/>
{/* Dialog */}
<div className="relative min-h-screen flex items-start justify-center pt-[15vh] px-4">
<div className="relative w-full max-w-2xl bg-white rounded-2xl shadow-2xl overflow-hidden">
{/* Search Input */}
<div className="flex items-center gap-3 px-4 py-3 border-b border-gray-200">
<svg className="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<input
ref={inputRef}
type="text"
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Suchen oder Befehl eingeben..."
className="flex-1 text-lg bg-transparent border-none outline-none placeholder-gray-400"
/>
{isLoading && (
<svg className="animate-spin w-5 h-5 text-purple-600" fill="none" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
)}
<kbd className="px-2 py-1 text-xs text-gray-400 bg-gray-100 rounded">ESC</kbd>
</div>
{/* Suggestions */}
<div className="max-h-96 overflow-y-auto py-2">
{suggestions.length === 0 ? (
<div className="px-4 py-8 text-center text-gray-500">
Keine Ergebnisse für &quot;{query}&quot;
</div>
) : (
suggestions.map((suggestion, index) => (
<button
key={suggestion.id}
onClick={() => executeSuggestion(suggestion)}
className={`w-full flex items-center gap-3 px-4 py-3 text-left transition-colors ${
index === selectedIndex
? 'bg-purple-50 text-purple-900'
: 'text-gray-700 hover:bg-gray-50'
}`}
>
<div
className={`flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center ${
index === selectedIndex
? 'bg-purple-100 text-purple-600'
: 'bg-gray-100 text-gray-500'
}`}
>
{suggestion.icon}
</div>
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{suggestion.label}</div>
<div className="text-sm text-gray-500 truncate">{suggestion.description}</div>
</div>
<div className="flex-shrink-0 flex items-center gap-2">
<span
className={`px-2 py-0.5 text-xs rounded-full ${
suggestion.type === 'NAVIGATION'
? 'bg-blue-100 text-blue-700'
: suggestion.type === 'ACTION'
? 'bg-green-100 text-green-700'
: suggestion.type === 'GENERATE'
? 'bg-purple-100 text-purple-700'
: 'bg-gray-100 text-gray-700'
}`}
>
{getTypeLabel(suggestion.type)}
</span>
{suggestion.shortcut && (
<kbd className="px-1.5 py-0.5 text-xs bg-gray-100 rounded">
{suggestion.shortcut}
</kbd>
)}
</div>
</button>
))
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between px-4 py-2 border-t border-gray-200 bg-gray-50 text-xs text-gray-500">
<div className="flex items-center gap-4">
<span className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-gray-200 rounded"></kbd>
<kbd className="px-1.5 py-0.5 bg-gray-200 rounded"></kbd>
navigieren
</span>
<span className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-gray-200 rounded"></kbd>
auswählen
</span>
</div>
<span>AI Compliance SDK v1.0</span>
</div>
</div>
</div>
</div>
)
}