Commit Graph

6 Commits (main)

Author SHA1 Message Date
Boban Blaskovic 88945b47f6 feat(tasks): multi-type questions (cloze + geo-map) + Run.php type dispatch
Tasks Run page now switches on question $type and renders one of three layouts:
choice (existing multiple-choice), cloze (Lückentext text input), geo-map
(clickable SVG region picker). Adds Deutsch-Lückentexte + Geografie-Europa
question banks with the required type-specific keys.

- Run.php: replaces submitAnswer/lastAnswerIndex with submitChoice + submitCloze
  + submitRegion. lastAnswer is now an array { type, ...payload }. Cross-type
  calls are no-ops so a stray dispatch can't pollute state.
- run.blade.php: branches on $q['type']; cloze has wire:model.live debounce
  + Enter-to-submit; geo-map has SVG + accessible label-button grid; feedback
  pill below each layout.
- config/tasks.php: adds deutsch-lueckentexte + geografie-europa-laender.
- config/task-questions.php: type=cloze entries with correct+accept[] for
  case-insensitive alt-spelling match; type=geo-map entries with regions[]
  and SVG paths.
- Tests: renames submitAnswer→submitChoice across run page suite; adds full
  coverage for cloze (correct/wrong/empty/case/accept[]/payload shape/clear
  on next), geo-map (correct/wrong/payload/SVG render), and cross-type
  guards. TaskQuestionBank test is now type-aware (choice needs options[4],
  cloze needs correct string, geo-map needs regions[]+matching correct key).

Test: 204/204 Pest green. Smoke: 10/10 task routes 200, 4/4 modal components
mount cleanly, no errors written to laravel.log during the smoke pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 19:28:09 +02:00
Boban Blaskovic d3f976b18e feat(tasks): semantic slugs + per-slug question banks + Lerni AI-helper
Before:
  - Task slug "t1" was generic, meaningless to user and crawlers.
  - Question bank was keyed by `color`, so EVERY math task (multiplikation,
    geometrie, ...) shared identical questions.
  - No way for child / parent to ask AI for an explanation step-by-step.

Now (Workbook + Lerni variant from Übungsseite template):

Semantic slugs in config/tasks.php:
  math-multiplikation-bis-100
  math-geometrie-flaechen
  deutsch-wortarten-nomen-verben
  deutsch-gedicht-lernen
  englisch-vocab-animals
  englisch-past-simple
  sachkunde-wasserkreislauf

Each task now has:
  slug, title, topic, subject, meta, icon, color, points, status, due

config/task-questions.php re-keyed by `slug` (not color):
  Every task gets its OWN question set — math-multiplikation has
  multiplication questions, math-geometrie has area/perimeter, deutsch
  has wortarten/grammar, etc.

Each question now has 5 keys:
  question, options[4], correct, lerni_hint, lerni_steps[]

Lerni AI-helper (right sidebar in Workbook layout):
  - Closed by default; "Frag Lerni" button reveals
  - Hint box (💡) with short text-explanation
  - Numbered Lösungsweg with method-steps (colored badges per subject)
  - Shortlink URL `?lerni=1` (parents/teachers can deep-link to a
    pre-opened explanation)
  - #[Url(as: 'lerni')] hydrates from URL
  - Auto-closes on nextStep (fresh question = fresh thinking)

Route updated:
  Route::get('/tasks/{slug}/run', \App\Livewire\Tasks\Run::class)
    ->where('slug', '[a-z0-9-]+')   // tighter than whereAlphaNumeric;
                                     // allows hyphens, rejects UPPER
    ->name('tasks.run')

Tasks/Index::startTask / repeatTask: $slug arg + redirectRoute to
tasks.run with slug.

View livewire/tasks/run.blade.php switched from single column to
2-column grid (max-w-6xl): col-span-2 question card + col-span-1
Lerni aside. Per-subject color theming (focus-ring, progress bar,
step badges, option border on feedback).

Cleanup:
  - Removed App\Livewire\Tasks\Modals\Runner (obsolete; page-based now)
  - Removed livewire.tasks.modals.runner view
  - Removed TasksRunnerModalTest

