341 lines
20 KiB
PHP
341 lines
20 KiB
PHP
@php
|
|
$colorMap = [
|
|
'primary' => ['bg' => 'bg-primary-soft', 'ink' => 'text-primary-ink', 'bar' => 'bg-primary-ink', 'ring' => 'focus-visible:ring-primary', 'btn' => 'bg-primary-ink'],
|
|
'rose' => ['bg' => 'bg-rose-soft', 'ink' => 'text-rose-ink', 'bar' => 'bg-rose-ink', 'ring' => 'focus-visible:ring-rose-ink', 'btn' => 'bg-rose-ink'],
|
|
'violet' => ['bg' => 'bg-violet-soft', 'ink' => 'text-violet-ink', 'bar' => 'bg-violet-ink', 'ring' => 'focus-visible:ring-violet-ink','btn' => 'bg-violet-ink'],
|
|
'green' => ['bg' => 'bg-green-soft', 'ink' => 'text-green-ink', 'bar' => 'bg-green-ink', 'ring' => 'focus-visible:ring-green-ink', 'btn' => 'bg-green-ink'],
|
|
'amber' => ['bg' => 'bg-amber-soft', 'ink' => 'text-amber-ink', 'bar' => 'bg-amber-ink', 'ring' => 'focus-visible:ring-amber-ink', 'btn' => 'bg-amber-ink'],
|
|
];
|
|
$c = $colorMap[$task['color']] ?? $colorMap['primary'];
|
|
$progressPercent = $totalSteps > 0 ? (int) round(($currentStep / $totalSteps) * 100) : 0;
|
|
$q = $finished ? null : $this->currentQuestion();
|
|
$qType = $q['type'] ?? 'choice';
|
|
$answered = $lastAnswer !== null;
|
|
@endphp
|
|
|
|
<div class="flex flex-col gap-6 max-w-6xl mx-auto w-full" data-testid="task-run-page">
|
|
|
|
{{-- Breadcrumb / back nav --}}
|
|
<div class="flex items-center justify-between">
|
|
<a href="{{ route('tasks') }}" wire:navigate class="inline-flex items-center gap-2 text-sm font-semibold text-ink-2 hover:text-ink-1 transition-colors">
|
|
<x-heroicon-o-arrow-left class="w-4 h-4" />
|
|
Zurück zu Aufgaben
|
|
</a>
|
|
<div class="flex items-center gap-2">
|
|
<x-pill>{{ $task['subject'] ?? '' }}</x-pill>
|
|
<x-pill variant="primary">{{ $task['topic'] ?? '' }}</x-pill>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Task header --}}
|
|
<div class="bg-bg-soft border border-line rounded-xl p-5 shadow-sm flex items-center gap-4">
|
|
<div class="w-14 h-14 rounded-lg {{ $c['bg'] }} {{ $c['ink'] }} grid place-items-center font-bold text-2xl shrink-0">
|
|
{{ $task['icon'] }}
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="text-[11px] uppercase tracking-[0.12em] text-ink-3 font-semibold">{{ $task['meta'] }}</div>
|
|
<h1 class="font-bold text-xl text-ink-1 leading-tight truncate" data-testid="run-title">{{ $task['title'] }}</h1>
|
|
</div>
|
|
<div class="text-right shrink-0">
|
|
<div class="text-[10px] uppercase tracking-wider text-ink-3 font-semibold">Belohnung</div>
|
|
<div class="text-lg font-bold font-mono tabular-nums text-amber-ink">+{{ (int) $task['points'] }} Pkt.</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if(! $finished)
|
|
{{-- Progress --}}
|
|
<div>
|
|
<div class="flex justify-between items-baseline mb-2">
|
|
<span class="text-xs font-semibold text-ink-2 font-mono tabular-nums" data-testid="run-progress-label">
|
|
Frage {{ $currentStep }} / {{ $totalSteps }}
|
|
</span>
|
|
<span class="text-xs font-semibold text-green-ink font-mono tabular-nums" data-testid="run-correct-count">
|
|
{{ $correctAnswers }} richtig
|
|
</span>
|
|
</div>
|
|
<div class="h-2 rounded-full bg-bg-tint overflow-hidden">
|
|
<div class="h-full rounded-full {{ $c['bar'] }} transition-all duration-500" style="width:{{ $progressPercent }}%;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Workbook: Question left (2 cols), Lerni AI helper right --}}
|
|
<div class="grid grid-cols-3 gap-5">
|
|
|
|
{{-- Question card (col-span 2) --}}
|
|
<div class="col-span-2 bg-bg-soft border border-line rounded-xl p-8 shadow-sm" wire:key="step-{{ $currentStep }}">
|
|
<div class="text-[11px] uppercase tracking-[0.12em] text-ink-3 font-semibold mb-3" data-testid="run-q-type-{{ $qType }}">
|
|
@switch($qType)
|
|
@case('cloze') Lückentext @break
|
|
@case('geo-map') Klicke das Land @break
|
|
@default Aufgabe
|
|
@endswitch
|
|
</div>
|
|
<h2 class="text-2xl font-bold tracking-tight text-ink-1 mb-8 leading-snug" data-testid="run-question">
|
|
{{ $q['question'] }}
|
|
</h2>
|
|
|
|
{{-- ========== CHOICE ========== --}}
|
|
@if($qType === 'choice')
|
|
<div class="grid grid-cols-2 gap-3">
|
|
@foreach($q['options'] as $i => $opt)
|
|
@php
|
|
$isLast = $answered && ($lastAnswer['type'] ?? null) === 'choice' && ($lastAnswer['index'] ?? null) === $i;
|
|
$cls = 'group px-5 py-5 rounded-lg border-2 text-ink-1 font-semibold text-base text-left transition-all cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 ' . $c['ring'];
|
|
$cls .= ! $answered
|
|
? ' bg-bg-soft border-line hover:border-primary hover:bg-primary-soft'
|
|
: ($isLast
|
|
? ($lastAnswerCorrect ? ' bg-green-soft border-green-ink' : ' bg-rose-soft border-rose-ink')
|
|
: ' bg-bg-soft border-line opacity-60');
|
|
@endphp
|
|
<button
|
|
type="button"
|
|
@if($answered) disabled @else wire:click="submitChoice({{ $i }})" @endif
|
|
class="{{ $cls }}"
|
|
data-testid="run-option-{{ $i }}"
|
|
>
|
|
<span class="flex items-center gap-3">
|
|
<span class="w-7 h-7 rounded-full grid place-items-center text-xs font-bold font-mono tabular-nums shrink-0
|
|
{{ $isLast ? ($lastAnswerCorrect ? 'bg-green-ink text-white' : 'bg-rose-ink text-white') : 'bg-bg-tint text-ink-2' }}">
|
|
{{ chr(65 + $i) }}
|
|
</span>
|
|
<span>{{ $opt }}</span>
|
|
@if($isLast && $lastAnswerCorrect)
|
|
<x-heroicon-s-check-circle class="w-5 h-5 text-green-ink ml-auto shrink-0" />
|
|
@elseif($isLast)
|
|
<x-heroicon-s-x-circle class="w-5 h-5 text-rose-ink ml-auto shrink-0" />
|
|
@endif
|
|
</span>
|
|
</button>
|
|
@endforeach
|
|
</div>
|
|
|
|
{{-- ========== CLOZE ========== --}}
|
|
@elseif($qType === 'cloze')
|
|
<div class="bg-bg-tint rounded-lg p-5 mb-4 border border-line">
|
|
<div class="text-[10px] uppercase tracking-wider text-ink-3 font-semibold mb-2">Setze ein</div>
|
|
<div class="flex items-center gap-3 flex-wrap">
|
|
<input
|
|
type="text"
|
|
wire:model.live.debounce.150ms="clozeInput"
|
|
@if(! $answered) wire:keydown.enter="submitCloze" @endif
|
|
@if($answered) disabled @endif
|
|
autocomplete="off"
|
|
autocapitalize="none"
|
|
spellcheck="false"
|
|
class="flex-1 min-w-[180px] px-4 py-3 rounded-lg border-2 font-mono text-lg bg-bg-soft text-ink-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 {{ $c['ring'] }}
|
|
{{ $answered ? ($lastAnswerCorrect ? 'border-green-ink bg-green-soft' : 'border-rose-ink bg-rose-soft') : 'border-line focus:border-primary' }}"
|
|
placeholder="Antwort eingeben…"
|
|
data-testid="run-cloze-input"
|
|
/>
|
|
@if(! $answered)
|
|
<x-btn variant="primary" wire:click="submitCloze" data-testid="run-cloze-submit">
|
|
Prüfen
|
|
<x-heroicon-o-check class="w-4 h-4" />
|
|
</x-btn>
|
|
@endif
|
|
</div>
|
|
@if($answered)
|
|
<div class="mt-3 text-sm font-semibold {{ $lastAnswerCorrect ? 'text-green-ink' : 'text-rose-ink' }}" data-testid="run-cloze-feedback">
|
|
@if($lastAnswerCorrect)
|
|
<x-heroicon-s-check-circle class="w-4 h-4 inline" /> Richtig!
|
|
@else
|
|
<x-heroicon-s-x-circle class="w-4 h-4 inline" />
|
|
Leider falsch. Lösung: <span class="font-mono">{{ $q['correct'] }}</span>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- ========== GEO-MAP ========== --}}
|
|
@elseif($qType === 'geo-map')
|
|
@php
|
|
$viewBox = $q['viewBox'] ?? '0 0 400 320';
|
|
$picked = ($lastAnswer['type'] ?? null) === 'geo-map' ? ($lastAnswer['region'] ?? null) : null;
|
|
$correctKey = $q['correct'] ?? null;
|
|
@endphp
|
|
<div class="bg-primary-soft/30 rounded-lg p-4 border border-line">
|
|
<svg viewBox="{{ $viewBox }}" class="w-full h-auto max-h-[420px]" role="img" aria-label="Europa-Karte" data-testid="run-geomap">
|
|
@foreach($q['regions'] as $region)
|
|
@php
|
|
$rk = $region['key'];
|
|
$isPicked = $picked === $rk;
|
|
$isCorrect = $correctKey === $rk;
|
|
if (! $answered) {
|
|
$fill = '#E5E5DF'; $stroke = '#B4B4AC';
|
|
} elseif ($isPicked && $lastAnswerCorrect) {
|
|
$fill = 'oklch(0.92 0.06 155)'; $stroke = 'oklch(0.55 0.16 155)';
|
|
} elseif ($isPicked) {
|
|
$fill = 'oklch(0.92 0.06 25)'; $stroke = 'oklch(0.6 0.18 25)';
|
|
} elseif ($isCorrect) {
|
|
$fill = 'oklch(0.92 0.06 155)'; $stroke = 'oklch(0.55 0.16 155)';
|
|
} else {
|
|
$fill = '#E5E5DF'; $stroke = '#B4B4AC';
|
|
}
|
|
@endphp
|
|
<path
|
|
d="{{ $region['d'] }}"
|
|
fill="{{ $fill }}"
|
|
stroke="{{ $stroke }}"
|
|
stroke-width="2"
|
|
@if(! $answered)
|
|
wire:click="submitRegion('{{ $rk }}')"
|
|
class="cursor-pointer transition-all hover:opacity-80"
|
|
@endif
|
|
data-testid="run-geomap-region-{{ $rk }}"
|
|
>
|
|
<title>{{ $region['label'] }}</title>
|
|
</path>
|
|
@endforeach
|
|
</svg>
|
|
|
|
{{-- Legend / labels --}}
|
|
<div class="mt-4 grid grid-cols-3 gap-2 text-xs">
|
|
@foreach($q['regions'] as $region)
|
|
<button
|
|
type="button"
|
|
@if($answered) disabled @else wire:click="submitRegion('{{ $region['key'] }}')" @endif
|
|
class="px-3 py-2 rounded-md border font-semibold text-ink-2 transition-colors text-left
|
|
@if($answered && $region['key'] === ($picked ?: '') && $lastAnswerCorrect) bg-green-soft border-green-ink text-green-ink
|
|
@elseif($answered && $region['key'] === ($picked ?: '')) bg-rose-soft border-rose-ink text-rose-ink
|
|
@elseif($answered && $region['key'] === $correctKey) bg-green-soft border-green-ink text-green-ink
|
|
@else bg-bg-soft border-line hover:border-primary @endif"
|
|
data-testid="run-geomap-btn-{{ $region['key'] }}"
|
|
>
|
|
{{ $region['label'] }}
|
|
</button>
|
|
@endforeach
|
|
</div>
|
|
@if($answered)
|
|
<div class="mt-4 text-sm font-semibold {{ $lastAnswerCorrect ? 'text-green-ink' : 'text-rose-ink' }}" data-testid="run-geomap-feedback">
|
|
@if($lastAnswerCorrect)
|
|
<x-heroicon-s-check-circle class="w-4 h-4 inline" /> Richtig!
|
|
@else
|
|
<x-heroicon-s-x-circle class="w-4 h-4 inline" /> Leider falsch. Markiertes Land ist die Lösung.
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
@if($answered)
|
|
<div class="mt-6 flex justify-end">
|
|
<x-btn variant="primary" wire:click="nextStep" data-testid="run-next">
|
|
@if($currentStep < $totalSteps)
|
|
Nächste Frage
|
|
<x-heroicon-o-arrow-right class="w-4 h-4" />
|
|
@else
|
|
Aufgabe abschließen
|
|
<x-heroicon-o-check class="w-4 h-4" />
|
|
@endif
|
|
</x-btn>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- Lerni AI-Helper sidebar --}}
|
|
<aside class="bg-bg-soft border border-line rounded-xl shadow-sm overflow-hidden" data-testid="lerni-panel">
|
|
<header class="px-5 py-4 border-b border-line bg-bg-tint">
|
|
<div class="flex items-center gap-2.5">
|
|
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-primary-ink to-violet-ink text-white grid place-items-center font-bold shrink-0">
|
|
<x-heroicon-s-sparkles class="w-5 h-5" />
|
|
</div>
|
|
<div class="min-w-0">
|
|
<div class="font-bold text-sm text-ink-1">Lerni</div>
|
|
<div class="text-[10px] uppercase tracking-wider text-ink-3 font-semibold">KI-Helfer</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
@if(! $showLerni)
|
|
<div class="p-5 text-center">
|
|
<p class="text-sm text-ink-2 mb-4 leading-relaxed">
|
|
Steckst du fest? Ich erkläre dir Schritt für Schritt, wie du diese Aufgabe lösen kannst.
|
|
</p>
|
|
<x-btn variant="primary" wire:click="toggleLerni" class="w-full justify-center" data-testid="lerni-open">
|
|
<x-heroicon-o-sparkles class="w-4 h-4" />
|
|
Frag Lerni
|
|
</x-btn>
|
|
</div>
|
|
@else
|
|
<div class="p-5 flex flex-col gap-4">
|
|
<div class="bg-primary-soft border border-primary-soft rounded-lg p-4">
|
|
<div class="text-[10px] uppercase tracking-wider text-primary-ink font-bold mb-1">💡 Tipp</div>
|
|
<p class="text-sm text-ink-1 leading-relaxed" data-testid="lerni-hint">{{ $q['lerni_hint'] ?? 'Versuch es mit dem Lösungsweg unten.' }}</p>
|
|
</div>
|
|
|
|
@if(! empty($q['lerni_steps']))
|
|
<div>
|
|
<div class="text-[10px] uppercase tracking-wider text-ink-3 font-bold mb-2">Lösungsweg</div>
|
|
<ol class="flex flex-col gap-2" data-testid="lerni-steps">
|
|
@foreach($q['lerni_steps'] as $i => $step)
|
|
<li class="flex gap-2.5 items-start">
|
|
<span class="w-5 h-5 rounded-full {{ $c['btn'] }} text-white grid place-items-center text-[10px] font-bold font-mono shrink-0 mt-0.5">{{ $i + 1 }}</span>
|
|
<span class="text-sm text-ink-1 leading-snug">{{ $step }}</span>
|
|
</li>
|
|
@endforeach
|
|
</ol>
|
|
</div>
|
|
@endif
|
|
|
|
<button type="button" wire:click="toggleLerni"
|
|
class="text-xs text-ink-3 hover:text-ink-1 underline self-start transition-colors"
|
|
data-testid="lerni-close">
|
|
Lerni schließen
|
|
</button>
|
|
|
|
{{-- Shortlink share (for parents/teachers) --}}
|
|
<div class="mt-2 pt-3 border-t border-line">
|
|
<div class="text-[10px] uppercase tracking-wider text-ink-3 font-bold mb-1.5">Shortlink</div>
|
|
<code class="font-mono text-[11px] text-ink-2 break-all" data-testid="lerni-shortlink">{{ route('tasks.run', ['slug' => $slug]) }}?lerni=1</code>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</aside>
|
|
|
|
</div>
|
|
@else
|
|
@php
|
|
$passed = $correctAnswers >= (int) ceil($totalSteps / 2);
|
|
$percent = $totalSteps > 0 ? (int) round(($correctAnswers / $totalSteps) * 100) : 0;
|
|
$pointsEarned = $passed ? (int) $task['points'] : 0;
|
|
@endphp
|
|
<div class="bg-bg-soft border border-line rounded-xl p-10 shadow-sm text-center max-w-2xl mx-auto" data-testid="run-finished">
|
|
<div class="w-20 h-20 rounded-full grid place-items-center mx-auto mb-5 {{ $passed ? 'bg-green-soft text-green-ink' : 'bg-amber-soft text-amber-ink' }}">
|
|
@if($passed)
|
|
<x-heroicon-s-trophy class="w-10 h-10" />
|
|
@else
|
|
<x-heroicon-s-arrow-path class="w-10 h-10" />
|
|
@endif
|
|
</div>
|
|
<h2 class="text-2xl font-bold text-ink-1 mb-2">
|
|
@if($passed) Super gemacht! @else Fast geschafft @endif
|
|
</h2>
|
|
<p class="text-sm text-ink-2 mb-1">
|
|
Du hast <span class="font-mono tabular-nums font-bold text-ink-1">{{ $correctAnswers }}</span>
|
|
von <span class="font-mono tabular-nums font-bold text-ink-1">{{ $totalSteps }}</span> Aufgaben richtig.
|
|
</p>
|
|
<p class="text-xs text-ink-3 mb-6 font-mono tabular-nums">{{ $percent }} % Erfolgsquote</p>
|
|
|
|
@if($pointsEarned > 0)
|
|
<div class="bg-amber-soft border border-amber-soft rounded-lg px-4 py-3 inline-flex items-center gap-2 mb-6">
|
|
<x-heroicon-s-star class="w-5 h-5 text-amber-ink" />
|
|
<span class="font-mono tabular-nums font-bold text-amber-ink">+{{ $pointsEarned }} Pkt.</span>
|
|
<span class="text-xs text-amber-ink font-semibold">verdient</span>
|
|
</div>
|
|
@endif
|
|
|
|
<div class="flex gap-2 justify-center">
|
|
<x-btn wire:click="restart" data-testid="run-restart">
|
|
<x-heroicon-o-arrow-path class="w-4 h-4" />
|
|
Nochmal
|
|
</x-btn>
|
|
<x-btn variant="primary" as="a" href="{{ route('tasks') }}" wire:navigate>
|
|
<x-heroicon-o-check class="w-4 h-4" />
|
|
Fertig — zurück
|
|
</x-btn>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
</div>
|