import React, { useEffect, useState, useCallback } from 'react' import { Book, Plus, Search, Trash2, Edit2, Clock, Eye, EyeOff, X, ArrowLeft, Folder, FolderPlus, MoreVertical, Check, ChevronRight } from 'lucide-react' import { motion, AnimatePresence } from 'framer-motion' import { noteService } from '../../services/noteService' import { noteCategoryService } from '../../services/noteCategoryService' import { Note, NoteCategory } from '../../types' import { useLanguage } from '../../contexts/LanguageContext' import { useToast } from '../../contexts/ToastContext' import { useConfirm } from '../../contexts/ConfirmContext' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' import rehypeKatex from 'rehype-katex' interface MemosViewProps { authToken: string isAdmin?: boolean } export const MemosView: React.FC = ({ authToken, isAdmin = false }) => { const { t } = useLanguage() const { showError, showSuccess } = useToast() const { confirm } = useConfirm() const [notes, setNotes] = useState([]) const [isLoading, setIsLoading] = useState(true) const [filterText, setFilterText] = useState('') // Editor state const [isEditing, setIsEditing] = useState(false) const [currentNote, setCurrentNote] = useState>({}) const [showPreview, setShowPreview] = useState(true) // Category state const [categories, setCategories] = useState([]) const [selectedCategoryId, setSelectedCategoryId] = useState(null) // null = All const [isCategoryLoading, setIsCategoryLoading] = useState(false) const [showAddCategory, setShowAddCategory] = useState(false) const [newCategoryName, setNewCategoryName] = useState('') const [editingCategoryId, setEditingCategoryId] = useState(null) const [editCategoryName, setEditCategoryName] = useState('') const [addingSubCategoryId, setAddingSubCategoryId] = useState(null) const [expandedCategories, setExpandedCategories] = useState>(new Set()) const fetchNotes = useCallback(async () => { if (!authToken) return try { setIsLoading(true) const data = await noteService.getAll(authToken, undefined, selectedCategoryId || undefined) setNotes(data) } catch (error: any) { console.error(error) const errorMsg = error.message || '' if (!errorMsg.includes('401') && !errorMsg.includes('403')) { showError(`${t('errorLoadData')}: ${errorMsg}`) } } finally { setIsLoading(false) } }, [authToken, selectedCategoryId, showError, t]) const fetchCategories = useCallback(async () => { if (!authToken) return try { setIsCategoryLoading(true) const data = await noteCategoryService.getAll(authToken) setCategories(data) } catch (error: any) { console.error(error) } finally { setIsCategoryLoading(false) } }, [authToken]) useEffect(() => { fetchNotes() }, [fetchNotes]) useEffect(() => { fetchCategories() }, [fetchCategories]) const handleSaveNote = async () => { if (!currentNote.title || !currentNote.content) { showError(t('errorTitleContentRequired')) return } try { if (currentNote.id) { await noteService.update(authToken, currentNote.id, { title: currentNote.title, content: currentNote.content, categoryId: currentNote.categoryId }) showSuccess(t('successNoteUpdated')) } else { await noteService.create(authToken, { title: currentNote.title, content: currentNote.content, groupId: '', categoryId: currentNote.categoryId || selectedCategoryId || undefined }) showSuccess(t('successNoteCreated')) } setIsEditing(false) setCurrentNote({}) fetchNotes() } catch (error: any) { showError(t('errorSaveFailed', error.message)) } } const handleDeleteNote = async (id: string) => { if (!(await confirm(t('confirmDeleteNote')))) return try { await noteService.delete(authToken, id) showSuccess(t('successNoteDeleted')) fetchNotes() } catch (error) { showError(t('deleteFailed')) } } const filteredNotes = notes.filter(n => n.title.toLowerCase().includes(filterText.toLowerCase()) || n.content.toLowerCase().includes(filterText.toLowerCase()) ) const handleCreateCategory = async (e: React.FormEvent, parentId?: string) => { e.preventDefault() if (!newCategoryName.trim()) return try { await noteCategoryService.create(authToken, newCategoryName.trim(), parentId) setNewCategoryName('') setShowAddCategory(false) setAddingSubCategoryId(null) fetchCategories() showSuccess(t('categoryCreated')) } catch (error: any) { showError(`${t('failedToCreateCategory')}: ${error.message}`) } } const handleUpdateCategory = async (id: string) => { if (!editCategoryName.trim()) return try { await noteCategoryService.update(authToken, id, editCategoryName.trim()) setEditingCategoryId(null) fetchCategories() showSuccess(t('groupUpdated')) } catch (error) { showError(t('actionFailed')) } } const handleDeleteCategory = async (e: React.MouseEvent, id: string) => { e.stopPropagation() if (!(await confirm(t('confirmDeleteCategory')))) return try { await noteCategoryService.delete(authToken, id) if (selectedCategoryId === id) setSelectedCategoryId(null) fetchCategories() showSuccess(t('groupDeleted')) } catch (error) { showError(t('failedToDeleteCategory')) } } const toggleCategory = (id: string, e: React.MouseEvent) => { e.stopPropagation() const newExpanded = new Set(expandedCategories) if (newExpanded.has(id)) newExpanded.delete(id) else newExpanded.add(id) setExpandedCategories(newExpanded) } const renderCategoryTree = (parentId: string | null) => { const items = categories.filter(c => (c.parentId || null) === (parentId || null)) return items.map(cat => { const hasChildren = categories.some(c => c.parentId === cat.id) const isExpanded = expandedCategories.has(cat.id) return (
{editingCategoryId === cat.id ? (
setEditCategoryName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleUpdateCategory(cat.id)} />
) : (
setSelectedCategoryId(cat.id)} className={`w-full flex items-center justify-between px-3 py-2 rounded-lg text-sm group cursor-pointer transition-all ${selectedCategoryId === cat.id ? 'bg-blue-50 text-blue-700 font-bold shadow-sm' : 'text-slate-500 hover:bg-slate-100/50'}`} style={{ paddingLeft: `${(cat.level - 1) * 12 + 12}px` }} >
toggleCategory(cat.id, e)} className={`p-0.5 hover:bg-blue-100 rounded transition-colors ${!hasChildren ? 'invisible' : ''}`} >
{cat.name}
{cat.level < 3 && ( )}
)}
{isExpanded && (
{renderCategoryTree(cat.id)} {addingSubCategoryId === cat.id && (
handleCreateCategory(e, cat.id)} className="my-1 mx-2" style={{ paddingLeft: `${cat.level * 12 + 12}px` }} >
setNewCategoryName(e.target.value)} onBlur={() => !newCategoryName && setAddingSubCategoryId(null)} />
)}
)}
) }) } if (isEditing) { return (

{currentNote.id ? t('editNote') : t('newNote')}

{t('directoryLabel')}:
setCurrentNote({ ...currentNote, title: e.target.value })} className="text-2xl font-bold text-slate-900 bg-transparent border-none focus:ring-0 placeholder:text-slate-200 w-full" />