From 8dbe5c2a1d8dc15bbdd6810b2582c680e1c0bb9b Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Thu, 1 Jan 2026 22:06:40 +0900
Subject: feat(import): add CSV bulk import for notes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Add client-side CSV parsing and bulk import API endpoint for importing
notes from CSV files. Supports quoted fields, newlines in values, and
escaped quotes.

- New POST /api/decks/{deckId}/notes/import endpoint for bulk creation
- CSV parser with RFC 4180 compliance
- Multi-phase import modal (upload → validate → preview → import)
- Client-side validation with per-row error reporting

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 src/client/pages/DeckDetailPage.tsx | 50 ++++++++++++++++++++++++++++---------
 1 file changed, 38 insertions(+), 12 deletions(-)

(limited to 'src/client/pages/DeckDetailPage.tsx')

diff --git a/src/client/pages/DeckDetailPage.tsx b/src/client/pages/DeckDetailPage.tsx
index d018d1f..3741111 100644
--- a/src/client/pages/DeckDetailPage.tsx
+++ b/src/client/pages/DeckDetailPage.tsx
@@ -2,6 +2,7 @@ import {
 	faChevronLeft,
 	faCirclePlay,
 	faFile,
+	faFileImport,
 	faLayerGroup,
 	faPen,
 	faPlus,
@@ -17,6 +18,7 @@ import { DeleteCardModal } from "../components/DeleteCardModal";
 import { DeleteNoteModal } from "../components/DeleteNoteModal";
 import { EditCardModal } from "../components/EditCardModal";
 import { EditNoteModal } from "../components/EditNoteModal";
+import { ImportNotesModal } from "../components/ImportNotesModal";
 
 interface Card {
 	id: string;
@@ -183,6 +185,7 @@ export function DeckDetailPage() {
 	const [isLoading, setIsLoading] = useState(true);
 	const [error, setError] = useState<string | null>(null);
 	const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
+	const [isImportModalOpen, setIsImportModalOpen] = useState(false);
 	const [editingCard, setEditingCard] = useState<Card | null>(null);
 	const [editingNoteId, setEditingNoteId] = useState<string | null>(null);
 	const [deletingCard, setDeletingCard] = useState<Card | null>(null);
@@ -397,18 +400,32 @@ export function DeckDetailPage() {
 								Cards{" "}
 								<span className="text-muted font-normal">({cards.length})</span>
 							</h2>
-							<button
-								type="button"
-								onClick={() => setIsCreateModalOpen(true)}
-								className="inline-flex items-center gap-2 bg-primary hover:bg-primary-dark text-white font-medium py-2 px-4 rounded-lg transition-all duration-200 active:scale-[0.98]"
-							>
-								<FontAwesomeIcon
-									icon={faPlus}
-									className="w-5 h-5"
-									aria-hidden="true"
-								/>
-								Add Note
-							</button>
+							<div className="flex items-center gap-2">
+								<button
+									type="button"
+									onClick={() => setIsImportModalOpen(true)}
+									className="inline-flex items-center gap-2 border border-border hover:bg-ivory text-slate font-medium py-2 px-4 rounded-lg transition-all duration-200 active:scale-[0.98]"
+								>
+									<FontAwesomeIcon
+										icon={faFileImport}
+										className="w-5 h-5"
+										aria-hidden="true"
+									/>
+									Import CSV
+								</button>
+								<button
+									type="button"
+									onClick={() => setIsCreateModalOpen(true)}
+									className="inline-flex items-center gap-2 bg-primary hover:bg-primary-dark text-white font-medium py-2 px-4 rounded-lg transition-all duration-200 active:scale-[0.98]"
+								>
+									<FontAwesomeIcon
+										icon={faPlus}
+										className="w-5 h-5"
+										aria-hidden="true"
+									/>
+									Add Note
+								</button>
+							</div>
 						</div>
 
 						{/* Empty State */}
@@ -471,6 +488,15 @@ export function DeckDetailPage() {
 				/>
 			)}
 
+			{deckId && (
+				<ImportNotesModal
+					isOpen={isImportModalOpen}
+					deckId={deckId}
+					onClose={() => setIsImportModalOpen(false)}
+					onImportComplete={fetchCards}
+				/>
+			)}
+
 			{deckId && (
 				<EditCardModal
 					isOpen={editingCard !== null}
-- 
cgit v1.2.3-70-g09d2

