Files
creator/frontend/src/components/PracticePanel.vue
2026-07-03 11:45:27 +02:00

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>