From b51d4efaa1e5e0417d4306c02797f424938766cb Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Wed, 31 Dec 2025 02:38:05 +0900
Subject: feat(client): add NoteTypesPage for note type management
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Implement Phase 6 of the roadmap - NoteType list page with CRUD modals:
- NoteTypesPage displays all user's note types with templates and reversible badge
- CreateNoteTypeModal for creating new note types with templates
- EditNoteTypeModal for updating existing note types
- DeleteNoteTypeModal with constraint warning
- Navigation link from HomePage header
- Comprehensive tests for all components (65 new tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 src/client/components/DeleteNoteTypeModal.tsx | 156 ++++++++++++++++++++++++++
 1 file changed, 156 insertions(+)
 create mode 100644 src/client/components/DeleteNoteTypeModal.tsx

(limited to 'src/client/components/DeleteNoteTypeModal.tsx')

diff --git a/src/client/components/DeleteNoteTypeModal.tsx b/src/client/components/DeleteNoteTypeModal.tsx
new file mode 100644
index 0000000..bd6b4a5
--- /dev/null
+++ b/src/client/components/DeleteNoteTypeModal.tsx
@@ -0,0 +1,156 @@
+import { useState } from "react";
+import { ApiClientError, apiClient } from "../api";
+
+interface NoteType {
+	id: string;
+	name: string;
+}
+
+interface DeleteNoteTypeModalProps {
+	isOpen: boolean;
+	noteType: NoteType | null;
+	onClose: () => void;
+	onNoteTypeDeleted: () => void;
+}
+
+export function DeleteNoteTypeModal({
+	isOpen,
+	noteType,
+	onClose,
+	onNoteTypeDeleted,
+}: DeleteNoteTypeModalProps) {
+	const [error, setError] = useState<string | null>(null);
+	const [isDeleting, setIsDeleting] = useState(false);
+
+	const handleClose = () => {
+		setError(null);
+		onClose();
+	};
+
+	const handleDelete = async () => {
+		if (!noteType) return;
+
+		setError(null);
+		setIsDeleting(true);
+
+		try {
+			const authHeader = apiClient.getAuthHeader();
+			if (!authHeader) {
+				throw new ApiClientError("Not authenticated", 401);
+			}
+
+			const res = await fetch(`/api/note-types/${noteType.id}`, {
+				method: "DELETE",
+				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,
+				);
+			}
+
+			onNoteTypeDeleted();
+			onClose();
+		} catch (err) {
+			if (err instanceof ApiClientError) {
+				setError(err.message);
+			} else {
+				setError("Failed to delete note type. Please try again.");
+			}
+		} finally {
+			setIsDeleting(false);
+		}
+	};
+
+	if (!isOpen || !noteType) {
+		return null;
+	}
+
+	return (
+		<div
+			role="dialog"
+			aria-modal="true"
+			aria-labelledby="delete-note-type-title"
+			className="fixed inset-0 bg-ink/40 backdrop-blur-sm flex items-center justify-center z-50 p-4 animate-fade-in"
+			onClick={(e) => {
+				if (e.target === e.currentTarget) {
+					handleClose();
+				}
+			}}
+			onKeyDown={(e) => {
+				if (e.key === "Escape") {
+					handleClose();
+				}
+			}}
+		>
+			<div className="bg-white rounded-2xl shadow-xl w-full max-w-md animate-scale-in">
+				<div className="p-6">
+					<div className="w-12 h-12 mx-auto mb-4 bg-error/10 rounded-full flex items-center justify-center">
+						<svg
+							className="w-6 h-6 text-error"
+							fill="none"
+							stroke="currentColor"
+							viewBox="0 0 24 24"
+							aria-hidden="true"
+						>
+							<path
+								strokeLinecap="round"
+								strokeLinejoin="round"
+								strokeWidth={2}
+								d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
+							/>
+						</svg>
+					</div>
+
+					<h2
+						id="delete-note-type-title"
+						className="font-display text-xl font-medium text-ink text-center mb-2"
+					>
+						Delete Note Type
+					</h2>
+
+					{error && (
+						<div
+							role="alert"
+							className="bg-error/5 text-error text-sm px-4 py-3 rounded-lg border border-error/20 mb-4"
+						>
+							{error}
+						</div>
+					)}
+
+					<p className="text-slate text-center mb-2">
+						Are you sure you want to delete{" "}
+						<span className="font-semibold">{noteType.name}</span>?
+					</p>
+					<p className="text-muted text-sm text-center mb-6">
+						Note types with existing notes cannot be deleted. Remove all notes
+						using this type first.
+					</p>
+
+					<div className="flex gap-3 justify-center">
+						<button
+							type="button"
+							onClick={handleClose}
+							disabled={isDeleting}
+							className="px-4 py-2 text-slate hover:bg-ivory rounded-lg transition-colors disabled:opacity-50 min-w-[100px]"
+						>
+							Cancel
+						</button>
+						<button
+							type="button"
+							onClick={handleDelete}
+							disabled={isDeleting}
+							className="px-4 py-2 bg-error hover:bg-error/90 text-white font-medium rounded-lg transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed min-w-[100px]"
+						>
+							{isDeleting ? "Deleting..." : "Delete"}
+						</button>
+					</div>
+				</div>
+			</div>
+		</div>
+	);
+}
-- 
cgit v1.2.3-70-g09d2

