import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Search, Play, Pause, Heart, ChevronRight, ChevronLeft, Copy, Check, X, Loader2 } from 'lucide-react'; import { Song, Playlist } from '../types'; import { songsApi, usersApi, playlistsApi, searchApi, UserProfile, getAudioUrl } from '../services/api'; import { useI18n } from '../context/I18nContext'; import { GENRE_KEYS } from '../data/genres'; interface SearchPageProps { onPlaySong?: (song: Song, list?: Song[]) => void; currentSong?: Song | null; isPlaying?: boolean; onNavigateToProfile?: (username: string) => void; onNavigateToSong?: (songId: string) => void; onNavigateToPlaylist?: (playlistId: string) => void; } const MAX_RESULTS = 20; interface ExtendedSong extends Song { creator_avatar?: string | null; } export const SearchPage: React.FC = ({ onPlaySong, currentSong, isPlaying, onNavigateToProfile, onNavigateToSong, onNavigateToPlaylist, }) => { const { t } = useI18n(); const [searchQuery, setSearchQuery] = useState(''); const [featuredSongs, setFeaturedSongs] = useState([]); const [featuredCreators, setFeaturedCreators] = useState>([]); const [featuredPlaylists, setFeaturedPlaylists] = useState>([]); const [searchResults, setSearchResults] = useState<{ songs: ExtendedSong[]; creators: Array; playlists: Array; } | null>(null); const [loading, setLoading] = useState(true); const [searching, setSearching] = useState(false); const [copiedTag, setCopiedTag] = useState(null); const songsScrollRef = useRef(null); const creatorsScrollRef = useRef(null); const playlistsScrollRef = useRef(null); const searchTimeoutRef = useRef(null); useEffect(() => { loadFeaturedContent(); }, []); const transformSong = (s: any): ExtendedSong => ({ ...s, id: s.id, title: s.title, lyrics: s.lyrics || '', style: s.style || s.caption || '', coverUrl: s.cover_url || s.coverUrl || `https://picsum.photos/seed/${s.id}/400/400`, duration: s.duration ? (typeof s.duration === 'string' ? s.duration : `${Math.floor(s.duration / 60)}:${String(Math.floor(s.duration % 60)).padStart(2, '0')}`) : '0:00', createdAt: new Date(s.created_at || s.createdAt), tags: s.tags || [], audioUrl: getAudioUrl(s.audio_url || s.audioUrl, s.id), isPublic: s.is_public ?? s.isPublic, likeCount: s.like_count || s.likeCount || 0, viewCount: s.view_count || s.viewCount || 0, creator: s.creator, creator_avatar: s.creator_avatar || s.creatorAvatar || null, }); // Shuffle array randomly const shuffleArray = (array: T[]): T[] => { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; }; const loadFeaturedContent = async () => { setLoading(true); try { const [songsRes, creatorsRes, playlistsRes] = await Promise.allSettled([ songsApi.getFeaturedSongs(), usersApi.getFeaturedCreators().catch(() => ({ creators: [] })), playlistsApi.getFeaturedPlaylists().catch(() => ({ playlists: [] })), ]); if (songsRes.status === 'fulfilled') { const songs = songsRes.value.songs.map(transformSong); setFeaturedSongs(shuffleArray(songs).slice(0, MAX_RESULTS)); } if (creatorsRes.status === 'fulfilled' && creatorsRes.value.creators?.length > 0) { setFeaturedCreators(creatorsRes.value.creators.slice(0, MAX_RESULTS)); } else if (songsRes.status === 'fulfilled' && songsRes.value.songs?.length > 0) { const uniqueCreators = new Map(); songsRes.value.songs.forEach((song: any) => { if (song.creator && !uniqueCreators.has(song.creator)) { uniqueCreators.set(song.creator, { id: song.user_id || song.userId || song.creator, username: song.creator, created_at: song.created_at || song.createdAt, avatar_url: song.creator_avatar || song.creatorAvatar || null, }); } }); setFeaturedCreators(Array.from(uniqueCreators.values()).slice(0, MAX_RESULTS)); } if (playlistsRes.status === 'fulfilled') { setFeaturedPlaylists((playlistsRes.value.playlists || []).slice(0, MAX_RESULTS)); } } catch (error) { console.error('Failed to load featured content:', error); } finally { setLoading(false); } }; const performSearch = useCallback(async (query: string) => { if (!query.trim()) { setSearchResults(null); return; } setSearching(true); try { const results = await searchApi.search(query); setSearchResults({ songs: (results.songs || []).slice(0, MAX_RESULTS).map(transformSong), creators: (results.creators || []).slice(0, MAX_RESULTS), playlists: (results.playlists || []).slice(0, MAX_RESULTS), }); } catch (error) { console.error('Search failed:', error); setSearchResults({ songs: [], creators: [], playlists: [] }); } finally { setSearching(false); } }, []); const handleSearchChange = (value: string) => { setSearchQuery(value); if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } if (!value.trim()) { setSearchResults(null); return; } searchTimeoutRef.current = setTimeout(() => { performSearch(value); }, 300); }; const handleGenreClick = (genre: string) => { setSearchQuery(genre); performSearch(genre); }; const handleCopyTag = (tag: string) => { navigator.clipboard.writeText(tag); setCopiedTag(tag); setTimeout(() => setCopiedTag(null), 2000); }; const formatNumber = (count: number | undefined): string => { if (!count) return '0'; if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`; if (count >= 1000) return `${(count / 1000).toFixed(1)}K`; return count.toString(); }; const scroll = (ref: React.RefObject, direction: 'left' | 'right') => { if (ref.current) { const scrollAmount = 400; ref.current.scrollBy({ left: direction === 'left' ? -scrollAmount : scrollAmount, behavior: 'smooth' }); } }; const displaySongs = searchResults?.songs || featuredSongs; const displayCreators = searchResults?.creators || featuredCreators; const displayPlaylists = searchResults?.playlists || featuredPlaylists; const isSearching = searchQuery.trim().length > 0; return (
{/* Search Input */}
handleSearchChange(e.target.value)} className="w-full h-11 pl-12 pr-12 bg-white dark:bg-zinc-900/80 border border-zinc-200 dark:border-white/10 rounded-full text-zinc-900 dark:text-white placeholder-zinc-400 focus:outline-none focus:border-pink-500 dark:focus:border-pink-500 focus:ring-2 focus:ring-pink-500/20 transition-all" /> {searching ? ( ) : searchQuery && ( )}
{/* Songs Section */}

