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