aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <54318333+nsfisis@users.noreply.github.com>2026-02-08 21:24:08 +0900
committerGitHub <noreply@github.com>2026-02-08 21:24:08 +0900
commitc0d092b3bfef491d9aa02a4e7e8f503ea35e6420 (patch)
treeac2e447cc7a2e568187a414c241258adf009c1d5
parent5e7c3ad7ed8c287b538de97d4de3a4df87e9a100 (diff)
parent6d53e63d9f3fd81125d0f61e9701ecd262318875 (diff)
downloadkioku-c0d092b3bfef491d9aa02a4e7e8f503ea35e6420.tar.gz
kioku-c0d092b3bfef491d9aa02a4e7e8f503ea35e6420.tar.zst
kioku-c0d092b3bfef491d9aa02a4e7e8f503ea35e6420.zip
Merge pull request #13 from nsfisis/claude/clarify-deck-numbers-mg3d4
Simplify deck stats to show due card count from server
-rw-r--r--src/client/pages/DeckDetailPage.test.tsx29
-rw-r--r--src/client/pages/DeckDetailPage.tsx52
-rw-r--r--src/server/routes/decks.ts18
-rw-r--r--src/server/routes/study.test.ts2
-rw-r--r--src/server/routes/study.ts2
5 files changed, 30 insertions, 73 deletions
diff --git a/src/client/pages/DeckDetailPage.test.tsx b/src/client/pages/DeckDetailPage.test.tsx
index 815dff1..3c741ad 100644
--- a/src/client/pages/DeckDetailPage.test.tsx
+++ b/src/client/pages/DeckDetailPage.test.tsx
@@ -258,33 +258,16 @@ describe("DeckDetailPage", () => {
);
});
- it("displays card counts by state", () => {
+ it("displays due card count from deck data", () => {
renderWithProviders({
- initialDeck: mockDeck,
+ initialDeck: { ...mockDeck, dueCardCount: 5 },
initialCards: mockCards,
});
- // New cards (state=0, but card-1 is not due yet, so 0)
- const newLabel = screen.getByText("New");
- expect(newLabel).toBeDefined();
- const newContainer = newLabel.parentElement;
- expect(newContainer?.querySelector(".text-info")?.textContent).toBe("0");
-
- // Learning cards (state=1 or 3, none in mockCards)
- const learningLabel = screen.getByText("Learning");
- expect(learningLabel).toBeDefined();
- const learningContainer = learningLabel.parentElement;
- expect(learningContainer?.querySelector(".text-warning")?.textContent).toBe(
- "0",
- );
-
- // Review cards (state=2, card-2 is due now)
- const reviewLabel = screen.getByText("Review");
- expect(reviewLabel).toBeDefined();
- const reviewContainer = reviewLabel.parentElement;
- expect(reviewContainer?.querySelector(".text-success")?.textContent).toBe(
- "1",
- );
+ const dueLabel = screen.getByText("Due");
+ expect(dueLabel).toBeDefined();
+ const dueContainer = dueLabel.parentElement;
+ expect(dueContainer?.querySelector(".text-primary")?.textContent).toBe("5");
});
it("does not display card list (cards are hidden)", () => {
diff --git a/src/client/pages/DeckDetailPage.tsx b/src/client/pages/DeckDetailPage.tsx
index 6bc89ba..d717d60 100644
--- a/src/client/pages/DeckDetailPage.tsx
+++ b/src/client/pages/DeckDetailPage.tsx
@@ -7,7 +7,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useAtomValue } from "jotai";
import { Suspense } from "react";
import { Link, useParams } from "wouter";
-import { getEndOfStudyDayBoundary } from "../../shared/date";
import { cardsByDeckAtomFamily, deckByIdAtomFamily } from "../atoms";
import { ErrorBoundary } from "../components/ErrorBoundary";
import { LoadingSpinner } from "../components/LoadingSpinner";
@@ -25,52 +24,21 @@ function DeckHeader({ deckId }: { deckId: string }) {
);
}
-// CardState values from FSRS
-const CardState = {
- New: 0,
- Learning: 1,
- Review: 2,
- Relearning: 3,
-} as const;
-
function DeckStats({ deckId }: { deckId: string }) {
+ const { data: deck } = useAtomValue(deckByIdAtomFamily(deckId));
const { data: cards } = useAtomValue(cardsByDeckAtomFamily(deckId));
- // Count cards due today (study day boundary is 3:00 AM)
- const boundary = getEndOfStudyDayBoundary();
- const dueCards = cards.filter((card) => new Date(card.due) < boundary);
-
- // Count by card state
- const newCards = dueCards.filter((card) => card.state === CardState.New);
- const learningCards = dueCards.filter(
- (card) =>
- card.state === CardState.Learning || card.state === CardState.Relearning,
- );
- const reviewCards = dueCards.filter(
- (card) => card.state === CardState.Review,
- );
-
return (
<div className="bg-white rounded-xl border border-border/50 p-6 mb-6">
- <div className="grid grid-cols-4 gap-4">
+ <div className="grid grid-cols-2 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">{newCards.length}</p>
- </div>
- <div>
- <p className="text-sm text-muted mb-1">Learning</p>
- <p className="text-2xl font-semibold text-warning">
- {learningCards.length}
- </p>
- </div>
- <div>
- <p className="text-sm text-muted mb-1">Review</p>
- <p className="text-2xl font-semibold text-success">
- {reviewCards.length}
+ <p className="text-sm text-muted mb-1">Due</p>
+ <p className="text-2xl font-semibold text-primary">
+ {deck.dueCardCount}
</p>
</div>
</div>
@@ -100,7 +68,7 @@ function DeckContent({ deckId }: { deckId: string }) {
<Suspense
fallback={
<div className="bg-white rounded-xl border border-border/50 p-6 mb-6">
- <div className="grid grid-cols-4 gap-4">
+ <div className="grid grid-cols-2 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" />
@@ -109,14 +77,6 @@ function DeckContent({ deckId }: { deckId: string }) {
<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-16 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-14 bg-muted/20 rounded animate-pulse mb-1" />
- <div className="h-8 w-10 bg-muted/20 rounded animate-pulse" />
- </div>
</div>
</div>
}
diff --git a/src/server/routes/decks.ts b/src/server/routes/decks.ts
index 6a24c66..d73aa0c 100644
--- a/src/server/routes/decks.ts
+++ b/src/server/routes/decks.ts
@@ -22,7 +22,7 @@ const deckIdParamSchema = z.object({
id: z.uuid(),
});
-const REVIEW_CARDS_LIMIT = 100;
+const REVIEW_CARDS_LIMIT = 80;
export function createDecksRouter(deps: DeckDependencies) {
const { deckRepo, cardRepo, reviewLogRepo } = deps;
@@ -82,7 +82,21 @@ export function createDecksRouter(deps: DeckDependencies) {
throw Errors.notFound("Deck not found", "DECK_NOT_FOUND");
}
- return c.json({ deck }, 200);
+ const now = new Date();
+ const [dueNewCards, dueReviewCards, reviewedNewCards] = await Promise.all(
+ [
+ cardRepo.countDueNewCards(deck.id, now),
+ cardRepo.countDueReviewCards(deck.id, now),
+ reviewLogRepo.countTodayNewCardReviews(deck.id, now),
+ ],
+ );
+
+ const newCardBudget = Math.max(0, deck.newCardsPerDay - reviewedNewCards);
+ const newCardsToStudy = Math.min(dueNewCards, newCardBudget);
+ const reviewCardsToStudy = Math.min(dueReviewCards, REVIEW_CARDS_LIMIT);
+ const dueCardCount = newCardsToStudy + reviewCardsToStudy;
+
+ return c.json({ deck: { ...deck, dueCardCount } }, 200);
})
.put(
"/:id",
diff --git a/src/server/routes/study.test.ts b/src/server/routes/study.test.ts
index a58ea0d..514d966 100644
--- a/src/server/routes/study.test.ts
+++ b/src/server/routes/study.test.ts
@@ -198,7 +198,7 @@ describe("GET /api/decks/:deckId/study", () => {
expect(mockCardRepo.findDueReviewCardsForStudy).toHaveBeenCalledWith(
DECK_ID,
expect.any(Date),
- 100,
+ 80,
);
});
diff --git a/src/server/routes/study.ts b/src/server/routes/study.ts
index efd450c..d05f3ca 100644
--- a/src/server/routes/study.ts
+++ b/src/server/routes/study.ts
@@ -62,7 +62,7 @@ export function createStudyRouter(deps: StudyDependencies) {
// Fetch new cards (limited) and review cards separately
const [newCards, reviewCards] = await Promise.all([
cardRepo.findDueNewCardsForStudy(deckId, now, newCardBudget),
- cardRepo.findDueReviewCardsForStudy(deckId, now, 100),
+ cardRepo.findDueReviewCardsForStudy(deckId, now, 80),
]);
return c.json({ cards: [...newCards, ...reviewCards] }, 200);