From 107b8d9db02481257006bffd80dacc74ced38136 Mon Sep 17 00:00:00 2001 From: Yiannis Christodoulou Date: Sat, 11 Oct 2025 02:38:56 +0300 Subject: [PATCH] Enhance end screen overlay with responsive grid and swiper Redesigned the end screen overlay to support a responsive grid layout for related videos on larger screens and a horizontal swiper for small screens. Improved card consistency, added navigation indicators for the swiper, and unified styling in both CSS and JS for better user experience and maintainability. --- .../components/overlays/EndScreenOverlay.css | 254 ++++++++++ .../components/overlays/EndScreenOverlay.js | 438 +++++++++++++++--- 2 files changed, 615 insertions(+), 77 deletions(-) diff --git a/frontend-tools/video-js/src/components/overlays/EndScreenOverlay.css b/frontend-tools/video-js/src/components/overlays/EndScreenOverlay.css index 03359f62..1a6fa4f6 100644 --- a/frontend-tools/video-js/src/components/overlays/EndScreenOverlay.css +++ b/frontend-tools/video-js/src/components/overlays/EndScreenOverlay.css @@ -1,3 +1,257 @@ +/* ===== END SCREEN OVERLAY STYLES ===== */ + +.vjs-end-screen-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 60px; /* Leave space for control bar */ + background: #000000; /* Solid black background */ + display: none; + z-index: 100; + overflow: hidden; /* Prevent content from overflowing */ + box-sizing: border-box; +} + +.vjs-end-screen-overlay.vjs-show { + display: flex !important; +} + +/* Related videos grid */ +.vjs-related-videos-grid { + display: grid; + gap: 12px; + padding: 20px; + height: 100%; + overflow-y: auto; + align-content: start; + justify-items: stretch; +} + +/* Video item cards */ +.vjs-related-video-item { + position: relative; + background-color: #1a1a1a; + border-radius: 6px; + overflow: hidden; + cursor: pointer; + transition: + transform 0.15s ease, + box-shadow 0.15s ease; + display: flex; + flex-direction: column; + height: 180px; + min-height: 180px; + width: 100%; +} + +.vjs-related-video-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); +} + +/* Swiper specific styles */ +.vjs-related-videos-swiper-container { + position: relative; + padding: 20px; + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + overflow: hidden; /* Prevent container overflow */ + box-sizing: border-box; +} + +.vjs-related-videos-swiper { + display: flex; + overflow-x: auto; + overflow-y: hidden; + gap: 12px; + padding-bottom: 10px; + scroll-behavior: smooth; + scroll-snap-type: x mandatory; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE/Edge */ + width: 100%; + max-width: 100%; + box-sizing: border-box; + /* Prevent scroll propagation */ + overscroll-behavior-x: contain; +} + +.vjs-related-videos-swiper::-webkit-scrollbar { + display: none; /* Chrome/Safari */ +} + +.vjs-swiper-item { + min-width: calc(50% - 6px); /* 2 items visible with gap */ + width: calc(50% - 6px); + max-width: 180px; + height: 220px; /* Increased height for better content display */ + min-height: 220px; + flex-shrink: 0; + scroll-snap-align: start; +} + +.vjs-swiper-indicators { + display: flex; + justify-content: center; + gap: 8px; + margin-top: 10px; +} + +.vjs-swiper-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: rgba(255, 255, 255, 0.4); + cursor: pointer; + transition: background-color 0.2s ease; +} + +.vjs-swiper-dot.active { + background-color: #ffffff; +} + +/* Thumbnail container */ +.vjs-related-video-thumbnail-container { + position: relative; + width: 100%; + height: 100px; + overflow: hidden; + flex-shrink: 0; +} + +.vjs-related-video-thumbnail { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +/* Duration badge */ +.vjs-video-duration { + position: absolute; + bottom: 4px; + right: 4px; + background-color: rgba(0, 0, 0, 0.85); + color: white; + padding: 2px 6px; + border-radius: 3px; + font-size: 11px; + font-weight: 600; + line-height: 1; + z-index: 2; +} + +/* Video info section */ +.vjs-related-video-info { + padding: 10px; + color: white; + flex: 1; + display: flex; + flex-direction: column; + justify-content: flex-start; + min-height: 50px; +} + +.vjs-related-video-title { + font-size: 13px; + font-weight: 600; + line-height: 1.2; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + margin-bottom: 6px; + color: #ffffff; +} + +.vjs-related-video-meta { + font-size: 11px; + color: #b3b3b3; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex-shrink: 0; + line-height: 1.2; +} + +/* Swiper specific info styling */ +.vjs-swiper-item .vjs-related-video-info { + padding: 10px; + height: 110px; /* Increased height for better content display */ + min-height: 110px; +} + +.vjs-swiper-item .vjs-related-video-title { + font-size: 13px; /* Slightly larger for better readability */ + line-height: 1.3; + margin-bottom: 8px; + -webkit-line-clamp: 3; /* Allow 3 lines for titles */ +} + +.vjs-swiper-item .vjs-related-video-meta { + font-size: 11px; /* Slightly larger for better readability */ + margin-top: 4px; +} + +/* Responsive breakpoints */ +@media (max-width: 599px) { + /* Small screens use swiper - styles handled by JS */ + .vjs-related-video-thumbnail-container { + height: 100px; + } + + .vjs-related-video-title { + font-size: 12px; + } +} + +@media (min-width: 600px) and (max-width: 899px) { + .vjs-related-videos-grid { + grid-template-columns: repeat(2, 1fr); + } + + .vjs-related-video-thumbnail-container { + height: 110px; + } +} + +@media (min-width: 900px) and (max-width: 1199px) { + .vjs-related-videos-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (min-width: 1200px) and (max-width: 1599px) { + .vjs-related-videos-grid { + grid-template-columns: repeat(4, 1fr); + } +} + +@media (min-width: 1600px) { + .vjs-related-videos-grid { + grid-template-columns: repeat(5, 1fr); + } +} + +/* Hide poster and video when end screen is shown */ .vjs-ended .vjs-poster { display: none !important; } + +/* Ensure video is completely hidden when end screen is active */ +.video-js.vjs-ended video { + display: none !important; + opacity: 0 !important; + visibility: hidden !important; +} + +/* Ensure end screen overlay covers everything with solid background */ +.video-js.vjs-ended .vjs-end-screen-overlay { + background: #000000 !important; + z-index: 99999 !important; + display: flex !important; +} diff --git a/frontend-tools/video-js/src/components/overlays/EndScreenOverlay.js b/frontend-tools/video-js/src/components/overlays/EndScreenOverlay.js index 038e51b6..c2fa3335 100644 --- a/frontend-tools/video-js/src/components/overlays/EndScreenOverlay.js +++ b/frontend-tools/video-js/src/components/overlays/EndScreenOverlay.js @@ -25,15 +25,17 @@ class EndScreenOverlay extends Component { className: 'vjs-end-screen-overlay', }); - // Position overlay above control bar + // Position overlay above control bar with solid black background overlay.style.position = 'absolute'; overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.right = '0'; overlay.style.bottom = '60px'; // Leave space for control bar overlay.style.display = 'none'; // Hidden by default - overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; + overlay.style.backgroundColor = '#000000'; // Solid black background overlay.style.zIndex = '100'; + overlay.style.overflow = 'hidden'; + overlay.style.boxSizing = 'border-box'; // Create responsive grid const grid = this.createGrid(); @@ -43,30 +45,38 @@ class EndScreenOverlay extends Component { } createGrid() { - const grid = videojs.dom.createEl('div', { - className: 'vjs-related-videos-grid', - }); - - // Responsive grid styling - grid.style.display = 'grid'; - grid.style.gap = '12px'; - grid.style.padding = '20px'; - grid.style.height = '100%'; - grid.style.overflowY = 'auto'; - - // Responsive grid columns based on player size - const { columns, maxVideos } = this.getGridConfig(); - grid.style.gridTemplateColumns = `repeat(${columns}, 1fr)`; + const { columns, maxVideos, useSwiper } = this.getGridConfig(); // Get videos to show - access directly from options during createEl const relatedVideos = this.options_?.relatedVideos || this.relatedVideos || []; - const videosToShow = relatedVideos.length > 0 ? relatedVideos.slice(0, maxVideos) : this.createSampleVideos().slice(0, maxVideos); - // Create video items + if (useSwiper) { + return this.createSwiperGrid(videosToShow); + } else { + return this.createRegularGrid(columns, videosToShow); + } + } + + createRegularGrid(columns, videosToShow) { + const grid = videojs.dom.createEl('div', { + className: 'vjs-related-videos-grid', + }); + + // Responsive grid styling with consistent dimensions + grid.style.display = 'grid'; + grid.style.gridTemplateColumns = `repeat(${columns}, 1fr)`; + grid.style.gap = '12px'; + grid.style.padding = '20px'; + grid.style.height = '100%'; + grid.style.overflowY = 'auto'; + grid.style.alignContent = 'start'; + grid.style.justifyItems = 'stretch'; + + // Create video items with consistent dimensions videosToShow.forEach((video) => { const videoItem = this.createVideoItem(video); grid.appendChild(videoItem); @@ -75,20 +85,114 @@ class EndScreenOverlay extends Component { return grid; } + createSwiperGrid(videosToShow) { + const container = videojs.dom.createEl('div', { + className: 'vjs-related-videos-swiper-container', + }); + + // Container styling - ensure it stays within bounds + container.style.position = 'relative'; + container.style.padding = '20px'; + container.style.height = '100%'; + container.style.width = '100%'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.overflow = 'hidden'; // Prevent container overflow + container.style.boxSizing = 'border-box'; + + // Create swiper wrapper with proper containment + const swiperWrapper = videojs.dom.createEl('div', { + className: 'vjs-related-videos-swiper', + }); + + swiperWrapper.style.display = 'flex'; + swiperWrapper.style.overflowX = 'auto'; + swiperWrapper.style.overflowY = 'hidden'; + swiperWrapper.style.gap = '12px'; + swiperWrapper.style.paddingBottom = '10px'; + swiperWrapper.style.scrollBehavior = 'smooth'; + swiperWrapper.style.scrollSnapType = 'x mandatory'; + swiperWrapper.style.width = '100%'; + swiperWrapper.style.maxWidth = '100%'; + swiperWrapper.style.boxSizing = 'border-box'; + + // Hide scrollbar and prevent scroll propagation + swiperWrapper.style.scrollbarWidth = 'none'; // Firefox + swiperWrapper.style.msOverflowStyle = 'none'; // IE/Edge + + // Prevent scroll events from bubbling up to parent + swiperWrapper.addEventListener( + 'wheel', + (e) => { + e.stopPropagation(); + // Only prevent default if we're actually scrolling horizontally + const isScrollingHorizontally = Math.abs(e.deltaX) > Math.abs(e.deltaY); + if (isScrollingHorizontally) { + e.preventDefault(); + swiperWrapper.scrollLeft += e.deltaX; + } + }, + { passive: false } + ); + + // Prevent touch events from affecting parent + swiperWrapper.addEventListener( + 'touchstart', + (e) => { + e.stopPropagation(); + }, + { passive: true } + ); + + swiperWrapper.addEventListener( + 'touchmove', + (e) => { + e.stopPropagation(); + }, + { passive: true } + ); + + // Create video items for swiper (show 2 at a time, but allow scrolling through all) + videosToShow.forEach((video) => { + const videoItem = this.createVideoItem(video, true); // Pass true for swiper mode + swiperWrapper.appendChild(videoItem); + }); + + container.appendChild(swiperWrapper); + + // Add navigation indicators if there are more than 2 videos + if (videosToShow.length > 2) { + const indicators = this.createSwiperIndicators(videosToShow.length, swiperWrapper); + container.appendChild(indicators); + } + + return container; + } + getGridConfig() { const playerEl = this.player().el(); const playerWidth = playerEl?.offsetWidth || window.innerWidth; const playerHeight = playerEl?.offsetHeight || window.innerHeight; - // Responsive grid configuration - if (playerWidth >= 1200) { - return { columns: 4, maxVideos: 8 }; // 4x2 grid for large screens - } else if (playerWidth >= 800) { - return { columns: 3, maxVideos: 6 }; // 3x2 grid for medium screens - } else if (playerWidth >= 500) { - return { columns: 2, maxVideos: 4 }; // 2x2 grid for small screens + // Calculate available space for better utilization + const availableHeight = playerHeight - 140; // Account for controls and padding + const cardHeight = 180; // Consistent card height + const maxRows = Math.max(2, Math.floor(availableHeight / cardHeight)); + + // Enhanced grid configuration to fill large screens better + if (playerWidth >= 1600) { + const columns = 5; + return { columns, maxVideos: columns * Math.min(maxRows, 3), useSwiper: false }; // 5 columns, up to 3 rows + } else if (playerWidth >= 1200) { + const columns = 4; + return { columns, maxVideos: columns * Math.min(maxRows, 3), useSwiper: false }; // 4 columns, up to 3 rows + } else if (playerWidth >= 900) { + const columns = 3; + return { columns, maxVideos: columns * Math.min(maxRows, 2), useSwiper: false }; // 3 columns, up to 2 rows + } else if (playerWidth >= 600) { + return { columns: 2, maxVideos: 4, useSwiper: false }; // 2x2 grid for medium screens } else { - return { columns: 1, maxVideos: 3 }; // 1 column for very small screens + return { columns: 2, maxVideos: 6, useSwiper: true }; // Use swiper for small screens } } @@ -102,39 +206,57 @@ class EndScreenOverlay extends Component { return this.createSampleVideos().slice(0, maxVideos); } - createVideoItem(video) { + createVideoItem(video, isSwiperMode = false) { const item = videojs.dom.createEl('div', { - className: 'vjs-related-video-item', + className: `vjs-related-video-item ${isSwiperMode ? 'vjs-swiper-item' : ''}`, }); - // Item styling + // Consistent item styling with fixed dimensions item.style.position = 'relative'; item.style.backgroundColor = '#1a1a1a'; - item.style.borderRadius = '8px'; + item.style.borderRadius = '6px'; item.style.overflow = 'hidden'; item.style.cursor = 'pointer'; - item.style.transition = 'transform 0.2s ease, box-shadow 0.2s ease'; + item.style.transition = 'transform 0.15s ease, box-shadow 0.15s ease'; + item.style.display = 'flex'; + item.style.flexDirection = 'column'; - // Hover/touch effects + // Consistent dimensions for all cards + if (isSwiperMode) { + // Calculate proper width for swiper items (2 items visible + gap) + item.style.minWidth = 'calc(50% - 6px)'; // 50% width minus half the gap + item.style.width = 'calc(50% - 6px)'; + item.style.maxWidth = '180px'; // Maximum width for larger screens + item.style.height = '220px'; // Increased height for better content display + item.style.minHeight = '220px'; + item.style.flexShrink = '0'; + item.style.scrollSnapAlign = 'start'; + } else { + item.style.height = '180px'; + item.style.minHeight = '180px'; + item.style.width = '100%'; + } + + // Subtle hover/touch effects if (this.isTouchDevice) { item.style.touchAction = 'manipulation'; } else { item.addEventListener('mouseenter', () => { - item.style.transform = 'scale(1.05)'; - item.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.3)'; + item.style.transform = 'translateY(-2px)'; + item.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.25)'; }); item.addEventListener('mouseleave', () => { - item.style.transform = 'scale(1)'; + item.style.transform = 'translateY(0)'; item.style.boxShadow = 'none'; }); } - // Create thumbnail - const thumbnail = this.createThumbnail(video); - item.appendChild(thumbnail); + // Create thumbnail container + const thumbnailContainer = this.createThumbnailContainer(video, isSwiperMode); + item.appendChild(thumbnailContainer); - // Create info overlay - const info = this.createVideoInfo(video); + // Create simplified info section + const info = this.createVideoInfo(video, isSwiperMode); item.appendChild(info); // Add click handler @@ -143,7 +265,18 @@ class EndScreenOverlay extends Component { return item; } - createThumbnail(video) { + createThumbnailContainer(video, isSwiperMode = false) { + const container = videojs.dom.createEl('div', { + className: 'vjs-related-video-thumbnail-container', + }); + + // Container styling with consistent height + container.style.position = 'relative'; + container.style.width = '100%'; + container.style.height = isSwiperMode ? '100px' : '110px'; // Slightly taller for regular grid + container.style.overflow = 'hidden'; + container.style.flexShrink = '0'; + const thumbnail = videojs.dom.createEl('img', { className: 'vjs-related-video-thumbnail', src: video.thumbnail || this.getPlaceholderImage(video.title), @@ -152,74 +285,158 @@ class EndScreenOverlay extends Component { // Thumbnail styling thumbnail.style.width = '100%'; - thumbnail.style.height = '120px'; + thumbnail.style.height = '100%'; thumbnail.style.objectFit = 'cover'; thumbnail.style.display = 'block'; - // Add duration badge if available + container.appendChild(thumbnail); + + // Add duration badge at bottom right of thumbnail if (video.duration && video.duration > 0) { const duration = videojs.dom.createEl('div', { className: 'vjs-video-duration', }); duration.textContent = this.formatDuration(video.duration); duration.style.position = 'absolute'; - duration.style.bottom = '50px'; - duration.style.right = '8px'; - duration.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; + duration.style.bottom = '4px'; + duration.style.right = '4px'; + duration.style.backgroundColor = 'rgba(0, 0, 0, 0.85)'; duration.style.color = 'white'; duration.style.padding = '2px 6px'; - duration.style.borderRadius = '4px'; - duration.style.fontSize = '12px'; - duration.style.fontWeight = 'bold'; + duration.style.borderRadius = '3px'; + duration.style.fontSize = isSwiperMode ? '10px' : '11px'; + duration.style.fontWeight = '600'; + duration.style.lineHeight = '1'; + duration.style.zIndex = '2'; - // Add duration to parent item (will be added later) - thumbnail.durationBadge = duration; + container.appendChild(duration); } - return thumbnail; + return container; } - createVideoInfo(video) { + createVideoInfo(video, isSwiperMode = false) { const info = videojs.dom.createEl('div', { className: 'vjs-related-video-info', }); - // Info styling - info.style.padding = '12px'; - info.style.color = 'white'; + // Consistent info styling with increased height for swiper mode + const padding = isSwiperMode ? '10px' : '10px'; + const infoHeight = isSwiperMode ? '110px' : '70px'; // Increased height for swiper mode - // Title + info.style.padding = padding; + info.style.color = 'white'; + info.style.flex = '1'; + info.style.display = 'flex'; + info.style.flexDirection = 'column'; + info.style.justifyContent = 'flex-start'; // Align content to top + info.style.height = infoHeight; + info.style.minHeight = infoHeight; + + // Title with responsive text handling const title = videojs.dom.createEl('div', { className: 'vjs-related-video-title', }); title.textContent = video.title; - title.style.fontSize = '14px'; - title.style.fontWeight = 'bold'; - title.style.marginBottom = '4px'; + title.style.fontSize = isSwiperMode ? '13px' : '13px'; // Slightly larger font for better readability + title.style.fontWeight = '600'; title.style.lineHeight = '1.3'; title.style.overflow = 'hidden'; title.style.textOverflow = 'ellipsis'; title.style.display = '-webkit-box'; - title.style.webkitLineClamp = '2'; + title.style.webkitLineClamp = isSwiperMode ? '3' : '2'; // Allow 3 lines for swiper mode title.style.webkitBoxOrient = 'vertical'; + title.style.marginBottom = isSwiperMode ? '8px' : '4px'; + title.style.color = '#ffffff'; - // Author and views + // Meta information - always show for swiper mode const meta = videojs.dom.createEl('div', { className: 'vjs-related-video-meta', }); - meta.textContent = `${video.author} • ${video.views}`; - meta.style.fontSize = '12px'; - meta.style.color = '#aaa'; + + // Format meta text more cleanly - ensure both author and views are shown + let metaText = ''; + if (video.author && video.views) { + metaText = `${video.author} • ${video.views}`; + } else if (video.author) { + metaText = video.author; + } else if (video.views) { + metaText = video.views; + } else { + // Fallback for sample data + metaText = 'Unknown • No views'; + } + + meta.textContent = metaText; + meta.style.fontSize = isSwiperMode ? '11px' : '11px'; // Slightly larger font for better readability + meta.style.color = '#b3b3b3'; meta.style.overflow = 'hidden'; meta.style.textOverflow = 'ellipsis'; meta.style.whiteSpace = 'nowrap'; + meta.style.flexShrink = '0'; + meta.style.lineHeight = '1.3'; + meta.style.marginTop = isSwiperMode ? '4px' : '0px'; // Add some spacing info.appendChild(title); - info.appendChild(meta); + info.appendChild(meta); // Always append meta for consistent layout return info; } + createSwiperIndicators(totalVideos, swiperWrapper) { + const indicators = videojs.dom.createEl('div', { + className: 'vjs-swiper-indicators', + }); + + indicators.style.display = 'flex'; + indicators.style.justifyContent = 'center'; + indicators.style.gap = '8px'; + indicators.style.marginTop = '10px'; + + const itemsPerView = 2; + const totalPages = Math.ceil(totalVideos / itemsPerView); + + for (let i = 0; i < totalPages; i++) { + const dot = videojs.dom.createEl('div', { + className: 'vjs-swiper-dot', + }); + + dot.style.width = '8px'; + dot.style.height = '8px'; + dot.style.borderRadius = '50%'; + dot.style.backgroundColor = i === 0 ? '#ffffff' : 'rgba(255, 255, 255, 0.4)'; + dot.style.cursor = 'pointer'; + dot.style.transition = 'background-color 0.2s ease'; + + dot.addEventListener('click', () => { + // Calculate scroll position based on container width + const containerWidth = swiperWrapper.offsetWidth; + const scrollPosition = i * containerWidth; // Scroll by full container width + swiperWrapper.scrollTo({ left: scrollPosition, behavior: 'smooth' }); + + // Update active dot + indicators.querySelectorAll('.vjs-swiper-dot').forEach((d, index) => { + d.style.backgroundColor = index === i ? '#ffffff' : 'rgba(255, 255, 255, 0.4)'; + }); + }); + + indicators.appendChild(dot); + } + + // Update active dot on scroll + swiperWrapper.addEventListener('scroll', () => { + const scrollLeft = swiperWrapper.scrollLeft; + const containerWidth = swiperWrapper.offsetWidth; + const currentPage = Math.round(scrollLeft / containerWidth); + + indicators.querySelectorAll('.vjs-swiper-dot').forEach((dot, index) => { + dot.style.backgroundColor = index === currentPage ? '#ffffff' : 'rgba(255, 255, 255, 0.4)'; + }); + }); + + return indicators; + } + addClickHandler(item, video) { const clickHandler = () => { const isEmbedPlayer = this.player().id() === 'video-embed' || window.parent !== window; @@ -239,11 +456,6 @@ class EndScreenOverlay extends Component { } else { item.addEventListener('click', clickHandler); } - - // Add duration badge if it exists - if (item.querySelector('img').durationBadge) { - item.appendChild(item.querySelector('img').durationBadge); - } } formatDuration(seconds) { @@ -280,18 +492,90 @@ class EndScreenOverlay extends Component { createSampleVideos() { return [ - { id: 'sample1', title: 'React Full Course', author: 'Bro Code', views: '2.1M views', duration: 1800 }, - { id: 'sample2', title: 'JavaScript ES6+', author: 'Tech Tutorials', views: '850K views', duration: 1200 }, - { id: 'sample3', title: 'CSS Grid Layout', author: 'Web Dev Academy', views: '1.2M views', duration: 2400 }, - { id: 'sample4', title: 'Node.js Backend', author: 'Code Master', views: '650K views', duration: 3600 }, - { id: 'sample5', title: 'Vue.js Guide', author: 'Frontend Pro', views: '980K views', duration: 2800 }, + { + id: 'sample1', + title: 'React Full Course - Complete Tutorial for Beginners', + author: 'Bro Code', + views: '2.1M views', + duration: 1800, + }, + { + id: 'sample2', + title: 'JavaScript ES6+ Modern Features', + author: 'Tech Tutorials', + views: '850K views', + duration: 1200, + }, + { + id: 'sample3', + title: 'CSS Grid Layout Masterclass', + author: 'Web Dev Academy', + views: '1.2M views', + duration: 2400, + }, + { + id: 'sample4', + title: 'Node.js Backend Development', + author: 'Code Master', + views: '650K views', + duration: 3600, + }, + { + id: 'sample5', + title: 'Vue.js Complete Guide', + author: 'Frontend Pro', + views: '980K views', + duration: 2800, + }, { id: 'sample6', - title: 'Python Data Science', + title: 'Python Data Science Bootcamp', author: 'Data Academy', views: '1.5M views', duration: 4200, }, + { + id: 'sample7', + title: 'TypeScript for Beginners', + author: 'Code School', + views: '750K views', + duration: 1950, + }, + { + id: 'sample8', + title: 'Docker Container Tutorial', + author: 'DevOps Pro', + views: '920K views', + duration: 2700, + }, + { + id: 'sample9', + title: 'MongoDB Database Design', + author: 'DB Expert', + views: '580K views', + duration: 3200, + }, + { + id: 'sample10', + title: 'AWS Cloud Computing', + author: 'Cloud Master', + views: '1.8M views', + duration: 4800, + }, + { + id: 'sample11', + title: 'GraphQL API Development', + author: 'API Guru', + views: '420K views', + duration: 2100, + }, + { + id: 'sample12', + title: 'Kubernetes Orchestration', + author: 'Container Pro', + views: '680K views', + duration: 3900, + }, ]; }