Changeset 947fcaa for frontend/src/components/Sidebar.tsx
- Timestamp:
- 02/21/26 19:19:51 (5 months ago)
- Branches:
- main
- Parents:
- 765e166
- File:
-
- 1 edited
-
frontend/src/components/Sidebar.tsx (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
frontend/src/components/Sidebar.tsx
r765e166 r947fcaa 5 5 import { useAuth } from "../context/authContext"; 6 6 import { usePlayer } from "../context/playerContext"; 7 import { useCreatedPlaylists } from "../context/playlistContext"; 8 import { getErrorMessage } from "../utils/error"; 7 9 import type { BasicSong, SidebarProps } from "../utils/types"; 8 10 import { toEmbedUrl } from "../utils/utils"; 9 import { useCreatedPlaylists } from "../context/playlistContext";10 11 import CreatePlaylistModal from "./playlist/CreatePlaylistModal"; 11 import { getErrorMessage } from "../utils/error";12 12 13 13 const Sidebar = ({ isOpen, onClose }: SidebarProps) => { 14 const { user } = useAuth();15 const {16 createdPlaylists,17 isLoading: playlistsLoading,18 refreshPlaylists,19 } = useCreatedPlaylists();20 const navigate = useNavigate();21 const { play, currentSong } = usePlayer();22 const [recentlyListened, setRecentlyListened] = useState<BasicSong[]>([]);23 const [songsLoading, setSongsLoading] = useState(false);24 const [isModalOpen, setIsModalOpen] = useState(false);25 26 useEffect(() => {27 const fetchData = async () => {28 setSongsLoading(true);29 try {30 const data = await axiosInstance.get<BasicSong[]>("/songs/recent");31 setRecentlyListened(data.data);32 } catch (error) {33 toast.error(getErrorMessage(error));34 } finally {35 setSongsLoading(false);36 }37 };38 if (user) {39 fetchData();40 } else {41 setRecentlyListened([]);42 setSongsLoading(false);43 }44 }, [user]);45 46 const isLoading = songsLoading || playlistsLoading;47 48 return (49 <>50 <div51 className={`fixed left-0 top-0 h-full bg-[#121212] border-r border-white/10 transition-transform duration-300 ease-in-out z-40 ${52 isOpen ? "translate-x-0" : "-translate-x-full"53 } w-64 overflow-y-auto`}54 >55 <div className="p-6">56 {/* Sidebar Header */}57 <div className="flex justify-between items-center mb-6 pt-2">58 <h2 className="text-xl font-bold text-white">Library</h2>59 <button60 onClick={onClose}61 className="text-gray-400 hover:text-white transition-colors"62 aria-label="Close sidebar"63 >64 <svg65 className="w-6 h-6"66 fill="none"67 stroke="currentColor"68 viewBox="0 0 24 24"69 >70 <path71 strokeLinecap="round"72 strokeLinejoin="round"73 strokeWidth={2}74 d="M6 18L18 6M6 6l12 12"75 />76 </svg>77 </button>78 </div>79 80 {/* Loading State */}81 {isLoading ? (82 <div className="flex items-center justify-center py-16">83 <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#1db954]"></div>84 </div>85 ) : (86 <>87 {/* Recently Listened */}88 <div className="mb-8">89 <h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4">90 Recently Played91 </h3>92 <div className="space-y-3">93 {recentlyListened.map((song) => (94 <div95 key={song.id}96 className="flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 cursor-pointer transition-colors group relative"97 >98 <img99 src={100 song.cover101 ? `${baseURL}/${song.cover}`102 : "/favicon.png"103 }104 alt={song.title}105 className="w-10 h-10 rounded object-cover"106 onError={(e) => {107 (e.target as HTMLImageElement).src = "/favicon.png";108 }}109 />110 <div className="flex-1 min-w-0">111 <p112 onClick={() => navigate(`/songs/${song.id}`)}113 className="text-sm font-medium text-white truncate hover:underline cursor-pointer"114 >115 {song.title}116 </p>117 <div className="flex items-center gap-1 text-xs text-gray-400">118 <span119 onClick={(e) => {120 e.stopPropagation();121 if (song.artistUsername) {122 navigate(`/users/${song.artistUsername}`);123 }124 }}125 className={`truncate ${126 song.artistUsername127 ? "hover:underline cursor-pointer hover:text-white"128 : ""129 }`}130 >131 {song.artist}132 </span>133 {song.album && (134 <>135 <span>•</span>136 <span137 onClick={(e) => {138 e.stopPropagation();139 if (song.albumId) {140 navigate(141 `/collection/album/${song.albumId}`,142 );143 }144 }}145 className={`truncate ${146 song.albumId147 ? "hover:underline cursor-pointer hover:text-white"148 : ""149 }`}150 >151 {song.album}152 </span>153 </>154 )}155 </div>156 </div>157 {song.link && (158 <button159 onClick={(e) => {160 e.stopPropagation();161 play({162 id: song.id,163 title: song.title,164 artist: song.artist,165 cover: song.cover,166 embedUrl: toEmbedUrl(song.link!),167 });168 }}169 className={`p-1.5 rounded-full transition-all opacity-0 group-hover:opacity-100${170 currentSong?.id === song.id171 ? "bg-white text-[#1db954]"172 : "bg-[#1db954] text-black hover:scale-110"173 }`}174 aria-label={175 currentSong?.id === song.id176 ? "Now playing"177 : "Play song"178 }179 >180 {currentSong?.id === song.id ? (181 <svg182 className="w-4 h-4"183 fill="currentColor"184 viewBox="0 0 24 24"185 >186 <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />187 </svg>188 ) : (189 <svg190 className="w-4 h-4"191 fill="currentColor"192 viewBox="0 0 24 24"193 >194 <path d="M8 5v14l11-7z" />195 </svg>196 )}197 </button>198 )}199 </div>200 ))}201 </div>202 </div>203 204 {/* Playlists */}205 <div>206 <h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4">207 Your Playlists208 </h3>209 <div className="space-y-2">210 {createdPlaylists?.map((playlist) => (211 <div212 key={playlist.id}213 onClick={() => {214 navigate(`/collection/playlist/${playlist.id}`);215 }}216 className="flex items-center justify-between p-3 rounded-lg hover:bg-white/5 cursor-pointer transition-colors"217 >218 <div className="flex items-center gap-3">219 <div className="w-10 h-10 bg-linear-to-br from-purple-500 to-pink-500 rounded flex items-center justify-center">220 <svg221 className="w-5 h-5 text-white"222 fill="currentColor"223 viewBox="0 0 20 20"224 >225 <path d="M18 3a1 1 0 00-1.196-.98l-10 2A1 1 0 006 5v9.114A4.369 4.369 0 005 14c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V7.82l8-1.6v5.894A4.37 4.37 0 0015 12c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V3z" />226 </svg>227 </div>228 <div>229 <p className="text-sm font-medium text-white">230 {playlist.name}231 </p>232 <p className="text-xs text-gray-400">233 {playlist.songCount} songs234 </p>235 </div>236 </div>237 </div>238 ))}239 240 {/* Create Playlist Button */}241 <button242 onClick={() => setIsModalOpen(true)}243 className="w-full flex items-center gap-3 p-3 rounded-lg hover:bg-white/5 cursor-pointer transition-colors group mt-2"244 >245 <div className="w-10 h-10 bg-[#282828] group-hover:bg-[#3a3a3a] rounded flex items-center justify-center transition-colors">246 <svg247 className="w-5 h-5 text-gray-400 group-hover:text-white transition-colors"248 fill="none"249 stroke="currentColor"250 viewBox="0 0 24 24"251 >252 <path253 strokeLinecap="round"254 strokeLinejoin="round"255 strokeWidth={2}256 d="M12 4v16m8-8H4"257 />258 </svg>259 </div>260 <p className="text-sm font-medium text-gray-400 group-hover:text-white transition-colors">261 Create Playlist262 </p>263 </button>264 </div>265 </div>266 </>267 )}268 </div>269 </div>270 271 <CreatePlaylistModal272 isOpen={isModalOpen}273 onClose={() => setIsModalOpen(false)}274 onSuccess={() => refreshPlaylists(false)}275 songId={null}276 />277 </>278 );14 const { user } = useAuth(); 15 const { 16 createdPlaylists, 17 isLoading: playlistsLoading, 18 refreshPlaylists, 19 } = useCreatedPlaylists(); 20 const navigate = useNavigate(); 21 const { play, currentSong } = usePlayer(); 22 const [recentlyListened, setRecentlyListened] = useState<BasicSong[]>([]); 23 const [songsLoading, setSongsLoading] = useState(false); 24 const [isModalOpen, setIsModalOpen] = useState(false); 25 26 useEffect(() => { 27 const fetchData = async () => { 28 setSongsLoading(true); 29 try { 30 const data = await axiosInstance.get<BasicSong[]>("/songs/recent"); 31 setRecentlyListened(data.data); 32 } catch (error) { 33 toast.error(getErrorMessage(error)); 34 } finally { 35 setSongsLoading(false); 36 } 37 }; 38 if (user) { 39 fetchData(); 40 } else { 41 setRecentlyListened([]); 42 setSongsLoading(false); 43 } 44 }, [user]); 45 46 const isLoading = songsLoading || playlistsLoading; 47 48 return ( 49 <> 50 <div 51 className={`fixed left-0 top-0 h-full bg-[#121212] border-r border-white/10 transition-transform duration-300 ease-in-out z-40 ${ 52 isOpen ? "translate-x-0" : "-translate-x-full" 53 } w-64 overflow-y-auto`} 54 > 55 <div className="p-6"> 56 {/* Sidebar Header */} 57 <div className="flex justify-between items-center mb-6 pt-2"> 58 <h2 className="text-xl font-bold text-white">Library</h2> 59 <button 60 onClick={onClose} 61 className="text-gray-400 hover:text-white transition-colors cursor-pointer" 62 aria-label="Close sidebar" 63 > 64 <svg 65 className="w-6 h-6" 66 fill="none" 67 stroke="currentColor" 68 viewBox="0 0 24 24" 69 > 70 <path 71 strokeLinecap="round" 72 strokeLinejoin="round" 73 strokeWidth={2} 74 d="M6 18L18 6M6 6l12 12" 75 /> 76 </svg> 77 </button> 78 </div> 79 80 {/* Loading State */} 81 {isLoading ? ( 82 <div className="flex items-center justify-center py-16"> 83 <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#1db954]"></div> 84 </div> 85 ) : ( 86 <> 87 {/* Recently Listened */} 88 <div className="mb-8"> 89 <h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4"> 90 Recently Played 91 </h3> 92 <div className="space-y-3"> 93 {recentlyListened.map((song) => ( 94 <div 95 key={song.id} 96 className="flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 cursor-pointer transition-colors group relative" 97 > 98 <img 99 src={ 100 song.cover 101 ? `${baseURL}/${song.cover}` 102 : "/favicon.png" 103 } 104 alt={song.title} 105 className="w-10 h-10 rounded object-cover" 106 onError={(e) => { 107 (e.target as HTMLImageElement).src = "/favicon.png"; 108 }} 109 /> 110 <div className="flex-1 min-w-0"> 111 <p 112 onClick={() => navigate(`/songs/${song.id}`)} 113 className="text-sm font-medium text-white truncate hover:underline cursor-pointer" 114 > 115 {song.title} 116 </p> 117 <div className="flex items-center gap-1 text-xs text-gray-400"> 118 <span 119 onClick={(e) => { 120 e.stopPropagation(); 121 if (song.artistUsername) { 122 navigate(`/users/${song.artistUsername}`); 123 } 124 }} 125 className={`truncate ${ 126 song.artistUsername 127 ? "hover:underline cursor-pointer hover:text-white" 128 : "" 129 }`} 130 > 131 {song.artist} 132 </span> 133 {song.album && ( 134 <> 135 <span>•</span> 136 <span 137 onClick={(e) => { 138 e.stopPropagation(); 139 if (song.albumId) { 140 navigate( 141 `/collection/album/${song.albumId}`, 142 ); 143 } 144 }} 145 className={`truncate ${ 146 song.albumId 147 ? "hover:underline cursor-pointer hover:text-white" 148 : "" 149 }`} 150 > 151 {song.album} 152 </span> 153 </> 154 )} 155 </div> 156 </div> 157 {song.link && ( 158 <button 159 onClick={(e) => { 160 e.stopPropagation(); 161 play({ 162 id: song.id, 163 title: song.title, 164 artist: song.artist, 165 cover: song.cover, 166 embedUrl: toEmbedUrl(song.link!), 167 }); 168 }} 169 className={`p-1.5 rounded-full transition-all opacity-0 group-hover:opacity-100 ${ 170 currentSong?.id === song.id 171 ? "bg-white text-[#1db954]" 172 : "bg-[#1db954] text-black hover:scale-110 cursor-pointer" 173 }`} 174 aria-label={ 175 currentSong?.id === song.id 176 ? "Now playing" 177 : "Play song" 178 } 179 > 180 {currentSong?.id === song.id ? ( 181 <svg 182 className="w-4 h-4" 183 fill="currentColor" 184 viewBox="0 0 24 24" 185 > 186 <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" /> 187 </svg> 188 ) : ( 189 <svg 190 className="w-4 h-4" 191 fill="currentColor" 192 viewBox="0 0 24 24" 193 > 194 <path d="M8 5v14l11-7z" /> 195 </svg> 196 )} 197 </button> 198 )} 199 </div> 200 ))} 201 </div> 202 </div> 203 204 {/* Playlists */} 205 <div> 206 <h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4"> 207 Your Playlists 208 </h3> 209 <div className="space-y-2"> 210 {createdPlaylists?.map((playlist) => ( 211 <div 212 key={playlist.id} 213 onClick={() => { 214 navigate(`/collection/playlist/${playlist.id}`); 215 }} 216 className="flex items-center justify-between p-3 rounded-lg hover:bg-white/5 cursor-pointer transition-colors" 217 > 218 <div className="flex items-center gap-3"> 219 <div className="w-10 h-10 bg-linear-to-br from-purple-500 to-pink-500 rounded flex items-center justify-center"> 220 <svg 221 className="w-5 h-5 text-white" 222 fill="currentColor" 223 viewBox="0 0 20 20" 224 > 225 <path d="M18 3a1 1 0 00-1.196-.98l-10 2A1 1 0 006 5v9.114A4.369 4.369 0 005 14c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V7.82l8-1.6v5.894A4.37 4.37 0 0015 12c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V3z" /> 226 </svg> 227 </div> 228 <div> 229 <p className="text-sm font-medium text-white"> 230 {playlist.name} 231 </p> 232 <p className="text-xs text-gray-400"> 233 {playlist.songCount} songs 234 </p> 235 </div> 236 </div> 237 </div> 238 ))} 239 240 {/* Create Playlist Button */} 241 <button 242 onClick={() => setIsModalOpen(true)} 243 className="w-full flex items-center gap-3 p-3 rounded-lg hover:bg-white/5 cursor-pointer transition-colors group mt-2" 244 > 245 <div className="w-10 h-10 bg-[#282828] group-hover:bg-[#3a3a3a] rounded flex items-center justify-center transition-colors"> 246 <svg 247 className="w-5 h-5 text-gray-400 group-hover:text-white transition-colors" 248 fill="none" 249 stroke="currentColor" 250 viewBox="0 0 24 24" 251 > 252 <path 253 strokeLinecap="round" 254 strokeLinejoin="round" 255 strokeWidth={2} 256 d="M12 4v16m8-8H4" 257 /> 258 </svg> 259 </div> 260 <p className="text-sm font-medium text-gray-400 group-hover:text-white transition-colors"> 261 Create Playlist 262 </p> 263 </button> 264 </div> 265 </div> 266 </> 267 )} 268 </div> 269 </div> 270 271 <CreatePlaylistModal 272 isOpen={isModalOpen} 273 onClose={() => setIsModalOpen(false)} 274 onSuccess={() => refreshPlaylists(false)} 275 songId={null} 276 /> 277 </> 278 ); 279 279 }; 280 280
Note:
See TracChangeset
for help on using the changeset viewer.
