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

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

add loading spinner; don't show sidebar for unauthenticated users

  • Property mode set to 100644
File size: 7.0 KB
Line 
1import { useEffect, useState } from "react";
2import { useNavigate, useParams } from "react-router-dom";
3import axiosInstance from "../api/axiosInstance";
4import LoadingSpinner from "../components/LoadingSpinner";
5import type { Song, Album, Playlist } from "../utils/types";
6import { handleError } from "../utils/error";
7interface CollectionView {
8 id: number;
9 title: string;
10 genre?: string;
11 type: string;
12 releasedBy: string;
13 isLikedByCurrentUser?: boolean;
14 songs: Song[];
15}
16
17const MusicalCollection = () => {
18 const { type, id } = useParams();
19 const navigate = useNavigate();
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 genre: album.genre,
34 type: album.type,
35 releasedBy: album.releasedBy,
36 isLikedByCurrentUser: album.isLikedByCurrentUser,
37 songs: album.songs,
38 };
39 } else {
40 const playlist = data as Playlist;
41 return {
42 id: playlist.id,
43 title: playlist.name,
44 genre: undefined,
45 type: "PLAYLIST",
46 releasedBy: playlist.creatorName,
47 isLikedByCurrentUser: undefined,
48 songs: playlist.songsInPlaylist,
49 };
50 }
51 };
52
53 useEffect(() => {
54 const fetchData = async () => {
55 setIsLoading(true);
56 setError(null);
57 try {
58 const endpoint =
59 type === "album" ? `/albums/${id}` : `/playlists/${id}`;
60 const response = await axiosInstance.get(endpoint);
61
62 const normalized = normalizeCollection(response.data, type!);
63 setCollection(normalized);
64 } catch (err: any) {
65 setError(handleError(err));
66 } finally {
67 setIsLoading(false);
68 }
69 };
70 fetchData();
71 }, [id, type]);
72
73 if (isLoading) {
74 return <LoadingSpinner />;
75 }
76
77 if (error) {
78 return (
79 <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg">
80 <h2 className="font-bold">Error</h2>
81 <p>{error}</p>
82 </div>
83 );
84 }
85
86 if (!collection) return null;
87
88 return (
89 <div className="container mx-auto p-6">
90 <button
91 onClick={() => navigate(-1)}
92 className="mb-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors duration-200 cursor-pointer"
93 >
94 ← Back
95 </button>
96
97 <div className="bg-white shadow-lg rounded-lg p-8">
98 <div className="flex items-start gap-6 mb-8">
99 <div className="shrink-0">
100 <div className="w-40 h-40 rounded-lg bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center text-white text-5xl font-bold shadow-lg">
101 {collection.title.charAt(0).toUpperCase()}
102 </div>
103 </div>
104
105 <div className="flex-1">
106 <span className="inline-block px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium mb-2">
107 {collection.type}
108 </span>
109 <h1 className="text-4xl font-bold mb-3">{collection.title}</h1>
110
111 <div className="flex items-center gap-3 text-gray-700 mb-4">
112 <span className="font-semibold">{collection.releasedBy}</span>
113 {collection.genre && (
114 <>
115 <span>•</span>
116 <span className="text-gray-600">{collection.genre}</span>
117 </>
118 )}
119 {collection.songs && (
120 <>
121 <span>•</span>
122 <span className="text-gray-600">
123 {collection.songs.length} song
124 {collection.songs.length !== 1 ? "s" : ""}
125 </span>
126 </>
127 )}
128 </div>
129
130 {type === "album" && (
131 <button
132 className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors duration-200"
133 aria-label={collection.isLikedByCurrentUser ? "Unlike" : "Like"}
134 >
135 <svg
136 className="w-5 h-5"
137 fill={collection.isLikedByCurrentUser ? "#ef4444" : "none"}
138 stroke={
139 collection.isLikedByCurrentUser ? "#ef4444" : "#6b7280"
140 }
141 strokeWidth="2"
142 viewBox="0 0 24 24"
143 >
144 <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" />
145 </svg>
146 <span className="text-sm font-medium text-gray-700">
147 {collection.isLikedByCurrentUser ? "Liked" : "Like"}
148 </span>
149 </button>
150 )}
151 </div>
152 </div>
153
154 <div className="border-t pt-6">
155 <h2 className="text-2xl font-bold mb-4 text-gray-800">Songs</h2>
156
157 {collection.songs && collection.songs.length > 0 ? (
158 <div className="space-y-2">
159 {collection.songs.map((song, index) => (
160 <div
161 key={song.id}
162 className="flex items-center gap-4 p-3 rounded-lg hover:bg-gray-50 transition-colors duration-150"
163 >
164 <span className="text-gray-500 font-medium w-8 text-center">
165 {index + 1}
166 </span>
167
168 <div className="flex-1 min-w-0">
169 <p className="font-semibold text-gray-900 truncate">
170 {song.title}
171 </p>
172 <p className="text-sm text-gray-600 truncate">
173 {song.releasedBy}
174 </p>
175 </div>
176
177 <span className="text-sm text-gray-600 px-3 py-1 bg-gray-100 rounded-full">
178 {song.genre}
179 </span>
180
181 <button
182 className="p-2 hover:bg-gray-100 rounded-full transition-colors duration-200"
183 aria-label={song.isLikedByCurrentUser ? "Unlike" : "Like"}
184 >
185 <svg
186 className="w-5 h-5"
187 fill={song.isLikedByCurrentUser ? "#ef4444" : "none"}
188 stroke={song.isLikedByCurrentUser ? "#ef4444" : "#6b7280"}
189 strokeWidth="2"
190 viewBox="0 0 24 24"
191 >
192 <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" />
193 </svg>
194 </button>
195 </div>
196 ))}
197 </div>
198 ) : (
199 <p className="text-center text-gray-500 py-8">No songs available</p>
200 )}
201 </div>
202 </div>
203 </div>
204 );
205};
206
207export default MusicalCollection;
Note: See TracBrowser for help on using the repository browser.