2285 lines
120 KiB
PHP
2285 lines
120 KiB
PHP
@props([
|
||
'server',
|
||
'view',
|
||
'status' => null,
|
||
'statusPolling' => false,
|
||
'statusPollingInterval' => '30s',
|
||
])
|
||
|
||
@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" x-data="serverIconModal({
|
||
serverId: '{{ $server->id }}',
|
||
initialIcon: {{ $server->user_set_icon ? "'" . $server->user_set_icon . "'" : 'null' }},
|
||
initialColor: {{ $server->user_set_icon_color ? "'" . $server->user_set_icon_color . "'" : 'null' }},
|
||
initialPrefix: {{ $server->user_set_icon_prefix ? "'" . $server->user_set_icon_prefix . "'" : "'ri'" }}
|
||
})">
|
||
<button @click="openModal()" :title="icon ? 'Customize server icon' : 'Set server icon'" class="flex items-center gap-3 hover:opacity-80 transition-opacity">
|
||
<div x-show="icon" :style="color ? 'color: ' + color : ''" x-html="getIconSvg(icon, iconPrefix)" class="size-6"></div>
|
||
<x-ri-server-line x-show="!icon" class="size-6 text-secondary" />
|
||
<span class="text-2xl font-bold">
|
||
{{ $server->hostname }}
|
||
</span>
|
||
</button>
|
||
|
||
<!-- Modal -->
|
||
<div x-show="show" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||
<div class="bg-white dark:bg-background-secondary rounded-xl shadow-2xl p-6 w-full max-w-md relative">
|
||
<button @click="closeModal()" class="absolute top-3 right-3 text-base/50 hover:text-base transition-colors">
|
||
<x-ri-close-line class="size-5" />
|
||
</button>
|
||
<h3 class="text-lg font-semibold mb-4">Customize Server Icon</h3>
|
||
<div class="flex items-center gap-4 mb-4">
|
||
<div class="size-12 rounded-xl border flex items-center justify-center" :style="color ? 'border-color: ' + color : ''">
|
||
<div x-show="icon" :style="color ? 'color: ' + color : ''" x-html="getIconSvg(icon, iconPrefix)"></div>
|
||
<x-ri-server-line x-show="!icon" class="size-8 text-base/40" />
|
||
</div>
|
||
<input x-model="search" x-on:input="filterIcons()" type="text" placeholder="Search icons..." class="border rounded px-2 py-1 text-sm w-40 dark:bg-background dark:border-base/20" />
|
||
</div>
|
||
<div class="grid grid-cols-6 gap-2 mb-4 max-h-32 overflow-y-auto p-1">
|
||
@php
|
||
$iconList = [
|
||
// Remix Icons
|
||
['icon' => 'server-line', 'prefix' => 'ri'],
|
||
['icon' => 'database-2-line', 'prefix' => 'ri'],
|
||
['icon' => 'computer-line', 'prefix' => 'ri'],
|
||
['icon' => 'cpu-line', 'prefix' => 'ri'],
|
||
['icon' => 'hard-drive-2-line', 'prefix' => 'ri'],
|
||
['icon' => 'cloud-line', 'prefix' => 'ri'],
|
||
['icon' => 'terminal-box-line', 'prefix' => 'ri'],
|
||
['icon' => 'code-s-slash-line', 'prefix' => 'ri'],
|
||
['icon' => 'braces-line', 'prefix' => 'ri'],
|
||
['icon' => 'global-line', 'prefix' => 'ri'],
|
||
['icon' => 'earth-line', 'prefix' => 'ri'],
|
||
['icon' => 'wifi-line', 'prefix' => 'ri'],
|
||
['icon' => 'signal-tower-line', 'prefix' => 'ri'],
|
||
['icon' => 'shield-check-line', 'prefix' => 'ri'],
|
||
['icon' => 'shield-keyhole-line', 'prefix' => 'ri'],
|
||
['icon' => 'lock-line', 'prefix' => 'ri'],
|
||
['icon' => 'lock-unlock-line', 'prefix' => 'ri'],
|
||
['icon' => 'key-2-line', 'prefix' => 'ri'],
|
||
['icon' => 'rocket-line', 'prefix' => 'ri'],
|
||
['icon' => 'rocket-2-line', 'prefix' => 'ri'],
|
||
['icon' => 'send-plane-line', 'prefix' => 'ri'],
|
||
['icon' => 'flashlight-line', 'prefix' => 'ri'],
|
||
['icon' => 'fire-line', 'prefix' => 'ri'],
|
||
['icon' => 'sparkling-line', 'prefix' => 'ri'],
|
||
['icon' => 'star-line', 'prefix' => 'ri'],
|
||
['icon' => 'heart-line', 'prefix' => 'ri'],
|
||
['icon' => 'home-line', 'prefix' => 'ri'],
|
||
['icon' => 'home-2-line', 'prefix' => 'ri'],
|
||
['icon' => 'building-line', 'prefix' => 'ri'],
|
||
['icon' => 'building-2-line', 'prefix' => 'ri'],
|
||
['icon' => 'store-line', 'prefix' => 'ri'],
|
||
['icon' => 'box-3-line', 'prefix' => 'ri'],
|
||
['icon' => 'archive-line', 'prefix' => 'ri'],
|
||
['icon' => 'inbox-line', 'prefix' => 'ri'],
|
||
['icon' => 'inbox-2-line', 'prefix' => 'ri'],
|
||
['icon' => 'folder-line', 'prefix' => 'ri'],
|
||
['icon' => 'folder-open-line', 'prefix' => 'ri'],
|
||
['icon' => 'settings-3-line', 'prefix' => 'ri'],
|
||
['icon' => 'settings-4-line', 'prefix' => 'ri'],
|
||
['icon' => 'tools-line', 'prefix' => 'ri'],
|
||
['icon' => 'hammer-line', 'prefix' => 'ri'],
|
||
['icon' => 'flask-line', 'prefix' => 'ri'],
|
||
['icon' => 'bug-line', 'prefix' => 'ri'],
|
||
['icon' => 'puzzle-line', 'prefix' => 'ri'],
|
||
['icon' => 'bar-chart-box-line', 'prefix' => 'ri'],
|
||
['icon' => 'pie-chart-line', 'prefix' => 'ri'],
|
||
['icon' => 'line-chart-line', 'prefix' => 'ri'],
|
||
['icon' => 'money-dollar-circle-line', 'prefix' => 'ri'],
|
||
['icon' => 'money-euro-circle-line', 'prefix' => 'ri'],
|
||
['icon' => 'bank-card-line', 'prefix' => 'ri'],
|
||
['icon' => 'wallet-line', 'prefix' => 'ri'],
|
||
['icon' => 'user-line', 'prefix' => 'ri'],
|
||
['icon' => 'user-3-line', 'prefix' => 'ri'],
|
||
['icon' => 'group-line', 'prefix' => 'ri'],
|
||
['icon' => 'team-line', 'prefix' => 'ri'],
|
||
['icon' => 'mail-line', 'prefix' => 'ri'],
|
||
['icon' => 'mail-open-line', 'prefix' => 'ri'],
|
||
['icon' => 'chat-3-line', 'prefix' => 'ri'],
|
||
['icon' => 'discuss-line', 'prefix' => 'ri'],
|
||
['icon' => 'camera-line', 'prefix' => 'ri'],
|
||
['icon' => 'vidicon-line', 'prefix' => 'ri'],
|
||
['icon' => 'film-line', 'prefix' => 'ri'],
|
||
['icon' => 'music-line', 'prefix' => 'ri'],
|
||
['icon' => 'book-open-line', 'prefix' => 'ri'],
|
||
['icon' => 'file-text-line', 'prefix' => 'ri'],
|
||
['icon' => 'sun-line', 'prefix' => 'ri'],
|
||
['icon' => 'moon-line', 'prefix' => 'ri'],
|
||
['icon' => 'palette-line', 'prefix' => 'ri'],
|
||
['icon' => 'brush-line', 'prefix' => 'ri'],
|
||
['icon' => 'trophy-line', 'prefix' => 'ri'],
|
||
['icon' => 'graduation-cap-line', 'prefix' => 'ri'],
|
||
['icon' => 'lightbulb-line', 'prefix' => 'ri'],
|
||
['icon' => 'fingerprint-line', 'prefix' => 'ri'],
|
||
['icon' => 'eye-line', 'prefix' => 'ri'],
|
||
['icon' => 'map-2-line', 'prefix' => 'ri'],
|
||
['icon' => 'map-pin-line', 'prefix' => 'ri'],
|
||
['icon' => 'time-line', 'prefix' => 'ri'],
|
||
['icon' => 'calendar-line', 'prefix' => 'ri'],
|
||
['icon' => 'gift-line', 'prefix' => 'ri'],
|
||
['icon' => 'cake-2-line', 'prefix' => 'ri'],
|
||
['icon' => 'gamepad-line', 'prefix' => 'ri'],
|
||
['icon' => 'ghost-line', 'prefix' => 'ri'],
|
||
['icon' => 'aliens-line', 'prefix' => 'ri'],
|
||
['icon' => 'space-ship-line', 'prefix' => 'ri'],
|
||
['icon' => 'android-line', 'prefix' => 'ri'],
|
||
['icon' => 'apple-line', 'prefix' => 'ri'],
|
||
['icon' => 'windows-line', 'prefix' => 'ri'],
|
||
['icon' => 'ubuntu-line', 'prefix' => 'ri'],
|
||
['icon' => 'github-line', 'prefix' => 'ri'],
|
||
['icon' => 'gitlab-line', 'prefix' => 'ri'],
|
||
['icon' => 'discord-line', 'prefix' => 'ri'],
|
||
['icon' => 'slack-line', 'prefix' => 'ri'],
|
||
['icon' => 'desktop', 'prefix' => 'vaadin'],
|
||
['icon' => 'laptop', 'prefix' => 'vaadin'],
|
||
['icon' => 'cluster', 'prefix' => 'vaadin'],
|
||
['icon' => 'cloud', 'prefix' => 'vaadin'],
|
||
['icon' => 'cloud-o', 'prefix' => 'vaadin'],
|
||
['icon' => 'cloud-upload', 'prefix' => 'vaadin'],
|
||
['icon' => 'cloud-download', 'prefix' => 'vaadin'],
|
||
['icon' => 'storage', 'prefix' => 'vaadin'],
|
||
['icon' => 'harddrive', 'prefix' => 'vaadin'],
|
||
['icon' => 'compile', 'prefix' => 'vaadin'],
|
||
['icon' => 'code', 'prefix' => 'vaadin'],
|
||
['icon' => 'terminal', 'prefix' => 'vaadin'],
|
||
['icon' => 'globe', 'prefix' => 'vaadin'],
|
||
['icon' => 'globe-wire', 'prefix' => 'vaadin'],
|
||
['icon' => 'connect', 'prefix' => 'vaadin'],
|
||
['icon' => 'sitemap', 'prefix' => 'vaadin'],
|
||
['icon' => 'automation', 'prefix' => 'vaadin'],
|
||
['icon' => 'cogs', 'prefix' => 'vaadin'],
|
||
['icon' => 'cog', 'prefix' => 'vaadin'],
|
||
['icon' => 'wrench', 'prefix' => 'vaadin'],
|
||
['icon' => 'tools', 'prefix' => 'vaadin'],
|
||
['icon' => 'hammer', 'prefix' => 'vaadin'],
|
||
['icon' => 'shield', 'prefix' => 'vaadin'],
|
||
['icon' => 'lock', 'prefix' => 'vaadin'],
|
||
['icon' => 'unlock', 'prefix' => 'vaadin'],
|
||
['icon' => 'key', 'prefix' => 'vaadin'],
|
||
['icon' => 'key-o', 'prefix' => 'vaadin'],
|
||
['icon' => 'rocket', 'prefix' => 'vaadin'],
|
||
['icon' => 'flash', 'prefix' => 'vaadin'],
|
||
['icon' => 'bolt', 'prefix' => 'vaadin'],
|
||
['icon' => 'fire', 'prefix' => 'vaadin'],
|
||
['icon' => 'star', 'prefix' => 'vaadin'],
|
||
['icon' => 'star-o', 'prefix' => 'vaadin'],
|
||
['icon' => 'heart', 'prefix' => 'vaadin'],
|
||
['icon' => 'heart-o', 'prefix' => 'vaadin'],
|
||
['icon' => 'home', 'prefix' => 'vaadin'],
|
||
['icon' => 'home-o', 'prefix' => 'vaadin'],
|
||
['icon' => 'building', 'prefix' => 'vaadin'],
|
||
['icon' => 'building-o', 'prefix' => 'vaadin'],
|
||
['icon' => 'office', 'prefix' => 'vaadin'],
|
||
['icon' => 'server', 'prefix' => 'vaadin'],
|
||
['icon' => 'database', 'prefix' => 'vaadin'],
|
||
['icon' => 'cloud', 'prefix' => 'vaadin'],
|
||
['icon' => 'cloud-o', 'prefix' => 'vaadin'],
|
||
['icon' => 'storage', 'prefix' => 'vaadin'],
|
||
['icon' => 'harddrive', 'prefix' => 'vaadin'],
|
||
['icon' => 'package', 'prefix' => 'vaadin'],
|
||
['icon' => 'archive', 'prefix' => 'vaadin'],
|
||
['icon' => 'inbox', 'prefix' => 'vaadin'],
|
||
['icon' => 'folder', 'prefix' => 'vaadin'],
|
||
['icon' => 'folder-o', 'prefix' => 'vaadin'],
|
||
['icon' => 'folder-open', 'prefix' => 'vaadin'],
|
||
['icon' => 'folder-open-o', 'prefix' => 'vaadin'],
|
||
['icon' => 'bug', 'prefix' => 'vaadin'],
|
||
['icon' => 'chart', 'prefix' => 'vaadin'],
|
||
['icon' => 'bar-chart', 'prefix' => 'vaadin'],
|
||
['icon' => 'line-chart', 'prefix' => 'vaadin'],
|
||
['icon' => 'pie-chart', 'prefix' => 'vaadin'],
|
||
['icon' => 'dashboard', 'prefix' => 'vaadin'],
|
||
['icon' => 'progressbar', 'prefix' => 'vaadin'],
|
||
['icon' => 'money', 'prefix' => 'vaadin'],
|
||
['icon' => 'dollar', 'prefix' => 'vaadin'],
|
||
['icon' => 'euro', 'prefix' => 'vaadin'],
|
||
['icon' => 'credit-card', 'prefix' => 'vaadin'],
|
||
['icon' => 'wallet', 'prefix' => 'vaadin'],
|
||
['icon' => 'user', 'prefix' => 'vaadin'],
|
||
['icon' => 'users', 'prefix' => 'vaadin'],
|
||
['icon' => 'group', 'prefix' => 'vaadin'],
|
||
['icon' => 'user-card', 'prefix' => 'vaadin'],
|
||
['icon' => 'male', 'prefix' => 'vaadin'],
|
||
['icon' => 'female', 'prefix' => 'vaadin'],
|
||
['icon' => 'envelope', 'prefix' => 'vaadin'],
|
||
['icon' => 'envelope-o', 'prefix' => 'vaadin'],
|
||
['icon' => 'envelope-open', 'prefix' => 'vaadin'],
|
||
['icon' => 'mailbox', 'prefix' => 'vaadin'],
|
||
['icon' => 'comment', 'prefix' => 'vaadin'],
|
||
['icon' => 'comments', 'prefix' => 'vaadin'],
|
||
['icon' => 'comments-o', 'prefix' => 'vaadin'],
|
||
['icon' => 'camera', 'prefix' => 'vaadin'],
|
||
['icon' => 'film', 'prefix' => 'vaadin'],
|
||
['icon' => 'music', 'prefix' => 'vaadin'],
|
||
['icon' => 'book', 'prefix' => 'vaadin'],
|
||
['icon' => 'file-text', 'prefix' => 'vaadin'],
|
||
['icon' => 'file-text-o', 'prefix' => 'vaadin'],
|
||
['icon' => 'sun-o', 'prefix' => 'vaadin'],
|
||
['icon' => 'moon-o', 'prefix' => 'vaadin'],
|
||
['icon' => 'palete', 'prefix' => 'vaadin'],
|
||
['icon' => 'paintbrush', 'prefix' => 'vaadin'],
|
||
['icon' => 'trophy', 'prefix' => 'vaadin'],
|
||
['icon' => 'academy-cap', 'prefix' => 'vaadin'],
|
||
['icon' => 'lightbulb', 'prefix' => 'vaadin'],
|
||
['icon' => 'eye', 'prefix' => 'vaadin'],
|
||
['icon' => 'eye-slash', 'prefix' => 'vaadin'],
|
||
['icon' => 'map-marker', 'prefix' => 'vaadin'],
|
||
['icon' => 'location-arrow', 'prefix' => 'vaadin'],
|
||
['icon' => 'clock', 'prefix' => 'vaadin'],
|
||
['icon' => 'calendar', 'prefix' => 'vaadin'],
|
||
['icon' => 'calendar-o', 'prefix' => 'vaadin'],
|
||
['icon' => 'gift', 'prefix' => 'vaadin'],
|
||
['icon' => 'gamepad', 'prefix' => 'vaadin'],
|
||
['icon' => 'controller', 'prefix' => 'vaadin'],
|
||
|
||
// File Icons (codeat3/blade-file-icons)
|
||
['icon' => 'docker', 'prefix' => 'fileicon'],
|
||
['icon' => 'kubernetes', 'prefix' => 'fileicon'],
|
||
['icon' => 'nginx', 'prefix' => 'fileicon'],
|
||
['icon' => 'alpine-linux', 'prefix' => 'fileicon'],
|
||
['icon' => 'arch-linux', 'prefix' => 'fileicon'],
|
||
['icon' => 'xubuntu', 'prefix' => 'fileicon'],
|
||
['icon' => 'virtualbox', 'prefix' => 'fileicon'],
|
||
['icon' => 'virtualbox-alt', 'prefix' => 'fileicon'],
|
||
['icon' => 'vmware', 'prefix' => 'fileicon'],
|
||
['icon' => 'terminal', 'prefix' => 'fileicon'],
|
||
['icon' => 'powershell', 'prefix' => 'fileicon'],
|
||
['icon' => 'serverless', 'prefix' => 'fileicon'],
|
||
['icon' => 'devcontainer', 'prefix' => 'fileicon'],
|
||
['icon' => 'config', 'prefix' => 'fileicon'],
|
||
['icon' => 'editorconfig', 'prefix' => 'fileicon'],
|
||
['icon' => 'php', 'prefix' => 'fileicon'],
|
||
['icon' => 'go', 'prefix' => 'fileicon'],
|
||
['icon' => 'kotlin', 'prefix' => 'fileicon'],
|
||
['icon' => 'c', 'prefix' => 'fileicon'],
|
||
['icon' => 'npm', 'prefix' => 'fileicon'],
|
||
['icon' => 'yarn', 'prefix' => 'fileicon'],
|
||
['icon' => 'vscode', 'prefix' => 'fileicon'],
|
||
['icon' => 'nano', 'prefix' => 'fileicon'],
|
||
['icon' => 'gitlab', 'prefix' => 'fileicon'],
|
||
['icon' => 'sqlite', 'prefix' => 'fileicon'],
|
||
['icon' => 'json-1', 'prefix' => 'fileicon'],
|
||
['icon' => 'json-2', 'prefix' => 'fileicon'],
|
||
['icon' => 'yaml', 'prefix' => 'fileicon'],
|
||
['icon' => 'typescript', 'prefix' => 'fileicon'],
|
||
['icon' => 'vue', 'prefix' => 'fileicon'],
|
||
['icon' => 'svelte', 'prefix' => 'fileicon'],
|
||
['icon' => 'laravel', 'prefix' => 'fileicon'],
|
||
['icon' => 'flask', 'prefix' => 'fileicon'],
|
||
['icon' => 'terraform', 'prefix' => 'fileicon'],
|
||
['icon' => 'ansible', 'prefix' => 'fileicon'],
|
||
['icon' => 'ansible-alt', 'prefix' => 'fileicon'],
|
||
['icon' => 'puppet', 'prefix' => 'fileicon'],
|
||
['icon' => 'jenkins', 'prefix' => 'fileicon'],
|
||
['icon' => 'jsx', 'prefix' => 'fileicon'],
|
||
['icon' => 'tsx', 'prefix' => 'fileicon'],
|
||
['icon' => 'perl6', 'prefix' => 'fileicon'],
|
||
['icon' => 'rubygems', 'prefix' => 'fileicon'],
|
||
['icon' => 'toml', 'prefix' => 'fileicon'],
|
||
['icon' => 'tailwind', 'prefix' => 'fileicon'],
|
||
['icon' => 'webpack', 'prefix' => 'fileicon'],
|
||
['icon' => 'vite', 'prefix' => 'fileicon'],
|
||
['icon' => 'nextjs', 'prefix' => 'fileicon'],
|
||
['icon' => 'nestjs', 'prefix' => 'fileicon'],
|
||
['icon' => 'nodemon', 'prefix' => 'fileicon'],
|
||
['icon' => 'vagrant', 'prefix' => 'fileicon'],
|
||
['icon' => 'saltstack', 'prefix' => 'fileicon'],
|
||
['icon' => 'chef', 'prefix' => 'fileicon'],
|
||
['icon' => 'circleci', 'prefix' => 'fileicon'],
|
||
['icon' => 'r', 'prefix' => 'fileicon'],
|
||
['icon' => 'apple', 'prefix' => 'fileicon'],
|
||
['icon' => 'arduino', 'prefix' => 'fileicon'],
|
||
['icon' => 'babel', 'prefix' => 'fileicon'],
|
||
['icon' => 'composer', 'prefix' => 'fileicon'],
|
||
['icon' => 'snyk', 'prefix' => 'fileicon'],
|
||
['icon' => 'storybook', 'prefix' => 'fileicon'],
|
||
|
||
// Dev Icons (codeat3/blade-devicons) - Technology logos
|
||
['icon' => 'docker', 'prefix' => 'devicon'],
|
||
['icon' => 'kubernetes', 'prefix' => 'devicon'],
|
||
['icon' => 'linux', 'prefix' => 'devicon'],
|
||
['icon' => 'ubuntu', 'prefix' => 'devicon'],
|
||
['icon' => 'debian', 'prefix' => 'devicon'],
|
||
['icon' => 'centos', 'prefix' => 'devicon'],
|
||
['icon' => 'redhat', 'prefix' => 'devicon'],
|
||
['icon' => 'fedora', 'prefix' => 'devicon'],
|
||
['icon' => 'opensuse', 'prefix' => 'devicon'],
|
||
['icon' => 'nixos', 'prefix' => 'devicon'],
|
||
['icon' => 'gentoo', 'prefix' => 'devicon'],
|
||
['icon' => 'android', 'prefix' => 'devicon'],
|
||
['icon' => 'apache', 'prefix' => 'devicon'],
|
||
['icon' => 'mysql', 'prefix' => 'devicon'],
|
||
['icon' => 'postgresql', 'prefix' => 'devicon'],
|
||
['icon' => 'mongodb', 'prefix' => 'devicon'],
|
||
['icon' => 'redis', 'prefix' => 'devicon'],
|
||
['icon' => 'sqlite', 'prefix' => 'devicon'],
|
||
['icon' => 'neo4j', 'prefix' => 'devicon'],
|
||
['icon' => 'couchdb', 'prefix' => 'devicon'],
|
||
['icon' => 'microsoftsqlserver', 'prefix' => 'devicon'],
|
||
['icon' => 'python', 'prefix' => 'devicon'],
|
||
['icon' => 'php', 'prefix' => 'devicon'],
|
||
['icon' => 'java', 'prefix' => 'devicon'],
|
||
['icon' => 'javascript', 'prefix' => 'devicon'],
|
||
['icon' => 'typescript', 'prefix' => 'devicon'],
|
||
['icon' => 'nodejs', 'prefix' => 'devicon'],
|
||
['icon' => 'go', 'prefix' => 'devicon'],
|
||
['icon' => 'rust', 'prefix' => 'devicon'],
|
||
['icon' => 'c', 'prefix' => 'devicon'],
|
||
['icon' => 'cplusplus', 'prefix' => 'devicon'],
|
||
['icon' => 'csharp', 'prefix' => 'devicon'],
|
||
['icon' => 'ruby', 'prefix' => 'devicon'],
|
||
['icon' => 'perl', 'prefix' => 'devicon'],
|
||
['icon' => 'swift', 'prefix' => 'devicon'],
|
||
['icon' => 'kotlin', 'prefix' => 'devicon'],
|
||
['icon' => 'scala', 'prefix' => 'devicon'],
|
||
['icon' => 'r', 'prefix' => 'devicon'],
|
||
['icon' => 'lua', 'prefix' => 'devicon'],
|
||
['icon' => 'dart', 'prefix' => 'devicon'],
|
||
['icon' => 'elixir', 'prefix' => 'devicon'],
|
||
['icon' => 'erlang', 'prefix' => 'devicon'],
|
||
['icon' => 'haskell', 'prefix' => 'devicon'],
|
||
['icon' => 'julia', 'prefix' => 'devicon'],
|
||
['icon' => 'bash', 'prefix' => 'devicon'],
|
||
['icon' => 'html5', 'prefix' => 'devicon'],
|
||
['icon' => 'css3', 'prefix' => 'devicon'],
|
||
['icon' => 'laravel', 'prefix' => 'devicon'],
|
||
['icon' => 'django', 'prefix' => 'devicon'],
|
||
['icon' => 'rails', 'prefix' => 'devicon'],
|
||
['icon' => 'spring', 'prefix' => 'devicon'],
|
||
['icon' => 'flask', 'prefix' => 'devicon'],
|
||
['icon' => 'fastapi', 'prefix' => 'devicon'],
|
||
['icon' => 'nestjs', 'prefix' => 'devicon'],
|
||
['icon' => 'vuejs', 'prefix' => 'devicon'],
|
||
['icon' => 'angularjs', 'prefix' => 'devicon'],
|
||
['icon' => 'svelte', 'prefix' => 'devicon'],
|
||
['icon' => 'nuxtjs', 'prefix' => 'devicon'],
|
||
['icon' => 'gatsby', 'prefix' => 'devicon'],
|
||
['icon' => 'bootstrap', 'prefix' => 'devicon'],
|
||
['icon' => 'tailwindcss', 'prefix' => 'devicon'],
|
||
['icon' => 'materialui', 'prefix' => 'devicon'],
|
||
['icon' => 'git', 'prefix' => 'devicon'],
|
||
['icon' => 'gitlab', 'prefix' => 'devicon'],
|
||
['icon' => 'azure', 'prefix' => 'devicon'],
|
||
['icon' => 'googlecloud', 'prefix' => 'devicon'],
|
||
['icon' => 'digitalocean', 'prefix' => 'devicon'],
|
||
['icon' => 'heroku', 'prefix' => 'devicon'],
|
||
['icon' => 'firebase', 'prefix' => 'devicon'],
|
||
['icon' => 'terraform', 'prefix' => 'devicon'],
|
||
['icon' => 'ansible', 'prefix' => 'devicon'],
|
||
['icon' => 'jenkins', 'prefix' => 'devicon'],
|
||
['icon' => 'circleci', 'prefix' => 'devicon'],
|
||
['icon' => 'travis', 'prefix' => 'devicon'],
|
||
['icon' => 'argocd', 'prefix' => 'devicon'],
|
||
['icon' => 'vagrant', 'prefix' => 'devicon'],
|
||
['icon' => 'podman', 'prefix' => 'devicon'],
|
||
['icon' => 'webpack', 'prefix' => 'devicon'],
|
||
['icon' => 'babel', 'prefix' => 'devicon'],
|
||
['icon' => 'gradle', 'prefix' => 'devicon'],
|
||
['icon' => 'vscode', 'prefix' => 'devicon'],
|
||
['icon' => 'visualstudio', 'prefix' => 'devicon'],
|
||
['icon' => 'intellij', 'prefix' => 'devicon'],
|
||
['icon' => 'pycharm', 'prefix' => 'devicon'],
|
||
['icon' => 'phpstorm', 'prefix' => 'devicon'],
|
||
['icon' => 'webstorm', 'prefix' => 'devicon'],
|
||
['icon' => 'vim', 'prefix' => 'devicon'],
|
||
['icon' => 'xcode', 'prefix' => 'devicon'],
|
||
['icon' => 'androidstudio', 'prefix' => 'devicon'],
|
||
['icon' => 'jupyter', 'prefix' => 'devicon'],
|
||
['icon' => 'chrome', 'prefix' => 'devicon'],
|
||
['icon' => 'firefox', 'prefix' => 'devicon'],
|
||
['icon' => 'safari', 'prefix' => 'devicon'],
|
||
['icon' => 'opera', 'prefix' => 'devicon'],
|
||
['icon' => 'wordpress', 'prefix' => 'devicon'],
|
||
['icon' => 'drupal', 'prefix' => 'devicon'],
|
||
['icon' => 'woocommerce', 'prefix' => 'devicon'],
|
||
['icon' => 'graphql', 'prefix' => 'devicon'],
|
||
['icon' => 'arduino', 'prefix' => 'devicon'],
|
||
['icon' => 'flutter', 'prefix' => 'devicon'],
|
||
['icon' => 'figma', 'prefix' => 'devicon'],
|
||
['icon' => 'photoshop', 'prefix' => 'devicon'],
|
||
['icon' => 'illustrator', 'prefix' => 'devicon'],
|
||
['icon' => 'premierepro', 'prefix' => 'devicon'],
|
||
['icon' => 'aftereffects', 'prefix' => 'devicon'],
|
||
['icon' => 'slack', 'prefix' => 'devicon'],
|
||
['icon' => 'trello', 'prefix' => 'devicon'],
|
||
['icon' => 'jira', 'prefix' => 'devicon'],
|
||
['icon' => 'putty', 'prefix' => 'devicon'],
|
||
['icon' => 'solidity', 'prefix' => 'devicon'],
|
||
];
|
||
@endphp
|
||
@foreach($iconList as $iconData)
|
||
<button @click="icon = '{{ $iconData['icon'] }}'; iconPrefix = '{{ $iconData['prefix'] }}'" :class="icon === '{{ $iconData['icon'] }}' && iconPrefix === '{{ $iconData['prefix'] }}' ? 'ring-2 ring-primary' : ''" class="icon-btn rounded p-2 border flex items-center justify-center hover:bg-secondary/10 dark:border-base/20" data-icon="{{ $iconData['icon'] }}" data-prefix="{{ $iconData['prefix'] }}">
|
||
@php
|
||
if ($iconData['prefix'] === 'vaadin') {
|
||
$componentName = 'vaadin-' . str_replace('_', '-', $iconData['icon']);
|
||
} else {
|
||
$componentName = $iconData['prefix'] . '-' . $iconData['icon'];
|
||
}
|
||
@endphp
|
||
<x-dynamic-component :component="$componentName" class="size-5" />
|
||
</button>
|
||
@endforeach
|
||
</div>
|
||
<div class="flex flex-wrap gap-2 mb-4">
|
||
<template x-for="c in colors" :key="c">
|
||
<button @click="color = c" :style="'background:' + (c || '#e5e7eb')" :class="color === c ? 'ring-2 ring-primary' : ''" class="size-6 rounded-full border"></button>
|
||
</template>
|
||
</div>
|
||
<div class="flex gap-2 justify-end">
|
||
<button @click="resetIcon()" class="text-xs text-base/60 hover:text-danger">Reset</button>
|
||
<button @click="saveIcon()" class="px-3 py-1 rounded bg-primary text-white text-xs hover:bg-primary/80">Save</button>
|
||
</div>
|
||
<template x-if="message">
|
||
<div class="mt-2 text-xs text-success" x-text="message"></div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
function serverIconModal({serverId, initialIcon, initialColor, initialPrefix}) {
|
||
return {
|
||
show: false,
|
||
icon: initialIcon || '',
|
||
iconPrefix: initialPrefix || 'ri',
|
||
color: initialColor || '',
|
||
search: '',
|
||
message: '',
|
||
colors: [
|
||
'', '#ef4444', '#f97316', '#f59e0b', '#eab308', '#84cc16', '#22c55e', '#10b981',
|
||
'#14b8a6', '#06b6d4', '#0ea5e9', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7',
|
||
'#d946ef', '#ec4899', '#f43f5e', '#78716c', '#64748b'
|
||
],
|
||
openModal() {
|
||
this.show = true;
|
||
this.message = '';
|
||
this.$nextTick(() => this.filterIcons());
|
||
},
|
||
closeModal() { this.show = false; },
|
||
resetIcon() {
|
||
this.icon = '';
|
||
this.iconPrefix = 'ri';
|
||
this.color = '';
|
||
},
|
||
filterIcons() {
|
||
const query = this.search.toLowerCase();
|
||
document.querySelectorAll('.icon-btn').forEach(btn => {
|
||
const iconName = btn.dataset.icon;
|
||
const iconPrefix = btn.dataset.prefix;
|
||
const searchText = iconPrefix + '-' + iconName;
|
||
btn.style.display = !query || searchText.includes(query) ? 'flex' : 'none';
|
||
});
|
||
},
|
||
getIconSvg(iconName, prefix) {
|
||
const btn = document.querySelector(`.icon-btn[data-icon="${iconName}"][data-prefix="${prefix}"]`);
|
||
if (btn) {
|
||
const svg = btn.querySelector('svg');
|
||
if (svg) {
|
||
const cloned = svg.cloneNode(true);
|
||
cloned.classList.remove('size-5');
|
||
cloned.classList.add('size-8');
|
||
return cloned.outerHTML;
|
||
}
|
||
}
|
||
return '';
|
||
},
|
||
saveIcon() {
|
||
fetch(`/proxmox/server/${serverId}/icon`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content
|
||
},
|
||
body: JSON.stringify({ icon: this.icon, color: this.color, prefix: this.iconPrefix })
|
||
})
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
this.message = 'Icon saved!';
|
||
setTimeout(() => { this.closeModal(); location.reload(); }, 800);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
</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>
|
||
|
||
@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>
|
||
|
||
<!-- Server Icon Customization Modal -->
|
||
<div id="serverIconModal" style="display: none;" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||
<div class="bg-white dark:bg-background-secondary rounded-xl shadow-2xl p-6 w-full max-w-md relative">
|
||
<button onclick="closeServerIconModal()" class="absolute top-3 right-3 text-base/50 hover:text-base transition-colors">
|
||
<x-ri-close-line class="size-5" />
|
||
</button>
|
||
<h3 class="text-lg font-semibold mb-4">Customize Server Icon</h3>
|
||
|
||
<div class="flex items-center gap-4 mb-4">
|
||
<div id="iconPreview" class="size-12 rounded-xl border flex items-center justify-center text-3xl">
|
||
<i id="iconPreviewIcon" class="ri-server-line text-base/40"></i>
|
||
</div>
|
||
<input id="iconSearch" type="text" placeholder="Search icons..." class="border rounded px-2 py-1 text-sm w-40 dark:bg-background dark:border-base/20" oninput="filterIcons()" />
|
||
</div>
|
||
|
||
<div id="iconGrid" class="grid grid-cols-6 gap-2 mb-4 max-h-32 overflow-y-auto">
|
||
@php
|
||
$availableIcons = [
|
||
'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'
|
||
];
|
||
@endphp
|
||
@foreach($availableIcons as $iconName)
|
||
<button onclick="selectIcon('{{ $iconName }}')" data-icon="{{ $iconName }}" class="icon-button rounded p-1 border flex items-center justify-center hover:bg-secondary/10 text-xl transition-all dark:border-base/20">
|
||
<i class="ri-{{ $iconName }}"></i>
|
||
</button>
|
||
@endforeach
|
||
</div>
|
||
|
||
<div class="flex flex-wrap gap-2 mb-4">
|
||
@php
|
||
$availableColors = [
|
||
'' => 'Default',
|
||
'#ef4444' => 'Red',
|
||
'#f97316' => 'Orange',
|
||
'#f59e0b' => 'Amber',
|
||
'#eab308' => 'Yellow',
|
||
'#84cc16' => 'Lime',
|
||
'#22c55e' => 'Green',
|
||
'#10b981' => 'Emerald',
|
||
'#14b8a6' => 'Teal',
|
||
'#06b6d4' => 'Cyan',
|
||
'#0ea5e9' => 'Sky',
|
||
'#3b82f6' => 'Blue',
|
||
'#6366f1' => 'Indigo',
|
||
'#8b5cf6' => 'Violet',
|
||
'#a855f7' => 'Purple',
|
||
'#d946ef' => 'Fuchsia',
|
||
'#ec4899' => 'Pink',
|
||
'#f43f5e' => 'Rose',
|
||
'#78716c' => 'Stone',
|
||
'#64748b' => 'Slate'
|
||
];
|
||
@endphp
|
||
@foreach($availableColors as $colorValue => $colorName)
|
||
<button onclick="selectColor('{{ $colorValue }}')" data-color="{{ $colorValue }}" title="{{ $colorName }}" class="color-button size-6 rounded-full border transition-all" style="background: {{ $colorValue ?: '#e5e7eb' }}"></button>
|
||
@endforeach
|
||
</div>
|
||
|
||
<div class="flex gap-2 justify-end">
|
||
<button onclick="resetServerIcon()" class="text-xs text-base/60 hover:text-danger transition-colors">Reset</button>
|
||
<button onclick="saveServerIcon()" class="px-3 py-1 rounded bg-primary text-white text-xs hover:bg-primary/80 transition-colors">Save</button>
|
||
</div>
|
||
|
||
<div id="iconMessage" class="mt-2 text-xs text-success hidden"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let serverIconModalData = {
|
||
serverId: null,
|
||
selectedIcon: '',
|
||
selectedColor: ''
|
||
};
|
||
|
||
function openServerIconModal(serverId, currentIcon, currentColor) {
|
||
serverIconModalData.serverId = serverId;
|
||
serverIconModalData.selectedIcon = currentIcon || '';
|
||
serverIconModalData.selectedColor = currentColor || '';
|
||
|
||
updateIconPreview();
|
||
updateIconSelection();
|
||
updateColorSelection();
|
||
|
||
document.getElementById('serverIconModal').style.display = 'flex';
|
||
document.getElementById('iconSearch').value = '';
|
||
filterIcons();
|
||
}
|
||
|
||
function closeServerIconModal() {
|
||
document.getElementById('serverIconModal').style.display = 'none';
|
||
document.getElementById('iconMessage').classList.add('hidden');
|
||
}
|
||
|
||
function selectIcon(iconName) {
|
||
serverIconModalData.selectedIcon = iconName;
|
||
updateIconPreview();
|
||
updateIconSelection();
|
||
}
|
||
|
||
function selectColor(color) {
|
||
serverIconModalData.selectedColor = color;
|
||
updateIconPreview();
|
||
updateColorSelection();
|
||
}
|
||
|
||
function resetServerIcon() {
|
||
serverIconModalData.selectedIcon = '';
|
||
serverIconModalData.selectedColor = '';
|
||
updateIconPreview();
|
||
updateIconSelection();
|
||
updateColorSelection();
|
||
}
|
||
|
||
function updateIconPreview() {
|
||
const preview = document.getElementById('iconPreview');
|
||
const previewIcon = document.getElementById('iconPreviewIcon');
|
||
|
||
if (serverIconModalData.selectedIcon) {
|
||
previewIcon.className = 'ri-' + serverIconModalData.selectedIcon;
|
||
previewIcon.style.color = serverIconModalData.selectedColor || '';
|
||
preview.style.borderColor = serverIconModalData.selectedColor || '';
|
||
} else {
|
||
previewIcon.className = 'ri-server-line text-base/40';
|
||
previewIcon.style.color = '';
|
||
preview.style.borderColor = '';
|
||
}
|
||
}
|
||
|
||
function updateIconSelection() {
|
||
document.querySelectorAll('.icon-button').forEach(btn => {
|
||
if (btn.dataset.icon === serverIconModalData.selectedIcon) {
|
||
btn.classList.add('ring-2', 'ring-primary');
|
||
} else {
|
||
btn.classList.remove('ring-2', 'ring-primary');
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateColorSelection() {
|
||
document.querySelectorAll('.color-button').forEach(btn => {
|
||
if (btn.dataset.color === serverIconModalData.selectedColor) {
|
||
btn.classList.add('ring-2', 'ring-primary');
|
||
} else {
|
||
btn.classList.remove('ring-2', 'ring-primary');
|
||
}
|
||
});
|
||
}
|
||
|
||
function filterIcons() {
|
||
const search = document.getElementById('iconSearch').value.toLowerCase();
|
||
document.querySelectorAll('.icon-button').forEach(btn => {
|
||
const iconName = btn.dataset.icon;
|
||
if (iconName.includes(search)) {
|
||
btn.style.display = 'flex';
|
||
} else {
|
||
btn.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
function saveServerIcon() {
|
||
fetch(`/proxmox/server/${serverIconModalData.serverId}/icon`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content
|
||
},
|
||
body: JSON.stringify({
|
||
icon: serverIconModalData.selectedIcon,
|
||
color: serverIconModalData.selectedColor
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
const messageEl = document.getElementById('iconMessage');
|
||
messageEl.textContent = 'Icon saved!';
|
||
messageEl.classList.remove('hidden');
|
||
setTimeout(() => {
|
||
closeServerIconModal();
|
||
location.reload();
|
||
}, 800);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error saving icon:', error);
|
||
});
|
||
}
|
||
</script>
|