Changeset 1579b4f for frontend/src/components/userProfile/ArtistView.tsx
- Timestamp:
- 02/10/26 21:58:39 (5 months ago)
- Branches:
- main
- Children:
- 92db381
- Parents:
- 0808ef2
- File:
-
- 1 edited
-
frontend/src/components/userProfile/ArtistView.tsx (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
frontend/src/components/userProfile/ArtistView.tsx
r0808ef2 r1579b4f 1 import { Disc3, Music } from "lucide-react"; 2 import { useState } from "react"; 1 3 import { useNavigate } from "react-router-dom"; 2 import { useState } from "react";3 import { Music, Disc3, Play, Plus } from "lucide-react";4 import { toast } from "react-toastify"; 5 import axiosInstance from "../../api/axiosInstance"; 4 6 import type { ArtistContribution } from "../../utils/types"; 5 import axiosInstance from "../../api/axiosInstance";7 import SongItem from "../SongItem"; 6 8 7 9 interface ArtistViewProps { 8 contributions: ArtistContribution[];10 contributions: ArtistContribution[]; 9 11 } 10 12 11 13 const ArtistView = ({ contributions }: ArtistViewProps) => { 12 const navigate = useNavigate(); 13 const [items, setItems] = useState(contributions); 14 const [toast, setToast] = useState<{ message: string; show: boolean }>({ 15 message: "", 16 show: false, 17 }); 14 const navigate = useNavigate(); 15 const [items, setItems] = useState(contributions); 18 16 19 const albums = items.filter((c) => c.entityType === "ALBUM");20 const songs = items.filter((c) => c.entityType === "SONG");17 const albums = items.filter((c) => c.entityType === "ALBUM"); 18 const songs = items.filter((c) => c.entityType === "SONG"); 21 19 22 const getRoleColor = (role: string) => {23 const colors: { [key: string]: string } = {24 COMPOSER: "bg-purple-100 text-purple-700",25 PERFORMER: "bg-blue-100 text-blue-700",26 PRODUCER: "bg-green-100 text-green-700",27 MAIN_VOCAL: "bg-pink-100 text-pink-700",28 };29 return colors[role] || "bg-gray-100 text-gray-700";30 };20 const getRoleColor = (role: string) => { 21 const colors: { [key: string]: string } = { 22 COMPOSER: "bg-purple-500/20 text-purple-300", 23 PERFORMER: "bg-blue-500/20 text-blue-300", 24 PRODUCER: "bg-green-500/20 text-green-300", 25 MAIN_VOCAL: "bg-pink-500/20 text-pink-300", 26 }; 27 return colors[role] || "bg-white/10 text-gray-300"; 28 }; 31 29 32 const showToast = (message: string) => { 33 setToast({ message, show: true }); 34 setTimeout(() => { 35 setToast({ message: "", show: false }); 36 }, 2000); 37 }; 30 const handleLike = async (id: number, title: string) => { 31 try { 32 const response = await axiosInstance.post(`/musical-entity/${id}/like`); 33 const data = response.data; 38 34 39 const handleLike = async (id: number, title: string) => { 40 try { 41 const response = await axiosInstance.post(`/musical-entity/${id}/like`); 42 const data = response.data; 35 setItems((prevItems) => 36 prevItems.map((item) => 37 item.id === data.entityId 38 ? { ...item, isLikedByCurrentUser: data.isLiked } 39 : item, 40 ), 41 ); 42 toast.success( 43 data.isLiked ? `Liked "${title}"` : `Removed "${title}" from likes`, 44 ); 45 } catch (err: any) { 46 toast.error(err.response?.data?.error || "Failed to like the item"); 47 } 48 }; 43 49 44 setItems((prevItems) => 45 prevItems.map((item) => 46 item.id === data.entityId 47 ? { ...item, isLikedByCurrentUser: data.isLiked } 48 : item, 49 ), 50 ); 50 return ( 51 <div className="mt-8"> 52 {albums.length > 0 && ( 53 <div className="mb-12"> 54 <div className="flex items-center gap-3 mb-6"> 55 <Disc3 className="w-6 h-6 text-[#1db954]" /> 56 <h2 className="text-2xl font-bold text-white">Albums</h2> 57 </div> 58 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> 59 {albums.map((album) => ( 60 <div 61 key={album.id} 62 className="group cursor-pointer" 63 onClick={() => navigate(`/collection/album/${album.id}`)} 64 > 65 <div className="relative aspect-square rounded-lg mb-3 overflow-hidden shadow-md group-hover:shadow-xl transition-all duration-300 bg-[#181818]"> 66 <img 67 src={album.cover || "/favicon.png"} 68 alt={album.title} 69 className="w-full h-full object-cover" 70 onError={(e) => { 71 (e.target as HTMLImageElement).src = "/favicon.png"; 72 }} 73 /> 51 74 52 showToast( 53 data.isLiked ? `Liked "${title}"` : `Removed ${title} from likes`, 54 ); 55 } catch (err: any) { 56 showToast(err.response?.data?.error); 57 } 58 }; 75 <button 76 className="absolute top-2 right-2 p-2 bg-black/70 hover:bg-black/90 rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100 cursor-pointer" 77 title={album.isLikedByCurrentUser ? "Unlike" : "Like"} 78 onClick={(e) => { 79 e.stopPropagation(); 80 handleLike(album.id, album.title); 81 }} 82 > 83 <svg 84 className="w-5 h-5" 85 fill={album.isLikedByCurrentUser ? "#ef4444" : "none"} 86 stroke={ 87 album.isLikedByCurrentUser ? "#ef4444" : "#9ca3af" 88 } 89 strokeWidth="2" 90 viewBox="0 0 24 24" 91 > 92 <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" /> 93 </svg> 94 </button> 59 95 60 return ( 61 <div className="mt-8"> 62 {toast.show && ( 63 <div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 animate-fade-in-up"> 64 <div className="bg-gray-900 text-white px-6 py-3 rounded-full shadow-lg text-sm font-medium"> 65 {toast.message} 66 </div> 67 </div> 68 )} 96 <div className="absolute bottom-0 left-0 right-0 bg-linear-to-t from-black/80 to-transparent p-3"> 97 <span 98 className={`text-xs px-2 py-1 rounded-full font-medium ${getRoleColor(album.role)}`} 99 > 100 {album.role.replace("_", " ")} 101 </span> 102 </div> 103 </div> 104 <h3 className="font-semibold text-sm line-clamp-2 text-white group-hover:text-[#1db954] transition-colors"> 105 {album.title} 106 </h3> 107 </div> 108 ))} 109 </div> 110 </div> 111 )} 69 112 70 {albums.length > 0 && ( 71 <div className="mb-12"> 72 <div className="flex items-center gap-3 mb-6"> 73 <Disc3 className="w-6 h-6 text-gray-700" /> 74 <h2 className="text-2xl font-bold text-gray-800">Albums</h2> 75 </div> 76 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> 77 {albums.map((album) => ( 78 <div 79 key={album.id} 80 className="group cursor-pointer" 81 onClick={() => navigate(`/collection/album/${album.id}`)} 82 > 83 <div className="relative aspect-square bg-gradient-to-br from-blue-400 to-purple-500 rounded-lg mb-3 overflow-hidden shadow-md group-hover:shadow-lg transition-all duration-300"> 84 <div className="absolute inset-0 flex items-center justify-center"> 85 <Disc3 className="w-16 h-16 text-white opacity-30" /> 86 </div> 113 {songs.length > 0 && ( 114 <div className="mb-12"> 115 <div className="flex items-center gap-3 mb-6"> 116 <Music className="w-6 h-6 text-[#1db954]" /> 117 <h2 className="text-2xl font-bold text-white">Songs</h2> 118 </div> 119 <div className="space-y-1"> 120 {songs.map((song) => ( 121 <SongItem 122 key={song.id} 123 song={{ 124 id: song.id, 125 title: song.title, 126 cover: song.cover, 127 genre: song.genre, 128 link: song.link, 129 isLikedByCurrentUser: song.isLikedByCurrentUser, 130 }} 131 role={song.role} 132 onLikeToggle={() => handleLike(song.id, song.title)} 133 /> 134 ))} 135 </div> 136 </div> 137 )} 87 138 88 <button 89 className="absolute top-2 right-2 p-2 bg-white/90 hover:bg-white rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100 cursor-pointer" 90 title={album.isLikedByCurrentUser ? "Unlike" : "Like"} 91 onClick={(e) => { 92 e.stopPropagation(); 93 handleLike(album.id, album.title); 94 }} 95 > 96 <svg 97 className="w-5 h-5" 98 fill={album.isLikedByCurrentUser ? "#ef4444" : "none"} 99 stroke={ 100 album.isLikedByCurrentUser ? "#ef4444" : "#6b7280" 101 } 102 strokeWidth="2" 103 viewBox="0 0 24 24" 104 > 105 <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" /> 106 </svg> 107 </button> 108 109 <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent p-3"> 110 <span 111 className={`text-xs px-2 py-1 rounded-full font-medium ${getRoleColor(album.role)}`} 112 > 113 {album.role.replace("_", " ")} 114 </span> 115 </div> 116 </div> 117 <h3 className="font-semibold text-sm line-clamp-2 text-gray-900 group-hover:text-blue-600 transition-colors"> 118 {album.title} 119 </h3> 120 </div> 121 ))} 122 </div> 123 </div> 124 )} 125 126 {songs.length > 0 && ( 127 <div className="mb-12"> 128 <div className="flex items-center gap-3 mb-6"> 129 <Music className="w-6 h-6 text-gray-700" /> 130 <h2 className="text-2xl font-bold text-gray-800">Songs</h2> 131 </div> 132 <div className="space-y-2"> 133 {songs.map((song) => ( 134 <div 135 key={song.id} 136 className="group relative flex items-center gap-4 p-3 rounded-lg hover:bg-gray-50 transition-all cursor-pointer" 137 onClick={() => navigate(`/musical-entity/${song.id}`)} 138 > 139 <div className="relative shrink-0"> 140 <div className="w-12 h-12 bg-gradient-to-br from-blue-400 to-purple-500 rounded flex items-center justify-center shadow-sm"> 141 <Music className="w-6 h-6 text-white" /> 142 </div> 143 <button 144 className="absolute inset-0 bg-black/60 rounded flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200" 145 aria-label="Play song" 146 > 147 <Play className="w-6 h-6 text-white fill-white" /> 148 </button> 149 </div> 150 151 <div className="flex-1 min-w-0"> 152 <h3 className="font-semibold text-base text-gray-900 group-hover:text-blue-600 transition-colors truncate"> 153 {song.title} 154 </h3> 155 <div className="flex items-center gap-2 mt-1"> 156 <span className="text-sm text-gray-600">{song.genre}</span> 157 </div> 158 </div> 159 160 <span 161 className={`px-3 py-1 rounded-full text-sm font-medium ${getRoleColor(song.role)}`} 162 > 163 {song.role.replace("_", " ")} 164 </span> 165 166 <div className="flex items-center gap-2"> 167 <button 168 className="p-2 hover:bg-gray-200 rounded-full transition-colors duration-200 cursor-pointer" 169 title="Add to playlist" 170 > 171 <Plus className="w-5 h-5 text-gray-600" /> 172 </button> 173 174 <button 175 className="p-2 hover:bg-gray-200 rounded-full transition-colors duration-200 cursor-pointer" 176 title={song.isLikedByCurrentUser ? "Unlike" : "Like"} 177 onClick={(e) => { 178 e.stopPropagation(); 179 handleLike(song.id, song.title); 180 }} 181 > 182 <svg 183 className="w-5 h-5" 184 fill={song.isLikedByCurrentUser ? "#ef4444" : "none"} 185 stroke={song.isLikedByCurrentUser ? "#ef4444" : "#6b7280"} 186 strokeWidth="2" 187 viewBox="0 0 24 24" 188 > 189 <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" /> 190 </svg> 191 </button> 192 </div> 193 </div> 194 ))} 195 </div> 196 </div> 197 )} 198 199 {contributions.length === 0 && ( 200 <div className="flex flex-col items-center justify-center py-16 text-gray-400"> 201 <Music className="w-20 h-20 mb-4 opacity-20" /> 202 <p className="text-lg font-medium">No contributions yet</p> 203 <p className="text-sm mt-2">Start creating music to see it here</p> 204 </div> 205 )} 206 </div> 207 ); 139 {contributions.length === 0 && ( 140 <div className="flex flex-col items-center justify-center py-16 text-gray-500"> 141 <Music className="w-20 h-20 mb-4 opacity-20" /> 142 <p className="text-lg font-medium">No contributions yet</p> 143 <p className="text-sm mt-2">Start creating music to see it here</p> 144 </div> 145 )} 146 </div> 147 ); 208 148 }; 209 149
Note:
See TracChangeset
for help on using the changeset viewer.
