const { useState, useEffect } = React; // ── Config ──────────────────────────────────────────────────────────────── const _API = { key: ['AIzaSyCSGJzK6KrsujWb', 'LUQkhYsVoHt_oemMLDg'].join(''), channels: { main: { handle: 'IdolKiller', label: 'Idol Killer' }, music: { handle: 'IdolKiller2', label: 'Idol Killer 2' }, // pending }, base: 'https://www.googleapis.com/youtube/v3', }; const CACHE_TTL = 20 * 60 * 1000; function cacheKey(handle) { return `ik_feed_handle_${handle}`; } function getCache(handle) { try { const raw = localStorage.getItem(cacheKey(handle)); if (!raw) return null; const { ts, videos } = JSON.parse(raw); if (Date.now() - ts > CACHE_TTL) return null; return { videos }; } catch { return null; } } function setCache(handle, videos) { try { localStorage.setItem(cacheKey(handle), JSON.stringify({ ts: Date.now(), videos })); } catch {} } function clearChannelCache(handle) { // Clear old ID-based caches too try { localStorage.removeItem(cacheKey(handle)); localStorage.removeItem('ik_feed_UCjZ83CJKuU03jmeeKRP4wiw'); localStorage.removeItem('ik_feed_UCh8NcWbRDzwszrgvZ6nmVcw'); localStorage.removeItem('ik_videos_v2'); localStorage.removeItem('ik_videos_cache'); } catch {} } // ── Step 1: resolve handle → uploads playlist ID ───────────────────────── async function fetchUploadsPlaylistId(handle) { const url = `${_API.base}/channels?part=contentDetails&forHandle=${encodeURIComponent(handle)}&key=${_API.key}`; const res = await fetch(url); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error?.message || `HTTP ${res.status}`); } const data = await res.json(); const uploadsId = data.items?.[0]?.contentDetails?.relatedPlaylists?.uploads; if (!uploadsId) throw new Error(`Could not resolve uploads playlist for @${handle}`); return uploadsId; } // ── Step 2: playlistItems.list → all videos ────────────────────────────── async function fetchPlaylistVideos(playlistId, onProgress) { const BASE = `${_API.base}/playlistItems`; let videos = []; let pageToken = ''; let pages = 0; do { const url = `${BASE}?part=snippet&playlistId=${playlistId}&maxResults=50&key=${_API.key}${pageToken ? '&pageToken=' + pageToken : ''}`; const res = await fetch(url); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error?.message || `HTTP ${res.status}`); } const data = await res.json(); pageToken = data.nextPageToken || ''; pages++; const batch = (data.items || []) .filter(item => { const t = item.snippet?.title || ''; return t !== 'Private video' && t !== 'Deleted video'; }) .map(item => { const s = item.snippet; const videoId = s.resourceId?.videoId || ''; const thumbs = s.thumbnails || {}; const thumb = (thumbs.maxres || thumbs.high || thumbs.medium || thumbs.default || {}).url || `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`; return { id: videoId, title: s.title || '', date: s.publishedAt || '', description: s.description || '', thumb, category: classifyVideo(s.title, s.description || ''), }; }); videos = videos.concat(batch); if (onProgress) onProgress(videos.length); } while (pageToken && pages < 40); return videos; } // ── Combined: handle → videos (with cache) ───────────────────────────── async function fetchChannelVideos(handle, onProgress) { const cached = getCache(handle); if (cached) { if (onProgress) onProgress(cached.videos.length); return cached; } const playlistId = await fetchUploadsPlaylistId(handle); const videos = await fetchPlaylistVideos(playlistId, onProgress); setCache(handle, videos); return { videos }; } // ── Classifier ──────────────────────────────────────────────────────────── const CATEGORIES = ['All', 'Calvinism', 'Original Sin', 'Atonement', 'Molinism', 'Dynamic Omniscience', 'Debates & Responses', 'Church History', 'Theology']; function classifyVideo(title, description) { const t = (title + ' ' + (description || '')).toLowerCase(); if (/calvin|tulip|predestination|reformed|total depravity|irresistible grace|limited atonement|perseverance of/.test(t)) return 'Calvinism'; if (/original sin|inherited sin|adam.s sin|federal head/.test(t)) return 'Original Sin'; if (/atonement|penal sub|ransom|propitiation|expiation/.test(t)) return 'Atonement'; if (/molinis|middle knowledge|counterfactual|luis de molina/.test(t)) return 'Molinism'; if (/omniscience|open theism|foreknow|dynamic omniscience|greg boyd|open view/.test(t)) return 'Dynamic Omniscience'; if (/\bdebate\b|respond|reply|refut| vs |against|rebuttal|james white|response to/.test(t)) return 'Debates & Responses'; if (/church history|church father|origen|augustine|early church|patristic|irenaeus|tertullian|clement|justin martyr|chrysostom/.test(t)) return 'Church History'; return 'Theology'; } // ── Status bar ──────────────────────────────────────────────────────────── function StatusBar({ count, loading, progress, error, onRefresh }) { if (error) return (
⚠ {error}
); if (loading && count === 0) return (
{progress > 0 ? `Loading… ${progress} videos fetched` : 'Connecting to YouTube…'}
); return (
✓ {count} videos loaded from YouTube
); } // ── Video section (reusable) ────────────────────────────────────────────── function VideoSection({ handle, label, eyebrow, onVideoClick, perPage = 12 }) { const [videos, setVideos] = useState([]); const [loading, setLoading] = useState(true); const [progress, setProgress] = useState(0); const [error, setError] = useState(''); const [activeCategory, setActiveCategory] = useState('All'); const [searchQuery, setSearchQuery] = useState(''); const [page, setPage] = useState(1); function load() { if (!handle) { setLoading(false); return; } setLoading(true); setError(''); setProgress(0); fetchChannelVideos(handle, n => setProgress(n)) .then(({ videos: vids }) => { setVideos(vids); setLoading(false); }) .catch(err => { setError(err.message); setLoading(false); }); } useEffect(() => { load(); }, [handle]); useScrollReveal(label + activeCategory + page + videos.length); useEffect(() => { setPage(1); }, [activeCategory, searchQuery]); const counts = {}; videos.forEach(v => { counts[v.category] = (counts[v.category] || 0) + 1; }); const filtered = videos.filter(v => { const matchCat = activeCategory === 'All' || v.category === activeCategory; const matchSearch = !searchQuery || v.title.toLowerCase().includes(searchQuery.toLowerCase()); return matchCat && matchSearch; }); const paginated = filtered.slice(0, page * perPage); const hasMore = paginated.length < filtered.length; return (

