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

main
Last change on this file since ce45c7a was ce45c7a, checked in by Dimitar Arsov <dimitararsov04@…>, 5 months ago

add song to playlist

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