initial commit

This commit is contained in:
2025-06-14 18:25:25 -04:00
commit 22a6ef2d33
15 changed files with 2033 additions and 0 deletions

35
utils/youtube.ts Normal file
View File

@ -0,0 +1,35 @@
/**
* Extracts a YouTube video ID from various URL formats.
* @param url The YouTube URL.
* @returns The video ID, or null if not found.
*/
const extractYouTubeVideoId = (url: string): string | null => {
if (!url || typeof url !== 'string') return null;
const regExp = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=|[^\/]+\/(?:live|shorts)\/)|youtu\.be\/)([^"&?\/\s]{11})/;
const match = url.match(regExp);
if (match && match[1]) {
return match[1];
}
return null;
};
/**
* Generates a YouTube embed URL with common parameters and API support.
* @param url The original YouTube URL.
* @returns The embed URL string, or null if the video ID cannot be extracted or URL is invalid.
*/
export const getYouTubeEmbedUrl = (url: string): string | null => {
const videoId = extractYouTubeVideoId(url);
if (!videoId) {
return null;
}
// Common parameters + enablejsapi for API control + origin for security
// mute=1 is important for allowing autoplay and programmatic unmuting.
// The origin parameter MUST match the domain where the iframe is hosted.
const origin = typeof window !== 'undefined' ? window.location.origin : '';
const params = `autoplay=1&mute=1&playsinline=1&controls=1&modestbranding=1&rel=0&fs=1&iv_load_policy=3&enablejsapi=1&origin=${encodeURIComponent(origin)}`;
return `https://www.youtube.com/embed/${videoId}?${params}`;
};

73
utils/youtubeApiLoader.ts Normal file
View File

@ -0,0 +1,73 @@
export const loadYouTubeIframeApi = (): Promise<typeof window.YT> => {
return new Promise((resolve, reject) => {
if (window.YT && window.YT.Player) {
resolve(window.YT);
return;
}
const existingScript = document.getElementById('youtube-iframe-api');
const checkReady = () => {
if (window.YT && window.YT.Player) {
resolve(window.YT);
} else {
// API script might be loaded but YT object not yet initialized
// This can happen if onYouTubeIframeAPIReady hasn't fired or was missed.
// We poll briefly.
let attempts = 0;
const interval = setInterval(() => {
attempts++;
if (window.YT && window.YT.Player) {
clearInterval(interval);
resolve(window.YT);
} else if (attempts > 20) { // Try for ~2 seconds
clearInterval(interval);
reject(new Error('YouTube Iframe API loaded but YT object not found after timeout.'));
}
}, 100);
}
};
if (existingScript) {
// If script tag exists, it might be loading or loaded.
if (window.YT && window.YT.Player) {
resolve(window.YT);
return;
}
// Cast to access readyState, which might not be in default TS lib HTMLScriptElement
const scriptElement = existingScript as HTMLScriptElement & { readyState?: string };
if (scriptElement.readyState === 'loaded' || scriptElement.readyState === 'complete') {
checkReady();
} else {
existingScript.addEventListener('load', checkReady);
existingScript.addEventListener('error', () => reject(new Error('Failed to load existing YouTube API script.')));
}
return;
}
const tag = document.createElement('script');
tag.id = 'youtube-iframe-api';
tag.src = 'https://www.youtube.com/iframe_api';
const firstScriptTag = document.getElementsByTagName('script')[0];
if (firstScriptTag && firstScriptTag.parentNode) {
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
} else {
// Fallback if no script tags exist (unlikely for a working app)
document.head.appendChild(tag);
}
(window as any).onYouTubeIframeAPIReady = () => {
if (window.YT && window.YT.Player) {
resolve(window.YT);
} else {
// This case should ideally not happen if API is working correctly
reject(new Error('onYouTubeIframeAPIReady fired but YT.Player not found.'));
}
};
tag.addEventListener('error', (e) => {
// Use a more generic error message or inspect 'e' for details if needed
reject(new Error('Failed to load YouTube Iframe API script.'));
});
});
};