'use client' import React, { createContext, useContext, useState, useCallback, useRef, useEffect, ReactNode } from 'react' import type { Canvas, Object as FabricObject } from 'fabric' import type { EditorTool, WorksheetDocument, WorksheetPage, PageFormat, HistoryEntry, DEFAULT_PAGE_FORMAT } from '@/app/worksheet-editor/types' // Context Types interface WorksheetContextType { // Canvas canvas: Canvas | null setCanvas: (canvas: Canvas | null) => void // Document document: WorksheetDocument | null setDocument: (doc: WorksheetDocument | null) => void // Tool State activeTool: EditorTool setActiveTool: (tool: EditorTool) => void // Selection selectedObjects: FabricObject[] setSelectedObjects: (objects: FabricObject[]) => void // Zoom zoom: number setZoom: (zoom: number) => void zoomIn: () => void zoomOut: () => void zoomToFit: () => void // Grid showGrid: boolean setShowGrid: (show: boolean) => void snapToGrid: boolean setSnapToGrid: (snap: boolean) => void gridSize: number setGridSize: (size: number) => void // Pages currentPageIndex: number setCurrentPageIndex: (index: number) => void addPage: () => void deletePage: (index: number) => void // History canUndo: boolean canRedo: boolean undo: () => void redo: () => void saveToHistory: (action: string) => void // Save/Load saveDocument: () => Promise loadDocument: (id: string) => Promise // Export exportToPDF: () => Promise exportToImage: (format: 'png' | 'jpg') => Promise // Dirty State isDirty: boolean setIsDirty: (dirty: boolean) => void } const WorksheetContext = createContext(null) // Provider Props interface WorksheetProviderProps { children: ReactNode } // Generate unique ID const generateId = () => `ws_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` // Default Page Format const defaultPageFormat: PageFormat = { width: 210, height: 297, orientation: 'portrait', margins: { top: 15, right: 15, bottom: 15, left: 15 } } export function WorksheetProvider({ children }: WorksheetProviderProps) { // Canvas State const [canvas, setCanvas] = useState(null) // Document State const [document, setDocument] = useState(null) // Editor State const [activeTool, setActiveTool] = useState('select') const [selectedObjects, setSelectedObjects] = useState([]) const [zoom, setZoom] = useState(1) const [showGrid, setShowGrid] = useState(true) const [snapToGrid, setSnapToGrid] = useState(true) const [gridSize, setGridSize] = useState(10) const [currentPageIndex, setCurrentPageIndex] = useState(0) const [isDirty, setIsDirty] = useState(false) // History State const historyRef = useRef([]) const historyIndexRef = useRef(-1) const [canUndo, setCanUndo] = useState(false) const [canRedo, setCanRedo] = useState(false) // Initialize empty document useEffect(() => { if (!document) { const newDoc: WorksheetDocument = { id: generateId(), title: 'Neues Arbeitsblatt', pages: [{ id: generateId(), index: 0, canvasJSON: '' }], pageFormat: defaultPageFormat, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } setDocument(newDoc) } }, [document]) // Zoom functions const zoomIn = useCallback(() => { setZoom(prev => Math.min(prev * 1.2, 4)) }, []) const zoomOut = useCallback(() => { setZoom(prev => Math.max(prev / 1.2, 0.25)) }, []) const zoomToFit = useCallback(() => { setZoom(1) }, []) // Page functions const addPage = useCallback(() => { if (!document) return const newPage: WorksheetPage = { id: generateId(), index: document.pages.length, canvasJSON: '' } setDocument({ ...document, pages: [...document.pages, newPage], updatedAt: new Date().toISOString() }) setCurrentPageIndex(document.pages.length) setIsDirty(true) }, [document]) const deletePage = useCallback((index: number) => { if (!document || document.pages.length <= 1) return const newPages = document.pages.filter((_, i) => i !== index) .map((page, i) => ({ ...page, index: i })) setDocument({ ...document, pages: newPages, updatedAt: new Date().toISOString() }) if (currentPageIndex >= newPages.length) { setCurrentPageIndex(newPages.length - 1) } setIsDirty(true) }, [document, currentPageIndex]) // History functions const saveToHistory = useCallback((action: string) => { if (!canvas) return try { const canvasData = canvas.toJSON() if (!canvasData) return const entry: HistoryEntry = { canvasJSON: JSON.stringify(canvasData), timestamp: Date.now(), action } // Remove any future history if we're not at the end if (historyIndexRef.current < historyRef.current.length - 1) { historyRef.current = historyRef.current.slice(0, historyIndexRef.current + 1) } historyRef.current.push(entry) historyIndexRef.current = historyRef.current.length - 1 // Limit history size if (historyRef.current.length > 50) { historyRef.current.shift() historyIndexRef.current-- } setCanUndo(historyIndexRef.current > 0) setCanRedo(false) setIsDirty(true) } catch (error) { console.error('Failed to save history:', error) } }, [canvas]) const undo = useCallback(() => { if (!canvas || historyIndexRef.current <= 0) return historyIndexRef.current-- const entry = historyRef.current[historyIndexRef.current] canvas.loadFromJSON(JSON.parse(entry.canvasJSON), () => { canvas.renderAll() }) setCanUndo(historyIndexRef.current > 0) setCanRedo(true) }, [canvas]) const redo = useCallback(() => { if (!canvas || historyIndexRef.current >= historyRef.current.length - 1) return historyIndexRef.current++ const entry = historyRef.current[historyIndexRef.current] canvas.loadFromJSON(JSON.parse(entry.canvasJSON), () => { canvas.renderAll() }) setCanUndo(true) setCanRedo(historyIndexRef.current < historyRef.current.length - 1) }, [canvas]) // Save/Load functions const saveDocument = useCallback(async () => { if (!canvas || !document) return // Save current page state const currentPage = document.pages[currentPageIndex] currentPage.canvasJSON = JSON.stringify(canvas.toJSON()) // Get API base URL (use same protocol as page) const { hostname, protocol } = window.location const apiBase = hostname === 'localhost' ? 'http://localhost:8086' : `${protocol}//${hostname}:8086` try { const response = await fetch(`${apiBase}/api/v1/worksheet/save`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(document) }) if (!response.ok) throw new Error('Failed to save') const result = await response.json() setDocument({ ...document, id: result.id }) setIsDirty(false) } catch (error) { console.error('Save failed:', error) // Fallback to localStorage localStorage.setItem(`worksheet_${document.id}`, JSON.stringify(document)) setIsDirty(false) } }, [canvas, document, currentPageIndex]) const loadDocument = useCallback(async (id: string) => { const { hostname, protocol } = window.location const apiBase = hostname === 'localhost' ? 'http://localhost:8086' : `${protocol}//${hostname}:8086` try { const response = await fetch(`${apiBase}/api/v1/worksheet/${id}`) if (!response.ok) throw new Error('Failed to load') const doc = await response.json() setDocument(doc) setCurrentPageIndex(0) if (canvas && doc.pages[0]?.canvasJSON) { canvas.loadFromJSON(JSON.parse(doc.pages[0].canvasJSON), () => { canvas.renderAll() }) } } catch (error) { console.error('Load failed:', error) // Try localStorage fallback const stored = localStorage.getItem(`worksheet_${id}`) if (stored) { const doc = JSON.parse(stored) setDocument(doc) setCurrentPageIndex(0) } } }, [canvas]) // Export functions const exportToPDF = useCallback(async (): Promise => { if (!canvas || !document) throw new Error('No canvas or document') const { PDFDocument, rgb } = await import('pdf-lib') const pdfDoc = await PDFDocument.create() for (const page of document.pages) { const pdfPage = pdfDoc.addPage([ document.pageFormat.width * 2.83465, // mm to points document.pageFormat.height * 2.83465 ]) // Export canvas as PNG and embed if (page.canvasJSON) { // Load the page content if (currentPageIndex !== page.index) { await new Promise((resolve) => { canvas.loadFromJSON(JSON.parse(page.canvasJSON), () => { canvas.renderAll() resolve() }) }) } const dataUrl = canvas.toDataURL({ format: 'png', multiplier: 2 }) const imageBytes = await fetch(dataUrl).then(res => res.arrayBuffer()) const image = await pdfDoc.embedPng(imageBytes) pdfPage.drawImage(image, { x: 0, y: 0, width: pdfPage.getWidth(), height: pdfPage.getHeight() }) } } const pdfBytes = await pdfDoc.save() // Convert Uint8Array to ArrayBuffer for Blob compatibility const arrayBuffer = pdfBytes.slice().buffer as ArrayBuffer return new Blob([arrayBuffer], { type: 'application/pdf' }) }, [canvas, document, currentPageIndex]) const exportToImage = useCallback(async (format: 'png' | 'jpg'): Promise => { if (!canvas) throw new Error('No canvas') // Fabric.js uses 'jpeg' not 'jpg' const fabricFormat = format === 'jpg' ? 'jpeg' : format const dataUrl = canvas.toDataURL({ format: fabricFormat as 'png' | 'jpeg', quality: format === 'jpg' ? 0.9 : undefined, multiplier: 2 }) const response = await fetch(dataUrl) return response.blob() }, [canvas]) const value: WorksheetContextType = { canvas, setCanvas, document, setDocument, activeTool, setActiveTool, selectedObjects, setSelectedObjects, zoom, setZoom, zoomIn, zoomOut, zoomToFit, showGrid, setShowGrid, snapToGrid, setSnapToGrid, gridSize, setGridSize, currentPageIndex, setCurrentPageIndex, addPage, deletePage, canUndo, canRedo, undo, redo, saveToHistory, saveDocument, loadDocument, exportToPDF, exportToImage, isDirty, setIsDirty } return ( {children} ) } export function useWorksheet() { const context = useContext(WorksheetContext) if (!context) { throw new Error('useWorksheet must be used within a WorksheetProvider') } return context }