import React, { useState, useEffect, useRef } from 'react'; import { Song, Playlist } from '../types'; import { usersApi, getAudioUrl, UserProfile as UserProfileType, songsApi } from '../services/api'; import { useAuth } from '../context/AuthContext'; import { ArrowLeft, Play, Pause, Heart, Eye, Users, Music as MusicIcon, ChevronRight, Share2, MoreHorizontal, Edit3, X, Camera, Image as ImageIcon, Upload, Loader2 } from 'lucide-react'; import { useI18n } from '../context/I18nContext'; interface UserProfileProps { username: string; onBack: () => void; onPlaySong: (song: Song, list?: Song[]) => void; onNavigateToProfile: (username: string) => void; onNavigateToPlaylist?: (playlistId: string) => void; currentSong?: Song | null; isPlaying?: boolean; likedSongIds?: Set; onToggleLike?: (songId: string) => void; } export const UserProfile: React.FC = ({ username, onBack, onPlaySong, onNavigateToProfile, onNavigateToPlaylist, currentSong, isPlaying, likedSongIds = new Set(), onToggleLike }) => { const { t, language } = useI18n(); const { user: currentUser, token } = useAuth(); const [profileUser, setProfileUser] = useState(null); const [publicSongs, setPublicSongs] = useState([]); const [publicPlaylists, setPublicPlaylists] = useState([]); const [songsTab, setSongsTab] = useState<'recent' | 'top'>('recent'); const [loading, setLoading] = useState(true); // Edit State const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [editBio, setEditBio] = useState(''); const [editAvatarUrl, setEditAvatarUrl] = useState(''); const [editBannerUrl, setEditBannerUrl] = useState(''); const [isSaving, setIsSaving] = useState(false); const [avatarFile, setAvatarFile] = useState(null); const [bannerFile, setBannerFile] = useState(null); const [avatarPreview, setAvatarPreview] = useState(null); const [bannerPreview, setBannerPreview] = useState(null); const [uploadingAvatar, setUploadingAvatar] = useState(false); const [uploadingBanner, setUploadingBanner] = useState(false); const [avatarFailed, setAvatarFailed] = useState(false); const avatarInputRef = useRef(null); const bannerInputRef = useRef(null); useEffect(() => { loadUserProfile(); }, [username]); const loadUserProfile = async () => { setLoading(true); try { const [profileRes, songsRes, playlistsRes] = await Promise.all([ usersApi.getProfile(username, token), usersApi.getPublicSongs(username), usersApi.getPublicPlaylists(username) ]); setProfileUser(profileRes.user); setEditBio(profileRes.user.bio || ''); setEditAvatarUrl(profileRes.user.avatar_url || ''); setEditBannerUrl(profileRes.user.banner_url || ''); const transformedSongs: Song[] = songsRes.songs.map(s => ({ id: s.id, title: s.title, lyrics: s.lyrics, style: s.style, coverUrl: `https://picsum.photos/seed/${s.id}/400/400`, duration: s.duration ? `${Math.floor(s.duration / 60)}:${String(Math.floor(s.duration % 60)).padStart(2, '0')}` : '0:00', createdAt: new Date(s.created_at), tags: s.tags || [], audioUrl: getAudioUrl(s.audio_url, s.id), isPublic: true, likeCount: s.like_count || 0, viewCount: s.view_count || 0, creator: s.creator, })); setPublicSongs(transformedSongs); setPublicPlaylists(playlistsRes.playlists || []); } catch (error) { console.error('Failed to load user profile:', error); } finally { setLoading(false); } }; const handleAvatarChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { setAvatarFile(file); const reader = new FileReader(); reader.onload = (ev) => setAvatarPreview(ev.target?.result as string); reader.readAsDataURL(file); } }; const handleBannerChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { setBannerFile(file); const reader = new FileReader(); reader.onload = (ev) => setBannerPreview(ev.target?.result as string); reader.readAsDataURL(file); } }; const handleSaveProfile = async () => { if (!token) return; setIsSaving(true); try { // Upload avatar if changed if (avatarFile) { setUploadingAvatar(true); const avatarRes = await usersApi.uploadAvatar(avatarFile, token); setEditAvatarUrl(avatarRes.url); setUploadingAvatar(false); } // Upload banner if changed if (bannerFile) { setUploadingBanner(true); const bannerRes = await usersApi.uploadBanner(bannerFile, token); setEditBannerUrl(bannerRes.url); setUploadingBanner(false); } // Update bio (and any URL-based avatar/banner if not using file upload) const updates: Record = { bio: editBio }; if (!avatarFile && editAvatarUrl !== profileUser.avatar_url) { updates.avatarUrl = editAvatarUrl; } if (!bannerFile && editBannerUrl !== profileUser.banner_url) { updates.bannerUrl = editBannerUrl; } if (Object.keys(updates).length > 0) { await usersApi.updateProfile(updates, token); } setIsEditModalOpen(false); setAvatarFile(null); setBannerFile(null); setAvatarPreview(null); setBannerPreview(null); // Reload to get fresh data loadUserProfile(); } catch (error) { console.error('Failed to update profile:', error); alert(t('profileUpdateFailed')); } finally { setIsSaving(false); setUploadingAvatar(false); setUploadingBanner(false); } }; if (loading) { return (
{t('loadingProfile')}
); } if (!profileUser) { return (
{t('userNotFound')}
); } const totalLikes = publicSongs.reduce((sum, song) => sum + (song.likeCount || 0), 0); const totalPlays = publicSongs.reduce((sum, song) => sum + (song.viewCount || 0), 0); const isOwner = currentUser?.id === profileUser.id; // Generate random gradient for banner fallback const gradients = [ 'from-purple-600 via-pink-600 to-red-600', 'from-blue-600 via-purple-600 to-pink-600', 'from-green-600 via-teal-600 to-blue-600', 'from-orange-600 via-red-600 to-pink-600', 'from-indigo-600 via-purple-600 to-pink-600', ]; const bannerGradient = gradients[username.length % gradients.length]; const primaryBadge = profileUser.badges?.[0]; const badgeRing = primaryBadge?.color === 'yellow' ? 'ring-yellow-400/80 shadow-yellow-500/30' : primaryBadge?.color === 'purple' ? 'ring-purple-400/80 shadow-purple-500/30' : primaryBadge?.color === 'blue' ? 'ring-blue-400/80 shadow-blue-500/30' : primaryBadge?.color === 'teal' ? 'ring-teal-400/80 shadow-teal-500/30' : primaryBadge?.color === 'green' ? 'ring-green-400/80 shadow-green-500/30' : primaryBadge?.color === 'orange' ? 'ring-orange-400/80 shadow-orange-500/30' : primaryBadge?.color === 'pink' ? 'ring-pink-400/80 shadow-pink-500/30' : 'ring-zinc-500/50 shadow-zinc-500/20'; const paidPulse = profileUser.accountTier && profileUser.accountTier !== 'free' ? 'group-hover/avatar:animate-[wiggle_0.6s_ease-in-out] group-hover/avatar:rotate-1' : ''; const paidNameStyle = primaryBadge?.color === 'yellow' ? 'bg-gradient-to-r from-yellow-300 via-amber-300 to-orange-400 text-transparent bg-clip-text drop-shadow-[0_2px_12px_rgba(251,191,36,0.45)]' : primaryBadge?.color === 'purple' ? 'bg-gradient-to-r from-fuchsia-400 via-purple-500 to-indigo-400 text-transparent bg-clip-text drop-shadow-[0_2px_12px_rgba(168,85,247,0.45)]' : primaryBadge?.color === 'blue' ? 'bg-gradient-to-r from-sky-400 via-blue-500 to-indigo-400 text-transparent bg-clip-text drop-shadow-[0_2px_12px_rgba(59,130,246,0.45)]' : primaryBadge?.color === 'teal' ? 'bg-gradient-to-r from-teal-300 via-emerald-400 to-cyan-400 text-transparent bg-clip-text drop-shadow-[0_2px_12px_rgba(45,212,191,0.45)]' : primaryBadge?.color === 'orange' ? 'bg-gradient-to-r from-orange-300 via-amber-400 to-yellow-300 text-transparent bg-clip-text drop-shadow-[0_2px_12px_rgba(251,146,60,0.4)]' : primaryBadge?.color === 'pink' ? 'bg-gradient-to-r from-pink-400 via-rose-500 to-fuchsia-500 text-transparent bg-clip-text drop-shadow-[0_2px_12px_rgba(244,114,182,0.45)]' : ''; // Banner Style const bannerStyle = profileUser.banner_url ? { backgroundImage: `url(${profileUser.banner_url})`, backgroundSize: 'cover', backgroundPosition: 'center' } : {}; const bannerClass = profileUser.banner_url ? `h-48 md:h-64 relative overflow-hidden bg-zinc-200 dark:bg-zinc-900` : `h-48 md:h-64 bg-gradient-to-r ${bannerGradient} relative overflow-hidden`; const featuredSongs = publicSongs.slice(0, 6); const displaySongs = songsTab === 'recent' ? publicSongs : [...publicSongs].sort((a, b) => (b.likeCount || 0) - (a.likeCount || 0)); return (
{/* Hero Banner */}
{/* Background Banner */}
{!profileUser.banner_url && (
)}
{/* Back Button */} {/* Edit Banner Button (Owner Only) - Visual Cue */} {isOwner && ( )} {/* Profile Info */}
{/* Avatar */}
{profileUser.avatar_url && !avatarFailed ? ( {profileUser.username} setAvatarFailed(true)} /> ) : (
{profileUser.username[0].toUpperCase()}
)}
{isOwner && ( )}
{/* User Info */}