Tests (TasksRunPageTest 22, TaskQuestionBankTest 6, TasksStartButton 8):
  - Semantic slug validation (no t[0-9]+ patterns)
  - Per-slug question selection (math vs deutsch vs english vs sachkunde)
  - math-multiplikation != math-geometrie (different content same color)
  - Lerni-panel renders; lerni-open trigger; toggleLerni reveals steps
  - Shortlink contains slug + ?lerni=1
  - ?lerni=1 query auto-opens helper
  - Route 404 for UPPER chars and unknown slugs
  - startTask redirects to tasks.run with slug

Verification:
  - 188 tests passed, 0 failed, 0 skipped (1024 assertions)
  - npm build OK
  - All 8 page routes 302 unauth
  - All 7 task slug routes 302 unauth
  - tasks/UPPER/run 404 (route regex)
  - 7 modal aliases resolve (Runner removed)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 22:48:05 +02:00
Boban Blaskovic cae775ba93 feat(tasks): dedicated /tasks/{key}/run page with per-subject question bank
Before: tasks.modals.runner showed the same 4 multiplication questions
regardless of subject (Mathe / Deutsch / Englisch / Sachkunde / Musik).
Made no sense — Englisch task shouldn't ask 12 × 8.

Now: dedicated full-page route /tasks/{key}/run loads the subject-correct
question bank based on the task's `color` token, with structured URL,
proper auth + license middleware, and Lazy skeleton placeholder.

New artifacts:

  config/task-questions.php  — Question bank keyed by color/subject:
    primary -> Mathematik (5 multiplication / division questions)
    rose    -> Deutsch (5 grammar/wortart/artikel questions)
    violet  -> Englisch (5 vocab/grammar in English)
    green   -> Sachkunde (5 nature/animals/space)
    amber   -> Musik (5 musical notation / theory)

  config/tasks.php  — Placeholder task list extracted from Index so
    both Tasks\Index (list page) and Tasks\Run (quiz page) share data.
    Future: replace with DB-backed Task model.

  app/Livewire/Tasks/Run.php
    - Lazy page-component, #[Layout('layouts.dashboard', active=tasks)]
    - mount(string $key) -> abort 404 if task or question bank missing
    - findTask() reads config('tasks')
    - questions = config("task-questions.{$task['color']}")
    - submitAnswer(int) records lastAnswerIndex + lastAnswerCorrect +
      tallies correctAnswers (visual feedback before advance)
    - nextStep() advances or sets finished=true on final
    - restart() resets all progress
    - placeholder() returns livewire.tasks.run-placeholder (skeleton)

  resources/views/livewire/tasks/run.blade.php
    - Back link + task header card (icon, meta, title, points)
    - Progress bar (Frage X / Y + correct counter)
    - Question card with 2x2 option grid, A/B/C/D labels
    - Per-color theme (primary/rose/violet/green/amber bar + ring)
    - Two-step interaction: pick option -> show correct/wrong feedback ->
      "Nächste Frage" / "Aufgabe abschließen"
    - Finished card: trophy or arrow-path; points earned; restart / done
    - All buttons use design tokens + heroicons (no inline SVG)

  resources/views/livewire/tasks/run-placeholder.blade.php
    - Skeleton with same layout shape (back link, header card, question)
    - data-testid="task-run-skeleton"

  routes/web.php
    Route::get('/tasks/{key}/run', \App\Livewire\Tasks\Run::class)
      ->whereAlphaNumeric('key')->name('tasks.run')
    Behind auth + license group like other dashboard pages.

  Tasks/Index::startTask() simplified:
    $this->redirectRoute('tasks.run', ['key' => $key], navigate: true)
    (Server-side redirect; client navigates without full reload.)

Security:
- whereAlphaNumeric('key') restricts URL param to safe chars
- abort_if($task === null, 404) prevents directory probing
- auth+license middleware applies to whole route group
- Question bank is config (no user-supplied content interpolated)
- Answer index validated against question[correct] only — no dynamic eval

Tests (27 new + 0 broken):
- TaskQuestionBankTest (4): config exists; 5 buckets; required keys;
  per-color content differs
- TasksRunPageTest (15): route registered; auth happy path; 404 for
  unknown key; per-color question selection (math/deutsch/eng/sachkunde);
  submitAnswer correct/wrong; nextStep advances + clears feedback;
  finishing 5 steps -> finished=true; restart resets; Index.startTask
  redirects; unauth redirected to login; placeholder testid
