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 { useStore } from '../../store/useStore'; import { useTranslation } from 'react-i18next'; import ProjectDropdown from '../Shared/ProjectDropdown'; 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 { tagsStore } = useStore(); const availableTagsStore = tagsStore.getTags(); const { addNewTags } = tagsStore; const [formData, setFormData] = useState( note || { title: '', content: '', tags: [], } ); const [tags, setTags] = useState( (note?.tags || note?.Tags)?.map((tag) => tag.name) || [] ); const [error, setError] = useState(null); const modalRef = useRef(null); const titleInputRef = 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; // Auto-focus on the title input when modal opens setTimeout(() => { titleInputRef.current?.focus(); }, 100); }, [isOpen]); // Initialize filtered projects from props - like TaskModal useEffect(() => { setFilteredProjects(memoizedProjects); }, [memoizedProjects]); // Initialize form data when modal opens - exactly like TaskModal useEffect(() => { if (isOpen) { if (note) { // Initialize form data directly from note (like TaskModal) setFormData(note); const tagNames = (note?.tags || note?.Tags)?.map((tag) => tag.name) || []; setTags(tagNames); setError(null); // Initialize project name from note - exactly like TaskModal const projectIdToFind = note?.project?.id || note?.Project?.id || note?.project_id; const currentProject = memoizedProjects.find( (project) => project.id === projectIdToFind ); setNewProjectName(currentProject ? currentProject.name : ''); // Auto-expand sections if they have content from existing note or always for editing const shouldExpandTags = tagNames.length > 0 || !!note.id; // Expand if has tags OR editing existing note const shouldExpandProject = !!currentProject || !!note.id; // Expand if has project OR editing existing note setExpandedSections({ tags: shouldExpandTags, project: shouldExpandProject, }); } else { // Reset for new note setFormData({ title: '', content: '', tags: [], }); setTags([]); setNewProjectName(''); setError(null); setExpandedSections({ tags: false, project: false, }); } } }, [isOpen, note, memoizedProjects]); // Tags are now loaded automatically via getTags() when needed const handleClose = useCallback(() => { setIsClosing(true); setTimeout(() => { onClose(); setIsClosing(false); // Reset expanded sections when closing setExpandedSections({ tags: false, project: 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 handleShowAllProjects = () => { setNewProjectName(''); setFilteredProjects(memoizedProjects); setDropdownOpen(!dropdownOpen); }; const handleSubmit = async () => { if (!formData.title.trim()) { setError(t('errors.noteTitleRequired')); return; } setIsSubmitting(true); setError(null); try { // Add new tags to the global store const existingTagNames = availableTagsStore.map( (tag: any) => tag.name ); const newTagNames = tags.filter( (tag) => !existingTagNames.includes(tag) ); if (newTagNames.length > 0) { addNewTags(newTagNames); } // 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 ( <>
{ // Close modal when clicking on backdrop, but not on the modal content if (e.target === e.currentTarget) { handleClose(); } }} >
{/* Main Form Section */}
{ e.preventDefault(); handleSubmit(); }} >
{/* Note Title Section - Always Visible */}
{ if ( e.key === 'Enter' ) { e.preventDefault(); handleSubmit(); } }} required className="block w-full text-xl font-semibold bg-transparent text-black dark:text-white border-none focus:outline-none shadow-sm py-2" placeholder={t( 'forms.noteTitlePlaceholder' )} autoComplete="off" />
{/* Content Section - Always Visible */}
{activeTab === 'edit' ? (