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

Znovupoužitelné UI: Blade komponenty, atributy a sloty

💡 Shrnutí lekce

Kompozice uživatelského rozhraní pomocí Blade komponent (<x-...>), sloty ($slot, pojmenované @slot), předávání dat a atributů ($attributes) na příkladu layoutu a detailu lekce.

Moderní kompozice rozhraní: Blade komponenty

Zatímco starší verze Laravelu spoléhaly převážně na direktivy @include a @extends, moderní Laravel klade důraz na Blade komponenty (zapisované pomocí XML tagu <x-...>).

Komponenty přinášejí do Blade šablon výhody známé z frontendových frameworků (React, Vue):

  • Samostatnost a znovupoužitelnost kódu.
  • Izolované předávání vlastností (props) a HTML atributů.
  • Kompozici pomocí výchozích i pojmenovaných slotů.

1. Sloty: Výchozí obsah i pojmenované sekce

Každá komponenta může přijímat libovolný vnitřní obsah přes proměnnou $slot:

{{-- Komponenta layouts/app.blade.php --}}
<main class="flex-1">
    {{ $slot }}
</main>

Použití komponenty v klientské šabloně:

<x-layouts.app>
    <x-slot:header>
        <h1 class="text-xl font-bold">Přehled lekce</h1>
    </x-slot:header>

    <div class="prose">
        Zde je hlavní tělo lekce předané do výchozího $slot.
    </div>
</x-layouts.app>

Pomocí <x-slot:header> můžeme definovat libovolné množství pojmenovaných slotů, které se v komponentě zobrazí jako proměnná odpovídajícího názvu ($header).


2. Balíček atributů $attributes a direktiva @props

Častou potřebou je předat komponentě HTML třídy nebo Alpine.js direktivy zvenčí. K tomu slouží speciální objekt $attributes:

{{-- resources/views/components/card.blade.php --}}
@props([
    'variant' => 'default',
    'title',
])

<div {{ $attributes->merge(['class' => 'rounded-xl p-5 border ' . ($variant === 'highlight' ? 'border-amber-500 bg-amber-500/10' : 'border-slate-800 bg-slate-900')]) }}>
    <h3 class="font-bold text-white">{{ $title }}</h3>
    <div>{{ $slot }}</div>
</div>

Metoda $attributes->merge() inteligentně sloučí výchozí CSS třídy komponenty s dodatečnými třídami předanými při volání, aniž by došlo k jejich nechtěnému přepsání!


3. Kompozice dvousloupcového rozložení v resources/views/course/lesson.blade.php

V naší aplikaci je detail lekce vzorem špičkové kompozice:

  1. Drobečková navigace a fáze životního cyklu: Rychlá orientace studenta v kurzu.
  2. Levý sloupec: Výklad lekce, interaktivní blok pro kopírování kódu (využívající Alpine.js x-data="{ copied: false }") a Livewire kvíz <livewire:lesson-quiz :lesson="$lesson" />.
  3. Pravý sloupec: Živý inspektor kódu a navigace mezi soubory anatomie.
💻 Klíčová konstrukce z praxe
{{-- resources/views/course/lesson.blade.php (Ukázka znovupoužitelné kompozice a Alpine.js) --}}
@extends('layouts.app')

