From ce9011bf351d9666bb2e81c92ae06a0eb1716d12 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Wed, 31 Dec 2025 13:25:42 +0900 Subject: feat(study): render note-based cards using template system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update StudyPage to support both legacy cards (direct front/back) and note-based cards (template rendering with field values). Add new CardForStudy type that includes note type templates and field values as a name-value map for efficient client-side rendering. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/server/repositories/card.ts | 107 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 2 deletions(-) (limited to 'src/server/repositories/card.ts') diff --git a/src/server/repositories/card.ts b/src/server/repositories/card.ts index 92811d4..7116642 100644 --- a/src/server/repositories/card.ts +++ b/src/server/repositories/card.ts @@ -1,7 +1,19 @@ import { and, eq, isNull, lte, sql } from "drizzle-orm"; import { db } from "../db/index.js"; -import { CardState, cards, noteFieldValues, notes } from "../db/schema.js"; -import type { Card, CardRepository, CardWithNoteData } from "./types.js"; +import { + CardState, + cards, + noteFieldTypes, + noteFieldValues, + notes, + noteTypes, +} from "../db/schema.js"; +import type { + Card, + CardForStudy, + CardRepository, + CardWithNoteData, +} from "./types.js"; export const cardRepository: CardRepository = { async findByDeckId(deckId: string): Promise { @@ -219,6 +231,97 @@ export const cardRepository: CardRepository = { return cardsWithNoteData; }, + async findDueCardsForStudy( + deckId: string, + now: Date, + limit: number, + ): Promise { + const dueCards = await this.findDueCards(deckId, now, limit); + + const cardsForStudy: CardForStudy[] = []; + + for (const card of dueCards) { + // Legacy card (no note association) + if (!card.noteId) { + cardsForStudy.push({ + ...card, + noteType: null, + fieldValuesMap: {}, + }); + continue; + } + + // Fetch note to get noteTypeId + const noteResult = await db + .select() + .from(notes) + .where(and(eq(notes.id, card.noteId), isNull(notes.deletedAt))); + + const note = noteResult[0]; + if (!note) { + // Note was deleted, treat as legacy card + cardsForStudy.push({ + ...card, + noteType: null, + fieldValuesMap: {}, + }); + continue; + } + + // Fetch note type for templates + const noteTypeResult = await db + .select({ + frontTemplate: noteTypes.frontTemplate, + backTemplate: noteTypes.backTemplate, + }) + .from(noteTypes) + .where( + and(eq(noteTypes.id, note.noteTypeId), isNull(noteTypes.deletedAt)), + ); + + const noteType = noteTypeResult[0]; + if (!noteType) { + // Note type was deleted, treat as legacy card + cardsForStudy.push({ + ...card, + noteType: null, + fieldValuesMap: {}, + }); + continue; + } + + // Fetch field values with their field names + const fieldValuesWithNames = await db + .select({ + fieldName: noteFieldTypes.name, + value: noteFieldValues.value, + }) + .from(noteFieldValues) + .innerJoin( + noteFieldTypes, + eq(noteFieldValues.noteFieldTypeId, noteFieldTypes.id), + ) + .where(eq(noteFieldValues.noteId, card.noteId)); + + // Convert to name-value map + const fieldValuesMap: Record = {}; + for (const fv of fieldValuesWithNames) { + fieldValuesMap[fv.fieldName] = fv.value; + } + + cardsForStudy.push({ + ...card, + noteType: { + frontTemplate: noteType.frontTemplate, + backTemplate: noteType.backTemplate, + }, + fieldValuesMap, + }); + } + + return cardsForStudy; + }, + async updateFSRSFields( id: string, deckId: string, -- cgit v1.2.3-70-g09d2