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:
cproudlock
2025-11-18 09:08:21 -05:00
parent e1e54eded1
commit ea3442c9db

View File

@@ -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
}
});