'use client'; import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { BookOpen, Book, Archive, Grid3X3, AlignLeft, File, Palette, Leaf, Plus, Star, Trash2, ChevronLeft, MoreHorizontal, Music, PenTool, Search, X, Share2, Eye, EyeOff, Edit3, Hash, Bookmark, PenLine, Layers, BookMarked, Globe, Sparkles, Bold, Italic as ItalicIcon, Underline as UnderlineIcon, List, ListOrdered, AlignLeftIcon, AlignCenterIcon, AlignRightIcon, Heading1, Heading2, Heading3, CheckCircle2, User as UserIcon, GraduationCap, FileDown, Copy, FlaskConical, Languages, Calculator, Paintbrush, Megaphone, Strikethrough, Type, Highlighter, GripVertical, } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Textarea } from '@/components/ui/textarea'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Separator } from '@/components/ui/separator'; import { Switch } from '@/components/ui/switch'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { useAppStore } from '@/lib/store'; import { t } from '@/lib/i18n'; import { apiGet, apiPost, apiPut, apiDelete } from '@/lib/api'; import { toast } from 'sonner'; // ─── Types ─────────────────────────────────────────────────────────── interface NotebookPage { id: string; notebookId: string; pageNumber: number; title: string | null; contentType: string; textContent: string | null; drawingData: string | null; background: string; isBookmark: boolean; createdAt: string; updatedAt: string; } interface Notebook { id: string; schoolId: string; ownerId: string; ownerType: string; subjectId: string | null; classGroupId: string | null; title: string; description: string | null; notebookType: string; color: string; icon: string | null; isArchived: boolean; isPublic: boolean; sortOrder: number; createdAt: string; updatedAt: string; pages?: NotebookPage[]; _count?: { pages: number }; subject?: { id: string; name: string } | null; classGroup?: { id: string; name: string } | null; owner?: { id: string; firstName: string; lastName: string; role: string } | null; } interface Subject { id: string; name: string; schoolId: string; } interface ClassGroup { id: string; name: string; schoolId: string; gradeLevel: number; } // ─── Constants ─────────────────────────────────────────────────────── const NOTEBOOK_TYPES: Array<{ key: string; icon: React.ElementType; labelKey: string; descKey: string }> = [ { key: 'lined', icon: AlignLeft, labelKey: 'notebooks.type_lined', descKey: 'notebooks.type_lined_desc' }, { key: 'grid', icon: Grid3X3, labelKey: 'notebooks.type_grid', descKey: 'notebooks.type_grid_desc' }, { key: 'blank', icon: File, labelKey: 'notebooks.type_blank', descKey: 'notebooks.type_blank_desc' }, { key: 'dotted', icon: MoreHorizontal, labelKey: 'notebooks.type_dotted', descKey: 'notebooks.type_dotted_desc' }, { key: 'music', icon: Music, labelKey: 'notebooks.type_music', descKey: 'notebooks.type_music_desc' }, { key: 'calligraphy', icon: PenTool, labelKey: 'notebooks.type_calligraphy', descKey: 'notebooks.type_calligraphy_desc' }, ]; const COVER_COLORS = [ { key: 'emerald', hex: '#10b981', labelKey: 'notebooks.color_emerald' }, { key: 'blue', hex: '#3b82f6', labelKey: 'notebooks.color_blue' }, { key: 'red', hex: '#ef4444', labelKey: 'notebooks.color_red' }, { key: 'yellow', hex: '#f59e0b', labelKey: 'notebooks.color_yellow' }, { key: 'purple', hex: '#8b5cf6', labelKey: 'notebooks.color_purple' }, { key: 'orange', hex: '#f97316', labelKey: 'notebooks.color_orange' }, { key: 'teal', hex: '#14b8a6', labelKey: 'notebooks.color_teal' }, { key: 'pink', hex: '#ec4899', labelKey: 'notebooks.color_pink' }, ]; const ICON_OPTIONS = [ 'BookOpen', 'Book', 'PenLine', 'Music', 'PenTool', 'Palette', 'Leaf', 'Star', 'Globe', 'Sparkles', 'Hash', 'Layers', ]; const ICON_MAP: Record = { BookOpen, Book, PenLine, Music, PenTool, Palette, Leaf, Star, Globe, Sparkles, Hash, Layers, }; // ─── Template Definitions ──────────────────────────────────────────── interface NotebookTemplate { key: string; titleKey: string; descKey: string; notebookType: string; color: string; icon: string; iconComponent: React.ElementType; pages: Array<{ title: string | null; content: string }>; } const NOTEBOOK_TEMPLATES: NotebookTemplate[] = [ { key: 'math', titleKey: 'notebooks.template_math', descKey: 'notebooks.template_math_desc', notebookType: 'grid', color: '#3b82f6', icon: 'Hash', iconComponent: Calculator, pages: [ { title: 'Aufgaben', content: '# Aufgaben\n\n1. \n2. \n3. \n' }, { title: 'Rechnungen', content: '# Rechnungen\n\n' }, { title: 'Formeln', content: '# Formeln\n\n' }, { title: 'Geometrie', content: '# Geometrie\n\n' }, { title: 'Ergebnisse', content: '# Ergebnisse\n\n' }, ], }, { key: 'german', titleKey: 'notebooks.template_german', descKey: 'notebooks.template_german_desc', notebookType: 'lined', color: '#ef4444', icon: 'BookOpen', iconComponent: BookOpen, pages: [ { title: 'Aufsaetze', content: '# Aufsaetze\n\n' }, { title: 'Lesetagebuch', content: '# Lesetagebuch\n\n' }, { title: 'Grammatik', content: '# Grammatik\n\n' }, { title: 'Rechtschreibung', content: '# Rechtschreibung\n\n' }, { title: 'Kreatives Schreiben', content: '# Kreatives Schreiben\n\n' }, ], }, { key: 'english', titleKey: 'notebooks.template_english', descKey: 'notebooks.template_english_desc', notebookType: 'lined', color: '#f59e0b', icon: 'Globe', iconComponent: Languages, pages: [ { title: 'Vocabulary', content: '# Vocabulary\n\n| English | Deutsch |\n|---------|--------|\n| | |\n| | |\n' }, { title: 'Grammar', content: '# Grammar\n\n' }, { title: 'Reading', content: '# Reading\n\n' }, { title: 'Writing', content: '# Writing\n\n' }, { title: 'Exercises', content: '# Exercises\n\n' }, ], }, { key: 'art', titleKey: 'notebooks.template_art', descKey: 'notebooks.template_art_desc', notebookType: 'blank', color: '#8b5cf6', icon: 'Palette', iconComponent: Paintbrush, pages: [ { title: 'Skizzen', content: '' }, { title: 'Farbstudien', content: '' }, { title: 'Komposition', content: '' }, { title: 'Perspektive', content: '' }, { title: 'Portfolio', content: '' }, ], }, { key: 'music', titleKey: 'notebooks.template_music', descKey: 'notebooks.template_music_desc', notebookType: 'music', color: '#10b981', icon: 'Music', iconComponent: Music, pages: [ { title: 'Noten', content: '' }, { title: 'Rhythmus', content: '' }, { title: 'Melodie', content: '' }, { title: 'Harmonie', content: '' }, { title: 'Komposition', content: '' }, ], }, { key: 'science', titleKey: 'notebooks.template_science', descKey: 'notebooks.template_science_desc', notebookType: 'grid', color: '#14b8a6', icon: 'Book', iconComponent: FlaskConical, pages: [ { title: 'Versuche', content: '# Versuche\n\n**Fragestellung:**\n\n**Vermutung:**\n\n**Durchfuehrung:**\n\n**Beobachtung:**\n\n**Ergebnis:**\n' }, { title: 'Beobachtungen', content: '# Beobachtungen\n\n' }, { title: 'Ergebnisse', content: '# Ergebnisse\n\n' }, { title: 'Versuchsprotokoll', content: '# Versuchsprotokoll\n\nDatum:\n\nMaterial:\n\n' }, { title: 'Fragen', content: '# Fragen\n\n' }, ], }, ]; // ─── CSS Background Patterns ───────────────────────────────────────── function getPageBackgroundCSS(type: string): React.CSSProperties { switch (type) { case 'lined': return { backgroundImage: `repeating-linear-gradient(0deg, transparent, transparent 31px, #d1d5db 31px, #d1d5db 32px)`, backgroundSize: '100% 32px', backgroundPosition: '0 16px', }; case 'grid': return { backgroundImage: ` repeating-linear-gradient(0deg, transparent, transparent 31px, #d1d5db 31px, #d1d5db 32px), repeating-linear-gradient(90deg, transparent, transparent 31px, #d1d5db 31px, #d1d5db 32px) `, backgroundSize: '32px 32px', }; case 'dotted': return { backgroundImage: `radial-gradient(circle, #9ca3af 1px, transparent 1px)`, backgroundSize: '24px 24px', }; case 'music': return { backgroundImage: ` repeating-linear-gradient(0deg, transparent, transparent 7px, #d1d5db 7px, #d1d5db 8px), repeating-linear-gradient(0deg, transparent, transparent 15px, #9ca3af 15px, #9ca3af 16px), repeating-linear-gradient(0deg, transparent, transparent 23px, #9ca3af 23px, #9ca3af 24px), repeating-linear-gradient(0deg, transparent, transparent 31px, #9ca3af 31px, #9ca3af 32px), repeating-linear-gradient(0deg, transparent, transparent 39px, #d1d5db 39px, #d1d5db 40px) `, backgroundSize: '100% 40px', }; case 'calligraphy': return { backgroundImage: ` repeating-linear-gradient(90deg, transparent, transparent 59px, #e5e7eb 59px, #e5e7eb 60px), repeating-linear-gradient(0deg, transparent, transparent 31px, #d1d5db 31px, #d1d5db 32px) `, backgroundSize: '60px 32px', backgroundPosition: '0 16px', }; case 'blank': return {}; default: return {}; } } // ─── WYSIWYG Rich Text Toolbar ─────────────────────────────────────── const TEXT_COLORS = [ { key: 'black', color: '#1f2937', labelKey: 'notebooks.toolbar_color_black' }, { key: 'red', color: '#dc2626', labelKey: 'notebooks.toolbar_color_red' }, { key: 'blue', color: '#2563eb', labelKey: 'notebooks.toolbar_color_blue' }, { key: 'green', color: '#16a34a', labelKey: 'notebooks.toolbar_color_green' }, { key: 'orange', color: '#ea580c', labelKey: 'notebooks.toolbar_color_orange' }, ]; const HIGHLIGHT_COLORS = [ { key: 'yellow', color: '#fef08a', labelKey: 'notebooks.toolbar_highlight_yellow' }, { key: 'green', color: '#bbf7d0', labelKey: 'notebooks.toolbar_highlight_green' }, { key: 'blue', color: '#bfdbfe', labelKey: 'notebooks.toolbar_highlight_blue' }, { key: 'pink', color: '#fbcfe8', labelKey: 'notebooks.toolbar_highlight_pink' }, { key: 'none', color: 'transparent', labelKey: 'notebooks.toolbar_highlight_none' }, ]; function WysiwygToolbar({ editorRef, onFormatChange }: { editorRef: React.RefObject; onFormatChange: () => void }) { const [activeStates, setActiveStates] = useState>({}); const updateActiveStates = useCallback(() => { setActiveStates({ bold: document.queryCommandState('bold'), italic: document.queryCommandState('italic'), underline: document.queryCommandState('underline'), strikethrough: document.queryCommandState('strikeThrough'), justifyLeft: document.queryCommandState('justifyLeft'), justifyCenter: document.queryCommandState('justifyCenter'), justifyRight: document.queryCommandState('justifyRight'), }); }, []); const execCommand = useCallback((command: string, value?: string) => { document.execCommand(command, false, value); editorRef.current?.focus(); updateActiveStates(); onFormatChange(); }, [editorRef, updateActiveStates, onFormatChange]); const applyHeading = useCallback((level: number) => { document.execCommand('formatBlock', false, `h${level}`); editorRef.current?.focus(); updateActiveStates(); onFormatChange(); }, [editorRef, updateActiveStates, onFormatChange]); const applyTextColor = useCallback((color: string) => { document.execCommand('foreColor', false, color); editorRef.current?.focus(); updateActiveStates(); onFormatChange(); }, [editorRef, updateActiveStates, onFormatChange]); const applyHighlight = useCallback((color: string) => { if (color === 'transparent') { document.execCommand('removeFormat', false); } else { document.execCommand('hiliteColor', false, color); } editorRef.current?.focus(); updateActiveStates(); onFormatChange(); }, [editorRef, updateActiveStates, onFormatChange]); const fmtBtnClass = (active: boolean) => `min-h-[36px] min-w-[36px] h-9 w-9 p-0 shrink-0 ${active ? 'bg-gray-200 dark:bg-gray-600 text-gray-900 dark:text-gray-100' : ''}`; return (
{/* Format group */} {t('notebooks.toolbar_bold')} {t('notebooks.toolbar_italic')} {t('notebooks.toolbar_underline')} {t('notebooks.toolbar_strikethrough')} {/* Heading group */} {t('notebooks.toolbar_heading1')} {t('notebooks.toolbar_heading2')} {t('notebooks.toolbar_heading3')} {/* List group */} {t('notebooks.toolbar_bullet_list')} {t('notebooks.toolbar_numbered_list')} {/* Alignment group */} {t('notebooks.toolbar_align_left')} {t('notebooks.toolbar_align_center')} {t('notebooks.toolbar_align_right')} {/* Text color */} {t('notebooks.toolbar_text_color')}
{TEXT_COLORS.map((tc) => (
{/* Highlight color */} {t('notebooks.toolbar_highlight')}
{HIGHLIGHT_COLORS.map((hc) => ( ))}
); } // ─── Notebook Card Component ───────────────────────────────────────── function NotebookCard({ notebook, subjectName, onOpen, onArchive, onDelete, onShare, onDuplicate, isShared = false, }: { notebook: Notebook; subjectName: string | null; onOpen: () => void; onArchive: () => void; onDelete: () => void; onShare?: () => void; onDuplicate?: () => void; isShared?: boolean; }) { const pageCount = notebook._count?.pages ?? notebook.pages?.length ?? 0; const IconComponent = notebook.icon ? ICON_MAP[notebook.icon] ?? BookOpen : BookOpen; const typeInfo = NOTEBOOK_TYPES.find(nt => nt.key === notebook.notebookType) ?? NOTEBOOK_TYPES[0]; const TypeIcon = typeInfo.icon; const ownerName = notebook.owner ? `${notebook.owner.firstName} ${notebook.owner.lastName}` : null; return ( {/* Cover section */}
{/* Book spine effect */}
{/* Public badge */} {notebook.isPublic && !isShared && ( {t('notebooks.shared')} )} {/* Shared badge */} {isShared && ( {t('notebooks.shared_notebook')} )} {/* Archive overlay */} {notebook.isArchived && (
{t('notebooks.archived')}
)}
{notebook.title}
{/* Type badge */} {t(typeInfo.labelKey)} {/* Subject badge */} {subjectName && ( {subjectName} )}
{/* Owner name for shared notebooks */} {isShared && ownerName && (
{ownerName}
)} {/* Page count */}
{t('notebooks.page_count', { count: pageCount })}
{/* Eco message */}
{t('notebooks.eco_tip')}
{/* Action buttons — only show for own notebooks, not shared */} {!isShared && (
{onShare && ( )} {onDuplicate && ( )}
)}
); } // ─── Create Notebook Dialog ────────────────────────────────────────── function CreateNotebookDialog({ open, onClose, subjects, classes, onCreate, }: { open: boolean; onClose: () => void; subjects: Subject[]; classes: ClassGroup[]; onCreate: (data: Partial) => Promise; }) { const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [notebookType, setNotebookType] = useState('lined'); const [subjectId, setSubjectId] = useState(''); const [classGroupId, setClassGroupId] = useState(''); const [color, setColor] = useState('#10b981'); const [icon, setIcon] = useState('BookOpen'); const [isPublic, setIsPublic] = useState(false); const [creating, setCreating] = useState(false); const handleCreate = async () => { if (!title.trim()) { toast.error(t('notebooks.title_required')); return; } setCreating(true); try { await onCreate({ title: title.trim(), description: description.trim() || null, notebookType, subjectId: subjectId || null, classGroupId: classGroupId || null, color, icon, isPublic, }); setTitle(''); setDescription(''); setNotebookType('lined'); setSubjectId(''); setClassGroupId(''); setColor('#10b981'); setIcon('BookOpen'); setIsPublic(false); onClose(); } catch { toast.error(t('notebooks.error_create')); } finally { setCreating(false); } }; return ( { if (!v) onClose(); }}> {t('notebooks.create_title')} {t('notebooks.create_desc')}
{/* Title */}
setTitle(e.target.value)} placeholder={t('notebooks.field_title_placeholder')} className="min-h-[44px]" />
{/* Description */}