diff --git a/.env.testing b/.env.testing new file mode 100644 index 0000000..15722e3 --- /dev/null +++ b/.env.testing @@ -0,0 +1,8 @@ +APP_ENV=testing +APP_KEY=base64:olpsnUi1o5DBLbA0lK2LCJiCnhMPldp7Pl3kuj1Iz7A= +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=lernschiff_test +DB_USERNAME=lernschiff +DB_PASSWORD=secret diff --git a/app/Livewire/Dashboard/Index.php b/app/Livewire/Dashboard/Index.php new file mode 100644 index 0000000..88dd339 --- /dev/null +++ b/app/Livewire/Dashboard/Index.php @@ -0,0 +1,68 @@ + 'dashboard'])] +#[Lazy] +class Index extends Component +{ + public static function placeholder(array $params = []): View + { + return view('components.skeleton.page', ['rows' => 5]); + } + + + public int $totalPoints = 2480; + public int $streakDays = 7; + public int $weekMinutes = 86; + public string $testRatio = '18 / 24'; + + public function render(): View + { + $user = auth()->user(); + $firstName = $user ? explode(' ', $user->name)[0] : 'Lina'; + + return view('livewire.dashboard.index', [ + 'firstName' => $firstName, + 'today' => Carbon::now()->locale('de')->isoFormat('dddd · D. MMMM'), + 'subjects' => $this->subjects(), + 'tasks' => $this->tasks(), + 'rewards' => $this->rewards(), + ]); + } + + protected function subjects(): array + { + return [ + ['name' => 'Mathematik', 'icon' => '∑', 'color' => 'primary', 'lessons' => '18 / 24', 'progress' => 75, 'next' => 'Schriftliches Multiplizieren'], + ['name' => 'Deutsch', 'icon' => 'A', 'color' => 'rose', 'lessons' => '13 / 20', 'progress' => 65, 'next' => 'Wortarten erkennen'], + ['name' => 'Englisch', 'icon' => 'E', 'color' => 'violet', 'lessons' => '8 / 16', 'progress' => 50, 'next' => 'Past Simple üben'], + ['name' => 'Sachkunde', 'icon' => '🌱', 'color' => 'green', 'lessons' => '14 / 18', 'progress' => 78, 'next' => 'Wasserkreislauf'], + ]; + } + + protected function tasks(): array + { + return [ + ['title' => 'Multiplikation bis 100', 'meta' => 'Mathematik · 12 Aufgaben · 15 Min.', 'icon' => '∑', 'color' => 'primary', 'points' => 120], + ['title' => 'Wortarten: Nomen & Verben', 'meta' => 'Deutsch · Lese-Aufgabe · 10 Min.', 'icon' => 'A', 'color' => 'rose', 'points' => 80], + ['title' => 'Vocab Review: Animals', 'meta' => 'Englisch · Karteikarten · 8 Min.', 'icon' => 'E', 'color' => 'violet', 'points' => 60], + ['title' => 'Kreislauf des Wassers', 'meta' => 'Sachkunde · Video + Quiz · 12 Min.', 'icon' => '🌱', 'color' => 'green', 'points' => 100], + ]; + } + + protected function rewards(): array + { + return [ + ['title' => 'Avatar-Brille (gold)', 'emoji' => '🎨', 'status' => 'Verfügbar', 'price' => 800, 'available' => true], + ['title' => 'Einhorn-Begleiter', 'emoji' => '🦄', 'status' => 'Verfügbar', 'price' => 1500, 'available' => true], + ['title' => 'Raketen-Theme', 'emoji' => '🚀', 'status' => 'Noch 720 Pkt. nötig', 'price' => 3200, 'available' => false], + ]; + } +} diff --git a/app/Livewire/Notifications/Index.php b/app/Livewire/Notifications/Index.php new file mode 100644 index 0000000..3dbff69 --- /dev/null +++ b/app/Livewire/Notifications/Index.php @@ -0,0 +1,96 @@ + 'notifications'])] +#[Lazy] +class Index extends Component +{ + public static function placeholder(array $params = []): View + { + return view('components.skeleton.page', ['rows' => 4]); + } + + + #[Url(as: 'filter', keep: true)] + public string $filter = 'all'; + + /** @var array */ + protected array $allowedFilters = ['all', 'unread']; + + public array $readKeys = []; + + public function mount(): void + { + if (! in_array($this->filter, $this->allowedFilters, true)) { + $this->filter = 'all'; + } + } + + public function setFilter(string $filter): void + { + if (in_array($filter, $this->allowedFilters, true)) { + $this->filter = $filter; + } + } + + public function markRead(string $key): void + { + if (! in_array($key, $this->readKeys, true)) { + $this->readKeys[] = $key; + } + } + + public function markAllRead(): void + { + $this->readKeys = array_column($this->notifications(), 'key'); + } + + #[Computed] + public function notifications(): array + { + $items = [ + ['key' => 'n1', 'title' => 'Heutige Aufgaben warten', 'body' => 'Du hast noch 4 offene Aufgaben für heute. Streak nicht brechen!', 'time' => 'vor 10 Min.', 'icon' => 'clock', 'color' => 'primary', 'baseRead' => false], + ['key' => 'n2', 'title' => 'Neuer Rekord erreicht!', 'body' => 'Du hast 7 Tage am Stück gelernt — neuer persönlicher Rekord.', 'time' => 'vor 2 Std.', 'icon' => 'trophy', 'color' => 'amber', 'baseRead' => false], + ['key' => 'n3', 'title' => 'Nachricht von Frau Müller', 'body' => 'Hallo Lina, super Arbeit gestern! Schau dir die Zusatzaufgabe an.', 'time' => 'vor 4 Std.', 'icon' => 'chat-bubble-left-right', 'color' => 'rose', 'baseRead' => false], + ['key' => 'n4', 'title' => 'Neues Foto-Hilfe-Feature', 'body' => 'Du kannst jetzt Aufgaben fotografieren und die KI erklärt den Weg.', 'time' => 'gestern', 'icon' => 'sparkles', 'color' => 'violet', 'baseRead' => true], + ['key' => 'n5', 'title' => 'Mathematik: 75% erreicht', 'body' => 'Du hast 18 von 24 Lektionen geschafft. Noch 6 bis zum Abschluss.', 'time' => '2 Tage', 'icon' => 'check-badge', 'color' => 'green', 'baseRead' => true], + ['key' => 'n6', 'title' => 'Wochenende Übung', 'body' => 'Auch am Wochenende lernen hilft beim Streak. Heute 10 Minuten.', 'time' => '3 Tage', 'icon' => 'calendar-days', 'color' => 'primary', 'baseRead' => true], + ]; + + return array_map(function ($n) { + $n['read'] = $n['baseRead'] || in_array($n['key'], $this->readKeys, true); + return $n; + }, $items); + } + + #[Computed] + public function filteredNotifications(): array + { + if ($this->filter === 'unread') { + return array_values(array_filter($this->notifications, fn($n) => ! $n['read'])); + } + return $this->notifications; + } + + #[Computed] + public function counts(): array + { + return [ + 'all' => count($this->notifications), + 'unread' => count(array_filter($this->notifications, fn($n) => ! $n['read'])), + ]; + } + + public function render(): View + { + return view('livewire.notifications.index'); + } +} diff --git a/app/Livewire/Notifications/Modals/Detail.php b/app/Livewire/Notifications/Modals/Detail.php new file mode 100644 index 0000000..08d5356 --- /dev/null +++ b/app/Livewire/Notifications/Modals/Detail.php @@ -0,0 +1,26 @@ +notification = $notification; + } + + public static function modalMaxWidth(): string + { + return 'lg'; + } + + public function render(): View + { + return view('livewire.notifications.modals.detail'); + } +} diff --git a/app/Livewire/PhotoHelp/Index.php b/app/Livewire/PhotoHelp/Index.php new file mode 100644 index 0000000..d524d61 --- /dev/null +++ b/app/Livewire/PhotoHelp/Index.php @@ -0,0 +1,23 @@ + 'photo'])] +#[Lazy] +class Index extends Component +{ + public static function placeholder(array $params = []): View + { + return view('components.skeleton.page', ['rows' => 3]); + } + + public function render(): View + { + return view('livewire.photo-help.index'); + } +} diff --git a/app/Livewire/PhotoHelp/Modals/Result.php b/app/Livewire/PhotoHelp/Modals/Result.php new file mode 100644 index 0000000..14d0802 --- /dev/null +++ b/app/Livewire/PhotoHelp/Modals/Result.php @@ -0,0 +1,19 @@ + 'progress'])] +#[Lazy] +class Index extends Component +{ + public static function placeholder(array $params = []): View + { + return view('components.skeleton.page', ['rows' => 4]); + } + + public function render(): View + { + return view('livewire.progress.index'); + } +} diff --git a/app/Livewire/Rewards/Index.php b/app/Livewire/Rewards/Index.php new file mode 100644 index 0000000..67bdd0f --- /dev/null +++ b/app/Livewire/Rewards/Index.php @@ -0,0 +1,23 @@ + 'rewards'])] +#[Lazy] +class Index extends Component +{ + public static function placeholder(array $params = []): View + { + return view('components.skeleton.page', ['rows' => 4]); + } + + public function render(): View + { + return view('livewire.rewards.index'); + } +} diff --git a/app/Livewire/Rewards/Modals/Purchase.php b/app/Livewire/Rewards/Modals/Purchase.php new file mode 100644 index 0000000..b1b31e0 --- /dev/null +++ b/app/Livewire/Rewards/Modals/Purchase.php @@ -0,0 +1,45 @@ +item = $item; + $this->balance = $balance; + } + + public function confirm(): void + { + $this->confirming = true; + } + + public function purchase(): void + { + if (($this->item['price'] ?? 0) > $this->balance) { + return; + } + $this->balance -= (int) $this->item['price']; + $this->purchased = true; + $this->confirming = false; + } + + public static function modalMaxWidth(): string + { + return 'md'; + } + + public function render(): View + { + return view('livewire.rewards.modals.purchase'); + } +} diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php new file mode 100644 index 0000000..b5f5862 --- /dev/null +++ b/app/Livewire/Settings/Index.php @@ -0,0 +1,45 @@ + 'settings'])] +#[Lazy] +class Index extends Component +{ + public static function placeholder(array $params = []): View + { + return view('components.skeleton.page', ['rows' => 4]); + } + + + #[Url(as: 'section', keep: true)] + public string $section = 'profile'; + + /** @var array */ + protected array $allowedSections = ['profile', 'language', 'security', 'notifications', 'danger']; + + public function mount(): void + { + if (! in_array($this->section, $this->allowedSections, true)) { + $this->section = 'profile'; + } + } + + public function setSection(string $section): void + { + if (in_array($section, $this->allowedSections, true)) { + $this->section = $section; + } + } + + public function render(): View + { + return view('livewire.settings.index'); + } +} diff --git a/app/Livewire/Settings/Modals/Delete.php b/app/Livewire/Settings/Modals/Delete.php new file mode 100644 index 0000000..9166b9a --- /dev/null +++ b/app/Livewire/Settings/Modals/Delete.php @@ -0,0 +1,29 @@ +confirmText !== 'LÖSCHEN') { + return; + } + $this->closeModal(); + } + + public static function modalMaxWidth(): string + { + return 'md'; + } + + public function render(): View + { + return view('livewire.settings.modals.delete'); + } +} diff --git a/app/Livewire/Settings/Modals/LogoutAll.php b/app/Livewire/Settings/Modals/LogoutAll.php new file mode 100644 index 0000000..b7ba56e --- /dev/null +++ b/app/Livewire/Settings/Modals/LogoutAll.php @@ -0,0 +1,26 @@ +closeModal(); + } + + public static function modalMaxWidth(): string + { + return 'md'; + } + + public function render(): View + { + return view('livewire.settings.modals.logout-all'); + } +} diff --git a/app/Livewire/Subjects/Index.php b/app/Livewire/Subjects/Index.php new file mode 100644 index 0000000..1afc2eb --- /dev/null +++ b/app/Livewire/Subjects/Index.php @@ -0,0 +1,23 @@ + 'subjects'])] +#[Lazy] +class Index extends Component +{ + public static function placeholder(array $params = []): View + { + return view('components.skeleton.page', ['rows' => 4]); + } + + public function render(): View + { + return view('livewire.subjects.index'); + } +} diff --git a/app/Livewire/Subjects/Modals/Detail.php b/app/Livewire/Subjects/Modals/Detail.php new file mode 100644 index 0000000..53987f1 --- /dev/null +++ b/app/Livewire/Subjects/Modals/Detail.php @@ -0,0 +1,26 @@ +subject = $subject; + } + + public static function modalMaxWidth(): string + { + return 'xl'; + } + + public function render(): View + { + return view('livewire.subjects.modals.detail'); + } +} diff --git a/app/Livewire/Tasks/Index.php b/app/Livewire/Tasks/Index.php new file mode 100644 index 0000000..6b6c35d --- /dev/null +++ b/app/Livewire/Tasks/Index.php @@ -0,0 +1,105 @@ + 'tasks'])] +#[Lazy] +class Index extends Component +{ + public static function placeholder(array $params = []): View + { + return view('components.skeleton.page', ['rows' => 5]); + } + + + #[Url(as: 'filter', keep: true)] + public string $filter = 'open'; + + /** @var array */ + protected array $allowedFilters = ['open', 'done', 'all']; + + public function mount(): void + { + if (! in_array($this->filter, $this->allowedFilters, true)) { + $this->filter = 'open'; + } + } + + public function setFilter(string $filter): void + { + if (in_array($filter, $this->allowedFilters, true)) { + $this->filter = $filter; + } + } + + /** @var array */ + public array $startedKeys = []; + + public function startTask(string $key): void + { + $task = collect($this->tasks)->firstWhere('key', $key); + if ($task === null) { + return; + } + + if (! in_array($key, $this->startedKeys, true)) { + $this->startedKeys[] = $key; + } + + // Future: redirect to a real lesson runner. For now open detail modal. + // Must target wire-elements modal component explicitly — Livewire 4 + // server-side dispatch does not auto-broadcast to other components. + $this->dispatch('openModal', component: 'tasks.modals.detail', arguments: ['task' => $task]) + ->to('livewire-ui-modal'); + } + + public function repeatTask(string $key): void + { + $this->startTask($key); + } + + #[Computed] + public function tasks(): array + { + return [ + ['key' => 't1', 'title' => 'Multiplikation bis 100', 'meta' => 'Mathematik · 12 Aufgaben · 15 Min.', 'icon' => '∑', 'color' => 'primary', 'points' => 120, 'status' => 'open', 'due' => 'Heute'], + ['key' => 't2', 'title' => 'Wortarten: Nomen & Verben', 'meta' => 'Deutsch · Lese-Aufgabe · 10 Min.', 'icon' => 'A', 'color' => 'rose', 'points' => 80, 'status' => 'open', 'due' => 'Heute'], + ['key' => 't3', 'title' => 'Vocab Review: Animals', 'meta' => 'Englisch · Karteikarten · 8 Min.', 'icon' => 'E', 'color' => 'violet', 'points' => 60, 'status' => 'open', 'due' => 'Heute'], + ['key' => 't4', 'title' => 'Kreislauf des Wassers', 'meta' => 'Sachkunde · Video + Quiz · 12 Min.', 'icon' => '🌱', 'color' => 'green', 'points' => 100, 'status' => 'open', 'due' => 'Heute'], + ['key' => 't5', 'title' => 'Geometrie: Flächen', 'meta' => 'Mathematik · Übung · 20 Min.', 'icon' => '∑', 'color' => 'primary', 'points' => 150, 'status' => 'open', 'due' => 'Morgen'], + ['key' => 't6', 'title' => 'Gedicht lernen', 'meta' => 'Deutsch · Lese-Aufgabe · 15 Min.', 'icon' => 'A', 'color' => 'rose', 'points' => 90, 'status' => 'done', 'due' => 'Gestern'], + ['key' => 't7', 'title' => 'Past Simple Übung', 'meta' => 'Englisch · Übung · 10 Min.', 'icon' => 'E', 'color' => 'violet', 'points' => 70, 'status' => 'done', 'due' => 'Gestern'], + ]; + } + + #[Computed] + public function filteredTasks(): array + { + if ($this->filter === 'all') { + return $this->tasks; + } + return array_values(array_filter($this->tasks, fn($t) => $t['status'] === $this->filter)); + } + + #[Computed] + public function counts(): array + { + return [ + 'open' => count(array_filter($this->tasks, fn($t) => $t['status'] === 'open')), + 'done' => count(array_filter($this->tasks, fn($t) => $t['status'] === 'done')), + 'all' => count($this->tasks), + ]; + } + + public function render(): View + { + return view('livewire.tasks.index'); + } +} diff --git a/app/Livewire/Tasks/Modals/Detail.php b/app/Livewire/Tasks/Modals/Detail.php new file mode 100644 index 0000000..d70a90b --- /dev/null +++ b/app/Livewire/Tasks/Modals/Detail.php @@ -0,0 +1,26 @@ +task = $task; + } + + public static function modalMaxWidth(): string + { + return 'lg'; + } + + public function render(): View + { + return view('livewire.tasks.modals.detail'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index c75b55e..f2a5e9f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -31,6 +31,16 @@ class User extends Authenticatable return $this->role->contains('name', $role); } + public function hasAnyRole(array $roles): bool + { + foreach ($roles as $role) { + if ($this->hasRole($role)) { + return true; + } + } + return false; + } + public function tenant() { return $this->belongsTo(Tenant::class); diff --git a/composer.json b/composer.json index f930236..c8db371 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "laravel/reverb": "^1.10", "laravel/tinker": "^3.0", "livewire/livewire": "^4.0", - "livewire/volt": "^1.7.0" + "livewire/volt": "^1.7.0", + "wire-elements/modal": "*" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index 37595d7..b6b19a9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bdd78351d73171465c3922dafb001ba4", + "content-hash": "eac2e89f7c819b5d6fbfe22b5c260f04", "packages": [ { "name": "blade-ui-kit/blade-heroicons", @@ -4503,6 +4503,67 @@ ], "time": "2024-06-11T12:45:25+00:00" }, + { + "name": "spatie/laravel-package-tools", + "version": "1.93.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "d5552849801f2642aea710557463234b59ef65eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/d5552849801f2642aea710557463234b59ef65eb", + "reference": "d5552849801f2642aea710557463234b59ef65eb", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^8.0|^9.2|^10.0|^11.0", + "pestphp/pest": "^2.1|^3.1|^4.0", + "phpunit/php-code-coverage": "^10.0|^11.0|^12.0", + "phpunit/phpunit": "^10.5|^11.5|^12.5", + "spatie/pest-plugin-test-time": "^2.2|^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\LaravelPackageTools\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", + "keywords": [ + "laravel-package-tools", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.93.1" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-05-19T14:06:37+00:00" + }, { "name": "symfony/clock", "version": "v8.0.8", @@ -7177,6 +7238,70 @@ } ], "time": "2026-04-26T05:33:54+00:00" + }, + { + "name": "wire-elements/modal", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/wire-elements/modal.git", + "reference": "3c17ed4e5d1506773db37dbbfdcacff12b037317" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wire-elements/modal/zipball/3c17ed4e5d1506773db37dbbfdcacff12b037317", + "reference": "3c17ed4e5d1506773db37dbbfdcacff12b037317", + "shasum": "" + }, + "require": { + "livewire/livewire": "^3.2.3|^4.0", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.9" + }, + "require-dev": { + "orchestra/testbench": "^8.5", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "LivewireUI\\Modal\\LivewireModalServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "LivewireUI\\Modal\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Philo Hermans", + "email": "me@philohermans.com" + } + ], + "description": "Laravel Livewire modal component", + "keywords": [ + "laravel", + "livewire", + "modal" + ], + "support": { + "issues": "https://github.com/wire-elements/modal/issues", + "source": "https://github.com/wire-elements/modal/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/PhiloNL", + "type": "github" + } + ], + "time": "2026-01-23T11:08:48+00:00" } ], "packages-dev": [ diff --git a/config/reverb.php b/config/reverb.php index e4dff40..dab0c3d 100644 --- a/config/reverb.php +++ b/config/reverb.php @@ -82,7 +82,7 @@ return [ 'scheme' => env('REVERB_SCHEME', 'https'), 'useTLS' => env('REVERB_SCHEME', 'https') === 'https', ], - 'allowed_origins' => ['https://app.dev.lernschiff.com'], + 'allowed_origins' => ['app.dev.lernschiff.com'], 'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60), 'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30), 'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'), diff --git a/config/sidebar.php b/config/sidebar.php new file mode 100644 index 0000000..6fd1dd9 --- /dev/null +++ b/config/sidebar.php @@ -0,0 +1,80 @@ + [ + [ + 'label' => 'Hauptmenü', + 'items' => [ + [ + 'key' => 'dashboard', + 'route' => 'dashboard', + 'label' => 'Startseite', + 'icon' => 'home', + ], + [ + 'key' => 'subjects', + 'route' => 'subjects', + 'label' => 'Meine Fächer', + 'icon' => 'book-open', + ], + [ + 'key' => 'tasks', + 'route' => 'tasks', + 'label' => 'Aufgaben', + 'icon' => 'clipboard-document-check', + 'badge' => 4, + ], + [ + 'key' => 'photo', + 'route' => 'photo', + 'label' => 'Foto-Hilfe', + 'icon' => 'camera', + ], + [ + 'key' => 'rewards', + 'route' => 'rewards', + 'label' => 'Belohnungen', + 'icon' => 'trophy', + ], + [ + 'key' => 'progress', + 'route' => 'progress', + 'label' => 'Fortschritt', + 'icon' => 'chart-bar', + ], + ], + ], + [ + 'label' => 'Mehr', + 'items' => [ + [ + 'key' => 'notifications', + 'route' => 'notifications', + 'label' => 'Mitteilungen', + 'icon' => 'bell', + ], + [ + 'key' => 'settings', + 'route' => 'settings', + 'label' => 'Einstellungen', + 'icon' => 'cog-6-tooth', + ], + ], + ], + ], +]; diff --git a/database/seeders/MusterschuleSeeder.php b/database/seeders/MusterschuleSeeder.php index c43858b..7616cc9 100644 --- a/database/seeders/MusterschuleSeeder.php +++ b/database/seeders/MusterschuleSeeder.php @@ -26,8 +26,9 @@ class MusterschuleSeeder extends Seeder $users = [ ['email' => 'schuladmin@musterschule.at', 'name' => 'Schuladmin Muster', 'role' => 'school-admin'], ['email' => 'lehrer@musterschule.at', 'name' => 'Lehrerin Muster', 'role' => 'teacher'], - ['email' => 'kind1@musterschule.at', 'name' => 'Kind 1', 'role' => 'child'], - ['email' => 'kind2@musterschule.at', 'name' => 'Kind 2', 'role' => 'child'], + ['email' => 'eltern@musterschule.at', 'name' => 'Elternteil Muster', 'role' => 'parent'], + ['email' => 'leon@musterschule.at', 'name' => 'Leon Muster', 'role' => 'child'], + ['email' => 'mia@musterschule.at', 'name' => 'Mia Muster', 'role' => 'child'], ]; foreach ($users as $data) { diff --git a/docker-compose.yml b/docker-compose.yml index d372505..7b48950 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,21 @@ services: depends_on: [app] networks: [lernschiff_net] + vite: + build: + context: . + dockerfile: docker/app/Dockerfile + args: + UID: 1000 + GID: 1000 + working_dir: /var/www/html + command: sh -c "[ -d node_modules ] || npm install --silent; npm run dev -- --host 0.0.0.0" + volumes: + - .:/var/www/html + env_file: .env + networks: [lernschiff_net] + restart: unless-stopped + queue: build: context: . @@ -77,9 +92,9 @@ services: args: UID: 1000 GID: 1000 - command: php artisan reverb:start --host=0.0.0.0 --port=8081 + command: php artisan reverb:start --host=0.0.0.0 --port=8080 ports: - - "8080:8081" + - "8080:8080" volumes: - .:/var/www/html - app_storage:/var/www/html/storage/app diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf index 82a7a4a..5a33eaf 100644 --- a/docker/nginx/default.conf +++ b/docker/nginx/default.conf @@ -5,6 +5,35 @@ server { root /var/www/html/public; index index.php; + # Vite HMR WebSocket + location /vite-hmr { + proxy_pass http://vite:5173/vite-hmr; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 86400; + } + + # Vite dev asset paths — prefix locations with ^~ to skip regex eval + location ^~ /resources/ { + proxy_pass http://vite:5173; + proxy_http_version 1.1; + proxy_set_header Host $host; + } + + location ^~ /node_modules/ { + proxy_pass http://vite:5173; + proxy_http_version 1.1; + proxy_set_header Host $host; + } + + location ~ ^/(@vite|@id|@fs|@react-refresh)/ { + proxy_pass http://vite:5173; + proxy_http_version 1.1; + proxy_set_header Host $host; + } + location / { try_files $uri $uri/ /index.php?$query_string; } diff --git a/docs/superpowers/specs/2026-05-22-button-spinner-overlay-design.md b/docs/superpowers/specs/2026-05-22-button-spinner-overlay-design.md new file mode 100644 index 0000000..b434354 --- /dev/null +++ b/docs/superpowers/specs/2026-05-22-button-spinner-overlay-design.md @@ -0,0 +1,86 @@ +# Button Spinner Overlay — Design + +**Datum:** 2026-05-22 +**Scope:** Auth-Page Buttons (Login, Confirm-Password, Forgot-Password, Reset-Password, Verify-Email) + +## Problem + +Aktuelles Loading-Pattern der Auth-Buttons: +- Text + Icon werden via `wire:loading.remove` ausgeblendet +- Spinner + "Wird geladen…"-Text werden via `wire:loading` eingeblendet +- Button-Größe ist stabil, aber UX wirkt unruhig (Text wechselt komplett) +- Spinner steht links neben Loading-Text, nicht zentriert + +## Anforderungen + +1. **Spinner zentriert als Overlay** über dem Button-Inhalt +2. **Button-Größe stabil** beim Wechsel Loading/Idle +3. **Text bekommt `opacity-50`** beim Laden, damit Spinner besser sichtbar ist +4. **Konsistentes Pattern** über alle Auth-Buttons + +## Lösungsansatz + +`relative`/`absolute` Positionierung — Text bleibt im Document-Flow, Spinner wird darüber gelegt. + +### Markup-Pattern + +```html + + + + + Button-Text + + + + + + + + + +``` + +### Key-Mechanismen + +| Aspekt | Implementierung | +|--------|-----------------| +| Button-Größe stabil | Text-Span bleibt immer im Flow (kein `display:none`) | +| Spinner zentriert | `absolute inset-0 flex items-center justify-center` | +| Text dimmt | `wire:loading.class="opacity-50"` + `transition-opacity` | +| Spinner über Text | `absolute` positioniert auf `relative` Button | +| Disabled-State | `wire:loading.attr="disabled"` (unverändert) | + +## Scope — 5 Dateien + +| Datei | Aktion | +|-------|--------| +| `livewire/pages/auth/login.blade.php` | Pattern umbauen | +| `livewire/pages/auth/confirm-password.blade.php` | Pattern umbauen | +| `livewire/pages/auth/forgot-password.blade.php` | Pattern umbauen | +| `livewire/pages/auth/reset-password.blade.php` | Pattern umbauen | +| `livewire/pages/auth/verify-email.blade.php` | Spinner neu hinzufügen (gleiches Pattern) | + +`primary-button.blade.php` bleibt unverändert (Slot-basiert). + +## Nicht im Scope + +- Andere Buttons im Codebase (z.B. Dashboard, Settings) — nur Auth-Pages +- Änderungen an `primary-button.blade.php` selbst — Pattern wird per Page erstellt +- Backend-Logic, Routes, Auth-Flow — reine UI-Änderung + +## Testing + +- Bestehende 46 Tests (Pest) laufen unverändert durch +- Keine neuen Tests nötig (UI-only) +- Manuelle Verifikation: Login-Button im Browser, Submit auslösen, Spinner-Position prüfen + +## Verifikation nach Implementation + +1. Curl: Login-HTML enthält `relative` + `absolute inset-0` Klassen +2. Browser: Spinner mittig sichtbar beim Submit, Text dimmt, Button springt nicht +3. `php artisan test`: 46 passed diff --git a/package-lock.json b/package-lock.json index 59d38c1..2374e6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,13 @@ { - "name": "lernschiff", + "name": "html", "lockfileVersion": 3, "requires": true, "packages": { "": { + "dependencies": { + "laravel-echo": "^2.3.4", + "pusher-js": "^8.5.0" + }, "devDependencies": { "@tailwindcss/vite": "^4.0.0", "concurrently": "^9.0.1", @@ -2440,6 +2444,13 @@ "win32" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT", + "peer": true + }, "node_modules/@tailwindcss/node": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", @@ -3299,7 +3310,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3414,6 +3424,30 @@ "dev": true, "license": "MIT" }, + "node_modules/engine.io-client": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.5.tgz", + "integrity": "sha512-QCwxUDULPlXv8F6tqMMKx5dNkTe6OaBYRMPYeXKBlyOoKvAmE0ac6pW7fFhSscJ/5SI7666/U/B+MElbsrJlIg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.20.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz", @@ -4600,6 +4634,19 @@ "node": ">=0.10.0" } }, + "node_modules/laravel-echo": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-2.3.4.tgz", + "integrity": "sha512-rpALCIK1uw2SrttcK9P5JzItt5I85RcfXQKUNnkcorzhtKeXi5GS0PVFFBH8ppNo8wnbdBKuD1EtIHgTbXo9FQ==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "pusher-js": "*", + "socket.io-client": "*" + } + }, "node_modules/laravel-vite-plugin": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-3.1.0.tgz", @@ -4972,7 +5019,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -5199,6 +5245,15 @@ "node": ">=6" } }, + "node_modules/pusher-js": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.5.0.tgz", + "integrity": "sha512-V7uzGi9bqOOOyM/6IkJdpFyjGZj7llz1v0oWnYkZKcYLvbz6VcHVLmzKqkvegjuMumpfIEKGLmWHwFb39XFCpw==", + "license": "MIT", + "dependencies": { + "tweetnacl": "^1.0.3" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5698,6 +5753,36 @@ "node": ">=20.0.0" } }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", @@ -6039,6 +6124,12 @@ "dev": true, "license": "0BSD" }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, "node_modules/type-fest": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", @@ -6753,6 +6844,37 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 2b924d3..bc74577 100644 --- a/package.json +++ b/package.json @@ -13,5 +13,9 @@ "tailwindcss": "^4.0.0", "vite": "^8.0.0", "vite-plugin-pwa": "^1.3.0" + }, + "dependencies": { + "laravel-echo": "^2.3.4", + "pusher-js": "^8.5.0" } } diff --git a/public/favicon.ico b/public/favicon.ico index e69de29..b3c8a77 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png new file mode 100644 index 0000000..bd7fcda Binary files /dev/null and b/public/icons/apple-touch-icon.png differ diff --git a/public/icons/favicon-16.png b/public/icons/favicon-16.png new file mode 100644 index 0000000..ec16185 Binary files /dev/null and b/public/icons/favicon-16.png differ diff --git a/public/icons/favicon-32.png b/public/icons/favicon-32.png new file mode 100644 index 0000000..b847e61 Binary files /dev/null and b/public/icons/favicon-32.png differ diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png index 2542f79..1e684ea 100644 Binary files a/public/icons/icon-192.png and b/public/icons/icon-192.png differ diff --git a/public/icons/icon-512-maskable.png b/public/icons/icon-512-maskable.png new file mode 100644 index 0000000..5532028 Binary files /dev/null and b/public/icons/icon-512-maskable.png differ diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png index 2542f79..e48f4f3 100644 Binary files a/public/icons/icon-512.png and b/public/icons/icon-512.png differ diff --git a/public/icons/icon-source.svg b/public/icons/icon-source.svg new file mode 100644 index 0000000..f4c132b --- /dev/null +++ b/public/icons/icon-source.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/logo/favicon.ico b/public/logo/favicon.ico new file mode 100644 index 0000000..b3c8a77 Binary files /dev/null and b/public/logo/favicon.ico differ diff --git a/public/logo/favicon.svg b/public/logo/favicon.svg new file mode 100644 index 0000000..48fdc63 --- /dev/null +++ b/public/logo/favicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/logo/lernschiff-icon-monochrome.svg b/public/logo/lernschiff-icon-monochrome.svg new file mode 100644 index 0000000..6e0172c --- /dev/null +++ b/public/logo/lernschiff-icon-monochrome.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/logo/lernschiff-icon.svg b/public/logo/lernschiff-icon.svg new file mode 100644 index 0000000..48fdc63 --- /dev/null +++ b/public/logo/lernschiff-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/logo/lernschiff-wordmark.svg b/public/logo/lernschiff-wordmark.svg new file mode 100644 index 0000000..130bd1d --- /dev/null +++ b/public/logo/lernschiff-wordmark.svg @@ -0,0 +1,10 @@ + + + + + + + + + Lernschiff + diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest index 350fe4a..84f17d2 100644 --- a/public/manifest.webmanifest +++ b/public/manifest.webmanifest @@ -1,22 +1,32 @@ { - "name": "Lernschiff", - "short_name": "Lernschiff", - "description": "Kinderlernplattform für 3–14 Jahre", - "start_url": "/", - "display": "standalone", - "background_color": "#f0eee9", - "theme_color": "#f0eee9", - "lang": "de", - "icons": [ - { - "src": "/icons/icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/icons/icon-512.png", - "sizes": "512x512", - "type": "image/png" - } - ] + "name": "Lernschiff", + "short_name": "Lernschiff", + "description": "Kinderlernplattform für 3–14 Jahre", + "lang": "de", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "portrait-primary", + "background_color": "#f5f3ee", + "theme_color": "#f5f3ee", + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-512-maskable.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] } diff --git a/resources/css/app.css b/resources/css/app.css index d14f83c..26f7a31 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,7 +1,47 @@ @import "tailwindcss"; @theme { - --color-bg-base: #f0eee9; + /* === Background-Layers === */ + --color-bg-base: #f5f3ee; + --color-bg-soft: #ffffff; + --color-bg-elev: #fafaf7; + --color-bg-tint: #f0eee9; + + /* === Ink-Hierarchie === */ + --color-ink-1: oklch(0.22 0.015 270); + --color-ink-2: oklch(0.42 0.012 270); + --color-ink-3: oklch(0.58 0.01 270); + --color-ink-4: oklch(0.72 0.008 270); + + /* === Lines / Borders === */ + --color-line: oklch(0.92 0.006 85); + --color-line-strong: oklch(0.85 0.008 85); + + /* === Brand / Primary === */ + --color-primary: oklch(0.50 0.18 255); + --color-primary-soft: oklch(0.95 0.04 255); + --color-primary-ink: oklch(0.42 0.22 255); + --color-primary-hover: oklch(0.45 0.20 255); + + /* === Subject-Color-Tokens === */ + --color-rose-soft: oklch(0.95 0.04 25); + --color-rose-ink: oklch(0.50 0.16 25); + + --color-violet-soft: oklch(0.95 0.04 295); + --color-violet-ink: oklch(0.50 0.16 295); + + --color-green-soft: oklch(0.95 0.04 155); + --color-green-ink: oklch(0.50 0.14 155); + + --color-amber-soft: oklch(0.95 0.05 75); + --color-amber-ink: oklch(0.55 0.14 75); + + /* === Feedback === */ + --color-success: oklch(0.55 0.16 155); + --color-warning: oklch(0.65 0.16 75); + --color-danger: oklch(0.55 0.20 25); + + /* === Legacy-Tokens (Auth-Pages) === */ --color-text-primary: oklch(0.22 0.015 270); --color-text-secondary: oklch(0.42 0.012 270); --color-text-muted: oklch(0.58 0.01 270); @@ -10,10 +50,24 @@ --color-accent: oklch(0.42 0.22 255); --color-accent-hover: oklch(0.36 0.24 255); --color-accent-muted: oklch(0.96 0.04 255); - --color-success: oklch(0.52 0.18 155); - --color-danger: oklch(0.50 0.22 25); - --font-display: 'Plus Jakarta Sans', sans-serif; + + /* === Fonts === */ + --font-display: 'Plus Jakarta Sans', system-ui, sans-serif; --font-mono: 'JetBrains Mono', monospace; + + /* === Radii === */ + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 14px; + --radius-xl: 18px; + + /* === Shadows === */ + --shadow-sm: 0 1px 2px rgba(20, 20, 30, 0.04); + --shadow-md: 0 4px 12px rgba(20, 20, 30, 0.06); + + /* === Layout-Spacing === */ + --spacing-sb: 240px; + --spacing-tb: 56px; } @font-face { @@ -21,7 +75,7 @@ src: url('/fonts/PlusJakartaSans-VariableFont_wght.woff2') format('woff2'); font-weight: 100 900; font-style: normal; - font-display: swap; + font-display: block; } @font-face { @@ -29,11 +83,19 @@ src: url('/fonts/JetBrainsMono-Regular.woff2') format('woff2'); font-weight: 400; font-style: normal; - font-display: swap; + font-display: block; } body { background-color: var(--color-bg-base); - color: var(--color-text-primary); + color: var(--color-ink-1); font-family: var(--font-display); + font-feature-settings: 'cv11','ss01'; } + +.font-mono, .mono { + font-family: var(--font-mono); + font-feature-settings: 'tnum','zero'; +} + +[x-cloak] { display: none !important; } diff --git a/resources/css/safelist-tailwindcss.txt b/resources/css/safelist-tailwindcss.txt new file mode 100644 index 0000000..c99c89b --- /dev/null +++ b/resources/css/safelist-tailwindcss.txt @@ -0,0 +1,5 @@ +sm:max-w-md +md:max-w-xl +lg:max-w-3xl +xl:max-w-5xl +2xl:max-w-7xl diff --git a/resources/js/app.js b/resources/js/app.js index 8337712..fe59342 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1 +1 @@ -// +import './echo'; diff --git a/resources/js/auth.js b/resources/js/auth.js index d49d29a..fcd0dda 100644 --- a/resources/js/auth.js +++ b/resources/js/auth.js @@ -1 +1,2 @@ // Auth entry point +import './echo'; diff --git a/resources/js/echo.js b/resources/js/echo.js new file mode 100644 index 0000000..41ce403 --- /dev/null +++ b/resources/js/echo.js @@ -0,0 +1,15 @@ +import Echo from 'laravel-echo'; +import Pusher from 'pusher-js'; + +if (!window.Echo) { + window.Pusher = Pusher; + window.Echo = new Echo({ + broadcaster: 'reverb', + key: import.meta.env.VITE_REVERB_APP_KEY, + wsHost: import.meta.env.VITE_REVERB_HOST, + wsPort: import.meta.env.VITE_REVERB_PORT ?? 80, + wssPort: import.meta.env.VITE_REVERB_PORT ?? 443, + forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', + enabledTransports: ['ws', 'wss'], + }); +} diff --git a/resources/views/components/auth-session-status.blade.php b/resources/views/components/auth-session-status.blade.php index 59ba268..6009ada 100644 --- a/resources/views/components/auth-session-status.blade.php +++ b/resources/views/components/auth-session-status.blade.php @@ -2,7 +2,7 @@ @if ($status)
merge([ - 'class' => 'flex items-center gap-2 text-sm font-medium text-[--color-success] bg-[oklch(0.96_0.05_155)] border border-[oklch(0.88_0.10_155)] rounded-lg px-4 py-3' + 'class' => 'flex items-center gap-2 text-sm font-medium text-success bg-[oklch(0.96_0.05_155)] border border-[oklch(0.88_0.10_155)] rounded-lg px-4 py-3' ]) }}> {{ $status }} diff --git a/resources/views/components/btn.blade.php b/resources/views/components/btn.blade.php new file mode 100644 index 0000000..795d79c --- /dev/null +++ b/resources/views/components/btn.blade.php @@ -0,0 +1,30 @@ +@props([ + 'variant' => 'default', + 'size' => 'md', + 'as' => 'button', +]) + +@php + $base = 'inline-flex items-center justify-center gap-2 rounded-lg font-semibold cursor-pointer transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed'; + + $variants = [ + 'default' => 'border border-line bg-bg-soft text-ink-1 hover:bg-bg-tint hover:border-line-strong focus-visible:ring-line-strong', + 'primary' => 'bg-ink-1 text-bg-base border border-ink-1 hover:bg-ink-2 hover:border-ink-2 focus-visible:ring-ink-1', + 'accent' => 'bg-primary text-white border border-primary hover:bg-primary-hover hover:border-primary-hover focus-visible:ring-primary', + 'ghost' => 'bg-transparent text-ink-2 hover:bg-bg-tint border border-transparent focus-visible:ring-line-strong', + ]; + + $sizes = [ + 'sm' => 'px-2.5 py-1.5 text-xs', + 'md' => 'px-3.5 py-2 text-sm', + 'lg' => 'px-5 py-2.5 text-sm', + ]; + + $classes = "{$base} {$variants[$variant]} {$sizes[$size]}"; +@endphp + +@if($as === 'a') + merge(['class' => $classes]) }}>{{ $slot }} +@else + +@endif diff --git a/resources/views/components/card.blade.php b/resources/views/components/card.blade.php new file mode 100644 index 0000000..22917ec --- /dev/null +++ b/resources/views/components/card.blade.php @@ -0,0 +1,18 @@ +@props([ + 'variant' => 'default', +]) + +@php + $base = 'rounded-xl p-5 border'; + + $variants = [ + 'default' => 'bg-bg-soft border-line', + 'gradient-primary' => 'bg-primary-soft border-primary-soft', + 'gradient-violet' => 'bg-violet-soft border-violet-soft', + 'tint' => 'bg-bg-tint border-line', + ]; +@endphp + +
merge(['class' => "{$base} {$variants[$variant]}"]) }}> + {{ $slot }} +
diff --git a/resources/views/components/input-error.blade.php b/resources/views/components/input-error.blade.php index 850fad8..2474a9c 100644 --- a/resources/views/components/input-error.blade.php +++ b/resources/views/components/input-error.blade.php @@ -1,7 +1,7 @@ @props(['messages']) @if ($messages) -