Fáze požadavku: 9. Blade & Livewire prezentace
Modul 5 · Lekce 3

Kdy použít Livewire (server) a kdy Alpine.js (okamžitá odezva v prohlížeči)

💡 Shrnutí lekce

Livewire a Alpine.js tvoří perfektní tandem. Pochopte hranici mezi operacemi vyžadujícími server (validace, databáze, autorizace) a čistě klientskými mikro-interakcemi (kopírování do schránky, přepínání tabů, animace).

TALL Stack: Dokonalá symbióza Livewire a Alpine.js

V moderním ekosystému Laravelu tvoří kombinace Tailwind CSS + Alpine.js + Laravel + Livewire (tzv. TALL stack) jeden z nejproduktivnějších vývojářských nástrojů současnosti.

Častou chybou méně zkušených vývojářů je však snaha řešit veškerou interaktivitu na serveru přes Livewire. Odesílat síťový požadavek na server kvůli rozbalení dropdown menu nebo přepnutí záložky je architektonický antipattern, který vede ke zbytečné latenci a zatížení serveru.

Pojďme se podívat na to, jak je tato dělící linie navržena v naší aplikaci.

1. Klientská mikro-interaktivita: Alpine.js (0 ms latence)

Alpine.js je lehký JavaScriptový framework (cca 15 kB), který vkládá reaktivitu přímo do HTML atributů podobně jako Vue, ale bez nutnosti build stepu nebo virtuálního DOMu.

Podívejte se do souboru resources/views/course/lesson.blade.php:

Příklad A: Kopírování úryvku kódu do schránky

<div class="rounded-xl bg-slate-950" x-data="{ copied: false }">
    <button type="button"
            @click="navigator.clipboard.writeText($refs.codeSnippet.innerText); copied = true; setTimeout(() => copied = false, 2000)">
        <span x-show="!copied">Kopírovat kód</span>
        <span x-show="copied" class="text-emerald-400">✓ Zkopírováno!</span>
    </button>
    <pre><code x-ref="codeSnippet">{{ $lesson->code_snippet }}</code></pre>
</div>
  • Pomocí x-data="{ copied: false }" deklarujeme lokální reaktivní stav komponenty.
  • Direktiva @click (zkratka pro x-on:click) zavolá nativní Web Clipboard API prohlížeče.
  • Stav se změní okamžitě na klientovi – uživatel vidí potvrzení „✓ Zkopírováno!“ s nulovou latencí a server o tom vůbec nemusí vědět.

Příklad B: Přepínání záložek anatomie

Pravý panel anatomie zobrazuje několik souborů. Všechny soubory jsou vyrenderovány do DOMu a přepínání mezi nimi řídí Alpine:

<div x-data="{ activeTab: 0 }">
    <button @click="activeTab = 0">bootstrap/app.php</button>
    <button @click="activeTab = 1">routes/web.php</button>

    <div x-show="activeTab === 0">...kód 1...</div>
    <div x-show="activeTab === 1" style="display: none;">...kód 2...</div>
</div>

Přepnutí záložky proběhne za 0 milisekund bez ohledu na rychlost internetového připojení.

2. Kdy naopak MUSÍ nastoupit Livewire (Server)

Serverovou komponentu Livewire použijeme vždy, když:

  1. Ověřujeme bezpečnost a byznys pravidla: Při vyhodnocení kvízu student nesmí mít v JavaScriptu k dispozici pole se správnými odpověďmi (correct_option), jinak by mohl výsledek snadno podvrhnout v DevTools.
  2. Přistupujeme k databázi nebo storage: Zápis dokončené lekce a skóre do PostgreSQL (student_profiles).
  3. Provádíme složité serverové operace: Čtení a filtrování souborů z disku (AnatomyInspectorService), volání externích API nebo odesílání e-mailů.

3. Zlaté pravidlo architektury

