import { faChevronDown, faChevronUp, faGripVertical, faPlus, faTrash, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { type FormEvent, useCallback, useEffect, useRef, useState, } from "react"; import { ApiClientError, apiClient } from "../api"; interface NoteFieldType { id: string; noteTypeId: string; name: string; order: number; fieldType: string; } interface NoteType { id: string; name: string; frontTemplate: string; backTemplate: string; isReversible: boolean; } interface NoteTypeWithFields extends NoteType { fields: NoteFieldType[]; } interface NoteTypeEditorProps { isOpen: boolean; noteTypeId: string | null; onClose: () => void; onNoteTypeUpdated: () => void; } export function NoteTypeEditor({ isOpen, noteTypeId, onClose, onNoteTypeUpdated, }: NoteTypeEditorProps) { const [noteType, setNoteType] = useState(null); const [name, setName] = useState(""); const [frontTemplate, setFrontTemplate] = useState(""); const [backTemplate, setBackTemplate] = useState(""); const [isReversible, setIsReversible] = useState(false); const [fields, setFields] = useState([]); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [newFieldName, setNewFieldName] = useState(""); const [isAddingField, setIsAddingField] = useState(false); const [editingFieldId, setEditingFieldId] = useState(null); const [editingFieldName, setEditingFieldName] = useState(""); const [fieldError, setFieldError] = useState(null); const editInputRef = useRef(null); const fetchNoteType = useCallback(async () => { if (!noteTypeId) return; setIsLoading(true); setError(null); try { const res = await apiClient.rpc.api["note-types"][":id"].$get({ param: { id: noteTypeId }, }); const data = await apiClient.handleResponse<{ noteType: NoteTypeWithFields; }>(res); const fetchedNoteType = data.noteType; setNoteType(fetchedNoteType); setName(fetchedNoteType.name); setFrontTemplate(fetchedNoteType.frontTemplate); setBackTemplate(fetchedNoteType.backTemplate); setIsReversible(fetchedNoteType.isReversible); setFields(fetchedNoteType.fields.sort((a, b) => a.order - b.order) || []); } catch (err) { if (err instanceof ApiClientError) { setError(err.message); } else { setError("Failed to load note type. Please try again."); } } finally { setIsLoading(false); } }, [noteTypeId]); useEffect(() => { if (isOpen && noteTypeId) { fetchNoteType(); } }, [isOpen, noteTypeId, fetchNoteType]); useEffect(() => { if (editingFieldId && editInputRef.current) { editInputRef.current.focus(); } }, [editingFieldId]); const handleClose = () => { setError(null); setFieldError(null); setNewFieldName(""); setIsAddingField(false); setEditingFieldId(null); setNoteType(null); onClose(); }; const handleSubmit = async (e: FormEvent) => { e.preventDefault(); if (!noteType) return; setError(null); setIsSubmitting(true); try { const res = await apiClient.rpc.api["note-types"][":id"].$put({ param: { id: noteType.id }, json: { name: name.trim(), frontTemplate: frontTemplate.trim(), backTemplate: backTemplate.trim(), isReversible, }, }); await apiClient.handleResponse(res); onNoteTypeUpdated(); onClose(); } catch (err) { if (err instanceof ApiClientError) { setError(err.message); } else { setError("Failed to update note type. Please try again."); } } finally { setIsSubmitting(false); } }; const handleAddField = async () => { if (!noteType || !newFieldName.trim()) return; setFieldError(null); setIsAddingField(true); try { const newOrder = fields.length > 0 ? Math.max(...fields.map((f) => f.order)) + 1 : 0; const res = await apiClient.rpc.api["note-types"][":id"].fields.$post({ param: { id: noteType.id }, json: { name: newFieldName.trim(), order: newOrder, fieldType: "text", }, }); const data = await apiClient.handleResponse<{ field: NoteFieldType }>( res, ); setFields([...fields, data.field]); setNewFieldName(""); } catch (err) { if (err instanceof ApiClientError) { setFieldError(err.message); } else { setFieldError("Failed to add field. Please try again."); } } finally { setIsAddingField(false); } }; const handleUpdateFieldName = async (fieldId: string) => { if (!noteType || !editingFieldName.trim()) return; setFieldError(null); try { const res = await apiClient.rpc.api["note-types"][":id"].fields[ ":fieldId" ].$put({ param: { id: noteType.id, fieldId }, json: { name: editingFieldName.trim(), }, }); const data = await apiClient.handleResponse<{ field: NoteFieldType }>( res, ); setFields(fields.map((f) => (f.id === fieldId ? data.field : f))); setEditingFieldId(null); setEditingFieldName(""); } catch (err) { if (err instanceof ApiClientError) { setFieldError(err.message); } else { setFieldError("Failed to update field. Please try again."); } } }; const handleDeleteField = async (fieldId: string) => { if (!noteType) return; setFieldError(null); try { const res = await apiClient.rpc.api["note-types"][":id"].fields[ ":fieldId" ].$delete({ param: { id: noteType.id, fieldId }, }); await apiClient.handleResponse(res); setFields(fields.filter((f) => f.id !== fieldId)); } catch (err) { if (err instanceof ApiClientError) { setFieldError(err.message); } else { setFieldError("Failed to delete field. Please try again."); } } }; const handleMoveField = async (fieldId: string, direction: "up" | "down") => { if (!noteType) return; const fieldIndex = fields.findIndex((f) => f.id === fieldId); if (fieldIndex === -1) return; const newIndex = direction === "up" ? fieldIndex - 1 : fieldIndex + 1; if (newIndex < 0 || newIndex >= fields.length) return; const newFields = [...fields]; const temp = newFields[fieldIndex]; newFields[fieldIndex] = newFields[newIndex] as NoteFieldType; newFields[newIndex] = temp as NoteFieldType; const fieldIds = newFields.map((f) => f.id); setFieldError(null); try { const res = await apiClient.rpc.api["note-types"][ ":id" ].fields.reorder.$put({ param: { id: noteType.id }, json: { fieldIds }, }); const data = await apiClient.handleResponse<{ fields: NoteFieldType[] }>( res, ); setFields( data.fields.sort( (a: NoteFieldType, b: NoteFieldType) => a.order - b.order, ), ); } catch (err) { if (err instanceof ApiClientError) { setFieldError(err.message); } else { setFieldError("Failed to reorder fields. Please try again."); } } }; const startEditingField = (field: NoteFieldType) => { setEditingFieldId(field.id); setEditingFieldName(field.name); }; const cancelEditingField = () => { setEditingFieldId(null); setEditingFieldName(""); }; if (!isOpen) { return null; } return (
{ if (e.target === e.currentTarget) { handleClose(); } }} onKeyDown={(e) => { if (e.key === "Escape") { handleClose(); } }} >

Edit Note Type

{isLoading && (
)} {error && !isLoading && (
{error}
)} {noteType && !isLoading && (
{/* Basic Info Section */}
setName(e.target.value)} required maxLength={255} disabled={isSubmitting} className="w-full px-4 py-2.5 bg-ivory border border-border rounded-lg text-slate placeholder-muted transition-all duration-200 hover:border-muted focus:border-primary focus:ring-2 focus:ring-primary/10 disabled:opacity-50 disabled:cursor-not-allowed" />
setIsReversible(e.target.checked)} disabled={isSubmitting} className="w-4 h-4 text-primary bg-ivory border-border rounded focus:ring-primary/20 focus:ring-2 disabled:opacity-50" />

Only affects new notes; existing cards are not modified

{/* Fields Section */}

Fields

{fieldError && (
{fieldError}
)}
{fields.map((field, index) => (
))}
{/* Add Field */}
setNewFieldName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); handleAddField(); } }} placeholder="New field name" disabled={isAddingField} className="flex-1 px-3 py-2 bg-white border border-border rounded-lg text-sm text-slate placeholder-muted transition-all duration-200 hover:border-muted focus:border-primary focus:ring-2 focus:ring-primary/10 disabled:opacity-50" />
{/* Templates Section */}

Templates

Use {"{{FieldName}}"} to insert field values. Available fields:{" "} {fields.length > 0 ? fields.map((f) => `{{${f.name}}}`).join(", ") : "(no fields yet)"}