source: frontend/src/components/Sidebar.tsx@ 947fcaa

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

add cursor pointer to buttons

  • Property mode set to 100644
File size: 8.8 KB
Line 
1import { useEffect, useState } from "react";
2import { useNavigate } from "react-router-dom";
3import { toast } from "react-toastify";
4import axiosInstance, { baseURL } from "../api/axiosInstance";
5import { useAuth } from "../context/authContext";
6import { usePlayer } from "../context/playerContext";
7import { useCreatedPlaylists } from "../context/playlistContext";
8import { getErrorMessage } from "../utils/error";
9import type { BasicSong, SidebarProps } from "../utils/types";
10import { toEmbedUrl } from "../utils/utils";
11import CreatePlaylistModal from "./playlist/CreatePlaylistModal";
12
13const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
14 const { user } = useAuth();
15 const {
16 createdPlaylists,
17 isLoading: playlistsLoading,
18 refreshPlaylists,
19 } = useCreatedPlaylists();
20 const navigate = useNavigate();
21 const { play, currentSong } = usePlayer();
22 const [recentlyListened, setRecentlyListened] = useState<BasicSong[]>([]);
23 const [songsLoading, setSongsLoading] = useState(false);
24 const [isModalOpen, setIsModalOpen] = useState(false);
25
26 useEffect(() => {
27 const fetchData = async () => {
28 setSongsLoading(true);
29 try {
30 const data = await axiosInstance.get<BasicSong[]>("/songs/recent");
31 setRecentlyListened(data.data);
32 } catch (error) {
33 toast.error(getErrorMessage(error));
34 } finally {
35 setSongsLoading(false);
36 }
37 };
38 if (user) {
39 fetchData();
40 } else {
41 setRecentlyListened([]);
42 setSongsLoading(false);
43 }
44 }, [user]);
45
46 const isLoading = songsLoading || playlistsLoading;
47
48 return (
49 <>
50 <div
51 className={`fixed left-0 top-0 h-full bg-[#121212] border-r border-white/10 transition-transform duration-300 ease-in-out z-40 ${
52 isOpen ? "translate-x-0" : "-translate-x-full"
53 } w-64 overflow-y-auto`}
54 >
55 <div className="p-6">
56 {/* Sidebar Header */}
57 <div className="flex justify-between items-center mb-6 pt-2">
58 <h2 className="text-xl font-bold text-white">Library</h2>
59 <button
60 onClick={onClose}
61 className="text-gray-400 hover:text-white transition-colors cursor-pointer"
62 aria-label="Close sidebar"
63 >
64 <svg
65 className="w-6 h-6"
66 fill="none"
67 stroke="currentColor"
68 viewBox="0 0 24 24"
69 >
70 <path
71 strokeLinecap="round"
72 strokeLinejoin="round"
73 strokeWidth={2}
74 d="M6 18L18 6M6 6l12 12"
75 />
76 </svg>
77 </button>
78 </div>
79
80 {/* Loading State */}
81 {isLoading ? (
82 <div className="flex items-center justify-center py-16">
83 <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#1db954]"></div>
84 </div>
85 ) : (
86 <>
87 {/* Recently Listened */}
88 <div className="mb-8">
89 <h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4">
90 Recently Played
91 </h3>
92 <div className="space-y-3">
93 {recentlyListened.map((song) => (
94 <div
95 key={song.id}
96 className="flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 cursor-pointer transition-colors group relative"
97 >
98 <img
99 src={
100 song.cover
101 ? `${baseURL}/${song.cover}`
102 : "/favicon.png"
103 }
104 alt={song.title}
105 className="w-10 h-10 rounded object-cover"
106 onError={(e) => {
107 (e.target as HTMLImageElement).src = "/favicon.png";
108 }}
109 />
110 <div className="flex-1 min-w-0">
111 <p
112 onClick={() => navigate(`/songs/${song.id}`)}
113 className="text-sm font-medium text-white truncate hover:underline cursor-pointer"
114 >
115 {song.title}
116 </p>
117 <div className="flex items-center gap-1 text-xs text-gray-400">
118 <span
119 onClick={(e) => {
120 e.stopPropagation();
121 if (song.artistUsername) {
122 navigate(`/users/${song.artistUsername}`);
123 }
124 }}
125 className={`truncate ${
126 song.artistUsername
127 ? "hover:underline cursor-pointer hover:text-white"
128 : ""
129 }`}
130 >
131 {song.artist}
132 </span>
133 {song.album && (
134 <>
135 <span>•</span>
136 <span
137 onClick={(e) => {
138 e.stopPropagation();
139 if (song.albumId) {
140 navigate(
141 `/collection/album/${song.albumId}`,
142 );
143 }
144 }}
145 className={`truncate ${
146 song.albumId
147 ? "hover:underline cursor-pointer hover:text-white"
148 : ""
149 }`}
150 >
151 {song.album}
152 </span>
153 </>
154 )}
155 </div>
156 </div>
157 {song.link && (
158 <button
159 onClick={(e) => {
160 e.stopPropagation();
161 play({
162 id: song.id,
163 title: song.title,
164 artist: song.artist,
165 cover: song.cover,
166 embedUrl: toEmbedUrl(song.link!),
167 });
168 }}
169 className={`p-1.5 rounded-full transition-all opacity-0 group-hover:opacity-100 ${
170 currentSong?.id === song.id
171 ? "bg-white text-[#1db954]"
172 : "bg-[#1db954] text-black hover:scale-110 cursor-pointer"
173 }`}
174 aria-label={
175 currentSong?.id === song.id
176 ? "Now playing"
177 : "Play song"
178 }
179 >
180 {currentSong?.id === song.id ? (
181 <svg
182 className="w-4 h-4"
183 fill="currentColor"
184 viewBox="0 0 24 24"
185 >
186 <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
187 </svg>
188 ) : (
189 <svg
190 className="w-4 h-4"
191 fill="currentColor"
192 viewBox="0 0 24 24"
193 >
194 <path d="M8 5v14l11-7z" />
195 </svg>
196 )}
197 </button>
198 )}
199 </div>
200 ))}
201 </div>
202 </div>
203
204 {/* Playlists */}
205 <div>
206 <h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4">
207 Your Playlists
208 </h3>
209 <div className="space-y-2">
210 {createdPlaylists?.map((playlist) => (
211 <div
212 key={playlist.id}
213 onClick={() => {
214 navigate(`/collection/playlist/${playlist.id}`);
215 }}
216 className="flex items-center justify-between p-3 rounded-lg hover:bg-white/5 cursor-pointer transition-colors"
217 >
218 <div className="flex items-center gap-3">
219 <div className="w-10 h-10 bg-linear-to-br from-purple-500 to-pink-500 rounded flex items-center justify-center">
220 <svg
221 className="w-5 h-5 text-white"
222 fill="currentColor"
223 viewBox="0 0 20 20"
224 >
225 <path d="M18 3a1 1 0 00-1.196-.98l-10 2A1 1 0 006 5v9.114A4.369 4.369 0 005 14c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V7.82l8-1.6v5.894A4.37 4.37 0 0015 12c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V3z" />
226 </svg>
227 </div>
228 <div>
229 <p className="text-sm font-medium text-white">
230 {playlist.name}
231 </p>
232 <p className="text-xs text-gray-400">
233 {playlist.songCount} songs
234 </p>
235 </div>
236 </div>
237 </div>
238 ))}
239
240 {/* Create Playlist Button */}
241 <button
242 onClick={() => setIsModalOpen(true)}
243 className="w-full flex items-center gap-3 p-3 rounded-lg hover:bg-white/5 cursor-pointer transition-colors group mt-2"
244 >
245 <div className="w-10 h-10 bg-[#282828] group-hover:bg-[#3a3a3a] rounded flex items-center justify-center transition-colors">
246 <svg
247 className="w-5 h-5 text-gray-400 group-hover:text-white transition-colors"
248 fill="none"
249 stroke="currentColor"
250 viewBox="0 0 24 24"
251 >
252 <path
253 strokeLinecap="round"
254 strokeLinejoin="round"
255 strokeWidth={2}
256 d="M12 4v16m8-8H4"
257 />
258 </svg>
259 </div>
260 <p className="text-sm font-medium text-gray-400 group-hover:text-white transition-colors">
261 Create Playlist
262 </p>
263 </button>
264 </div>
265 </div>
266 </>
267 )}
268 </div>
269 </div>
270
271 <CreatePlaylistModal
272 isOpen={isModalOpen}
273 onClose={() => setIsModalOpen(false)}
274 onSuccess={() => refreshPlaylists(false)}
275 songId={null}
276 />
277 </>
278 );
279};
280
281export default Sidebar;
Note: See TracBrowser for help on using the repository browser.