- Added Remix Icon CDN CSS (remixicon@4.5.0) to layout - Replaced Heroicon components with Remix Icon font classes (ri-*) - Using dynamic icon class binding in Alpine.js for modal grid - Icon names updated to Remix format (e.g., server-line, github-line) - Added more icons including gaming, OS, and brand icons - Simplified icon rendering - no more individual template/x-if blocks
1754 lines
85 KiB
PHP
1754 lines
85 KiB
PHP
@props([
|
||
'server',
|
||
'view',
|
||
'status' => null,
|
||
'statusPolling' => false,
|
||
'statusPollingInterval' => '30s',
|
||
])
|
||
|
||
{{-- Remix Icons for server icon customization --}}
|
||
@once
|
||
<link href="https://cdn.jsdelivr.net/npm/remixicon@4.5.0/fonts/remixicon.css" rel="stylesheet" />
|
||
@endonce
|
||
|
||
@php
|
||
$vmStatus = optional($status)->status;
|
||
$pollSuffix = $statusPollingInterval ? '.' . $statusPollingInterval : '';
|
||
$pollAttribute = $statusPolling ? 'wire:poll' . $pollSuffix . '="loadVmStatus"' : '';
|
||
@endphp
|
||
|
||
<div {!! $pollAttribute !!} class="flex flex-col" style="min-height: calc(100vh - 30rem);">
|
||
@if($server->status === 'active')
|
||
|
||
<div class="flex flex-col flex-1" x-data="{
|
||
isAdvancedView: localStorage.getItem('proxmox-view-mode') === 'advanced',
|
||
isAdvancedOnlyView: {{ in_array($view, ['boot-order', 'cloud-init', 'display', 'system', 'storage', 'subdomain', 'statistics', 'help', 'firewall']) ? 'true' : 'false' }},
|
||
toggleView() {
|
||
if (!this.isAdvancedOnlyView) {
|
||
this.isAdvancedView = !this.isAdvancedView;
|
||
localStorage.setItem('proxmox-view-mode', this.isAdvancedView ? 'advanced' : 'basic');
|
||
window.dispatchEvent(new CustomEvent('proxmox-view-mode-changed', {
|
||
detail: { isAdvancedView: this.isAdvancedView }
|
||
}));
|
||
}
|
||
}
|
||
}" x-init="
|
||
if (!localStorage.getItem('proxmox-view-mode')) {
|
||
localStorage.setItem('proxmox-view-mode', 'basic');
|
||
isAdvancedView = false;
|
||
}">
|
||
@php
|
||
$advancedOnlyViews = ['boot-order', 'cloud-init', 'display', 'system', 'subdomain', 'statistics', 'help', 'firewall'];
|
||
$isAdvancedOnlyView = in_array($view, $advancedOnlyViews);
|
||
@endphp
|
||
|
||
<div class="flex flex-col gap-4 pb-6 pt-6 lg:flex-row lg:items-center lg:justify-between">
|
||
<div class="flex items-center gap-3">
|
||
<!-- Server Icon -->
|
||
<button
|
||
wire:click="openIconModal"
|
||
class="flex-shrink-0 size-10 rounded-lg bg-background-secondary border border-neutral hover:border-primary/50 hover:bg-background-secondary/80 transition-all duration-200 flex items-center justify-center group"
|
||
title="Click to customize server icon"
|
||
>
|
||
@if($server->user_set_icon)
|
||
@php
|
||
$iconStyle = $server->user_set_icon_color ? 'color: ' . $server->user_set_icon_color : '';
|
||
@endphp
|
||
<i class="ri-{{ $server->user_set_icon }} text-xl transition-transform group-hover:scale-110" style="{{ $iconStyle }}"></i>
|
||
@else
|
||
<i class="ri-server-line text-xl text-base/60 transition-transform group-hover:scale-110"></i>
|
||
@endif
|
||
</button>
|
||
<span class="text-2xl font-bold">
|
||
{{ $server->hostname }}
|
||
</span>
|
||
</div>
|
||
|
||
|
||
<div class="hidden lg:flex flex-row gap-2 ml-auto justify-end items-center">
|
||
<x-button.primary wire:click="do('start')" class="disabled:cursor-not-allowed"
|
||
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
|
||
:disabled="$vmStatus !== 'stopped'">
|
||
<x-loading target="do('start')" />
|
||
<x-ri-play-fill class="size-4" />
|
||
<div wire:loading.remove wire:target="do('start')">
|
||
Start
|
||
</div>
|
||
</x-button.primary>
|
||
<div x-show="isAdvancedView" x-transition>
|
||
<x-button.secondary wire:click="do('shutdown')" class="text-nowrap disabled:cursor-not-allowed"
|
||
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
|
||
:disabled="$vmStatus !== 'running'">
|
||
<x-loading target="do('shutdown')" />
|
||
<x-ri-stop-fill class="size-4" />
|
||
<div wire:loading.remove wire:target="do('shutdown')">
|
||
Stop (ACPI)
|
||
</div>
|
||
</x-button.secondary>
|
||
</div>
|
||
<div x-show="isAdvancedView" x-transition>
|
||
<x-button.secondary wire:click="do('reboot')" class="text-nowrap disabled:cursor-not-allowed"
|
||
:disabled="$vmStatus !== 'running'">
|
||
<x-loading target="do('reboot')" />
|
||
<x-ri-loop-left-line class="size-4" />
|
||
<div wire:loading.remove wire:target="do('reboot')">
|
||
Reboot (ACPI)
|
||
</div>
|
||
</x-button.secondary>
|
||
</div>
|
||
<x-button.danger wire:click="do('stop')" class="text-nowrap disabled:cursor-not-allowed"
|
||
:disabled="$vmStatus !== 'running'">
|
||
<x-loading target="do('stop')" />
|
||
<x-ri-shut-down-line class="size-4" />
|
||
<div wire:loading.remove wire:target="do('stop')">
|
||
Power Off
|
||
</div>
|
||
</x-button.danger>
|
||
</div>
|
||
|
||
<div class="flex lg:hidden w-full flex-col gap-2">
|
||
<div class="flex flex-col gap-2 w-full">
|
||
<x-button.primary wire:click="do('start')" class="w-full disabled:cursor-not-allowed"
|
||
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
|
||
:disabled="$vmStatus !== 'stopped'">
|
||
<x-loading target="do('start')" />
|
||
<x-ri-play-fill class="size-4" />
|
||
<div wire:loading.remove wire:target="do('start')">
|
||
Start
|
||
</div>
|
||
</x-button.primary>
|
||
<div x-show="isAdvancedView" x-transition>
|
||
<x-button.secondary wire:click="do('shutdown')" class="w-full text-nowrap disabled:cursor-not-allowed"
|
||
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
|
||
:disabled="$vmStatus !== 'running'">
|
||
<x-loading target="do('shutdown')" />
|
||
<x-ri-stop-fill class="size-4" />
|
||
<div wire:loading.remove wire:target="do('shutdown')">
|
||
Stop (ACPI)
|
||
</div>
|
||
</x-button.secondary>
|
||
</div>
|
||
<div x-show="isAdvancedView" x-transition>
|
||
<x-button.secondary wire:click="do('reboot')" class="w-full text-nowrap disabled:cursor-not-allowed"
|
||
:disabled="$vmStatus !== 'running'">
|
||
<x-loading target="do('reboot')" />
|
||
<x-ri-loop-left-line class="size-4" />
|
||
<div wire:loading.remove wire:target="do('reboot')">
|
||
Reboot (ACPI)
|
||
</div>
|
||
</x-button.secondary>
|
||
</div>
|
||
<x-button.danger wire:click="do('stop')"
|
||
class="text-nowrap w-full disabled:cursor-not-allowed"
|
||
:disabled="$vmStatus !== 'running'">
|
||
<x-loading target="do('stop')" />
|
||
<x-ri-shut-down-line class="size-4" />
|
||
<div wire:loading.remove wire:target="do('stop')">
|
||
Power Off
|
||
</div>
|
||
</x-button.danger>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@if ($view !== 'statistics')
|
||
<!-- Preload Statistics tab content when entering the server page -->
|
||
<div
|
||
x-init="
|
||
if (window.fetch) {
|
||
setTimeout(function () {
|
||
try {
|
||
fetch('{{ route('proxmox.server.statistics', ['server' => $server->id]) }}', {
|
||
credentials: 'same-origin',
|
||
headers: { 'X-Proxmox-Preload': 'true' },
|
||
});
|
||
} catch (e) {}
|
||
}, 500);
|
||
}
|
||
"
|
||
class="hidden"
|
||
></div>
|
||
@endif
|
||
|
||
@php
|
||
$tabs = [
|
||
['label' => 'Overview', 'view' => 'overview', 'route' => route('proxmox.server.overview', ['server' => $server->id])],
|
||
['label' => 'Statistics', 'view' => 'statistics', 'route' => route('proxmox.server.statistics', ['server' => $server->id]), 'advanced' => true],
|
||
[
|
||
'label' => 'Hardware',
|
||
'dropdown' => true,
|
||
'items' => [
|
||
['label' => 'Boot Settings', 'view' => 'boot-order', 'route' => route('proxmox.server.boot-order', ['server' => $server->id]), 'advanced' => true],
|
||
['label' => 'Display', 'view' => 'display', 'route' => route('proxmox.server.display', ['server' => $server->id]), 'advanced' => true],
|
||
['label' => 'Storage', 'view' => 'storage', 'route' => route('proxmox.server.storage', ['server' => $server->id]), 'advanced' => true],
|
||
['label' => 'System', 'view' => 'system', 'route' => route('proxmox.server.system', ['server' => $server->id]), 'advanced' => true],
|
||
]
|
||
],
|
||
[
|
||
'label' => 'Networking',
|
||
'dropdown' => true,
|
||
'items' => [
|
||
['label' => 'Configuration', 'view' => 'network', 'route' => route('proxmox.server.network', ['server' => $server->id])],
|
||
['label' => 'Firewall', 'view' => 'firewall', 'route' => route('proxmox.server.firewall', ['server' => $server->id]), 'advanced' => true],
|
||
['label' => 'Subdomain', 'view' => 'subdomain', 'route' => route('proxmox.server.subdomain', ['server' => $server->id]), 'advanced' => true],
|
||
]
|
||
],
|
||
['label' => 'Cloud-init', 'view' => 'cloud-init', 'route' => route('proxmox.server.cloud-init', ['server' => $server->id]), 'advanced' => true],
|
||
];
|
||
if($server->service->product->settings()->where('key', 'backups')->first()?->value > 0) {
|
||
$tabs[] = ['label' => 'Backups', 'view' => 'backups', 'route' => route('proxmox.server.backups', ['server' => $server->id])];
|
||
}
|
||
if($server->node->vnc_enabled) {
|
||
$tabs[] = [
|
||
'label' => 'Console',
|
||
'dropdown' => true,
|
||
'items' => [
|
||
['label' => 'NoVNC', 'view' => 'vnc', 'route' => route('proxmox.server.vnc', ['server' => $server->id])],
|
||
['label' => 'Xterm.js', 'view' => 'xterm', 'route' => route('proxmox.server.xterm', ['server' => $server->id])],
|
||
]
|
||
];
|
||
}
|
||
$tabs[] = ['label' => 'Help', 'view' => 'help', 'route' => route('proxmox.server.help', ['server' => $server->id]), 'advanced' => true];
|
||
@endphp
|
||
|
||
<div class="mb-10 mt-3">
|
||
<style>
|
||
/* Modern Navigation Styles */
|
||
.proxmox-nav-container {
|
||
background: color-mix(in srgb, var(--color-background-secondary) 60%, transparent);
|
||
backdrop-filter: blur(12px);
|
||
border: 1px solid color-mix(in srgb, var(--color-neutral) 30%, transparent);
|
||
border-radius: 14px;
|
||
padding: 8px;
|
||
box-shadow:
|
||
0 1px 3px rgba(0, 0, 0, 0.05),
|
||
0 0 0 1px rgba(0, 0, 0, 0.02);
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
position: relative;
|
||
z-index: 10;
|
||
}
|
||
|
||
.dark .proxmox-nav-container {
|
||
background: color-mix(in srgb, var(--color-background-secondary) 80%, transparent);
|
||
border-color: color-mix(in srgb, var(--color-neutral) 40%, transparent);
|
||
box-shadow:
|
||
0 1px 3px rgba(0, 0, 0, 0.3),
|
||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.proxmox-nav-tabs {
|
||
display: flex;
|
||
gap: 4px;
|
||
flex-wrap: wrap;
|
||
flex: 1;
|
||
}
|
||
|
||
.proxmox-tab-link {
|
||
position: relative;
|
||
font-size: 0.8125rem;
|
||
font-weight: 600;
|
||
padding: 10px 16px;
|
||
border-radius: 10px;
|
||
letter-spacing: -0.01em;
|
||
white-space: nowrap;
|
||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||
border: 1px solid transparent;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.proxmox-tab-link::before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(135deg,
|
||
rgba(37, 99, 235, 0.08),
|
||
rgba(59, 130, 246, 0.05));
|
||
opacity: 0;
|
||
transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||
z-index: 0;
|
||
}
|
||
|
||
.proxmox-tab-link:hover::before {
|
||
opacity: 1;
|
||
}
|
||
|
||
.proxmox-tab-link span {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.proxmox-tab-link:hover {
|
||
transform: translateY(-1px);
|
||
border-color: rgba(37, 99, 235, 0.15);
|
||
}
|
||
|
||
.proxmox-tab-link.active {
|
||
background: linear-gradient(135deg,
|
||
rgba(37, 99, 235, 0.12),
|
||
rgba(59, 130, 246, 0.08));
|
||
border-color: rgba(37, 99, 235, 0.25);
|
||
box-shadow:
|
||
0 2px 8px rgba(37, 99, 235, 0.15),
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||
color: rgb(37, 99, 235);
|
||
}
|
||
|
||
.dark .proxmox-tab-link.active {
|
||
background: linear-gradient(135deg,
|
||
rgba(59, 130, 246, 0.18),
|
||
rgba(37, 99, 235, 0.12));
|
||
border-color: rgba(59, 130, 246, 0.35);
|
||
color: rgb(96, 165, 250);
|
||
}
|
||
|
||
.proxmox-tab-link:not(.active) {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: color-mix(in srgb, var(--color-base) 65%, transparent);
|
||
}
|
||
|
||
.dark .proxmox-tab-link:not(.active) {
|
||
color: color-mix(in srgb, var(--color-base) 65%, transparent);
|
||
}
|
||
|
||
/* Modern Toggle Switch */
|
||
.modern-toggle-container {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 6px 12px;
|
||
background: color-mix(in srgb, var(--color-background) 50%, transparent);
|
||
border: 1px solid color-mix(in srgb, var(--color-neutral) 40%, transparent);
|
||
border-radius: 12px;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.dark .modern-toggle-container {
|
||
background: color-mix(in srgb, var(--color-background) 60%, transparent);
|
||
border-color: color-mix(in srgb, var(--color-neutral) 50%, transparent);
|
||
}
|
||
|
||
.modern-toggle-label {
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
letter-spacing: 0.02em;
|
||
text-transform: uppercase;
|
||
transition: all 0.2s ease;
|
||
user-select: none;
|
||
}
|
||
|
||
.modern-toggle-label.inactive {
|
||
color: color-mix(in srgb, var(--color-base) 40%, transparent);
|
||
}
|
||
|
||
.modern-toggle-label.active {
|
||
color: rgb(37, 99, 235);
|
||
}
|
||
|
||
.dark .modern-toggle-label.active {
|
||
color: rgb(96, 165, 250);
|
||
}
|
||
|
||
.dark .modern-toggle-label.inactive {
|
||
color: color-mix(in srgb, var(--color-base) 40%, transparent);
|
||
}
|
||
|
||
.modern-toggle-switch {
|
||
position: relative;
|
||
width: 48px;
|
||
height: 26px;
|
||
background: linear-gradient(135deg,
|
||
rgba(156, 163, 175, 0.4),
|
||
rgba(209, 213, 219, 0.4));
|
||
border-radius: 13px;
|
||
cursor: pointer;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
border: 1px solid rgba(var(--neutral-rgb, 156, 163, 175), 0.2);
|
||
box-shadow:
|
||
inset 0 1px 3px rgba(0, 0, 0, 0.1),
|
||
0 1px 2px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.dark .modern-toggle-switch {
|
||
background: linear-gradient(135deg,
|
||
rgba(55, 65, 81, 0.6),
|
||
rgba(75, 85, 99, 0.6));
|
||
border-color: rgba(var(--neutral-rgb, 75, 85, 99), 0.4);
|
||
}
|
||
|
||
.modern-toggle-switch.active {
|
||
background: linear-gradient(135deg,
|
||
rgb(37, 99, 235),
|
||
rgb(59, 130, 246));
|
||
border-color: rgba(37, 99, 235, 0.3);
|
||
box-shadow:
|
||
0 0 0 3px rgba(37, 99, 235, 0.1),
|
||
inset 0 1px 3px rgba(0, 0, 0, 0.15),
|
||
0 2px 6px rgba(37, 99, 235, 0.25);
|
||
}
|
||
|
||
.dark .modern-toggle-switch.active {
|
||
background: linear-gradient(135deg,
|
||
rgb(59, 130, 246),
|
||
rgb(96, 165, 250));
|
||
box-shadow:
|
||
0 0 0 3px rgba(59, 130, 246, 0.15),
|
||
inset 0 1px 3px rgba(0, 0, 0, 0.2),
|
||
0 2px 8px rgba(59, 130, 246, 0.3);
|
||
}
|
||
|
||
.modern-toggle-handle {
|
||
position: absolute;
|
||
top: 2px;
|
||
left: 2px;
|
||
width: 20px;
|
||
height: 20px;
|
||
background: linear-gradient(135deg,
|
||
rgb(255, 255, 255),
|
||
rgb(249, 250, 251));
|
||
border-radius: 50%;
|
||
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
box-shadow:
|
||
0 2px 4px rgba(0, 0, 0, 0.15),
|
||
0 0 0 1px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.modern-toggle-switch.active .modern-toggle-handle {
|
||
transform: translateX(22px);
|
||
box-shadow:
|
||
0 3px 8px rgba(0, 0, 0, 0.2),
|
||
0 0 0 1px rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.modern-toggle-switch.disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* Animation for tab transitions */
|
||
@keyframes tabSlideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-8px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.proxmox-tab-wrapper {
|
||
animation: tabSlideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
/* Dropdown Styles */
|
||
.dropdown-wrapper {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
}
|
||
|
||
.dropdown-trigger {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.dropdown-icon {
|
||
transition: transform 0.2s ease;
|
||
width: 14px;
|
||
height: 14px;
|
||
}
|
||
|
||
.dropdown-wrapper.open .dropdown-icon {
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
.dropdown-menu {
|
||
position: absolute;
|
||
top: calc(100% + 8px);
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
min-width: 180px;
|
||
background: rgba(255, 255, 255, 0.705);
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(209, 213, 219, 0.3);
|
||
border-radius: 12px;
|
||
box-shadow:
|
||
0 4px 16px rgba(0, 0, 0, 0.15),
|
||
0 0 0 1px rgba(0, 0, 0, 0.05);
|
||
z-index: 100;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.dark .dropdown-menu {
|
||
background: rgba(30, 30, 35, 0.98);
|
||
border-color: rgba(75, 85, 99, 0.4);
|
||
box-shadow:
|
||
0 4px 20px rgba(0, 0, 0, 0.5),
|
||
0 0 0 1px rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.dropdown-item {
|
||
display: block;
|
||
padding: 10px 16px;
|
||
font-size: 0.8125rem;
|
||
font-weight: 600;
|
||
transition: all 0.2s ease;
|
||
border-left: 3px solid transparent;
|
||
color: var(--color-base);
|
||
}
|
||
|
||
.dropdown-item:hover {
|
||
background: linear-gradient(135deg,
|
||
rgba(37, 99, 235, 0.15),
|
||
rgba(59, 130, 246, 0.1));
|
||
border-left-color: rgba(37, 99, 235, 0.6);
|
||
color: var(--color-base);
|
||
}
|
||
|
||
.dropdown-item.active {
|
||
background: linear-gradient(135deg,
|
||
rgba(37, 99, 235, 0.15),
|
||
rgba(59, 130, 246, 0.1));
|
||
border-left-color: rgb(37, 99, 235);
|
||
color: rgb(37, 99, 235);
|
||
}
|
||
|
||
.dark .dropdown-item.active {
|
||
background: linear-gradient(135deg,
|
||
rgba(59, 130, 246, 0.2),
|
||
rgba(37, 99, 235, 0.15));
|
||
border-left-color: rgb(96, 165, 250);
|
||
color: rgb(96, 165, 250);
|
||
}
|
||
|
||
.dropdown-divider {
|
||
height: 1px;
|
||
background: color-mix(in srgb, var(--color-neutral) 30%, transparent);
|
||
margin: 4px 0;
|
||
}
|
||
</style>
|
||
|
||
<div class="hidden lg:block">
|
||
<div class="proxmox-nav-container">
|
||
<div class="flex items-center justify-between gap-4">
|
||
<div class="proxmox-nav-tabs">
|
||
@foreach($tabs as $tabIndex => $tab)
|
||
@php
|
||
$isAdvancedTab = $tab['advanced'] ?? false;
|
||
$isDropdown = $tab['dropdown'] ?? false;
|
||
|
||
// Check if current view is in dropdown items
|
||
$isDropdownActive = false;
|
||
$hasVisibleItems = false;
|
||
if ($isDropdown && isset($tab['items'])) {
|
||
foreach ($tab['items'] as $item) {
|
||
if ($view === $item['view']) {
|
||
$isDropdownActive = true;
|
||
}
|
||
$itemAdvanced = $item['advanced'] ?? false;
|
||
if (!$itemAdvanced) {
|
||
$hasVisibleItems = true;
|
||
}
|
||
}
|
||
}
|
||
@endphp
|
||
|
||
@if($isDropdown)
|
||
<div class="proxmox-tab-wrapper dropdown-wrapper"
|
||
x-data="{ open: false }"
|
||
@click.outside="open = false"
|
||
@if(!$hasVisibleItems)
|
||
x-show="isAdvancedView"
|
||
x-transition
|
||
@endif>
|
||
<button @click="open = !open"
|
||
class="proxmox-tab-link {{ $isDropdownActive ? 'active' : '' }}">
|
||
<span class="dropdown-trigger">
|
||
{{ $tab['label'] }}
|
||
<svg class="dropdown-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||
</svg>
|
||
</span>
|
||
</button>
|
||
|
||
<div x-show="open"
|
||
x-transition:enter="transition ease-out duration-200"
|
||
x-transition:enter-start="opacity-0 scale-95 -translate-y-2"
|
||
x-transition:enter-end="opacity-100 scale-100 translate-y-0"
|
||
x-transition:leave="transition ease-in duration-150"
|
||
x-transition:leave-start="opacity-100 scale-100"
|
||
x-transition:leave-end="opacity-0 scale-95"
|
||
class="dropdown-menu">
|
||
@foreach($tab['items'] as $itemIndex => $item)
|
||
@php
|
||
$itemAdvanced = $item['advanced'] ?? false;
|
||
@endphp
|
||
<div @if($itemAdvanced)
|
||
x-show="isAdvancedView"
|
||
@endif>
|
||
<a href="{{ $item['route'] }}" wire:navigate
|
||
class="dropdown-item {{ $view === $item['view'] ? 'active' : '' }}"
|
||
@click="open = false">
|
||
{{ $item['label'] }}
|
||
</a>
|
||
@if($itemIndex < count($tab['items']) - 1 && $itemAdvanced && !($tab['items'][$itemIndex + 1]['advanced'] ?? false))
|
||
<div x-show="isAdvancedView" class="dropdown-divider"></div>
|
||
@endif
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
@else
|
||
<div class="proxmox-tab-wrapper"
|
||
@if($isAdvancedTab)
|
||
x-show="isAdvancedView"
|
||
x-transition:enter="transition ease-out duration-200"
|
||
x-transition:enter-start="opacity-0 scale-95"
|
||
x-transition:enter-end="opacity-100 scale-100"
|
||
x-transition:leave="transition ease-in duration-150"
|
||
x-transition:leave-start="opacity-100 scale-100"
|
||
x-transition:leave-end="opacity-0 scale-95"
|
||
@endif>
|
||
<a href="{{ $tab['route'] }}" wire:navigate
|
||
class="proxmox-tab-link {{ $view === $tab['view'] ? 'active' : '' }}">
|
||
<span>{{ $tab['label'] }}</span>
|
||
</a>
|
||
</div>
|
||
@endif
|
||
@endforeach
|
||
</div>
|
||
|
||
<div class="modern-toggle-container"
|
||
:class="isAdvancedOnlyView ? 'opacity-50 cursor-not-allowed pointer-events-none' : ''">
|
||
<span class="modern-toggle-label"
|
||
:class="!isAdvancedView ? 'active' : 'inactive'">
|
||
Standard
|
||
</span>
|
||
<div class="modern-toggle-switch"
|
||
:class="{ 'active': isAdvancedView, 'disabled': isAdvancedOnlyView }"
|
||
@click="!isAdvancedOnlyView && toggleView()">
|
||
<div class="modern-toggle-handle"></div>
|
||
</div>
|
||
<span class="modern-toggle-label"
|
||
:class="isAdvancedView ? 'active' : 'inactive'">
|
||
Advanced
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="lg:hidden" x-data="{
|
||
open: false,
|
||
isAdvancedView: localStorage.getItem('proxmox-view-mode') === 'advanced',
|
||
isAdvancedOnlyView: {{ in_array($view, ['boot-order', 'cloud-init', 'display', 'system', 'storage', 'subdomain', 'statistics', 'help', 'firewall']) ? 'true' : 'false' }},
|
||
toggleView() {
|
||
if (!this.isAdvancedOnlyView) {
|
||
this.isAdvancedView = !this.isAdvancedView;
|
||
localStorage.setItem('proxmox-view-mode', this.isAdvancedView ? 'advanced' : 'basic');
|
||
window.dispatchEvent(new CustomEvent('proxmox-view-mode-changed', {
|
||
detail: { isAdvancedView: this.isAdvancedView }
|
||
}));
|
||
}
|
||
}
|
||
}" x-init="
|
||
if (!localStorage.getItem('proxmox-view-mode')) {
|
||
localStorage.setItem('proxmox-view-mode', 'basic');
|
||
isAdvancedView = false;
|
||
}
|
||
isAdvancedView = localStorage.getItem('proxmox-view-mode') === 'advanced';
|
||
window.addEventListener('proxmox-view-mode-changed', (e) => {
|
||
isAdvancedView = e.detail.isAdvancedView;
|
||
});
|
||
$watch('open', () => {
|
||
if (open) {
|
||
isAdvancedView = localStorage.getItem('proxmox-view-mode') === 'advanced';
|
||
}
|
||
});
|
||
">
|
||
<style>
|
||
.mobile-nav-button {
|
||
background: color-mix(in srgb, var(--color-background-secondary) 60%, transparent);
|
||
backdrop-filter: blur(12px);
|
||
border: 1px solid color-mix(in srgb, var(--color-neutral) 30%, transparent);
|
||
border-radius: 12px;
|
||
padding: 12px 16px;
|
||
box-shadow:
|
||
0 1px 3px rgba(0, 0, 0, 0.05),
|
||
0 0 0 1px rgba(0, 0, 0, 0.02);
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.dark .mobile-nav-button {
|
||
background: color-mix(in srgb, var(--color-background-secondary) 80%, transparent);
|
||
border-color: color-mix(in srgb, var(--color-neutral) 40%, transparent);
|
||
}
|
||
|
||
.mobile-nav-button:active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.mobile-dropdown {
|
||
background: rgba(var(--background-secondary-rgb, 255, 255, 255), 0.95);
|
||
backdrop-filter: blur(16px);
|
||
border: 1px solid rgba(var(--neutral-rgb, 229, 231, 235), 0.3);
|
||
border-radius: 14px;
|
||
box-shadow:
|
||
0 4px 16px rgba(0, 0, 0, 0.1),
|
||
0 0 0 1px rgba(0, 0, 0, 0.03);
|
||
}
|
||
|
||
.dark .mobile-dropdown {
|
||
background: rgba(var(--background-secondary-rgb, 31, 41, 55), 0.95);
|
||
border-color: rgba(var(--neutral-rgb, 75, 85, 99), 0.4);
|
||
box-shadow:
|
||
0 4px 16px rgba(0, 0, 0, 0.3),
|
||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.mobile-tab-item {
|
||
padding: 12px 16px;
|
||
transition: all 0.2s ease;
|
||
border-left: 3px solid transparent;
|
||
}
|
||
|
||
.mobile-tab-item:hover {
|
||
background: rgba(37, 99, 235, 0.08);
|
||
border-left-color: rgba(37, 99, 235, 0.3);
|
||
}
|
||
|
||
.mobile-tab-item.active {
|
||
background: rgba(37, 99, 235, 0.12);
|
||
border-left-color: rgb(37, 99, 235);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.dark .mobile-tab-item.active {
|
||
background: rgba(59, 130, 246, 0.15);
|
||
border-left-color: rgb(96, 165, 250);
|
||
}
|
||
|
||
.mobile-toggle-compact {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 12px;
|
||
background: rgba(var(--background-rgb, 255, 255, 255), 0.5);
|
||
border: 1px solid rgba(var(--neutral-rgb, 229, 231, 235), 0.4);
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.dark .mobile-toggle-compact {
|
||
background: rgba(var(--background-rgb, 17, 24, 39), 0.6);
|
||
border-color: rgba(var(--neutral-rgb, 75, 85, 99), 0.5);
|
||
}
|
||
|
||
.mobile-toggle-label-compact {
|
||
font-size: 0.7rem;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.03em;
|
||
}
|
||
|
||
.mobile-toggle-switch-compact {
|
||
width: 40px;
|
||
height: 22px;
|
||
background: linear-gradient(135deg,
|
||
rgba(156, 163, 175, 0.4),
|
||
rgba(209, 213, 219, 0.4));
|
||
border-radius: 11px;
|
||
position: relative;
|
||
transition: all 0.3s ease;
|
||
border: 1px solid rgba(var(--neutral-rgb, 156, 163, 175), 0.2);
|
||
}
|
||
|
||
.mobile-toggle-switch-compact.active {
|
||
background: linear-gradient(135deg,
|
||
rgb(37, 99, 235),
|
||
rgb(59, 130, 246));
|
||
}
|
||
|
||
.dark .mobile-toggle-switch-compact.active {
|
||
background: linear-gradient(135deg,
|
||
rgb(59, 130, 246),
|
||
rgb(96, 165, 250));
|
||
}
|
||
|
||
.mobile-toggle-handle-compact {
|
||
position: absolute;
|
||
top: 1px;
|
||
left: 1px;
|
||
width: 18px;
|
||
height: 18px;
|
||
background: white;
|
||
border-radius: 50%;
|
||
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.mobile-toggle-switch-compact.active .mobile-toggle-handle-compact {
|
||
transform: translateX(18px);
|
||
}
|
||
|
||
.mobile-section-header {
|
||
background: linear-gradient(135deg,
|
||
rgba(37, 99, 235, 0.05),
|
||
rgba(59, 130, 246, 0.03));
|
||
border-bottom: 1px solid color-mix(in srgb, var(--color-neutral) 20%, transparent);
|
||
}
|
||
|
||
.dark .mobile-section-header {
|
||
background: linear-gradient(135deg,
|
||
rgba(59, 130, 246, 0.08),
|
||
rgba(37, 99, 235, 0.05));
|
||
}
|
||
</style>
|
||
|
||
<div class="flex gap-3">
|
||
@php
|
||
// Find the current tab label, checking both regular tabs and dropdown items
|
||
$currentLabel = 'Menu';
|
||
foreach ($tabs as $tab) {
|
||
if (isset($tab['view']) && $tab['view'] === $view) {
|
||
$currentLabel = $tab['label'];
|
||
break;
|
||
}
|
||
if (isset($tab['dropdown']) && $tab['dropdown'] && isset($tab['items'])) {
|
||
foreach ($tab['items'] as $item) {
|
||
if ($item['view'] === $view) {
|
||
$currentLabel = $item['label'];
|
||
break 2;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
@endphp
|
||
<button @click="open = !open"
|
||
class="mobile-nav-button flex items-center gap-3 flex-1 justify-between font-semibold">
|
||
<span class="text-sm">{{ $currentLabel }}</span>
|
||
<x-ri-menu-line class="size-5" />
|
||
</button>
|
||
<div class="mobile-toggle-compact flex-shrink-0"
|
||
:class="isAdvancedOnlyView ? 'opacity-50 pointer-events-none' : ''">
|
||
<span class="mobile-toggle-label-compact"
|
||
:class="!isAdvancedView ? 'text-base' : 'text-base/40'">
|
||
Std
|
||
</span>
|
||
<div class="mobile-toggle-switch-compact"
|
||
:class="{ 'active': isAdvancedView }"
|
||
@click="!isAdvancedOnlyView && toggleView()">
|
||
<div class="mobile-toggle-handle-compact"></div>
|
||
</div>
|
||
<span class="mobile-toggle-label-compact"
|
||
:class="isAdvancedView ? 'text-base' : 'text-base/40'">
|
||
Adv
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-show="open"
|
||
x-transition:enter="transition ease-out duration-200"
|
||
x-transition:enter-start="opacity-0 scale-95"
|
||
x-transition:enter-end="opacity-100 scale-100"
|
||
x-transition:leave="transition ease-in duration-150"
|
||
x-transition:leave-start="opacity-100 scale-100"
|
||
x-transition:leave-end="opacity-0 scale-95"
|
||
@click.outside="open = false"
|
||
class="mobile-dropdown mt-3 overflow-hidden">
|
||
<div class="flex flex-col">
|
||
@foreach($tabs as $tab)
|
||
@php
|
||
$isAdvancedTab = $tab['advanced'] ?? false;
|
||
$isDropdown = $tab['dropdown'] ?? false;
|
||
@endphp
|
||
|
||
@if($isDropdown)
|
||
{{-- Render dropdown items as individual menu items for mobile --}}
|
||
<div class="mobile-section-header px-3 py-2 text-xs font-bold uppercase tracking-wider text-base/50">
|
||
{{ $tab['label'] }}
|
||
</div>
|
||
@foreach($tab['items'] as $item)
|
||
@php
|
||
$itemAdvanced = $item['advanced'] ?? false;
|
||
@endphp
|
||
<div @if($itemAdvanced)
|
||
x-show="isAdvancedView"
|
||
x-transition
|
||
@endif>
|
||
<a href="{{ $item['route'] }}" wire:navigate
|
||
class="mobile-tab-item flex items-center justify-between pl-8 {{ $view === $item['view'] ? 'active text-base' : 'text-base/70' }}"
|
||
@click="open = false">
|
||
<span>{{ $item['label'] }}</span>
|
||
@if($view === $item['view'])
|
||
<x-ri-check-line class="size-4 text-primary" />
|
||
@endif
|
||
</a>
|
||
</div>
|
||
@endforeach
|
||
@else
|
||
<div @if($isAdvancedTab)
|
||
x-show="isAdvancedView"
|
||
x-transition
|
||
@endif>
|
||
<a href="{{ $tab['route'] }}" wire:navigate
|
||
class="mobile-tab-item flex items-center justify-between {{ $view === $tab['view'] ? 'active text-base' : 'text-base/70' }}"
|
||
@click="open = false">
|
||
<span>{{ $tab['label'] }}</span>
|
||
@if($view === $tab['view'])
|
||
<x-ri-check-line class="size-4 text-primary" />
|
||
@endif
|
||
</a>
|
||
</div>
|
||
@endif
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="flex-1 flex flex-col">
|
||
<div class="flex-1 flex flex-col">
|
||
{{ $slot }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Server Icon Customization Modal -->
|
||
@if($view === 'overview')
|
||
<div x-data="{
|
||
open: @entangle('showIconModal'),
|
||
selectedIcon: @entangle('selectedIcon'),
|
||
selectedColor: @entangle('selectedIconColor'),
|
||
searchQuery: '',
|
||
icons: [
|
||
'server-line', 'database-2-line', 'computer-line', 'cpu-line', 'hard-drive-2-line',
|
||
'cloud-line', 'terminal-box-line', 'code-s-slash-line', 'braces-line',
|
||
'global-line', 'earth-line', 'wifi-line', 'signal-tower-line',
|
||
'shield-check-line', 'shield-keyhole-line', 'lock-line', 'lock-unlock-line', 'key-2-line',
|
||
'rocket-line', 'rocket-2-line', 'send-plane-line', 'flashlight-line', 'fire-line',
|
||
'sparkling-line', 'star-line', 'heart-line', 'home-line', 'home-2-line',
|
||
'building-line', 'building-2-line', 'store-line', 'box-3-line', 'archive-line',
|
||
'inbox-line', 'inbox-2-line', 'folder-line', 'folder-open-line',
|
||
'settings-3-line', 'settings-4-line', 'tools-line', 'hammer-line',
|
||
'flask-line', 'bug-line', 'puzzle-line', 'bar-chart-box-line', 'pie-chart-line',
|
||
'line-chart-line', 'money-dollar-circle-line', 'money-euro-circle-line',
|
||
'bank-card-line', 'wallet-line', 'user-line', 'user-3-line', 'group-line', 'team-line',
|
||
'mail-line', 'mail-open-line', 'chat-3-line', 'discuss-line',
|
||
'camera-line', 'vidicon-line', 'film-line', 'music-line',
|
||
'book-open-line', 'file-text-line', 'sun-line', 'moon-line',
|
||
'palette-line', 'brush-line', 'trophy-line', 'graduation-cap-line',
|
||
'lightbulb-line', 'fingerprint-line', 'eye-line', 'map-2-line', 'map-pin-line',
|
||
'time-line', 'calendar-line', 'gift-line', 'cake-2-line',
|
||
'gamepad-line', 'ghost-line', 'aliens-line', 'space-ship-line',
|
||
'android-line', 'apple-line', 'windows-line', 'ubuntu-line',
|
||
'github-line', 'gitlab-line', 'discord-line', 'slack-line'
|
||
],
|
||
colors: [
|
||
'', '#ef4444', '#f97316', '#f59e0b', '#eab308', '#84cc16', '#22c55e', '#10b981',
|
||
'#14b8a6', '#06b6d4', '#0ea5e9', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7',
|
||
'#d946ef', '#ec4899', '#f43f5e', '#78716c', '#64748b'
|
||
],
|
||
get filteredIcons() {
|
||
if (!this.searchQuery) return this.icons;
|
||
const query = this.searchQuery.toLowerCase();
|
||
return this.icons.filter(icon => icon.toLowerCase().includes(query));
|
||
}
|
||
}">
|
||
<!-- Modal Backdrop -->
|
||
<div x-cloak x-show="open"
|
||
x-transition:enter=\"transition ease-out duration-300\"
|
||
x-transition:enter-start=\"opacity-0\"
|
||
x-transition:enter-end=\"opacity-100\"
|
||
x-transition:leave=\"transition ease-in duration-200\"
|
||
x-transition:leave-start=\"opacity-100\"
|
||
x-transition:leave-end=\"opacity-0\"
|
||
@click=\"$wire.closeIconModal()\"
|
||
@keydown.escape.window=\"$wire.closeIconModal()\"
|
||
class=\"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm\"
|
||
style=\"display: none;\">
|
||
</div>
|
||
<!-- Modal Content -->
|
||
<div x-cloak x-show="open"
|
||
x-transition:enter="transition ease-out duration-300"
|
||
x-transition:enter-start="opacity-0 scale-95"
|
||
x-transition:enter-end="opacity-100 scale-100"
|
||
x-transition:leave="transition ease-in duration-200"
|
||
x-transition:leave-start="opacity-100 scale-100"
|
||
x-transition:leave-end="opacity-0 scale-95"
|
||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||
style="display: none;">
|
||
<div class="bg-background-secondary border border-neutral rounded-2xl shadow-2xl w-full max-w-2xl max-h-[80vh] flex flex-col" @click.away="$wire.closeIconModal()">
|
||
<!-- Header -->
|
||
<div class="p-6 border-b border-neutral">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h3 class="text-lg font-semibold">Customize Server Icon</h3>
|
||
<button @click="$wire.closeIconModal()" class="text-base/50 hover:text-base transition-colors">
|
||
<i class="ri-close-line text-xl"></i>
|
||
</button>
|
||
</div>
|
||
<!-- Search -->
|
||
<input type="text"
|
||
x-model="searchQuery"
|
||
placeholder="Search icons..."
|
||
class="w-full px-4 py-2 bg-background border border-neutral rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary/50">
|
||
</div>
|
||
|
||
<!-- Preview -->
|
||
<div class="px-6 py-4 border-b border-neutral bg-background/50">
|
||
<div class="flex items-center gap-4">
|
||
<div class="size-16 rounded-xl bg-background-secondary border-2 border-neutral flex items-center justify-center"
|
||
:style="selectedColor ? 'border-color: ' + selectedColor : ''">
|
||
<template x-if="selectedIcon">
|
||
<span class="size-8 flex items-center justify-center" :style="selectedColor ? 'color: ' + selectedColor : ''">
|
||
<i class="text-3xl" :class="'ri-' + selectedIcon"></i>
|
||
</span>
|
||
</template>
|
||
<template x-if="!selectedIcon">
|
||
<i class="ri-server-line text-3xl text-base/40"></i>
|
||
</template>
|
||
</div>
|
||
<div>
|
||
<p class="font-medium">Preview</p>
|
||
<p class="text-sm text-base/60" x-text="selectedIcon || 'No icon selected'"></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Color Picker -->
|
||
<div class="px-6 py-4 border-b border-neutral">
|
||
<p class="text-sm font-medium mb-3">Color</p>
|
||
<div class="flex flex-wrap gap-2">
|
||
<template x-for="color in colors" :key="color || 'default'">
|
||
<button @click="selectedColor = color"
|
||
class="size-8 rounded-full border-2 transition-all duration-150 hover:scale-110"
|
||
:class="selectedColor === color ? 'ring-2 ring-offset-2 ring-primary' : ''"
|
||
:style="color ? 'background-color: ' + color + '; border-color: ' + color : 'background: linear-gradient(135deg, #666 50%, #999 50%); border-color: #888'"
|
||
:title="color || 'Default'">
|
||
</button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Icons Grid -->
|
||
<div class="flex-1 overflow-y-auto p-6">
|
||
<div class="grid grid-cols-8 sm:grid-cols-10 gap-2">
|
||
<template x-for="icon in filteredIcons" :key="icon">
|
||
<button @click="selectedIcon = icon"
|
||
class="aspect-square rounded-lg border flex items-center justify-center transition-all duration-150 hover:bg-background hover:border-primary/50"
|
||
:class="selectedIcon === icon ? 'bg-primary/10 border-primary ring-2 ring-primary/30' : 'border-neutral'"
|
||
:data-icon-preview="icon">
|
||
<span class="size-5 flex items-center justify-center" :style="selectedColor ? 'color: ' + selectedColor : ''">
|
||
<i class="text-xl" :class="'ri-' + icon"></i>
|
||
</span>
|
||
</button>
|
||
</template>
|
||
</div>
|
||
<p x-show="filteredIcons.length === 0" class="text-center text-base/50 py-8">No icons match your search</p>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<div class="p-6 border-t border-neutral flex items-center justify-between gap-4">
|
||
<button @click="$wire.resetIcon()"
|
||
class="px-4 py-2 text-sm text-base/70 hover:text-danger transition-colors">
|
||
Reset to Default
|
||
</button>
|
||
<div class="flex gap-3">
|
||
<x-button.secondary @click="$wire.closeIconModal()">Cancel</x-button.secondary>
|
||
<x-button.primary @click="$wire.saveIcon(selectedIcon, selectedColor)">
|
||
Save Icon
|
||
</x-button.primary>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
|
||
@else
|
||
@php
|
||
$statuses = [
|
||
'suspended' => [
|
||
'color' => 'text-yellow-500',
|
||
'message' => 'This server is suspended. Please contact support for more information.'
|
||
],
|
||
'installing' => [
|
||
'color' => 'text-blue-500',
|
||
'message' => ''
|
||
],
|
||
];
|
||
@endphp
|
||
|
||
<div class="flex flex-col" wire:key='{{ $server->id }}-{{ $server->status }}' style="min-height: calc(100vh - 30rem);">
|
||
|
||
@if($server->status === 'installing')
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css?family=Fira+Mono:400');
|
||
|
||
@keyframes pulse {
|
||
0% {
|
||
transform: scale(1);
|
||
}
|
||
50% {
|
||
transform: scale(1.05);
|
||
}
|
||
100% {
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
.installing-container {
|
||
min-height: calc(100vh - 30rem);
|
||
height: calc(100vh - 30rem);
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.installing-body {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-family: "Inter", system-ui, sans-serif;
|
||
color: #1d2c3f;
|
||
width: 100%;
|
||
}
|
||
|
||
.loading-card {
|
||
width: min(460px, 92vw);
|
||
background: #ffffff;
|
||
border-radius: 20px;
|
||
padding: 34px 30px 38px;
|
||
box-shadow:
|
||
0 25px 50px rgba(86, 121, 168, 0.22),
|
||
0 0 0 1px rgba(109, 178, 255, 0.15);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.dark .loading-card {
|
||
background: var(--background-secondary);
|
||
color: var(--base);
|
||
}
|
||
|
||
.loading-card::after {
|
||
content: "";
|
||
position: absolute;
|
||
inset: -2px;
|
||
border-radius: 22px;
|
||
background: linear-gradient(
|
||
90deg,
|
||
rgba(44, 185, 149, 0.15),
|
||
rgba(0, 163, 255, 0.15),
|
||
rgba(0, 84, 255, 0.15),
|
||
rgba(85, 0, 255, 0.15)
|
||
);
|
||
background-size: 200% 200%;
|
||
animation: border-glow 8s ease infinite;
|
||
z-index: -1;
|
||
pointer-events: none;
|
||
opacity: 0.3;
|
||
}
|
||
|
||
@keyframes border-glow {
|
||
0%, 100% { background-position: 0% 50%; }
|
||
50% { background-position: 100% 50%; }
|
||
}
|
||
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
margin-bottom: 28px;
|
||
}
|
||
|
||
.logo-shell {
|
||
width: 58px;
|
||
height: 58px;
|
||
display: grid;
|
||
place-items: center;
|
||
}
|
||
|
||
.logo-shell img {
|
||
width: 42px;
|
||
height: auto;
|
||
display: block;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
.header span {
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.26em;
|
||
font-size: 0.75rem;
|
||
color: rgba(32, 56, 84, 0.6);
|
||
}
|
||
|
||
.dark .header span {
|
||
color: rgba(255, 255, 255, 0.6);
|
||
}
|
||
|
||
.header h1 {
|
||
margin: 4px 0 0;
|
||
font-size: clamp(1.4rem, 1vw, 1.65rem);
|
||
}
|
||
|
||
.line {
|
||
position: relative;
|
||
height: 6px;
|
||
border-radius: 999px;
|
||
background: rgba(37, 74, 179, 0.596);
|
||
overflow: hidden;
|
||
margin-bottom: 32px;
|
||
box-shadow:
|
||
inset 0 0 12px rgba(0, 0, 0, 0.05),
|
||
0 16px 32px rgba(109, 178, 255, 0.2);
|
||
}
|
||
|
||
.line::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(
|
||
89deg,
|
||
#2cb995,
|
||
#00a3ff,
|
||
#0054ff,
|
||
#001cff,
|
||
#5500ff
|
||
);
|
||
background-size: 800% 800%;
|
||
animation: line-gradient 13s ease infinite;
|
||
}
|
||
|
||
.line::after {
|
||
content: "";
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 0%;
|
||
width: 36px;
|
||
height: 12px;
|
||
border-radius: 999px;
|
||
border: 1px solid rgba(0, 20, 60, 0.25);
|
||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0));
|
||
box-shadow:
|
||
0 4px 20px rgba(0, 90, 255, 0.25),
|
||
inset 0 0 8px rgba(255, 255, 255, 0.4);
|
||
transform: translate(-50%, -50%) skewX(-8deg);
|
||
animation: line-marker 2.8s cubic-bezier(0.55, 0.03, 0.35, 0.95) infinite;
|
||
}
|
||
|
||
.status-cycle {
|
||
margin-top: 24px;
|
||
height: 20px;
|
||
font-size: 0.7rem;
|
||
letter-spacing: 0.18em;
|
||
text-transform: uppercase;
|
||
color: rgba(32, 56, 84, 0.65);
|
||
position: relative;
|
||
}
|
||
|
||
.dark .status-cycle {
|
||
color: rgba(255, 255, 255, 0.65);
|
||
}
|
||
|
||
.status-cycle ul {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
position: relative;
|
||
height: 20px;
|
||
}
|
||
|
||
.status-cycle li {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
opacity: 0;
|
||
animation: status-step 30s linear infinite;
|
||
}
|
||
|
||
.status-cycle li::before {
|
||
content: "";
|
||
width: 12px;
|
||
height: 2px;
|
||
background: linear-gradient(90deg, #18d4ff, transparent);
|
||
}
|
||
|
||
.status-cycle li:nth-child(2) {
|
||
animation-delay: 3s;
|
||
}
|
||
|
||
.status-cycle li:nth-child(3) {
|
||
animation-delay: 6s;
|
||
}
|
||
|
||
.status-cycle li:nth-child(4) {
|
||
animation-delay: 9s;
|
||
}
|
||
|
||
.status-cycle li:nth-child(5) {
|
||
animation-delay: 12s;
|
||
}
|
||
|
||
.status-cycle li:nth-child(6) {
|
||
animation-delay: 15s;
|
||
}
|
||
|
||
.status-cycle li:nth-child(7) {
|
||
animation-delay: 18s;
|
||
}
|
||
|
||
.status-cycle li:nth-child(8) {
|
||
animation-delay: 21s;
|
||
}
|
||
|
||
.status-cycle li:nth-child(9) {
|
||
animation-delay: 24s;
|
||
}
|
||
|
||
.status-cycle li:nth-child(10) {
|
||
animation-delay: 27s;
|
||
}
|
||
|
||
@keyframes line-gradient {
|
||
0% {
|
||
background-position: 0% 79%;
|
||
}
|
||
50% {
|
||
background-position: 100% 22%;
|
||
}
|
||
100% {
|
||
background-position: 0% 79%;
|
||
}
|
||
}
|
||
|
||
@keyframes line-marker {
|
||
0% {
|
||
left: 0%;
|
||
opacity: 0.15;
|
||
transform: translate(-50%, -50%) skewX(-8deg) scaleX(0.85);
|
||
}
|
||
10% {
|
||
opacity: 0.85;
|
||
}
|
||
50% {
|
||
opacity: 1;
|
||
transform: translate(-50%, -50%) skewX(-6deg) scaleX(1);
|
||
}
|
||
85% {
|
||
opacity: 0.4;
|
||
}
|
||
100% {
|
||
left: 100%;
|
||
opacity: 0.15;
|
||
transform: translate(-50%, -50%) skewX(-10deg) scaleX(0.75);
|
||
}
|
||
}
|
||
|
||
@keyframes status-step {
|
||
0%,
|
||
8% {
|
||
opacity: 1;
|
||
}
|
||
10%,
|
||
100% {
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
.watermark {
|
||
position: absolute;
|
||
bottom: 14px;
|
||
right: 24px;
|
||
font-size: 0.55rem;
|
||
letter-spacing: 0.22em;
|
||
color: rgba(32, 56, 84, 0.35);
|
||
cursor: default;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.dark .watermark {
|
||
color: rgba(255, 255, 255, 0.35);
|
||
}
|
||
|
||
.audio-modal {
|
||
display: none;
|
||
position: fixed;
|
||
z-index: 10000;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
backdrop-filter: blur(4px);
|
||
animation: fadeIn 0.3s ease;
|
||
}
|
||
|
||
.audio-modal.active {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.audio-modal-content {
|
||
background: transparent;
|
||
border-radius: 20px;
|
||
padding: 0;
|
||
width: min(400px, 90vw);
|
||
max-width: 90vw;
|
||
box-shadow:
|
||
0 25px 50px rgba(86, 121, 168, 0.3),
|
||
0 0 0 1px rgba(109, 178, 255, 0.2);
|
||
animation: slideUp 0.3s ease;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
.audio-modal-content::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background-image: url('https://billing.virtnet.bond/assets/images/glados.jpg');
|
||
background-size: cover;
|
||
background-position: center;
|
||
opacity: .95;
|
||
border-radius: 20px;
|
||
z-index: -1;
|
||
}
|
||
|
||
.audio-modal-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 24px 28px 10px;
|
||
position: relative;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
}
|
||
|
||
.dark .audio-modal-header {
|
||
background: rgba(0, 0, 0, 0.9);
|
||
}
|
||
|
||
.audio-modal-header h2 {
|
||
margin: 0;
|
||
position: relative;
|
||
}
|
||
|
||
.audio-modal-header h2 .data-corruption {
|
||
display: inline-block;
|
||
font-family: 'Fira Mono', monospace;
|
||
font-size: 1.1rem;
|
||
font-weight: 400;
|
||
color: #1d2c3f;
|
||
letter-spacing: 0.1em;
|
||
position: relative;
|
||
}
|
||
|
||
.dark .audio-modal-header h2 .data-corruption {
|
||
color: var(--base);
|
||
}
|
||
|
||
.audio-modal-header h2 .data-corruption::before,
|
||
.audio-modal-header h2 .data-corruption::after {
|
||
content: attr(title);
|
||
position: absolute;
|
||
left: 0;
|
||
color: #1d2c3f;
|
||
}
|
||
|
||
.dark .audio-modal-header h2 .data-corruption::before,
|
||
.dark .audio-modal-header h2 .data-corruption::after {
|
||
color: var(--base);
|
||
}
|
||
|
||
.audio-modal-close {
|
||
background: none;
|
||
border: none;
|
||
font-size: 2rem;
|
||
color: rgba(32, 56, 84, 0.4);
|
||
cursor: pointer;
|
||
line-height: 1;
|
||
padding: 0;
|
||
width: 32px;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.audio-modal-close:hover {
|
||
background: rgba(32, 56, 84, 0.1);
|
||
color: rgba(32, 56, 84, 0.7);
|
||
}
|
||
|
||
.dark .audio-modal-close {
|
||
color: rgba(255, 255, 255, 0.4);
|
||
}
|
||
|
||
.dark .audio-modal-close:hover {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
color: rgba(255, 255, 255, 0.7);
|
||
}
|
||
|
||
.audio-modal-body {
|
||
padding-left: 28px;
|
||
padding-right: 28px;
|
||
padding-bottom: 28px;
|
||
display: flex;
|
||
font-size: small;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: left;
|
||
gap: 16px;
|
||
position: relative;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
}
|
||
|
||
.dark .audio-modal-body {
|
||
background: rgba(0, 0, 0, 0.9);
|
||
}
|
||
|
||
.audio-modal-body audio {
|
||
width: 100%;
|
||
outline: none;
|
||
}
|
||
|
||
.audio-modal-body a {
|
||
color: inherit;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.audio-modal-body a:hover,
|
||
.audio-modal-body a:visited,
|
||
.audio-modal-body a:active {
|
||
color: inherit;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.easter-egg-message {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 16px;
|
||
padding: 20px;
|
||
background: linear-gradient(135deg, rgba(118, 124, 128, 0.08), rgba(136, 136, 136, 0.445));
|
||
border-radius: 12px;
|
||
border-left: 3px solid rgba(54, 62, 66, 0.4);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.easter-egg-text {
|
||
margin: 0;
|
||
font-size: 0.75rem;
|
||
line-height: 1.6;
|
||
color: rgba(32, 56, 84, 0.85);
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.dark .easter-egg-text {
|
||
color: rgba(255, 255, 255, 0.85);
|
||
}
|
||
|
||
.easter-egg-highlight {
|
||
font-weight: 700;
|
||
font-size: 1.1em;
|
||
background: linear-gradient(135deg, #00000070, #4e4d69);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
letter-spacing: 0.05em;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
@keyframes slideUp {
|
||
from {
|
||
transform: translateY(30px);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translateY(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<!-- Auto-refresh every 10s only in installing state -->
|
||
<script>
|
||
function scheduleInstallingRefresh() {
|
||
if (window.installingRefreshTimeout) {
|
||
clearTimeout(window.installingRefreshTimeout);
|
||
}
|
||
window.installingRefreshTimeout = setTimeout(function() {
|
||
window.location.reload();
|
||
}, 10000);
|
||
}
|
||
|
||
scheduleInstallingRefresh();
|
||
|
||
// Audio Modal Functions
|
||
function openAudioModal(e) {
|
||
e.preventDefault();
|
||
const modal = document.getElementById('audioModal');
|
||
const audio = document.getElementById('gladosAudio');
|
||
const watermark = document.querySelector('.watermark');
|
||
|
||
if (modal && audio) {
|
||
modal.classList.add('active');
|
||
audio.currentTime = 5;
|
||
audio.play().catch(function(error) {
|
||
console.log('Audio playback failed:', error);
|
||
});
|
||
|
||
if (window.installingRefreshTimeout) {
|
||
clearTimeout(window.installingRefreshTimeout);
|
||
window.installingRefreshTimeout = null;
|
||
}
|
||
|
||
audio.addEventListener('loadedmetadata', function() {
|
||
if (audio.currentTime < 5) {
|
||
audio.currentTime = 5;
|
||
}
|
||
}, { once: true });
|
||
|
||
if (watermark) {
|
||
watermark.textContent = 'Nice job breaking it, hero.';
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function closeAudioModal() {
|
||
const modal = document.getElementById('audioModal');
|
||
const audio = document.getElementById('gladosAudio');
|
||
|
||
if (modal && audio) {
|
||
audio.pause();
|
||
audio.currentTime = 0;
|
||
modal.classList.remove('active');
|
||
|
||
scheduleInstallingRefresh();
|
||
}
|
||
}
|
||
|
||
window.addEventListener('click', function(event) {
|
||
const modal = document.getElementById('audioModal');
|
||
if (event.target === modal) {
|
||
closeAudioModal();
|
||
}
|
||
});
|
||
|
||
document.addEventListener('keydown', function(event) {
|
||
if (event.key === 'Escape') {
|
||
closeAudioModal();
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<div class="installing-container">
|
||
<div class="installing-body">
|
||
<div class="loading-card">
|
||
<div class="header">
|
||
<div class="logo-shell" aria-hidden="true">
|
||
<img src="https://billing.virtnet.bond/assets/images/logofresh.png" alt="Virtnet.bond logo" width="46" height="34" />
|
||
</div>
|
||
<div>
|
||
<span>Virtnet.bond</span>
|
||
<h1>Creating Server</h1>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="line" aria-hidden="true"></div>
|
||
|
||
<div class="status-cycle" aria-live="polite">
|
||
<ul>
|
||
<li>Telling the memory to try harder</li>
|
||
<li>Begging the network to behave (again)</li>
|
||
<li>Pretending the storage is organized</li>
|
||
<li>Asking the CPU to stop being dramatic</li>
|
||
<li>Informing telemetry it missed everything</li>
|
||
<li>Encouraging the firewall to chill out</li>
|
||
<li>Reminding the logs nobody reads them</li>
|
||
<li>Lecturing the cluster to stop gossiping</li>
|
||
<li>Asking the API politely to behave today</li>
|
||
<li>Calming down the swap space (it is stressed)</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<a class="watermark" href="#" onclick="openAudioModal(event); return false;">
|
||
GLaDOS r4.32-03
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Audio Modal -->
|
||
<div id="audioModal" class="audio-modal">
|
||
<div class="audio-modal-content">
|
||
<div class="audio-modal-header">
|
||
<h2>
|
||
<div class="data-corruption">
|
||
<b>GLaDOS</b> - <small><em>"Still Alive"</em></small>
|
||
</div>
|
||
</h2>
|
||
<button class="audio-modal-close" onclick="closeAudioModal()">×</button>
|
||
</div>
|
||
<div class="audio-modal-body">
|
||
<div class="easter-egg-message">
|
||
<p class="easter-egg-text">
|
||
<span class="easter-egg-highlight">Congrats!</span> You found a hidden Easter egg! Enjoy the tune and the same love Virtnet.bond has for the Portal series and all video games! We might be serious about work, but we're still gamers at heart.
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<strong><a href="https://store.steampowered.com/app/400/Portal/">Portal</a></strong> -
|
||
<em>"Still Alive"</em> by Ellen McLain (voice of GLaDOS)
|
||
</div>
|
||
|
||
<audio id="gladosAudio" controls>
|
||
<source src="https://billing.virtnet.bond/assets/mp3/glados.mp3" type="audio/mpeg">
|
||
Your browser does not support the audio element.
|
||
</audio>
|
||
|
||
<div>
|
||
<small>
|
||
Virtnet.bond claims no rights to the "GLaDOS" name or the "Still Alive" audio content. This content is used under the doctrine of fair use (<b>17 U.S. Code § 107</b> – Limitations on exclusive rights: Fair use). It is provided for entertainment and cultural reference purposes only. All rights to the aforementioned content are retained by their respective copyright holders.
|
||
</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@else
|
||
<div class="text-center">
|
||
<h2 class="text-2xl font-bold mb-4">Oops!</h2>
|
||
<p class="{{ $statuses[$server->status]['color'] ?? 'text-black-100' }} font-semibold mb-4">
|
||
@php
|
||
$message = $statuses[$server->status]['message'] ?? "This server is currently inactive. Please view the Services tab in the Dashboard to see more information about the current state of this server. If more information is required, please contact support via the support portal under Tickets. This message can be displayed after a service is suspended, cancelled, revoked, or potentially in error. If you believe this message has been displayed in error, please contact us via a ticket and be sure to include the following information: VM{$server->vm_id}-ID{$server->id}-{$server->status}.";
|
||
@endphp
|
||
|
||
<p class="{{ $statuses[$server->status]['color'] ?? 'text-black-100' }}">
|
||
{!! $message !!}
|
||
</p>
|
||
</p>
|
||
</div>
|
||
@endif
|
||
|
||
</div>
|
||
@endif
|
||
</div>
|