Changeset d1ee039 for frontend/src/components
- Timestamp:
- 02/15/26 23:28:14 (5 months ago)
- Branches:
- main
- Children:
- 615dcee, c807e22
- Parents:
- c8baad1
- Location:
- frontend/src/components
- Files:
-
- 6 edited
-
Sidebar.tsx (modified) (1 diff)
-
SongItem.tsx (modified) (2 diffs)
-
playlist/CreatePlaylistModal.tsx (modified) (2 diffs)
-
playlist/PlaylistDropdown.tsx (modified) (15 diffs)
-
userProfile/ListenerView.tsx (modified) (6 diffs)
-
userProfile/UserListModal.tsx (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
frontend/src/components/Sidebar.tsx
rc8baad1 rd1ee039 273 273 onClose={() => setIsModalOpen(false)} 274 274 onSuccess={() => refreshPlaylists(false)} 275 songId={null} 275 276 /> 276 277 </> -
frontend/src/components/SongItem.tsx
rc8baad1 rd1ee039 67 67 68 68 const isPlaying = currentSong?.id === song.id; 69 70 const handleCreateNewPlaylist = () => {71 console.log(`Creating new playlist for song ${song.id}`);72 // TODO: Implement actual playlist creation73 setPlaylistOpen(false);74 };75 69 76 70 const subtitleParts: string[] = []; … … 240 234 usePortal={true} 241 235 direction={dropdownDirection} 242 onCreateNewPlaylist={handleCreateNewPlaylist}243 236 /> 244 237 </div> -
frontend/src/components/playlist/CreatePlaylistModal.tsx
rc8baad1 rd1ee039 8 8 onClose, 9 9 onSuccess, 10 songId, 10 11 }: CreatePlaylistModalProps) => { 11 12 const [playlistName, setPlaylistName] = useState(""); … … 39 40 setIsSubmitting(true); 40 41 try { 41 await axiosInstance.post( 42 `/playlists?playlistName=${encodeURIComponent(playlistName.trim())}`, 43 ); 42 await axiosInstance.post("/playlists", null, { 43 params: { 44 playlistName: playlistName.trim(), 45 ...(songId != null && { songId }), 46 }, 47 }); 48 44 49 toast.success("Playlist created successfully!"); 45 onSuccess();50 await onSuccess(); 46 51 handleClose(); 47 52 } catch (err: any) { -
frontend/src/components/playlist/PlaylistDropdown.tsx
rc8baad1 rd1ee039 3 3 import axiosInstance from "../../api/axiosInstance"; 4 4 import { useCreatedPlaylists } from "../../context/playlistContext"; 5 import CreatePlaylistModal from "./CreatePlaylistModal"; 5 6 6 7 interface PlaylistDropdownProps { … … 13 14 14 15 direction?: "above" | "below"; 15 16 onCreateNewPlaylist?: () => void;17 16 } 18 17 … … 24 23 usePortal = false, 25 24 direction = "above", 26 onCreateNewPlaylist,27 25 }: PlaylistDropdownProps) => { 28 26 const { createdPlaylists, refreshPlaylists } = useCreatedPlaylists(); … … 31 29 ); 32 30 const [loading, setLoading] = useState(false); 33 const [processingPlaylistId, setProcessingPlaylistId] = useState< 34 number | null 35 >(null); 31 const [processingIds, setProcessingIds] = useState<Set<number>>(new Set()); 32 const [isModalOpen, setIsModalOpen] = useState(false); 36 33 const dropdownRef = useRef<HTMLDivElement>(null); 37 34 … … 75 72 76 73 const handleTogglePlaylist = async (playlistId: number, songId: number) => { 77 if (processing PlaylistId !== null) return;78 79 setProcessing PlaylistId(playlistId);74 if (processingIds.has(playlistId)) return; 75 76 setProcessingIds((prev) => new Set(prev).add(playlistId)); 80 77 81 78 try { … … 104 101 refreshPlaylists(true); 105 102 setTimeout(() => { 106 setProcessingPlaylistId(null); 103 setProcessingIds((prev) => { 104 const next = new Set(prev); 105 next.delete(playlistId); 106 return next; 107 }); 107 108 }, 500); 108 109 } … … 110 111 111 112 const handleCreateNew = () => { 112 onCreateNewPlaylist?.();113 113 onClose(); 114 setIsModalOpen(true); 114 115 }; 115 116 116 if (!isOpen) return null; 117 const handleModalSuccess = async () => { 118 await refreshPlaylists(false); 119 }; 117 120 118 121 const inlinePositionClass = … … 121 124 : "absolute right-0 bottom-full mb-2"; 122 125 123 const dropdownContent = (126 const dropdownContent = !isOpen ? null : ( 124 127 <div 125 128 ref={dropdownRef} 126 className={`${usePortal ? "fixed" : inlinePositionClass} w-56 bg-[#282828] rounded-lg shadow-2xl py-2 z- [9999]border border-white/10 max-h-60 overflow-y-auto custom-scrollbar`}129 className={`${usePortal ? "fixed" : inlinePositionClass} w-56 bg-[#282828] rounded-lg shadow-2xl py-2 z-9999 border border-white/10 max-h-60 overflow-y-auto custom-scrollbar`} 127 130 style={ 128 131 usePortal && position … … 147 150 ) : createdPlaylists && createdPlaylists.length > 0 ? ( 148 151 createdPlaylists.map((playlist) => { 149 const isProcessing = processing PlaylistId === playlist.id;152 const isProcessing = processingIds.has(playlist.id); 150 153 const isChecked = containingPlaylistIds.includes(playlist.id); 151 154 … … 153 156 <label 154 157 key={playlist.id} 155 className={`flex items-center px-4 py-2 hover:bg-white/10 transition-colors group/item ${158 className={`flex items-center gap-1.5 px-4 py-2 hover:bg-white/10 transition-colors group/item ${ 156 159 isProcessing ? "pointer-events-none" : "cursor-pointer" 157 160 }`} 158 161 onClick={(e) => e.stopPropagation()} 159 162 > 160 <div className="relative flex items-center justify-center ">163 <div className="relative flex items-center justify-center shrink-0"> 161 164 <input 162 165 type="checkbox" 163 166 className="peer sr-only" 164 167 checked={isChecked} 165 disabled={ processingPlaylistId !== null}168 disabled={isProcessing} 166 169 onChange={() => handleTogglePlaylist(playlist.id, songId)} 167 170 /> … … 172 175 : isChecked 173 176 ? "bg-[#1db954] border-[#1db954]" 174 : "border-gray-500 "177 : "border-gray-500 text=wjot" 175 178 }`} 176 179 > … … 182 185 </div> 183 186 <svg 184 className={`absolute w-3 h-3 transition-opacity ${187 className={`absolute w-3 h-3 transition-opacity ${ 185 188 isChecked && !isProcessing ? "opacity-100" : "opacity-0" 186 189 }`} 187 190 fill="none" 188 stroke=" currentColor"191 stroke="white" 189 192 strokeWidth="3" 190 193 viewBox="0 0 24 24" … … 198 201 </div> 199 202 <span 200 className={` ml-3text-sm truncate transition-all ${203 className={`text-sm truncate transition-all ${ 201 204 isProcessing 202 205 ? "text-[#1db954] animate-pulse" … … 206 209 {playlist.name} 207 210 </span> 208 {isProcessing && (209 <span className="ml-auto text-xs text-[#1db954] animate-pulse">210 •••211 </span>212 )}213 211 </label> 214 212 ); … … 245 243 ); 246 244 247 return usePortal 248 ? createPortal(dropdownContent, document.body) 249 : dropdownContent; 245 const dropdown = dropdownContent 246 ? usePortal 247 ? createPortal(dropdownContent, document.body) 248 : dropdownContent 249 : null; 250 251 return ( 252 <> 253 {dropdown} 254 {createPortal( 255 <CreatePlaylistModal 256 isOpen={isModalOpen} 257 onClose={() => setIsModalOpen(false)} 258 onSuccess={handleModalSuccess} 259 songId={songId} 260 />, 261 document.body, 262 )} 263 </> 264 ); 250 265 }; 251 266 -
frontend/src/components/userProfile/ListenerView.tsx
rc8baad1 rd1ee039 1 import { Album, Bookmark, Heart, ListMusic, Music } from "lucide-react";1 import { Album, Bookmark, Heart, ListMusic, Music, Trash2 } from "lucide-react"; 2 2 import { useState } from "react"; 3 3 import { useNavigate, useParams } from "react-router-dom"; … … 7 7 import SongItem from "../SongItem"; 8 8 import { useAuth } from "../../context/authContext"; 9 import { useCreatedPlaylists } from "../../context/playlistContext"; 9 10 10 11 interface ListenerViewProps { … … 20 21 }: ListenerViewProps) => { 21 22 const navigate = useNavigate(); 23 const { refreshPlaylists } = useCreatedPlaylists(); 22 24 const { username: usernameParam } = useParams(); 23 25 const { user: currentUser } = useAuth(); … … 86 88 } catch (err: any) { 87 89 toast.error(err.response?.data?.error || "Failed to save the playlist"); 90 } 91 }; 92 93 const handleDeletePlaylist = async ( 94 e: React.MouseEvent, 95 playlistId: number, 96 playlistName: string, 97 ) => { 98 e.stopPropagation(); 99 100 try { 101 await axiosInstance.delete(`/playlists/${playlistId}`); 102 refreshPlaylists(false); 103 104 setCreatedItems((prev) => prev.filter((p) => p.id !== playlistId)); 105 toast.success(`Deleted "${playlistName}"`); 106 } catch (err: any) { 107 toast.error(err.response?.data?.error || "Failed to delete playlist"); 88 108 } 89 109 }; … … 142 162 }} 143 163 /> 144 {!isOwnProfile && 164 {isOwnProfile ? ( 165 <button 166 onClick={(e) => 167 handleDeletePlaylist(e, playlist.id, playlist.name) 168 } 169 className="absolute top-2 right-2 p-2 bg-black/70 hover:bg-red-600/90 rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100 cursor-pointer" 170 title="Delete playlist" 171 > 172 <Trash2 className="w-5 h-5 text-gray-400 hover:text-white transition-colors" /> 173 </button> 174 ) : ( 145 175 currentUser?.username != playlist.creatorUsername && ( 146 176 <button … … 161 191 /> 162 192 </button> 163 )} 193 ) 194 )} 164 195 </div> 165 196 <p className="text-sm font-semibold text-white truncate group-hover:text-[#1db954] transition-colors"> -
frontend/src/components/userProfile/UserListModal.tsx
rc8baad1 rd1ee039 2 2 import { baseURL } from "../../api/axiosInstance"; 3 3 import type { BaseNonAdminUser } from "../../utils/types"; 4 import { useEffect } from "react"; 4 5 5 6 interface ModalProps { 6 title: string;7 users: BaseNonAdminUser[];8 onClose: () => void;9 onFollowToggle: (targetUsername: string) => Promise<void>;7 title: string; 8 users: BaseNonAdminUser[]; 9 onClose: () => void; 10 onFollowToggle: (targetUsername: string) => Promise<void>; 10 11 } 11 12 12 13 const UserListModal = ({ 13 title,14 users,15 onClose,16 onFollowToggle,14 title, 15 users, 16 onClose, 17 onFollowToggle, 17 18 }: ModalProps) => { 18 const navigate = useNavigate(); 19 const navigate = useNavigate(); 20 useEffect(() => { 21 document.body.style.overflow = "hidden"; 22 return () => { 23 document.body.style.overflow = ""; 24 }; 25 }, []); 19 26 20 return (21 <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">22 <div className="bg-[#1a1a2e] border border-white/10 rounded-xl shadow-2xl w-full max-w-md max-h-[70vh] flex flex-col">23 <div className="p-4 border-b border-white/10 flex justify-between items-center">24 <h2 className="text-xl font-bold text-white">{title}</h2>25 <button26 onClick={onClose}27 className="text-gray-400 hover:text-white text-2xl cursor-pointer transition-colors"28 >29 ×30 </button>31 </div>27 return ( 28 <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"> 29 <div className="bg-[#1a1a2e] border border-white/10 rounded-xl shadow-2xl w-full max-w-md max-h-[70vh] flex flex-col"> 30 <div className="p-4 border-b border-white/10 flex justify-between items-center"> 31 <h2 className="text-xl font-bold text-white">{title}</h2> 32 <button 33 onClick={onClose} 34 className="text-gray-400 hover:text-white text-2xl cursor-pointer transition-colors" 35 > 36 × 37 </button> 38 </div> 32 39 33 <div className="overflow-y-auto p-4 flex-1">34 {users.length === 0 ? (35 <p className="text-center text-gray-500 py-8">No users found.</p>36 ) : (37 users.map((u) => (38 <div39 key={u.username}40 className="flex items-center justify-between p-3 hover:bg-white/5 rounded-lg transition-colors"41 >42 <div43 className="flex items-center gap-4 cursor-pointer flex-1"44 onClick={() => {45 onClose();46 navigate(`/users/${u.username}`);47 }}48 >49 <div className="w-10 h-10 rounded-full bg-[#282828] overflow-hidden shrink-0 flex items-center justify-center">50 {u.profilePhoto ? (51 <img52 src={`${baseURL}/${u.profilePhoto}`}53 className="w-full h-full object-cover"54 alt=""55 />56 ) : (57 <span className="text-[#1db954] font-bold">58 {u.fullName.charAt(0)}59 </span>60 )}61 </div>62 <div>63 <p className="font-semibold text-white">{u.fullName}</p>64 <p className="text-sm text-gray-400">@{u.username}</p>65 </div>66 </div>40 <div className="overflow-y-auto p-4 flex-1"> 41 {users.length === 0 ? ( 42 <p className="text-center text-gray-500 py-8">No users found.</p> 43 ) : ( 44 users.map((u) => ( 45 <div 46 key={u.username} 47 className="flex items-center justify-between p-3 hover:bg-white/5 rounded-lg transition-colors" 48 > 49 <div 50 className="flex items-center gap-4 cursor-pointer flex-1" 51 onClick={() => { 52 onClose(); 53 navigate(`/users/${u.username}`); 54 }} 55 > 56 <div className="w-10 h-10 rounded-full bg-[#282828] overflow-hidden shrink-0 flex items-center justify-center"> 57 {u.profilePhoto ? ( 58 <img 59 src={`${baseURL}/${u.profilePhoto}`} 60 className="w-full h-full object-cover" 61 alt="" 62 /> 63 ) : ( 64 <span className="text-[#1db954] font-bold"> 65 {u.fullName.charAt(0)} 66 </span> 67 )} 68 </div> 69 <div> 70 <p className="font-semibold text-white">{u.fullName}</p> 71 <p className="text-sm text-gray-400">@{u.username}</p> 72 </div> 73 </div> 67 74 68 <button69 onClick={() => onFollowToggle(u.username)}70 className={`px-4 py-1.5 text-sm font-medium rounded-full transition-colors cursor-pointer ${71 u.isFollowedByCurrentUser72 ? "bg-white/10 text-white hover:bg-white/20"73 : "bg-[#1db954] text-black hover:bg-[#1ed760]"74 }`}75 >76 {u.isFollowedByCurrentUser ? "Following" : "Follow"}77 </button>78 </div>79 ))80 )}81 </div>82 </div>83 <div className="absolute inset-0 -z-10" onClick={onClose}></div>84 </div>85 );75 <button 76 onClick={() => onFollowToggle(u.username)} 77 className={`px-4 py-1.5 text-sm font-medium rounded-full transition-colors cursor-pointer ${ 78 u.isFollowedByCurrentUser 79 ? "bg-white/10 text-white hover:bg-white/20" 80 : "bg-[#1db954] text-black hover:bg-[#1ed760]" 81 }`} 82 > 83 {u.isFollowedByCurrentUser ? "Following" : "Follow"} 84 </button> 85 </div> 86 )) 87 )} 88 </div> 89 </div> 90 <div className="absolute inset-0 -z-10" onClick={onClose}></div> 91 </div> 92 ); 86 93 }; 87 94
Note:
See TracChangeset
for help on using the changeset viewer.
