142 lines
8.0 KiB
PHP
142 lines
8.0 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
<link rel="apple-touch-icon" href="/favicon.svg">
|
|
<title>{{ $title ?? config('app.name') }}</title>
|
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
|
@livewireStyles
|
|
</head>
|
|
<body class="min-h-full bg-bg text-body antialiased" x-data="{ nav: false }">
|
|
@php
|
|
$maintenanceWindows = collect();
|
|
if (auth()->check()) {
|
|
$portalCustomer = \App\Models\Customer::query()->where('user_id', auth()->id())->first()
|
|
?? \App\Models\Customer::query()->where('email', auth()->user()->email)->first();
|
|
if ($portalCustomer) {
|
|
$maintenanceWindows = \App\Models\MaintenanceWindow::bannerFor($portalCustomer);
|
|
}
|
|
}
|
|
@endphp
|
|
@foreach ($maintenanceWindows as $mw)
|
|
@php $isActive = now()->greaterThanOrEqualTo($mw->starts_at) && now()->lessThanOrEqualTo($mw->ends_at); @endphp
|
|
<div class="flex flex-wrap items-center justify-center gap-x-3 gap-y-1 border-b {{ $isActive ? 'border-warning-border bg-warning-bg' : 'border-info-border bg-info-bg' }} px-4 py-2 text-center text-sm text-ink">
|
|
<x-ui.icon name="alert-triangle" class="size-4 shrink-0 {{ $isActive ? 'text-warning' : 'text-info' }}" />
|
|
<span class="font-medium">{{ $mw->title }}</span>
|
|
<span class="text-muted">
|
|
{{ $isActive
|
|
? __('maintenance.banner_active', ['end' => $mw->ends_at->isoFormat('LT')])
|
|
: __('maintenance.banner_upcoming', ['start' => $mw->starts_at->isoFormat('LLL'), 'end' => $mw->ends_at->isoFormat('LT')]) }}
|
|
</span>
|
|
</div>
|
|
@endforeach
|
|
@if (session('impersonator_id'))
|
|
{{-- Admin impersonation banner — visible on every portal page until the admin returns. --}}
|
|
<div class="flex flex-wrap items-center justify-center gap-x-3 gap-y-1 border-b border-warning-border bg-warning-bg px-4 py-2 text-center text-sm font-medium text-ink">
|
|
<span>{{ __('impersonate.banner', ['name' => auth()->user()->name]) }}</span>
|
|
<form method="POST" action="{{ route('impersonate.leave') }}" class="inline">
|
|
@csrf
|
|
<button type="submit" class="inline-flex items-center gap-1 rounded-pill border border-warning-border px-3 py-0.5 font-semibold text-warning underline-offset-2 hover:underline">
|
|
<x-ui.icon name="log-out" class="size-4" />
|
|
{{ __('impersonate.leave') }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
@endif
|
|
<div class="flex min-h-screen lg:h-screen lg:overflow-hidden">
|
|
{{-- Sidebar: fixed drawer on small screens, fixed full-height column on large (R7). --}}
|
|
<aside
|
|
class="fixed inset-y-0 left-0 z-40 flex w-60 flex-col overflow-y-auto border-r border-line bg-surface px-3 py-4 transition-transform lg:static lg:h-screen lg:translate-x-0"
|
|
:class="nav ? 'translate-x-0' : '-translate-x-full'"
|
|
>
|
|
<div class="flex items-center justify-between px-3 pb-4">
|
|
<span class="flex items-center gap-2">
|
|
<x-ui.logo class="size-7" />
|
|
<span class="font-mono text-lg font-semibold tracking-tight text-ink">CluPilot</span>
|
|
</span>
|
|
<button type="button" class="lg:hidden text-muted" @click="nav = false" aria-label="{{ __('dashboard.close_nav') }}">
|
|
<x-ui.icon name="chevron-down" class="size-5 rotate-90" />
|
|
</button>
|
|
</div>
|
|
<nav class="space-y-1">
|
|
@foreach ([
|
|
['dashboard', 'gauge', 'overview'],
|
|
['cloud', 'cloud', 'cloud'],
|
|
['users', 'users', 'users'],
|
|
['backups', 'database', 'backups'],
|
|
['invoices', 'receipt', 'invoices'],
|
|
['billing', 'box', 'billing'],
|
|
['settings', 'settings', 'settings'],
|
|
['support', 'life-buoy', 'support'],
|
|
] as [$route, $icon, $key])
|
|
<x-ui.nav-item :href="route($route)" :active="request()->routeIs($route)">
|
|
<x-slot:icon><x-ui.icon :name="$icon" /></x-slot:icon>
|
|
{{ __('dashboard.nav.'.$key) }}
|
|
</x-ui.nav-item>
|
|
@endforeach
|
|
</nav>
|
|
</aside>
|
|
|
|
{{-- Mobile backdrop --}}
|
|
<div x-show="nav" x-cloak @click="nav = false" class="fixed inset-0 z-30 bg-ink/20 lg:hidden"></div>
|
|
|
|
<div class="flex min-w-0 flex-1 flex-col lg:h-screen lg:overflow-hidden">
|
|
<header class="flex h-14 shrink-0 items-center justify-between border-b border-line bg-surface px-4">
|
|
<button type="button" class="lg:hidden inline-flex min-h-11 min-w-11 items-center justify-center rounded text-muted hover:bg-surface-hover" @click="nav = true" aria-label="{{ __('dashboard.open_nav') }}">
|
|
<x-ui.icon name="menu" />
|
|
</button>
|
|
<div class="flex-1"></div>
|
|
|
|
@auth
|
|
<div class="relative" x-data="{ open: false }" @keydown.escape="open = false">
|
|
<button type="button" class="flex items-center gap-2 rounded px-2 py-1.5 hover:bg-surface-hover" @click="open = !open" :aria-expanded="open" aria-haspopup="menu">
|
|
<span class="flex size-8 items-center justify-center rounded-pill bg-accent-subtle text-sm font-semibold text-accent-text">{{ \Illuminate\Support\Str::upper(\Illuminate\Support\Str::substr(auth()->user()->name, 0, 1)) }}</span>
|
|
<span class="hidden text-sm text-body sm:block">{{ auth()->user()->name }}</span>
|
|
<x-ui.icon name="chevron-down" class="size-4 text-muted" />
|
|
</button>
|
|
<div x-show="open" x-cloak @click.outside="open = false" class="absolute right-0 mt-1 w-48 rounded-lg border border-line bg-surface py-1 shadow-md">
|
|
<form method="POST" action="{{ route('logout') }}">
|
|
@csrf
|
|
<button type="submit" class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-body hover:bg-surface-hover">
|
|
<x-ui.icon name="log-out" class="size-4" />
|
|
{{ __('dashboard.logout') }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
@endauth
|
|
</header>
|
|
|
|
<main class="mx-auto w-full max-w-[1200px] flex-1 p-6 lg:overflow-y-auto lg:p-8">
|
|
{{ $slot }}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Global toast — any element can trigger it with $dispatch('notify', { message }) --}}
|
|
<div
|
|
x-data="{ show: false, msg: '', timer: null }"
|
|
@notify.window="msg = $event.detail.message; show = true; clearTimeout(timer); timer = setTimeout(() => show = false, 3200)"
|
|
x-show="show"
|
|
x-cloak
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 translate-y-12"
|
|
x-transition:enter-end="opacity-100 translate-y-0"
|
|
x-transition:leave="transition ease-in duration-300"
|
|
x-transition:leave-start="opacity-100 translate-y-0"
|
|
x-transition:leave-end="opacity-0 translate-y-12"
|
|
role="status"
|
|
aria-live="polite"
|
|
class="fixed bottom-6 left-1/2 z-[80] -translate-x-1/2 rounded-pill bg-ink px-5 py-3 text-sm font-medium text-on-accent shadow-md"
|
|
>
|
|
<span x-text="msg"></span>
|
|
</div>
|
|
|
|
@livewire('wire-elements-modal')
|
|
@livewireScripts
|
|
</body>
|
|
</html>
|