aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/pages
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/pages')
-rw-r--r--src/client/pages/DeckDetailPage.test.tsx73
-rw-r--r--src/client/pages/DeckDetailPage.tsx156
-rw-r--r--src/client/pages/StudyPage.tsx8
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>;
}