import { faSpinner } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { type FormEvent, useCallback, useEffect, useState } from "react"; import { ApiClientError, apiClient } from "../api"; interface NoteField { id: string; name: string; order: number; } interface NoteType { id: string; name: string; frontTemplate: string; backTemplate: string; isReversible: boolean; fields: NoteField[]; } interface NoteFieldValue { id: string; noteId: string; noteFieldTypeId: string; value: string; } interface NoteWithFieldValues { id: string; deckId: string; noteTypeId: string; fieldValues: NoteFieldValue[]; } interface EditNoteModalProps { isOpen: boolean; deckId: string; noteId: string | null; onClose: () => void; onNoteUpdated: () => void; } export function EditNoteModal({ isOpen, deckId, noteId, onClose, onNoteUpdated, }: EditNoteModalProps) { const [note, setNote] = useState(null); const [noteType, setNoteType] = useState(null); const [fieldValues, setFieldValues] = useState>({}); const [error, setError] = useState(null); const [isLoadingNote, setIsLoadingNote] = useState(false); const [isLoadingNoteType, setIsLoadingNoteType] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const fetchNoteTypeDetails = useCallback(async (noteTypeId: string) => { setIsLoadingNoteType(true); setError(null); try { const authHeader = apiClient.getAuthHeader(); if (!authHeader) { throw new ApiClientError("Not authenticated", 401); } const res = await fetch(`/api/note-types/${noteTypeId}`, { headers: authHeader, }); if (!res.ok) { const errorBody = await res.json().catch(() => ({})); throw new ApiClientError( (errorBody as { error?: string }).error || `Request failed with status ${res.status}`, res.status, ); } const data = await res.json(); setNoteType(data.noteType); } catch (err) { if (err instanceof ApiClientError) { setError(err.message); } else { setError("Failed to load note type details. Please try again."); } } finally { setIsLoadingNoteType(false); } }, []); const fetchNote = useCallback(async () => { if (!noteId) return; setIsLoadingNote(true); setError(null); try { const authHeader = apiClient.getAuthHeader(); if (!authHeader) { throw new ApiClientError("Not authenticated", 401); } const res = await fetch(`/api/decks/${deckId}/notes/${noteId}`, { headers: authHeader, }); if (!res.ok) { const errorBody = await res.json().catch(() => ({})); throw new ApiClientError( (errorBody as { error?: string }).error || `Request failed with status ${res.status}`, res.status, ); } const data = await res.json(); setNote(data.note); // Initialize field values from note const initialValues: Record = {}; for (const fv of data.note.fieldValues) { initialValues[fv.noteFieldTypeId] = fv.value; } setFieldValues(initialValues); // Fetch note type details await fetchNoteTypeDetails(data.note.noteTypeId); } catch (err) { if (err instanceof ApiClientError) { setError(err.message); } else { setError("Failed to load note. Please try again."); } } finally { setIsLoadingNote(false); } }, [noteId, deckId, fetchNoteTypeDetails]); useEffect(() => { if (isOpen && noteId) { fetchNote(); } }, [isOpen, noteId, fetchNote]); const resetForm = () => { setNote(null); setNoteType(null); setFieldValues({}); setError(null); }; const handleClose = () => { resetForm(); onClose(); }; const handleFieldChange = (fieldId: string, value: string) => { setFieldValues((prev) => ({ ...prev, [fieldId]: value, })); }; const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setError(null); if (!note) { setError("Note data is not loaded."); return; } setIsSubmitting(true); try { const authHeader = apiClient.getAuthHeader(); if (!authHeader) { throw new ApiClientError("Not authenticated", 401); } // Trim all field values const trimmedFields: Record = {}; for (const [fieldId, value] of Object.entries(fieldValues)) { trimmedFields[fieldId] = value.trim(); } const res = await fetch(`/api/decks/${deckId}/notes/${note.id}`, { method: "PUT", headers: { "Content-Type": "application/json", ...authHeader, }, body: JSON.stringify({ fields: trimmedFields, }), }); if (!res.ok) { const errorBody = await res.json().catch(() => ({})); throw new ApiClientError( (errorBody as { error?: string }).error || `Request failed with status ${res.status}`, res.status, ); } onNoteUpdated(); handleClose(); } catch (err) { if (err instanceof ApiClientError) { setError(err.message); } else { setError("Failed to update note. Please try again."); } } finally { setIsSubmitting(false); } }; if (!isOpen || !noteId) { return null; } // Check if all required fields have values const isFormValid = noteType && noteType.fields.length > 0 && noteType.fields.every((field) => fieldValues[field.id]?.trim()); const isLoading = isLoadingNote || isLoadingNoteType; return (
{ if (e.target === e.currentTarget) { handleClose(); } }} onKeyDown={(e) => { if (e.key === "Escape") { handleClose(); } }} >

Edit Note

{error && (
{error}
)} {/* Loading indicator */} {isLoading && (
)} {/* Note Type Display (read-only) */} {noteType && !isLoading && (
Note Type
{noteType.name} {noteType.isReversible ? " (reversed)" : ""}
)} {/* Dynamic Field Inputs */} {noteType && !isLoading && (noteType.fields.length === 0 ? (
This note type has no fields.
) : ( noteType.fields .sort((a, b) => a.order - b.order) .map((field) => (