{eyebrow}

{label} Videos

{ clearChannelCache(handle); load(); }} /> {videos.length > 0 && ( <>
setSearchQuery(e.target.value)} /> {searchQuery && }
{CATEGORIES.filter(c => c !== 'All').map(c => counts[c] ? ( ) : null)}
)}
{loading && videos.length === 0 && (

{progress > 0 ? `${progress} videos loaded…` : 'Fetching from YouTube…'}

)}
{paginated.map(v => )} {!loading && filtered.length === 0 && videos.length > 0 && (

No videos found{searchQuery ? ` for "${searchQuery}"` : ` in ${activeCategory}`}

)}
{hasMore && !loading && (
)}
); } // ── Watch Page ───────────────────────────────────────────────────────────── function WatchPage({ onVideoClick }) { return (

Content Library

Watch Videos

In-depth theology, apologetics, and church history from Warren McGrew.

{/* Section 1: Main Channel — resolved via @IdolKiller handle */} {/* Divider */}
Also on YouTube
{/* Section 2: Idol Killer 2 — resolved via @IdolKiller2 handle */}
); } // Expose for home page featured section async function fetchRecentVideos(n) { try { const { videos } = await fetchChannelVideos(_API.channels.main.handle, null); return videos.slice(0, n); } catch { return []; } } Object.assign(window, { WatchPage, fetchRecentVideos });