Files
wizarr/app/templates/admin/home.html
Matthieu B 187b8758c1 Refactor wizard templates and JavaScript controller
- Updated title block in frame.html for clarity.
- Improved localization for the "Next" button in steps.html.
- Refactored the WizardController JavaScript for better readability and organization.
- Added detailed comments to the wizard architecture for future reference.
- Enhanced button visibility and state management based on interaction requirements.
- Implemented swipe gesture handling for mobile navigation.
- Updated subproject commit to indicate a dirty state.
2025-10-07 14:01:16 +02:00

341 lines
13 KiB
HTML

<section class="py-8 bg-gray-50 dark:bg-gray-900 animate__animated animate__fadeIn animate__faster min-h-screen">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Header with New Invite button -->
<div class="flex justify-between items-center mb-8">
<div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ _("Dashboard") }}</h1>
</div>
<button id="newInviteBtn"
class="bg-primary hover:bg-primary_hover text-white font-medium px-6 py-3 rounded-lg shadow-lg transition-all duration-200 flex items-center gap-2"
onclick="openInviteModal()">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
{{ _("New Invite") }}
</button>
</div>
<!-- Dashboard Grid Layout -->
<div class="dashboard-content space-y-6">
<!-- Now Playing Section (adaptive height based on content) -->
<div id="nowPlayingSection"
class="transition-all duration-500 ease-in-out">
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<svg class="w-6 h-6 text-primary"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M8.6 5.2A1 1 0 0 0 7 6v12a1 1 0 0 0 1.6.8l8-6a1 1 0 0 0 0-1.6l-8-6Z" clip-rule="evenodd" />
</svg>
{{ _("Now Playing") }}
</h2>
</div>
<div id="nowPlayingContent"
class="relative z-20"
hx-get="/now-playing-cards"
hx-trigger="load, every 10s"
hx-swap="innerHTML settle:0ms">
<!-- Now playing cards container -->
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
<!-- Loading placeholder -->
<div class="col-span-full flex justify-center items-center py-12">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
</div>
</div>
</div>
<!-- Widget Grid - main widgets row -->
<div id="widgetGrid"
class="grid grid-cols-1 lg:grid-cols-2 gap-6 transition-all duration-500 ease-in-out items-stretch lg:grid-rows-1">
<!-- Latest Accepted Invites Card -->
<div id="latestInvitesContainer"
class="min-h-[400px]"
hx-get="/accepted-invites-card"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
<!-- Loading placeholder -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 h-full flex items-center justify-center">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto"></div>
</div>
</div>
<!-- Server Health Card -->
<div id="serverHealthContainer"
class="min-h-[400px]"
hx-get="/server-health-card"
hx-trigger="load, every 45s"
hx-swap="innerHTML">
<!-- Loading placeholder -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 h-full flex items-center justify-center">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto"></div>
</div>
</div>
</div>
<!-- Removed additional widgets (Library Breakdown & Recent Content) -->
</div>
<!-- Invite Modal Container -->
<div id="invite-modal"></div>
</section>
<script>
// Modal functions
function openInviteModal() {
// Load invite form into the modal container
htmx.ajax('GET', '/invite', {
target: '#invite-modal',
swap: 'innerHTML'
});
}
function closeInviteModal() {
document.getElementById('invite-modal').innerHTML = '';
}
// Close modal on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeInviteModal();
}
});
// Live timestamp updating
let liveTimers = [];
let nowPlayingTimers = [];
function startLiveTimers() {
// Find all live timestamp elements and start their timers
document.querySelectorAll('[data-timestamp]').forEach(function(elem) {
const timestamp = parseInt(elem.getAttribute('data-timestamp'));
if (timestamp) {
const timer = setInterval(function() {
const now = Math.floor(Date.now() / 1000);
const diff = now - timestamp;
elem.textContent = formatDuration(diff);
}, 1000);
liveTimers.push(timer);
}
});
}
function startNowPlayingTimers() {
// Clear existing timers
nowPlayingTimers.forEach(timer => clearInterval(timer));
nowPlayingTimers = [];
// Find all now playing cards and start live time updates
document.querySelectorAll('.now-playing-card[data-session-state="playing"]').forEach(function(card) {
const duration = parseInt(card.getAttribute('data-session-duration')) || 0;
const initialPosition = parseInt(card.getAttribute('data-session-position')) || 0;
const sessionId = card.getAttribute('data-session-id');
if (duration > 0 && sessionId) {
let currentPosition = initialPosition;
const startTime = Date.now();
const timer = setInterval(function() {
// Calculate elapsed time since timer started (in milliseconds)
const elapsed = Date.now() - startTime;
// Add elapsed time to initial position
const newPosition = currentPosition + elapsed;
// Don't exceed duration
if (newPosition < duration) {
updateCardTime(card, newPosition, duration);
} else {
clearInterval(timer);
}
}, 1000);
nowPlayingTimers.push(timer);
}
});
}
function updateCardTime(card, position, duration) {
const timeDisplay = card.querySelector('.session-time-display');
const progressBar = card.querySelector('.session-progress-bar');
if (timeDisplay) {
const posSeconds = Math.floor(position / 1000);
const durSeconds = Math.floor(duration / 1000);
const posMins = Math.floor(posSeconds / 60);
const durMins = Math.floor(durSeconds / 60);
const posSecsRemainder = posSeconds % 60;
const durSecsRemainder = durSeconds % 60;
timeDisplay.textContent = `${posMins}:${posSecsRemainder.toString().padStart(2, '0')} / ${durMins}:${durSecsRemainder.toString().padStart(2, '0')}`;
}
if (progressBar && duration > 0) {
const progressPercent = (position / duration) * 100;
progressBar.style.width = `${Math.min(progressPercent, 100)}%`;
}
}
function formatDuration(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
return `${minutes}:${secs.toString().padStart(2, '0')}`;
}
// Re-init Flowbite popovers when HTMX swaps in new now-playing cards
function reinitFlowbitePopovers() {
if (typeof initFlowbite === 'function') {
initFlowbite(); // official helper (ES module / CDN helper)
} else if (window.Flowbite && typeof window.Flowbite.init === 'function') {
window.Flowbite.init();
}
// After Flowbite (or fallback) initialises, explicitly set the placement to bottom
document.querySelectorAll('[data-popover-target]').forEach(trigger => {
const targetId = trigger.getAttribute('data-popover-target');
const pop = document.getElementById(targetId);
if (pop) {
pop.setAttribute('data-popper-placement', 'bottom');
}
});
// Manual fallback binding when Flowbite JS is not available
if (!(typeof initFlowbite === 'function' || (window.Flowbite && typeof window.Flowbite.init === 'function'))) {
document.querySelectorAll('[data-popover-target]').forEach(trigger => {
if (trigger.dataset.popoverBound) return; // skip if already bound
const targetId = trigger.getAttribute('data-popover-target');
const pop = document.getElementById(targetId);
if (!pop) return;
const show = () => {
pop.classList.remove('invisible', 'opacity-0');
};
const hide = () => {
pop.classList.add('opacity-0');
setTimeout(() => pop.classList.add('invisible'), 300);
};
trigger.addEventListener('mouseenter', show);
trigger.addEventListener('mouseleave', hide);
pop.addEventListener('mouseenter', show);
pop.addEventListener('mouseleave', hide);
trigger.dataset.popoverBound = 'true';
});
}
}
document.addEventListener('htmx:afterSwap', function(evt) {
if (evt.target && evt.target.id === 'nowPlayingContent') {
reinitFlowbitePopovers();
// Restart live-timer so newly swapped cards animate correctly
liveTimers.forEach(timer => clearInterval(timer));
liveTimers = [];
startLiveTimers();
// Start live time updates for now playing cards
startNowPlayingTimers();
// Removed adaptive additionalWidgets logic since those widgets are no longer present
}
});
document.addEventListener('DOMContentLoaded', function() {
// Initialize live timers
startLiveTimers();
// Initialize now playing timers
startNowPlayingTimers();
// Initialize Flowbite popovers
reinitFlowbitePopovers();
// Removed adaptive additionalWidgets logic since those widgets are no longer present
});
</script>
<style>
/* Ensure equal height cards in widget grid on desktop */
@media (min-width: 1024px) {
#widgetGrid {
display: grid;
grid-template-rows: 1fr;
align-items: stretch;
}
#latestInvitesContainer,
#serverHealthContainer {
height: 100%;
display: flex;
flex-direction: column;
}
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Blobby hover animation for session cards */
.session-card {
transform-origin: center top;
transition: all 0.3s ease;
/* simpler transition */
overflow: visible;
}
.session-card:hover {
position: relative;
z-index: 50;
/* remove scaling/shadow animation */
}
/* Remove sibling shrink effect */
.session-card:hover~.session-card {
transform: none;
opacity: 1;
}
/* Hover overlay animation */
.hover-overlay {
backdrop-filter: blur(8px);
border-radius: 0.5rem;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Ensure cards don't interfere with each other during hover */
.session-card:hover~.session-card {
transform: none;
opacity: 1;
}
/* Grid container adjustments for hover effects */
#nowPlayingContent {
perspective: 1000px;
overflow: visible;
/* Allow cards to overflow outside grid */
}
/* Smooth transitions for grid items */
#nowPlayingContent>div {
transition: all 0.3s ease;
overflow: visible;
}
/* Additional hover effects for better UX */
.session-card:hover .hover-overlay {
animation: slideInUp 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
</style>