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

save playlist

File:
1 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
Note: See TracChangeset for help on using the changeset viewer.