diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-01-01 22:06:40 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-01-01 22:06:40 +0900 |
| commit | 8dbe5c2a1d8dc15bbdd6810b2582c680e1c0bb9b (patch) | |
| tree | be39e1436f83c716fc45df133106fba7dd4bc23a /src/client/pages | |
| parent | 830b370f1b8e0f3a384b2d242ab120812e81977d (diff) | |
| download | kioku-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.tsx | 50 |
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} |
