diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/atoms/decks.ts | 1 | ||||
| -rw-r--r-- | src/client/pages/DeckDetailPage.test.tsx | 1 | ||||
| -rw-r--r-- | src/client/pages/HomePage.test.tsx | 4 | ||||
| -rw-r--r-- | src/client/pages/HomePage.tsx | 23 | ||||
| -rw-r--r-- | src/server/repositories/card.test.ts | 1 | ||||
| -rw-r--r-- | src/server/repositories/card.ts | 14 | ||||
| -rw-r--r-- | src/server/repositories/types.ts | 1 | ||||
| -rw-r--r-- | src/server/routes/cards.test.ts | 1 | ||||
| -rw-r--r-- | src/server/routes/decks.test.ts | 59 | ||||
| -rw-r--r-- | src/server/routes/decks.ts | 20 | ||||
| -rw-r--r-- | src/server/routes/study.test.ts | 1 |
11 files changed, 109 insertions, 17 deletions
diff --git a/src/client/atoms/decks.ts b/src/client/atoms/decks.ts index 57abef4..e9b0d03 100644 --- a/src/client/atoms/decks.ts +++ b/src/client/atoms/decks.ts @@ -6,6 +6,7 @@ export interface Deck { name: string; description: string | null; newCardsPerDay: number; + dueCardCount: number; createdAt: string; updatedAt: string; } diff --git a/src/client/pages/DeckDetailPage.test.tsx b/src/client/pages/DeckDetailPage.test.tsx index 402ecd4..b138a0b 100644 --- a/src/client/pages/DeckDetailPage.test.tsx +++ b/src/client/pages/DeckDetailPage.test.tsx @@ -70,6 +70,7 @@ const mockDeck = { name: "Japanese Vocabulary", description: "Common Japanese words", newCardsPerDay: 20, + dueCardCount: 0, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", }; diff --git a/src/client/pages/HomePage.test.tsx b/src/client/pages/HomePage.test.tsx index 4921e22..8946fcf 100644 --- a/src/client/pages/HomePage.test.tsx +++ b/src/client/pages/HomePage.test.tsx @@ -84,6 +84,7 @@ const mockDecks = [ name: "Japanese Vocabulary", description: "Common Japanese words", newCardsPerDay: 20, + dueCardCount: 5, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", }, @@ -92,6 +93,7 @@ const mockDecks = [ name: "Spanish Verbs", description: null, newCardsPerDay: 10, + dueCardCount: 0, createdAt: "2024-01-02T00:00:00Z", updatedAt: "2024-01-02T00:00:00Z", }, @@ -239,6 +241,7 @@ describe("HomePage", () => { name: "No Description Deck", description: null, newCardsPerDay: 20, + dueCardCount: 0, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", }; @@ -312,6 +315,7 @@ describe("HomePage", () => { name: "New Deck", description: "A new deck", newCardsPerDay: 20, + dueCardCount: 0, createdAt: "2024-01-03T00:00:00Z", updatedAt: "2024-01-03T00:00:00Z", }; diff --git a/src/client/pages/HomePage.tsx b/src/client/pages/HomePage.tsx index e0e9e9e..ad6ece4 100644 --- a/src/client/pages/HomePage.tsx +++ b/src/client/pages/HomePage.tsx @@ -57,14 +57,21 @@ function DeckList({ > <div className="flex items-start justify-between gap-4"> <div className="flex-1 min-w-0"> - <Link - href={`/decks/${deck.id}`} - className="block group-hover:text-primary transition-colors" - > - <h3 className="font-display text-lg font-medium text-slate truncate"> - {deck.name} - </h3> - </Link> + <div className="flex items-center gap-3"> + <Link + href={`/decks/${deck.id}`} + className="block group-hover:text-primary transition-colors flex-1 min-w-0" + > + <h3 className="font-display text-lg font-medium text-slate truncate"> + {deck.name} + </h3> + </Link> + {deck.dueCardCount > 0 && ( + <span className="shrink-0 inline-flex items-center justify-center min-w-[1.5rem] h-6 px-2 bg-primary text-white text-sm font-medium rounded-full"> + {deck.dueCardCount} + </span> + )} + </div> {deck.description && ( <p className="text-muted text-sm mt-1 line-clamp-2"> {deck.description} diff --git a/src/server/repositories/card.test.ts b/src/server/repositories/card.test.ts index 0a46a76..b492fd7 100644 --- a/src/server/repositories/card.test.ts +++ b/src/server/repositories/card.test.ts @@ -111,6 +111,7 @@ function createMockCardRepo(): CardRepository { softDelete: vi.fn(), softDeleteByNoteId: vi.fn(), findDueCards: vi.fn(), + countDueCards: 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 04425a2..ac03bc6 100644 --- a/src/server/repositories/card.ts +++ b/src/server/repositories/card.ts @@ -204,6 +204,20 @@ export const cardRepository: CardRepository = { return result; }, + async countDueCards(deckId: string, now: Date): Promise<number> { + const result = await db + .select({ count: sql<number>`count(*)::int` }) + .from(cards) + .where( + and( + eq(cards.deckId, deckId), + isNull(cards.deletedAt), + lte(cards.due, now), + ), + ); + 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 4768d49..cb3a287 100644 --- a/src/server/repositories/types.ts +++ b/src/server/repositories/types.ts @@ -147,6 +147,7 @@ export interface CardRepository { softDelete(id: string, deckId: string): Promise<boolean>; softDeleteByNoteId(noteId: string): Promise<boolean>; findDueCards(deckId: string, now: Date, limit: number): Promise<Card[]>; + countDueCards(deckId: string, now: Date): Promise<number>; findDueCardsWithNoteData( deckId: string, now: Date, diff --git a/src/server/routes/cards.test.ts b/src/server/routes/cards.test.ts index 66ba601..e5fb0d4 100644 --- a/src/server/routes/cards.test.ts +++ b/src/server/routes/cards.test.ts @@ -25,6 +25,7 @@ function createMockCardRepo(): CardRepository { softDelete: vi.fn(), softDeleteByNoteId: vi.fn(), findDueCards: vi.fn(), + countDueCards: 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 8f5be9d..55aca2d 100644 --- a/src/server/routes/decks.test.ts +++ b/src/server/routes/decks.test.ts @@ -2,7 +2,11 @@ import { Hono } from "hono"; import { sign } from "hono/jwt"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { errorHandler } from "../middleware/index.js"; -import type { Deck, DeckRepository } from "../repositories/index.js"; +import type { + CardRepository, + Deck, + DeckRepository, +} from "../repositories/index.js"; import { createDecksRouter } from "./decks.js"; function createMockDeckRepo(): DeckRepository { @@ -15,6 +19,24 @@ function createMockDeckRepo(): DeckRepository { }; } +function createMockCardRepo(): CardRepository { + return { + findByDeckId: vi.fn(), + findById: vi.fn(), + findByIdWithNoteData: vi.fn(), + findByNoteId: vi.fn(), + create: vi.fn(), + update: vi.fn(), + softDelete: vi.fn(), + softDeleteByNoteId: vi.fn(), + findDueCards: vi.fn(), + countDueCards: vi.fn().mockResolvedValue(0), + findDueCardsWithNoteData: vi.fn(), + findDueCardsForStudy: vi.fn(), + updateFSRSFields: vi.fn(), + }; +} + const JWT_SECRET = process.env.JWT_SECRET || "test-secret"; async function createTestToken(userId: string): Promise<string> { @@ -57,12 +79,17 @@ interface DeckResponse { describe("GET /api/decks", () => { let app: Hono; let mockDeckRepo: ReturnType<typeof createMockDeckRepo>; + let mockCardRepo: ReturnType<typeof createMockCardRepo>; let authToken: string; beforeEach(async () => { vi.clearAllMocks(); mockDeckRepo = createMockDeckRepo(); - const decksRouter = createDecksRouter({ deckRepo: mockDeckRepo }); + mockCardRepo = createMockCardRepo(); + const decksRouter = createDecksRouter({ + deckRepo: mockDeckRepo, + cardRepo: mockCardRepo, + }); app = new Hono(); app.onError(errorHandler); app.route("/api/decks", decksRouter); @@ -112,12 +139,17 @@ describe("GET /api/decks", () => { describe("POST /api/decks", () => { let app: Hono; let mockDeckRepo: ReturnType<typeof createMockDeckRepo>; + let mockCardRepo: ReturnType<typeof createMockCardRepo>; let authToken: string; beforeEach(async () => { vi.clearAllMocks(); mockDeckRepo = createMockDeckRepo(); - const decksRouter = createDecksRouter({ deckRepo: mockDeckRepo }); + mockCardRepo = createMockCardRepo(); + const decksRouter = createDecksRouter({ + deckRepo: mockDeckRepo, + cardRepo: mockCardRepo, + }); app = new Hono(); app.onError(errorHandler); app.route("/api/decks", decksRouter); @@ -220,12 +252,17 @@ describe("POST /api/decks", () => { describe("GET /api/decks/:id", () => { let app: Hono; let mockDeckRepo: ReturnType<typeof createMockDeckRepo>; + let mockCardRepo: ReturnType<typeof createMockCardRepo>; let authToken: string; beforeEach(async () => { vi.clearAllMocks(); mockDeckRepo = createMockDeckRepo(); - const decksRouter = createDecksRouter({ deckRepo: mockDeckRepo }); + mockCardRepo = createMockCardRepo(); + const decksRouter = createDecksRouter({ + deckRepo: mockDeckRepo, + cardRepo: mockCardRepo, + }); app = new Hono(); app.onError(errorHandler); app.route("/api/decks", decksRouter); @@ -285,12 +322,17 @@ describe("GET /api/decks/:id", () => { describe("PUT /api/decks/:id", () => { let app: Hono; let mockDeckRepo: ReturnType<typeof createMockDeckRepo>; + let mockCardRepo: ReturnType<typeof createMockCardRepo>; let authToken: string; beforeEach(async () => { vi.clearAllMocks(); mockDeckRepo = createMockDeckRepo(); - const decksRouter = createDecksRouter({ deckRepo: mockDeckRepo }); + mockCardRepo = createMockCardRepo(); + const decksRouter = createDecksRouter({ + deckRepo: mockDeckRepo, + cardRepo: mockCardRepo, + }); app = new Hono(); app.onError(errorHandler); app.route("/api/decks", decksRouter); @@ -410,12 +452,17 @@ describe("PUT /api/decks/:id", () => { describe("DELETE /api/decks/:id", () => { let app: Hono; let mockDeckRepo: ReturnType<typeof createMockDeckRepo>; + let mockCardRepo: ReturnType<typeof createMockCardRepo>; let authToken: string; beforeEach(async () => { vi.clearAllMocks(); mockDeckRepo = createMockDeckRepo(); - const decksRouter = createDecksRouter({ deckRepo: mockDeckRepo }); + mockCardRepo = createMockCardRepo(); + const decksRouter = createDecksRouter({ + deckRepo: mockDeckRepo, + cardRepo: mockCardRepo, + }); app = new Hono(); app.onError(errorHandler); app.route("/api/decks", decksRouter); diff --git a/src/server/routes/decks.ts b/src/server/routes/decks.ts index 2450bcd..2e170db 100644 --- a/src/server/routes/decks.ts +++ b/src/server/routes/decks.ts @@ -2,11 +2,17 @@ import { zValidator } from "@hono/zod-validator"; import { Hono } from "hono"; import { z } from "zod"; import { authMiddleware, Errors, getAuthUser } from "../middleware/index.js"; -import { type DeckRepository, deckRepository } from "../repositories/index.js"; +import { + type CardRepository, + cardRepository, + type DeckRepository, + deckRepository, +} from "../repositories/index.js"; import { createDeckSchema, updateDeckSchema } from "../schemas/index.js"; export interface DeckDependencies { deckRepo: DeckRepository; + cardRepo: CardRepository; } const deckIdParamSchema = z.object({ @@ -14,14 +20,21 @@ const deckIdParamSchema = z.object({ }); export function createDecksRouter(deps: DeckDependencies) { - const { deckRepo } = deps; + const { deckRepo, cardRepo } = deps; return new Hono() .use("*", authMiddleware) .get("/", async (c) => { const user = getAuthUser(c); const decks = await deckRepo.findByUserId(user.id); - return c.json({ decks }, 200); + const now = new Date(); + const decksWithDueCount = await Promise.all( + decks.map(async (deck) => { + const dueCardCount = await cardRepo.countDueCards(deck.id, now); + return { ...deck, dueCardCount }; + }), + ); + return c.json({ decks: decksWithDueCount }, 200); }) .post("/", zValidator("json", createDeckSchema), async (c) => { const user = getAuthUser(c); @@ -79,4 +92,5 @@ export function createDecksRouter(deps: DeckDependencies) { export const decks = createDecksRouter({ deckRepo: deckRepository, + cardRepo: cardRepository, }); diff --git a/src/server/routes/study.test.ts b/src/server/routes/study.test.ts index e2fb457..a5ac817 100644 --- a/src/server/routes/study.test.ts +++ b/src/server/routes/study.test.ts @@ -25,6 +25,7 @@ function createMockCardRepo(): CardRepository { softDelete: vi.fn(), softDeleteByNoteId: vi.fn(), findDueCards: vi.fn(), + countDueCards: vi.fn(), findDueCardsWithNoteData: vi.fn(), findDueCardsForStudy: vi.fn(), updateFSRSFields: vi.fn(), |
