Changeset 85512ff for frontend/src/pages/MusicalCollection.tsx
- Timestamp:
- 02/14/26 18:21:18 (5 months ago)
- Branches:
- main
- Children:
- ce45c7a
- Parents:
- f5bc95e
- File:
-
- 1 edited
-
frontend/src/pages/MusicalCollection.tsx (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
frontend/src/pages/MusicalCollection.tsx
rf5bc95e r85512ff 3 3 import axiosInstance, { baseURL } from "../api/axiosInstance"; 4 4 import SongItem from "../components/SongItem"; 5 import { handleError} from "../utils/error";5 import { getErrorMessage } from "../utils/error"; 6 6 import type { Album, Playlist, Song } from "../utils/types"; 7 7 interface CollectionView { 8 id: number;9 title: string;10 cover?: string | null;11 genre?: string;12 type: string;13 releasedBy: string;14 isLikedByCurrentUser?: boolean;15 songs: Song[];8 id: number; 9 title: string; 10 cover?: string | null; 11 genre?: string; 12 type: string; 13 releasedBy: string; 14 isLikedByCurrentUser?: boolean; 15 songs: Song[]; 16 16 } 17 17 18 18 const MusicalCollection = () => { 19 const { type, id } = useParams();20 const [collection, setCollection] = useState<CollectionView | null>(null);21 const [isLoading, setIsLoading] = useState(true);22 const [error, setError] = useState<string | null>(null);23 24 const normalizeCollection = (25 data: Album | Playlist,26 type: string,27 ): CollectionView => {28 if (type === "album") {29 const album = data as Album;30 return {31 id: album.id,32 title: album.title,33 cover: album.cover,34 genre: album.genre,35 type: album.type,36 releasedBy: album.releasedBy,37 isLikedByCurrentUser: album.isLikedByCurrentUser,38 songs: album.songs,39 };40 } else {41 const playlist = data as Playlist;42 return {43 id: playlist.id,44 title: playlist.name,45 cover: playlist.cover,46 genre: undefined,47 type: "PLAYLIST",48 releasedBy: playlist.creatorName,49 isLikedByCurrentUser: undefined,50 songs: playlist.songsInPlaylist,51 };52 }53 };54 55 const toggleLike = async (songId: number) => {56 try {57 await axiosInstance.post(`/musical-entity/${songId}/like`);58 setCollection((prev) => {59 if (!prev) return null;60 return {61 ...prev,62 songs: prev.songs.map((s) =>63 s.id === songId64 ? { ...s, isLikedByCurrentUser: !s.isLikedByCurrentUser }65 : s,66 ),67 };68 });69 } catch (err) {70 console.error("Error toggling like:", err);71 }72 };73 74 const toggleCollectionLike = async () => {75 if (!collection) return;76 try {77 await axiosInstance.post(`/musical-entity/${collection.id}/like`);78 setCollection((prev) => {79 if (!prev) return null;80 return { ...prev, isLikedByCurrentUser: !prev.isLikedByCurrentUser };81 });82 } catch (err) {83 console.error("Error toggling collection like:", err);84 }85 };86 87 useEffect(() => {88 const fetchData = async () => {89 setIsLoading(true);90 setError(null);91 try {92 const endpoint =93 type === "album" ? `/albums/${id}` : `/playlists/${id}`;94 const response = await axiosInstance.get(endpoint);95 96 const normalized = normalizeCollection(response.data, type!);97 setCollection(normalized);98 } catch (err: any) {99 setError(handleError(err));100 } finally {101 setIsLoading(false);102 }103 };104 fetchData();105 }, [id, type]);106 107 if (isLoading) {108 return (109 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">110 <div className="flex flex-col items-center gap-4">111 <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin" />112 <p className="text-gray-400 text-lg">Loading collection…</p>113 </div>114 </div>115 );116 }117 118 if (error) {119 return (120 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">121 <div className="text-center">122 <p className="text-red-400 text-xl mb-4">{error}</p>123 <Link to="/" className="text-[#1db954] hover:underline text-sm">124 ← Back to Home125 </Link>126 </div>127 </div>128 );129 }130 131 if (!collection) return null;132 133 return (134 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white">135 <div className="max-w-5xl mx-auto px-6 py-10">136 {/* Back link */}137 <Link138 to="/"139 className="inline-flex items-center gap-1 text-gray-400 hover:text-white text-sm mb-8 transition-colors"140 >141 <svg142 className="w-4 h-4"143 fill="none"144 stroke="currentColor"145 viewBox="0 0 24 24"146 >147 <path148 strokeLinecap="round"149 strokeLinejoin="round"150 strokeWidth={2}151 d="M15 19l-7-7 7-7"152 />153 </svg>154 Back to Home155 </Link>156 157 {/* Hero section */}158 <div className="flex flex-col md:flex-row gap-8 mb-10">159 {/* Cover art */}160 <div className="w-full md:w-72 shrink-0">161 <div className="relative w-full pt-[100%] rounded-xl overflow-hidden shadow-2xl bg-[#181818]">162 <img163 src={164 collection.cover165 ? `${baseURL}/${collection.cover}`166 : "/favicon.png"167 }168 alt={collection.title}169 className="absolute inset-0 w-full h-full object-cover"170 onError={(e) => {171 (e.target as HTMLImageElement).src = "/favicon.png";172 }}173 />174 </div>175 </div>176 177 {/* Collection info */}178 <div className="flex flex-col justify-end gap-3 min-w-0">179 <span className="text-xs uppercase tracking-widest text-gray-400 font-medium">180 {collection.genre ? `${collection.genre} • ` : ""}181 {collection.type === "PLAYLIST" ? "Playlist" : "Album"}182 </span>183 <h1 className="text-4xl md:text-5xl font-extrabold leading-tight truncate">184 {collection.title}185 </h1>186 187 <p className="text-xl text-gray-300 font-semibold">188 {collection.releasedBy}189 </p>190 191 {collection.songs && (192 <p className="text-sm text-gray-500">193 {collection.songs.length} song194 {collection.songs.length !== 1 ? "s" : ""}195 </p>196 )}197 198 {/* Action buttons */}199 <div className="flex items-center gap-3 mt-4">200 {type === "album" && (201 <button202 onClick={toggleCollectionLike}203 className={`flex items-center gap-2 px-5 py-3 rounded-full text-sm font-semibold transition-colors cursor-pointer ${204 collection.isLikedByCurrentUser205 ? "bg-[#1db954] text-black"206 : "bg-white/10 text-white hover:bg-white/20"207 }`}208 >209 <svg210 className="w-5 h-5"211 fill={212 collection.isLikedByCurrentUser ? "currentColor" : "none"213 }214 stroke="currentColor"215 viewBox="0 0 24 24"216 >217 <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" />218 </svg>219 {collection.isLikedByCurrentUser ? "Liked" : "Like"}220 </button>221 )}222 </div>223 </div>224 </div>225 226 {/* Songs list */}227 <div className="border-t border-white/10 pt-6">228 <h2 className="text-2xl font-bold mb-4">Songs</h2>229 230 {collection.songs && collection.songs.length > 0 ? (231 <div className="space-y-1">232 {collection.songs.map((song, index) => (233 <SongItem234 key={song.id}235 song={song}236 index={index + 1}237 onLikeToggle={() => toggleLike(song.id)}238 />239 ))}240 </div>241 ) : (242 <div className="text-center py-12 text-gray-400">243 <p className="text-lg">No songs available</p>244 </div>245 )}246 </div>247 </div>248 </div>249 );19 const { type, id } = useParams(); 20 const [collection, setCollection] = useState<CollectionView | null>(null); 21 const [isLoading, setIsLoading] = useState(true); 22 const [error, setError] = useState<string | null>(null); 23 24 const normalizeCollection = ( 25 data: Album | Playlist, 26 type: string, 27 ): CollectionView => { 28 if (type === "album") { 29 const album = data as Album; 30 return { 31 id: album.id, 32 title: album.title, 33 cover: album.cover, 34 genre: album.genre, 35 type: album.type, 36 releasedBy: album.releasedBy, 37 isLikedByCurrentUser: album.isLikedByCurrentUser, 38 songs: album.songs, 39 }; 40 } else { 41 const playlist = data as Playlist; 42 return { 43 id: playlist.id, 44 title: playlist.name, 45 cover: playlist.cover, 46 genre: undefined, 47 type: "PLAYLIST", 48 releasedBy: playlist.creatorName, 49 isLikedByCurrentUser: undefined, 50 songs: playlist.songsInPlaylist, 51 }; 52 } 53 }; 54 55 const toggleLike = async (songId: number) => { 56 try { 57 await axiosInstance.post(`/musical-entity/${songId}/like`); 58 setCollection((prev) => { 59 if (!prev) return null; 60 return { 61 ...prev, 62 songs: prev.songs.map((s) => 63 s.id === songId 64 ? { ...s, isLikedByCurrentUser: !s.isLikedByCurrentUser } 65 : s, 66 ), 67 }; 68 }); 69 } catch (err) { 70 console.error("Error toggling like:", err); 71 } 72 }; 73 74 const toggleCollectionLike = async () => { 75 if (!collection) return; 76 try { 77 await axiosInstance.post(`/musical-entity/${collection.id}/like`); 78 setCollection((prev) => { 79 if (!prev) return null; 80 return { ...prev, isLikedByCurrentUser: !prev.isLikedByCurrentUser }; 81 }); 82 } catch (err) { 83 console.error("Error toggling collection like:", err); 84 } 85 }; 86 87 useEffect(() => { 88 const fetchData = async () => { 89 setIsLoading(true); 90 setError(null); 91 try { 92 const endpoint = 93 type === "album" ? `/albums/${id}` : `/playlists/${id}`; 94 const response = await axiosInstance.get(endpoint); 95 96 const normalized = normalizeCollection(response.data, type!); 97 setCollection(normalized); 98 } catch (err: any) { 99 setError(getErrorMessage(err)); 100 } finally { 101 setIsLoading(false); 102 } 103 }; 104 fetchData(); 105 }, [id, type]); 106 107 if (isLoading) { 108 return ( 109 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center"> 110 <div className="flex flex-col items-center gap-4"> 111 <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin" /> 112 <p className="text-gray-400 text-lg">Loading collection…</p> 113 </div> 114 </div> 115 ); 116 } 117 118 if (error) { 119 return ( 120 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center"> 121 <div className="text-center"> 122 <p className="text-red-400 text-xl mb-4">{error}</p> 123 <Link to="/" className="text-[#1db954] hover:underline text-sm"> 124 ← Back to Home 125 </Link> 126 </div> 127 </div> 128 ); 129 } 130 131 if (!collection) return null; 132 133 return ( 134 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white"> 135 <div className="max-w-5xl mx-auto px-6 py-10"> 136 {/* Back link */} 137 <Link 138 to="/" 139 className="inline-flex items-center gap-1 text-gray-400 hover:text-white text-sm mb-8 transition-colors" 140 > 141 <svg 142 className="w-4 h-4" 143 fill="none" 144 stroke="currentColor" 145 viewBox="0 0 24 24" 146 > 147 <path 148 strokeLinecap="round" 149 strokeLinejoin="round" 150 strokeWidth={2} 151 d="M15 19l-7-7 7-7" 152 /> 153 </svg> 154 Back to Home 155 </Link> 156 157 {/* Hero section */} 158 <div className="flex flex-col md:flex-row gap-8 mb-10"> 159 {/* Cover art */} 160 <div className="w-full md:w-72 shrink-0"> 161 <div className="relative w-full pt-[100%] rounded-xl overflow-hidden shadow-2xl bg-[#181818]"> 162 <img 163 src={ 164 collection.cover 165 ? `${baseURL}/${collection.cover}` 166 : "/favicon.png" 167 } 168 alt={collection.title} 169 className="absolute inset-0 w-full h-full object-cover" 170 onError={(e) => { 171 (e.target as HTMLImageElement).src = "/favicon.png"; 172 }} 173 /> 174 </div> 175 </div> 176 177 {/* Collection info */} 178 <div className="flex flex-col justify-end gap-3 min-w-0"> 179 <span className="text-xs uppercase tracking-widest text-gray-400 font-medium"> 180 {collection.genre ? `${collection.genre} • ` : ""} 181 {collection.type === "PLAYLIST" ? "Playlist" : "Album"} 182 </span> 183 <h1 className="text-4xl md:text-5xl font-extrabold leading-tight truncate"> 184 {collection.title} 185 </h1> 186 187 <p className="text-xl text-gray-300 font-semibold"> 188 {collection.releasedBy} 189 </p> 190 191 {collection.songs && ( 192 <p className="text-sm text-gray-500"> 193 {collection.songs.length} song 194 {collection.songs.length !== 1 ? "s" : ""} 195 </p> 196 )} 197 198 {/* Action buttons */} 199 <div className="flex items-center gap-3 mt-4"> 200 {type === "album" && ( 201 <button 202 onClick={toggleCollectionLike} 203 className={`flex items-center gap-2 px-5 py-3 rounded-full text-sm font-semibold transition-colors cursor-pointer ${ 204 collection.isLikedByCurrentUser 205 ? "bg-[#1db954] text-black" 206 : "bg-white/10 text-white hover:bg-white/20" 207 }`} 208 > 209 <svg 210 className="w-5 h-5" 211 fill={ 212 collection.isLikedByCurrentUser ? "currentColor" : "none" 213 } 214 stroke="currentColor" 215 viewBox="0 0 24 24" 216 > 217 <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" /> 218 </svg> 219 {collection.isLikedByCurrentUser ? "Liked" : "Like"} 220 </button> 221 )} 222 </div> 223 </div> 224 </div> 225 226 {/* Songs list */} 227 <div className="border-t border-white/10 pt-6"> 228 <h2 className="text-2xl font-bold mb-4">Songs</h2> 229 230 {collection.songs && collection.songs.length > 0 ? ( 231 <div className="space-y-1"> 232 {collection.songs.map((song, index) => ( 233 <SongItem 234 key={song.id} 235 song={song} 236 index={index + 1} 237 onLikeToggle={() => toggleLike(song.id)} 238 /> 239 ))} 240 </div> 241 ) : ( 242 <div className="text-center py-12 text-gray-400"> 243 <p className="text-lg">No songs available</p> 244 </div> 245 )} 246 </div> 247 </div> 248 </div> 249 ); 250 250 }; 251 251
Note:
See TracChangeset
for help on using the changeset viewer.