Úloha Správný nástroj Důvod
Otevření / zavření modálního okna nebo menu Alpine.js Čistě vizuální stav klienta (0 ms latence)
Kopírování do schránky, tooltipy, akordeony Alpine.js Využívá Browser API, server není potřeba
Odeslání formuláře s validací do databáze Livewire Zabezpečení, práce s PostgreSQL
Vyhodnocení kvízu a uložení postupu Livewire Utajení správných odpovědí, autorizace
Dynamické vyhledávání v databázi Livewire Serverové SQL dotazy s ochranou před SQLi
💻 Klíčová konstrukce z praxe
{{-- resources/views/course/lesson.blade.php --}}

{{-- 1. Klient: Okamžitá odezva přes Alpine.js (žádný síťový požadavek) --}}
<div class="rounded-xl bg-slate-950 border border-slate-800" x-data="{ copied: false }">
    <div class="flex items-center justify-between px-4 py-2.5 bg-slate-900 border-b border-slate-800">
        <span class="font-mono text-slate-300 font-semibold">💻 Klíčová konstrukce z praxe</span>
        <button type="button"
                @click="navigator.clipboard.writeText($refs.codeSnippet.innerText); copied = true; setTimeout(() => copied = false, 2000)"
                class="px-2.5 py-1 rounded bg-slate-800 hover:bg-slate-700 text-[11px] font-mono text-slate-300 transition">
            <span x-show="!copied">Kopírovat kód</span>
            <span x-show="copied" class="text-emerald-400" style="display: none;">✓ Zkopírováno!</span>
        </button>
    </div>
    <pre class="p-4 text-xs font-mono text-emerald-300"><code x-ref="codeSnippet">{{ $lesson->code_snippet }}</code></pre>
</div>

{{-- 2. Server: Bezpečná logika a uložení postupu do databáze přes Livewire --}}
<div class="pt-4">
    <livewire:lesson-quiz :lesson="$lesson" />
</div>

🎯 Ověření znalostí z lekce

Kvíz otestuje pochopení architektury a reálného kódu probíraného v této lekci.

Otázka 1 z 3

Proč je přepínání záložek anatomie v pravém sloupci lesson.blade.php implementováno pomocí Alpine.js (x-data="{ activeTab: 0 }") namísto Livewire?

Otázka 2 z 3

Co dělá direktiva @click v kontextu šablony využívající Alpine.js?

Otázka 3 z 3

Která z následujících operací by MĚLA být zpracována na serveru přes Livewire namísto čistého Alpine.js?

