Ignore:
Timestamp:
02/15/26 00:39:55 (5 months ago)
Author:
Dimitar Arsov <dimitararsov04@…>
Branches:
main
Children:
c8baad1
Parents:
85512ff
Message:

add song to playlist

Location:
frontend/src/components
Files:
1 added
4 edited

Legend:

Unmodified
Added
Removed
  • frontend/src/components/Sidebar.tsx

    r85512ff rce45c7a  
    4848      await axiosInstance.post("/playlists", { name: playlistName });
    4949      toast.success("Playlist created successfully!");
    50       await refreshPlaylists();
     50      await refreshPlaylists(false);
    5151    } catch (error) {
    5252      toast.error(getErrorMessage(error));
  • frontend/src/components/SongItem.tsx

    r85512ff rce45c7a  
    1 import { useEffect, useRef, useState } from "react";
     1import { useRef, useState } from "react";
    22import { useNavigate } from "react-router-dom";
    33import { baseURL } from "../api/axiosInstance";
    44import { usePlayer } from "../context/playerContext";
    55import { toEmbedUrl } from "../utils/utils";
     6import PlaylistDropdown from "./PlaylistDropdown";
    67
    78export 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;
     9  id: number;
     10  title: string;
     11  cover?: string | null;
     12  genre?: string;
     13  link?: string | null;
     14  releasedBy?: string;
     15  isLikedByCurrentUser?: boolean;
    1516}
    1617
    1718interface 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;
     19  song: SongItemData;
     20  /** Optional label shown before the artist, e.g. "Song" for search results */
     21  label?: string;
     22  /** Optional role badge for artist contributions, e.g. "PERFORMER" */
     23  role?: string;
     24  /** Optional index number for playlist/collection views */
     25  index?: number;
     26  /** Callback when the like button is clicked */
     27  onLikeToggle?: (songId: number) => void;
     28  /** Controlled: whether the playlist dropdown is open */
     29  isDropdownOpen?: boolean;
     30  /** Controlled: callback when dropdown should open/close */
     31  onDropdownToggle?: (songId: number | null) => void;
    2732}
    2833
    2934const 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",
     35  COMPOSER: "bg-purple-500/20 text-purple-300",
     36  PERFORMER: "bg-blue-500/20 text-blue-300",
     37  PRODUCER: "bg-green-500/20 text-green-300",
     38  MAIN_VOCAL: "bg-pink-500/20 text-pink-300",
    3439};
    3540
    3641const SongItem = ({
    37         song,
    38         label,
    39         role,
    40         index,
    41         onLikeToggle,
     42  song,
     43  label,
     44  role,
     45  index,
     46  onLikeToggle,
     47  isDropdownOpen,
     48  onDropdownToggle,
    4249}: 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         );
     50  const navigate = useNavigate();
     51  const { play, currentSong } = usePlayer();
     52  const [internalOpen, setInternalOpen] = useState(false);
     53  const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
     54  const [dropdownDirection, setDropdownDirection] = useState<"above" | "below">(
     55    "below",
     56  );
     57  const buttonRef = useRef<HTMLButtonElement>(null);
     58
     59  const playlistOpen = isDropdownOpen ?? internalOpen;
     60  const setPlaylistOpen = (open: boolean) => {
     61    if (onDropdownToggle) {
     62      onDropdownToggle(open ? song.id : null);
     63    } else {
     64      setInternalOpen(open);
     65    }
     66  };
     67
     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 creation
     73    setPlaylistOpen(false);
     74  };
     75
     76  const subtitleParts: string[] = [];
     77  if (label) subtitleParts.push(label);
     78  if (song.releasedBy) subtitleParts.push(song.releasedBy);
     79  const subtitle = subtitleParts.join(" • ");
     80
     81  return (
     82    <div
     83      onClick={() => navigate(`/songs/${song.id}`)}
     84      onMouseEnter={() => {
     85        if (onDropdownToggle && !playlistOpen) {
     86          onDropdownToggle(null);
     87        }
     88      }}
     89      className="flex items-center gap-4 p-3 rounded-lg hover:bg-white/5 cursor-pointer transition-colors group"
     90    >
     91      {/* Optional index */}
     92      {index != null && (
     93        <span className="text-gray-500 font-medium w-8 text-center text-sm shrink-0">
     94          {index}
     95        </span>
     96      )}
     97
     98      {/* Cover */}
     99      <img
     100        src={song.cover ? `${baseURL}/${song.cover}` : "/favicon.png"}
     101        alt={song.title}
     102        className="w-12 h-12 rounded object-cover shrink-0"
     103        onError={(e) => {
     104          (e.target as HTMLImageElement).src = "/favicon.png";
     105        }}
     106      />
     107
     108      {/* Title & subtitle */}
     109      <div className="flex-1 min-w-0">
     110        <p
     111          className={`font-medium truncate ${
     112            isPlaying ? "text-[#1db954]" : "text-white"
     113          }`}
     114        >
     115          {song.title}
     116        </p>
     117        {subtitle && (
     118          <p className="text-sm text-gray-400 truncate">{subtitle}</p>
     119        )}
     120      </div>
     121
     122      {/* Role badge (artist contributions) */}
     123      {role && (
     124        <span
     125          className={`px-3 py-1 rounded-full text-xs font-medium hidden sm:block ${
     126            ROLE_COLORS[role] || "bg-white/10 text-gray-300"
     127          }`}
     128        >
     129          {role.replace("_", " ")}
     130        </span>
     131      )}
     132
     133      {/* Genre */}
     134      {song.genre && (
     135        <span className="text-xs text-gray-500 uppercase tracking-wider mr-2 hidden sm:block">
     136          {song.genre}
     137        </span>
     138      )}
     139
     140      {/* Play button */}
     141      {song.link && (
     142        <button
     143          onClick={(e) => {
     144            e.stopPropagation();
     145            play({
     146              id: song.id,
     147              title: song.title,
     148              artist: song.releasedBy || "",
     149              cover: song.cover,
     150              embedUrl: toEmbedUrl(song.link!),
     151            });
     152          }}
     153          className={`p-2 rounded-full transition-all cursor-pointer ${
     154            isPlaying
     155              ? "bg-white text-[#1db954] opacity-100"
     156              : "bg-[#1db954] text-black hover:scale-110 opacity-0 group-hover:opacity-100"
     157          }`}
     158          aria-label={isPlaying ? "Now playing" : "Play song"}
     159        >
     160          {isPlaying ? (
     161            <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
     162              <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
     163            </svg>
     164          ) : (
     165            <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
     166              <path d="M8 5v14l11-7z" />
     167            </svg>
     168          )}
     169        </button>
     170      )}
     171
     172      {/* Like button */}
     173      {onLikeToggle && (
     174        <button
     175          onClick={(e) => {
     176            e.stopPropagation();
     177            onLikeToggle(song.id);
     178          }}
     179          className="p-2 hover:bg-white/10 rounded-full transition-colors cursor-pointer"
     180          aria-label={song.isLikedByCurrentUser ? "Unlike" : "Like"}
     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      )}
     193
     194      {/* Three-dot menu */}
     195      <div className="relative">
     196        <button
     197          ref={buttonRef}
     198          onClick={(e) => {
     199            e.stopPropagation();
     200            if (playlistOpen) {
     201              setPlaylistOpen(false);
     202            } else {
     203              if (buttonRef.current) {
     204                const rect = buttonRef.current.getBoundingClientRect();
     205                const spaceBelow = window.innerHeight - rect.bottom;
     206                const dropdownHeight = 260;
     207
     208                if (spaceBelow < dropdownHeight) {
     209                  setDropdownDirection("above");
     210                  setDropdownPosition({
     211                    top: rect.top - 8,
     212                    left: rect.right - 224,
     213                  });
     214                } else {
     215                  setDropdownDirection("below");
     216                  setDropdownPosition({
     217                    top: rect.bottom + 8,
     218                    left: rect.right - 224,
     219                  });
     220                }
     221              }
     222              setPlaylistOpen(true);
     223            }
     224          }}
     225          className="p-2 hover:bg-white/10 rounded-full transition-colors cursor-pointer text-gray-400 hover:text-white"
     226          aria-label="More options"
     227        >
     228          <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
     229            <circle cx="12" cy="5" r="1.5" />
     230            <circle cx="12" cy="12" r="1.5" />
     231            <circle cx="12" cy="19" r="1.5" />
     232          </svg>
     233        </button>
     234
     235        <PlaylistDropdown
     236          songId={song.id}
     237          isOpen={playlistOpen}
     238          onClose={() => setPlaylistOpen(false)}
     239          position={dropdownPosition}
     240          usePortal={true}
     241          direction={dropdownDirection}
     242          onCreateNewPlaylist={handleCreateNewPlaylist}
     243        />
     244      </div>
     245    </div>
     246  );
    258247};
    259248
  • frontend/src/components/userProfile/ArtistView.tsx

    r85512ff rce45c7a  
    88
    99interface ArtistViewProps {
    10         contributions: ArtistContribution[];
     10  contributions: ArtistContribution[];
    1111}
    1212
    1313const ArtistView = ({ contributions }: ArtistViewProps) => {
    14         const navigate = useNavigate();
    15         const [items, setItems] = useState(contributions);
     14  const navigate = useNavigate();
     15  const [items, setItems] = useState(contributions);
     16  const [openDropdownSongId, setOpenDropdownSongId] = useState<number | null>(
     17    null,
     18  );
    1619
    17         const albums = items.filter((c) => c.entityType === "ALBUM");
    18         const songs = items.filter((c) => c.entityType === "SONG");
     20  const albums = items.filter((c) => c.entityType === "ALBUM");
     21  const songs = items.filter((c) => c.entityType === "SONG");
    1922
    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         };
     23  const getRoleColor = (role: string) => {
     24    const colors: { [key: string]: string } = {
     25      COMPOSER: "bg-purple-500/20 text-purple-300",
     26      PERFORMER: "bg-blue-500/20 text-blue-300",
     27      PRODUCER: "bg-green-500/20 text-green-300",
     28      MAIN_VOCAL: "bg-pink-500/20 text-pink-300",
     29    };
     30    return colors[role] || "bg-white/10 text-gray-300";
     31  };
    2932
    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;
     33  const handleLike = async (id: number, title: string) => {
     34    try {
     35      const response = await axiosInstance.post(`/musical-entity/${id}/like`);
     36      const data = response.data;
    3437
    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         };
     38      setItems((prevItems) =>
     39        prevItems.map((item) =>
     40          item.id === data.entityId
     41            ? { ...item, isLikedByCurrentUser: data.isLiked }
     42            : item,
     43        ),
     44      );
     45      toast.success(
     46        data.isLiked ? `Liked "${title}"` : `Removed "${title}" from likes`,
     47      );
     48    } catch (err: any) {
     49      toast.error(err.response?.data?.error || "Failed to like the item");
     50    }
     51  };
    4952
    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={
    68                                                                                         album.cover ? `${baseURL}/${album.cover}` : "/favicon.png"
    69                                                                                 }
    70                                                                                 alt={album.title}
    71                                                                                 className="w-full h-full object-cover"
    72                                                                                 onError={(e) => {
    73                                                                                         (e.target as HTMLImageElement).src = "/favicon.png";
    74                                                                                 }}
    75                                                                         />
     53  return (
     54    <div className="mt-8">
     55      {albums.length > 0 && (
     56        <div className="mb-12">
     57          <div className="flex items-center gap-3 mb-6">
     58            <Disc3 className="w-6 h-6 text-[#1db954]" />
     59            <h2 className="text-2xl font-bold text-white">Albums</h2>
     60          </div>
     61          <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
     62            {albums.map((album) => (
     63              <div
     64                key={album.id}
     65                className="group cursor-pointer"
     66                onClick={() => navigate(`/collection/album/${album.id}`)}
     67              >
     68                <div className="relative aspect-square rounded-lg mb-3 overflow-hidden shadow-md group-hover:shadow-xl transition-all duration-300 bg-[#181818]">
     69                  <img
     70                    src={
     71                      album.cover ? `${baseURL}/${album.cover}` : "/favicon.png"
     72                    }
     73                    alt={album.title}
     74                    className="w-full h-full object-cover"
     75                    onError={(e) => {
     76                      (e.target as HTMLImageElement).src = "/favicon.png";
     77                    }}
     78                  />
    7679
    77                                                                         <button
    78                                                                                 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"
    79                                                                                 title={album.isLikedByCurrentUser ? "Unlike" : "Like"}
    80                                                                                 onClick={(e) => {
    81                                                                                         e.stopPropagation();
    82                                                                                         handleLike(album.id, album.title);
    83                                                                                 }}
    84                                                                         >
    85                                                                                 <svg
    86                                                                                         className="w-5 h-5"
    87                                                                                         fill={album.isLikedByCurrentUser ? "#ef4444" : "none"}
    88                                                                                         stroke={
    89                                                                                                 album.isLikedByCurrentUser ? "#ef4444" : "#9ca3af"
    90                                                                                         }
    91                                                                                         strokeWidth="2"
    92                                                                                         viewBox="0 0 24 24"
    93                                                                                 >
    94                                                                                         <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" />
    95                                                                                 </svg>
    96                                                                         </button>
     80                  <button
     81                    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"
     82                    title={album.isLikedByCurrentUser ? "Unlike" : "Like"}
     83                    onClick={(e) => {
     84                      e.stopPropagation();
     85                      handleLike(album.id, album.title);
     86                    }}
     87                  >
     88                    <svg
     89                      className="w-5 h-5"
     90                      fill={album.isLikedByCurrentUser ? "#ef4444" : "none"}
     91                      stroke={
     92                        album.isLikedByCurrentUser ? "#ef4444" : "#9ca3af"
     93                      }
     94                      strokeWidth="2"
     95                      viewBox="0 0 24 24"
     96                    >
     97                      <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" />
     98                    </svg>
     99                  </button>
    97100
    98                                                                         <div className="absolute bottom-0 left-0 right-0 bg-linear-to-t from-black/80 to-transparent p-3">
    99                                                                                 <span
    100                                                                                         className={`text-xs px-2 py-1 rounded-full font-medium ${getRoleColor(album.role)}`}
    101                                                                                 >
    102                                                                                         {album.role.replace("_", " ")}
    103                                                                                 </span>
    104                                                                         </div>
    105                                                                 </div>
    106                                                                 <h3 className="font-semibold text-sm line-clamp-2 text-white group-hover:text-[#1db954] transition-colors">
    107                                                                         {album.title}
    108                                                                 </h3>
    109                                                         </div>
    110                                                 ))}
    111                                         </div>
    112                                 </div>
    113                         )}
     101                  <div className="absolute bottom-0 left-0 right-0 bg-linear-to-t from-black/80 to-transparent p-3">
     102                    <span
     103                      className={`text-xs px-2 py-1 rounded-full font-medium ${getRoleColor(album.role)}`}
     104                    >
     105                      {album.role.replace("_", " ")}
     106                    </span>
     107                  </div>
     108                </div>
     109                <h3 className="font-semibold text-sm line-clamp-2 text-white group-hover:text-[#1db954] transition-colors">
     110                  {album.title}
     111                </h3>
     112              </div>
     113            ))}
     114          </div>
     115        </div>
     116      )}
    114117
    115                         {songs.length > 0 && (
    116                                 <div className="mb-12">
    117                                         <div className="flex items-center gap-3 mb-6">
    118                                                 <Music className="w-6 h-6 text-[#1db954]" />
    119                                                 <h2 className="text-2xl font-bold text-white">Songs</h2>
    120                                         </div>
    121                                         <div className="space-y-1">
    122                                                 {songs.map((song) => (
    123                                                         <SongItem
    124                                                                 key={song.id}
    125                                                                 song={{
    126                                                                         id: song.id,
    127                                                                         title: song.title,
    128                                                                         cover: song.cover,
    129                                                                         genre: song.genre,
    130                                                                         link: song.link,
    131                                                                         isLikedByCurrentUser: song.isLikedByCurrentUser,
    132                                                                 }}
    133                                                                 role={song.role}
    134                                                                 onLikeToggle={() => handleLike(song.id, song.title)}
    135                                                         />
    136                                                 ))}
    137                                         </div>
    138                                 </div>
    139                         )}
     118      {songs.length > 0 && (
     119        <div className="mb-12">
     120          <div className="flex items-center gap-3 mb-6">
     121            <Music className="w-6 h-6 text-[#1db954]" />
     122            <h2 className="text-2xl font-bold text-white">Songs</h2>
     123          </div>
     124          <div className="space-y-1">
     125            {songs.map((song) => (
     126              <SongItem
     127                key={song.id}
     128                song={{
     129                  id: song.id,
     130                  title: song.title,
     131                  cover: song.cover,
     132                  genre: song.genre,
     133                  link: song.link,
     134                  isLikedByCurrentUser: song.isLikedByCurrentUser,
     135                }}
     136                role={song.role}
     137                onLikeToggle={() => handleLike(song.id, song.title)}
     138                isDropdownOpen={openDropdownSongId === song.id}
     139                onDropdownToggle={setOpenDropdownSongId}
     140              />
     141            ))}
     142          </div>
     143        </div>
     144      )}
    140145
    141                         {contributions.length === 0 && (
    142                                 <div className="flex flex-col items-center justify-center py-16 text-gray-500">
    143                                         <Music className="w-20 h-20 mb-4 opacity-20" />
    144                                         <p className="text-lg font-medium">No contributions yet</p>
    145                                         <p className="text-sm mt-2">Start creating music to see it here</p>
    146                                 </div>
    147                         )}
    148                 </div>
    149         );
     146      {contributions.length === 0 && (
     147        <div className="flex flex-col items-center justify-center py-16 text-gray-500">
     148          <Music className="w-20 h-20 mb-4 opacity-20" />
     149          <p className="text-lg font-medium">No contributions yet</p>
     150          <p className="text-sm mt-2">Start creating music to see it here</p>
     151        </div>
     152      )}
     153    </div>
     154  );
    150155};
    151156
  • frontend/src/components/userProfile/ListenerView.tsx

    r85512ff rce45c7a  
    2525  const [createdItems, setCreatedItems] = useState(createdPlaylists);
    2626  const [savedItems, setSavedItems] = useState(savedPlaylists);
     27  const [openDropdownSongId, setOpenDropdownSongId] = useState<number | null>(
     28    null,
     29  );
    2730
    2831  const likedSongs = items.filter((e) => e.type === "SONG");
     
    258261                index={index + 1}
    259262                onLikeToggle={() => handleLike(song.id, song.title)}
     263                isDropdownOpen={openDropdownSongId === song.id}
     264                onDropdownToggle={setOpenDropdownSongId}
    260265              />
    261266            ))}
Note: See TracChangeset for help on using the changeset viewer.