aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-15 16:45:35 +0900
committernsfisis <nsfisis@gmail.com>2026-02-15 16:45:35 +0900
commitcea7703ac440c38636ce2abd5baab35a23d05843 (patch)
tree38de7bdf13a6b4b006319ee7800206eab9e35215 /src
parent65fcf4111bc64267624b4e348bb704d006de5327 (diff)
downloadkioku-cea7703ac440c38636ce2abd5baab35a23d05843.tar.gz
kioku-cea7703ac440c38636ce2abd5baab35a23d05843.tar.zst
kioku-cea7703ac440c38636ce2abd5baab35a23d05843.zip
feat(deck): display new card count on deck detail page
Add newCardCount alongside Total and Due stats in the deck detail view, queried from cards with state=New via a new countNewCards repository method. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/client/atoms/decks.ts1
-rw-r--r--src/client/pages/DeckCardsPage.test.tsx1
-rw-r--r--src/client/pages/DeckDetailPage.test.tsx1
-rw-r--r--src/client/pages/DeckDetailPage.tsx14
-rw-r--r--src/client/pages/HomePage.test.tsx4
-rw-r--r--src/server/repositories/card.test.ts1
-rw-r--r--src/server/repositories/card.ts14
-rw-r--r--src/server/repositories/types.ts1
-rw-r--r--src/server/routes/cards.test.ts1
-rw-r--r--src/server/routes/decks.test.ts1
-rw-r--r--src/server/routes/decks.ts14
-rw-r--r--src/server/routes/study.test.ts1
12 files changed, 48 insertions, 6 deletions
diff --git a/src/client/atoms/decks.ts b/src/client/atoms/decks.ts
index 5a4d44e..a0e569f 100644
--- a/src/client/atoms/decks.ts
+++ b/src/client/atoms/decks.ts
@@ -7,6 +7,7 @@ export interface Deck {
name: string;
description: string | null;
dueCardCount: number;
+ newCardCount: number;
createdAt: string;
updatedAt: string;
}
diff --git a/src/client/pages/DeckCardsPage.test.tsx b/src/client/pages/DeckCardsPage.test.tsx
index 7c3c184..91b0b28 100644
--- a/src/client/pages/DeckCardsPage.test.tsx
+++ b/src/client/pages/DeckCardsPage.test.tsx
@@ -73,6 +73,7 @@ const mockDeck = {
name: "Japanese Vocabulary",
description: "Common Japanese words",
dueCardCount: 0,
+ newCardCount: 0,
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
};
diff --git a/src/client/pages/DeckDetailPage.test.tsx b/src/client/pages/DeckDetailPage.test.tsx
index 9dcb152..0b9216b 100644
--- a/src/client/pages/DeckDetailPage.test.tsx
+++ b/src/client/pages/DeckDetailPage.test.tsx
@@ -60,6 +60,7 @@ const mockDeck = {
name: "Japanese Vocabulary",
description: "Common Japanese words",
dueCardCount: 0,
+ newCardCount: 0,
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
};
diff --git a/src/client/pages/DeckDetailPage.tsx b/src/client/pages/DeckDetailPage.tsx
index bb8d42a..0a02051 100644
--- a/src/client/pages/DeckDetailPage.tsx
+++ b/src/client/pages/DeckDetailPage.tsx
@@ -33,12 +33,18 @@ function DeckStats({ deckId }: { deckId: string }) {
return (
<div className="bg-white rounded-xl border border-border/50 p-6 mb-6">
- <div className="grid grid-cols-2 gap-4">
+ <div className="grid grid-cols-3 gap-4">
<div>
<p className="text-sm text-muted mb-1">Total</p>
<p className="text-2xl font-semibold text-ink">{cards.length}</p>
</div>
<div>
+ <p className="text-sm text-muted mb-1">New</p>
+ <p className="text-2xl font-semibold text-info">
+ {deck.newCardCount}
+ </p>
+ </div>
+ <div>
<p className="text-sm text-muted mb-1">Due</p>
<p className="text-2xl font-semibold text-primary">
{deck.dueCardCount}
@@ -77,7 +83,11 @@ function DeckContent({
<Suspense
fallback={
<div className="bg-white rounded-xl border border-border/50 p-6 mb-6">
- <div className="grid grid-cols-2 gap-4">
+ <div className="grid grid-cols-3 gap-4">
+ <div>
+ <div className="h-4 w-12 bg-muted/20 rounded animate-pulse mb-1" />
+ <div className="h-8 w-10 bg-muted/20 rounded animate-pulse" />
+ </div>
<div>
<div className="h-4 w-12 bg-muted/20 rounded animate-pulse mb-1" />
<div className="h-8 w-10 bg-muted/20 rounded animate-pulse" />
diff --git a/src/client/pages/HomePage.test.tsx b/src/client/pages/HomePage.test.tsx
index 179c649..3d15777 100644
--- a/src/client/pages/HomePage.test.tsx
+++ b/src/client/pages/HomePage.test.tsx
@@ -93,6 +93,7 @@ const mockDecks = [
name: "Japanese Vocabulary",
description: "Common Japanese words",
dueCardCount: 5,
+ newCardCount: 0,
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
},
@@ -101,6 +102,7 @@ const mockDecks = [
name: "Spanish Verbs",
description: null,
dueCardCount: 0,
+ newCardCount: 0,
createdAt: "2024-01-02T00:00:00Z",
updatedAt: "2024-01-02T00:00:00Z",
},
@@ -253,6 +255,7 @@ describe("HomePage", () => {
name: "No Description Deck",
description: null,
dueCardCount: 0,
+ newCardCount: 0,
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
};
@@ -332,6 +335,7 @@ describe("HomePage", () => {
name: "New Deck",
description: "A new deck",
dueCardCount: 0,
+ newCardCount: 0,
createdAt: "2024-01-03T00:00:00Z",
updatedAt: "2024-01-03T00:00:00Z",
};
diff --git a/src/server/repositories/card.test.ts b/src/server/repositories/card.test.ts
index b492fd7..b959709 100644
--- a/src/server/repositories/card.test.ts
+++ b/src/server/repositories/card.test.ts
@@ -112,6 +112,7 @@ function createMockCardRepo(): CardRepository {
softDeleteByNoteId: vi.fn(),
findDueCards: vi.fn(),
countDueCards: vi.fn(),
+ countNewCards: vi.fn(),
findDueCardsWithNoteData: vi.fn(),
findDueCardsForStudy: vi.fn(),
updateFSRSFields: vi.fn(),
diff --git a/src/server/repositories/card.ts b/src/server/repositories/card.ts
index 0f1ef79..223050a 100644
--- a/src/server/repositories/card.ts
+++ b/src/server/repositories/card.ts
@@ -216,6 +216,20 @@ export const cardRepository: CardRepository = {
return result[0]?.count ?? 0;
},
+ async countNewCards(deckId: string): Promise<number> {
+ const result = await db
+ .select({ count: sql<number>`count(*)::int` })
+ .from(cards)
+ .where(
+ and(
+ eq(cards.deckId, deckId),
+ isNull(cards.deletedAt),
+ eq(cards.state, CardState.New),
+ ),
+ );
+ return result[0]?.count ?? 0;
+ },
+
async findDueCardsWithNoteData(
deckId: string,
now: Date,
diff --git a/src/server/repositories/types.ts b/src/server/repositories/types.ts
index 71cb811..b70b247 100644
--- a/src/server/repositories/types.ts
+++ b/src/server/repositories/types.ts
@@ -145,6 +145,7 @@ export interface CardRepository {
softDeleteByNoteId(noteId: string): Promise<boolean>;
findDueCards(deckId: string, now: Date): Promise<Card[]>;
countDueCards(deckId: string, now: Date): Promise<number>;
+ countNewCards(deckId: string): Promise<number>;
findDueCardsWithNoteData(
deckId: string,
now: Date,
diff --git a/src/server/routes/cards.test.ts b/src/server/routes/cards.test.ts
index a063c95..cd0493c 100644
--- a/src/server/routes/cards.test.ts
+++ b/src/server/routes/cards.test.ts
@@ -26,6 +26,7 @@ function createMockCardRepo(): CardRepository {
softDeleteByNoteId: vi.fn(),
findDueCards: vi.fn(),
countDueCards: vi.fn(),
+ countNewCards: vi.fn(),
findDueCardsWithNoteData: vi.fn(),
findDueCardsForStudy: vi.fn(),
updateFSRSFields: vi.fn(),
diff --git a/src/server/routes/decks.test.ts b/src/server/routes/decks.test.ts
index f686024..a1412d2 100644
--- a/src/server/routes/decks.test.ts
+++ b/src/server/routes/decks.test.ts
@@ -31,6 +31,7 @@ function createMockCardRepo(): CardRepository {
softDeleteByNoteId: vi.fn(),
findDueCards: vi.fn(),
countDueCards: vi.fn().mockResolvedValue(0),
+ countNewCards: vi.fn().mockResolvedValue(0),
findDueCardsWithNoteData: vi.fn(),
findDueCardsForStudy: vi.fn(),
updateFSRSFields: vi.fn(),
diff --git a/src/server/routes/decks.ts b/src/server/routes/decks.ts
index ed7077e..dcf758a 100644
--- a/src/server/routes/decks.ts
+++ b/src/server/routes/decks.ts
@@ -30,8 +30,11 @@ export function createDecksRouter(deps: DeckDependencies) {
const now = new Date();
const decksWithDueCount = await Promise.all(
decks.map(async (deck) => {
- const dueCardCount = await cardRepo.countDueCards(deck.id, now);
- return { ...deck, dueCardCount };
+ const [dueCardCount, newCardCount] = await Promise.all([
+ cardRepo.countDueCards(deck.id, now),
+ cardRepo.countNewCards(deck.id),
+ ]);
+ return { ...deck, dueCardCount, newCardCount };
}),
);
return c.json({ decks: decksWithDueCount }, 200);
@@ -58,9 +61,12 @@ export function createDecksRouter(deps: DeckDependencies) {
}
const now = new Date();
- const dueCardCount = await cardRepo.countDueCards(deck.id, now);
+ const [dueCardCount, newCardCount] = await Promise.all([
+ cardRepo.countDueCards(deck.id, now),
+ cardRepo.countNewCards(deck.id),
+ ]);
- return c.json({ deck: { ...deck, dueCardCount } }, 200);
+ return c.json({ deck: { ...deck, dueCardCount, newCardCount } }, 200);
})
.put(
"/:id",
diff --git a/src/server/routes/study.test.ts b/src/server/routes/study.test.ts
index 119b25d..84bf3a7 100644
--- a/src/server/routes/study.test.ts
+++ b/src/server/routes/study.test.ts
@@ -26,6 +26,7 @@ function createMockCardRepo(): CardRepository {
softDeleteByNoteId: vi.fn(),
findDueCards: vi.fn(),
countDueCards: vi.fn(),
+ countNewCards: vi.fn(),
findDueCardsWithNoteData: vi.fn(),
findDueCardsForStudy: vi.fn(),
updateFSRSFields: vi.fn(),