// app/frontend/components/Note/NoteModal.tsx import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Note } from '../../entities/Note'; import { useDataContext } from '../../contexts/DataContext'; import { useToast } from '../Shared/ToastContext'; import TagInput from '../Tag/TagInput'; import { Tag } from '../../entities/Tag'; interface NoteModalProps { isOpen: boolean; onClose: () => void; note?: Note | null; onSave: (noteData: Note) => void; } const NoteModal: React.FC = ({ isOpen, onClose, note, onSave }) => { const { createNote, updateNote } = useDataContext(); const [formData, setFormData] = useState({ id: note?.id || 0, 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 { showSuccessToast, showErrorToast } = useToast(); // Fetch available tags when modal is opened useEffect(() => { if (isOpen) { fetch('/api/tags') .then((response) => response.json()) .then((data: Tag[]) => setAvailableTags(data)) .catch((error) => { console.error('Failed to fetch tags', error); showErrorToast('Failed to load available tags.'); }); } }, [isOpen, showErrorToast]); // Reset form data when modal opens or note changes useEffect(() => { if (isOpen) { setFormData({ id: note?.id || 0, title: note?.title || '', content: note?.content || '', tags: note?.tags || [], }); setTags(note?.tags?.map((tag) => tag.name) || []); setError(null); } }, [isOpen, note]); // Handle click outside to close modal 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]); // Handle Escape key to close modal useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { handleClose(); } }; if (isOpen) { document.addEventListener('keydown', handleKeyDown); } return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [isOpen]); const handleChange = ( e: React.ChangeEvent ) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value, })); }; const handleTagsChange = useCallback((newTags: string[]) => { setTags(newTags); // Map newTags to Tag objects with 'id' if they exist in availableTags const updatedTags: Tag[] = newTags.map((name) => { const existingTag = availableTags.find((tag) => tag.name === name); return existingTag ? { id: existingTag.id, name } : { name }; }); setFormData((prev) => ({ ...prev, tags: updatedTags, })); }, [availableTags]); const handleSubmit = async () => { if (!formData.title.trim()) { setError('Note title is required.'); return; } setIsSubmitting(true); setError(null); try { if (formData.id && formData.id !== 0) { await updateNote(formData.id, formData); showSuccessToast('Note updated successfully!'); } else { await createNote(formData); showSuccessToast('Note created successfully!'); } onSave(formData); handleClose(); } catch (err) { setError((err as Error).message); showErrorToast('Failed to save note.'); } finally { setIsSubmitting(false); } }; const handleClose = () => { setIsClosing(true); setTimeout(() => { onClose(); setIsClosing(false); }, 300); }; if (!isOpen) return null; return ( <>
{/* Note Title */}
{/* Tags */}
{/* Note Content */}
{/* Error Message */} {error &&
{error}
}
{/* Action Buttons */}
); }; export default NoteModal;