Ignore:
Timestamp:
02/15/26 23:28:14 (5 months ago)
Author:
Dimitar Arsov <dimitararsov04@…>
Branches:
main
Children:
615dcee, c807e22
Parents:
c8baad1
Message:

create new playlist, delete playlists and add song to playlist on playlist creation

Location:
frontend/src/components
Files:
6 edited

Legend:

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

    rc8baad1 rd1ee039  
    273273        onClose={() => setIsModalOpen(false)}
    274274        onSuccess={() => refreshPlaylists(false)}
     275        songId={null}
    275276      />
    276277    </>
  • frontend/src/components/SongItem.tsx

    rc8baad1 rd1ee039  
    6767
    6868  const isPlaying = currentSong?.id === song.id;
    69 
    70   const handleCreateNewPlaylist = () => {
    71     console.log(`Creating new playlist for song ${song.id}`);
    72     // TODO: Implement actual playlist creation
    73     setPlaylistOpen(false);
    74   };
    7569
    7670  const subtitleParts: string[] = [];
     
    240234          usePortal={true}
    241235          direction={dropdownDirection}
    242           onCreateNewPlaylist={handleCreateNewPlaylist}
    243236        />
    244237      </div>
  • frontend/src/components/playlist/CreatePlaylistModal.tsx

    rc8baad1 rd1ee039  
    88  onClose,
    99  onSuccess,
     10  songId,
    1011}: CreatePlaylistModalProps) => {
    1112  const [playlistName, setPlaylistName] = useState("");
     
    3940    setIsSubmitting(true);
    4041    try {
    41       await axiosInstance.post(
    42         `/playlists?playlistName=${encodeURIComponent(playlistName.trim())}`,
    43       );
     42      await axiosInstance.post("/playlists", null, {
     43        params: {
     44          playlistName: playlistName.trim(),
     45          ...(songId != null && { songId }),
     46        },
     47      });
     48
    4449      toast.success("Playlist created successfully!");
    45       onSuccess();
     50      await onSuccess();
    4651      handleClose();
    4752    } catch (err: any) {
  • frontend/src/components/playlist/PlaylistDropdown.tsx

    rc8baad1 rd1ee039  
    33import axiosInstance from "../../api/axiosInstance";
    44import { useCreatedPlaylists } from "../../context/playlistContext";
     5import CreatePlaylistModal from "./CreatePlaylistModal";
    56
    67interface PlaylistDropdownProps {
     
    1314
    1415  direction?: "above" | "below";
    15 
    16   onCreateNewPlaylist?: () => void;
    1716}
    1817
     
    2423  usePortal = false,
    2524  direction = "above",
    26   onCreateNewPlaylist,
    2725}: PlaylistDropdownProps) => {
    2826  const { createdPlaylists, refreshPlaylists } = useCreatedPlaylists();
     
    3129  );
    3230  const [loading, setLoading] = useState(false);
    33   const [processingPlaylistId, setProcessingPlaylistId] = useState<
    34     number | null
    35   >(null);
     31  const [processingIds, setProcessingIds] = useState<Set<number>>(new Set());
     32  const [isModalOpen, setIsModalOpen] = useState(false);
    3633  const dropdownRef = useRef<HTMLDivElement>(null);
    3734
     
    7572
    7673  const handleTogglePlaylist = async (playlistId: number, songId: number) => {
    77     if (processingPlaylistId !== null) return;
    78 
    79     setProcessingPlaylistId(playlistId);
     74    if (processingIds.has(playlistId)) return;
     75
     76    setProcessingIds((prev) => new Set(prev).add(playlistId));
    8077
    8178    try {
     
    104101      refreshPlaylists(true);
    105102      setTimeout(() => {
    106         setProcessingPlaylistId(null);
     103        setProcessingIds((prev) => {
     104          const next = new Set(prev);
     105          next.delete(playlistId);
     106          return next;
     107        });
    107108      }, 500);
    108109    }
     
    110111
    111112  const handleCreateNew = () => {
    112     onCreateNewPlaylist?.();
    113113    onClose();
     114    setIsModalOpen(true);
    114115  };
    115116
    116   if (!isOpen) return null;
     117  const handleModalSuccess = async () => {
     118    await refreshPlaylists(false);
     119  };
    117120
    118121  const inlinePositionClass =
     
    121124      : "absolute right-0 bottom-full mb-2";
    122125
    123   const dropdownContent = (
     126  const dropdownContent = !isOpen ? null : (
    124127    <div
    125128      ref={dropdownRef}
    126       className={`${usePortal ? "fixed" : inlinePositionClass} w-56 bg-[#282828] rounded-lg shadow-2xl py-2 z-[9999] border border-white/10 max-h-60 overflow-y-auto custom-scrollbar`}
     129      className={`${usePortal ? "fixed" : inlinePositionClass} w-56 bg-[#282828] rounded-lg shadow-2xl py-2 z-9999 border border-white/10 max-h-60 overflow-y-auto custom-scrollbar`}
    127130      style={
    128131        usePortal && position
     
    147150      ) : createdPlaylists && createdPlaylists.length > 0 ? (
    148151        createdPlaylists.map((playlist) => {
    149           const isProcessing = processingPlaylistId === playlist.id;
     152          const isProcessing = processingIds.has(playlist.id);
    150153          const isChecked = containingPlaylistIds.includes(playlist.id);
    151154
     
    153156            <label
    154157              key={playlist.id}
    155               className={`flex items-center px-4 py-2 hover:bg-white/10 transition-colors group/item ${
     158              className={`flex items-center gap-1.5 px-4 py-2 hover:bg-white/10 transition-colors group/item ${
    156159                isProcessing ? "pointer-events-none" : "cursor-pointer"
    157160              }`}
    158161              onClick={(e) => e.stopPropagation()}
    159162            >
    160               <div className="relative flex items-center justify-center">
     163              <div className="relative flex items-center justify-center  shrink-0">
    161164                <input
    162165                  type="checkbox"
    163166                  className="peer sr-only"
    164167                  checked={isChecked}
    165                   disabled={processingPlaylistId !== null}
     168                  disabled={isProcessing}
    166169                  onChange={() => handleTogglePlaylist(playlist.id, songId)}
    167170                />
     
    172175                      : isChecked
    173176                        ? "bg-[#1db954] border-[#1db954]"
    174                         : "border-gray-500"
     177                        : "border-gray-500 text=wjot"
    175178                  }`}
    176179                >
     
    182185                </div>
    183186                <svg
    184                   className={`absolute w-3 h-3  transition-opacity ${
     187                  className={`absolute w-3 h-3 transition-opacity ${
    185188                    isChecked && !isProcessing ? "opacity-100" : "opacity-0"
    186189                  }`}
    187190                  fill="none"
    188                   stroke="currentColor"
     191                  stroke="white"
    189192                  strokeWidth="3"
    190193                  viewBox="0 0 24 24"
     
    198201              </div>
    199202              <span
    200                 className={`ml-3 text-sm truncate transition-all ${
     203                className={`text-sm truncate transition-all ${
    201204                  isProcessing
    202205                    ? "text-[#1db954] animate-pulse"
     
    206209                {playlist.name}
    207210              </span>
    208               {isProcessing && (
    209                 <span className="ml-auto text-xs text-[#1db954] animate-pulse">
    210                   •••
    211                 </span>
    212               )}
    213211            </label>
    214212          );
     
    245243  );
    246244
    247   return usePortal
    248     ? createPortal(dropdownContent, document.body)
    249     : dropdownContent;
     245  const dropdown = dropdownContent
     246    ? usePortal
     247      ? createPortal(dropdownContent, document.body)
     248      : dropdownContent
     249    : null;
     250
     251  return (
     252    <>
     253      {dropdown}
     254      {createPortal(
     255        <CreatePlaylistModal
     256          isOpen={isModalOpen}
     257          onClose={() => setIsModalOpen(false)}
     258          onSuccess={handleModalSuccess}
     259          songId={songId}
     260        />,
     261        document.body,
     262      )}
     263    </>
     264  );
    250265};
    251266
  • frontend/src/components/userProfile/ListenerView.tsx

    rc8baad1 rd1ee039  
    1 import { Album, Bookmark, Heart, ListMusic, Music } from "lucide-react";
     1import { Album, Bookmark, Heart, ListMusic, Music, Trash2 } from "lucide-react";
    22import { useState } from "react";
    33import { useNavigate, useParams } from "react-router-dom";
     
    77import SongItem from "../SongItem";
    88import { useAuth } from "../../context/authContext";
     9import { useCreatedPlaylists } from "../../context/playlistContext";
    910
    1011interface ListenerViewProps {
     
    2021}: ListenerViewProps) => {
    2122  const navigate = useNavigate();
     23  const { refreshPlaylists } = useCreatedPlaylists();
    2224  const { username: usernameParam } = useParams();
    2325  const { user: currentUser } = useAuth();
     
    8688    } catch (err: any) {
    8789      toast.error(err.response?.data?.error || "Failed to save the playlist");
     90    }
     91  };
     92
     93  const handleDeletePlaylist = async (
     94    e: React.MouseEvent,
     95    playlistId: number,
     96    playlistName: string,
     97  ) => {
     98    e.stopPropagation();
     99
     100    try {
     101      await axiosInstance.delete(`/playlists/${playlistId}`);
     102      refreshPlaylists(false);
     103
     104      setCreatedItems((prev) => prev.filter((p) => p.id !== playlistId));
     105      toast.success(`Deleted "${playlistName}"`);
     106    } catch (err: any) {
     107      toast.error(err.response?.data?.error || "Failed to delete playlist");
    88108    }
    89109  };
     
    142162                    }}
    143163                  />
    144                   {!isOwnProfile &&
     164                  {isOwnProfile ? (
     165                    <button
     166                      onClick={(e) =>
     167                        handleDeletePlaylist(e, playlist.id, playlist.name)
     168                      }
     169                      className="absolute top-2 right-2 p-2 bg-black/70 hover:bg-red-600/90 rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100 cursor-pointer"
     170                      title="Delete playlist"
     171                    >
     172                      <Trash2 className="w-5 h-5 text-gray-400 hover:text-white transition-colors" />
     173                    </button>
     174                  ) : (
    145175                    currentUser?.username != playlist.creatorUsername && (
    146176                      <button
     
    161191                        />
    162192                      </button>
    163                     )}
     193                    )
     194                  )}
    164195                </div>
    165196                <p className="text-sm font-semibold text-white truncate group-hover:text-[#1db954] transition-colors">
  • frontend/src/components/userProfile/UserListModal.tsx

    rc8baad1 rd1ee039  
    22import { baseURL } from "../../api/axiosInstance";
    33import type { BaseNonAdminUser } from "../../utils/types";
     4import { useEffect } from "react";
    45
    56interface ModalProps {
    6         title: string;
    7         users: BaseNonAdminUser[];
    8         onClose: () => void;
    9         onFollowToggle: (targetUsername: string) => Promise<void>;
     7  title: string;
     8  users: BaseNonAdminUser[];
     9  onClose: () => void;
     10  onFollowToggle: (targetUsername: string) => Promise<void>;
    1011}
    1112
    1213const UserListModal = ({
    13         title,
    14         users,
    15         onClose,
    16         onFollowToggle,
     14  title,
     15  users,
     16  onClose,
     17  onFollowToggle,
    1718}: ModalProps) => {
    18         const navigate = useNavigate();
     19  const navigate = useNavigate();
     20  useEffect(() => {
     21    document.body.style.overflow = "hidden";
     22    return () => {
     23      document.body.style.overflow = "";
     24    };
     25  }, []);
    1926
    20         return (
    21                 <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
    22                         <div className="bg-[#1a1a2e] border border-white/10 rounded-xl shadow-2xl w-full max-w-md max-h-[70vh] flex flex-col">
    23                                 <div className="p-4 border-b border-white/10 flex justify-between items-center">
    24                                         <h2 className="text-xl font-bold text-white">{title}</h2>
    25                                         <button
    26                                                 onClick={onClose}
    27                                                 className="text-gray-400 hover:text-white text-2xl cursor-pointer transition-colors"
    28                                         >
    29                                                 &times;
    30                                         </button>
    31                                 </div>
     27  return (
     28    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
     29      <div className="bg-[#1a1a2e] border border-white/10 rounded-xl shadow-2xl w-full max-w-md max-h-[70vh] flex flex-col">
     30        <div className="p-4 border-b border-white/10 flex justify-between items-center">
     31          <h2 className="text-xl font-bold text-white">{title}</h2>
     32          <button
     33            onClick={onClose}
     34            className="text-gray-400 hover:text-white text-2xl cursor-pointer transition-colors"
     35          >
     36            &times;
     37          </button>
     38        </div>
    3239
    33                                 <div className="overflow-y-auto p-4 flex-1">
    34                                         {users.length === 0 ? (
    35                                                 <p className="text-center text-gray-500 py-8">No users found.</p>
    36                                         ) : (
    37                                                 users.map((u) => (
    38                                                         <div
    39                                                                 key={u.username}
    40                                                                 className="flex items-center justify-between p-3 hover:bg-white/5 rounded-lg transition-colors"
    41                                                         >
    42                                                                 <div
    43                                                                         className="flex items-center gap-4 cursor-pointer flex-1"
    44                                                                         onClick={() => {
    45                                                                                 onClose();
    46                                                                                 navigate(`/users/${u.username}`);
    47                                                                         }}
    48                                                                 >
    49                                                                         <div className="w-10 h-10 rounded-full bg-[#282828] overflow-hidden shrink-0 flex items-center justify-center">
    50                                                                                 {u.profilePhoto ? (
    51                                                                                         <img
    52                                                                                                 src={`${baseURL}/${u.profilePhoto}`}
    53                                                                                                 className="w-full h-full object-cover"
    54                                                                                                 alt=""
    55                                                                                         />
    56                                                                                 ) : (
    57                                                                                         <span className="text-[#1db954] font-bold">
    58                                                                                                 {u.fullName.charAt(0)}
    59                                                                                         </span>
    60                                                                                 )}
    61                                                                         </div>
    62                                                                         <div>
    63                                                                                 <p className="font-semibold text-white">{u.fullName}</p>
    64                                                                                 <p className="text-sm text-gray-400">@{u.username}</p>
    65                                                                         </div>
    66                                                                 </div>
     40        <div className="overflow-y-auto p-4 flex-1">
     41          {users.length === 0 ? (
     42            <p className="text-center text-gray-500 py-8">No users found.</p>
     43          ) : (
     44            users.map((u) => (
     45              <div
     46                key={u.username}
     47                className="flex items-center justify-between p-3 hover:bg-white/5 rounded-lg transition-colors"
     48              >
     49                <div
     50                  className="flex items-center gap-4 cursor-pointer flex-1"
     51                  onClick={() => {
     52                    onClose();
     53                    navigate(`/users/${u.username}`);
     54                  }}
     55                >
     56                  <div className="w-10 h-10 rounded-full bg-[#282828] overflow-hidden shrink-0 flex items-center justify-center">
     57                    {u.profilePhoto ? (
     58                      <img
     59                        src={`${baseURL}/${u.profilePhoto}`}
     60                        className="w-full h-full object-cover"
     61                        alt=""
     62                      />
     63                    ) : (
     64                      <span className="text-[#1db954] font-bold">
     65                        {u.fullName.charAt(0)}
     66                      </span>
     67                    )}
     68                  </div>
     69                  <div>
     70                    <p className="font-semibold text-white">{u.fullName}</p>
     71                    <p className="text-sm text-gray-400">@{u.username}</p>
     72                  </div>
     73                </div>
    6774
    68                                                                 <button
    69                                                                         onClick={() => onFollowToggle(u.username)}
    70                                                                         className={`px-4 py-1.5 text-sm font-medium rounded-full transition-colors cursor-pointer ${
    71                                                                                 u.isFollowedByCurrentUser
    72                                                                                         ? "bg-white/10 text-white hover:bg-white/20"
    73                                                                                         : "bg-[#1db954] text-black hover:bg-[#1ed760]"
    74                                                                         }`}
    75                                                                 >
    76                                                                         {u.isFollowedByCurrentUser ? "Following" : "Follow"}
    77                                                                 </button>
    78                                                         </div>
    79                                                 ))
    80                                         )}
    81                                 </div>
    82                         </div>
    83                         <div className="absolute inset-0 -z-10" onClick={onClose}></div>
    84                 </div>
    85         );
     75                <button
     76                  onClick={() => onFollowToggle(u.username)}
     77                  className={`px-4 py-1.5 text-sm font-medium rounded-full transition-colors cursor-pointer ${
     78                    u.isFollowedByCurrentUser
     79                      ? "bg-white/10 text-white hover:bg-white/20"
     80                      : "bg-[#1db954] text-black hover:bg-[#1ed760]"
     81                  }`}
     82                >
     83                  {u.isFollowedByCurrentUser ? "Following" : "Follow"}
     84                </button>
     85              </div>
     86            ))
     87          )}
     88        </div>
     89      </div>
     90      <div className="absolute inset-0 -z-10" onClick={onClose}></div>
     91    </div>
     92  );
    8693};
    8794
Note: See TracChangeset for help on using the changeset viewer.