Some checks are pending
CI / validate (push) Waiting to run
Phase 2 des cards→wordeck Big-Bang-Rebrand:
- 4 package.json: @cards/* → @wordeck/*
- packages/cards-domain/ → packages/wordeck-domain/
- 41+12 Files: from '@cards/domain' → '@wordeck/domain'
- pgSchema('cards') → pgSchema('wordeck') (Drizzle-Schema)
- 17 Files: process.env.CARDS_* → process.env.WORDECK_*
- docker-compose Service-Names: cards-* → wordeck-*
- docker-compose Volume: /Volumes/ManaData/cards → wordeck
- env-vars in compose: CARDS_DB_PASSWORD/_API_VERSION/_DSGVO_SERVICE_KEY etc. → WORDECK_*
- Log-Prefixes + Error-Strings + manifest-id 'cards' → 'wordeck'
- CORS-Origin cardecky.mana.how → wordeck.com
- .env.production.example umbenannt + S3-Key entfernt (kein MinIO mehr)
Type-Check 0 Errors in api+domain+web, 51/51 Domain-Tests grün.
DB-Rename + Container/Volume-Rename auf mana-server folgen in nächstem
Commit nach Verzeichnis-Rename.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
278 lines
6.8 KiB
Svelte
278 lines
6.8 KiB
Svelte
<script lang="ts">
|
|
import { checkTypingAnswer, type TypingMatchResult } from '@wordeck/domain';
|
|
import type { Rating } from '@wordeck/domain';
|
|
|
|
let {
|
|
promptHtml,
|
|
answer,
|
|
aliases,
|
|
answerHtml,
|
|
ongrade,
|
|
}: {
|
|
promptHtml: string;
|
|
answer: string;
|
|
aliases?: string;
|
|
answerHtml: string;
|
|
ongrade: (r: Rating) => void;
|
|
} = $props();
|
|
|
|
let input = $state('');
|
|
let submitted = $state(false);
|
|
let result = $state<TypingMatchResult | null>(null);
|
|
let inputEl = $state<HTMLInputElement | null>(null);
|
|
|
|
$effect(() => {
|
|
if (inputEl && !submitted) inputEl.focus();
|
|
});
|
|
|
|
function submit() {
|
|
if (submitted || !input.trim()) return;
|
|
result = checkTypingAnswer(input, answer, aliases);
|
|
submitted = true;
|
|
}
|
|
|
|
function handleKey(e: KeyboardEvent) {
|
|
if (!submitted) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
submit();
|
|
}
|
|
return;
|
|
}
|
|
// Nach Submit: Keyboard-Shortcuts für Grade
|
|
if (result === 'correct') {
|
|
if (e.key === ' ' || e.key === 'Enter') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
ongrade('good');
|
|
}
|
|
} else if (result === 'close') {
|
|
if (e.key === '1') { e.stopPropagation(); ongrade('again'); }
|
|
else if (e.key === '3' || e.key === ' ' || e.key === 'Enter') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
ongrade('good');
|
|
}
|
|
} else if (result === 'wrong') {
|
|
if (e.key === '1') { e.stopPropagation(); ongrade('again'); }
|
|
else if (e.key === '2') { e.stopPropagation(); ongrade('hard'); }
|
|
else if (e.key === '3' || e.key === ' ' || e.key === 'Enter') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
ongrade('good');
|
|
}
|
|
else if (e.key === '4') { e.stopPropagation(); ongrade('easy'); }
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<svelte:window onkeydown={handleKey} />
|
|
|
|
<div class="typing-view">
|
|
<div class="prose">{@html promptHtml}</div>
|
|
|
|
{#if !submitted}
|
|
<div class="input-row">
|
|
<input
|
|
bind:this={inputEl}
|
|
bind:value={input}
|
|
class="typing-input"
|
|
type="text"
|
|
placeholder="Antwort eingeben …"
|
|
autocomplete="off"
|
|
autocorrect="off"
|
|
autocapitalize="off"
|
|
spellcheck={false}
|
|
/>
|
|
<button class="submit-btn" onclick={submit} disabled={!input.trim()} aria-label="Antwort prüfen">
|
|
↵
|
|
</button>
|
|
</div>
|
|
{:else}
|
|
<div class="result-row">
|
|
<span class="badge badge-{result}">
|
|
{#if result === 'correct'}✓ Richtig
|
|
{:else if result === 'close'}≈ Fast
|
|
{:else}✗ Falsch{/if}
|
|
</span>
|
|
<span class="your-input">„{input}"</span>
|
|
</div>
|
|
|
|
<hr class="divider" />
|
|
<div class="prose answer">{@html answerHtml}</div>
|
|
|
|
<div class="grade-row grade-row-{result}">
|
|
{#if result === 'correct'}
|
|
<button class="grade-btn primary" onclick={() => ongrade('good')}>
|
|
Weiter <kbd>Space</kbd>
|
|
</button>
|
|
{:else if result === 'close'}
|
|
<button class="grade-btn" onclick={() => ongrade('again')}>
|
|
Nochmal <kbd>1</kbd>
|
|
</button>
|
|
<button class="grade-btn primary" onclick={() => ongrade('good')}>
|
|
War richtig <kbd>3</kbd>
|
|
</button>
|
|
{:else}
|
|
<button class="grade-btn danger" onclick={() => ongrade('again')}>
|
|
Wieder <kbd>1</kbd>
|
|
</button>
|
|
<button class="grade-btn" onclick={() => ongrade('hard')}>
|
|
Schwer <kbd>2</kbd>
|
|
</button>
|
|
<button class="grade-btn primary" onclick={() => ongrade('good')}>
|
|
Gut <kbd>3</kbd>
|
|
</button>
|
|
<button class="grade-btn success" onclick={() => ongrade('easy')}>
|
|
Leicht <kbd>4</kbd>
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.typing-view {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
width: 100%;
|
|
}
|
|
|
|
.prose {
|
|
font-size: 1.125rem;
|
|
line-height: 1.55;
|
|
color: hsl(var(--color-foreground));
|
|
}
|
|
.prose :global(p) { margin: 0 0 0.875em; }
|
|
.prose :global(p:last-child) { margin-bottom: 0; }
|
|
.prose :global(h1) {
|
|
font-size: 4rem;
|
|
font-weight: 700;
|
|
text-align: center;
|
|
margin: 0;
|
|
line-height: 1;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
.prose.answer { color: hsl(var(--color-foreground)); }
|
|
|
|
.input-row {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.typing-input {
|
|
flex: 1;
|
|
padding: 0.625rem 0.75rem;
|
|
border: 1px solid hsl(var(--color-border));
|
|
border-radius: 0.5rem;
|
|
background: hsl(var(--color-surface));
|
|
color: hsl(var(--color-foreground));
|
|
font: inherit;
|
|
font-size: 1rem;
|
|
outline: none;
|
|
transition: border-color 0.15s ease;
|
|
}
|
|
.typing-input:focus {
|
|
border-color: hsl(var(--color-primary));
|
|
}
|
|
|
|
.submit-btn {
|
|
padding: 0 0.875rem;
|
|
border: 1px solid hsl(var(--color-border));
|
|
border-radius: 0.5rem;
|
|
background: hsl(var(--color-surface));
|
|
color: hsl(var(--color-foreground));
|
|
font: inherit;
|
|
font-size: 1.125rem;
|
|
cursor: pointer;
|
|
transition: background-color 0.15s ease;
|
|
}
|
|
.submit-btn:hover:not(:disabled) {
|
|
background: hsl(var(--color-primary));
|
|
color: hsl(var(--color-primary-foreground));
|
|
border-color: hsl(var(--color-primary));
|
|
}
|
|
.submit-btn:disabled {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.result-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.625rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 0.2rem 0.625rem;
|
|
border-radius: 0.375rem;
|
|
font-size: 0.8125rem;
|
|
font-weight: 600;
|
|
white-space: nowrap;
|
|
}
|
|
.badge-correct { background: hsl(var(--color-success) / 0.15); color: hsl(var(--color-success)); }
|
|
.badge-close { background: hsl(40 80% 50% / 0.15); color: hsl(40 80% 40%); }
|
|
.badge-wrong { background: hsl(var(--color-error) / 0.15); color: hsl(var(--color-error)); }
|
|
|
|
.your-input {
|
|
font-size: 0.875rem;
|
|
color: hsl(var(--color-muted-foreground));
|
|
}
|
|
|
|
.divider {
|
|
border: none;
|
|
border-top: 1px solid hsl(var(--color-border));
|
|
margin: 0;
|
|
}
|
|
|
|
.grade-row {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-top: 0.25rem;
|
|
}
|
|
.grade-row-wrong {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
}
|
|
|
|
.grade-btn {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.375rem;
|
|
padding: 0.625rem 0.5rem;
|
|
border: 1px solid hsl(var(--color-border));
|
|
border-radius: 0.5rem;
|
|
background: hsl(var(--color-surface));
|
|
color: hsl(var(--color-foreground));
|
|
font: inherit;
|
|
font-size: 0.875rem;
|
|
cursor: pointer;
|
|
transition: background-color 0.15s ease;
|
|
}
|
|
.grade-btn:hover { background: hsl(var(--color-surface-hover)); }
|
|
.grade-btn.primary {
|
|
background: hsl(var(--color-primary));
|
|
color: hsl(var(--color-primary-foreground));
|
|
border-color: hsl(var(--color-primary));
|
|
}
|
|
.grade-btn.primary:hover { background: hsl(var(--color-primary) / 0.9); }
|
|
.grade-btn.danger { border-color: hsl(var(--color-error) / 0.4); }
|
|
.grade-btn.danger:hover { background: hsl(var(--color-error) / 0.08); }
|
|
.grade-btn.success { border-color: hsl(var(--color-success) / 0.4); }
|
|
.grade-btn.success:hover { background: hsl(var(--color-success) / 0.08); }
|
|
.grade-btn kbd {
|
|
font-size: 0.6875rem;
|
|
color: inherit;
|
|
opacity: 0.6;
|
|
font-family: inherit;
|
|
}
|
|
.grade-btn.primary kbd { opacity: 0.7; }
|
|
</style>
|