import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { MediaItem } from '../api/jellyfin'; import { useJellyfinContext } from '../context/JellyfinContext/JellyfinContext'; import { usePlaybackContext } from '../context/PlaybackContext/PlaybackContext'; import { useThemeContext } from '../context/ThemeContext/ThemeContext'; import { useJellyfinTracksData } from '../hooks/Jellyfin/Infinite/useJellyfinTracksData'; import { useInstantMix } from '../hooks/useInstantMix'; import { JellyImg } from '../components/JellyImg'; import { Play, ChevronRight, User, Music, TrendingUp, Clock, Star, Shuffle } from 'lucide-react'; import './HomeEnhancedV2.css'; // iPhone specific CSS will be lazy injected when needed interface FeaturedPlaylist { id: string; name: string; description: string; image?: string; artistImage?: string; artistName?: string; gradient: string; tracks: MediaItem[]; playCount?: number; theme: 'discovery' | 'mood' | 'artist' | 'genre' | 'curated'; } export const HomeEnhancedV2: React.FC = () => { const navigate = useNavigate(); const api = useJellyfinContext(); const { playTrack, setCurrentPlaylistSimple } = usePlaybackContext(); const { playInstantMix } = useInstantMix(); const { items: allTracks, isLoading: tracksLoading } = useJellyfinTracksData(); const [featuredPlaylists, setFeaturedPlaylists] = useState([]); const [discoverData, setDiscoverData] = useState(null); // fix missing setter reference const [madeForYou, setMadeForYou] = useState([]); const [recentlyPlayed, setRecentlyPlayed] = useState([]); const [trendingNow, setTrendingNow] = useState([]); const [welcomeMessage, setWelcomeMessage] = useState(''); const [backgroundImages, setBackgroundImages] = useState([]); const [currentImageIndex, setCurrentImageIndex] = useState(0); const [artistBannerImages, setArtistBannerImages] = useState([]); const [currentBannerIndex, setCurrentBannerIndex] = useState(0); useEffect(() => { const initializeData = async () => { generateFeaturedPlaylists(); setTimeBasedWelcome(); initializeBackgroundImages(); if (!tracksLoading && allTracks.length > 0) { loadRecentContent(); loadDiscoverData(); await initializeArtistBanner(); } }; initializeData(); }, [tracksLoading, allTracks]); // Mobile: ensure sidebar starts closed when loading this enhanced home useEffect(() => { const isMobile = window.innerWidth <= 768; if (!isMobile) return; const ua = navigator.userAgent || ''; const isIPhone = /iPhone|iPod/.test(ua); if (isIPhone) { document.documentElement.classList.add('iphone-home'); // inject stylesheet once if (!document.getElementById('iphone-home-css')) { const link = document.createElement('link'); link.id = 'iphone-home-css'; link.rel = 'stylesheet'; link.href = '/src/pages/HomeEnhancedV2.iphone.css'; document.head.appendChild(link); } // prepare off-canvas state const wrapper = document.querySelector('.sidenav > .sidenav_wrapper'); if (wrapper) { wrapper.classList.add('iphone-closed'); } // add backdrop element if (!document.querySelector('.iphone-sidebar-backdrop')) { const backdrop = document.createElement('div'); backdrop.className = 'iphone-sidebar-backdrop'; backdrop.addEventListener('click', () => toggleIPhoneSidebar(false)); document.body.appendChild(backdrop); } } // expose toggle for debugging (window as any).toggleIPhoneSidebar = toggleIPhoneSidebar; }, []); const toggleIPhoneSidebar = (force?: boolean) => { const wrapper = document.querySelector('.sidenav > .sidenav_wrapper'); if (!wrapper) return; const backdrop = document.querySelector('.iphone-sidebar-backdrop'); const shouldOpen = typeof force === 'boolean' ? force : !wrapper.classList.contains('iphone-open'); if (shouldOpen) { wrapper.classList.remove('iphone-closed'); wrapper.classList.add('iphone-open'); backdrop?.classList.add('visible'); } else { wrapper.classList.remove('iphone-open'); wrapper.classList.add('iphone-closed'); backdrop?.classList.remove('visible'); } }; // Background rotation timer useEffect(() => { if (backgroundImages.length > 1) { const timer = setInterval(() => { setCurrentImageIndex((prevIndex) => (prevIndex + 1) % backgroundImages.length ); }, 10000); // Change every 10 seconds return () => clearInterval(timer); } }, [backgroundImages]); // Artist banner rotation timer useEffect(() => { if (artistBannerImages.length > 1) { const timer = setInterval(() => { setCurrentBannerIndex((prevIndex) => (prevIndex + 1) % artistBannerImages.length ); }, 8000); // Change every 8 seconds return () => clearInterval(timer); } }, [artistBannerImages]); const setTimeBasedWelcome = () => { const hour = new Date().getHours(); // Dawn (5-6am) if (hour >= 5 && hour < 7) setWelcomeMessage('Rise and Shine'); // Morning (7-11am) else if (hour >= 7 && hour < 12) setWelcomeMessage('Good Morning, Music Lover'); // Noon (12pm) else if (hour === 12) setWelcomeMessage('Midday Melodies'); // Afternoon (1-4pm) else if (hour >= 13 && hour < 17) setWelcomeMessage('Afternoon Vibes'); // Evening (5-7pm) else if (hour >= 17 && hour < 20) setWelcomeMessage('Evening Grooves'); // Sunset/Dusk (8-9pm) else if (hour >= 20 && hour < 22) setWelcomeMessage('Sunset Sessions'); // Night (10pm-12am) else if (hour >= 22 || hour === 0) setWelcomeMessage('Late Night Beats'); // Late Night (1-4am) else if (hour >= 1 && hour < 5) setWelcomeMessage('Midnight Mood'); // Default fallback else setWelcomeMessage('Welcome Back'); }; const initializeBackgroundImages = () => { if (allTracks.length === 0) return; console.log('🖼️ Initializing background images with album covers only'); // Focus on album covers only for hero backgrounds with better quality const albumImages = allTracks .filter(track => track.ImageTags?.Primary || track.SpotifyImage) .map(track => { if (track.SpotifyImage) return track.SpotifyImage; // Use higher quality and better sizing for backgrounds return `${api.auth.serverUrl}/Items/${track.Id}/Images/Primary?quality=95&maxWidth=2560&maxHeight=1440`; }) .slice(0, 15); // Get more album covers for variety // Shuffle for variety const shuffledImages = albumImages.sort(() => Math.random() - 0.5).slice(0, 12); console.log(`Got ${shuffledImages.length} album cover images for backgrounds`); setBackgroundImages(shuffledImages); }; const initializeArtistBanner = async () => { if (allTracks.length === 0) return; console.log('🎨 Initializing artist banner with Jellyfin artists'); try { // Get all artists from Jellyfin const artists = await api.getAllArtists(0, 20); if (artists && artists.length > 0) { console.log(`Found ${artists.length} artists in Jellyfin`); // Get artist backdrop images for full-width banner const artistImages = artists .filter((artist: any) => artist.BackdropImageTags && artist.BackdropImageTags.length > 0) .slice(0, 8) // Take up to 8 artists with backdrop images .map((artist: any) => `${api.auth.serverUrl}/Items/${artist.Id}/Images/Backdrop?quality=90&fillWidth=1400&fillHeight=200` ); console.log(`Got ${artistImages.length} artist backdrop images for banner`); setArtistBannerImages(artistImages); return; } } catch (error) { console.error('Error loading Jellyfin artists:', error); } // Fallback: Use album covers as wide banners console.log('Using album covers as fallback for artist banner'); const albumBanners = allTracks .filter(track => track.ImageTags?.Primary || track.SpotifyImage) .slice(0, 8) .map(track => { if (track.SpotifyImage) return track.SpotifyImage; return `${api.auth.serverUrl}/Items/${track.Id}/Images/Primary?quality=85&fillWidth=1400&fillHeight=200`; }); setArtistBannerImages(albumBanners); }; const generateFeaturedPlaylists = () => { const playlists: FeaturedPlaylist[] = [ { id: 'late-night-beats', name: 'Late Night Beats', description: 'Find something new to love today.', gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', artistName: 'AtlasAlbums', tracks: [], theme: 'mood' }, { id: 'mood-boosters', name: 'Mood Boosters', description: 'High playcount tracks you love', gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', tracks: [], theme: 'mood' }, { id: 'feds-sweep', name: 'Feds Did A Sweep', description: 'YbnLamarr & Duvy', gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', artistName: 'YbnLamarr & Duvy', tracks: [], theme: 'artist' }, { id: 'recent-vibes', name: 'Recent Vibes', description: 'Your most recent listens', gradient: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)', tracks: [], theme: 'curated' } ]; setFeaturedPlaylists(playlists); }; const loadDiscoverData = async () => { try { // Determine the correct backend URL based on environment let fetchUrl; if (window.location.hostname === 'music.samcloud.ca') { fetchUrl = '/api/discover'; } else { const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5140'; fetchUrl = `${backendUrl}/api/discover`; } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 8000); const response = await fetch(fetchUrl, { signal: controller.signal, headers: { 'Accept': 'application/json', 'X-Region': 'north-america' // Request North American content } }); clearTimeout(timeoutId); if (response.ok) { const data = await response.json(); setDiscoverData(data); // Use discover data for Made For You and Trending Now if (data.for_you && data.for_you.tracks && data.for_you.tracks.length > 0) { console.log('Loading Made For You from discover API:', data.for_you.tracks.length, 'tracks'); setMadeForYou(data.for_you.tracks.slice(0, 5)); } else { console.log('No Made For You data from discover API'); setMadeForYou([]); } if (data.trending && data.trending.tracks && data.trending.tracks.length > 0) { console.log('Loading Trending Now from discover API:', data.trending.tracks.length, 'tracks'); setTrendingNow(data.trending.tracks.slice(0, 5)); } else { // Fallback: Try to get trending via direct Spotify API await loadSpotifyTrending(); } } else { console.warn('Discover API not available, trying Spotify trending'); await loadSpotifyTrending(); } } catch (error) { console.error('Error loading discover data:', error); // Try direct Spotify trending as fallback await loadSpotifyTrending(); } }; const loadSpotifyTrending = async () => { try { const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5140'; const trendingUrl = `${backendUrl}/api/spotify/trending/north-america`; const response = await fetch(trendingUrl, { headers: { 'Accept': 'application/json' } }); if (response.ok) { const trendingData = await response.json(); if (trendingData.tracks && trendingData.tracks.length > 0) { console.log('Loading trending from Spotify API:', trendingData.tracks.length, 'tracks'); setTrendingNow(trendingData.tracks.slice(0, 5)); } } } catch (error) { console.error('Error loading Spotify trending:', error); console.log('No trending data available, keeping empty'); setTrendingNow([]); } }; const generateQuickSections = async () => { try { // Load recently played tracks const recent = await api.fetchRecentlyPlayed(0, 5); setRecentlyPlayed(recent); } catch (error) { console.error('Error loading recently played:', error); setRecentlyPlayed([]); } }; const loadRecentContent = async () => { await generateQuickSections(); }; const handleDiscoverClick = () => { navigate('/discover'); }; const handleSeeAllClick = (section: string) => { switch (section) { case 'Made For You': navigate('/discover'); break; case 'Recently Played': navigate('/tracks'); break; case 'Trending Now': navigate('/discover'); break; default: navigate('/discover'); } }; const handleShufflePlay = async () => { if (allTracks.length > 0) { const shuffled = [...allTracks].sort(() => Math.random() - 0.5); setCurrentPlaylistSimple({ playlist: shuffled, title: 'Shuffle All Music' }); playTrack(0); } }; const handleMadeForYouTrackPlay = async (track: MediaItem, index: number) => { try { await playInstantMix(track, 'similar'); } catch (error) { console.error('Error playing instant mix:', error); // Fallback to playing from Made For You list setCurrentPlaylistSimple({ playlist: madeForYou, title: 'Made For You' }); playTrack(index); } }; const handleTrendingTrackPlay = async (track: MediaItem, index: number) => { try { await playInstantMix(track, 'similar'); } catch (error) { console.error('Error playing instant mix:', error); // Fallback to playing from Trending list setCurrentPlaylistSimple({ playlist: trendingNow, title: 'Trending Now' }); playTrack(index); } }; const playPlaylist = async (playlist: FeaturedPlaylist) => { if (playlist.tracks.length > 0) { setCurrentPlaylistSimple({ playlist: playlist.tracks, title: playlist.name }); playTrack(0); } else { // If no tracks, create instant mix from random track or navigate to discover if (playlist.id === 'late-night-beats') { if (allTracks.length > 0) { const randomTrack = allTracks[Math.floor(Math.random() * allTracks.length)]; await playInstantMix(randomTrack, 'similar'); } } else if (playlist.id === 'mood-boosters') { // Play recent tracks with high play counts if (recentlyPlayed.length > 0) { setCurrentPlaylistSimple({ playlist: recentlyPlayed, title: 'Mood Boosters' }); playTrack(0); } } else if (playlist.id === 'feds-sweep') { // Create mix from trending if (trendingNow.length > 0) { setCurrentPlaylistSimple({ playlist: trendingNow, title: 'Feds Did A Sweep' }); playTrack(0); } } else { // Default: navigate to discover navigate('/discover'); } } }; const FeaturedPlaylistCard: React.FC<{ playlist: FeaturedPlaylist }> = ({ playlist }) => (
playPlaylist(playlist)} >

