refactor(api): review-row-Erstellung extrahieren + QW-Fixes
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
a883ba87b6
commit
b182bac2fb
7 changed files with 64 additions and 67 deletions
27
apps/api/src/lib/reviews.ts
Normal file
27
apps/api/src/lib/reviews.ts
Normal file
|
|
@ -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,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -6,11 +6,12 @@ import {
|
||||||
CardUpdateSchema,
|
CardUpdateSchema,
|
||||||
cardContentHash,
|
cardContentHash,
|
||||||
maskRegionCount,
|
maskRegionCount,
|
||||||
newReview,
|
|
||||||
subIndexCount,
|
subIndexCount,
|
||||||
subIndexCountForCloze,
|
subIndexCountForCloze,
|
||||||
} from '@cards/domain';
|
} from '@cards/domain';
|
||||||
|
|
||||||
|
import { makeInitialReviewRows } from '../lib/reviews.ts';
|
||||||
|
|
||||||
import { getDb, type CardsDb } from '../db/connection.ts';
|
import { getDb, type CardsDb } from '../db/connection.ts';
|
||||||
import { cards, decks, reviews } from '../db/schema/index.ts';
|
import { cards, decks, reviews } from '../db/schema/index.ts';
|
||||||
import { authMiddleware, type AuthVars } from '../middleware/auth.ts';
|
import { authMiddleware, type AuthVars } from '../middleware/auth.ts';
|
||||||
|
|
@ -100,24 +101,7 @@ export function cardsRouter(deps: CardsDeps = {}): Hono<{ Variables: AuthVars }>
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const initialReviews = subIndices.map((subIndex) => {
|
const initialReviews = makeInitialReviewRows({ userId, cardId, subIndices, now });
|
||||||
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,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
if (initialReviews.length > 0) {
|
if (initialReviews.length > 0) {
|
||||||
await tx.insert(reviews).values(initialReviews);
|
await tx.insert(reviews).values(initialReviews);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ import { eq } from 'drizzle-orm';
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { z } from 'zod';
|
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 { getDb, type CardsDb } from '../db/connection.ts';
|
||||||
import { cards, decks, reviews } from '../db/schema/index.ts';
|
import { cards, decks, reviews } from '../db/schema/index.ts';
|
||||||
|
|
@ -72,24 +74,7 @@ export async function insertGeneratedDeck(
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
const subIndices = Array.from({ length: subIndexCount('basic') }, (_, i) => i);
|
const subIndices = Array.from({ length: subIndexCount('basic') }, (_, i) => i);
|
||||||
const initial = subIndices.map((subIndex) => {
|
const initial = makeInitialReviewRows({ userId, cardId: cr.id, subIndices, now });
|
||||||
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,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
await tx.insert(reviews).values(initial);
|
await tx.insert(reviews).values(initial);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ export function decksRouter(deps: DecksDeps = {}): Hono<{ Variables: AuthVars }>
|
||||||
r.get('/:deckId/distractors', async (c) => {
|
r.get('/:deckId/distractors', async (c) => {
|
||||||
const userId = c.get('userId');
|
const userId = c.get('userId');
|
||||||
const deckId = c.req.param('deckId');
|
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 countRaw = parseInt(c.req.query('count') ?? '3', 10);
|
||||||
const count = isNaN(countRaw) ? 3 : Math.min(10, Math.max(1, countRaw));
|
const count = isNaN(countRaw) ? 3 : Math.min(10, Math.max(1, countRaw));
|
||||||
const fieldParam = c.req.query('field') ?? 'back';
|
const fieldParam = c.req.query('field') ?? 'back';
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,12 @@ import {
|
||||||
CardsSearchInputSchema,
|
CardsSearchInputSchema,
|
||||||
cardContentHash,
|
cardContentHash,
|
||||||
maskRegionCount,
|
maskRegionCount,
|
||||||
newReview,
|
|
||||||
subIndexCount,
|
subIndexCount,
|
||||||
subIndexCountForCloze,
|
subIndexCountForCloze,
|
||||||
} from '@cards/domain';
|
} from '@cards/domain';
|
||||||
|
|
||||||
|
import { makeInitialReviewRows } from '../lib/reviews.ts';
|
||||||
|
|
||||||
import { getDb, type CardsDb } from '../db/connection.ts';
|
import { getDb, type CardsDb } from '../db/connection.ts';
|
||||||
import { cards, decks, reviews } from '../db/schema/index.ts';
|
import { cards, decks, reviews } from '../db/schema/index.ts';
|
||||||
import { authMiddleware, type AuthVars } from '../middleware/auth.ts';
|
import { authMiddleware, type AuthVars } from '../middleware/auth.ts';
|
||||||
|
|
@ -106,24 +107,8 @@ export function toolsRouter(deps: ToolsDeps = {}): Hono<{ Variables: AuthVars }>
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
const initial = Array.from({ length: count }, (_, i) => i).map((subIndex) => {
|
const subIndices = Array.from({ length: count }, (_, i) => i);
|
||||||
const r = newReview({ userId, cardId, subIndex, now });
|
const initial = makeInitialReviewRows({ userId, cardId, subIndices, 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,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
if (initial.length > 0) await tx.insert(reviews).values(initial);
|
if (initial.length > 0) await tx.insert(reviews).values(initial);
|
||||||
return [card];
|
return [card];
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export function fetchDistractors(
|
||||||
opts: { cardId?: string; count?: number; field?: string } = {},
|
opts: { cardId?: string; count?: number; field?: string } = {},
|
||||||
) {
|
) {
|
||||||
const params = new URLSearchParams();
|
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.count) params.set('count', String(opts.count));
|
||||||
if (opts.field) params.set('field', opts.field);
|
if (opts.field) params.set('field', opts.field);
|
||||||
const qs = params.size ? `?${params}` : '';
|
const qs = params.size ? `?${params}` : '';
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
} from '@cards/domain';
|
} from '@cards/domain';
|
||||||
import { createCard } from '$lib/api/cards.ts';
|
import { createCard } from '$lib/api/cards.ts';
|
||||||
import { listDecks, getDeck } from '$lib/api/decks.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 { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
||||||
import { renderMarkdown } from '$lib/markdown.ts';
|
import { renderMarkdown } from '$lib/markdown.ts';
|
||||||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||||
|
|
@ -360,14 +361,22 @@
|
||||||
{#snippet children()}
|
{#snippet children()}
|
||||||
<div class="preview-inner">
|
<div class="preview-inner">
|
||||||
{#if cardType === 'image-occlusion'}
|
{#if cardType === 'image-occlusion'}
|
||||||
|
{#if imageRef}
|
||||||
|
<img
|
||||||
|
src="{API_BASE}/api/v1/media/{imageRef}"
|
||||||
|
alt="Occlusion-Bild"
|
||||||
|
class="preview-occlusion-img"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
<div class="preview-placeholder">
|
<div class="preview-placeholder">
|
||||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" />
|
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||||
<circle cx="8.5" cy="8.5" r="1.5" />
|
<circle cx="8.5" cy="8.5" r="1.5" />
|
||||||
<path d="m21 15-5-5L5 21" />
|
<path d="m21 15-5-5L5 21" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Bildvorschau nicht verfügbar</span>
|
<span>Noch kein Bild gewählt</span>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{:else if cardType === 'audio-front'}
|
{:else if cardType === 'audio-front'}
|
||||||
<div class="preview-audio">
|
<div class="preview-audio">
|
||||||
|
|
@ -990,4 +999,11 @@
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-occlusion-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue