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;
|
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) */
|
/* RESPONSIVE SIZING FOR LOW-RES DISPLAYS (720p) */
|
||||||
/* ============================================ */
|
/* ============================================ */
|
||||||
@@ -656,6 +692,9 @@
|
|||||||
let clockInterval;
|
let clockInterval;
|
||||||
let selectedBusinessUnit = '';
|
let selectedBusinessUnit = '';
|
||||||
let businessUnitsLoaded = false;
|
let businessUnitsLoaded = false;
|
||||||
|
let upcomingEvents = [];
|
||||||
|
let currentUpcomingIndex = 0;
|
||||||
|
let upcomingCarouselInterval = null;
|
||||||
|
|
||||||
// Get business unit from URL parameter
|
// Get business unit from URL parameter
|
||||||
function getBusinessUnitFromURL() {
|
function getBusinessUnitFromURL() {
|
||||||
@@ -795,7 +834,7 @@
|
|||||||
html += '</div>'; // Close section
|
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) {
|
if (data.upcoming && data.upcoming.length > 0) {
|
||||||
// Sort upcoming events by type priority, then by date
|
// Sort upcoming events by type priority, then by date
|
||||||
const typePriority = {
|
const typePriority = {
|
||||||
@@ -820,24 +859,26 @@
|
|||||||
return dateA - dateB;
|
return dateA - dateB;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Store for carousel
|
||||||
|
upcomingEvents = sortedUpcoming;
|
||||||
|
|
||||||
html += '<div class="events-section">';
|
html += '<div class="events-section">';
|
||||||
html += '<div class="section-title upcoming">UPCOMING (Next 72 Hours)</div>';
|
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
|
// Render all upcoming events (only first will be visible)
|
||||||
const gridClass = sortedUpcoming.length === 1 ? 'single' : 'multi';
|
sortedUpcoming.forEach((event, index) => {
|
||||||
html += `<div class="events-grid ${gridClass}">`;
|
|
||||||
|
|
||||||
sortedUpcoming.forEach(event => {
|
|
||||||
const borderColor = getColorFromType(event.typecolor);
|
const borderColor = getColorFromType(event.typecolor);
|
||||||
const ticketBadge = event.ticketnumber
|
const ticketBadge = event.ticketnumber
|
||||||
? `<a href="${getServiceNowUrl(event.ticketnumber)}" target="_blank" class="event-ticket">${escapeHtml(event.ticketnumber)}</a>`
|
? `<a href="${getServiceNowUrl(event.ticketnumber)}" target="_blank" class="event-ticket">${escapeHtml(event.ticketnumber)}</a>`
|
||||||
: '';
|
: '';
|
||||||
|
const activeClass = index === 0 ? 'active' : 'enter-down';
|
||||||
html += `
|
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>${ticketBadge}</div>
|
||||||
<div class="event-header">
|
<div class="event-header">
|
||||||
<div class="event-title">${escapeHtml(event.notification)}</div>
|
<div class="event-title">${escapeHtml(event.notification)}</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="event-time">
|
<div class="event-time">
|
||||||
<strong>Starts:</strong> ${formatDateTime(event.starttime)}
|
<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
|
html += '</div>'; // Close section
|
||||||
|
|
||||||
|
// Reset carousel index
|
||||||
|
currentUpcomingIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No events message
|
// No events message
|
||||||
@@ -857,6 +902,61 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = html;
|
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
|
// Escape HTML to prevent XSS
|
||||||
@@ -1015,12 +1115,14 @@
|
|||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
clearInterval(updateInterval);
|
clearInterval(updateInterval);
|
||||||
clearInterval(clockInterval);
|
clearInterval(clockInterval);
|
||||||
|
stopUpcomingCarousel();
|
||||||
} else {
|
} else {
|
||||||
// Resume updates without reinitializing everything
|
// Resume updates without reinitializing everything
|
||||||
updateClock();
|
updateClock();
|
||||||
clockInterval = setInterval(updateClock, 1000);
|
clockInterval = setInterval(updateClock, 1000);
|
||||||
fetchNotifications();
|
fetchNotifications();
|
||||||
updateInterval = setInterval(fetchNotifications, 10000);
|
updateInterval = setInterval(fetchNotifications, 10000);
|
||||||
|
// Carousel will restart in renderEvents when data loads
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user