source: frontend/src/components/Sidebar.tsx@ 85512ff

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

save playlist

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