mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-12 10:28:55 -05:00
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:
parent
24e9fb4e40
commit
107b8d9db0
@ -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 {
|
.vjs-ended .vjs-poster {
|
||||||
display: none !important;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -25,15 +25,17 @@ class EndScreenOverlay extends Component {
|
|||||||
className: 'vjs-end-screen-overlay',
|
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.position = 'absolute';
|
||||||
overlay.style.top = '0';
|
overlay.style.top = '0';
|
||||||
overlay.style.left = '0';
|
overlay.style.left = '0';
|
||||||
overlay.style.right = '0';
|
overlay.style.right = '0';
|
||||||
overlay.style.bottom = '60px'; // Leave space for control bar
|
overlay.style.bottom = '60px'; // Leave space for control bar
|
||||||
overlay.style.display = 'none'; // Hidden by default
|
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.zIndex = '100';
|
||||||
|
overlay.style.overflow = 'hidden';
|
||||||
|
overlay.style.boxSizing = 'border-box';
|
||||||
|
|
||||||
// Create responsive grid
|
// Create responsive grid
|
||||||
const grid = this.createGrid();
|
const grid = this.createGrid();
|
||||||
@ -43,30 +45,38 @@ class EndScreenOverlay extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createGrid() {
|
createGrid() {
|
||||||
const grid = videojs.dom.createEl('div', {
|
const { columns, maxVideos, useSwiper } = this.getGridConfig();
|
||||||
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)`;
|
|
||||||
|
|
||||||
// Get videos to show - access directly from options during createEl
|
// Get videos to show - access directly from options during createEl
|
||||||
const relatedVideos = this.options_?.relatedVideos || this.relatedVideos || [];
|
const relatedVideos = this.options_?.relatedVideos || this.relatedVideos || [];
|
||||||
|
|
||||||
const videosToShow =
|
const videosToShow =
|
||||||
relatedVideos.length > 0
|
relatedVideos.length > 0
|
||||||
? relatedVideos.slice(0, maxVideos)
|
? relatedVideos.slice(0, maxVideos)
|
||||||
: this.createSampleVideos().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) => {
|
videosToShow.forEach((video) => {
|
||||||
const videoItem = this.createVideoItem(video);
|
const videoItem = this.createVideoItem(video);
|
||||||
grid.appendChild(videoItem);
|
grid.appendChild(videoItem);
|
||||||
@ -75,20 +85,114 @@ class EndScreenOverlay extends Component {
|
|||||||
return grid;
|
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() {
|
getGridConfig() {
|
||||||
const playerEl = this.player().el();
|
const playerEl = this.player().el();
|
||||||
const playerWidth = playerEl?.offsetWidth || window.innerWidth;
|
const playerWidth = playerEl?.offsetWidth || window.innerWidth;
|
||||||
const playerHeight = playerEl?.offsetHeight || window.innerHeight;
|
const playerHeight = playerEl?.offsetHeight || window.innerHeight;
|
||||||
|
|
||||||
// Responsive grid configuration
|
// Calculate available space for better utilization
|
||||||
if (playerWidth >= 1200) {
|
const availableHeight = playerHeight - 140; // Account for controls and padding
|
||||||
return { columns: 4, maxVideos: 8 }; // 4x2 grid for large screens
|
const cardHeight = 180; // Consistent card height
|
||||||
} else if (playerWidth >= 800) {
|
const maxRows = Math.max(2, Math.floor(availableHeight / cardHeight));
|
||||||
return { columns: 3, maxVideos: 6 }; // 3x2 grid for medium screens
|
|
||||||
} else if (playerWidth >= 500) {
|
// Enhanced grid configuration to fill large screens better
|
||||||
return { columns: 2, maxVideos: 4 }; // 2x2 grid for small screens
|
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 {
|
} 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);
|
return this.createSampleVideos().slice(0, maxVideos);
|
||||||
}
|
}
|
||||||
|
|
||||||
createVideoItem(video) {
|
createVideoItem(video, isSwiperMode = false) {
|
||||||
const item = videojs.dom.createEl('div', {
|
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.position = 'relative';
|
||||||
item.style.backgroundColor = '#1a1a1a';
|
item.style.backgroundColor = '#1a1a1a';
|
||||||
item.style.borderRadius = '8px';
|
item.style.borderRadius = '6px';
|
||||||
item.style.overflow = 'hidden';
|
item.style.overflow = 'hidden';
|
||||||
item.style.cursor = 'pointer';
|
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) {
|
if (this.isTouchDevice) {
|
||||||
item.style.touchAction = 'manipulation';
|
item.style.touchAction = 'manipulation';
|
||||||
} else {
|
} else {
|
||||||
item.addEventListener('mouseenter', () => {
|
item.addEventListener('mouseenter', () => {
|
||||||
item.style.transform = 'scale(1.05)';
|
item.style.transform = 'translateY(-2px)';
|
||||||
item.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.3)';
|
item.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.25)';
|
||||||
});
|
});
|
||||||
item.addEventListener('mouseleave', () => {
|
item.addEventListener('mouseleave', () => {
|
||||||
item.style.transform = 'scale(1)';
|
item.style.transform = 'translateY(0)';
|
||||||
item.style.boxShadow = 'none';
|
item.style.boxShadow = 'none';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create thumbnail
|
// Create thumbnail container
|
||||||
const thumbnail = this.createThumbnail(video);
|
const thumbnailContainer = this.createThumbnailContainer(video, isSwiperMode);
|
||||||
item.appendChild(thumbnail);
|
item.appendChild(thumbnailContainer);
|
||||||
|
|
||||||
// Create info overlay
|
// Create simplified info section
|
||||||
const info = this.createVideoInfo(video);
|
const info = this.createVideoInfo(video, isSwiperMode);
|
||||||
item.appendChild(info);
|
item.appendChild(info);
|
||||||
|
|
||||||
// Add click handler
|
// Add click handler
|
||||||
@ -143,7 +265,18 @@ class EndScreenOverlay extends Component {
|
|||||||
return item;
|
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', {
|
const thumbnail = videojs.dom.createEl('img', {
|
||||||
className: 'vjs-related-video-thumbnail',
|
className: 'vjs-related-video-thumbnail',
|
||||||
src: video.thumbnail || this.getPlaceholderImage(video.title),
|
src: video.thumbnail || this.getPlaceholderImage(video.title),
|
||||||
@ -152,74 +285,158 @@ class EndScreenOverlay extends Component {
|
|||||||
|
|
||||||
// Thumbnail styling
|
// Thumbnail styling
|
||||||
thumbnail.style.width = '100%';
|
thumbnail.style.width = '100%';
|
||||||
thumbnail.style.height = '120px';
|
thumbnail.style.height = '100%';
|
||||||
thumbnail.style.objectFit = 'cover';
|
thumbnail.style.objectFit = 'cover';
|
||||||
thumbnail.style.display = 'block';
|
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) {
|
if (video.duration && video.duration > 0) {
|
||||||
const duration = videojs.dom.createEl('div', {
|
const duration = videojs.dom.createEl('div', {
|
||||||
className: 'vjs-video-duration',
|
className: 'vjs-video-duration',
|
||||||
});
|
});
|
||||||
duration.textContent = this.formatDuration(video.duration);
|
duration.textContent = this.formatDuration(video.duration);
|
||||||
duration.style.position = 'absolute';
|
duration.style.position = 'absolute';
|
||||||
duration.style.bottom = '50px';
|
duration.style.bottom = '4px';
|
||||||
duration.style.right = '8px';
|
duration.style.right = '4px';
|
||||||
duration.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
|
duration.style.backgroundColor = 'rgba(0, 0, 0, 0.85)';
|
||||||
duration.style.color = 'white';
|
duration.style.color = 'white';
|
||||||
duration.style.padding = '2px 6px';
|
duration.style.padding = '2px 6px';
|
||||||
duration.style.borderRadius = '4px';
|
duration.style.borderRadius = '3px';
|
||||||
duration.style.fontSize = '12px';
|
duration.style.fontSize = isSwiperMode ? '10px' : '11px';
|
||||||
duration.style.fontWeight = 'bold';
|
duration.style.fontWeight = '600';
|
||||||
|
duration.style.lineHeight = '1';
|
||||||
|
duration.style.zIndex = '2';
|
||||||
|
|
||||||
// Add duration to parent item (will be added later)
|
container.appendChild(duration);
|
||||||
thumbnail.durationBadge = duration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return thumbnail;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
createVideoInfo(video) {
|
createVideoInfo(video, isSwiperMode = false) {
|
||||||
const info = videojs.dom.createEl('div', {
|
const info = videojs.dom.createEl('div', {
|
||||||
className: 'vjs-related-video-info',
|
className: 'vjs-related-video-info',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Info styling
|
// Consistent info styling with increased height for swiper mode
|
||||||
info.style.padding = '12px';
|
const padding = isSwiperMode ? '10px' : '10px';
|
||||||
info.style.color = 'white';
|
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', {
|
const title = videojs.dom.createEl('div', {
|
||||||
className: 'vjs-related-video-title',
|
className: 'vjs-related-video-title',
|
||||||
});
|
});
|
||||||
title.textContent = video.title;
|
title.textContent = video.title;
|
||||||
title.style.fontSize = '14px';
|
title.style.fontSize = isSwiperMode ? '13px' : '13px'; // Slightly larger font for better readability
|
||||||
title.style.fontWeight = 'bold';
|
title.style.fontWeight = '600';
|
||||||
title.style.marginBottom = '4px';
|
|
||||||
title.style.lineHeight = '1.3';
|
title.style.lineHeight = '1.3';
|
||||||
title.style.overflow = 'hidden';
|
title.style.overflow = 'hidden';
|
||||||
title.style.textOverflow = 'ellipsis';
|
title.style.textOverflow = 'ellipsis';
|
||||||
title.style.display = '-webkit-box';
|
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.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', {
|
const meta = videojs.dom.createEl('div', {
|
||||||
className: 'vjs-related-video-meta',
|
className: 'vjs-related-video-meta',
|
||||||
});
|
});
|
||||||
meta.textContent = `${video.author} • ${video.views}`;
|
|
||||||
meta.style.fontSize = '12px';
|
// Format meta text more cleanly - ensure both author and views are shown
|
||||||
meta.style.color = '#aaa';
|
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.overflow = 'hidden';
|
||||||
meta.style.textOverflow = 'ellipsis';
|
meta.style.textOverflow = 'ellipsis';
|
||||||
meta.style.whiteSpace = 'nowrap';
|
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(title);
|
||||||
info.appendChild(meta);
|
info.appendChild(meta); // Always append meta for consistent layout
|
||||||
|
|
||||||
return info;
|
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) {
|
addClickHandler(item, video) {
|
||||||
const clickHandler = () => {
|
const clickHandler = () => {
|
||||||
const isEmbedPlayer = this.player().id() === 'video-embed' || window.parent !== window;
|
const isEmbedPlayer = this.player().id() === 'video-embed' || window.parent !== window;
|
||||||
@ -239,11 +456,6 @@ class EndScreenOverlay extends Component {
|
|||||||
} else {
|
} else {
|
||||||
item.addEventListener('click', clickHandler);
|
item.addEventListener('click', clickHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add duration badge if it exists
|
|
||||||
if (item.querySelector('img').durationBadge) {
|
|
||||||
item.appendChild(item.querySelector('img').durationBadge);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
formatDuration(seconds) {
|
formatDuration(seconds) {
|
||||||
@ -280,18 +492,90 @@ class EndScreenOverlay extends Component {
|
|||||||
|
|
||||||
createSampleVideos() {
|
createSampleVideos() {
|
||||||
return [
|
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: 'sample1',
|
||||||
{ id: 'sample3', title: 'CSS Grid Layout', author: 'Web Dev Academy', views: '1.2M views', duration: 2400 },
|
title: 'React Full Course - Complete Tutorial for Beginners',
|
||||||
{ id: 'sample4', title: 'Node.js Backend', author: 'Code Master', views: '650K views', duration: 3600 },
|
author: 'Bro Code',
|
||||||
{ id: 'sample5', title: 'Vue.js Guide', author: 'Frontend Pro', views: '980K views', duration: 2800 },
|
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',
|
id: 'sample6',
|
||||||
title: 'Python Data Science',
|
title: 'Python Data Science Bootcamp',
|
||||||
author: 'Data Academy',
|
author: 'Data Academy',
|
||||||
views: '1.5M views',
|
views: '1.5M views',
|
||||||
duration: 4200,
|
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,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user