- TasksStartButtonTest updated: Start now redirects, no longer
  dispatches modal

Full verification:
- 186 tests passed, 0 failed, 0 skipped (661 assertions)
- npm build OK, manifest generated
- All 8 page routes return 302 unauth (correct)
- All 5 /tasks/{key}/run routes return 302 unauth (correct)
- All 8 modal aliases still resolve (legacy Runner modal kept)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 22:13:32 +02:00
Boban Blaskovic 7c6ea8d9f0 feat(tasks): separate Runner modal — Start actually runs the task
Before: Both info-icon (Details) and Start button opened the same
modal (tasks.modals.detail). Made no sense — Start should DO the task,
not just show metadata.

Now:
- Info-icon (i)     -> tasks.modals.detail  (read-only metadata)
- Start / Wiederholen-> tasks.modals.runner (interactive quiz)

New: App\Livewire\Tasks\Modals\Runner

Multi-step quiz modal with:
- 4-step progress bar (Frage X / Y + colored bar)
- Question + 4 answer options (2x2 grid, Tailwind cards)
- submitAnswer(int $option) advances step + tallies correctAnswers
- finished state shows trophy (passed) or arrow-path (partial)
- restart() resets quiz to step 1
- closeModalOnClickAway() = false (forces explicit cancel/finish)
- 2xl max-width for comfortable reading

Tasks/Index::startTask() now dispatches tasks.modals.runner via
$this->js("Livewire.dispatch('openModal', ...)") — same dispatch pattern
as before, different target component.

Placeholder data: 4 multiplication questions hardcoded. Future iteration
should load task content from DB (TaskItem / Question models).

Tests:
- TasksRunnerModalTest (10 tests, 25 assertions):
    * Runner extends ModalComponent + alias resolves
    * Step 1 renders question + 4 options
    * Correct answer increments correctAnswers + advances step
    * Wrong answer advances step without point
    * Final step sets finished=true
    * restart() resets state
    * closeModalOnClickAway = false
    * Progress label "Frage X / Y"
    * Success path shows trophy
- TasksStartButtonTest updated:
    * Asserts startTask emits dispatch with 'tasks.modals.runner'
      (NOT 'tasks.modals.detail')
    * Asserts info-button still routes to detail
- 167 tests passed, 0 failed, 0 skipped

Verification:
- All 8 modal aliases resolve via livewire.finder
- All 8 page routes return 302 unauth
- npm build OK
- No HTTP / no console errors expected

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:59:51 +02:00
Boban Blaskovic a6c44fd169 fix(ui): per-page skeletons, photo Alpine scope, tasks modal dispatch
Bug fixes (browser console + UX):

1. Photo /photo: "Alpine Expression Error: preview is not defined" during
   Lazy hydration. Placeholder now pre-declares the same x-data scope as
   the real view (file, preview, status) so Alpine bindings on children
   resolve from first paint and survive the morph.

2. Tasks Start button: "Unable to resolve dependency [Parameter #0 $component]
   in LivewireUI\\Modal\\Modal". Livewire 4 server-side dispatch with
   ->to('livewire-ui-modal') drops named payload args. Switched to
   $this->js("Livewire.dispatch('openModal', {...})") which emits a
   browser-side dispatch that wire-elements reliably picks up.

3. Skeleton was generic across all pages. Replaced single
   components.skeleton.page placeholder with page-specific placeholders that
   mirror each view's actual layout (hero header, stat-tile grid,
   subject-card grid, table rows, sub-nav, etc.).

   New skeleton primitives:
     components/skeleton/header.blade.php
     components/skeleton/stat-tile.blade.php
     components/skeleton/subject-card.blade.php
     components/skeleton/table-row.blade.php
     components/skeleton/card.blade.php

   Per-page placeholders:
     livewire/{dashboard,subjects,tasks,photo-help,rewards,progress,
               notifications,settings}/placeholder.blade.php

   Each placeholder() now returns its feature-local view; each placeholder
   has a unique data-testid (dashboard-skeleton, tasks-skeleton, etc.)

Tests:
- PerPageSkeletonTest (18 assertions): every page returns its own
  placeholder view, each has unique testid, photo-help placeholder
  declares preview in x-data.
