From deef992b8cc7e57b880c1c38f994d38825240ca1 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 7 Dec 2025 18:25:40 +0900 Subject: feat(client): add create card modal with form validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add CreateCardModal component to allow users to create new flashcards with front and back text fields. Integrate the modal into DeckDetailPage with an "Add Card" button. Include comprehensive unit tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/client/components/CreateCardModal.tsx | 194 ++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 src/client/components/CreateCardModal.tsx (limited to 'src/client/components/CreateCardModal.tsx') diff --git a/src/client/components/CreateCardModal.tsx b/src/client/components/CreateCardModal.tsx new file mode 100644 index 0000000..c28cf0f --- /dev/null +++ b/src/client/components/CreateCardModal.tsx @@ -0,0 +1,194 @@ +import { type FormEvent, useState } from "react"; +import { ApiClientError, apiClient } from "../api"; + +interface CreateCardModalProps { + isOpen: boolean; + deckId: string; + onClose: () => void; + onCardCreated: () => void; +} + +export function CreateCardModal({ + isOpen, + deckId, + onClose, + onCardCreated, +}: CreateCardModalProps) { + const [front, setFront] = useState(""); + const [back, setBack] = useState(""); + const [error, setError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const resetForm = () => { + setFront(""); + setBack(""); + setError(null); + }; + + const handleClose = () => { + resetForm(); + onClose(); + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setError(null); + setIsSubmitting(true); + + try { + const res = await apiClient.rpc.api.decks[":deckId"].cards.$post( + { + param: { deckId }, + json: { + front: front.trim(), + back: back.trim(), + }, + }, + { + headers: apiClient.getAuthHeader(), + }, + ); + + 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(); + onCardCreated(); + onClose(); + } catch (err) { + if (err instanceof ApiClientError) { + setError(err.message); + } else { + setError("Failed to create card. Please try again."); + } + } finally { + setIsSubmitting(false); + } + }; + + if (!isOpen) { + return null; + } + + const isFormValid = front.trim() && back.trim(); + + return ( +
{ + if (e.target === e.currentTarget) { + handleClose(); + } + }} + onKeyDown={(e) => { + if (e.key === "Escape") { + handleClose(); + } + }} + > +
+

+ Create New Card +

+ +
+ {error && ( +
+ {error} +
+ )} + +
+ +