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 NoteTypeSummary { id: string; name: string; isReversible: boolean; } interface CreateNoteModalProps { isOpen: boolean; deckId: string; onClose: () => void; onNoteCreated: () => void; } export function CreateNoteModal({ isOpen, deckId, onClose, onNoteCreated, }: CreateNoteModalProps) { const [noteTypes, setNoteTypes] = useState([]); const [selectedNoteType, setSelectedNoteType] = useState( null, ); const [fieldValues, setFieldValues] = useState>({}); const [error, setError] = useState(null); const [isLoadingNoteTypes, setIsLoadingNoteTypes] = useState(false); const [isLoadingNoteType, setIsLoadingNoteType] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [hasLoadedNoteTypes, setHasLoadedNoteTypes] = 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(); setSelectedNoteType(data.noteType); // Initialize field values for the new note type const initialValues: Record = {}; for (const field of data.noteType.fields) { initialValues[field.id] = ""; } setFieldValues(initialValues); } catch (err) { if (err instanceof ApiClientError) { setError(err.message); } else { setError("Failed to load note type details. Please try again."); } } finally { setIsLoadingNoteType(false); } }, []); const fetchNoteTypes = useCallback(async () => { setIsLoadingNoteTypes(true); setError(null); try { const authHeader = apiClient.getAuthHeader(); if (!authHeader) { throw new ApiClientError("Not authenticated", 401); } const res = await fetch("/api/note-types", { 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(); setNoteTypes(data.noteTypes); setHasLoadedNoteTypes(true); // Auto-select first note type if available if (data.noteTypes.length > 0) { await fetchNoteTypeDetails(data.noteTypes[0].id); } } catch (err) { if (err instanceof ApiClientError) { setError(err.message); } else { setError("Failed to load note types. Please try again."); } } finally { setIsLoadingNoteTypes(false); } }, [fetchNoteTypeDetails]); useEffect(() => { if (isOpen && !hasLoadedNoteTypes) { fetchNoteTypes(); } }, [isOpen, hasLoadedNoteTypes, fetchNoteTypes]); const resetForm = () => { // Reset field values to empty for current note type if (selectedNoteType) { const initialValues: Record = {}; for (const field of selectedNoteType.fields) { initialValues[field.id] = ""; } setFieldValues(initialValues); } else { setFieldValues({}); } setError(null); }; const handleClose = () => { resetForm(); onClose(); }; const handleNoteTypeChange = async (noteTypeId: string) => { if (noteTypeId !== selectedNoteType?.id) { await fetchNoteTypeDetails(noteTypeId); } }; const handleFieldChange = (fieldId: string, value: string) => { setFieldValues((prev) => ({ ...prev, [fieldId]: value, })); }; const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setError(null); if (!selectedNoteType) { setError("Please select a note type."); 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`, { method: "POST", headers: { "Content-Type": "application/json", ...authHeader, }, body: JSON.stringify({ noteTypeId: selectedNoteType.id, 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, ); } resetForm(); onNoteCreated(); onClose(); } catch (err) { if (err instanceof ApiClientError) { setError(err.message); } else { setError("Failed to create note. Please try again."); } } finally { setIsSubmitting(false); } }; if (!isOpen) { return null; } // Check if all required fields have values const isFormValid = selectedNoteType && selectedNoteType.fields.length > 0 && selectedNoteType.fields.every((field) => fieldValues[field.id]?.trim()); const isLoading = isLoadingNoteTypes || isLoadingNoteType; return (
{ if (e.target === e.currentTarget) { handleClose(); } }} onKeyDown={(e) => { if (e.key === "Escape") { handleClose(); } }} >

Create New Note

{error && (
{error}
)} {/* Note Type Selector */}
{isLoadingNoteTypes ? (
) : noteTypes.length === 0 ? (
No note types available. Please create a note type first.
) : ( )}
{/* Loading indicator for note type details */} {isLoadingNoteType && (
)} {/* Dynamic Field Inputs */} {selectedNoteType && !isLoadingNoteType && ( <> {selectedNoteType.fields.length === 0 ? (
This note type has no fields. Please add fields to the note type first.
) : ( selectedNoteType.fields .sort((a, b) => a.order - b.order) .map((field) => (