fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
484
studio-v2/app/worksheet-editor/page.tsx
Normal file
484
studio-v2/app/worksheet-editor/page.tsx
Normal file
@@ -0,0 +1,484 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useTheme } from '@/lib/ThemeContext'
|
||||
import { useLanguage } from '@/lib/LanguageContext'
|
||||
import { WorksheetProvider, useWorksheet } from '@/lib/worksheet-editor/WorksheetContext'
|
||||
import { Sidebar } from '@/components/Sidebar'
|
||||
import { EditorToolbar } from '@/components/worksheet-editor/EditorToolbar'
|
||||
import { PropertiesPanel } from '@/components/worksheet-editor/PropertiesPanel'
|
||||
import { CanvasControls } from '@/components/worksheet-editor/CanvasControls'
|
||||
import { PageNavigator } from '@/components/worksheet-editor/PageNavigator'
|
||||
import { AIImageGenerator } from '@/components/worksheet-editor/AIImageGenerator'
|
||||
import { ExportPanel } from '@/components/worksheet-editor/ExportPanel'
|
||||
import { AIPromptBar } from '@/components/worksheet-editor/AIPromptBar'
|
||||
import { DocumentImporter } from '@/components/worksheet-editor/DocumentImporter'
|
||||
import { CleanupPanel } from '@/components/worksheet-editor/CleanupPanel'
|
||||
import { ThemeToggle } from '@/components/ThemeToggle'
|
||||
import { LanguageDropdown } from '@/components/LanguageDropdown'
|
||||
|
||||
// Dynamic import to prevent SSR issues with Fabric.js
|
||||
const FabricCanvas = dynamic(
|
||||
() => import('@/components/worksheet-editor/FabricCanvas').then(mod => mod.FabricCanvas),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="animate-spin w-8 h-8 border-4 border-purple-500 border-t-transparent rounded-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// Storage key for saved worksheets
|
||||
const WORKSHEETS_KEY = 'bp_worksheets'
|
||||
|
||||
interface SavedWorksheet {
|
||||
id: string
|
||||
title: string
|
||||
updatedAt: string
|
||||
thumbnail?: string
|
||||
}
|
||||
|
||||
function WorksheetEditorContent() {
|
||||
const { isDark } = useTheme()
|
||||
const { t } = useLanguage()
|
||||
const { document, setDocument, isDirty, setIsDirty, saveDocument, loadDocument, canvas } = useWorksheet()
|
||||
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [isAIGeneratorOpen, setIsAIGeneratorOpen] = useState(false)
|
||||
const [isExportPanelOpen, setIsExportPanelOpen] = useState(false)
|
||||
const [isDocumentImporterOpen, setIsDocumentImporterOpen] = useState(false)
|
||||
const [isCleanupPanelOpen, setIsCleanupPanelOpen] = useState(false)
|
||||
const [isDocumentListOpen, setIsDocumentListOpen] = useState(false)
|
||||
const [savedWorksheets, setSavedWorksheets] = useState<SavedWorksheet[]>([])
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [title, setTitle] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
loadSavedWorksheets()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (document) {
|
||||
setTitle(document.title)
|
||||
}
|
||||
}, [document])
|
||||
|
||||
// Load saved worksheets from localStorage
|
||||
const loadSavedWorksheets = useCallback(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem(WORKSHEETS_KEY)
|
||||
if (stored) {
|
||||
setSavedWorksheets(JSON.parse(stored))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load worksheets:', e)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Save current worksheet
|
||||
const handleSave = useCallback(async () => {
|
||||
if (!document) return
|
||||
setIsSaving(true)
|
||||
|
||||
try {
|
||||
// Save to context (which saves to API or localStorage)
|
||||
await saveDocument()
|
||||
|
||||
// Update worksheets list
|
||||
const worksheetEntry: SavedWorksheet = {
|
||||
id: document.id,
|
||||
title: document.title,
|
||||
updatedAt: new Date().toISOString(),
|
||||
thumbnail: canvas?.toDataURL({ format: 'png', multiplier: 0.1 })
|
||||
}
|
||||
|
||||
setSavedWorksheets(prev => {
|
||||
const filtered = prev.filter(w => w.id !== document.id)
|
||||
const updated = [worksheetEntry, ...filtered]
|
||||
localStorage.setItem(WORKSHEETS_KEY, JSON.stringify(updated))
|
||||
return updated
|
||||
})
|
||||
|
||||
setIsDirty(false)
|
||||
} catch (e) {
|
||||
console.error('Save failed:', e)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}, [document, saveDocument, canvas, setIsDirty])
|
||||
|
||||
// Load a saved worksheet
|
||||
const handleLoadWorksheet = useCallback(async (id: string) => {
|
||||
try {
|
||||
await loadDocument(id)
|
||||
setIsDocumentListOpen(false)
|
||||
} catch (e) {
|
||||
console.error('Failed to load worksheet:', e)
|
||||
}
|
||||
}, [loadDocument])
|
||||
|
||||
// Delete a saved worksheet
|
||||
const handleDeleteWorksheet = useCallback((id: string) => {
|
||||
setSavedWorksheets(prev => {
|
||||
const updated = prev.filter(w => w.id !== id)
|
||||
localStorage.setItem(WORKSHEETS_KEY, JSON.stringify(updated))
|
||||
localStorage.removeItem(`worksheet_${id}`)
|
||||
return updated
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Create new worksheet
|
||||
const handleNewWorksheet = useCallback(() => {
|
||||
const newDoc = {
|
||||
id: `ws_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
title: 'Neues Arbeitsblatt',
|
||||
pages: [{
|
||||
id: `page_${Date.now()}`,
|
||||
index: 0,
|
||||
canvasJSON: ''
|
||||
}],
|
||||
pageFormat: {
|
||||
width: 210,
|
||||
height: 297,
|
||||
orientation: 'portrait' as const,
|
||||
margins: { top: 15, right: 15, bottom: 15, left: 15 }
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
setDocument(newDoc)
|
||||
setIsDocumentListOpen(false)
|
||||
if (canvas) {
|
||||
canvas.clear()
|
||||
canvas.backgroundColor = '#ffffff'
|
||||
canvas.renderAll()
|
||||
}
|
||||
}, [setDocument, canvas])
|
||||
|
||||
const handleTitleChange = (newTitle: string) => {
|
||||
setTitle(newTitle)
|
||||
if (document) {
|
||||
setDocument({
|
||||
...document,
|
||||
title: newTitle,
|
||||
updatedAt: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className={`min-h-screen flex items-center justify-center ${
|
||||
isDark ? 'bg-slate-900' : 'bg-slate-100'
|
||||
}`}>
|
||||
<div className="animate-spin w-8 h-8 border-4 border-purple-500 border-t-transparent rounded-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`min-h-screen flex relative overflow-hidden ${
|
||||
isDark
|
||||
? 'bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800'
|
||||
: 'bg-gradient-to-br from-slate-100 via-blue-50 to-indigo-100'
|
||||
}`}>
|
||||
{/* Animated Background Blobs */}
|
||||
<div className={`absolute -top-40 -right-40 w-96 h-96 rounded-full mix-blend-multiply filter blur-3xl animate-blob ${
|
||||
isDark ? 'bg-purple-500 opacity-30' : 'bg-purple-300 opacity-40'
|
||||
}`} />
|
||||
<div className={`absolute top-1/2 -left-40 w-96 h-96 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-2000 ${
|
||||
isDark ? 'bg-pink-500 opacity-30' : 'bg-pink-300 opacity-40'
|
||||
}`} />
|
||||
<div className={`absolute -bottom-40 right-1/3 w-96 h-96 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-4000 ${
|
||||
isDark ? 'bg-blue-500 opacity-30' : 'bg-blue-300 opacity-40'
|
||||
}`} />
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="relative z-10 p-4">
|
||||
<Sidebar />
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex flex-col relative z-10 p-4 overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div>
|
||||
<h1 className={`text-2xl font-bold mb-1 ${isDark ? 'text-white' : 'text-slate-900'}`}>Arbeitsblatt-Editor</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => handleTitleChange(e.target.value)}
|
||||
placeholder="Arbeitsblatt-Titel..."
|
||||
className={`text-sm px-3 py-1.5 rounded-lg border transition-all w-56 ${
|
||||
isDark
|
||||
? 'bg-white/10 border-white/20 text-white placeholder-white/40 focus:border-purple-400'
|
||||
: 'bg-white/50 border-slate-300 text-slate-900 placeholder-slate-400 focus:border-purple-500'
|
||||
}`}
|
||||
/>
|
||||
{isDirty && (
|
||||
<span className={`px-2 py-1 rounded-lg text-xs font-medium ${
|
||||
isDark ? 'bg-yellow-500/20 text-yellow-300' : 'bg-yellow-100 text-yellow-700'
|
||||
}`}>
|
||||
Ungespeichert
|
||||
</span>
|
||||
)}
|
||||
{/* Save Button */}
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || !isDirty}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${
|
||||
isDirty
|
||||
? 'bg-green-500 text-white hover:bg-green-600'
|
||||
: isDark
|
||||
? 'bg-white/10 text-white/40 cursor-not-allowed'
|
||||
: 'bg-slate-200 text-slate-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{isSaving ? (
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
|
||||
</svg>
|
||||
)}
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Document List Button */}
|
||||
<button
|
||||
onClick={() => setIsDocumentListOpen(true)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-xl font-medium transition-all ${
|
||||
isDark
|
||||
? 'bg-white/10 text-white hover:bg-white/20'
|
||||
: 'bg-white text-slate-700 hover:bg-slate-100'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-5 h-5" 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>
|
||||
Meine Arbeitsblätter
|
||||
</button>
|
||||
|
||||
{/* Export Button */}
|
||||
<button
|
||||
onClick={() => setIsExportPanelOpen(true)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-xl font-medium transition-all ${
|
||||
isDark
|
||||
? 'bg-purple-500/30 text-purple-300 hover:bg-purple-500/40'
|
||||
: 'bg-purple-600 text-white hover:bg-purple-700'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
Exportieren
|
||||
</button>
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<ThemeToggle />
|
||||
|
||||
{/* Language Dropdown */}
|
||||
<LanguageDropdown />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor Area - New Layout */}
|
||||
<div className="flex-1 flex gap-4 overflow-hidden">
|
||||
{/* Left Toolbar */}
|
||||
<div className="flex-shrink-0">
|
||||
<EditorToolbar
|
||||
onOpenAIGenerator={() => setIsAIGeneratorOpen(true)}
|
||||
onOpenDocumentImporter={() => setIsDocumentImporterOpen(true)}
|
||||
onOpenCleanupPanel={() => setIsCleanupPanelOpen(true)}
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Canvas Area - takes remaining space */}
|
||||
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||
{/* Canvas with fixed aspect ratio container */}
|
||||
<div className={`flex-1 overflow-auto rounded-xl ${
|
||||
isDark ? 'bg-slate-800/50' : 'bg-slate-200/50'
|
||||
}`}>
|
||||
<FabricCanvas className="h-full" />
|
||||
</div>
|
||||
|
||||
{/* Bottom Controls */}
|
||||
<div className="flex items-center justify-between gap-4 py-2">
|
||||
<PageNavigator />
|
||||
<CanvasControls />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Panel - AI Prompt + Properties */}
|
||||
<div className="w-80 flex-shrink-0 flex flex-col gap-4 overflow-hidden">
|
||||
{/* AI Prompt Bar */}
|
||||
<div className="flex-shrink-0">
|
||||
<AIPromptBar />
|
||||
</div>
|
||||
|
||||
{/* Properties Panel */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<PropertiesPanel className="h-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Document List Modal */}
|
||||
{isDocumentListOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={() => setIsDocumentListOpen(false)} />
|
||||
<div className={`relative w-full max-w-2xl rounded-3xl p-6 ${
|
||||
isDark ? 'bg-slate-900/95' : 'bg-white/95'
|
||||
} backdrop-blur-xl border ${isDark ? 'border-white/10' : 'border-slate-200'}`}>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className={`text-xl font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||||
Meine Arbeitsblätter
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setIsDocumentListOpen(false)}
|
||||
className={`p-2 rounded-lg transition-colors ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-100'}`}
|
||||
>
|
||||
<svg className={`w-5 h-5 ${isDark ? 'text-white' : 'text-slate-600'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* New Worksheet Button */}
|
||||
<button
|
||||
onClick={handleNewWorksheet}
|
||||
className={`w-full mb-4 p-4 rounded-xl border-2 border-dashed transition-all flex items-center justify-center gap-2 ${
|
||||
isDark
|
||||
? 'border-white/20 text-white/60 hover:border-purple-400 hover:text-purple-300 hover:bg-purple-500/10'
|
||||
: 'border-slate-300 text-slate-500 hover:border-purple-500 hover:text-purple-600 hover:bg-purple-50'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Neues Arbeitsblatt erstellen
|
||||
</button>
|
||||
|
||||
{/* Worksheets List */}
|
||||
<div className="max-h-96 overflow-y-auto space-y-2">
|
||||
{savedWorksheets.length === 0 ? (
|
||||
<div className={`text-center py-8 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
||||
<svg className="w-12 h-12 mx-auto mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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>
|
||||
<p>Noch keine Arbeitsblätter gespeichert</p>
|
||||
</div>
|
||||
) : (
|
||||
savedWorksheets.map((worksheet) => (
|
||||
<div
|
||||
key={worksheet.id}
|
||||
className={`flex items-center gap-4 p-4 rounded-xl transition-all cursor-pointer ${
|
||||
isDark
|
||||
? 'bg-white/5 hover:bg-white/10'
|
||||
: 'bg-slate-50 hover:bg-slate-100'
|
||||
} ${document?.id === worksheet.id ? (isDark ? 'ring-2 ring-purple-500' : 'ring-2 ring-purple-500') : ''}`}
|
||||
onClick={() => handleLoadWorksheet(worksheet.id)}
|
||||
>
|
||||
{/* Thumbnail */}
|
||||
<div className={`w-16 h-20 rounded-lg flex-shrink-0 overflow-hidden ${
|
||||
isDark ? 'bg-white/10' : 'bg-slate-200'
|
||||
}`}>
|
||||
{worksheet.thumbnail ? (
|
||||
<img src={worksheet.thumbnail} alt="" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<svg className={`w-6 h-6 ${isDark ? 'text-white/30' : 'text-slate-400'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className={`font-medium truncate ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||||
{worksheet.title}
|
||||
</h3>
|
||||
<p className={`text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
|
||||
{new Date(worksheet.updatedAt).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</p>
|
||||
{document?.id === worksheet.id && (
|
||||
<span className="inline-block mt-1 px-2 py-0.5 rounded text-xs bg-purple-500/20 text-purple-400">
|
||||
Aktuell geöffnet
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Delete Button */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (confirm('Arbeitsblatt wirklich löschen?')) {
|
||||
handleDeleteWorksheet(worksheet.id)
|
||||
}
|
||||
}}
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
isDark ? 'hover:bg-red-500/20 text-white/50 hover:text-red-400' : 'hover:bg-red-50 text-slate-400 hover:text-red-500'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modals */}
|
||||
<AIImageGenerator
|
||||
isOpen={isAIGeneratorOpen}
|
||||
onClose={() => setIsAIGeneratorOpen(false)}
|
||||
/>
|
||||
|
||||
<ExportPanel
|
||||
isOpen={isExportPanelOpen}
|
||||
onClose={() => setIsExportPanelOpen(false)}
|
||||
/>
|
||||
|
||||
<DocumentImporter
|
||||
isOpen={isDocumentImporterOpen}
|
||||
onClose={() => setIsDocumentImporterOpen(false)}
|
||||
/>
|
||||
|
||||
<CleanupPanel
|
||||
isOpen={isCleanupPanelOpen}
|
||||
onClose={() => setIsCleanupPanelOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function WorksheetEditorPage() {
|
||||
return (
|
||||
<WorksheetProvider>
|
||||
<WorksheetEditorContent />
|
||||
</WorksheetProvider>
|
||||
)
|
||||
}
|
||||
237
studio-v2/app/worksheet-editor/types.ts
Normal file
237
studio-v2/app/worksheet-editor/types.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* Worksheet Editor - TypeScript Interfaces
|
||||
*
|
||||
* Types for the visual worksheet editor using Fabric.js
|
||||
*/
|
||||
|
||||
import type { Canvas, Object as FabricObject } from 'fabric'
|
||||
|
||||
// Tool Types
|
||||
export type EditorTool =
|
||||
| 'select'
|
||||
| 'text'
|
||||
| 'rectangle'
|
||||
| 'circle'
|
||||
| 'line'
|
||||
| 'arrow'
|
||||
| 'image'
|
||||
| 'ai-image'
|
||||
| 'table'
|
||||
|
||||
// Text Alignment
|
||||
export type TextAlign = 'left' | 'center' | 'right' | 'justify'
|
||||
|
||||
// Font Weight
|
||||
export type FontWeight = 'normal' | 'bold'
|
||||
|
||||
// Font Style
|
||||
export type FontStyle = 'normal' | 'italic'
|
||||
|
||||
// Object Type
|
||||
export type WorksheetObjectType =
|
||||
| 'text'
|
||||
| 'image'
|
||||
| 'rectangle'
|
||||
| 'circle'
|
||||
| 'line'
|
||||
| 'arrow'
|
||||
| 'table'
|
||||
| 'ai-image'
|
||||
|
||||
// Base Object Properties
|
||||
export interface BaseObjectProps {
|
||||
id: string
|
||||
type: WorksheetObjectType
|
||||
left: number
|
||||
top: number
|
||||
width?: number
|
||||
height?: number
|
||||
angle: number
|
||||
opacity: number
|
||||
fill?: string
|
||||
stroke?: string
|
||||
strokeWidth?: number
|
||||
locked?: boolean
|
||||
}
|
||||
|
||||
// Text Object Properties
|
||||
export interface TextObjectProps extends BaseObjectProps {
|
||||
type: 'text'
|
||||
text: string
|
||||
fontFamily: string
|
||||
fontSize: number
|
||||
fontWeight: FontWeight
|
||||
fontStyle: FontStyle
|
||||
textAlign: TextAlign
|
||||
lineHeight: number
|
||||
charSpacing: number
|
||||
underline?: boolean
|
||||
linethrough?: boolean
|
||||
}
|
||||
|
||||
// Image Object Properties
|
||||
export interface ImageObjectProps extends BaseObjectProps {
|
||||
type: 'image' | 'ai-image'
|
||||
src: string
|
||||
originalWidth: number
|
||||
originalHeight: number
|
||||
cropX?: number
|
||||
cropY?: number
|
||||
cropWidth?: number
|
||||
cropHeight?: number
|
||||
}
|
||||
|
||||
// Shape Object Properties
|
||||
export interface ShapeObjectProps extends BaseObjectProps {
|
||||
type: 'rectangle' | 'circle' | 'line' | 'arrow'
|
||||
rx?: number // Corner radius for rectangles
|
||||
ry?: number
|
||||
}
|
||||
|
||||
// Table Object Properties
|
||||
export interface TableObjectProps extends BaseObjectProps {
|
||||
type: 'table'
|
||||
rows: number
|
||||
cols: number
|
||||
cellWidth: number
|
||||
cellHeight: number
|
||||
cellData: string[][]
|
||||
}
|
||||
|
||||
// Union type for all objects
|
||||
export type WorksheetObject =
|
||||
| TextObjectProps
|
||||
| ImageObjectProps
|
||||
| ShapeObjectProps
|
||||
| TableObjectProps
|
||||
|
||||
// Page
|
||||
export interface WorksheetPage {
|
||||
id: string
|
||||
index: number
|
||||
canvasJSON: string // Serialized Fabric.js canvas state
|
||||
thumbnail?: string
|
||||
}
|
||||
|
||||
// Worksheet Document
|
||||
export interface WorksheetDocument {
|
||||
id: string
|
||||
title: string
|
||||
description?: string
|
||||
pages: WorksheetPage[]
|
||||
pageFormat: PageFormat
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// Page Format
|
||||
export interface PageFormat {
|
||||
width: number // in mm
|
||||
height: number // in mm
|
||||
orientation: 'portrait' | 'landscape'
|
||||
margins: {
|
||||
top: number
|
||||
right: number
|
||||
bottom: number
|
||||
left: number
|
||||
}
|
||||
}
|
||||
|
||||
// Default A4 Format
|
||||
export const DEFAULT_PAGE_FORMAT: PageFormat = {
|
||||
width: 210,
|
||||
height: 297,
|
||||
orientation: 'portrait',
|
||||
margins: {
|
||||
top: 15,
|
||||
right: 15,
|
||||
bottom: 15,
|
||||
left: 15
|
||||
}
|
||||
}
|
||||
|
||||
// Canvas Scale (mm to pixels at 96 DPI)
|
||||
export const MM_TO_PX = 3.7795275591 // 1mm = 3.78px at 96 DPI
|
||||
|
||||
// AI Image Generation
|
||||
export interface AIImageRequest {
|
||||
prompt: string
|
||||
style: AIImageStyle
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export type AIImageStyle =
|
||||
| 'realistic'
|
||||
| 'cartoon'
|
||||
| 'sketch'
|
||||
| 'clipart'
|
||||
| 'educational'
|
||||
|
||||
export interface AIImageResponse {
|
||||
image_base64: string
|
||||
prompt_used: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
// Editor State
|
||||
export interface EditorState {
|
||||
activeTool: EditorTool
|
||||
activeObject: FabricObject | null
|
||||
selectedObjects: FabricObject[]
|
||||
zoom: number
|
||||
showGrid: boolean
|
||||
snapToGrid: boolean
|
||||
gridSize: number
|
||||
currentPageIndex: number
|
||||
}
|
||||
|
||||
// History Entry for Undo/Redo
|
||||
export interface HistoryEntry {
|
||||
canvasJSON: string
|
||||
timestamp: number
|
||||
action: string
|
||||
}
|
||||
|
||||
// Typography Presets
|
||||
export interface TypographyPreset {
|
||||
id: string
|
||||
name: string
|
||||
fontFamily: string
|
||||
fontSize: number
|
||||
fontWeight: FontWeight
|
||||
lineHeight: number
|
||||
}
|
||||
|
||||
// Default Typography Presets
|
||||
export const DEFAULT_TYPOGRAPHY_PRESETS: TypographyPreset[] = [
|
||||
{ id: 'h1', name: 'Überschrift 1', fontFamily: 'Arial', fontSize: 32, fontWeight: 'bold', lineHeight: 1.2 },
|
||||
{ id: 'h2', name: 'Überschrift 2', fontFamily: 'Arial', fontSize: 24, fontWeight: 'bold', lineHeight: 1.3 },
|
||||
{ id: 'h3', name: 'Überschrift 3', fontFamily: 'Arial', fontSize: 18, fontWeight: 'bold', lineHeight: 1.4 },
|
||||
{ id: 'body', name: 'Fließtext', fontFamily: 'Arial', fontSize: 12, fontWeight: 'normal', lineHeight: 1.5 },
|
||||
{ id: 'small', name: 'Klein', fontFamily: 'Arial', fontSize: 10, fontWeight: 'normal', lineHeight: 1.4 },
|
||||
{ id: 'caption', name: 'Bildunterschrift', fontFamily: 'Arial', fontSize: 9, fontWeight: 'normal', lineHeight: 1.3 },
|
||||
]
|
||||
|
||||
// Available Fonts
|
||||
export const AVAILABLE_FONTS = [
|
||||
{ name: 'Arial', family: 'Arial, sans-serif' },
|
||||
{ name: 'Times New Roman', family: 'Times New Roman, serif' },
|
||||
{ name: 'Georgia', family: 'Georgia, serif' },
|
||||
{ name: 'Verdana', family: 'Verdana, sans-serif' },
|
||||
{ name: 'Comic Sans MS', family: 'Comic Sans MS, cursive' },
|
||||
{ name: 'OpenDyslexic', family: 'OpenDyslexic, sans-serif' },
|
||||
{ name: 'Schulschrift', family: 'Schulschrift, cursive' },
|
||||
{ name: 'Courier New', family: 'Courier New, monospace' },
|
||||
]
|
||||
|
||||
// Export Format
|
||||
export type ExportFormat = 'pdf' | 'png' | 'jpg' | 'json'
|
||||
|
||||
// Export Options
|
||||
export interface ExportOptions {
|
||||
format: ExportFormat
|
||||
quality?: number // 0-1 for images
|
||||
includeBackground?: boolean
|
||||
scale?: number
|
||||
}
|
||||
Reference in New Issue
Block a user