source: frontend/src/pages/MusicalCollection.tsx@ f5bc95e

main
Last change on this file since f5bc95e was f5bc95e, checked in by Filip Gavrilovski <filipgavrilovski28@…>, 5 months ago

fix music cover ui bug

  • Property mode set to 100644
File size: 6.9 KB
Line 
1import { useEffect, useState } from "react";
2import { Link, useParams } from "react-router-dom";
3import axiosInstance, { baseURL } from "../api/axiosInstance";
4import SongItem from "../components/SongItem";
5import { handleError } from "../utils/error";
6import type { Album, Playlist, Song } from "../utils/types";
7interface 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[];
16}
17
18const 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 === 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(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 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};
251
252export default MusicalCollection;
Note: See TracBrowser for help on using the repository browser.