refactor(admin): split vvt page.tsx into colocated components
Split the 1371-line VVT page into _components/ extractions (FormPrimitives, api, TabVerzeichnis, TabEditor, TabExport) to bring page.tsx under the 300 LOC soft target. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
96
admin-compliance/app/sdk/vvt/_components/FormPrimitives.tsx
Normal file
96
admin-compliance/app/sdk/vvt/_components/FormPrimitives.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
export function FormSection({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="p-4 space-y-3">
|
||||
<h4 className="font-medium text-gray-800 text-sm">{title}</h4>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function FormField({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">{label}</label>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function MultiTextInput({ values, onChange, placeholder }: { values: string[]; onChange: (v: string[]) => void; placeholder?: string }) {
|
||||
const [input, setInput] = useState('')
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && input.trim()) {
|
||||
e.preventDefault()
|
||||
onChange([...values, input.trim()])
|
||||
setInput('')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{values.map((v, i) => (
|
||||
<span key={i} className="flex items-center gap-1 px-2 py-1 bg-purple-100 text-purple-700 rounded text-sm">
|
||||
{v}
|
||||
<button onClick={() => onChange(values.filter((_, j) => j !== i))} className="text-purple-400 hover:text-purple-600">
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function CheckboxGrid({ options, selected, onChange }: {
|
||||
options: { value: string; label: string; highlight?: boolean }[]
|
||||
selected: string[]
|
||||
onChange: (v: string[]) => void
|
||||
}) {
|
||||
const toggle = (value: string) => {
|
||||
if (selected.includes(value)) {
|
||||
onChange(selected.filter(v => v !== value))
|
||||
} else {
|
||||
onChange([...selected, value])
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-1.5">
|
||||
{options.map(opt => (
|
||||
<label
|
||||
key={opt.value}
|
||||
className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer text-sm transition-colors ${
|
||||
selected.includes(opt.value)
|
||||
? opt.highlight ? 'bg-red-50 border border-red-300' : 'bg-purple-50 border border-purple-300'
|
||||
: opt.highlight ? 'bg-red-50/30 border border-gray-200' : 'border border-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.includes(opt.value)}
|
||||
onChange={() => toggle(opt.value)}
|
||||
className="w-3.5 h-3.5 text-purple-600 rounded"
|
||||
/>
|
||||
<span className={opt.highlight ? 'text-red-700' : 'text-gray-700'}>{opt.label}</span>
|
||||
{opt.highlight && <span className="text-xs text-red-400">Art.9</span>}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user