aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/pages
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-01-01 22:06:40 +0900
committernsfisis <nsfisis@gmail.com>2026-01-01 22:06:40 +0900
commit8dbe5c2a1d8dc15bbdd6810b2582c680e1c0bb9b (patch)
treebe39e1436f83c716fc45df133106fba7dd4bc23a /src/client/pages
parent830b370f1b8e0f3a384b2d242ab120812e81977d (diff)
downloadkioku-8dbe5c2a1d8dc15bbdd6810b2582c680e1c0bb9b.tar.gz
kioku-8dbe5c2a1d8dc15bbdd6810b2582c680e1c0bb9b.tar.zst
kioku-8dbe5c2a1d8dc15bbdd6810b2582c680e1c0bb9b.zip
feat(import): add CSV bulk import for notes
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>
Diffstat (limited to 'src/client/pages')
-rw-r--r--src/client/pages/DeckDetailPage.tsx50
1 files changed, 38 insertions, 12 deletions
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 */}
@@ -472,6 +489,15 @@ export function DeckDetailPage() {
)}
{deckId && (
+ <ImportNotesModal
+ isOpen={isImportModalOpen}
+ deckId={deckId}
+ onClose={() => setIsImportModalOpen(false)}
+ onImportComplete={fetchCards}
+ />
+ )}
+
+ {deckId && (
<EditCardModal
isOpen={editingCard !== null}
deckId={deckId}