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.
This commit is contained in:
Yiannis Christodoulou 2025-10-11 02:38:56 +03:00
parent 24e9fb4e40
commit 107b8d9db0
2 changed files with 615 additions and 77 deletions

View File

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

View File

@ -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,
},
];
}