feat: harden auth flows and server mobile navigation

Add normalized email identity rate limiting for registration and forgot-password requests, and refresh Sentinel status from restart broadcasts.

Rework server sidebars and navbar for mobile menus and active status visibility.
This commit is contained in:
Andras Bacsai committed 2026-07-08 09:42:52 +02:00
1 parent 94079c90f2
commit b0f0f7d8d0
42 files changed
+1064 -128

No files matched your search

@@ -1,16 +1,139 @@
<div class="sub-menu-wrapper">
<a class="{{ request()->routeIs('server.proxy') ? 'sub-menu-item menu-item-active' : 'sub-menu-item' }}" {{ wireNavigate() }}
href="{{ route('server.proxy', $parameters) }}">
<span class="menu-item-label">Configuration</span>
</a>
@if ($server->proxySet())
<a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'sub-menu-item menu-item-active' : 'sub-menu-item' }}" {{ wireNavigate() }}
href="{{ route('server.proxy.dynamic-confs', $parameters) }}">
<span class="menu-item-label">Dynamic Configurations</span>
</a>
<a class="{{ request()->routeIs('server.proxy.logs') ? 'sub-menu-item menu-item-active' : 'sub-menu-item' }}"
href="{{ route('server.proxy.logs', $parameters) }}">
<span class="menu-item-label">Logs</span>
</a>
@endif
@php
$serverPageItems = [
[
'label' => 'Configuration',
'route' => 'server.show',
'active' => request()->routeIs('server.show', 'server.advanced', 'server.private-key', 'server.cloud-provider-token', 'server.ca-certificate', 'server.cloudflare-tunnel', 'server.docker-cleanup', 'server.destinations', 'server.log-drains', 'server.metrics', 'server.swarm', 'server.delete'),
],
[
'label' => 'Proxy',
'route' => 'server.proxy',
'active' => request()->routeIs('server.proxy', 'server.proxy.*'),
'visible' => ! $server->isSwarmWorker() && ! $server->settings->is_build_server,
],
[
'label' => 'Sentinel',
'route' => 'server.sentinel',
'active' => request()->routeIs('server.sentinel', 'server.sentinel.*'),
'visible' => $server->isFunctional() && ! $server->isSwarm() && ! $server->settings->is_build_server && auth()->user()?->can('viewSentinel', $server),
],
[
'label' => 'Resources',
'route' => 'server.resources',
'active' => request()->routeIs('server.resources'),
],
[
'label' => 'Terminal',
'route' => 'server.command',
'active' => request()->routeIs('server.command'),
'navigate' => false,
'visible' => auth()->user()?->can('canAccessTerminal'),
],
[
'label' => 'Security',
'route' => 'server.security.patches',
'active' => request()->routeIs('server.security.patches'),
'visible' => auth()->user()?->can('update', $server),
],
];
$proxyMenuItems = [
[
'label' => 'Configuration',
'route' => 'server.proxy',
'active' => request()->routeIs('server.proxy'),
],
[
'label' => 'Dynamic Configurations',
'route' => 'server.proxy.dynamic-confs',
'active' => request()->routeIs('server.proxy.dynamic-confs'),
'visible' => $server->proxySet(),
],
[
'label' => 'Logs',
'route' => 'server.proxy.logs',
'active' => request()->routeIs('server.proxy.logs'),
'visible' => $server->proxySet(),
'navigate' => false,
],
];
$serverPageItems = array_values(array_filter(
$serverPageItems,
fn (array $item): bool => $item['visible'] ?? true,
));
$proxyMenuItems = array_values(array_filter(
$proxyMenuItems,
fn (array $item): bool => $item['visible'] ?? true,
));
$activeProxyMenuItem = collect($proxyMenuItems)->firstWhere('active', true) ?? $proxyMenuItems[0];
$activeProxyMenuValue = (($activeProxyMenuItem['navigate'] ?? true) ? 'navigate' : 'location').'|proxy|'.route($activeProxyMenuItem['route'], $parameters);
@endphp
<div class="w-full md:w-auto">
<div class="mb-4 w-full border-b-2 border-solid border-neutral-200 pb-6 md:hidden dark:border-coolgray-200">
<label id="server-mobile-section-label" for="server-mobile-section" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400">Section</label>
<select id="server-mobile-section" class="select w-full" aria-label="Proxy menu"
data-current-value="{{ $activeProxyMenuValue }}"
x-data="{
init() {
this.syncFromLocation();
window.Livewire?.hook?.('morphed', ({ el }) => {
if (el.contains(this.$el)) {
queueMicrotask(() => this.syncFromLocation());
}
});
},
selected: $el.dataset.currentValue,
syncFromLocation() {
const currentUrl = new URL(window.location.href);
const matchingOptions = Array.from(this.$el.options).filter((option) => {
const optionUrl = new URL(option.value.split('|').slice(2).join('|'), window.location.origin);
return optionUrl.pathname === currentUrl.pathname;
});
const selectedOption = matchingOptions.find((option) => option.value.startsWith('navigate|proxy|') || option.value.startsWith('location|proxy|')) || matchingOptions[0];
if (selectedOption) {
this.selected = selectedOption.value;
}
},
}"
x-on:livewire:navigated.window="syncFromLocation()"
x-model="selected"
x-on:change="
const value = $event.target.value;
const url = value.split('|').slice(2).join('|');
if (value.startsWith('navigate|')) {
window.Livewire?.navigate ? window.Livewire.navigate(url) : window.location.href = url;
return;
}
window.location.href = url;
">
<optgroup label="Server">
@foreach ($serverPageItems as $menuItem)
<option value="{{ ($menuItem['navigate'] ?? true) ? 'navigate' : 'location' }}|server|{{ route($menuItem['route'], $parameters) }}">
{{ $menuItem['label'] }}
</option>
@endforeach
</optgroup>
<optgroup label="Proxy">
@foreach ($proxyMenuItems as $menuItem)
<option value="{{ ($menuItem['navigate'] ?? true) ? 'navigate' : 'location' }}|proxy|{{ route($menuItem['route'], $parameters) }}">
{{ $menuItem['label'] }}
</option>
@endforeach
</optgroup>
</select>
</div>
<div class="sub-menu-wrapper hidden md:flex">
@foreach ($proxyMenuItems as $menuItem)
<a class="{{ $menuItem['active'] ? 'sub-menu-item menu-item-active' : 'sub-menu-item' }}" @if ($menuItem['navigate'] ?? true) {{ wireNavigate() }} @endif
href="{{ route($menuItem['route'], $parameters) }}">
<span class="menu-item-label">{{ $menuItem['label'] }}</span>
</a>
@endforeach
</div>
</div>
@@ -1,10 +1,127 @@
<div class="sub-menu-wrapper">
<a class="{{ request()->routeIs('server.security.patches') ? 'sub-menu-item menu-item-active' : 'sub-menu-item' }}" {{ wireNavigate() }}
href="{{ route('server.security.patches', $parameters) }}">
<span class="menu-item-label">Server Patching</span>
</a>
<a class="{{ request()->routeIs('server.security.terminal-access') ? 'sub-menu-item menu-item-active' : 'sub-menu-item' }}"
href="{{ route('server.security.terminal-access', $parameters) }}">
<span class="menu-item-label">Terminal Access</span>
</a>
@php
$serverPageItems = [
[
'label' => 'Configuration',
'route' => 'server.show',
'active' => request()->routeIs('server.show', 'server.advanced', 'server.private-key', 'server.cloud-provider-token', 'server.ca-certificate', 'server.cloudflare-tunnel', 'server.docker-cleanup', 'server.destinations', 'server.log-drains', 'server.metrics', 'server.swarm', 'server.delete'),
],
[
'label' => 'Proxy',
'route' => 'server.proxy',
'active' => request()->routeIs('server.proxy', 'server.proxy.*'),
'visible' => ! $server->isSwarmWorker() && ! $server->settings->is_build_server,
],
[
'label' => 'Sentinel',
'route' => 'server.sentinel',
'active' => request()->routeIs('server.sentinel', 'server.sentinel.*'),
'visible' => $server->isFunctional() && ! $server->isSwarm() && ! $server->settings->is_build_server && auth()->user()?->can('viewSentinel', $server),
],
[
'label' => 'Resources',
'route' => 'server.resources',
'active' => request()->routeIs('server.resources'),
],
[
'label' => 'Terminal',
'route' => 'server.command',
'active' => request()->routeIs('server.command'),
'navigate' => false,
'visible' => auth()->user()?->can('canAccessTerminal'),
],
[
'label' => 'Security',
'route' => 'server.security.patches',
'active' => request()->routeIs('server.security.patches'),
'visible' => auth()->user()?->can('update', $server),
],
];
$securityMenuItems = [
[
'label' => 'Server Patching',
'route' => 'server.security.patches',
'active' => request()->routeIs('server.security.patches'),
],
[
'label' => 'Terminal Access',
'route' => 'server.security.terminal-access',
'active' => request()->routeIs('server.security.terminal-access'),
'navigate' => false,
],
];
$serverPageItems = array_values(array_filter(
$serverPageItems,
fn (array $item): bool => $item['visible'] ?? true,
));
$activeSecurityMenuItem = collect($securityMenuItems)->firstWhere('active', true) ?? $securityMenuItems[0];
$activeSecurityMenuValue = (($activeSecurityMenuItem['navigate'] ?? true) ? 'navigate' : 'location').'|security|'.route($activeSecurityMenuItem['route'], $parameters);
@endphp
<div class="w-full md:w-auto">
<div class="mb-4 w-full border-b-2 border-solid border-neutral-200 pb-6 md:hidden dark:border-coolgray-200">
<label id="server-mobile-section-label" for="server-mobile-section" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400">Section</label>
<select id="server-mobile-section" class="select w-full" aria-label="Security menu"
data-current-value="{{ $activeSecurityMenuValue }}"
x-data="{
init() {
this.syncFromLocation();
window.Livewire?.hook?.('morphed', ({ el }) => {
if (el.contains(this.$el)) {
queueMicrotask(() => this.syncFromLocation());
}
});
},
selected: $el.dataset.currentValue,
syncFromLocation() {
const currentUrl = new URL(window.location.href);
const matchingOptions = Array.from(this.$el.options).filter((option) => {
const optionUrl = new URL(option.value.split('|').slice(2).join('|'), window.location.origin);
return optionUrl.pathname === currentUrl.pathname;
});
const selectedOption = matchingOptions.find((option) => option.value.startsWith('navigate|security|') || option.value.startsWith('location|security|')) || matchingOptions[0];
if (selectedOption) {
this.selected = selectedOption.value;
}
},
}"
x-on:livewire:navigated.window="syncFromLocation()"
x-model="selected"
x-on:change="
const value = $event.target.value;
const url = value.split('|').slice(2).join('|');
if (value.startsWith('navigate|')) {
window.Livewire?.navigate ? window.Livewire.navigate(url) : window.location.href = url;
return;
}
window.location.href = url;
">
<optgroup label="Server">
@foreach ($serverPageItems as $menuItem)
<option value="{{ ($menuItem['navigate'] ?? true) ? 'navigate' : 'location' }}|server|{{ route($menuItem['route'], $parameters) }}">
{{ $menuItem['label'] }}
</option>
@endforeach
</optgroup>
<optgroup label="Security">
@foreach ($securityMenuItems as $menuItem)
<option value="{{ ($menuItem['navigate'] ?? true) ? 'navigate' : 'location' }}|security|{{ route($menuItem['route'], $parameters) }}">
{{ $menuItem['label'] }}
</option>
@endforeach
</optgroup>
</select>
</div>
<div class="sub-menu-wrapper hidden md:flex">
@foreach ($securityMenuItems as $menuItem)
<a class="{{ $menuItem['active'] ? 'sub-menu-item menu-item-active' : 'sub-menu-item' }}" @if ($menuItem['navigate'] ?? true) {{ wireNavigate() }} @endif
href="{{ route($menuItem['route'], $parameters) }}">
<span class="menu-item-label">{{ $menuItem['label'] }}</span>
</a>
@endforeach
</div>
</div>
@@ -1,12 +1,121 @@
<div class="sub-menu-wrapper">
@php
$serverPageItems = [
[
'label' => 'Configuration',
'route' => 'server.show',
'active' => request()->routeIs('server.show', 'server.advanced', 'server.private-key', 'server.cloud-provider-token', 'server.ca-certificate', 'server.cloudflare-tunnel', 'server.docker-cleanup', 'server.destinations', 'server.log-drains', 'server.metrics', 'server.swarm', 'server.delete'),
],
[
'label' => 'Proxy',
'route' => 'server.proxy',
'active' => request()->routeIs('server.proxy', 'server.proxy.*'),
'visible' => ! $server->isSwarmWorker() && ! $server->settings->is_build_server,
],
[
'label' => 'Sentinel',
'route' => 'server.sentinel',
'active' => request()->routeIs('server.sentinel', 'server.sentinel.*'),
'visible' => $server->isFunctional() && ! $server->isSwarm() && ! $server->settings->is_build_server && auth()->user()?->can('viewSentinel', $server),
],
[
'label' => 'Resources',
'route' => 'server.resources',
'active' => request()->routeIs('server.resources'),
],
[
'label' => 'Terminal',
'route' => 'server.command',
'active' => request()->routeIs('server.command'),
'navigate' => false,
'visible' => auth()->user()?->can('canAccessTerminal'),
],
[
'label' => 'Security',
'route' => 'server.security.patches',
'active' => request()->routeIs('server.security.patches'),
'visible' => auth()->user()?->can('update', $server),
],
];
$sentinelMenuItems = [
[
'label' => 'Configuration',
'route' => 'server.sentinel',
'active' => request()->routeIs('server.sentinel'),
],
[
'label' => 'Logs',
'route' => 'server.sentinel.logs',
'active' => request()->routeIs('server.sentinel.logs'),
],
];
$serverPageItems = array_values(array_filter(
$serverPageItems,
fn (array $item): bool => $item['visible'] ?? true,
));
$activeSentinelMenuItem = collect($sentinelMenuItems)->firstWhere('active', true) ?? $sentinelMenuItems[0];
$activeSentinelMenuValue = 'navigate|sentinel|'.route($activeSentinelMenuItem['route'], $parameters);
@endphp
<div class="w-full md:w-auto">
@can('viewSentinel', $server)
<a class="{{ request()->routeIs('server.sentinel') ? 'sub-menu-item menu-item-active' : 'sub-menu-item' }}" {{ wireNavigate() }}
href="{{ route('server.sentinel', $parameters) }}">
<span class="menu-item-label">Configuration</span>
</a>
<a class="{{ request()->routeIs('server.sentinel.logs') ? 'sub-menu-item menu-item-active' : 'sub-menu-item' }}" {{ wireNavigate() }}
href="{{ route('server.sentinel.logs', $parameters) }}">
<span class="menu-item-label">Logs</span>
</a>
<div class="mb-4 w-full border-b-2 border-solid border-neutral-200 pb-6 md:hidden dark:border-coolgray-200">
<label id="server-mobile-section-label" for="server-mobile-section" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400">Section</label>
<select id="server-mobile-section" class="select w-full" aria-label="Sentinel menu"
data-current-value="{{ $activeSentinelMenuValue }}"
x-data="{
init() {
this.syncFromLocation();
window.Livewire?.hook?.('morphed', ({ el }) => {
if (el.contains(this.$el)) {
queueMicrotask(() => this.syncFromLocation());
}
});
},
selected: $el.dataset.currentValue,
syncFromLocation() {
const currentUrl = new URL(window.location.href);
const matchingOptions = Array.from(this.$el.options).filter((option) => {
const optionUrl = new URL(option.value.split('|').slice(2).join('|'), window.location.origin);
return optionUrl.pathname === currentUrl.pathname;
});
const selectedOption = matchingOptions.find((option) => option.value.startsWith('navigate|sentinel|')) || matchingOptions[0];
if (selectedOption) {
this.selected = selectedOption.value;
}
},
}"
x-on:livewire:navigated.window="syncFromLocation()"
x-model="selected"
x-on:change="
const url = $event.target.value.split('|').slice(2).join('|');
window.Livewire?.navigate ? window.Livewire.navigate(url) : window.location.href = url;
">
<optgroup label="Server">
@foreach ($serverPageItems as $menuItem)
<option value="{{ ($menuItem['navigate'] ?? true) ? 'navigate' : 'location' }}|server|{{ route($menuItem['route'], $parameters) }}">
{{ $menuItem['label'] }}
</option>
@endforeach
</optgroup>
<optgroup label="Sentinel">
@foreach ($sentinelMenuItems as $menuItem)
<option value="navigate|sentinel|{{ route($menuItem['route'], $parameters) }}">
{{ $menuItem['label'] }}
</option>
@endforeach
</optgroup>
</select>
</div>
<div class="sub-menu-wrapper hidden md:flex">
@foreach ($sentinelMenuItems as $menuItem)
<a class="{{ $menuItem['active'] ? 'sub-menu-item menu-item-active' : 'sub-menu-item' }}" {{ wireNavigate() }}
href="{{ route($menuItem['route'], $parameters) }}">
<span class="menu-item-label">{{ $menuItem['label'] }}</span>
</a>
@endforeach
</div>
@endcan
</div>
@@ -1,46 +1,201 @@
<div class="sub-menu-wrapper">
<a class="sub-menu-item {{ $activeMenu === 'general' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.show', ['server_uuid' => $server->uuid]) }}"><span class="menu-item-label">General</span></a>
@if ($server->isFunctional())
<a class="sub-menu-item {{ $activeMenu === 'advanced' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.advanced', ['server_uuid' => $server->uuid]) }}"><span class="menu-item-label">Advanced</span>
</a>
@endif
<a class="sub-menu-item {{ $activeMenu === 'private-key' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.private-key', ['server_uuid' => $server->uuid]) }}"><span class="menu-item-label">Private Key</span>
</a>
@if ($server->hetzner_server_id)
<a class="sub-menu-item {{ $activeMenu === 'cloud-provider-token' ? 'menu-item-active' : '' }}"
{{ wireNavigate() }}
href="{{ route('server.cloud-provider-token', ['server_uuid' => $server->uuid]) }}"><span class="menu-item-label">Hetzner Token</span>
</a>
@endif
<a class="sub-menu-item {{ $activeMenu === 'ca-certificate' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.ca-certificate', ['server_uuid' => $server->uuid]) }}"><span class="menu-item-label">CA Certificate</span>
</a>
@if (!$server->isLocalhost())
<a class="sub-menu-item {{ $activeMenu === 'cloudflare-tunnel' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.cloudflare-tunnel', ['server_uuid' => $server->uuid]) }}"><span class="menu-item-label">Cloudflare Tunnel</span></a>
@endif
@if ($server->isFunctional())
<a class="sub-menu-item {{ $activeMenu === 'docker-cleanup' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.docker-cleanup', ['server_uuid' => $server->uuid]) }}"><span class="menu-item-label">Docker Cleanup</span>
</a>
<a class="sub-menu-item {{ $activeMenu === 'destinations' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.destinations', ['server_uuid' => $server->uuid]) }}"><span class="menu-item-label">Destinations</span>
</a>
<a class="sub-menu-item {{ $activeMenu === 'log-drains' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.log-drains', ['server_uuid' => $server->uuid]) }}"><span class="menu-item-label">Log Drains</span></a>
<a class="sub-menu-item {{ $activeMenu === 'metrics' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.metrics', ['server_uuid' => $server->uuid]) }}"><span class="menu-item-label">Metrics</span></a>
@endif
@if (!$server->isBuildServer() && !$server->settings->is_cloudflare_tunnel)
<a class="sub-menu-item {{ $activeMenu === 'swarm' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.swarm', ['server_uuid' => $server->uuid]) }}"><span class="menu-item-label">Swarm</span>
</a>
@endif
@if (!$server->isLocalhost())
<a class="sub-menu-item {{ $activeMenu === 'danger' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.delete', ['server_uuid' => $server->uuid]) }}"><span class="menu-item-label">Danger</span></a>
@endif
@php
$serverRouteParameters = ['server_uuid' => $server->uuid];
$serverPageItems = [
[
'label' => 'Configuration',
'route' => 'server.show',
'active' => request()->routeIs('server.show', 'server.advanced', 'server.private-key', 'server.cloud-provider-token', 'server.ca-certificate', 'server.cloudflare-tunnel', 'server.docker-cleanup', 'server.destinations', 'server.log-drains', 'server.metrics', 'server.swarm', 'server.delete'),
],
[
'label' => 'Proxy',
'route' => 'server.proxy',
'active' => request()->routeIs('server.proxy', 'server.proxy.*'),
'visible' => ! $server->isSwarmWorker() && ! $server->settings->is_build_server,
],
[
'label' => 'Sentinel',
'route' => 'server.sentinel',
'active' => request()->routeIs('server.sentinel', 'server.sentinel.*'),
'visible' => $server->isFunctional() && ! $server->isSwarm() && ! $server->settings->is_build_server && auth()->user()?->can('viewSentinel', $server),
],
[
'label' => 'Resources',
'route' => 'server.resources',
'active' => request()->routeIs('server.resources'),
],
[
'label' => 'Terminal',
'route' => 'server.command',
'active' => request()->routeIs('server.command'),
'navigate' => false,
'visible' => auth()->user()?->can('canAccessTerminal'),
],
[
'label' => 'Security',
'route' => 'server.security.patches',
'active' => request()->routeIs('server.security.patches'),
'visible' => auth()->user()?->can('update', $server),
],
];
$serverMenuItems = [
[
'label' => 'General',
'route' => 'server.show',
'active' => $activeMenu === 'general',
],
[
'label' => 'Advanced',
'route' => 'server.advanced',
'active' => $activeMenu === 'advanced',
'visible' => $server->isFunctional(),
],
[
'label' => 'Private Key',
'route' => 'server.private-key',
'active' => $activeMenu === 'private-key',
],
[
'label' => 'Hetzner Token',
'route' => 'server.cloud-provider-token',
'active' => $activeMenu === 'cloud-provider-token',
'visible' => (bool) $server->hetzner_server_id,
],
[
'label' => 'CA Certificate',
'route' => 'server.ca-certificate',
'active' => $activeMenu === 'ca-certificate',
],
[
'label' => 'Cloudflare Tunnel',
'route' => 'server.cloudflare-tunnel',
'active' => $activeMenu === 'cloudflare-tunnel',
'visible' => ! $server->isLocalhost(),
],
[
'label' => 'Docker Cleanup',
'route' => 'server.docker-cleanup',
'active' => $activeMenu === 'docker-cleanup',
'visible' => $server->isFunctional(),
],
[
'label' => 'Destinations',
'route' => 'server.destinations',
'active' => $activeMenu === 'destinations',
'visible' => $server->isFunctional(),
],
[
'label' => 'Log Drains',
'route' => 'server.log-drains',
'active' => $activeMenu === 'log-drains',
'visible' => $server->isFunctional(),
],
[
'label' => 'Metrics',
'route' => 'server.metrics',
'active' => $activeMenu === 'metrics',
'visible' => $server->isFunctional(),
],
[
'label' => 'Swarm',
'route' => 'server.swarm',
'active' => $activeMenu === 'swarm',
'visible' => ! $server->isBuildServer() && ! $server->settings->is_cloudflare_tunnel,
],
[
'label' => 'Danger',
'route' => 'server.delete',
'active' => $activeMenu === 'danger',
'visible' => ! $server->isLocalhost(),
],
];
$serverPageItems = array_values(array_filter(
$serverPageItems,
fn (array $item): bool => $item['visible'] ?? true,
));
$serverMenuItems = array_values(array_filter(
$serverMenuItems,
fn (array $item): bool => $item['visible'] ?? true,
));
$activeServerMenuItem = collect($serverMenuItems)->firstWhere('active', true);
$activeServerPageItem = collect($serverPageItems)->firstWhere('active', true);
$activeServerMobileItem = $activeServerMenuItem ?? $activeServerPageItem ?? $serverMenuItems[0];
$activeServerMobileGroup = $activeServerMenuItem ? 'configuration' : 'server';
$activeServerMobileNavigation = ($activeServerMobileItem['navigate'] ?? true) ? 'navigate' : 'location';
$activeServerMenuValue = $activeServerMobileNavigation.'|'.$activeServerMobileGroup.'|'.route($activeServerMobileItem['route'], $serverRouteParameters);
@endphp
<div class="w-full md:w-auto">
<div class="mb-4 w-full border-b-2 border-solid border-neutral-200 pb-6 md:hidden dark:border-coolgray-200">
<label id="server-mobile-section-label" for="server-mobile-section" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400">Section</label>
<select id="server-mobile-section" class="select w-full" aria-label="Server menu"
data-current-value="{{ $activeServerMenuValue }}"
x-data="{
init() {
this.syncFromLocation();
window.Livewire?.hook?.('morphed', ({ el }) => {
if (el.contains(this.$el)) {
queueMicrotask(() => this.syncFromLocation());
}
});
},
selected: $el.dataset.currentValue,
current: $el.dataset.currentValue,
syncFromLocation() {
const currentUrl = new URL(window.location.href);
const selectedOption = Array.from(this.$el.options).find((option) => {
if (!option.value.startsWith('navigate|') && !option.value.startsWith('location|')) {
return false;
}
const optionUrl = new URL(option.value.split('|').slice(2).join('|'), window.location.origin);
return optionUrl.pathname === currentUrl.pathname;
});
if (selectedOption) {
this.current = selectedOption.value;
this.selected = selectedOption.value;
}
},
}"
x-on:livewire:navigated.window="syncFromLocation()"
x-model="selected"
x-on:change="
const value = $event.target.value;
if (value.startsWith('navigate|')) {
const url = value.split('|').slice(2).join('|');
window.Livewire?.navigate ? window.Livewire.navigate(url) : window.location.href = url;
return;
}
if (value.startsWith('location|')) {
const url = value.split('|').slice(2).join('|');
window.location.href = url;
}
">
<optgroup label="Server">
@foreach ($serverPageItems as $menuItem)
<option value="{{ ($menuItem['navigate'] ?? true) ? 'navigate' : 'location' }}|server|{{ route($menuItem['route'], $serverRouteParameters) }}">
{{ $menuItem['label'] }}
</option>
@endforeach
</optgroup>
<optgroup label="Configuration">
@foreach ($serverMenuItems as $menuItem)
<option value="navigate|configuration|{{ route($menuItem['route'], $serverRouteParameters) }}">
{{ $menuItem['label'] }}
</option>
@endforeach
</optgroup>
</select>
</div>
<div class="sub-menu-wrapper hidden md:flex">
@foreach ($serverMenuItems as $menuItem)
<a class="sub-menu-item {{ $menuItem['active'] ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route($menuItem['route'], $serverRouteParameters) }}"><span class="menu-item-label">{{ $menuItem['label'] }}</span></a>
@endforeach
</div>
</div>