Zodpovězeno: 0 / 3
🔬 Živá anatomie v této aplikaci
2 soubory
Na co se v kódu zaměřit: V resources/views/course/lesson.blade.php prozkoumejte řádky 59–75 (Alpine komponenta pro kopírování kódu) a řádky 117–158 (Alpine přepínání tabů activeTab). Srovnejte to s resources/views/livewire/lesson-quiz.blade.php, kde se kliknutí wire:click="submitQuiz" odesílá na server.
cesta: resources/views/livewire/lesson-quiz.blade.php
198 řádků · 11.5 KB
Soubor aplikační architektury v modulu Blade View.
1 <div class="rounded-2xl bg-slate-900 border border-slate-800 p-6 space-y-6 shadow-xl">
2
3 {{-- Horní stavová lišta dokončení lekce a skóre --}}
4 <div class="flex flex-wrap items-center justify-between gap-4 pb-4 border-b border-slate-800">
5 <div>
6 <h3 class="text-base font-bold text-white flex items-center gap-2">
7 <span>🎯</span>
8 <span>Ověření znalostí z lekce</span>
9 </h3>
10 <p class="text-xs text-slate-400 mt-0.5">
11 Kvíz otestuje pochopení architektury a reálného kódu probíraného v této lekci.
12 </p>
13 </div>
14
15 <div class="flex items-center gap-3">
16 @if($bestScore !== null)
17 <div class="px-3 py-1 rounded-lg bg-amber-500/10 border border-amber-500/20 text-xs font-mono text-amber-300">
18 Nejlepší skóre: <strong class="font-bold">{{ $bestScore }}%</strong>
19 </div>
20 @endif
21
22 {{-- Tlačítko pro ruční označení / odznačení lekce --}}
23 <button type="button"
24 wire:click="toggleComplete"
25 wire:loading.attr="disabled"
26 class="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-lg text-xs font-semibold transition focus:outline-none focus:ring-2 focus:ring-indigo-500 {{ $isCompleted ? 'bg-emerald-600 hover:bg-emerald-500 text-white shadow-sm' : 'bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700' }}">
27 <span wire:loading.remove wire:target="toggleComplete">
28 @if($isCompleted)
29 <span>✓ Lekce dokončena</span>
30 @else
31 <span>Označit jako dokončené</span>
32 @endif
33 </span>
34 <span wire:loading wire:target="toggleComplete" class="text-xs">
35 Ukládám...
36 </span>
37 </button>
38 </div>
39 </div>
40
41 {{-- Kvízové otázky --}}
42 @if($lesson->quizQuestions->isEmpty())
43 <div class="p-6 text-center rounded-xl bg-slate-950 border border-slate-800 space-y-2">
44 <p class="text-xs text-slate-400">
45 Tato lekce neobsahuje samostatný kvíz. Můžete ji rovnou označit jako dokončenou tlačítkem výše.
46 </p>
47 </div>
48 @else
49 <div class="space-y-6">
50 @foreach($lesson->quizQuestions as $qIndex => $question)
51 @php
52 $userAnswer = $selectedAnswers[$question->id] ?? null;
53 $isCorrect = $submitted && $userAnswer !== null && (int) $userAnswer === (int) $question->correct_option;
54 $isIncorrect = $submitted && $userAnswer !== null && (int) $userAnswer !== (int) $question->correct_option;
55 @endphp
56 <div class="rounded-xl bg-slate-950 border {{ $submitted ? ($isCorrect ? 'border-emerald-800/80 bg-emerald-950/20' : ($isIncorrect ? 'border-rose-800/80 bg-rose-950/20' : 'border-slate-800')) : 'border-slate-800' }} p-5 space-y-4">
57 {{-- Text otázky --}}
58 <div class="flex items-start justify-between gap-4">
59 <div class="space-y-1">
60 <span class="text-[11px] font-mono font-bold text-indigo-400 uppercase tracking-wider">
61 Otázka {{ $qIndex + 1 }} z {{ $lesson->quizQuestions->count() }}
62 </span>
63 <h4 class="text-sm font-semibold text-white">
64 {{ $question->question }}
65 </h4>
66 </div>
67
68 @if($submitted)
69 @if($isCorrect)
70 <span class="shrink-0 px-2 py-0.5 rounded text-[10px] font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/30">
71 ✓ Správně
72 </span>
73 @elseif($isIncorrect)
74 <span class="shrink-0 px-2 py-0.5 rounded text-[10px] font-bold bg-rose-500/20 text-rose-300 border border-rose-500/30">
75 ✗ Chybně
76 </span>
77 @else
78 <span class="shrink-0 px-2 py-0.5 rounded text-[10px] font-bold bg-slate-800 text-slate-400">
79 Nezodpovězeno
80 </span>
81 @endif
82 @endif
83 </div>
84
85 {{-- Možnosti odpovědí --}}
86 <div class="space-y-2">
87 @foreach($question->options as $optIndex => $optionText)
88 @php
89 $isSelected = $userAnswer !== null && (int) $userAnswer === (int) $optIndex;
90 $isThisCorrect = (int) $optIndex === (int) $question->correct_option;
91
92 if ($submitted) {
93 if ($isThisCorrect) {
94 $btnClass = 'bg-emerald-950/60 border-emerald-500 text-emerald-200 ring-1 ring-emerald-500/40 font-medium';
95 } elseif ($isSelected && ! $isThisCorrect) {
96 $btnClass = 'bg-rose-950/60 border-rose-500 text-rose-200 ring-1 ring-rose-500/40';
97 } else {
98 $btnClass = 'bg-slate-900/40 border-slate-800 text-slate-500 opacity-60 cursor-not-allowed';
99 }
100 } else {
101 if ($isSelected) {
102 $btnClass = 'bg-indigo-950/80 border-indigo-500 text-white font-medium ring-1 ring-indigo-500/50';
103 } else {
104 $btnClass = 'bg-slate-900 border-slate-800 hover:border-slate-700 text-slate-300 hover:text-white';
105 }
106 }
107 @endphp
108
109 <button type="button"
110 wire:click="selectOption({{ $question->id }}, {{ $optIndex }})"
111 @if($submitted) disabled @endif
112 class="w-full text-left p-3 rounded-lg border text-xs transition flex items-center justify-between gap-3 {{ $btnClass }}">
113 <div class="flex items-center gap-3">
114 <span class="w-5 h-5 rounded-full flex items-center justify-center font-mono text-[10px] shrink-0 {{ $isSelected ? 'bg-indigo-500 text-white font-bold' : 'bg-slate-800 text-slate-400' }}">
115 {{ chr(65 + $optIndex) }}
116 </span>
117 <span>{{ $optionText }}</span>
118 </div>
119
120 @if($submitted)
121 @if($isThisCorrect)
122 <span class="text-emerald-400 font-bold shrink-0">✓</span>
123 @elseif($isSelected && ! $isThisCorrect)
124 <span class="text-rose-400 font-bold shrink-0">✗</span>
125 @endif
126 @endif
127 </button>
128 @endforeach
129 </div>
130
131 {{-- Vysvětlení správné odpovědi po odeslání --}}
132 @if($submitted && !empty($question->explanation))
133 <div class="mt-3 p-3 rounded-lg bg-slate-900 border border-slate-800 text-xs text-slate-300 space-y-1">
134 <span class="text-[11px] font-semibold text-amber-400 block font-mono">
135 💡 Architektonické vysvětlení:
136 </span>
137 <p class="leading-relaxed text-slate-300">
138 {{ $question->explanation }}
139 </p>
140 </div>
141 @endif
142 </div>
143 @endforeach
144 </div>
145
146 {{-- Akce kvízu: Vyhodnotit / Výsledky / Zkusit znovu --}}
147 <div class="pt-4 border-t border-slate-800">
148 @if(! $submitted)
149 <div class="flex flex-wrap items-center justify-between gap-4">
150 <span class="text-xs text-slate-400 font-mono">
151 Zodpovězeno: {{ count($selectedAnswers) }} / {{ $lesson->quizQuestions->count() }}
152 </span>
153
154 <button type="button"
155 wire:click="submitQuiz"
156 wire:loading.attr="disabled"
157 @if(count($selectedAnswers) === 0) disabled @endif
158 class="px-5 py-2.5 rounded-lg bg-indigo-600 hover:bg-indigo-500 disabled:bg-slate-800 disabled:text-slate-500 disabled:cursor-not-allowed text-white font-semibold text-xs transition shadow-sm flex items-center gap-2">
159 <span wire:loading.remove wire:target="submitQuiz">Vyhodnotit kvíz</span>
160 <span wire:loading wire:target="submitQuiz">Vyhodnocuji...</span>
161 <span>&rarr;</span>
162 </button>
163 </div>
164 @else
165 <div class="rounded-xl bg-slate-950 border border-slate-800 p-5 space-y-4">
166 <div class="flex flex-wrap items-center justify-between gap-4">
167 <div>
168 <div class="text-xs text-slate-400">Výsledek kvízu:</div>
169 <div class="text-2xl font-bold font-mono {{ $scorePercent >= 70 ? 'text-emerald-400' : 'text-amber-400' }}">
170 {{ $scorePercent }}%
171 <span class="text-xs text-slate-400 font-normal ml-2">
172 ({{ $correctCount }} ze {{ $lesson->quizQuestions->count() }} správně)
173 </span>
174 </div>
175 </div>
176
177 <button type="button"
178 wire:click="resetQuiz"
179 class="px-4 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-xs font-semibold text-slate-200 transition">
180 🔄 Zkusit kvíz znovu
181 </button>
182 </div>
183
184 <p class="text-xs text-slate-300 leading-relaxed">
185 @if($scorePercent === 100)
186 🎉 <strong class="text-white">Perfektní práce!</strong> Všechny otázky máte správně. Architekturu v této lekci ovládáte na výbornou.
187 @elseif($scorePercent >= 70)
188 👍 <strong class="text-white">Skvělý výsledek!</strong> Kvíz jste úspěšně zvládli. Pokud chcete dosáhnout 100 %, projděte si vysvětlení výše a zkuste kvíz znovu.
189 @else
190 📖 <strong class="text-amber-300">Doporučujeme zopakovat.</strong> Prohlédněte si zdrojové kódy v pravém panelu anatomie a zkuste kvíz znovu.
191 @endif
192 </p>
193 </div>
194 @endif
195 </div>
196 @endif
197
198 </div>
199
cesta: resources/views/course/lesson.blade.php
229 řádků · 12.9 KB
Soubor aplikační architektury v modulu Blade View.
1 @extends('layouts.app')
2
3 @section('content')
4 <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-6">
5
6 {{-- Drobečková navigace & fáze --}}
7 <div class="flex flex-wrap items-center justify-between gap-4 pb-4 border-b border-slate-800">
8 <nav class="flex items-center gap-2 text-xs text-slate-400 font-medium">
9 <a href="{{ route('course.index') }}" class="hover:text-white transition">📚 Osnova</a>
10 <span>/</span>
11 <a href="{{ route('course.module', $module) }}" class="hover:text-white transition">Modul {{ $module->number }}: {{ $module->title }}</a>
12 <span>/</span>
13 <span class="text-slate-200">Lekce {{ $lesson->number }}</span>
14 </nav>
15
16 @if($lesson->lifecycle_phase)
17 <div class="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg text-xs font-medium bg-indigo-500/10 text-indigo-300 border border-indigo-500/20">
18 <span class="w-1.5 h-1.5 rounded-full bg-indigo-400"></span>
19 <span>Fáze požadavku: {{ $lesson->lifecycle_phase->label() }}</span>
20 </div>
21 @endif
22 </div>
23
24 {{-- Titulek lekce --}}
25 <div class="space-y-2">
26 <div class="flex items-center gap-2 text-xs font-mono text-indigo-400 font-bold uppercase tracking-wider">
27 Modul {{ $module->number }} · Lekce {{ $lesson->number }}
28 </div>
29 <h1 class="text-2xl sm:text-3xl font-bold text-white tracking-tight">
30 {{ $lesson->title }}
31 </h1>
32 </div>
33
34 {{-- Dvousloupcový layout lekce --}}
35 <div class="lg:grid lg:grid-cols-12 lg:gap-8 items-start">
36
37 {{-- Levý hlavní sloupec: Výklad, Klíčová konstrukce a Kvíz --}}
38 <div class="lg:col-span-7 space-y-8">
39
40 {{-- Shrnutí lekce --}}
41 <div class="rounded-xl bg-slate-900 border border-slate-800 p-5 space-y-2">
42 <div class="text-[11px] font-semibold text-amber-400 uppercase tracking-wider font-mono">
43 💡 Shrnutí lekce
44 </div>
45 <p class="text-sm text-slate-300 leading-relaxed">
46 {{ $lesson->summary }}
47 </p>
48 </div>
49
50 {{-- Vyrenderovaný výukový obsah lekce --}}
51 <div class="rounded-xl bg-slate-900/40 border border-slate-800/80 p-6 sm:p-8 space-y-6 text-slate-200 leading-relaxed text-sm">
52 <div class="prose prose-invert max-w-none space-y-4">
53 {!! $renderedHtml !!}
54 </div>
55 </div>
56
57 {{-- Klíčová konstrukce z praxe (pokud existuje) --}}
58 @if(!empty($lesson->code_snippet))
59 <div class="rounded-xl bg-slate-950 border border-slate-800 overflow-hidden shadow-xl" x-data="{ copied: false }">
60 <div class="flex items-center justify-between px-4 py-2.5 bg-slate-900 border-b border-slate-800 text-xs">
61 <div class="flex items-center gap-2">
62 <span class="w-2.5 h-2.5 rounded-full bg-rose-500"></span>
63 <span class="w-2.5 h-2.5 rounded-full bg-amber-500"></span>
64 <span class="w-2.5 h-2.5 rounded-full bg-emerald-500"></span>
65 <span class="ml-2 font-mono text-slate-300 font-semibold">💻 Klíčová konstrukce z praxe</span>
66 </div>
67 <button type="button"
68 @click="navigator.clipboard.writeText($refs.codeSnippet.innerText); copied = true; setTimeout(() => copied = false, 2000)"
69 class="px-2.5 py-1 rounded bg-slate-800 hover:bg-slate-700 text-[11px] font-mono text-slate-300 transition">
70 <span x-show="!copied">Kopírovat kód</span>
71 <span x-show="copied" class="text-emerald-400" style="display: none;">✓ Zkopírováno!</span>
72 </button>
73 </div>
74 <pre class="p-4 text-xs font-mono text-emerald-300 overflow-x-auto leading-relaxed bg-slate-950/90"><code x-ref="codeSnippet">{{ $lesson->code_snippet }}</code></pre>
75 </div>
76 @endif
77
78 {{-- Interaktivní kvíz (Livewire) --}}
79 <div class="pt-4">
80 <livewire:lesson-quiz :lesson="$lesson" />
81 </div>
82
83 {{-- Navigace na předchozí a následující lekci --}}
84 <div class="pt-6 border-t border-slate-800 flex flex-wrap items-center justify-between gap-4">
85 @if($previousLesson)
86 <a href="{{ route('course.lesson', [$previousLesson->module, $previousLesson]) }}"
87 class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-900 hover:bg-slate-800 border border-slate-800 text-xs font-semibold text-slate-200 hover:text-white transition">
88 <span>&larr;</span>
89 <span>Předchozí lekce ({{ $previousLesson->module->number }}.{{ $previousLesson->number }})</span>
90 </a>
91 @else
92 <div></div>
93 @endif
94
95 <a href="{{ route('course.module', $module) }}"
96 class="text-xs text-slate-400 hover:text-white transition">
97 Přehled modulu {{ $module->number }}
98 </a>
99
100 @if($nextLesson)
101 <a href="{{ route('course.lesson', [$nextLesson->module, $nextLesson]) }}"
102 class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-xs font-semibold text-white transition shadow-sm">
103 <span>Následující lekce ({{ $nextLesson->module->number }}.{{ $nextLesson->number }})</span>
104 <span>&rarr;</span>
105 </a>
106 @endif
107 </div>
108
109 </div>
110
111 {{-- Pravý sloupec: Živá Anatomie souborů aplikace (Sticky panel) --}}
112 <div class="lg:col-span-5 mt-10 lg:mt-0">
113 <div class="sticky top-20 space-y-6">
114
115 {{-- Box Živá Anatomie --}}
116 <div class="rounded-2xl bg-slate-900 border border-slate-800 overflow-hidden shadow-2xl"
117 x-data="{ activeTab: 0 }">
118
119 {{-- Hlavička panelu anatomie --}}
120 <div class="p-4 bg-slate-900/90 border-b border-slate-800 space-y-2">
121 <div class="flex items-center justify-between">
122 <div class="flex items-center gap-2 font-bold text-white text-sm">
123 <span class="text-indigo-400">🔬</span>
124 <span>Živá anatomie v této aplikaci</span>
125 </div>
126 <span class="px-2 py-0.5 rounded text-[10px] font-mono bg-indigo-500/10 text-indigo-400 border border-indigo-500/20">
127 {{ count($anatomyNodes) }} {{ count($anatomyNodes) === 1 ? 'soubor' : 'soubory' }}
128 </span>
129 </div>
130
131 {{-- Poznámka k souborům anatomie --}}
132 @if(!empty($lesson->anatomy_note))
133 <div class="p-2.5 rounded-lg bg-indigo-950/40 border border-indigo-800/40 text-xs text-indigo-200">
134 <strong class="text-amber-300 font-semibold block text-[11px] mb-0.5">Na co se v kódu zaměřit:</strong>
135 {{ $lesson->anatomy_note }}
136 </div>
137 @endif
138
139 {{-- Přepínací záložky souborů --}}
140 @if(count($anatomyNodes) > 1)
141 <div class="flex flex-wrap gap-1.5 pt-2">
142 @foreach($anatomyNodes as $idx => $node)
143 <button type="button"
144 @click="activeTab = {{ $idx }}"
145 :class="activeTab === {{ $idx }} ? 'bg-indigo-600 text-white font-semibold' : 'bg-slate-950 text-slate-400 hover:text-white border border-slate-800'"
146 class="px-2.5 py-1 rounded text-xs font-mono transition truncate max-w-[200px]">
147 {{ basename($node->path) }}
148 </button>
149 @endforeach
150 </div>
151 @endif
152 </div>
153
154 {{-- Tělo prohlížeče kódu s jednotlivými soubory --}}
155 <div class="bg-slate-950">
156 @forelse($anatomyNodes as $idx => $node)
157 <div x-show="activeTab === {{ $idx }}"
158 style="{{ $idx > 0 ? 'display: none;' : '' }}">
159
160 {{-- Informace o aktivním souboru --}}
161 <div class="px-4 py-2 bg-slate-900/60 border-b border-slate-800/80 flex items-center justify-between text-xs font-mono">
162 <div class="truncate text-slate-300 pr-2">
163 <span class="text-slate-500">cesta:</span> {{ $node->path }}
164 </div>
165 <div class="shrink-0 flex items-center gap-2 text-slate-400 text-[11px]">
166 <span>{{ $node->lineCount }} řádků</span>
167 <span>·</span>
168 <span>{{ number_format($node->sizeBytes / 1024, 1) }} KB</span>
169 </div>
170 </div>
171
172 @if(!empty($node->description))
173 <div class="px-4 py-2 bg-slate-950 border-b border-slate-900 text-xs text-slate-400">
174 {{ $node->description }}
175 </div>
176 @endif
177
178 {{-- Okno kódu s číslováním řádků --}}
179 @if($node->exists)
180 @php
181 $lines = explode("\n", $node->content);
182 @endphp
183 <div class="max-h-[520px] overflow-y-auto overflow-x-auto text-xs font-mono text-slate-200 p-3 bg-slate-950 selection:bg-indigo-600 select-text">
184 <table class="w-full border-collapse">
185 <tbody>
186 @foreach($lines as $lineNum => $lineText)
187 <tr class="hover:bg-slate-900/60">
188 <td class="text-right pr-4 text-slate-600 select-none text-[11px] align-top w-10">
189 {{ $lineNum + 1 }}
190 </td>
191 <td class="whitespace-pre font-mono text-slate-200"><code>{{ $lineText }}</code></td>
192 </tr>
193 @endforeach
194 </tbody>
195 </table>
196 </div>
197 @else
198 <div class="p-6 text-center text-xs text-amber-400 font-mono">
199 ⚠️ Soubor nebyl nalezen na disku nebo leží mimo povolený whitelist.
200 </div>
201 @endif
202 </div>
203 @empty
204 <div class="p-6 text-center text-xs text-slate-400">
205 Pro tuto lekci nejsou přiřazeny žádné soubory anatomie.
206 </div>
207 @endforelse
208 </div>
209
210 {{-- Spodní lišta panelu --}}
211 <div class="p-3 bg-slate-900 border-t border-slate-800 flex items-center justify-between text-xs">
212 <span class="text-slate-400 font-mono text-[11px]">
213 Zdroj: skutečné soubory této aplikace
214 </span>
215 <a href="{{ route('anatomy.index') }}" class="text-indigo-400 hover:text-indigo-300 font-medium transition text-xs flex items-center gap-1">
216 <span>Plný průzkumník anatomie</span>
217 <span>&rarr;</span>
218 </a>
219 </div>
220
221 </div>
222
223 </div>
224 </div>
225
226 </div>
227
228 </div>
229 @endsection
230
Zdroj: skutečné soubory této aplikace Plný průzkumník anatomie →