115 lines
4.1 KiB
Vue
115 lines
4.1 KiB
Vue
<script setup>
|
|
// Übungspool: EIN Leitner-Stapel pro Thema (fällige Karten zuerst, dann neue).
|
|
// Der Server wählt und ordnet die Karten (Entscheidungs-Entlastung); „Nochmal"
|
|
// schiebt die Karte ans Rundenende, gebucht wird jede Antwort sofort.
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { fetchPracticeDeck, answerPracticeCard } from '../api.js'
|
|
import FlashcardWidget from './FlashcardWidget.vue'
|
|
|
|
const props = defineProps({
|
|
topic: { type: String, required: true },
|
|
})
|
|
|
|
const deck = ref([]) // Karten in Übungsreihenfolge
|
|
const counts = ref(null) // {due, new, new_total, gesperrt}
|
|
const nextDueAt = ref(null)
|
|
const loadError = ref(null)
|
|
const loading = ref(true)
|
|
const erledigt = ref(0)
|
|
|
|
const current = computed(() => deck.value[0] || null)
|
|
const offen = computed(() => deck.value.length)
|
|
|
|
async function loadDeck() {
|
|
loading.value = true
|
|
loadError.value = null
|
|
erledigt.value = 0
|
|
try {
|
|
const d = await fetchPracticeDeck(props.topic)
|
|
deck.value = d.cards || []
|
|
counts.value = d.counts || null
|
|
nextDueAt.value = d.next_due_at
|
|
} catch (e) {
|
|
loadError.value = e.message || 'Übungsstapel nicht ladbar.'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(loadDeck)
|
|
|
|
async function onAnswer(correct) {
|
|
const card = current.value
|
|
if (!card) return
|
|
try {
|
|
await answerPracticeCard({
|
|
topic: props.topic, block_norm: card.block_norm,
|
|
sub_norm: card.sub_norm, correct,
|
|
})
|
|
} catch { /* Buchung offline fehlgeschlagen — Durchgang läuft lokal weiter */ }
|
|
deck.value.shift()
|
|
if (correct) {
|
|
erledigt.value += 1
|
|
} else {
|
|
deck.value.push(card) // Rundenende: Box 1 ist sofort wieder fällig
|
|
}
|
|
}
|
|
|
|
function naechsteFaelligkeit() {
|
|
if (!nextDueAt.value) return null
|
|
const d = new Date(nextDueAt.value)
|
|
return d.toLocaleDateString(undefined, { weekday: 'short', day: 'numeric', month: 'short' })
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="pr-panel">
|
|
<div class="pr-head">
|
|
<h2>Üben</h2>
|
|
<span v-if="counts" class="pr-counts">
|
|
{{ counts.due }} fällig · {{ counts.new }} neu<template v-if="counts.new_total > counts.new"> (von {{ counts.new_total }})</template>
|
|
</span>
|
|
<span v-if="offen" class="pr-rest">{{ offen }} übrig</span>
|
|
</div>
|
|
|
|
<p v-if="loadError" class="pr-msg">{{ loadError }}</p>
|
|
<p v-else-if="loading" class="pr-msg">Lade Stapel…</p>
|
|
|
|
<div v-else-if="current" class="pr-body">
|
|
<div class="pr-kontext">{{ current.block }} · {{ current.subblock }}</div>
|
|
<FlashcardWidget :card="current" @answer="onAnswer" />
|
|
</div>
|
|
|
|
<div v-else class="pr-done">
|
|
<p class="pr-done-title">Alles erledigt ✓</p>
|
|
<p v-if="erledigt" class="pr-msg">{{ erledigt }} Karten in dieser Runde.</p>
|
|
<p v-if="nextDueAt" class="pr-msg">Nächste Karten fällig: {{ naechsteFaelligkeit() }}</p>
|
|
<button
|
|
v-if="counts && counts.new_total > counts.new"
|
|
class="pr-mehr" @click="loadDeck"
|
|
>Weitere neue Karten üben</button>
|
|
<p v-if="counts && counts.gesperrt" class="pr-msg pr-faint">
|
|
{{ counts.gesperrt }} Karten schalten sich über Block-Prüfungen frei.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.pr-panel { padding: 16px 20px; max-width: 720px; margin: 0 auto; }
|
|
.pr-head { display: flex; align-items: center; gap: 14px; margin-bottom: 12px; }
|
|
.pr-head h2 { margin: 0; font-size: 1.1rem; }
|
|
.pr-counts { color: var(--text-muted); font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
.pr-rest { margin-left: auto; color: var(--text-faint); font-size: 0.85rem; font-variant-numeric: tabular-nums; }
|
|
.pr-msg { color: var(--text-muted); }
|
|
.pr-faint { color: var(--text-faint); font-size: 0.85rem; }
|
|
.pr-kontext { margin-bottom: 6px; color: var(--text-muted); font-size: 0.88rem; }
|
|
.pr-done { text-align: center; padding: 2.5rem 0; }
|
|
.pr-done-title { font-size: 1.15rem; font-weight: 700; margin-bottom: 0.6rem; }
|
|
.pr-mehr {
|
|
margin-top: 0.6rem; padding: 7px 14px; border: 1px solid var(--border-strong);
|
|
border-radius: 8px; background: var(--bg); cursor: pointer; font-weight: 600;
|
|
}
|
|
.pr-mehr:hover { color: var(--accent-hover); }
|
|
</style>
|