source: frontend/src/components/SongItem.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: 7.1 KB
Line 
1import { useEffect, useRef, useState } from "react";
2import { useNavigate } from "react-router-dom";
3import { baseURL } from "../api/axiosInstance";
4import { usePlayer } from "../context/playerContext";
5import { toEmbedUrl } from "../utils/utils";
6
7export interface SongItemData {
8 id: number;
9 title: string;
10 cover?: string | null;
11 genre?: string;
12 link?: string | null;
13 releasedBy?: string;
14 isLikedByCurrentUser?: boolean;
15}
16
17interface SongItemProps {
18 song: SongItemData;
19 /** Optional label shown before the artist, e.g. "Song" for search results */
20 label?: string;
21 /** Optional role badge for artist contributions, e.g. "PERFORMER" */
22 role?: string;
23 /** Optional index number for playlist/collection views */
24 index?: number;
25 /** Callback when the like button is clicked */
26 onLikeToggle?: (songId: number) => void;
27}
28
29const ROLE_COLORS: { [key: string]: string } = {
30 COMPOSER: "bg-purple-500/20 text-purple-300",
31 PERFORMER: "bg-blue-500/20 text-blue-300",
32 PRODUCER: "bg-green-500/20 text-green-300",
33 MAIN_VOCAL: "bg-pink-500/20 text-pink-300",
34};
35
36const SongItem = ({
37 song,
38 label,
39 role,
40 index,
41 onLikeToggle,
42}: SongItemProps) => {
43 const navigate = useNavigate();
44 const { play, currentSong } = usePlayer();
45 const [playlistOpen, setPlaylistOpen] = useState(false);
46 const dropdownRef = useRef<HTMLDivElement>(null);
47
48 const isPlaying = currentSong?.id === song.id;
49
50 useEffect(() => {
51 const handleClickOutside = (event: MouseEvent) => {
52 if (
53 dropdownRef.current &&
54 !dropdownRef.current.contains(event.target as Node)
55 ) {
56 setPlaylistOpen(false);
57 }
58 };
59 if (playlistOpen) {
60 document.addEventListener("mousedown", handleClickOutside);
61 }
62 return () => document.removeEventListener("mousedown", handleClickOutside);
63 }, [playlistOpen]);
64
65 const handleAddToPlaylist = (playlistName: string) => {
66 console.log(`Adding song ${song.id} to ${playlistName}`);
67 // TODO: Implement actual API call
68 setPlaylistOpen(false);
69 };
70
71 const handleCreateNewPlaylist = () => {
72 console.log(`Creating new playlist for song ${song.id}`);
73 // TODO: Implement actual playlist creation
74 setPlaylistOpen(false);
75 };
76
77 // Build subtitle
78 const subtitleParts: string[] = [];
79 if (label) subtitleParts.push(label);
80 if (song.releasedBy) subtitleParts.push(song.releasedBy);
81 const subtitle = subtitleParts.join(" • ");
82
83 return (
84 <div
85 onClick={() => navigate(`/songs/${song.id}`)}
86 className="flex items-center gap-4 p-3 rounded-lg hover:bg-white/5 cursor-pointer transition-colors group"
87 >
88 {/* Optional index */}
89 {index != null && (
90 <span className="text-gray-500 font-medium w-8 text-center text-sm shrink-0">
91 {index}
92 </span>
93 )}
94
95 {/* Cover */}
96 <img
97 src={song.cover ? `${baseURL}/${song.cover}` : "/favicon.png"}
98 alt={song.title}
99 className="w-12 h-12 rounded object-cover shrink-0"
100 onError={(e) => {
101 (e.target as HTMLImageElement).src = "/favicon.png";
102 }}
103 />
104
105 {/* Title & subtitle */}
106 <div className="flex-1 min-w-0">
107 <p
108 className={`font-medium truncate ${
109 isPlaying ? "text-[#1db954]" : "text-white"
110 }`}
111 >
112 {song.title}
113 </p>
114 {subtitle && (
115 <p className="text-sm text-gray-400 truncate">{subtitle}</p>
116 )}
117 </div>
118
119 {/* Role badge (artist contributions) */}
120 {role && (
121 <span
122 className={`px-3 py-1 rounded-full text-xs font-medium hidden sm:block ${
123 ROLE_COLORS[role] || "bg-white/10 text-gray-300"
124 }`}
125 >
126 {role.replace("_", " ")}
127 </span>
128 )}
129
130 {/* Genre */}
131 {song.genre && (
132 <span className="text-xs text-gray-500 uppercase tracking-wider mr-2 hidden sm:block">
133 {song.genre}
134 </span>
135 )}
136
137 {/* Play button */}
138 {song.link && (
139 <button
140 onClick={(e) => {
141 e.stopPropagation();
142 play({
143 id: song.id,
144 title: song.title,
145 artist: song.releasedBy || "",
146 cover: song.cover,
147 embedUrl: toEmbedUrl(song.link!),
148 });
149 }}
150 className={`p-2 rounded-full transition-all cursor-pointer ${
151 isPlaying
152 ? "bg-white text-[#1db954] opacity-100"
153 : "bg-[#1db954] text-black hover:scale-110 opacity-0 group-hover:opacity-100"
154 }`}
155 aria-label={isPlaying ? "Now playing" : "Play song"}
156 >
157 {isPlaying ? (
158 <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
159 <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
160 </svg>
161 ) : (
162 <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
163 <path d="M8 5v14l11-7z" />
164 </svg>
165 )}
166 </button>
167 )}
168
169 {/* Like button */}
170 {onLikeToggle && (
171 <button
172 onClick={(e) => {
173 e.stopPropagation();
174 onLikeToggle(song.id);
175 }}
176 className="p-2 hover:bg-white/10 rounded-full transition-colors cursor-pointer"
177 aria-label={song.isLikedByCurrentUser ? "Unlike" : "Like"}
178 >
179 <svg
180 className="w-5 h-5"
181 fill={song.isLikedByCurrentUser ? "#ef4444" : "none"}
182 stroke={song.isLikedByCurrentUser ? "#ef4444" : "#6b7280"}
183 strokeWidth="2"
184 viewBox="0 0 24 24"
185 >
186 <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" />
187 </svg>
188 </button>
189 )}
190
191 {/* Three-dot menu */}
192 <div className="relative" ref={dropdownRef}>
193 <button
194 onClick={(e) => {
195 e.stopPropagation();
196 setPlaylistOpen((prev) => !prev);
197 }}
198 className="p-2 hover:bg-white/10 rounded-full transition-colors cursor-pointer text-gray-400 hover:text-white"
199 aria-label="More options"
200 >
201 <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
202 <circle cx="12" cy="5" r="1.5" />
203 <circle cx="12" cy="12" r="1.5" />
204 <circle cx="12" cy="19" r="1.5" />
205 </svg>
206 </button>
207
208 {playlistOpen && (
209 <div className="absolute right-0 bottom-full mb-2 w-48 bg-[#282828] border border-white/10 rounded-lg shadow-lg py-1 z-50">
210 <div className="px-3 py-2 text-xs text-gray-400 border-b border-white/10">
211 Add to playlist
212 </div>
213 <button
214 onClick={(e) => {
215 e.stopPropagation();
216 handleAddToPlaylist("Playlist 1");
217 }}
218 className="w-full text-left px-4 py-2 text-sm text-white hover:bg-white/10 transition-colors"
219 >
220 Playlist 1
221 </button>
222 <button
223 onClick={(e) => {
224 e.stopPropagation();
225 handleAddToPlaylist("Playlist 2");
226 }}
227 className="w-full text-left px-4 py-2 text-sm text-white hover:bg-white/10 transition-colors"
228 >
229 Playlist 2
230 </button>
231 <button
232 onClick={(e) => {
233 e.stopPropagation();
234 handleCreateNewPlaylist();
235 }}
236 className="w-full text-left px-4 py-2 text-sm text-[#1db954] hover:bg-white/10 transition-colors border-t border-white/10 flex items-center gap-2"
237 >
238 <svg
239 className="w-4 h-4"
240 fill="none"
241 stroke="currentColor"
242 viewBox="0 0 24 24"
243 >
244 <path
245 strokeLinecap="round"
246 strokeLinejoin="round"
247 strokeWidth={2}
248 d="M12 4v16m8-8H4"
249 />
250 </svg>
251 Create new playlist
252 </button>
253 </div>
254 )}
255 </div>
256 </div>
257 );
258};
259
260export default SongItem;
Note: See TracBrowser for help on using the repository browser.