import { createSignal, For, Show, onMount } from 'solid-js'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { VideoPreviewModal } from '@/components/ui/VideoPreviewModal'; import { getMockVideos } from '@/lib/mockData'; import { getAuthHeaders } from '@/lib/auth'; import { IconAlertCircle } from '@tabler/icons-solidjs'; type TabType = 'search' | 'predefined' | 'bookmarked'; interface YouTubeVideo { video_id: string; channel_name: string; url: string; title: string; duration?: string; published_at?: string; view_count?: string; category?: string; } interface FeaturedChannel { id: string; name: string; channel_id: string; description?: string; } // VideoCard component interface VideoCardProps { video: YouTubeVideo; onPreview: (video: YouTubeVideo) => void; onSave?: (video: YouTubeVideo) => void; } const VideoCard = (props: VideoCardProps) => ( {/* Thumbnail */}
{props.video.title} { // Fallback to default thumbnail if maxresdefault fails (e.target as HTMLImageElement).src = `https://img.youtube.com/vi/${props.video.video_id}/hqdefault.jpg`; }} />
{/* Play button overlay */}
props.onPreview(props.video)} role="button" aria-label="Play video" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); props.onPreview(props.video); } }} >
{/* Video Info */}

{props.video.title}

{props.video.channel_name}

Views: {props.video.view_count} Published: {props.video.published_at}

Video from {props.video.channel_name}