{playlist.name}

{playlist.description}

{playlist.artistName && (
{playlist.artistName}
)}
{playlist.artistImage ? ( {playlist.artistName} ) : playlist.image ? ( {playlist.name} ) : (
)}
); const VerticalTrackList: React.FC<{ tracks: MediaItem[]; title: string; icon: React.ReactNode; color: string }> = ({ tracks, title, icon, color }) => { const getClickHandler = (track: MediaItem, index: number) => { if (title === 'Made For You') { return () => handleMadeForYouTrackPlay(track, index); } else if (title === 'Trending Now') { return () => handleTrendingTrackPlay(track, index); } else { // Recently Played - just play normally return () => { setCurrentPlaylistSimple({ playlist: tracks, title }); playTrack(index); }; } }; return (
{icon}

{title}

{tracks.slice(0, 5).map((track, index) => (
{index + 1}
{track.ImageTags?.Primary || track.SpotifyImage ? ( ) : (
)}
{track.Name}
{track.Artists?.join(', ') || 'Unknown Artist'}
))}
); }; return (
{/* dev: keep reference so build doesn't tree-shake discoverData prematurely */} {discoverData && false && {Object.keys(discoverData).length}} {/* Background Images */}
{backgroundImages.map((image, index) => (
))}
{/* Header Section */}

{welcomeMessage}

Ready to discover something new?

{/* Featured Playlists Grid */}
{featuredPlaylists.map(playlist => ( ))}
{/* Quick Access Actions */}
{/* Quick Online Search */}
{ const target = e.target as HTMLInputElement if (e.key === 'Enter' && target.value.trim()) { navigate(`/online-search?q=${encodeURIComponent(target.value.trim())}`) } }} />
{/* Artist Banner removed for now (not dynamic) */} {/* Three Column Track Sections */}
} color="#667eea" /> } color="#f093fb" /> } color="#4facfe" />
); };