@section('content')
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-6">

    {{-- Drobečková navigace & štítek fáze životního cyklu --}}
    <div class="flex flex-wrap items-center justify-between gap-4 pb-4 border-b border-slate-800">
        <nav class="flex items-center gap-2 text-xs text-slate-400">
            <a href="{{ route('course.index') }}" class="hover:text-white">📚 Osnova</a>
            <span>/</span>
            <span class="text-slate-200">Modul {{ $module->number }} · Lekce {{ $lesson->number }}</span>
        </nav>

        @if($lesson->lifecycle_phase)
            <div class="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg text-xs bg-indigo-500/10 text-indigo-300 border border-indigo-500/20">
                <span>Fáze: {{ $lesson->lifecycle_phase->label() }}</span>
            </div>
        @endif
    </div>

    {{-- Komponenta pro kód s okamžitou klientskou interakcí přes Alpine.js --}}
    @if(!empty($lesson->code_snippet))
        <div class="rounded-xl bg-slate-950 border border-slate-800 overflow-hidden shadow-xl" x-data="{ copied: false }">
            <div class="flex items-center justify-between px-4 py-2 bg-slate-900 border-b border-slate-800 text-xs">
                <span class="font-mono text-slate-300 font-semibold">💻 Klíčová konstrukce</span>
                <button type="button"
                        @click="navigator.clipboard.writeText($refs.snippet.innerText); copied = true; setTimeout(() => copied = false, 2000)"
                        class="px-2.5 py-1 rounded bg-slate-800 hover:bg-slate-700 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 overflow-x-auto"><code x-ref="snippet">{{ $lesson->code_snippet }}</code></pre>
        </div>
    @endif

    {{-- Začlenění reaktivní komponenty Livewire pro interaktivní kvíz --}}
    <div class="pt-4">
        <livewire:lesson-quiz :lesson="$lesson" />
    </div>
</div>
@endsection

🎯 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

Jak se v moderním Blade zapisuje anonymní komponenta s předáváním vlastností a HTML atributů?

Otázka 2 z 3

Co představuje proměnná $slot v těle Blade komponenty layoutu?

Otázka 3 z 3

