feat: implement server icon customization modal with AJAX support

This commit is contained in:
2025-12-06 10:43:39 -05:00
parent e0ee624589
commit 744187a2fc
2 changed files with 116 additions and 122 deletions

View File

@@ -38,15 +38,122 @@
@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">
@if($server->user_set_icon)
<i class="ri-{{ $server->user_set_icon }} text-2xl" style="{{ $server->user_set_icon_color ? 'color: ' . $server->user_set_icon_color : '' }}"></i>
@else
<i class="ri-server-line text-2xl text-secondary"></i>
@endif
<span class="text-2xl font-bold">
{{ $server->hostname }}
</span>
<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' }}
})">
<button @click="openModal()" :title="icon ? 'Customize server icon' : 'Set server icon'" class="flex items-center gap-3 hover:opacity-80 transition-opacity">
<span x-show="icon" :class="'ri-' + icon" class="text-2xl" :style="color ? 'color: ' + color : ''"></span>
<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 ? 'color: ' + color + '; border-color: ' + color : ''">
<span x-show="icon" :class="'ri-' + icon" class="text-3xl"></span>
<x-ri-server-line x-show="!icon" class="size-8 text-base/40" />
</div>
<input x-model="search" type="text" placeholder="Search icons..." class="border rounded px-2 py-1 text-sm w-40" />
</div>
<div class="grid grid-cols-6 gap-2 mb-4 max-h-32 overflow-y-auto">
<template x-for="i in filteredIcons" :key="i">
<button @click="icon = i" :class="icon === i ? 'ring-2 ring-primary' : ''" class="rounded p-1 border flex items-center justify-center hover:bg-secondary/10">
<span :class="'ri-' + i" class="text-xl"></span>
</button>
</template>
</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}) {
return {
show: false,
icon: initialIcon || '',
color: initialColor || '',
search: '',
message: '',
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.search) return this.icons;
const q = this.search.toLowerCase();
return this.icons.filter(i => i.toLowerCase().includes(q));
},
openModal() { this.show = true; this.message = ''; },
closeModal() { this.show = false; },
resetIcon() {
this.icon = '';
this.color = '';
},
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 })
})
.then(r => r.json())
.then(data => {
if (data.success) {
this.message = 'Icon saved!';
setTimeout(() => { this.closeModal(); location.reload(); }, 800);
}
});
}
}
}
</script>
</div>

View File

@@ -75,119 +75,6 @@
<div class="flex items-center gap-3">
<div class="flex items-center gap-2 min-w-0">
<h2 class="text-xl font-semibold break-words">Server Details</h2>
<!-- Standalone Server Icon Customization (Alpine.js, AJAX) -->
<div 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' }}
})" class="flex items-center gap-2">
<button @click="openModal()" :title="icon ? 'Customize server icon' : 'Set server icon'" class="p-2 rounded-lg bg-secondary/10 hover:bg-secondary/20 transition-colors">
<i x-show="icon" :class="'ri-' + icon" class="size-5" :style="color ? 'color: ' + color : ''"></i>
<x-ri-server-line x-show="!icon" class="size-5 text-secondary" />
</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 ? 'color: ' + color + '; border-color: ' + color : ''">
<i x-show="icon" :class="'ri-' + icon" class="size-8"></i>
<x-ri-server-line x-show="!icon" class="size-8 text-base/40" />
</div>
<input x-model="search" type="text" placeholder="Search icons..." class="border rounded px-2 py-1 text-sm w-40" />
</div>
<div class="grid grid-cols-6 gap-2 mb-4 max-h-32 overflow-y-auto">
<template x-for="i in filteredIcons" :key="i">
<button @click="icon = i" :class="icon === i ? 'ring-2 ring-primary' : ''" class="rounded p-1 border flex items-center justify-center hover:bg-secondary/10">
<i :class="'ri-' + i" class="size-5"></i>
</button>
</template>
</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>
</div>
<script>
function serverIconModal({serverId, initialIcon, initialColor}) {
return {
show: false,
icon: initialIcon || '',
color: initialColor || '',
search: '',
message: '',
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.search) return this.icons;
const q = this.search.toLowerCase();
return this.icons.filter(i => i.toLowerCase().includes(q));
},
openModal() { this.show = true; this.message = ''; },
closeModal() { this.show = false; },
resetIcon() {
this.icon = '';
this.color = '';
},
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 })
})
.then(r => r.json())
.then(data => {
if (data.success) {
this.message = 'Icon saved!';
setTimeout(() => { this.closeModal(); location.reload(); }, 800);
}
});
}
}
}
</script>
<div class="relative" x-data="{ open: false }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<x-ri-question-line class="size-4" />