From b182bac2fb8ca8c21ef914c9ed021336db495ee5 Mon Sep 17 00:00:00 2001 From: Till JS Date: Sun, 10 May 2026 16:12:28 +0200 Subject: [PATCH] refactor(api): review-row-Erstellung extrahieren + QW-Fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - makeInitialReviewRows() in lib/reviews.ts: eliminiert 45 Zeilen Duplikat aus cards.ts, decks-generate.ts und tools.ts - /distractors: Query-Param cardId → card_id (snake_case-Konsistenz) - cards/new: Image-Occlusion-Preview zeigt hochgeladenes Bild statt statischen Platzhalter Co-Authored-By: Claude Sonnet 4.6 --- apps/api/src/lib/reviews.ts | 27 ++++++++++++++++++ apps/api/src/routes/cards.ts | 22 ++------------- apps/api/src/routes/decks-generate.ts | 23 +++------------- apps/api/src/routes/decks.ts | 2 +- apps/api/src/routes/tools.ts | 23 +++------------- apps/web/src/lib/api/decks.ts | 2 +- apps/web/src/routes/cards/new/+page.svelte | 32 ++++++++++++++++------ 7 files changed, 64 insertions(+), 67 deletions(-) create mode 100644 apps/api/src/lib/reviews.ts diff --git a/apps/api/src/lib/reviews.ts b/apps/api/src/lib/reviews.ts new file mode 100644 index 0000000..9ae3be4 --- /dev/null +++ b/apps/api/src/lib/reviews.ts @@ -0,0 +1,27 @@ +import { newReview } from '@cards/domain'; + +export function makeInitialReviewRows(params: { + userId: string; + cardId: string; + subIndices: number[]; + now: Date; +}) { + return params.subIndices.map((subIndex) => { + const r = newReview({ userId: params.userId, cardId: params.cardId, subIndex, now: params.now }); + return { + cardId: r.card_id, + subIndex: r.sub_index, + userId: r.user_id, + due: new Date(r.due), + stability: r.stability, + difficulty: r.difficulty, + elapsedDays: r.elapsed_days, + scheduledDays: r.scheduled_days, + learningSteps: r.learning_steps, + reps: r.reps, + lapses: r.lapses, + state: r.state, + lastReview: r.last_review ? new Date(r.last_review) : null, + }; + }); +} diff --git a/apps/api/src/routes/cards.ts b/apps/api/src/routes/cards.ts index 421e83d..a175254 100644 --- a/apps/api/src/routes/cards.ts +++ b/apps/api/src/routes/cards.ts @@ -6,11 +6,12 @@ import { CardUpdateSchema, cardContentHash, maskRegionCount, - newReview, subIndexCount, subIndexCountForCloze, } from '@cards/domain'; +import { makeInitialReviewRows } from '../lib/reviews.ts'; + import { getDb, type CardsDb } from '../db/connection.ts'; import { cards, decks, reviews } from '../db/schema/index.ts'; import { authMiddleware, type AuthVars } from '../middleware/auth.ts'; @@ -100,24 +101,7 @@ export function cardsRouter(deps: CardsDeps = {}): Hono<{ Variables: AuthVars }> }) .returning(); - const initialReviews = subIndices.map((subIndex) => { - const r = newReview({ userId, cardId, subIndex, now }); - return { - cardId: r.card_id, - subIndex: r.sub_index, - userId: r.user_id, - due: new Date(r.due), - stability: r.stability, - difficulty: r.difficulty, - elapsedDays: r.elapsed_days, - scheduledDays: r.scheduled_days, - learningSteps: r.learning_steps, - reps: r.reps, - lapses: r.lapses, - state: r.state, - lastReview: r.last_review ? new Date(r.last_review) : null, - }; - }); + const initialReviews = makeInitialReviewRows({ userId, cardId, subIndices, now }); if (initialReviews.length > 0) { await tx.insert(reviews).values(initialReviews); } diff --git a/apps/api/src/routes/decks-generate.ts b/apps/api/src/routes/decks-generate.ts index 84febfb..6588206 100644 --- a/apps/api/src/routes/decks-generate.ts +++ b/apps/api/src/routes/decks-generate.ts @@ -2,7 +2,9 @@ import { eq } from 'drizzle-orm'; import { Hono } from 'hono'; import { z } from 'zod'; -import { cardContentHash, newReview, subIndexCount } from '@cards/domain'; +import { cardContentHash, subIndexCount } from '@cards/domain'; + +import { makeInitialReviewRows } from '../lib/reviews.ts'; import { getDb, type CardsDb } from '../db/connection.ts'; import { cards, decks, reviews } from '../db/schema/index.ts'; @@ -72,24 +74,7 @@ export async function insertGeneratedDeck( updatedAt: now, }); const subIndices = Array.from({ length: subIndexCount('basic') }, (_, i) => i); - const initial = subIndices.map((subIndex) => { - const r = newReview({ userId, cardId: cr.id, subIndex, now }); - return { - cardId: r.card_id, - subIndex: r.sub_index, - userId: r.user_id, - due: new Date(r.due), - stability: r.stability, - difficulty: r.difficulty, - elapsedDays: r.elapsed_days, - scheduledDays: r.scheduled_days, - learningSteps: r.learning_steps, - reps: r.reps, - lapses: r.lapses, - state: r.state, - lastReview: r.last_review ? new Date(r.last_review) : null, - }; - }); + const initial = makeInitialReviewRows({ userId, cardId: cr.id, subIndices, now }); await tx.insert(reviews).values(initial); } }); diff --git a/apps/api/src/routes/decks.ts b/apps/api/src/routes/decks.ts index ca9a120..2cee34a 100644 --- a/apps/api/src/routes/decks.ts +++ b/apps/api/src/routes/decks.ts @@ -123,7 +123,7 @@ export function decksRouter(deps: DecksDeps = {}): Hono<{ Variables: AuthVars }> r.get('/:deckId/distractors', async (c) => { const userId = c.get('userId'); const deckId = c.req.param('deckId'); - const cardId = c.req.query('cardId') ?? ''; + const cardId = c.req.query('card_id') ?? ''; const countRaw = parseInt(c.req.query('count') ?? '3', 10); const count = isNaN(countRaw) ? 3 : Math.min(10, Math.max(1, countRaw)); const fieldParam = c.req.query('field') ?? 'back'; diff --git a/apps/api/src/routes/tools.ts b/apps/api/src/routes/tools.ts index 854616d..4ef1195 100644 --- a/apps/api/src/routes/tools.ts +++ b/apps/api/src/routes/tools.ts @@ -6,11 +6,12 @@ import { CardsSearchInputSchema, cardContentHash, maskRegionCount, - newReview, subIndexCount, subIndexCountForCloze, } from '@cards/domain'; +import { makeInitialReviewRows } from '../lib/reviews.ts'; + import { getDb, type CardsDb } from '../db/connection.ts'; import { cards, decks, reviews } from '../db/schema/index.ts'; import { authMiddleware, type AuthVars } from '../middleware/auth.ts'; @@ -106,24 +107,8 @@ export function toolsRouter(deps: ToolsDeps = {}): Hono<{ Variables: AuthVars }> updatedAt: now, }) .returning(); - const initial = Array.from({ length: count }, (_, i) => i).map((subIndex) => { - const r = newReview({ userId, cardId, subIndex, now }); - return { - cardId: r.card_id, - subIndex: r.sub_index, - userId: r.user_id, - due: new Date(r.due), - stability: r.stability, - difficulty: r.difficulty, - elapsedDays: r.elapsed_days, - scheduledDays: r.scheduled_days, - learningSteps: r.learning_steps, - reps: r.reps, - lapses: r.lapses, - state: r.state, - lastReview: r.last_review ? new Date(r.last_review) : null, - }; - }); + const subIndices = Array.from({ length: count }, (_, i) => i); + const initial = makeInitialReviewRows({ userId, cardId, subIndices, now }); if (initial.length > 0) await tx.insert(reviews).values(initial); return [card]; }); diff --git a/apps/web/src/lib/api/decks.ts b/apps/web/src/lib/api/decks.ts index 0af9c6a..13d69e2 100644 --- a/apps/web/src/lib/api/decks.ts +++ b/apps/web/src/lib/api/decks.ts @@ -34,7 +34,7 @@ export function fetchDistractors( opts: { cardId?: string; count?: number; field?: string } = {}, ) { const params = new URLSearchParams(); - if (opts.cardId) params.set('cardId', opts.cardId); + if (opts.cardId) params.set('card_id', opts.cardId); if (opts.count) params.set('count', String(opts.count)); if (opts.field) params.set('field', opts.field); const qs = params.size ? `?${params}` : ''; diff --git a/apps/web/src/routes/cards/new/+page.svelte b/apps/web/src/routes/cards/new/+page.svelte index d78125f..e0b5d1a 100644 --- a/apps/web/src/routes/cards/new/+page.svelte +++ b/apps/web/src/routes/cards/new/+page.svelte @@ -10,6 +10,7 @@ } from '@cards/domain'; import { createCard } from '$lib/api/cards.ts'; import { listDecks, getDeck } from '$lib/api/decks.ts'; + import { API_BASE } from '$lib/api/client.ts'; import { devUser } from '$lib/auth/dev-stub.svelte.ts'; import { renderMarkdown } from '$lib/markdown.ts'; import { toasts } from '$lib/stores/toasts.svelte.ts'; @@ -360,14 +361,22 @@ {#snippet children()}
{#if cardType === 'image-occlusion'} -
- - - - - - Bildvorschau nicht verfügbar -
+ {#if imageRef} + Occlusion-Bild + {:else} +
+ + + + + + Noch kein Bild gewählt +
+ {/if} {:else if cardType === 'audio-front'}
@@ -990,4 +999,11 @@ font-size: 0.875rem; text-align: center; } + + .preview-occlusion-img { + width: 100%; + height: 100%; + object-fit: contain; + border-radius: 0.25rem; + }