- TasksStartButtonTest updated: assertScriptRan -> assertion against
  Livewire effects xjs key (correct Livewire 4 API).
- LazySkeletonTest removed (superseded by PerPageSkeletonTest).

Verification:
- 156 tests passed, 0 failed, 0 skipped (390 assertions)
- All 8 routes return 302 unauth (auth middleware correct)
- All 7 modal aliases resolve correctly via livewire.finder
- Build artifacts: manifest.json, logo/favicon.svg, manifest.webmanifest
  all reachable

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:51:28 +02:00
Boban Blaskovic 7e6d0d4ab4 feat(ui): dashboard MVP — feature-folders, lazy skeletons, wire-elements modals, brand logos
Major refactor + UX polish for authenticated dashboard MVP.

Architecture:
- Feature-based folder structure: app/Livewire/<Feature>/{Index, Modals/*}
  Dashboard, Subjects, Tasks, PhotoHelp, Rewards, Progress, Notifications, Settings
- Matching view structure: resources/views/livewire/<feature>/{index, modals/*}.blade.php
- Removed legacy Pages/ and Modals/ top-level folders

Modals (wire-elements/modal v3):
- 7 modal components extending LivewireUI\Modal\ModalComponent
- Vendor view override: z-50 (above sidebar z-20), items-center justify-center,
  bg-bg-soft surface, role=dialog + aria-modal, design-token-driven backdrop
- Server-side dispatch uses ->to('livewire-ui-modal') for cross-component routing

URL state + skeleton:
- Tasks + Notifications + Settings use #[Url(as, keep:true)] with allowlist
  validation in mount()/setSection()/setFilter() to block injection
- Tasks startTask + repeatTask actions dispatch openModal targeted at modal
- All 8 main pages declare #[Lazy] + placeholder() returning a page-level
  skeleton (animate-pulse, bg-bg-soft container, bg-line bars) so first paint
  is structured instead of blank

Skeleton/no-jump:
- Skeleton container uses bg-bg-soft (matches table card surface) — no color
  jump between skeleton and resolved table
- Settings sub-nav renders active state server-side (no Alpine :class flicker)
  with @if-server-render instead of x-show
- Font flicker eliminated: font-display: block + <link rel="preload"> on both
  layouts for Plus Jakarta Sans + JetBrains Mono

Logos / PWA:
- public/logo/, public/icons/, public/manifest.webmanifest deployed per
  Logos/DEPLOY.md
- Brand component (default / wordmark / icon-only variants) with href prop
- Favicon SVG/ICO + apple-touch-icon + manifest tags in dashboard + guest layouts
- Sidebar brand wires to route('dashboard')

Layout / a11y:
- Sidebar fixed left (top-0, h-screen, w-60) with header+footer shrink-0 and
  scrollable content
- Topbar sticky with z-10
- Sidebar uses x-layout.brand :href="route('dashboard')"
- Logout: POST /logout route + sidebar-user widget with @csrf form button

Bug fixes:
- Tasks rows are passive divs (not buttons) with explicit info-icon + Start
  button — eliminates nested-button invalid HTML
- Tasks Start/Wiederholen buttons wire:click to startTask/repeatTask actions
  (previously did nothing)
- Modals centered + above sidebar z-index
- Settings section sub-nav no longer flickers on hydration
- public/hot cleanup: Vite runs as docker compose service with auto-restart

Test infrastructure:
- Pest beforeEach calls Livewire::withoutLazyLoading() so #[Lazy] components
  hydrate fully under test
- 157 tests passing, 0 skipped, 0 failed (411 assertions)
- New test files:
  AssetReachabilityTest, BrandAssetsTest, FontLoadingTest, LazySkeletonTest,
  LogoutTest, ModalLayoutTest, ModalsTest, NoNestedButtonsTest,
  SettingsNavTest, SidebarPagesTest, TasksStartButtonTest, UrlStateTest

Config:
- config/sidebar.php as single source of truth for sidebar nav items
- config/reverb.php allowed_origins set
- Test DB isolated: phpunit.xml DB_DATABASE=lernschiff_test +
  tests/TestCase.php refreshApplication override

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:36:04 +02:00