AIUseCaseModuleEditor (698 LOC) → thin orchestrator (187) + constants (29) + barrel tabs (4) + tabs implementation split into SystemData (261), PurposeAct (149), RisksReview (219). DataPointCatalog (658 LOC) → main (291) + helpers (190) + CategoryGroup (124) + Row (108). ProjectSelector (656 LOC) → main (211) + CreateProjectDialog (169) + ProjectActionDialog (140) + ProjectCard (128). All files now under 300 LOC soft target and 500 LOC hard cap. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
212 lines
7.9 KiB
TypeScript
212 lines
7.9 KiB
TypeScript
'use client'
|
|
|
|
import React, { useEffect, useState } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { useSDK } from '@/lib/sdk'
|
|
import type { ProjectInfo } from '@/lib/sdk/types'
|
|
import { CreateProjectDialog, normalizeProject } from './CreateProjectDialog'
|
|
import { ProjectActionDialog } from './ProjectActionDialog'
|
|
import { ProjectCard } from './ProjectCard'
|
|
|
|
export function ProjectSelector() {
|
|
const router = useRouter()
|
|
const { listProjects, archiveProject, restoreProject, permanentlyDeleteProject } = useSDK()
|
|
const [projects, setProjects] = useState<ProjectInfo[]>([])
|
|
const [archivedProjects, setArchivedProjects] = useState<ProjectInfo[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
|
const [actionTarget, setActionTarget] = useState<ProjectInfo | null>(null)
|
|
const [isProcessing, setIsProcessing] = useState(false)
|
|
const [showArchived, setShowArchived] = useState(false)
|
|
|
|
useEffect(() => {
|
|
loadProjects()
|
|
}, [])
|
|
|
|
const loadProjects = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const result = await listProjects()
|
|
const all = result.map(normalizeProject)
|
|
setProjects(all.filter(p => p.status === 'active'))
|
|
setArchivedProjects(all.filter(p => p.status === 'archived'))
|
|
} catch (error) {
|
|
console.error('Failed to load projects:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleProjectClick = (project: ProjectInfo) => {
|
|
router.push(`/sdk?project=${project.id}`)
|
|
}
|
|
|
|
const handleProjectCreated = (project: ProjectInfo) => {
|
|
router.push(`/sdk?project=${project.id}`)
|
|
}
|
|
|
|
const handleArchive = async () => {
|
|
if (!actionTarget) return
|
|
setIsProcessing(true)
|
|
try {
|
|
await archiveProject(actionTarget.id)
|
|
setProjects(prev => prev.filter(p => p.id !== actionTarget.id))
|
|
setArchivedProjects(prev => [...prev, { ...actionTarget, status: 'archived' as const }])
|
|
setActionTarget(null)
|
|
} catch (error) {
|
|
console.error('Failed to archive project:', error)
|
|
} finally {
|
|
setIsProcessing(false)
|
|
}
|
|
}
|
|
|
|
const handlePermanentDelete = async () => {
|
|
if (!actionTarget) return
|
|
setIsProcessing(true)
|
|
try {
|
|
await permanentlyDeleteProject(actionTarget.id)
|
|
setProjects(prev => prev.filter(p => p.id !== actionTarget.id))
|
|
setArchivedProjects(prev => prev.filter(p => p.id !== actionTarget.id))
|
|
setActionTarget(null)
|
|
} catch (error) {
|
|
console.error('Failed to delete project:', error)
|
|
} finally {
|
|
setIsProcessing(false)
|
|
}
|
|
}
|
|
|
|
const handleRestore = async (project: ProjectInfo) => {
|
|
try {
|
|
await restoreProject(project.id)
|
|
setArchivedProjects(prev => prev.filter(p => p.id !== project.id))
|
|
setProjects(prev => [...prev, { ...project, status: 'active' as const }])
|
|
} catch (error) {
|
|
console.error('Failed to restore project:', error)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-4xl mx-auto py-12 px-4">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-8">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Ihre Projekte</h1>
|
|
<p className="mt-1 text-gray-500">
|
|
Waehlen Sie ein Compliance-Projekt oder erstellen Sie ein neues.
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setShowCreateDialog(true)}
|
|
className="flex items-center gap-2 px-4 py-2.5 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium text-sm"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
Neues Projekt
|
|
</button>
|
|
</div>
|
|
|
|
{/* Loading */}
|
|
{loading && (
|
|
<div className="flex items-center justify-center py-16">
|
|
<div className="w-8 h-8 border-4 border-purple-200 border-t-purple-600 rounded-full animate-spin" />
|
|
</div>
|
|
)}
|
|
|
|
{/* Empty State */}
|
|
{!loading && projects.length === 0 && archivedProjects.length === 0 && (
|
|
<div className="text-center py-16">
|
|
<div className="w-16 h-16 mx-auto mb-4 bg-purple-100 rounded-2xl flex items-center justify-center">
|
|
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
</div>
|
|
<h2 className="text-lg font-semibold text-gray-900">Noch keine Projekte</h2>
|
|
<p className="mt-2 text-gray-500 max-w-md mx-auto">
|
|
Erstellen Sie Ihr erstes Compliance-Projekt, um mit der DSGVO- und AI-Act-Konformitaet zu beginnen.
|
|
</p>
|
|
<button
|
|
onClick={() => setShowCreateDialog(true)}
|
|
className="mt-6 inline-flex items-center gap-2 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
Erstes Projekt erstellen
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Active Projects */}
|
|
{!loading && projects.length > 0 && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{projects.map(project => (
|
|
<ProjectCard
|
|
key={project.id}
|
|
project={project}
|
|
onClick={() => handleProjectClick(project)}
|
|
onDelete={() => setActionTarget(project)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Archived Projects Section */}
|
|
{!loading && archivedProjects.length > 0 && (
|
|
<div className="mt-8">
|
|
<button
|
|
onClick={() => setShowArchived(!showArchived)}
|
|
className="flex items-center gap-2 text-sm text-gray-500 hover:text-gray-700 transition-colors mb-4"
|
|
>
|
|
<svg
|
|
className={`w-4 h-4 transition-transform ${showArchived ? 'rotate-90' : ''}`}
|
|
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
|
</svg>
|
|
Archivierte Projekte ({archivedProjects.length})
|
|
</button>
|
|
|
|
{showArchived && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{archivedProjects.map(project => (
|
|
<ProjectCard
|
|
key={project.id}
|
|
project={project}
|
|
onClick={() => handleProjectClick(project)}
|
|
onRestore={() => handleRestore(project)}
|
|
onDelete={() => setActionTarget(project)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Create Dialog */}
|
|
<CreateProjectDialog
|
|
open={showCreateDialog}
|
|
onClose={() => setShowCreateDialog(false)}
|
|
onCreated={handleProjectCreated}
|
|
existingProjects={projects}
|
|
/>
|
|
|
|
{/* Action Dialog (Archive / Delete) */}
|
|
{actionTarget && (
|
|
<ProjectActionDialog
|
|
project={actionTarget}
|
|
onArchive={handleArchive}
|
|
onPermanentDelete={handlePermanentDelete}
|
|
onCancel={() => setActionTarget(null)}
|
|
isProcessing={isProcessing}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|