{isSearching ? `${t('songsMatching')} "${searchQuery}"` : t('featuredSongs')} {isSearching && displaySongs.length > 0 && ( ({displaySongs.length}) )}

{!isSearching && displaySongs.length > 4 && ( )}
{loading ? (
{Array.from({ length: 8 }).map((_, i) => (
))}
) : displaySongs.length > 0 ? (
{displaySongs.slice(0, isSearching ? MAX_RESULTS : 8).map((song) => ( onPlaySong?.(song, displaySongs)} onNavigateToProfile={onNavigateToProfile} onCopyTag={handleCopyTag} copiedTag={copiedTag} formatNumber={formatNumber} /> ))}
) : isSearching ? (
{t('noSongsFound')} "{searchQuery}"
) : null}
{/* Creators Section */}

{isSearching ? `${t('creatorsMatching')} "${searchQuery}"` : t('featuredCreators')} {isSearching && displayCreators.length > 0 && ( ({displayCreators.length}) )}

{!isSearching && displayCreators.length > 6 && (
)}
{loading ? (
{Array.from({ length: 8 }).map((_, i) => (
))}
) : displayCreators.length > 0 ? (
{displayCreators.map((creator) => ( ))}
) : (
{isSearching ? `${t('noCreatorsFound')} "${searchQuery}"` : t('noCreatorsYet')}
)}
{/* Playlists Section */}

{isSearching ? `${t('playlistsMatching')} "${searchQuery}"` : t('featuredPlaylists')} {isSearching && displayPlaylists.length > 0 && ( ({displayPlaylists.length}) )}

{!isSearching && displayPlaylists.length > 5 && (
)}
{loading ? (
{Array.from({ length: 6 }).map((_, i) => (
))}
) : displayPlaylists.length > 0 ? (
{displayPlaylists.map((playlist) => ( ))}
) : (
{isSearching ? `${t('noPlaylistsFound')} "${searchQuery}"` : t('noPlaylistsYet')}
)}
{/* Genres */}

{t('genres')}

{GENRE_KEYS.map((genreKey) => { const genreLabel = t(genreKey); return ( ); })}
); }; interface FeaturedSongCardProps { song: ExtendedSong; isPlaying?: boolean; onPlay: () => void; onNavigateToProfile?: (username: string) => void; onCopyTag: (tag: string) => void; copiedTag: string | null; formatNumber: (n: number | undefined) => string; } const FeaturedSongCard: React.FC = ({ song, isPlaying, onPlay, onNavigateToProfile, onCopyTag, copiedTag, formatNumber, }) => { const [isHovered, setIsHovered] = useState(false); const tags = song.style?.split(',').map(t => t.trim()).filter(Boolean).slice(0, 2) || []; return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} >
{song.title}
{isPlaying ? ( ) : ( )}
{song.title} {song.isPublic !== false && ( v5 )}
{tags.map((tag, i) => ( ))}
{song.creator && ( )} {formatNumber(song.viewCount)} {formatNumber(song.likeCount)}
); }; interface CreatorCardProps { creator: UserProfile & { song_count?: number }; onNavigateToProfile?: (username: string) => void; } const CreatorCard: React.FC = ({ creator, onNavigateToProfile, }) => { return (
onNavigateToProfile?.(creator.username)} >
{creator.username}
{creator.username}
@{creator.username.toLowerCase().replace(/\s/g, '')}
); }; interface PlaylistCardProps { playlist: Playlist & { creator?: string; creator_avatar?: string; song_count?: number }; onNavigateToPlaylist?: (playlistId: string) => void; onNavigateToProfile?: (username: string) => void; t: (key: string) => string; } const PlaylistCard: React.FC = ({ playlist, onNavigateToPlaylist, onNavigateToProfile, t, }) => { return (
onNavigateToPlaylist?.(playlist.id)} >
{playlist.name}
{playlist.name}
{playlist.song_count || 0} {t('songs')}
{playlist.creator && (
{ e.stopPropagation(); onNavigateToProfile?.(playlist.creator!); }} >
{playlist.creator}
{playlist.creator}
)}
); };