Changeset 85512ff for frontend/src/components/userProfile
- Timestamp:
- 02/14/26 18:21:18 (5 months ago)
- Branches:
- main
- Children:
- ce45c7a
- Parents:
- f5bc95e
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
frontend/src/components/userProfile/ListenerView.tsx
rf5bc95e r85512ff 1 1 import { Album, Bookmark, Heart, ListMusic, Music } from "lucide-react"; 2 2 import { useState } from "react"; 3 import { useNavigate } from "react-router-dom";3 import { useNavigate, useParams } from "react-router-dom"; 4 4 import { toast } from "react-toastify"; 5 5 import axiosInstance, { baseURL } from "../../api/axiosInstance"; 6 6 import type { MusicalEntity, Playlist } from "../../utils/types"; 7 7 import SongItem from "../SongItem"; 8 import { useAuth } from "../../context/authContext"; 8 9 9 10 interface ListenerViewProps { 10 likedEntities: MusicalEntity[] | [];11 createdPlaylists: Playlist[] | [];12 savedPlaylists: Playlist[] | [];11 likedEntities: MusicalEntity[] | []; 12 createdPlaylists: Playlist[] | []; 13 savedPlaylists: Playlist[] | []; 13 14 } 14 15 15 16 const ListenerView = ({ 16 likedEntities,17 createdPlaylists,18 savedPlaylists,17 likedEntities, 18 createdPlaylists, 19 savedPlaylists, 19 20 }: ListenerViewProps) => { 20 const navigate = useNavigate(); 21 const [items, setItems] = useState(likedEntities); 22 const [savedItems, setSavedItems] = useState(savedPlaylists); 23 24 const likedSongs = items.filter((e) => e.type === "SONG"); 25 const likedAlbums = items.filter((e) => e.type === "ALBUM"); 26 27 const handleSavePlaylist = async ( 28 e: React.MouseEvent, 29 playlistId: number, 30 playlistName: string, 31 ) => { 32 e.stopPropagation(); 33 34 try { 35 const response = await axiosInstance.post(`/playlist/${playlistId}/save`); 36 const data = response.data; 37 38 setSavedItems((prevItems) => 39 data.isSaved 40 ? [...prevItems, prevItems.find((p) => p.id === playlistId)!] 41 : prevItems.filter((p) => p.id !== playlistId), 42 ); 43 44 toast.success( 45 data.isSaved 46 ? `Saved "${playlistName}" to your library` 47 : `Removed "${playlistName}" from your library`, 48 ); 49 } catch (err: any) { 50 toast.error(err.response?.data?.error || "Failed to save the playlist"); 51 } 52 }; 53 54 const handleLike = async (id: number, title: string) => { 55 try { 56 const response = await axiosInstance.post(`/musical-entity/${id}/like`); 57 const data = response.data; 58 59 setItems((prevItems) => 60 prevItems.map((item) => 61 item.id === data.entityId 62 ? { ...item, isLikedByCurrentUser: data.isLiked } 63 : item, 64 ), 65 ); 66 67 toast.success( 68 data.isLiked ? `Liked "${title}"` : `Removed "${title}" from likes`, 69 ); 70 } catch (err: any) { 71 toast.error(err.response?.data?.error || "Failed to like the item"); 72 } 73 }; 74 75 return ( 76 <div className="mt-8 space-y-12"> 77 {createdPlaylists && createdPlaylists.length > 0 && ( 78 <section> 79 <div className="flex items-center gap-3 mb-6"> 80 <ListMusic className="w-6 h-6 text-[#1db954]" /> 81 <h3 className="text-2xl font-bold text-white">Created Playlists</h3> 82 <span className="text-sm text-gray-500"> 83 ({createdPlaylists.length}) 84 </span> 85 </div> 86 87 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> 88 {createdPlaylists.map((playlist) => ( 89 <div 90 key={playlist.id} 91 className="group cursor-pointer" 92 onClick={() => navigate(`/collection/playlist/${playlist.id}`)} 93 > 94 <div className="relative aspect-square rounded-lg overflow-hidden bg-[#181818] mb-3 shadow-md group-hover:shadow-xl transition-all"> 95 <img 96 src={ 97 playlist.cover 98 ? `${baseURL}/${playlist.cover}` 99 : "/favicon.png" 100 } 101 alt={playlist.name} 102 className="w-full h-full object-cover" 103 onError={(e) => { 104 (e.target as HTMLImageElement).src = "/favicon.png"; 105 }} 106 /> 107 108 <button 109 onClick={(e) => 110 handleSavePlaylist(e, playlist.id, playlist.name) 111 } 112 className="absolute top-2 right-2 p-2 bg-black/70 hover:bg-black/90 rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100" 113 title={playlist.isSavedByCurrentUser ? "Unsave" : "Save"} 114 > 115 <Bookmark 116 className={`w-5 h-5 ${ 117 playlist.isSavedByCurrentUser 118 ? "fill-[#1db954] text-[#1db954]" 119 : "text-gray-400" 120 }`} 121 /> 122 </button> 123 </div> 124 <p className="text-sm font-semibold text-white truncate group-hover:text-[#1db954] transition-colors"> 125 {playlist.name} 126 </p> 127 <p className="text-xs text-gray-400 truncate"> 128 {playlist.creatorName} 129 </p> 130 </div> 131 ))} 132 </div> 133 </section> 134 )} 135 136 {savedItems && savedItems.length > 0 && ( 137 <section> 138 <div className="flex items-center gap-3 mb-6"> 139 <Bookmark className="w-6 h-6 text-[#1db954] fill-[#1db954]" /> 140 <h3 className="text-2xl font-bold text-white">Saved Playlists</h3> 141 <span className="text-sm text-gray-500">({savedItems.length})</span> 142 </div> 143 144 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> 145 {savedItems.map((playlist) => ( 146 <div 147 key={playlist.id} 148 className="group cursor-pointer" 149 onClick={() => navigate(`/collection/playlist/${playlist.id}`)} 150 > 151 <div className="relative aspect-square rounded-lg overflow-hidden bg-[#181818] mb-3 shadow-md group-hover:shadow-xl transition-all"> 152 <img 153 src={ 154 playlist.cover 155 ? `${baseURL}/${playlist.cover}` 156 : "/favicon.png" 157 } 158 alt={playlist.name} 159 className="w-full h-full object-cover" 160 onError={(e) => { 161 (e.target as HTMLImageElement).src = "/favicon.png"; 162 }} 163 /> 164 165 <button 166 onClick={(e) => 167 handleSavePlaylist(e, playlist.id, playlist.name) 168 } 169 className="absolute top-2 right-2 p-2 bg-black/70 hover:bg-black/90 rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100" 170 title="Unsave" 171 > 172 <Bookmark className="w-5 h-5 fill-[#1db954] text-[#1db954]" /> 173 </button> 174 </div> 175 <p className="text-sm font-semibold text-white truncate group-hover:text-[#1db954] transition-colors"> 176 {playlist.name} 177 </p> 178 <p className="text-xs text-gray-400 truncate"> 179 {playlist.creatorName} 180 </p> 181 </div> 182 ))} 183 </div> 184 </section> 185 )} 186 187 {likedSongs && likedSongs.length > 0 && ( 188 <section> 189 <div className="flex items-center gap-3 mb-6"> 190 <Heart className="w-6 h-6 text-red-500 fill-red-500" /> 191 <h3 className="text-2xl font-bold text-white">Liked Songs</h3> 192 <span className="text-sm text-gray-500"> 193 ({likedSongs?.length}) 194 </span> 195 </div> 196 197 <div className="space-y-1"> 198 {likedSongs.map((song, index) => ( 199 <SongItem 200 key={song.id} 201 song={{ 202 id: song.id, 203 title: song.title, 204 cover: song.cover 205 ? `${baseURL}/${song.cover}` 206 : "/favicon.png", 207 genre: song.genre, 208 link: (song as any).link, 209 releasedBy: song.releasedBy, 210 isLikedByCurrentUser: song.isLikedByCurrentUser, 211 }} 212 index={index + 1} 213 onLikeToggle={() => handleLike(song.id, song.title)} 214 /> 215 ))} 216 </div> 217 </section> 218 )} 219 220 {likedAlbums && likedAlbums.length > 0 && ( 221 <section> 222 <div className="flex items-center gap-3 mb-6"> 223 <Album className="w-6 h-6 text-[#1db954]" /> 224 <h3 className="text-2xl font-bold text-white">Liked Albums</h3> 225 <span className="text-sm text-gray-500"> 226 ({likedAlbums.length}) 227 </span> 228 </div> 229 230 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> 231 {likedAlbums.map((album) => ( 232 <div 233 key={album.id} 234 className="group cursor-pointer" 235 onClick={() => navigate(`/collection/album/${album.id}`)} 236 > 237 <div className="relative aspect-square rounded-lg overflow-hidden bg-[#181818] mb-3 shadow-md group-hover:shadow-xl transition-all"> 238 <img 239 src={ 240 album.cover ? `${baseURL}/${album.cover}` : "/favicon.png" 241 } 242 alt={album.title} 243 className="w-full h-full object-cover" 244 onError={(e) => { 245 (e.target as HTMLImageElement).src = "/favicon.png"; 246 }} 247 /> 248 249 <button 250 onClick={(e) => { 251 e.stopPropagation(); 252 handleLike(album.id, album.title); 253 }} 254 className="absolute top-2 right-2 p-2 bg-black/70 hover:bg-black/90 rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100 cursor-pointer" 255 title={album.isLikedByCurrentUser ? "Unlike" : "Like"} 256 > 257 <svg 258 className="w-5 h-5" 259 fill={album.isLikedByCurrentUser ? "#ef4444" : "none"} 260 stroke={ 261 album.isLikedByCurrentUser ? "#ef4444" : "#9ca3af" 262 } 263 strokeWidth="2" 264 viewBox="0 0 24 24" 265 > 266 <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" /> 267 </svg> 268 </button> 269 </div> 270 <p className="font-semibold text-sm text-white truncate group-hover:text-[#1db954] transition-colors"> 271 {album.title} 272 </p> 273 <p className="text-xs text-gray-400 truncate mt-1"> 274 {album.releasedBy} 275 </p> 276 </div> 277 ))} 278 </div> 279 </section> 280 )} 281 282 {likedEntities && 283 likedEntities.length === 0 && 284 (!createdPlaylists || createdPlaylists.length === 0) && 285 (!savedItems || savedItems.length === 0) && ( 286 <div className="flex flex-col items-center justify-center py-16 text-gray-500"> 287 <Music className="w-20 h-20 mb-4 opacity-20" /> 288 <p className="text-lg font-medium">Nothing here yet</p> 289 <p className="text-sm mt-2"> 290 Start exploring music to build your collection 291 </p> 292 </div> 293 )} 294 </div> 295 ); 21 const navigate = useNavigate(); 22 const { username: usernameParam } = useParams(); 23 const { user: currentUser } = useAuth(); 24 const [items, setItems] = useState(likedEntities); 25 const [createdItems, setCreatedItems] = useState(createdPlaylists); 26 const [savedItems, setSavedItems] = useState(savedPlaylists); 27 28 const likedSongs = items.filter((e) => e.type === "SONG"); 29 const likedAlbums = items.filter((e) => e.type === "ALBUM"); 30 const username = usernameParam || currentUser?.username; 31 const isOwnProfile = currentUser?.username === username; 32 33 const handleSavePlaylist = async ( 34 e: React.MouseEvent, 35 playlistId: number, 36 playlistName: string, 37 ) => { 38 e.stopPropagation(); 39 40 try { 41 const response = await axiosInstance.post( 42 `/playlists/${playlistId}/save`, 43 ); 44 const data = response.data; 45 46 setCreatedItems((prevItems) => 47 prevItems.map((p) => 48 p.id === playlistId 49 ? { ...p, isSavedByCurrentUser: data.isSavedByCurrentUser } 50 : p, 51 ), 52 ); 53 54 setSavedItems((prevItems) => 55 prevItems.map((p) => 56 p.id === playlistId 57 ? { ...p, isSavedByCurrentUser: data.isSavedByCurrentUser } 58 : p, 59 ), 60 ); 61 62 if (isOwnProfile) { 63 if (data.isSavedByCurrentUser) { 64 const playlistToAdd = createdItems.find((p) => p.id === playlistId); 65 if (playlistToAdd && !savedItems.find((p) => p.id === playlistId)) { 66 setSavedItems((prevItems) => [ 67 ...prevItems, 68 { ...playlistToAdd, isSavedByCurrentUser: true }, 69 ]); 70 } 71 } else { 72 setSavedItems((prevItems) => 73 prevItems.filter((p) => p.id !== playlistId), 74 ); 75 } 76 } 77 78 toast.success( 79 data.isSavedByCurrentUser 80 ? `Saved "${playlistName}" to your library` 81 : `Removed "${playlistName}" from your library`, 82 ); 83 } catch (err: any) { 84 toast.error(err.response?.data?.error || "Failed to save the playlist"); 85 } 86 }; 87 88 const handleLike = async (id: number, title: string) => { 89 try { 90 const response = await axiosInstance.post(`/musical-entity/${id}/like`); 91 const data = response.data; 92 93 setItems((prevItems) => 94 prevItems.map((item) => 95 item.id === data.entityId 96 ? { ...item, isLikedByCurrentUser: data.isLiked } 97 : item, 98 ), 99 ); 100 101 toast.success( 102 data.isLiked ? `Liked "${title}"` : `Removed "${title}" from likes`, 103 ); 104 } catch (err: any) { 105 toast.error(err.response?.data?.error || "Failed to like the item"); 106 } 107 }; 108 109 return ( 110 <div className="mt-8 space-y-12"> 111 {createdItems && createdItems.length > 0 && ( 112 <section> 113 <div className="flex items-center gap-3 mb-6"> 114 <ListMusic className="w-6 h-6 text-[#1db954]" /> 115 <h3 className="text-2xl font-bold text-white">Created Playlists</h3> 116 <span className="text-sm text-gray-500"> 117 ({createdItems.length}) 118 </span> 119 </div> 120 121 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> 122 {createdItems.map((playlist) => ( 123 <div 124 key={playlist.id} 125 className="group cursor-pointer" 126 onClick={() => navigate(`/collection/playlist/${playlist.id}`)} 127 > 128 <div className="relative aspect-square rounded-lg overflow-hidden bg-[#181818] mb-3 shadow-md group-hover:shadow-xl transition-all"> 129 <img 130 src={ 131 playlist.cover 132 ? `${baseURL}/${playlist.cover}` 133 : "/favicon.png" 134 } 135 alt={playlist.name} 136 className="w-full h-full object-cover" 137 onError={(e) => { 138 (e.target as HTMLImageElement).src = "/favicon.png"; 139 }} 140 /> 141 {!isOwnProfile && 142 currentUser?.username != playlist.creatorUsername && ( 143 <button 144 onClick={(e) => 145 handleSavePlaylist(e, playlist.id, playlist.name) 146 } 147 className="absolute top-2 right-2 p-2 bg-black/70 hover:bg-black/90 rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100 cursor-pointer" 148 title={ 149 playlist.isSavedByCurrentUser ? "Unsave" : "Save" 150 } 151 > 152 <Bookmark 153 className={`w-5 h-5 ${ 154 playlist.isSavedByCurrentUser 155 ? "fill-[#1db954] text-[#1db954]" 156 : "text-gray-400" 157 }`} 158 /> 159 </button> 160 )} 161 </div> 162 <p className="text-sm font-semibold text-white truncate group-hover:text-[#1db954] transition-colors"> 163 {playlist.name} 164 </p> 165 <p className="text-xs text-gray-400 truncate"> 166 {playlist.creatorName} 167 </p> 168 </div> 169 ))} 170 </div> 171 </section> 172 )} 173 174 {savedItems && savedItems.length > 0 && ( 175 <section> 176 <div className="flex items-center gap-3 mb-6"> 177 <Bookmark className="w-6 h-6 text-[#1db954] fill-[#1db954]" /> 178 <h3 className="text-2xl font-bold text-white">Saved Playlists</h3> 179 <span className="text-sm text-gray-500">({savedItems.length})</span> 180 </div> 181 182 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> 183 {savedItems.map((playlist) => ( 184 <div 185 key={playlist.id} 186 className="group cursor-pointer" 187 onClick={() => navigate(`/collection/playlist/${playlist.id}`)} 188 > 189 <div className="relative aspect-square rounded-lg overflow-hidden bg-[#181818] mb-3 shadow-md group-hover:shadow-xl transition-all"> 190 <img 191 src={ 192 playlist.cover 193 ? `${baseURL}/${playlist.cover}` 194 : "/favicon.png" 195 } 196 alt={playlist.name} 197 className="w-full h-full object-cover" 198 onError={(e) => { 199 (e.target as HTMLImageElement).src = "/favicon.png"; 200 }} 201 /> 202 203 {currentUser?.username != playlist.creatorUsername && ( 204 <button 205 onClick={(e) => 206 handleSavePlaylist(e, playlist.id, playlist.name) 207 } 208 className="absolute top-2 right-2 p-2 bg-black/70 hover:bg-black/90 rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100 cursor-pointer" 209 title={playlist.isSavedByCurrentUser ? "Unsave" : "Save"} 210 > 211 <Bookmark 212 className={`w-5 h-5 ${ 213 playlist.isSavedByCurrentUser 214 ? "fill-[#1db954] text-[#1db954]" 215 : "text-gray-400" 216 }`} 217 /> 218 </button> 219 )} 220 </div> 221 <p className="text-sm font-semibold text-white truncate group-hover:text-[#1db954] transition-colors"> 222 {playlist.name} 223 </p> 224 <p className="text-xs text-gray-400 truncate"> 225 {playlist.creatorName} 226 </p> 227 </div> 228 ))} 229 </div> 230 </section> 231 )} 232 233 {likedSongs && likedSongs.length > 0 && ( 234 <section> 235 <div className="flex items-center gap-3 mb-6"> 236 <Heart className="w-6 h-6 text-red-500 fill-red-500" /> 237 <h3 className="text-2xl font-bold text-white">Liked Songs</h3> 238 <span className="text-sm text-gray-500"> 239 ({likedSongs?.length}) 240 </span> 241 </div> 242 243 <div className="space-y-1"> 244 {likedSongs.map((song, index) => ( 245 <SongItem 246 key={song.id} 247 song={{ 248 id: song.id, 249 title: song.title, 250 cover: song.cover 251 ? `${baseURL}/${song.cover}` 252 : "/favicon.png", 253 genre: song.genre, 254 link: (song as any).link, 255 releasedBy: song.releasedBy, 256 isLikedByCurrentUser: song.isLikedByCurrentUser, 257 }} 258 index={index + 1} 259 onLikeToggle={() => handleLike(song.id, song.title)} 260 /> 261 ))} 262 </div> 263 </section> 264 )} 265 266 {likedAlbums && likedAlbums.length > 0 && ( 267 <section> 268 <div className="flex items-center gap-3 mb-6"> 269 <Album className="w-6 h-6 text-[#1db954]" /> 270 <h3 className="text-2xl font-bold text-white">Liked Albums</h3> 271 <span className="text-sm text-gray-500"> 272 ({likedAlbums.length}) 273 </span> 274 </div> 275 276 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> 277 {likedAlbums.map((album) => ( 278 <div 279 key={album.id} 280 className="group cursor-pointer" 281 onClick={() => navigate(`/collection/album/${album.id}`)} 282 > 283 <div className="relative aspect-square rounded-lg overflow-hidden bg-[#181818] mb-3 shadow-md group-hover:shadow-xl transition-all"> 284 <img 285 src={ 286 album.cover ? `${baseURL}/${album.cover}` : "/favicon.png" 287 } 288 alt={album.title} 289 className="w-full h-full object-cover" 290 onError={(e) => { 291 (e.target as HTMLImageElement).src = "/favicon.png"; 292 }} 293 /> 294 295 <button 296 onClick={(e) => { 297 e.stopPropagation(); 298 handleLike(album.id, album.title); 299 }} 300 className="absolute top-2 right-2 p-2 bg-black/70 hover:bg-black/90 rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100 cursor-pointer" 301 title={album.isLikedByCurrentUser ? "Unlike" : "Like"} 302 > 303 <svg 304 className="w-5 h-5" 305 fill={album.isLikedByCurrentUser ? "#ef4444" : "none"} 306 stroke={ 307 album.isLikedByCurrentUser ? "#ef4444" : "#9ca3af" 308 } 309 strokeWidth="2" 310 viewBox="0 0 24 24" 311 > 312 <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" /> 313 </svg> 314 </button> 315 </div> 316 <p className="font-semibold text-sm text-white truncate group-hover:text-[#1db954] transition-colors"> 317 {album.title} 318 </p> 319 <p className="text-xs text-gray-400 truncate mt-1"> 320 {album.releasedBy} 321 </p> 322 </div> 323 ))} 324 </div> 325 </section> 326 )} 327 328 {likedEntities && 329 likedEntities.length === 0 && 330 (!createdItems || createdItems.length === 0) && 331 (!savedItems || savedItems.length === 0) && ( 332 <div className="flex flex-col items-center justify-center py-16 text-gray-500"> 333 <Music className="w-20 h-20 mb-4 opacity-20" /> 334 <p className="text-lg font-medium">Nothing here yet</p> 335 <p className="text-sm mt-2"> 336 Start exploring music to build your collection 337 </p> 338 </div> 339 )} 340 </div> 341 ); 296 342 }; 297 343
Note:
See TracChangeset
for help on using the changeset viewer.