{profileUser.username}

{profileUser.badges && profileUser.badges.length > 0 && (
{profileUser.badges.map((badge) => { const style = badge.color === 'yellow' ? 'from-yellow-300 via-amber-300 to-orange-400 text-amber-950 shadow-[0_0_20px_rgba(251,191,36,0.45)]' : badge.color === 'purple' ? 'from-fuchsia-400 via-purple-500 to-indigo-500 text-white shadow-[0_0_20px_rgba(168,85,247,0.45)]' : badge.color === 'blue' ? 'from-sky-400 via-blue-500 to-indigo-500 text-white shadow-[0_0_18px_rgba(59,130,246,0.45)]' : badge.color === 'teal' ? 'from-teal-300 via-emerald-400 to-cyan-400 text-emerald-950 shadow-[0_0_18px_rgba(45,212,191,0.45)]' : badge.color === 'orange' ? 'from-orange-300 via-amber-400 to-yellow-300 text-amber-950 shadow-[0_0_16px_rgba(251,146,60,0.4)]' : badge.color === 'pink' ? 'from-pink-400 via-rose-500 to-fuchsia-500 text-white shadow-[0_0_18px_rgba(244,114,182,0.45)]' : badge.color === 'green' ? 'from-emerald-300 via-green-400 to-lime-300 text-emerald-950 shadow-[0_0_16px_rgba(34,197,94,0.4)]' : 'from-zinc-200 via-zinc-300 to-zinc-200 text-zinc-700 dark:from-zinc-700 dark:via-zinc-600 dark:to-zinc-700 dark:text-zinc-100'; const icon = badge.id === 'supporter' ? '🏅' : badge.id === 'patron' ? '🏆' : badge.id === 'legendary' ? '👑' : badge.id === 'diamond' ? '💎' : badge.id === 'crown' ? '👑' : badge.id === 'champion' ? '🥇' : badge.id === 'music' ? '🎵' : badge.id === 'coffee' ? '☕' : '⭐'; return ( {icon} {badge.label} ); })}
)} {profileUser.supporter_since && profileUser.accountTier && profileUser.accountTier !== 'free' && (

{t('supportingSince')} {new Date(profileUser.supporter_since).toLocaleDateString(language === 'zh' ? 'zh-CN' : 'en-US', { month: 'long', year: 'numeric' })}

)} {/* Bio */} {profileUser.bio && (

{profileUser.bio}

)}