YouTube Video
{props.onSave && ( )}
); export const Youtube = () => { const [activeTab, setActiveTab] = createSignal('search'); const [searchQuery, setSearchQuery] = createSignal(''); const [videos, setVideos] = createSignal([]); const [predefinedVideos, setPredefinedVideos] = createSignal([]); const [savedVideos, setSavedVideos] = createSignal([]); const [isLoading, setIsLoading] = createSignal(false); const [isLoadingPredefined, setIsLoadingPredefined] = createSignal(false); const [isLoadingSaved, setIsLoadingSaved] = createSignal(false); const [error, setError] = createSignal(''); const [predefinedError, setPredefinedError] = createSignal(''); const [showPreviewModal, setShowPreviewModal] = createSignal(false); const [selectedVideo, setSelectedVideo] = createSignal(null); const [sortBy, setSortBy] = createSignal<'relevance' | 'date' | 'views'>('relevance'); const [showChannelEditor, setShowChannelEditor] = createSignal(false); const [featuredChannels, setFeaturedChannels] = createSignal([ { id: '1', name: 'NetworkChuck', channel_id: '@NetworkChuck', description: 'Networking and IT tutorials' }, { id: '2', name: 'Fireship', channel_id: '@Fireship', description: 'High-intensity tech tutorials' }, { id: '3', name: 'Beyond Fireship', channel_id: '@beyondfireship', description: 'Extended tech content' }, { id: '4', name: 'Linus Tech Tips', channel_id: '@LinusTechTips', description: 'Technology hardware and reviews' }, { id: '5', name: 'Mrwhosetheboss', channel_id: '@Mrwhosetheboss', description: 'Tech reviews and comparisons' }, { id: '6', name: 'JerryRigEverything', channel_id: '@JerryRigEverything', description: 'Durability tests and teardowns' }, { id: '7', name: 'Jeff Geerling', channel_id: '@JeffGeerling', description: 'Homelab and networking projects' }, { id: '8', name: 'MKBHD', channel_id: '@mkbhd', description: 'Tech reviews and industry analysis' } ]); const [newChannelName, setNewChannelName] = createSignal(''); const [newChannelId, setNewChannelId] = createSignal(''); const [newChannelDescription, setNewChannelDescription] = createSignal(''); const [editingChannel, setEditingChannel] = createSignal(null); const [successMessage, setSuccessMessage] = createSignal(''); const [channelFilter, setChannelFilter] = createSignal(''); // Filter channels based on search query const filteredChannels = () => { const filter = channelFilter().toLowerCase(); if (!filter) return featuredChannels(); return featuredChannels().filter(channel => channel.name.toLowerCase().includes(filter) || channel.channel_id.toLowerCase().includes(filter) || (channel.description && channel.description.toLowerCase().includes(filter)) ); }; // Check if we're in demo mode const isDemoMode = () => { const demoMode = localStorage.getItem('demoMode') === 'true' || document.title.includes('Demo Mode') || window.location.search.includes('demo=true'); console.log('YouTube page - Demo mode check:', { localStorage: localStorage.getItem('demoMode'), title: document.title, search: window.location.search, result: demoMode }); return demoMode; }; // Extract video ID from YouTube URL const extractVideoId = (url: string): string | null => { const regex = /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/; const match = url.match(regex); return match ? match[1] : null; }; // Get video info from YouTube API using video ID const getVideoInfo = async (videoId: string) => { try { if (isDemoMode()) { // Use mock data in demo mode const mockVideos = getMockVideos(); const mockVideo = mockVideos.find(v => v.id === videoId); if (mockVideo) { return { video_id: mockVideo.id, channel_name: mockVideo.channel, url: mockVideo.url, title: mockVideo.title, duration: mockVideo.duration, published_at: mockVideo.publishedAt, view_count: '1000', category: mockVideo.category }; } // Fallback mock data return { video_id: videoId, channel_name: 'Demo Channel', url: `https://www.youtube.com/watch?v=${videoId}`, title: `Demo Video ${videoId}`, duration: '10:30', published_at: '2024-01-15', view_count: '1000', category: 'Technology' }; } const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1'; const response = await fetch(`${API_BASE_URL}/youtube/video-details`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ video_id: videoId }), }); if (!response.ok) { const errorText = await response.text(); let errorData; try { errorData = JSON.parse(errorText); } catch { throw new Error(`Server error: ${response.status}`); } throw new Error(errorData?.details || errorData?.error || 'Failed to fetch video info'); } return await response.json(); } catch (err) { // Return a fallback video object with basic info return { video_id: videoId, channel_name: 'Unknown Channel', url: `https://www.youtube.com/watch?v=${videoId}`, title: `Video ${videoId}`, duration: 'Unknown', published_at: 'Unknown', view_count: '0', category: 'General' }; } }; // Load predefined channel videos const loadPredefinedVideos = async () => { setIsLoadingPredefined(true); setPredefinedError(''); try { const channels = featuredChannels(); console.log('Using integrated YouTube service for featured channels'); // Use the integrated backend API const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1'; try { // Fetch videos from all featured channels using the integrated API const channelPromises = channels.map(async (channel) => { try { const response = await fetch( `${API_BASE_URL}/youtube/channel-videos`, { method: 'POST', headers: { ...getAuthHeaders(), }, body: JSON.stringify({ channel_id: channel.channel_id, max_results: 5 }) } ); if (response.ok) { const data = await response.json(); return data.videos || []; } else if (response.status === 401) { console.warn(`Authentication required for ${channel.name}`); return []; } else { console.warn(`Failed to fetch videos for ${channel.name}:`, response.status); return []; } } catch (error) { console.warn(`Error fetching videos for ${channel.name}:`, error); return []; } }); const allChannelVideos = await Promise.all(channelPromises); const allVideos = allChannelVideos.flat(); // Convert scraping service format to our YouTubeVideo format const videos: YouTubeVideo[] = allVideos.map((video: any) => ({ video_id: video.video_id, channel_name: video.channel || 'Unknown Channel', url: `https://www.youtube.com/watch?v=${video.video_id}`, title: video.title || 'Untitled Video', duration: video.length || 'Unknown', published_at: video.published_date || video.published_text || 'Unknown', view_count: video.views ? video.views.toLocaleString() : '0', category: 'General' })); // Sort by published date (most recent first) and limit to 20 videos const sortedVideos = videos .sort((a, b) => { const dateA = a.published_at && a.published_at !== 'Unknown' ? new Date(a.published_at).getTime() : 0; const dateB = b.published_at && b.published_at !== 'Unknown' ? new Date(b.published_at).getTime() : 0; return dateB - dateA; }) .slice(0, 20); setPredefinedVideos(sortedVideos); setIsLoadingPredefined(false); return; } catch (scraperError) { console.warn('YouTube scraping service failed:', scraperError); } // Fallback to backend API const YOUTUBE_API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1'; try { const response = await fetch(`${YOUTUBE_API_BASE_URL}/youtube/predefined-channels`, { method: 'GET', headers: getAuthHeaders(), }); if (response.ok) { const data = await response.json(); // Convert the API response to our YouTubeVideo format const videos: YouTubeVideo[] = data.videos.map((video: any) => ({ video_id: video.id, channel_name: video.channel_title || 'Unknown Channel', url: `https://www.youtube.com/watch?v=${video.id}`, title: video.title, duration: video.duration || 'Unknown', published_at: video.published_at || 'Unknown', view_count: video.view_count?.toString() || '0', category: 'General' })); // Sort by published date (most recent first) and limit to 20 videos const sortedVideos = videos .sort((a, b) => { const dateA = a.published_at ? new Date(a.published_at).getTime() : 0; const dateB = b.published_at ? new Date(b.published_at).getTime() : 0; return dateB - dateA; }) .slice(0, 20); setPredefinedVideos(sortedVideos); setIsLoadingPredefined(false); return; } } catch (backendError) { console.warn('Backend API failed for featured channels:', backendError); } // Final fallback to demo mode console.log('All API methods failed, using demo mode for featured channels'); const mockVideos = getMockVideos(); const videos: YouTubeVideo[] = mockVideos.slice(0, 5).map((video) => ({ video_id: video.id, channel_name: video.channel, url: video.url, title: video.title, duration: video.duration, published_at: video.publishedAt, view_count: '1000', category: video.category || 'General' })); setPredefinedVideos(videos); } catch (err) { console.error('Error in loadPredefinedVideos:', err); setPredefinedError(err instanceof Error ? err.message : 'Failed to load predefined channel videos'); // Fallback to demo mode const mockVideos = getMockVideos(); const videos: YouTubeVideo[] = mockVideos.slice(0, 5).map((video) => ({ video_id: video.id, channel_name: video.channel, url: video.url, title: video.title, duration: video.duration, published_at: video.publishedAt, view_count: '1000', category: video.category || 'General' })); setPredefinedVideos(videos); } finally { setIsLoadingPredefined(false); } }; // Load saved YouTube videos from bookmarks const loadSavedVideos = async () => { setIsLoadingSaved(true); try { const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1'; const response = await fetch(`${API_BASE_URL}/video-bookmarks`, { headers: getAuthHeaders(), }); if (response.ok) { const data = await response.json(); const bookmarks = data.bookmarks || []; const videos: YouTubeVideo[] = bookmarks.map((bookmark: any) => ({ video_id: bookmark.video_id, channel_name: bookmark.channel || 'Unknown Channel', url: bookmark.url, title: bookmark.title || 'Untitled Video', duration: 'Unknown', published_at: bookmark.created_at || 'Unknown', view_count: '0', category: 'General', })); setSavedVideos(videos); } else { console.warn('Failed to load video bookmarks:', response.status); setSavedVideos([]); } } catch (err) { console.warn('Failed to load saved YouTube videos:', err); setSavedVideos([]); } finally { setIsLoadingSaved(false); } }; // Add keyboard event handler for ESC key const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' && showChannelEditor()) { setShowChannelEditor(false); setEditingChannel(null); setNewChannelName(''); setNewChannelId(''); setNewChannelDescription(''); } }; // Add and remove keyboard event listener onMount(() => { console.log('YouTube page mounted, demo mode:', isDemoMode()); loadPredefinedVideos(); loadSavedVideos(); document.addEventListener('keydown', handleKeyDown); // Return cleanup function return () => { document.removeEventListener('keydown', handleKeyDown); }; }); // Load predefined videos when tab is switched to predefined const handleTabChange = (tab: TabType) => { setActiveTab(tab); if (tab === 'predefined' && predefinedVideos().length === 0) { loadPredefinedVideos(); } }; const handleSearch = async () => { const query = searchQuery().trim(); if (!query) return; setIsLoading(true); setError(''); try { // Check if we're in demo mode first if (isDemoMode()) { console.log('Using demo mode for search'); const mockVideos = getMockVideos(); const filteredVideos = mockVideos .filter(video => video.title.toLowerCase().includes(query.toLowerCase()) || video.description.toLowerCase().includes(query.toLowerCase()) || video.channel.toLowerCase().includes(query.toLowerCase()) ) .slice(0, 10) .map((video) => ({ video_id: video.id, channel_name: video.channel, url: video.url, title: video.title, duration: video.duration, published_at: video.publishedAt, view_count: '1000', category: video.category || 'General' })); setVideos(filteredVideos); setIsLoading(false); return; } // Check if the input is a YouTube URL const videoId = extractVideoId(query); if (videoId) { // It's a YouTube URL, get video info directly const data = await getVideoInfo(videoId); const video: YouTubeVideo = { video_id: data.video_id, channel_name: data.channel_name, url: data.url, title: data.title, duration: data.duration || 'Unknown', published_at: data.published_at || 'Unknown', view_count: data.view_count || '0', category: 'General' }; setVideos([video]); } else { // It's a regular search query - use backend API for now (will be replaced with scraping service) try { const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1'; const response = await fetch(`${API_BASE_URL}/youtube/search`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ query: query }), }); if (response.ok) { const data = await response.json(); // Convert the API response to our YouTubeVideo format const videos: YouTubeVideo[] = data.videos.map((video: any) => ({ video_id: video.id, channel_name: video.channel_title || 'Unknown Channel', url: `https://www.youtube.com/watch?v=${video.id}`, title: video.title, duration: video.duration || 'Unknown', published_at: video.published_at || 'Unknown', view_count: video.view_count?.toString() || '0', category: 'General' })); setVideos(videos); } else { throw new Error('Search API failed'); } } catch (apiError) { console.warn('Backend search API failed:', apiError); // Fallback to demo mode console.log('Using demo mode fallback for search'); const mockVideos = getMockVideos(); const filteredVideos = mockVideos .filter(video => video.title.toLowerCase().includes(query.toLowerCase()) || video.description.toLowerCase().includes(query.toLowerCase()) || video.channel.toLowerCase().includes(query.toLowerCase()) ) .slice(0, 10) .map((video) => ({ video_id: video.id, channel_name: video.channel, url: video.url, title: video.title, duration: video.duration, published_at: video.publishedAt, view_count: '1000', category: video.category || 'General' })); setVideos(filteredVideos); } } } catch (err) { console.warn('Search failed, falling back to demo mode:', err); // Fallback to demo mode const mockVideos = getMockVideos(); const filteredVideos = mockVideos .filter(video => video.title.toLowerCase().includes(query.toLowerCase()) || video.description.toLowerCase().includes(query.toLowerCase()) || video.channel.toLowerCase().includes(query.toLowerCase()) ) .slice(0, 10) .map((video) => ({ video_id: video.id, channel_name: video.channel, url: video.url, title: video.title, duration: video.duration, published_at: video.publishedAt, view_count: '1000', category: video.category || 'General' })); setVideos(filteredVideos); } finally { setIsLoading(false); } }; const handleKeyPress = (e: KeyboardEvent) => { if (e.key === 'Enter') { handleSearch(); } }; const handleInput = (e: InputEvent) => { const target = e.currentTarget as HTMLInputElement; if (target) { setSearchQuery(target.value); } }; const handlePreviewVideo = (video: YouTubeVideo) => { setSelectedVideo(video); setShowPreviewModal(true); }; const handleSaveVideo = async (video: YouTubeVideo) => { try { if (isDemoMode()) { // Simulate save in demo mode console.log('Video saved (demo mode):', video); setSavedVideos((prev) => { if (prev.some((v) => v.video_id === video.video_id)) { return prev; } return [video, ...prev]; }); setSuccessMessage('Video saved successfully!'); setTimeout(() => setSuccessMessage(''), 3000); return; } const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1'; const bookmarkData = { url: video.url, description: `Video from ${video.channel_name}`, tags: '', is_favorite: false, }; const response = await fetch(`${API_BASE_URL}/video-bookmarks`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(bookmarkData), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to create bookmark'); } const result = await response.json(); console.log('Video bookmarked successfully:', result); // Refresh saved videos await loadSavedVideos(); setSuccessMessage('Video saved successfully!'); setTimeout(() => setSuccessMessage(''), 3000); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save video'); setTimeout(() => setError(''), 3000); } }; return (
{/* Header */}

YouTube Video Storage

Search, discover, and store YouTube videos

Demo Mode Using sample data
{/* Tabs */}
{/* Search Tab Content */}
{/* Search Input */}
{/* Filters */}
{/* Predefined Channels Tab Content */}

Featured YouTube Channels

Latest videos from your selected channels

{/* Channel Filter */}
{ const target = e.currentTarget as HTMLInputElement; if (target) { setChannelFilter(target.value); } }} class="text-base" />
{/* Bookmarked Videos Tab Content */}

