import React, { useState, useEffect, useRef, useCallback, useMemo, } from 'react'; import { Note } from '../../entities/Note'; import { Project } from '../../entities/Project'; import { useToast } from '../Shared/ToastContext'; import TagInput from '../Tag/TagInput'; import MarkdownRenderer from '../Shared/MarkdownRenderer'; import { Tag } from '../../entities/Tag'; import { fetchTags } from '../../utils/tagsService'; import { useTranslation } from 'react-i18next'; import { EyeIcon, PencilIcon, FolderIcon, TagIcon, TrashIcon, } from '@heroicons/react/24/outline'; interface NoteModalProps { isOpen: boolean; onClose: () => void; note?: Note | null; onSave: (noteData: Note) => Promise; onDelete?: (noteId: number) => Promise; projects?: Project[]; onCreateProject?: (name: string) => Promise; } const NoteModal: React.FC = ({ isOpen, onClose, note, onSave, onDelete, projects = [], onCreateProject, }) => { const { t } = useTranslation(); const [formData, setFormData] = useState({ id: note?.id, title: note?.title || '', content: note?.content || '', tags: note?.tags || [], }); const [tags, setTags] = useState( note?.tags?.map((tag) => tag.name) || [] ); const [availableTags, setAvailableTags] = useState([]); const [error, setError] = useState(null); const modalRef = useRef(null); const [isSubmitting, setIsSubmitting] = useState(false); const [isClosing, setIsClosing] = useState(false); const [activeTab, setActiveTab] = useState<'edit' | 'preview'>('edit'); // Project-related state const [filteredProjects, setFilteredProjects] = useState([]); const [newProjectName, setNewProjectName] = useState(''); const [isCreatingProject, setIsCreatingProject] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false); // Collapsible sections state const [expandedSections, setExpandedSections] = useState({ tags: false, project: false, }); const { showSuccessToast, showErrorToast } = useToast(); // Memoize projects to prevent infinite re-renders const memoizedProjects = useMemo(() => projects || [], [projects]); useEffect(() => { if (!isOpen) return; const loadTags = async () => { try { const data = await fetchTags(); setAvailableTags(data); } catch (error) { console.error('Failed to fetch tags', error); showErrorToast('Failed to load tags'); } }; loadTags(); }, [isOpen]); // Initialize filtered projects from props - like TaskModal useEffect(() => { setFilteredProjects(memoizedProjects); }, [memoizedProjects]); // Initialize form data when modal opens - exactly like TaskModal useEffect(() => { if (isOpen) { // Initialize form data const tagNames = note?.tags?.map((tag) => tag.name) || []; setFormData({ id: note?.id, title: note?.title || '', content: note?.content || '', tags: note?.tags || [], }); setTags(tagNames); setError(null); // Initialize project name from note - exactly like TaskModal const currentProject = memoizedProjects.find( (project) => project.id === (note?.project?.id || note?.Project?.id) ); setNewProjectName(currentProject ? currentProject.name : ''); } }, [isOpen, note, memoizedProjects]); const handleClose = useCallback(() => { setIsClosing(true); setTimeout(() => { onClose(); setIsClosing(false); }, 300); }, [onClose]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( modalRef.current && !modalRef.current.contains(event.target as Node) ) { handleClose(); } }; if (isOpen) { document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [isOpen, handleClose]); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { handleClose(); } }; if (isOpen) { document.addEventListener('keydown', handleKeyDown); } return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [isOpen, handleClose]); const handleChange = ( e: React.ChangeEvent ) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value, })); }; const handleTagsChange = useCallback((newTags: string[]) => { setTags(newTags); setFormData((prev) => ({ ...prev, tags: newTags.map((name) => ({ name })), })); }, []); const toggleSection = useCallback( (section: keyof typeof expandedSections) => { setExpandedSections((prev) => { const newExpanded = { ...prev, [section]: !prev[section], }; // Auto-scroll to show the expanded section if (newExpanded[section]) { setTimeout(() => { const scrollContainer = document.querySelector( '.absolute.inset-0.overflow-y-auto' ); if (scrollContainer) { scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior: 'smooth', }); } }, 100); // Small delay to ensure DOM is updated } return newExpanded; }); }, [] ); const handleProjectSearch = (e: React.ChangeEvent) => { const value = e.target.value; setNewProjectName(value); setDropdownOpen(true); if (!projects || projects.length === 0) { setFilteredProjects([]); return; } const query = value.toLowerCase(); const filtered = projects.filter((project) => project.name.toLowerCase().includes(query) ); setFilteredProjects(filtered); }; const handleProjectSelection = (project: Project) => { setFormData((prev) => ({ ...prev, project: { id: project.id!, name: project.name }, project_id: project.id, })); setNewProjectName(project.name); setDropdownOpen(false); }; const handleCreateProject = async () => { if (newProjectName.trim() !== '' && onCreateProject) { setIsCreatingProject(true); try { const newProject = await onCreateProject(newProjectName.trim()); setFormData((prev) => ({ ...prev, project: { id: newProject.id!, name: newProject.name }, project_id: newProject.id, })); setFilteredProjects([...filteredProjects, newProject]); setNewProjectName(newProject.name); setDropdownOpen(false); showSuccessToast(t('success.projectCreated')); } catch (error) { showErrorToast(t('errors.projectCreationFailed')); console.error('Error creating project:', error); } finally { setIsCreatingProject(false); } } }; const handleSubmit = async () => { if (!formData.title.trim()) { setError(t('errors.noteTitleRequired')); return; } setIsSubmitting(true); setError(null); try { // Convert string tags to tag objects const noteTags: Tag[] = tags.map((tagName) => ({ name: tagName })); // Create final form data with the tags const finalFormData = { ...formData, tags: noteTags }; await onSave(finalFormData); showSuccessToast( formData.id && formData.id !== 0 ? t('success.noteUpdated') : t('success.noteCreated') ); handleClose(); } catch (err) { setError((err as Error).message); showErrorToast(t('errors.failedToSaveNote')); } finally { setIsSubmitting(false); } }; const handleDeleteNote = async () => { if (formData.id && formData.id !== 0 && onDelete) { try { await onDelete(formData.id); showSuccessToast(t('success.noteDeleted')); handleClose(); } catch (err) { setError((err as Error).message); showErrorToast(t('errors.failedToDeleteNote')); } } }; if (!isOpen) return null; return ( <>
{/* Main Form Section */}
{/* Note Title Section - Always Visible */}
{/* Content Section - Always Visible */}
{activeTab === 'edit' ? (