feat(ui): app layout with sidebar and topbar

Implement Nimuli-specific authenticated layout with:
- Sidebar navigation with 7 main menu items (dashboard, links, qr codes, link-in-bio, analytics, domains, settings)
- Topbar header with user email and logout button
- Safe route checking using Route::has() for undefined routes
- Workspace display in sidebar when available
- Livewire AppLayout base class for future Livewire components
main
boban 2026-05-16 07:54:31 +02:00
parent a3930b5d25
commit 3929c48ea1
4 changed files with 88 additions and 0 deletions

View File

@ -0,0 +1,13 @@
<?php
namespace App\Livewire\Layouts;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.nimuli-app')]
abstract class AppLayout extends Component
{
// Base layout class for authenticated Livewire components
// Uses the Nimuli app layout with sidebar and topbar
}

View File

@ -0,0 +1,38 @@
@php
$workspace = app()->bound('current_workspace') ? app('current_workspace') : null;
$nav = [
['label' => 'Dashboard', 'route' => 'dashboard', 'icon' => '⊞'],
['label' => 'Links', 'route' => 'links.index', 'icon' => '↗'],
['label' => 'QR Codes', 'route' => 'qr.index', 'icon' => '▦'],
['label' => 'Link-in-Bio', 'route' => 'bio.index', 'icon' => '≡'],
['label' => 'Analytics', 'route' => 'analytics.index', 'icon' => '↑'],
['label' => 'Domains', 'route' => 'domains.index', 'icon' => '◎'],
['label' => 'Settings', 'route' => 'settings.index', 'icon' => '⚙'],
];
@endphp
<aside class="w-60 flex-shrink-0 bg-gray-900 border-r border-gray-800 flex flex-col">
<div class="h-16 flex items-center px-5 border-b border-gray-800">
<span class="text-lg font-bold tracking-tight">Nimuli</span>
</div>
@if($workspace)
<div class="px-3 py-3 border-b border-gray-800">
<div class="text-xs text-gray-500 uppercase tracking-wider mb-1">Workspace</div>
<div class="text-sm font-medium truncate">{{ $workspace->name }}</div>
</div>
@endif
<nav class="flex-1 px-2 py-3 space-y-0.5">
@foreach($nav as $item)
<a href="{{ Route::has($item['route']) ? route($item['route']) : '#' }}"
class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors
{{ request()->routeIs($item['route'] . '*') ? 'bg-gray-800 text-white' : 'text-gray-400 hover:text-white hover:bg-gray-800' }}">
<span class="w-5 text-center">{{ $item['icon'] }}</span>
{{ $item['label'] }}
</a>
@endforeach
</nav>
@if($workspace?->plan)
<div class="px-3 py-3 border-t border-gray-800">
<div class="text-xs text-gray-500">Plan: {{ ucfirst($workspace->plan->name) }}</div>
</div>
@endif
</aside>

View File

@ -0,0 +1,14 @@
<header class="h-16 bg-gray-900 border-b border-gray-800 flex items-center justify-between px-6 flex-shrink-0">
<div class="flex items-center gap-3">
<span class="text-sm text-gray-400">{{ $title ?? '' }}</span>
</div>
<div class="flex items-center gap-3">
@auth
<span class="text-sm text-gray-400">{{ auth()->user()->email }}</span>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="text-sm text-gray-400 hover:text-white transition-colors">Logout</button>
</form>
@endauth
</div>
</header>

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}" data-theme="{{ auth()->user()?->theme ?? 'dark' }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title ?? 'Nimuli' }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
</head>
<body class="bg-gray-950 text-gray-100 font-sans antialiased">
<div class="flex h-screen overflow-hidden">
<x-sidebar />
<div class="flex-1 flex flex-col overflow-hidden">
<x-topbar :title="$title ?? ''" />
<main class="flex-1 overflow-y-auto p-6">
{{ $slot }}
</main>
</div>
</div>
@livewireScripts
</body>
</html>