Your Bookmarked Videos

Videos you have saved for later

{/* Error Messages */}

{error()}

{successMessage()}

{predefinedError()}

{/* Search Results */} 0}>

Search Results

{(video) => ( )}
{/* Predefined Channel Videos */} 0}>

Latest Videos from Featured Channels

{(video) => ( )}
{/* Bookmarked Videos */} 0}>

Your Bookmarked Videos

{(video) => ( )}
{/* Saved Videos */} 0}>

Saved Videos

{isLoadingSaved() ? (
{() => (
)}
) : (
{(video) => ( )}
)}
{/* Bookmarked tab empty state */}

No bookmarked videos

Start saving videos from the search results to see them here.

{/* Bookmarked tab loading state */}

Loading Bookmarked Videos

Fetching your saved videos...

{/* Empty States */} {/* Search tab empty state */}

No videos found

Try searching with different keywords or check your spelling.

{/* Search tab initial state */}

Search YouTube Videos

Enter keywords above to search for videos, channels, or topics. Use filters to narrow down your results.

{/* Predefined tab loading state */}

Loading Featured Videos

Fetching latest videos from NetworkChuck, Fireship, and Beyond Fireship...

{/* Predefined tab empty state */}

No Videos Available

Click the Refresh button to load the latest videos from featured channels.

{/* Video Preview Modal */} setShowPreviewModal(false)} video={selectedVideo()} /> {/* Channel Editor Modal */}
{ if (e.target === e.currentTarget) { setShowChannelEditor(false); setEditingChannel(null); setNewChannelName(''); setNewChannelId(''); setNewChannelDescription(''); } }} >

Manage Featured Channels

{/* Add New Channel Form */}

{editingChannel() ? 'Edit Channel' : 'Add New Channel'}

{ const target = e.currentTarget as HTMLInputElement; if (target) { setNewChannelName(target.value); } }} />
{ const target = e.currentTarget as HTMLInputElement; if (target) { setNewChannelId(target.value); } }} />

Use the channel handle or full channel URL as accepted by the YouTube Channel Scraper API.

{ const target = e.currentTarget as HTMLInputElement; if (target) { setNewChannelDescription(target.value); } }} />
{editingChannel() && ( )}
{/* Current Channels List */}

Current Channels ({filteredChannels().length})

{(channel) => (

{channel.name}

Handle/URL: {channel.channel_id}

{channel.description && (

{channel.description}

)}
)}
{filteredChannels().length === 0 && (

No channels found matching "{channelFilter()}"

)}
); };