Ignore:
Timestamp:
02/14/26 18:21:18 (5 months ago)
Author:
Dimitar Arsov <dimitararsov04@…>
Branches:
main
Children:
ce45c7a
Parents:
f5bc95e
Message:

save playlist

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

Legend:

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

    rf5bc95e r85512ff  
    11import { useEffect, useState } from "react";
    22import { useNavigate } from "react-router-dom";
     3import { toast } from "react-toastify";
    34import axiosInstance, { baseURL } from "../api/axiosInstance";
    45import { useAuth } from "../context/authContext";
    56import { usePlayer } from "../context/playerContext";
    6 import type { BasicPlaylist, BasicSong, SidebarProps } from "../utils/types";
     7import type { BasicSong, SidebarProps } from "../utils/types";
    78import { toEmbedUrl } from "../utils/utils";
     9import { useCreatedPlaylists } from "../context/playlistContext";
     10import CreatePlaylistModal from "./playlist/CreatePlaylistModal";
     11import { getErrorMessage } from "../utils/error";
    812
    913const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
    10         const { user } = useAuth();
    11         const navigate = useNavigate();
    12         const { play, currentSong } = usePlayer();
    13         const [recentlyListened, setRecentlyListened] = useState<BasicSong[]>([]);
    14         const [playlists, setPlaylists] = useState<BasicPlaylist[]>([]);
    15 
    16         useEffect(() => {
    17                 const fetchData = async () => {
    18                         try {
    19                                 const data = await axiosInstance.get<BasicSong[]>("/songs/recent");
    20                                 setRecentlyListened(data.data);
    21                         } catch (error) {
    22                                 console.error("Error fetching recently listened songs:", error);
    23                                 // todo: show toast
    24                         }
    25                         try {
    26                                 const data =
    27                                         await axiosInstance.get<BasicPlaylist[]>("/playlists/user");
    28                                 setPlaylists(data.data);
    29                         } catch (error) {
    30                                 console.error("Error fetching playlists:", error);
    31                                 // todo: show toast
    32                         }
    33                 };
    34                 if (user) {
    35                         fetchData();
    36                 } else {
    37                         setRecentlyListened([]);
    38                         setPlaylists([]);
    39                 }
    40         }, [user]);
    41         return (
    42                 <div
    43                         className={`fixed left-0 top-0 h-full bg-[#121212] border-r border-white/10 transition-transform duration-300 ease-in-out z-40 ${
    44                                 isOpen ? "translate-x-0" : "-translate-x-full"
    45                         } w-64 overflow-y-auto`}
    46                 >
    47                         <div className="p-6">
    48                                 {/* Sidebar Header */}
    49                                 <div className="flex justify-between items-center mb-6 pt-2">
    50                                         <h2 className="text-xl font-bold text-white">Library</h2>
    51                                         <button
    52                                                 onClick={onClose}
    53                                                 className="text-gray-400 hover:text-white transition-colors"
    54                                                 aria-label="Close sidebar"
    55                                         >
    56                                                 <svg
    57                                                         className="w-6 h-6"
    58                                                         fill="none"
    59                                                         stroke="currentColor"
    60                                                         viewBox="0 0 24 24"
    61                                                 >
    62                                                         <path
    63                                                                 strokeLinecap="round"
    64                                                                 strokeLinejoin="round"
    65                                                                 strokeWidth={2}
    66                                                                 d="M6 18L18 6M6 6l12 12"
    67                                                         />
    68                                                 </svg>
    69                                         </button>
    70                                 </div>
    71 
    72                                 {/* Recently Listened */}
    73                                 <div className="mb-8">
    74                                         <h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4">
    75                                                 Recently Played
    76                                         </h3>
    77                                         <div className="space-y-3">
    78                                                 {recentlyListened.map((song) => (
    79                                                         <div
    80                                                                 key={song.id}
    81                                                                 className="flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 cursor-pointer transition-colors group relative"
    82                                                         >
    83                                                                 <img
    84                                                                         src={song.cover ? `${baseURL}/${song.cover}` : "/favicon.png"}
    85                                                                         alt={song.title}
    86                                                                         className="w-10 h-10 rounded object-cover"
    87                                                                         onError={(e) => {
    88                                                                                 (e.target as HTMLImageElement).src = "/favicon.png";
    89                                                                         }}
    90                                                                 />
    91                                                                 <div className="flex-1 min-w-0">
    92                                                                         <p
    93                                                                                 onClick={() => navigate(`/songs/${song.id}`)}
    94                                                                                 className="text-sm font-medium text-white truncate hover:underline cursor-pointer"
    95                                                                         >
    96                                                                                 {song.title}
    97                                                                         </p>
    98                                                                         <div className="flex items-center gap-1 text-xs text-gray-400">
    99                                                                                 <span
    100                                                                                         onClick={(e) => {
    101                                                                                                 e.stopPropagation();
    102                                                                                                 if (song.artistUsername) {
    103                                                                                                         navigate(`/users/${song.artistUsername}`);
    104                                                                                                 }
    105                                                                                         }}
    106                                                                                         className={`truncate ${
    107                                                                                                 song.artistUsername
    108                                                                                                         ? "hover:underline cursor-pointer hover:text-white"
    109                                                                                                         : ""
    110                                                                                         }`}
    111                                                                                 >
    112                                                                                         {song.artist}
    113                                                                                 </span>
    114                                                                                 {song.album && (
    115                                                                                         <>
    116                                                                                                 <span>•</span>
    117                                                                                                 <span
    118                                                                                                         onClick={(e) => {
    119                                                                                                                 e.stopPropagation();
    120                                                                                                                 if (song.albumId) {
    121                                                                                                                         navigate(`/collection/album/${song.albumId}`);
    122                                                                                                                 }
    123                                                                                                         }}
    124                                                                                                         className={`truncate ${
    125                                                                                                                 song.albumId
    126                                                                                                                         ? "hover:underline cursor-pointer hover:text-white"
    127                                                                                                                         : ""
    128                                                                                                         }`}
    129                                                                                                 >
    130                                                                                                         {song.album}
    131                                                                                                 </span>
    132                                                                                         </>
    133                                                                                 )}
    134                                                                         </div>
    135                                                                 </div>
    136                                                                 {song.link && (
    137                                                                         <button
    138                                                                                 onClick={(e) => {
    139                                                                                         e.stopPropagation();
    140                                                                                         play({
    141                                                                                                 id: song.id,
    142                                                                                                 title: song.title,
    143                                                                                                 artist: song.artist,
    144                                                                                                 cover: song.cover,
    145                                                                                                 embedUrl: toEmbedUrl(song.link!),
    146                                                                                         });
    147                                                                                 }}
    148                                                                                 className={`p-1.5 rounded-full transition-all opacity-0 group-hover:opacity-100 ${
    149                                                                                         currentSong?.id === song.id
    150                                                                                                 ? "bg-white text-[#1db954]"
    151                                                                                                 : "bg-[#1db954] text-black hover:scale-110"
    152                                                                                 }`}
    153                                                                                 aria-label={
    154                                                                                         currentSong?.id === song.id ? "Now playing" : "Play song"
    155                                                                                 }
    156                                                                         >
    157                                                                                 {currentSong?.id === song.id ? (
    158                                                                                         <svg
    159                                                                                                 className="w-4 h-4"
    160                                                                                                 fill="currentColor"
    161                                                                                                 viewBox="0 0 24 24"
    162                                                                                         >
    163                                                                                                 <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
    164                                                                                         </svg>
    165                                                                                 ) : (
    166                                                                                         <svg
    167                                                                                                 className="w-4 h-4"
    168                                                                                                 fill="currentColor"
    169                                                                                                 viewBox="0 0 24 24"
    170                                                                                         >
    171                                                                                                 <path d="M8 5v14l11-7z" />
    172                                                                                         </svg>
    173                                                                                 )}
    174                                                                         </button>
    175                                                                 )}
    176                                                         </div>
    177                                                 ))}
    178                                         </div>
    179                                 </div>
    180 
    181                                 {/* Playlists */}
    182                                 <div>
    183                                         <h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4">
    184                                                 Your Playlists
    185                                         </h3>
    186                                         <div className="space-y-2">
    187                                                 {playlists.map((playlist) => (
    188                                                         <div
    189                                                                 key={playlist.id}
    190                                                                 className="flex items-center justify-between p-3 rounded-lg hover:bg-white/5 cursor-pointer transition-colors"
    191                                                         >
    192                                                                 <div className="flex items-center gap-3">
    193                                                                         <div className="w-10 h-10 bg-linear-to-br from-purple-500 to-pink-500 rounded flex items-center justify-center">
    194                                                                                 <svg
    195                                                                                         className="w-5 h-5 text-white"
    196                                                                                         fill="currentColor"
    197                                                                                         viewBox="0 0 20 20"
    198                                                                                 >
    199                                                                                         <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" />
    200                                                                                 </svg>
    201                                                                         </div>
    202                                                                         <div>
    203                                                                                 <p className="text-sm font-medium text-white">
    204                                                                                         {playlist.name}
    205                                                                                 </p>
    206                                                                                 <p className="text-xs text-gray-400">
    207                                                                                         {playlist.songCount} songs
    208                                                                                 </p>
    209                                                                         </div>
    210                                                                 </div>
    211                                                         </div>
    212                                                 ))}
    213                                         </div>
    214                                 </div>
    215                         </div>
    216                 </div>
    217         );
     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  );
    218288};
    219289
  • frontend/src/components/userProfile/ListenerView.tsx

    rf5bc95e r85512ff  
    11import { Album, Bookmark, Heart, ListMusic, Music } from "lucide-react";
    22import { useState } from "react";
    3 import { useNavigate } from "react-router-dom";
     3import { useNavigate, useParams } from "react-router-dom";
    44import { toast } from "react-toastify";
    55import axiosInstance, { baseURL } from "../../api/axiosInstance";
    66import type { MusicalEntity, Playlist } from "../../utils/types";
    77import SongItem from "../SongItem";
     8import { useAuth } from "../../context/authContext";
    89
    910interface ListenerViewProps {
    10         likedEntities: MusicalEntity[] | [];
    11         createdPlaylists: Playlist[] | [];
    12         savedPlaylists: Playlist[] | [];
     11  likedEntities: MusicalEntity[] | [];
     12  createdPlaylists: Playlist[] | [];
     13  savedPlaylists: Playlist[] | [];
    1314}
    1415
    1516const ListenerView = ({
    16         likedEntities,
    17         createdPlaylists,
    18         savedPlaylists,
     17  likedEntities,
     18  createdPlaylists,
     19  savedPlaylists,
    1920}: ListenerViewProps) => {
    20         const navigate = useNavigate();
    21         const [items, setItems] = useState(likedEntities);
    22         const [savedItems, setSavedItems] = useState(savedPlaylists);
    23 
    24         const likedSongs = items.filter((e) => e.type === "SONG");
    25         const likedAlbums = items.filter((e) => e.type === "ALBUM");
    26 
    27         const handleSavePlaylist = async (
    28                 e: React.MouseEvent,
    29                 playlistId: number,
    30                 playlistName: string,
    31         ) => {
    32                 e.stopPropagation();
    33 
    34                 try {
    35                         const response = await axiosInstance.post(`/playlist/${playlistId}/save`);
    36                         const data = response.data;
    37 
    38                         setSavedItems((prevItems) =>
    39                                 data.isSaved
    40                                         ? [...prevItems, prevItems.find((p) => p.id === playlistId)!]
    41                                         : prevItems.filter((p) => p.id !== playlistId),
    42                         );
    43 
    44                         toast.success(
    45                                 data.isSaved
    46                                         ? `Saved "${playlistName}" to your library`
    47                                         : `Removed "${playlistName}" from your library`,
    48                         );
    49                 } catch (err: any) {
    50                         toast.error(err.response?.data?.error || "Failed to save the playlist");
    51                 }
    52         };
    53 
    54         const handleLike = async (id: number, title: string) => {
    55                 try {
    56                         const response = await axiosInstance.post(`/musical-entity/${id}/like`);
    57                         const data = response.data;
    58 
    59                         setItems((prevItems) =>
    60                                 prevItems.map((item) =>
    61                                         item.id === data.entityId
    62                                                 ? { ...item, isLikedByCurrentUser: data.isLiked }
    63                                                 : item,
    64                                 ),
    65                         );
    66 
    67                         toast.success(
    68                                 data.isLiked ? `Liked "${title}"` : `Removed "${title}" from likes`,
    69                         );
    70                 } catch (err: any) {
    71                         toast.error(err.response?.data?.error || "Failed to like the item");
    72                 }
    73         };
    74 
    75         return (
    76                 <div className="mt-8 space-y-12">
    77                         {createdPlaylists && createdPlaylists.length > 0 && (
    78                                 <section>
    79                                         <div className="flex items-center gap-3 mb-6">
    80                                                 <ListMusic className="w-6 h-6 text-[#1db954]" />
    81                                                 <h3 className="text-2xl font-bold text-white">Created Playlists</h3>
    82                                                 <span className="text-sm text-gray-500">
    83                                                         ({createdPlaylists.length})
    84                                                 </span>
    85                                         </div>
    86 
    87                                         <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
    88                                                 {createdPlaylists.map((playlist) => (
    89                                                         <div
    90                                                                 key={playlist.id}
    91                                                                 className="group cursor-pointer"
    92                                                                 onClick={() => navigate(`/collection/playlist/${playlist.id}`)}
    93                                                         >
    94                                                                 <div className="relative aspect-square rounded-lg overflow-hidden bg-[#181818] mb-3 shadow-md group-hover:shadow-xl transition-all">
    95                                                                         <img
    96                                                                                 src={
    97                                                                                         playlist.cover
    98                                                                                                 ? `${baseURL}/${playlist.cover}`
    99                                                                                                 : "/favicon.png"
    100                                                                                 }
    101                                                                                 alt={playlist.name}
    102                                                                                 className="w-full h-full object-cover"
    103                                                                                 onError={(e) => {
    104                                                                                         (e.target as HTMLImageElement).src = "/favicon.png";
    105                                                                                 }}
    106                                                                         />
    107 
    108                                                                         <button
    109                                                                                 onClick={(e) =>
    110                                                                                         handleSavePlaylist(e, playlist.id, playlist.name)
    111                                                                                 }
    112                                                                                 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"
    113                                                                                 title={playlist.isSavedByCurrentUser ? "Unsave" : "Save"}
    114                                                                         >
    115                                                                                 <Bookmark
    116                                                                                         className={`w-5 h-5 ${
    117                                                                                                 playlist.isSavedByCurrentUser
    118                                                                                                         ? "fill-[#1db954] text-[#1db954]"
    119                                                                                                         : "text-gray-400"
    120                                                                                         }`}
    121                                                                                 />
    122                                                                         </button>
    123                                                                 </div>
    124                                                                 <p className="text-sm font-semibold text-white truncate group-hover:text-[#1db954] transition-colors">
    125                                                                         {playlist.name}
    126                                                                 </p>
    127                                                                 <p className="text-xs text-gray-400 truncate">
    128                                                                         {playlist.creatorName}
    129                                                                 </p>
    130                                                         </div>
    131                                                 ))}
    132                                         </div>
    133                                 </section>
    134                         )}
    135 
    136                         {savedItems && savedItems.length > 0 && (
    137                                 <section>
    138                                         <div className="flex items-center gap-3 mb-6">
    139                                                 <Bookmark className="w-6 h-6 text-[#1db954] fill-[#1db954]" />
    140                                                 <h3 className="text-2xl font-bold text-white">Saved Playlists</h3>
    141                                                 <span className="text-sm text-gray-500">({savedItems.length})</span>
    142                                         </div>
    143 
    144                                         <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
    145                                                 {savedItems.map((playlist) => (
    146                                                         <div
    147                                                                 key={playlist.id}
    148                                                                 className="group cursor-pointer"
    149                                                                 onClick={() => navigate(`/collection/playlist/${playlist.id}`)}
    150                                                         >
    151                                                                 <div className="relative aspect-square rounded-lg overflow-hidden bg-[#181818] mb-3 shadow-md group-hover:shadow-xl transition-all">
    152                                                                         <img
    153                                                                                 src={
    154                                                                                         playlist.cover
    155                                                                                                 ? `${baseURL}/${playlist.cover}`
    156                                                                                                 : "/favicon.png"
    157                                                                                 }
    158                                                                                 alt={playlist.name}
    159                                                                                 className="w-full h-full object-cover"
    160                                                                                 onError={(e) => {
    161                                                                                         (e.target as HTMLImageElement).src = "/favicon.png";
    162                                                                                 }}
    163                                                                         />
    164 
    165                                                                         <button
    166                                                                                 onClick={(e) =>
    167                                                                                         handleSavePlaylist(e, playlist.id, playlist.name)
    168                                                                                 }
    169                                                                                 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"
    170                                                                                 title="Unsave"
    171                                                                         >
    172                                                                                 <Bookmark className="w-5 h-5 fill-[#1db954] text-[#1db954]" />
    173                                                                         </button>
    174                                                                 </div>
    175                                                                 <p className="text-sm font-semibold text-white truncate group-hover:text-[#1db954] transition-colors">
    176                                                                         {playlist.name}
    177                                                                 </p>
    178                                                                 <p className="text-xs text-gray-400 truncate">
    179                                                                         {playlist.creatorName}
    180                                                                 </p>
    181                                                         </div>
    182                                                 ))}
    183                                         </div>
    184                                 </section>
    185                         )}
    186 
    187                         {likedSongs && likedSongs.length > 0 && (
    188                                 <section>
    189                                         <div className="flex items-center gap-3 mb-6">
    190                                                 <Heart className="w-6 h-6 text-red-500 fill-red-500" />
    191                                                 <h3 className="text-2xl font-bold text-white">Liked Songs</h3>
    192                                                 <span className="text-sm text-gray-500">
    193                                                         ({likedSongs?.length})
    194                                                 </span>
    195                                         </div>
    196 
    197                                         <div className="space-y-1">
    198                                                 {likedSongs.map((song, index) => (
    199                                                         <SongItem
    200                                                                 key={song.id}
    201                                                                 song={{
    202                                                                         id: song.id,
    203                                                                         title: song.title,
    204                                                                         cover: song.cover
    205                                                                                 ? `${baseURL}/${song.cover}`
    206                                                                                 : "/favicon.png",
    207                                                                         genre: song.genre,
    208                                                                         link: (song as any).link,
    209                                                                         releasedBy: song.releasedBy,
    210                                                                         isLikedByCurrentUser: song.isLikedByCurrentUser,
    211                                                                 }}
    212                                                                 index={index + 1}
    213                                                                 onLikeToggle={() => handleLike(song.id, song.title)}
    214                                                         />
    215                                                 ))}
    216                                         </div>
    217                                 </section>
    218                         )}
    219 
    220                         {likedAlbums && likedAlbums.length > 0 && (
    221                                 <section>
    222                                         <div className="flex items-center gap-3 mb-6">
    223                                                 <Album className="w-6 h-6 text-[#1db954]" />
    224                                                 <h3 className="text-2xl font-bold text-white">Liked Albums</h3>
    225                                                 <span className="text-sm text-gray-500">
    226                                                         ({likedAlbums.length})
    227                                                 </span>
    228                                         </div>
    229 
    230                                         <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
    231                                                 {likedAlbums.map((album) => (
    232                                                         <div
    233                                                                 key={album.id}
    234                                                                 className="group cursor-pointer"
    235                                                                 onClick={() => navigate(`/collection/album/${album.id}`)}
    236                                                         >
    237                                                                 <div className="relative aspect-square rounded-lg overflow-hidden bg-[#181818] mb-3 shadow-md group-hover:shadow-xl transition-all">
    238                                                                         <img
    239                                                                                 src={
    240                                                                                         album.cover ? `${baseURL}/${album.cover}` : "/favicon.png"
    241                                                                                 }
    242                                                                                 alt={album.title}
    243                                                                                 className="w-full h-full object-cover"
    244                                                                                 onError={(e) => {
    245                                                                                         (e.target as HTMLImageElement).src = "/favicon.png";
    246                                                                                 }}
    247                                                                         />
    248 
    249                                                                         <button
    250                                                                                 onClick={(e) => {
    251                                                                                         e.stopPropagation();
    252                                                                                         handleLike(album.id, album.title);
    253                                                                                 }}
    254                                                                                 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"
    255                                                                                 title={album.isLikedByCurrentUser ? "Unlike" : "Like"}
    256                                                                         >
    257                                                                                 <svg
    258                                                                                         className="w-5 h-5"
    259                                                                                         fill={album.isLikedByCurrentUser ? "#ef4444" : "none"}
    260                                                                                         stroke={
    261                                                                                                 album.isLikedByCurrentUser ? "#ef4444" : "#9ca3af"
    262                                                                                         }
    263                                                                                         strokeWidth="2"
    264                                                                                         viewBox="0 0 24 24"
    265                                                                                 >
    266                                                                                         <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" />
    267                                                                                 </svg>
    268                                                                         </button>
    269                                                                 </div>
    270                                                                 <p className="font-semibold text-sm text-white truncate group-hover:text-[#1db954] transition-colors">
    271                                                                         {album.title}
    272                                                                 </p>
    273                                                                 <p className="text-xs text-gray-400 truncate mt-1">
    274                                                                         {album.releasedBy}
    275                                                                 </p>
    276                                                         </div>
    277                                                 ))}
    278                                         </div>
    279                                 </section>
    280                         )}
    281 
    282                         {likedEntities &&
    283                                 likedEntities.length === 0 &&
    284                                 (!createdPlaylists || createdPlaylists.length === 0) &&
    285                                 (!savedItems || savedItems.length === 0) && (
    286                                         <div className="flex flex-col items-center justify-center py-16 text-gray-500">
    287                                                 <Music className="w-20 h-20 mb-4 opacity-20" />
    288                                                 <p className="text-lg font-medium">Nothing here yet</p>
    289                                                 <p className="text-sm mt-2">
    290                                                         Start exploring music to build your collection
    291                                                 </p>
    292                                         </div>
    293                                 )}
    294                 </div>
    295         );
     21  const navigate = useNavigate();
     22  const { username: usernameParam } = useParams();
     23  const { user: currentUser } = useAuth();
     24  const [items, setItems] = useState(likedEntities);
     25  const [createdItems, setCreatedItems] = useState(createdPlaylists);
     26  const [savedItems, setSavedItems] = useState(savedPlaylists);
     27
     28  const likedSongs = items.filter((e) => e.type === "SONG");
     29  const likedAlbums = items.filter((e) => e.type === "ALBUM");
     30  const username = usernameParam || currentUser?.username;
     31  const isOwnProfile = currentUser?.username === username;
     32
     33  const handleSavePlaylist = async (
     34    e: React.MouseEvent,
     35    playlistId: number,
     36    playlistName: string,
     37  ) => {
     38    e.stopPropagation();
     39
     40    try {
     41      const response = await axiosInstance.post(
     42        `/playlists/${playlistId}/save`,
     43      );
     44      const data = response.data;
     45
     46      setCreatedItems((prevItems) =>
     47        prevItems.map((p) =>
     48          p.id === playlistId
     49            ? { ...p, isSavedByCurrentUser: data.isSavedByCurrentUser }
     50            : p,
     51        ),
     52      );
     53
     54      setSavedItems((prevItems) =>
     55        prevItems.map((p) =>
     56          p.id === playlistId
     57            ? { ...p, isSavedByCurrentUser: data.isSavedByCurrentUser }
     58            : p,
     59        ),
     60      );
     61
     62      if (isOwnProfile) {
     63        if (data.isSavedByCurrentUser) {
     64          const playlistToAdd = createdItems.find((p) => p.id === playlistId);
     65          if (playlistToAdd && !savedItems.find((p) => p.id === playlistId)) {
     66            setSavedItems((prevItems) => [
     67              ...prevItems,
     68              { ...playlistToAdd, isSavedByCurrentUser: true },
     69            ]);
     70          }
     71        } else {
     72          setSavedItems((prevItems) =>
     73            prevItems.filter((p) => p.id !== playlistId),
     74          );
     75        }
     76      }
     77
     78      toast.success(
     79        data.isSavedByCurrentUser
     80          ? `Saved "${playlistName}" to your library`
     81          : `Removed "${playlistName}" from your library`,
     82      );
     83    } catch (err: any) {
     84      toast.error(err.response?.data?.error || "Failed to save the playlist");
     85    }
     86  };
     87
     88  const handleLike = async (id: number, title: string) => {
     89    try {
     90      const response = await axiosInstance.post(`/musical-entity/${id}/like`);
     91      const data = response.data;
     92
     93      setItems((prevItems) =>
     94        prevItems.map((item) =>
     95          item.id === data.entityId
     96            ? { ...item, isLikedByCurrentUser: data.isLiked }
     97            : item,
     98        ),
     99      );
     100
     101      toast.success(
     102        data.isLiked ? `Liked "${title}"` : `Removed "${title}" from likes`,
     103      );
     104    } catch (err: any) {
     105      toast.error(err.response?.data?.error || "Failed to like the item");
     106    }
     107  };
     108
     109  return (
     110    <div className="mt-8 space-y-12">
     111      {createdItems && createdItems.length > 0 && (
     112        <section>
     113          <div className="flex items-center gap-3 mb-6">
     114            <ListMusic className="w-6 h-6 text-[#1db954]" />
     115            <h3 className="text-2xl font-bold text-white">Created Playlists</h3>
     116            <span className="text-sm text-gray-500">
     117              ({createdItems.length})
     118            </span>
     119          </div>
     120
     121          <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
     122            {createdItems.map((playlist) => (
     123              <div
     124                key={playlist.id}
     125                className="group cursor-pointer"
     126                onClick={() => navigate(`/collection/playlist/${playlist.id}`)}
     127              >
     128                <div className="relative aspect-square rounded-lg overflow-hidden bg-[#181818] mb-3 shadow-md group-hover:shadow-xl transition-all">
     129                  <img
     130                    src={
     131                      playlist.cover
     132                        ? `${baseURL}/${playlist.cover}`
     133                        : "/favicon.png"
     134                    }
     135                    alt={playlist.name}
     136                    className="w-full h-full object-cover"
     137                    onError={(e) => {
     138                      (e.target as HTMLImageElement).src = "/favicon.png";
     139                    }}
     140                  />
     141                  {!isOwnProfile &&
     142                    currentUser?.username != playlist.creatorUsername && (
     143                      <button
     144                        onClick={(e) =>
     145                          handleSavePlaylist(e, playlist.id, playlist.name)
     146                        }
     147                        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"
     148                        title={
     149                          playlist.isSavedByCurrentUser ? "Unsave" : "Save"
     150                        }
     151                      >
     152                        <Bookmark
     153                          className={`w-5 h-5 ${
     154                            playlist.isSavedByCurrentUser
     155                              ? "fill-[#1db954] text-[#1db954]"
     156                              : "text-gray-400"
     157                          }`}
     158                        />
     159                      </button>
     160                    )}
     161                </div>
     162                <p className="text-sm font-semibold text-white truncate group-hover:text-[#1db954] transition-colors">
     163                  {playlist.name}
     164                </p>
     165                <p className="text-xs text-gray-400 truncate">
     166                  {playlist.creatorName}
     167                </p>
     168              </div>
     169            ))}
     170          </div>
     171        </section>
     172      )}
     173
     174      {savedItems && savedItems.length > 0 && (
     175        <section>
     176          <div className="flex items-center gap-3 mb-6">
     177            <Bookmark className="w-6 h-6 text-[#1db954] fill-[#1db954]" />
     178            <h3 className="text-2xl font-bold text-white">Saved Playlists</h3>
     179            <span className="text-sm text-gray-500">({savedItems.length})</span>
     180          </div>
     181
     182          <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
     183            {savedItems.map((playlist) => (
     184              <div
     185                key={playlist.id}
     186                className="group cursor-pointer"
     187                onClick={() => navigate(`/collection/playlist/${playlist.id}`)}
     188              >
     189                <div className="relative aspect-square rounded-lg overflow-hidden bg-[#181818] mb-3 shadow-md group-hover:shadow-xl transition-all">
     190                  <img
     191                    src={
     192                      playlist.cover
     193                        ? `${baseURL}/${playlist.cover}`
     194                        : "/favicon.png"
     195                    }
     196                    alt={playlist.name}
     197                    className="w-full h-full object-cover"
     198                    onError={(e) => {
     199                      (e.target as HTMLImageElement).src = "/favicon.png";
     200                    }}
     201                  />
     202
     203                  {currentUser?.username != playlist.creatorUsername && (
     204                    <button
     205                      onClick={(e) =>
     206                        handleSavePlaylist(e, playlist.id, playlist.name)
     207                      }
     208                      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"
     209                      title={playlist.isSavedByCurrentUser ? "Unsave" : "Save"}
     210                    >
     211                      <Bookmark
     212                        className={`w-5 h-5 ${
     213                          playlist.isSavedByCurrentUser
     214                            ? "fill-[#1db954] text-[#1db954]"
     215                            : "text-gray-400"
     216                        }`}
     217                      />
     218                    </button>
     219                  )}
     220                </div>
     221                <p className="text-sm font-semibold text-white truncate group-hover:text-[#1db954] transition-colors">
     222                  {playlist.name}
     223                </p>
     224                <p className="text-xs text-gray-400 truncate">
     225                  {playlist.creatorName}
     226                </p>
     227              </div>
     228            ))}
     229          </div>
     230        </section>
     231      )}
     232
     233      {likedSongs && likedSongs.length > 0 && (
     234        <section>
     235          <div className="flex items-center gap-3 mb-6">
     236            <Heart className="w-6 h-6 text-red-500 fill-red-500" />
     237            <h3 className="text-2xl font-bold text-white">Liked Songs</h3>
     238            <span className="text-sm text-gray-500">
     239              ({likedSongs?.length})
     240            </span>
     241          </div>
     242
     243          <div className="space-y-1">
     244            {likedSongs.map((song, index) => (
     245              <SongItem
     246                key={song.id}
     247                song={{
     248                  id: song.id,
     249                  title: song.title,
     250                  cover: song.cover
     251                    ? `${baseURL}/${song.cover}`
     252                    : "/favicon.png",
     253                  genre: song.genre,
     254                  link: (song as any).link,
     255                  releasedBy: song.releasedBy,
     256                  isLikedByCurrentUser: song.isLikedByCurrentUser,
     257                }}
     258                index={index + 1}
     259                onLikeToggle={() => handleLike(song.id, song.title)}
     260              />
     261            ))}
     262          </div>
     263        </section>
     264      )}
     265
     266      {likedAlbums && likedAlbums.length > 0 && (
     267        <section>
     268          <div className="flex items-center gap-3 mb-6">
     269            <Album className="w-6 h-6 text-[#1db954]" />
     270            <h3 className="text-2xl font-bold text-white">Liked Albums</h3>
     271            <span className="text-sm text-gray-500">
     272              ({likedAlbums.length})
     273            </span>
     274          </div>
     275
     276          <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
     277            {likedAlbums.map((album) => (
     278              <div
     279                key={album.id}
     280                className="group cursor-pointer"
     281                onClick={() => navigate(`/collection/album/${album.id}`)}
     282              >
     283                <div className="relative aspect-square rounded-lg overflow-hidden bg-[#181818] mb-3 shadow-md group-hover:shadow-xl transition-all">
     284                  <img
     285                    src={
     286                      album.cover ? `${baseURL}/${album.cover}` : "/favicon.png"
     287                    }
     288                    alt={album.title}
     289                    className="w-full h-full object-cover"
     290                    onError={(e) => {
     291                      (e.target as HTMLImageElement).src = "/favicon.png";
     292                    }}
     293                  />
     294
     295                  <button
     296                    onClick={(e) => {
     297                      e.stopPropagation();
     298                      handleLike(album.id, album.title);
     299                    }}
     300                    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"
     301                    title={album.isLikedByCurrentUser ? "Unlike" : "Like"}
     302                  >
     303                    <svg
     304                      className="w-5 h-5"
     305                      fill={album.isLikedByCurrentUser ? "#ef4444" : "none"}
     306                      stroke={
     307                        album.isLikedByCurrentUser ? "#ef4444" : "#9ca3af"
     308                      }
     309                      strokeWidth="2"
     310                      viewBox="0 0 24 24"
     311                    >
     312                      <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" />
     313                    </svg>
     314                  </button>
     315                </div>
     316                <p className="font-semibold text-sm text-white truncate group-hover:text-[#1db954] transition-colors">
     317                  {album.title}
     318                </p>
     319                <p className="text-xs text-gray-400 truncate mt-1">
     320                  {album.releasedBy}
     321                </p>
     322              </div>
     323            ))}
     324          </div>
     325        </section>
     326      )}
     327
     328      {likedEntities &&
     329        likedEntities.length === 0 &&
     330        (!createdItems || createdItems.length === 0) &&
     331        (!savedItems || savedItems.length === 0) && (
     332          <div className="flex flex-col items-center justify-center py-16 text-gray-500">
     333            <Music className="w-20 h-20 mb-4 opacity-20" />
     334            <p className="text-lg font-medium">Nothing here yet</p>
     335            <p className="text-sm mt-2">
     336              Start exploring music to build your collection
     337            </p>
     338          </div>
     339        )}
     340    </div>
     341  );
    296342};
    297343
Note: See TracChangeset for help on using the changeset viewer.