diff options
Diffstat (limited to 'src/client/pages')
| -rw-r--r-- | src/client/pages/DeckDetailPage.test.tsx | 73 | ||||
| -rw-r--r-- | src/client/pages/DeckDetailPage.tsx | 156 | ||||
| -rw-r--r-- | src/client/pages/StudyPage.tsx | 8 |
3 files changed, 46 insertions, 191 deletions
diff --git a/src/client/pages/DeckDetailPage.test.tsx b/src/client/pages/DeckDetailPage.test.tsx index 35303d9..e02302f 100644 --- a/src/client/pages/DeckDetailPage.test.tsx +++ b/src/client/pages/DeckDetailPage.test.tsx @@ -51,13 +51,13 @@ const mockDeck = { updatedAt: "2024-01-01T00:00:00Z", }; -// Legacy cards (no noteId) for backward compatibility testing -const mockLegacyCards = [ +// Basic note-based cards (each with its own note) +const mockBasicCards = [ { id: "card-1", deckId: "deck-1", - noteId: null, - isReversed: null, + noteId: "note-1", + isReversed: false, front: "Hello", back: "こんにちは", state: 0, @@ -77,8 +77,8 @@ const mockLegacyCards = [ { id: "card-2", deckId: "deck-1", - noteId: null, - isReversed: null, + noteId: "note-2", + isReversed: false, front: "Goodbye", back: "さようなら", state: 2, @@ -143,11 +143,8 @@ const mockNoteBasedCards = [ }, ]; -// Mixed cards (both legacy and note-based) -const mockMixedCards = [...mockLegacyCards, ...mockNoteBasedCards]; - -// Alias for backward compatibility in existing tests -const mockCards = mockLegacyCards; +// Alias for existing tests +const mockCards = mockBasicCards; function renderWithProviders(path = "/decks/deck-1") { const { hook } = memoryLocation({ path, static: true }); @@ -430,8 +427,8 @@ describe("DeckDetailPage", () => { expect(screen.queryByText("Common Japanese words")).toBeNull(); }); - describe("Delete Card", () => { - it("shows Delete button for each card", async () => { + describe("Delete Note", () => { + it("shows Delete button for each note", async () => { mockFetch .mockResolvedValueOnce({ ok: true, @@ -449,7 +446,7 @@ describe("DeckDetailPage", () => { }); const deleteButtons = screen.getAllByRole("button", { - name: "Delete card", + name: "Delete note", }); expect(deleteButtons.length).toBe(2); }); @@ -474,7 +471,7 @@ describe("DeckDetailPage", () => { }); const deleteButtons = screen.getAllByRole("button", { - name: "Delete card", + name: "Delete note", }); const firstDeleteButton = deleteButtons[0]; if (firstDeleteButton) { @@ -483,7 +480,7 @@ describe("DeckDetailPage", () => { expect(screen.getByRole("dialog")).toBeDefined(); expect( - screen.getByRole("heading", { name: "Delete Card" }), + screen.getByRole("heading", { name: "Delete Note" }), ).toBeDefined(); }); @@ -507,7 +504,7 @@ describe("DeckDetailPage", () => { }); const deleteButtons = screen.getAllByRole("button", { - name: "Delete card", + name: "Delete note", }); const firstDeleteButton = deleteButtons[0]; if (firstDeleteButton) { @@ -521,7 +518,7 @@ describe("DeckDetailPage", () => { expect(screen.queryByRole("dialog")).toBeNull(); }); - it("deletes card and refreshes list on confirmation", async () => { + it("deletes note and refreshes list on confirmation", async () => { const user = userEvent.setup(); mockFetch @@ -537,7 +534,7 @@ describe("DeckDetailPage", () => { // Delete request .mockResolvedValueOnce({ ok: true, - json: async () => ({}), + json: async () => ({ success: true }), }) // Refresh cards after deletion .mockResolvedValueOnce({ @@ -552,7 +549,7 @@ describe("DeckDetailPage", () => { }); const deleteButtons = screen.getAllByRole("button", { - name: "Delete card", + name: "Delete note", }); const firstDeleteButton = deleteButtons[0]; if (firstDeleteButton) { @@ -575,8 +572,8 @@ describe("DeckDetailPage", () => { expect(screen.queryByRole("dialog")).toBeNull(); }); - // Verify DELETE request was made - expect(mockFetch).toHaveBeenCalledWith("/api/decks/deck-1/cards/card-1", { + // Verify DELETE request was made to notes endpoint + expect(mockFetch).toHaveBeenCalledWith("/api/decks/deck-1/notes/note-1", { method: "DELETE", headers: { Authorization: "Bearer access-token" }, }); @@ -604,7 +601,7 @@ describe("DeckDetailPage", () => { .mockResolvedValueOnce({ ok: false, status: 500, - json: async () => ({ error: "Failed to delete card" }), + json: async () => ({ error: "Failed to delete note" }), }); renderWithProviders(); @@ -614,7 +611,7 @@ describe("DeckDetailPage", () => { }); const deleteButtons = screen.getAllByRole("button", { - name: "Delete card", + name: "Delete note", }); const firstDeleteButton = deleteButtons[0]; if (firstDeleteButton) { @@ -635,7 +632,7 @@ describe("DeckDetailPage", () => { // Error should be displayed in the modal await waitFor(() => { expect(screen.getByRole("alert").textContent).toContain( - "Failed to delete card", + "Failed to delete note", ); }); }); @@ -665,32 +662,6 @@ describe("DeckDetailPage", () => { expect(noteCards.length).toBe(2); }); - it("displays legacy cards separately from note groups", async () => { - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: async () => ({ deck: mockDeck }), - }) - .mockResolvedValueOnce({ - ok: true, - json: async () => ({ cards: mockMixedCards }), - }); - - renderWithProviders(); - - await waitFor(() => { - // Should show both note groups and legacy cards - expect(screen.getByTestId("note-group")).toBeDefined(); - }); - - const legacyCards = screen.getAllByTestId("legacy-card"); - expect(legacyCards.length).toBe(2); // 2 legacy cards - - // Should show "Legacy" badge for legacy cards - const legacyBadges = screen.getAllByText("Legacy"); - expect(legacyBadges.length).toBe(2); - }); - it("shows Normal and Reversed badges for note-based cards", async () => { mockFetch .mockResolvedValueOnce({ diff --git a/src/client/pages/DeckDetailPage.tsx b/src/client/pages/DeckDetailPage.tsx index 87f9dc3..d018d1f 100644 --- a/src/client/pages/DeckDetailPage.tsx +++ b/src/client/pages/DeckDetailPage.tsx @@ -21,8 +21,8 @@ import { EditNoteModal } from "../components/EditNoteModal"; interface Card { id: string; deckId: string; - noteId: string | null; - isReversed: boolean | null; + noteId: string; + isReversed: boolean; front: string; back: string; state: number; @@ -33,10 +33,8 @@ interface Card { updatedAt: string; } -/** Combined type for display: either a note group or a legacy card */ -type CardDisplayItem = - | { type: "note"; noteId: string; cards: Card[] } - | { type: "legacy"; card: Card }; +/** Combined type for display: note group */ +type CardDisplayItem = { type: "note"; noteId: string; cards: Card[] }; interface Deck { id: string; @@ -178,95 +176,6 @@ function NoteGroupCard({ ); } -/** Component for displaying a legacy card (without note association) */ -function LegacyCardItem({ - card, - index, - onEdit, - onDelete, -}: { - card: Card; - index: number; - onEdit: () => void; - onDelete: () => void; -}) { - return ( - <div - data-testid="legacy-card" - className="bg-white rounded-xl border border-border/50 p-5 shadow-card hover:shadow-md transition-all duration-200" - style={{ animationDelay: `${index * 30}ms` }} - > - <div className="flex items-start justify-between gap-4"> - <div className="flex-1 min-w-0"> - {/* Front/Back Preview */} - <div className="grid grid-cols-2 gap-4 mb-3"> - <div> - <span className="text-xs font-medium text-muted uppercase tracking-wide"> - Front - </span> - <p className="mt-1 text-slate text-sm line-clamp-2 whitespace-pre-wrap break-words"> - {card.front} - </p> - </div> - <div> - <span className="text-xs font-medium text-muted uppercase tracking-wide"> - Back - </span> - <p className="mt-1 text-slate text-sm line-clamp-2 whitespace-pre-wrap break-words"> - {card.back} - </p> - </div> - </div> - - {/* Card Stats */} - <div className="flex items-center gap-3 text-xs"> - <span - className={`px-2 py-0.5 rounded-full font-medium ${CardStateColors[card.state] || "bg-muted/10 text-muted"}`} - > - {CardStateLabels[card.state] || "Unknown"} - </span> - <span className="px-2 py-0.5 rounded-full font-medium bg-amber-100 text-amber-700"> - Legacy - </span> - <span className="text-muted">{card.reps} reviews</span> - {card.lapses > 0 && ( - <span className="text-muted">{card.lapses} lapses</span> - )} - </div> - </div> - - {/* Actions */} - <div className="flex items-center gap-1 shrink-0"> - <button - type="button" - onClick={onEdit} - className="p-2 text-muted hover:text-slate hover:bg-ivory rounded-lg transition-colors" - title="Edit card" - > - <FontAwesomeIcon - icon={faPen} - className="w-4 h-4" - aria-hidden="true" - /> - </button> - <button - type="button" - onClick={onDelete} - className="p-2 text-muted hover:text-error hover:bg-error/5 rounded-lg transition-colors" - title="Delete card" - > - <FontAwesomeIcon - icon={faTrash} - className="w-4 h-4" - aria-hidden="true" - /> - </button> - </div> - </div> - </div> - ); -} - export function DeckDetailPage() { const { deckId } = useParams<{ deckId: string }>(); const [deck, setDeck] = useState<Deck | null>(null); @@ -282,24 +191,17 @@ export function DeckDetailPage() { // Group cards by note for display const displayItems = useMemo((): CardDisplayItem[] => { const noteGroups = new Map<string, Card[]>(); - const legacyCards: Card[] = []; for (const card of cards) { - if (card.noteId) { - const existing = noteGroups.get(card.noteId); - if (existing) { - existing.push(card); - } else { - noteGroups.set(card.noteId, [card]); - } + const existing = noteGroups.get(card.noteId); + if (existing) { + existing.push(card); } else { - legacyCards.push(card); + noteGroups.set(card.noteId, [card]); } } - const items: CardDisplayItem[] = []; - - // Add note groups first, sorted by earliest card creation + // Sort note groups by earliest card creation (newest first) const sortedNoteGroups = Array.from(noteGroups.entries()).sort( ([, cardsA], [, cardsB]) => { const minA = Math.min( @@ -312,6 +214,7 @@ export function DeckDetailPage() { }, ); + const items: CardDisplayItem[] = []; for (const [noteId, noteCards] of sortedNoteGroups) { // Sort cards within group: normal first, then reversed noteCards.sort((a, b) => { @@ -321,15 +224,6 @@ export function DeckDetailPage() { items.push({ type: "note", noteId, cards: noteCards }); } - // Add legacy cards, newest first - legacyCards.sort( - (a, b) => - new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), - ); - for (const card of legacyCards) { - items.push({ type: "legacy", card }); - } - return items; }, [cards]); @@ -551,26 +445,16 @@ export function DeckDetailPage() { {/* Card List - Grouped by Note */} {cards.length > 0 && ( <div className="space-y-4"> - {displayItems.map((item, index) => - item.type === "note" ? ( - <NoteGroupCard - key={item.noteId} - noteId={item.noteId} - cards={item.cards} - index={index} - onEditNote={() => setEditingNoteId(item.noteId)} - onDeleteNote={() => setDeletingNoteId(item.noteId)} - /> - ) : ( - <LegacyCardItem - key={item.card.id} - card={item.card} - index={index} - onEdit={() => setEditingCard(item.card)} - onDelete={() => setDeletingCard(item.card)} - /> - ), - )} + {displayItems.map((item, index) => ( + <NoteGroupCard + key={item.noteId} + noteId={item.noteId} + cards={item.cards} + index={index} + onEditNote={() => setEditingNoteId(item.noteId)} + onDeleteNote={() => setDeletingNoteId(item.noteId)} + /> + ))} </div> )} </div> diff --git a/src/client/pages/StudyPage.tsx b/src/client/pages/StudyPage.tsx index bdaf7e3..0eb5118 100644 --- a/src/client/pages/StudyPage.tsx +++ b/src/client/pages/StudyPage.tsx @@ -13,8 +13,8 @@ import { renderCard } from "../utils/templateRenderer"; interface Card { id: string; deckId: string; - noteId: string | null; - isReversed: boolean | null; + noteId: string; + isReversed: boolean; front: string; back: string; state: number; @@ -23,11 +23,11 @@ interface Card { difficulty: number; reps: number; lapses: number; - /** Note type templates for rendering (null for legacy cards) */ + /** Note type templates for rendering */ noteType: { frontTemplate: string; backTemplate: string; - } | null; + }; /** Field values as a name-value map for template rendering */ fieldValuesMap: Record<string, string>; } |