K čemu slouží direktiva @props(['title', 'variant' => 'default']) na začátku šablony komponenty?

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 si prohlédni blok pro kopírování kódu na řádcích 58–76. Všimni si, jak je čistý Blade elegantně propojen s Alpine.js direktivami x-data="{ copied: false }" a @click pro práci se schránkou.
cesta: resources/views/layouts/app.blade.php
261 řádků · 16.3 KB
Hlavní Blade layout aplikace s tmavým tématem, navigací, synchronizací profilu a Alpine.js reaktivitou.
1 <!DOCTYPE html>
2 <html lang="cs" class="h-full bg-slate-950">
3 <head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="csrf-token" content="{{ csrf_token() }}">
7
8 <title>{{ $title ?? 'Anatomie Laravelu 12 & Moderního PHP 8.4' }} — Živá výuková platforma</title>
9 <meta name="description" content="Interaktivní výuková platforma Anatomie Laravelu 12 & Moderního PHP 8.4 na reálné živě běžící aplikaci.">
10
11 <link rel="preconnect" href="https://fonts.bunny.net">
12 <link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600,700|jetbrains-mono:400,500,600&display=swap" rel="stylesheet" />
13
14 @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))
15 @vite(['resources/css/app.css', 'resources/js/app.js'])
16 @endif
17 @livewireStyles
18 </head>
19 <body class="min-h-full flex flex-col bg-slate-950 text-slate-100 antialiased selection:bg-indigo-500 selection:text-white font-sans">
20 @php
21 $student = $currentStudent ?? (app()->bound(\App\Services\StudentProgressService::class) ? app(\App\Services\StudentProgressService::class)->resolveProfile(request()->cookie(\App\Services\StudentProgressService::COOKIE_NAME ?? 'agy_student_token')) : null);
22 @endphp
23
24 {{-- Horní navigace --}}
25 <header class="sticky top-0 z-50 border-b border-slate-800/80 bg-slate-950/90 backdrop-blur-md">
26 <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
27 <div class="flex items-center justify-between h-16 gap-4">
28 {{-- Logo & Název --}}
29 <div class="flex items-center gap-3">
30 <a href="{{ route('course.index') }}" class="flex items-center gap-3 group focus:outline-none">
31 <div class="w-10 h-10 rounded-xl bg-gradient-to-br from-indigo-500 via-purple-600 to-amber-500 p-0.5 shadow-lg shadow-indigo-500/20 group-hover:shadow-indigo-500/40 transition">
32 <div class="w-full h-full bg-slate-950 rounded-[10px] flex items-center justify-center font-mono font-bold text-base text-amber-400 group-hover:text-amber-300">
33 &lt;/&gt;
34 </div>
35 </div>
36 <div>
37 <div class="flex items-center gap-2">
38 <span class="font-bold text-base tracking-tight text-white group-hover:text-amber-300 transition">
39 Anatomie Laravelu 12 & PHP 8.4
40 </span>
41 <span class="hidden sm:inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold bg-indigo-500/10 text-indigo-400 ring-1 ring-inset ring-indigo-500/20">
42 v12.x
43 </span>
44 </div>
45 <p class="text-xs text-slate-400">
46 Živá výuková platforma · <span class="text-slate-300">laravel.heatsharing.cz</span>
47 </p>
48 </div>
49 </a>
50 </div>
51
52 {{-- Hlavní menu --}}
53 <nav class="hidden md:flex items-center gap-1">
54 <a href="{{ route('course.index') }}"
55 class="px-3 py-2 rounded-lg text-sm font-medium transition {{ request()->routeIs('course.*') && !request()->routeIs('anatomy.*') ? 'bg-indigo-950/60 text-indigo-300 border border-indigo-800/50 shadow-sm' : 'text-slate-300 hover:text-white hover:bg-slate-900' }}">
56 📚 Osnova & Moduly
57 </a>
58 <a href="{{ route('anatomy.index') }}"
59 class="px-3 py-2 rounded-lg text-sm font-medium transition {{ request()->routeIs('anatomy.*') ? 'bg-indigo-950/60 text-indigo-300 border border-indigo-800/50 shadow-sm' : 'text-slate-300 hover:text-white hover:bg-slate-900' }}">
60 🔬 Živá Anatomie & Request Lifecycle
61 </a>
62 </nav>
63
64 {{-- Postup studenta & Přenosový kód (Alpine.js dropdown) --}}
65 <div class="flex items-center gap-3" x-data="{ open: false }">
66 <div class="relative">
67 <button @click="open = !open"
68 type="button"
69 class="flex items-center gap-2.5 px-3 py-1.5 rounded-lg bg-slate-900 border border-slate-800 hover:border-slate-700 text-xs font-medium text-slate-200 transition focus:outline-none focus:ring-2 focus:ring-indigo-500/50"
70 aria-expanded="false"
71 :aria-expanded="open.toString()">
72 <span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
73 <span class="hidden sm:inline text-slate-400">Profil:</span>
74 <span class="font-mono text-amber-300 font-semibold">
75 {{ $student->transfer_code ?? 'LRV-POSTUP' }}
76 </span>
77 <svg class="w-3.5 h-3.5 text-slate-400 transition-transform duration-200" :class="{ 'rotate-180': open }" fill="none" viewBox="0 0 24 24" stroke="currentColor">
78 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
79 </svg>
80 </button>
81
82 {{-- Dropdown panel --}}
83 <div x-show="open"
84 @click.outside="open = false"
85 x-transition:enter="transition ease-out duration-150"
86 x-transition:enter-start="opacity-0 scale-95"
87 x-transition:enter-end="opacity-100 scale-100"
88 x-transition:leave="transition ease-in duration-100"
89 x-transition:leave-start="opacity-100 scale-100"
90 x-transition:leave-end="opacity-0 scale-95"
91 class="absolute right-0 mt-2 w-80 sm:w-96 rounded-xl bg-slate-900 border border-slate-800 shadow-2xl p-4 text-xs text-slate-300 z-50"
92 style="display: none;">
93 <div class="flex items-center justify-between pb-3 border-b border-slate-800">
94 <div>
95 <h4 class="font-semibold text-white text-sm">Anonymní profil studenta</h4>
96 <p class="text-[11px] text-slate-400">Postup je uložen v cookie a v PostgreSQL.</p>
97 </div>
98 <span class="px-2 py-0.5 rounded text-[10px] font-semibold bg-emerald-500/10 text-emerald-400 ring-1 ring-inset ring-emerald-500/20">
99 Aktivní
100 </span>
101 </div>
102
103 <div class="py-3 space-y-2">
104 <span class="text-slate-400 font-medium block">Tvůj unikátní přenosový kód:</span>
105 <div class="flex items-center gap-2">
106 <code class="flex-1 px-3 py-2 bg-slate-950 border border-slate-800 rounded-lg text-amber-300 font-mono text-sm tracking-wider select-all text-center font-bold">
107 {{ $student->transfer_code ?? 'LRV-GENEROVANI' }}
108 </code>
109 </div>
110 <p class="text-[11px] text-slate-500">
111 Tento kód si uložte, pokud budete chtít pokračovat ve výuce na jiném zařízení nebo v jiném prohlížeči.
112 </p>
113 </div>
114
115 <div class="pt-3 border-t border-slate-800">
116 <form action="{{ route('course.switch-profile') }}" method="POST" class="space-y-2">
117 @csrf
118 <label for="transfer_code_input" class="block text-slate-300 font-medium">
119 Načíst profil z jiného zařízení:
120 </label>
121 <div class="flex gap-2">
122 <input type="text"
123 id="transfer_code_input"
124 name="transfer_code"
125 placeholder="LRV-XXXX-XXXX"
126 required
127 class="flex-1 px-3 py-1.5 rounded-lg bg-slate-950 border border-slate-700 text-white font-mono text-xs uppercase placeholder:text-slate-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500">
128 <button type="submit"
129 class="px-3 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg text-xs transition shadow-sm">
130 Aktivovat
131 </button>
132 </div>
133 </form>
134 </div>
135 </div>
136 </div>
137 </div>
138 </div>
139
140 {{-- Mobilní navigace --}}
141 <div class="flex md:hidden border-t border-slate-800/80 py-2 gap-2">
142 <a href="{{ route('course.index') }}"
143 class="flex-1 text-center px-3 py-1.5 rounded text-xs font-medium {{ request()->routeIs('course.*') && !request()->routeIs('anatomy.*') ? 'bg-indigo-950 text-indigo-300' : 'text-slate-400 hover:text-white' }}">
144 📚 Osnova & Moduly
145 </a>
146 <a href="{{ route('anatomy.index') }}"
147 class="flex-1 text-center px-3 py-1.5 rounded text-xs font-medium {{ request()->routeIs('anatomy.*') ? 'bg-indigo-950 text-indigo-300' : 'text-slate-400 hover:text-white' }}">
148 🔬 Živá Anatomie
149 </a>
150 </div>
151 </div>
152 </header>
153
154 {{-- Flash zprávy --}}
155 @if(session('success'))
156 <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
157 <div class="rounded-xl bg-emerald-950/60 border border-emerald-800/80 p-4 text-emerald-200 flex items-start gap-3 shadow-lg">
158 <svg class="w-5 h-5 text-emerald-400 shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
159 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
160 </svg>
161 <div class="text-sm">
162 <span class="font-semibold text-white">Úspěch:</span> {{ session('success') }}
163 </div>
164 </div>
165 </div>
166 @endif
167
168 @if(session('error'))
169 <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
170 <div class="rounded-xl bg-rose-950/60 border border-rose-800/80 p-4 text-rose-200 flex items-start gap-3 shadow-lg">
171 <svg class="w-5 h-5 text-rose-400 shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
172 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
173 </svg>
174 <div class="text-sm">
175 <span class="font-semibold text-white">Chyba:</span> {{ session('error') }}
176 </div>
177 </div>
178 </div>
179 @endif
180
181 {{-- Hlavní obsah --}}
182 <main class="flex-1">
183 {{ $slot ?? '' }}
184 @yield('content')
185 </main>
186
187 {{-- Patička --}}
188 <footer class="mt-20 border-t border-slate-800/80 bg-slate-950 text-slate-400 text-xs">
189 <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
190 <div class="grid grid-cols-1 md:grid-cols-3 gap-8 pb-8 border-b border-slate-900">
191 {{-- O platformě --}}
192 <div class="space-y-3">
193 <div class="flex items-center gap-2 text-white font-semibold">
194 <span class="text-amber-400 font-mono">&lt;/&gt;</span>
195 <span>Anatomie Laravelu 12 & PHP 8.4</span>
196 </div>
197 <p class="text-slate-400 leading-relaxed text-xs">
198 Sebereferenční výuková platforma. Aplikace sama slouží jako exponát – kód, architektura i konfigurace, které studujete, pohání tento běžící web.
199 </p>
200 </div>
201
202 {{-- Produkční infrastruktura --}}
203 <div class="space-y-3">
204 <h5 class="text-white font-semibold">Infrastruktura & Produkční Stack</h5>
205 <ul class="space-y-1.5 text-slate-400">
206 <li class="flex items-center gap-2">
207 <span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span>
208 <span>FrankenPHP v1 + PHP 8.4 (Worker Mode, multi-stage Docker)</span>
209 </li>
210 <li class="flex items-center gap-2">
211 <span class="w-1.5 h-1.5 rounded-full bg-indigo-400"></span>
212 <span>Laravel 12.x + Livewire 3 + Alpine.js + Tailwind CSS</span>
213 </li>
214 <li class="flex items-center gap-2">
215 <span class="w-1.5 h-1.5 rounded-full bg-sky-400"></span>
216 <span>PostgreSQL 18 na Hetzner serveru (<code class="text-slate-300 font-mono">debian-hell</code> / 65.109.132.79)</span>
217 </li>
218 <li class="flex items-center gap-2">
219 <span class="w-1.5 h-1.5 rounded-full bg-amber-400"></span>
220 <span>Reverzní proxy Caddy v2 s automatickým TLS Let's Encrypt</span>
221 </li>
222 </ul>
223 </div>
224
225 {{-- Rozcestník & Odkazy --}}
226 <div class="space-y-3">
227 <h5 class="text-white font-semibold">Rychlé odkazy</h5>
228 <ul class="space-y-1.5 text-slate-400">
229 <li>
230 <a href="{{ route('course.index') }}" class="hover:text-amber-300 transition">📚 Kompletní osnova (10 modulů, 30 lekcí)</a>
231 </li>
232 <li>
233 <a href="{{ route('anatomy.index') }}" class="hover:text-amber-300 transition">🔬 Živý Průzkumník anatomie</a>
234 </li>
235 <li>
236 <a href="https://heatsharing.cz" target="_blank" rel="noopener noreferrer" class="hover:text-amber-300 transition flex items-center gap-1">
237 <span>🌐 Rozcestník projektů heatsharing.cz</span>
238 <svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
239 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
240 </svg>
241 </a>
242 </li>
243 <li>
244 <a href="{{ route('api.status') }}" target="_blank" class="hover:text-amber-300 transition flex items-center gap-1 font-mono text-[11px] text-slate-500">
245 <span>⚡ Status API: /api/status</span>
246 </a>
247 </li>
248 </ul>
249 </div>
250 </div>
251
252 <div class="pt-6 flex flex-col sm:flex-row items-center justify-between gap-4 text-[11px] text-slate-500">
253 <p>© {{ date('Y') }} Heatsharing.cz · Anatomie Laravelu 12. Volně přístupné vzdělávací dílo.</p>
254 <p class="font-mono text-slate-500">PHP 8.4.x · Laravel 12.x · Zero Hallucinations Policy</p>
255 </div>
256 </div>
257 </footer>
258
259 @livewireScripts
260 </body>
261 </html>
262
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 →