Add slide-up carousel for upcoming events in shopfloor dashboard
Implemented automatic cycling carousel for upcoming events section with smooth slide-up transitions, showing one event at a time. Features: - Current events: Show all at once (unchanged) - Upcoming events: Cycle through one at a time - 5-second display time per event - 0.8-second smooth slide-up transition - Current event slides up and fades out - Next event slides up from bottom and fades in - Loops continuously through all upcoming events - Only one upcoming event visible at a time Implementation: - Added carousel CSS with position-based transitions - Three states: active (visible), exit-up (sliding out), enter-down (waiting) - JavaScript interval rotates events every 5 seconds - Automatically starts/stops based on number of upcoming events - Pauses when page is hidden (tab switching) - Responsive to all screen sizes (720p, 1080p, 4K) Benefits: - Saves screen space - Each upcoming event gets dedicated visibility - Clean, professional transition effect - No competing for attention in grid layout - Better readability for shop floor displays 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -383,6 +383,42 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Upcoming events carousel */
|
||||
.upcoming-carousel-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.upcoming-carousel-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.event-card.upcoming.carousel-item {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transition: transform 0.8s ease-in-out, opacity 0.8s ease-in-out;
|
||||
}
|
||||
|
||||
.event-card.upcoming.carousel-item.active {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.event-card.upcoming.carousel-item.exit-up {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.event-card.upcoming.carousel-item.enter-down {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* ============================================ */
|
||||
/* RESPONSIVE SIZING FOR LOW-RES DISPLAYS (720p) */
|
||||
/* ============================================ */
|
||||
@@ -656,6 +692,9 @@
|
||||
let clockInterval;
|
||||
let selectedBusinessUnit = '';
|
||||
let businessUnitsLoaded = false;
|
||||
let upcomingEvents = [];
|
||||
let currentUpcomingIndex = 0;
|
||||
let upcomingCarouselInterval = null;
|
||||
|
||||
// Get business unit from URL parameter
|
||||
function getBusinessUnitFromURL() {
|
||||
@@ -795,7 +834,7 @@
|
||||
html += '</div>'; // Close section
|
||||
}
|
||||
|
||||
// Upcoming Events - show all in grid layout
|
||||
// Upcoming Events - carousel with slide-up transition
|
||||
if (data.upcoming && data.upcoming.length > 0) {
|
||||
// Sort upcoming events by type priority, then by date
|
||||
const typePriority = {
|
||||
@@ -820,24 +859,26 @@
|
||||
return dateA - dateB;
|
||||
});
|
||||
|
||||
// Store for carousel
|
||||
upcomingEvents = sortedUpcoming;
|
||||
|
||||
html += '<div class="events-section">';
|
||||
html += '<div class="section-title upcoming">UPCOMING (Next 72 Hours)</div>';
|
||||
html += '<div class="upcoming-carousel-container">';
|
||||
html += '<div class="upcoming-carousel-wrapper" id="upcomingCarousel">';
|
||||
|
||||
// Determine grid class based on number of events
|
||||
const gridClass = sortedUpcoming.length === 1 ? 'single' : 'multi';
|
||||
html += `<div class="events-grid ${gridClass}">`;
|
||||
|
||||
sortedUpcoming.forEach(event => {
|
||||
// Render all upcoming events (only first will be visible)
|
||||
sortedUpcoming.forEach((event, index) => {
|
||||
const borderColor = getColorFromType(event.typecolor);
|
||||
const ticketBadge = event.ticketnumber
|
||||
? `<a href="${getServiceNowUrl(event.ticketnumber)}" target="_blank" class="event-ticket">${escapeHtml(event.ticketnumber)}</a>`
|
||||
: '';
|
||||
const activeClass = index === 0 ? 'active' : 'enter-down';
|
||||
html += `
|
||||
<div class="event-card upcoming" style="border-left-color: ${borderColor};">
|
||||
<div class="event-card upcoming carousel-item ${activeClass}" data-index="${index}" style="border-left-color: ${borderColor};">
|
||||
<div>${ticketBadge}</div>
|
||||
<div class="event-header">
|
||||
<div class="event-title">${escapeHtml(event.notification)}</div>
|
||||
|
||||
</div>
|
||||
<div class="event-time">
|
||||
<strong>Starts:</strong> ${formatDateTime(event.starttime)}
|
||||
@@ -847,8 +888,12 @@
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>'; // Close grid
|
||||
html += '</div>'; // Close carousel wrapper
|
||||
html += '</div>'; // Close carousel container
|
||||
html += '</div>'; // Close section
|
||||
|
||||
// Reset carousel index
|
||||
currentUpcomingIndex = 0;
|
||||
}
|
||||
|
||||
// No events message
|
||||
@@ -857,6 +902,61 @@
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// Start carousel if there are upcoming events
|
||||
if (upcomingEvents.length > 1) {
|
||||
startUpcomingCarousel();
|
||||
} else {
|
||||
stopUpcomingCarousel();
|
||||
}
|
||||
}
|
||||
|
||||
// Start the upcoming events carousel
|
||||
function startUpcomingCarousel() {
|
||||
// Clear any existing interval
|
||||
stopUpcomingCarousel();
|
||||
|
||||
// Start new interval (5 seconds)
|
||||
upcomingCarouselInterval = setInterval(rotateUpcomingEvent, 5000);
|
||||
}
|
||||
|
||||
// Stop the upcoming events carousel
|
||||
function stopUpcomingCarousel() {
|
||||
if (upcomingCarouselInterval) {
|
||||
clearInterval(upcomingCarouselInterval);
|
||||
upcomingCarouselInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate to next upcoming event
|
||||
function rotateUpcomingEvent() {
|
||||
if (upcomingEvents.length <= 1) return;
|
||||
|
||||
const carousel = document.getElementById('upcomingCarousel');
|
||||
if (!carousel) return;
|
||||
|
||||
const items = carousel.querySelectorAll('.carousel-item');
|
||||
if (items.length === 0) return;
|
||||
|
||||
const currentItem = items[currentUpcomingIndex];
|
||||
const nextIndex = (currentUpcomingIndex + 1) % upcomingEvents.length;
|
||||
const nextItem = items[nextIndex];
|
||||
|
||||
// Slide current item up and out
|
||||
currentItem.classList.remove('active');
|
||||
currentItem.classList.add('exit-up');
|
||||
|
||||
// Slide next item up from bottom
|
||||
nextItem.classList.remove('enter-down');
|
||||
nextItem.classList.add('active');
|
||||
|
||||
// After transition, reset the exited item for next rotation
|
||||
setTimeout(() => {
|
||||
currentItem.classList.remove('exit-up');
|
||||
currentItem.classList.add('enter-down');
|
||||
}, 800); // Match transition duration
|
||||
|
||||
currentUpcomingIndex = nextIndex;
|
||||
}
|
||||
|
||||
// Escape HTML to prevent XSS
|
||||
@@ -1015,12 +1115,14 @@
|
||||
if (document.hidden) {
|
||||
clearInterval(updateInterval);
|
||||
clearInterval(clockInterval);
|
||||
stopUpcomingCarousel();
|
||||
} else {
|
||||
// Resume updates without reinitializing everything
|
||||
updateClock();
|
||||
clockInterval = setInterval(updateClock, 1000);
|
||||
fetchNotifications();
|
||||
updateInterval = setInterval(fetchNotifications, 10000);
|
||||
// Carousel will restart in renderEvents when data loads
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user