Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions src/pages/broadcasts/details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,27 +85,28 @@ export default function VideoDetails(): ReactElement {
<div className="video-content">
<div className="video-info">
<div className="video-title">
<strong>{title}</strong>
<h1>{title}</h1>
<div className="video-type">
{video.type === 'shorts' ? '📱 Shorts' : '🎥 Video'}
</div>
</div>
<div className="video-type">
{video.type === 'shorts' ? '📱 Shorts' : '🎥 Video'}
<div className="video-description">
<p>{randomDescription}</p>
</div>
</div>
<div className="video-description">
<p>{randomDescription}</p>
</div>
<div className="video-embed-large">
<iframe
src={`https://www.youtube.com/embed/${getYoutubeVideoId(video.youtubeUrl)}${video.type === 'shorts' ? '?loop=1' : ''}`}
width="100%"
height="600"
frameBorder="0"
allowFullScreen
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
loading="lazy"
loading="eager"
title={title}
/>
</div>
<div className="video-meta">
<p>Watch in full screen for the best viewing experience</p>
</div>
</div>
</div>
</div>
Expand Down
96 changes: 83 additions & 13 deletions src/pages/broadcasts/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,10 @@ h1 {
}

.podcast-embed-large {
margin-bottom: 2rem;
}

.podcast-transcript {
background: #fff;
padding: 2rem;
position: relative;
width: 100%;
padding-top: 56.25%; /* 16:9 Aspect Ratio */
margin: 2rem 0;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
Expand All @@ -177,18 +175,22 @@ h1 {
}

.details-card {
max-width: 1000px;
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
border-radius: 12px;
background: var(--ifm-background-surface-color);
width: 100%;
box-sizing: border-box;
}

.video-container {
max-width: 1200px;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
padding: 2rem 1rem;
width: 100%;
box-sizing: border-box;
}

.video-subtitle {
Expand Down Expand Up @@ -310,10 +312,17 @@ h1 {
}

.video-embed-large {
width: 100%;
margin: 1rem 0;
position: relative;
width: 100%;
padding-top: 56.25%; /* 16:9 Aspect Ratio */
margin: 2rem 0;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
background: #000;
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}

.video-embed-large iframe {
Expand All @@ -322,9 +331,70 @@ h1 {
left: 0;
width: 100%;
height: 100%;
border: none;
border-radius: 8px;
}

.video-title h1 {
font-size: 2rem;
margin: 0 0 0.5rem 0;
color: var(--ifm-heading-color);
line-height: 1.2;
}

.video-type {
display: inline-block;
background: var(--ifm-color-primary);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.9rem;
margin-bottom: 1rem;
}

.video-description {
font-size: 1.1rem;
line-height: 1.6;
color: var(--ifm-font-color-base);
margin-bottom: 2rem;
max-width: 800px;
}

.video-meta {
text-align: center;
margin-top: 1.5rem;
color: var(--ifm-color-emphasis-600);
font-size: 0.9rem;
font-style: italic;
}

/* Responsive adjustments */
@media (max-width: 996px) {
.video-title h1 {
font-size: 1.75rem;
}

.video-description {
font-size: 1rem;
}
}

@media (max-width: 768px) {
.video-title h1 {
font-size: 1.5rem;
}

.video-embed-large {
border-radius: 0;
margin: 1.5rem -1rem;
width: calc(100% + 2rem);
}

.video-embed-large iframe {
border-radius: 0;
}
}

.pagination {
display: flex;
justify-content: center;
Expand Down
131 changes: 100 additions & 31 deletions src/pages/broadcasts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,25 @@ interface VideoData {
}

const getYoutubeVideoId = (url: string): string => {
let videoId = '';
const normalMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\s]+)/);
const shortsMatch = url.match(/youtube\.com\/shorts\/([^&\s]+)/);
if (normalMatch) videoId = normalMatch[1];
else if (shortsMatch) videoId = shortsMatch[1];
return videoId;
// Handle youtu.be short links
if (url.includes('youtu.be/')) {
const match = url.match(/youtu\.be\/([^?&\s]+)/);
return match ? match[1].split('?')[0] : '';
}

// Handle youtube.com/watch?v= links
if (url.includes('youtube.com/watch')) {
const match = url.match(/[?&]v=([^&\s]+)/);
return match ? match[1].split('&')[0] : '';
}

// Handle youtube.com/shorts/ links
if (url.includes('youtube.com/shorts/')) {
const match = url.match(/shorts\/([^?&\s]+)/);
return match ? match[1].split('?')[0] : '';
}

return '';
};

const getYoutubeContentType = (url: string): 'video' | 'shorts' =>
Expand Down Expand Up @@ -50,10 +63,64 @@ const VideoCard: React.FC<{
onClick: (video: VideoData, event: React.MouseEvent | React.KeyboardEvent) => void;
}> = ({ video, onClick }) => {
const [title, setTitle] = useState('Loading...');
const [thumbnailError, setThumbnailError] = useState(false);
const [thumbnailUrl, setThumbnailUrl] = useState('');
const videoId = getYoutubeVideoId(video.youtubeUrl);

// Try different thumbnail qualities in sequence
const tryThumbnailUrl = (url: string) => {
if (!videoId) return;

const img = new Image();
img.crossOrigin = 'anonymous'; // Handle CORS if needed

img.onload = () => {
// Only set the URL if it's not an error image
if (img.width > 0 && img.height > 0) {
setThumbnailUrl(url);
} else {
handleThumbnailError(url);
}
};

img.onerror = () => handleThumbnailError(url);
img.src = url;
};

const handleThumbnailError = (failedUrl: string) => {
console.log(`Failed to load thumbnail: ${failedUrl}`);

if (failedUrl.includes('maxresdefault')) {
// Try hqdefault if maxresdefault fails
tryThumbnailUrl(`https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`);
} else if (failedUrl.includes('hqdefault')) {
// Try mqdefault if hqdefault fails
tryThumbnailUrl(`https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`);
} else if (failedUrl.includes('mqdefault')) {
// Try default if mqdefault fails
tryThumbnailUrl(`https://i.ytimg.com/vi/${videoId}/default.jpg`);
} else {
// All options failed, show placeholder
setThumbnailUrl('');
}
};

useEffect(() => {
if (!videoId) return;

// Start with the highest quality thumbnail
console.log(`Loading thumbnails for video ID: ${videoId}`);
tryThumbnailUrl(`https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`);

// Also try the first frame as a fallback (sometimes works when others don't)
const firstFrameUrl = `https://img.youtube.com/vi/${videoId}/0.jpg`;
setTimeout(() => {
if (!thumbnailUrl) {
console.log('Trying first frame as fallback');
tryThumbnailUrl(firstFrameUrl);
}
}, 1000);

// Fetch video title
const fetchVideoTitle = async () => {
try {
const response = await fetch(`https://www.youtube.com/oembed?url=${encodeURIComponent(video.youtubeUrl)}&format=json`);
Expand All @@ -64,8 +131,7 @@ const VideoCard: React.FC<{
console.error('Error fetching video title:', error);
}
};

setThumbnailError(false);

fetchVideoTitle();
}, [video.youtubeUrl, videoId]);

Expand All @@ -80,7 +146,8 @@ const VideoCard: React.FC<{
}}
>
<div className="video-content">
<div className="video-info"> <div className="video-title">{title}</div>
<div className="video-info">
<div className="video-title">{title}</div>
<div className="video-type">
<span>
{video.type === 'shorts' ? (
Expand All @@ -96,26 +163,27 @@ const VideoCard: React.FC<{
</div>
</div>
<div className="video-thumbnail">
{!thumbnailError && (
<img
src={`https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`}
alt={title}
loading="lazy"
onError={(e) => {
const img = e.target as HTMLImageElement;
if (img.src.includes('maxresdefault')) {
img.src = `https://i.ytimg.com/vi/${videoId}/sddefault.jpg`;
} else if (img.src.includes('sddefault')) {
img.src = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
} else if (img.src.includes('hqdefault')) {
img.src = `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`;
} else {
setThumbnailError(true);
}
}}
/>
)}
<div className="video-play-button">▶</div>
<div className="thumbnail-container">
{thumbnailUrl ? (
<img
src={thumbnailUrl}
alt={title}
className="thumbnail-img"
loading="lazy"
onError={(e) => {
const img = e.target as HTMLImageElement;
console.log('Image error:', img.src);
// Let the parent component handle the error
setThumbnailUrl('');
}}
/>
) : (
<div className="thumbnail-placeholder">
<span>Loading thumbnail...</span>
</div>
)}
{/* Play button removed as per request */}
</div>
</div>
</div>
</div>
Expand Down Expand Up @@ -146,7 +214,8 @@ const Pagination: React.FC<{
totalPages: number;
setCurrentPage: (page: number) => void;
}> = ({ currentPage, totalPages, setCurrentPage }) => (
<div className="pagination"> <button
<div className="pagination">
<button
disabled={currentPage === 1}
onClick={() => setCurrentPage(currentPage - 1)}
title="Previous page"
Expand Down
Loading