{t('joined')} {new Date(profileUser.created_at).toLocaleDateString(language === 'zh' ? 'zh-CN' : 'en-US', { month: 'long', year: 'numeric' })}

{/* Edit Profile Button (Mobile/Desktop) */} {isOwner && ( )}
{/* Stats */}
{publicSongs.length} {t('songs')}
{totalLikes} {t('likes')}
{totalPlays} {t('plays')}
{/* Content */}
{/* Featured Songs */} {featuredSongs.length > 0 && (

{t('featuredSongs')}

{featuredSongs.map((song) => { const isCurrentSong = currentSong?.id === song.id; const isCurrentlyPlaying = isCurrentSong && isPlaying; const isLiked = likedSongIds.has(song.id); return (
onPlaySong(song, featuredSongs)} className="aspect-square rounded-lg overflow-hidden mb-2 md:mb-3 relative bg-zinc-200 dark:bg-zinc-800 cursor-pointer" > {song.title}
{isCurrentlyPlaying ? ( ) : ( )}
{isCurrentlyPlaying && (
)}

{song.title}

{song.style}

{onToggleLike && ( )}
{song.likeCount || 0} {song.viewCount || 0}
); })}
)} {/* Songs Section */}

{t('songs')}

{displaySongs.length === 0 ? (

{t('noPublicSongsYet')}

) : (
{displaySongs.map((song) => { const isCurrentSong = currentSong?.id === song.id; const isCurrentlyPlaying = isCurrentSong && isPlaying; const isLiked = likedSongIds.has(song.id); return (
onPlaySong(song, displaySongs)} className="relative w-14 h-14 md:w-16 md:h-16 flex-shrink-0 rounded-md overflow-hidden bg-zinc-200 dark:bg-zinc-800" > {song.title}
{isCurrentlyPlaying ? ( ) : ( )}
{isCurrentlyPlaying && (
)}
onPlaySong(song, displaySongs)}>

{song.title}

{song.style}

{song.likeCount || 0} {song.viewCount || 0} {song.duration}
{onToggleLike && ( )}
); })}
)}
{/* Playlists Section */} {publicPlaylists.length > 0 && (

{t('playlists')}

{publicPlaylists.map((playlist: any) => (
onNavigateToPlaylist?.(playlist.id)} className="group relative flex-shrink-0 w-36 md:w-48 cursor-pointer" >

{playlist.name}

{playlist.song_count} {t('songs')}

))}
)}
{/* Edit Profile Modal */} {isEditModalOpen && (

{t('editProfile')}

{/* Avatar Upload */}
{(avatarPreview || editAvatarUrl) ? ( (e.currentTarget.style.display = 'none')} /> ) : (
)} {uploadingAvatar && (
)}

{t('avatarFormats')}

{/* Banner Upload */}
bannerInputRef.current?.click()} className="relative w-full h-32 rounded-lg bg-zinc-100 dark:bg-zinc-800 border-2 border-zinc-300 dark:border-zinc-700 border-dashed overflow-hidden cursor-pointer hover:border-zinc-400 dark:hover:border-zinc-600 transition-colors" > {(bannerPreview || editBannerUrl) ? ( (e.currentTarget.style.display = 'none')} /> ) : (
{t('clickToUploadBanner')}
)} {uploadingBanner && (
)}

{t('bannerFormats')}

{/* Bio Input */}