Changeset 85512ff


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

save playlist

Files:
3 added
18 edited
2 moved

Legend:

Unmodified
Added
Removed
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/MusicalEntityController.java

    rf5bc95e r85512ff  
    11package com.ukim.finki.develop.finkwave.controller;
    22
    3 import com.ukim.finki.develop.finkwave.model.dto.LikeStatusDto;
     3import com.ukim.finki.develop.finkwave.model.dto.statusDto.LikeStatusDto;
    44import com.ukim.finki.develop.finkwave.model.dto.PublishAlbumRequestDto;
    55import com.ukim.finki.develop.finkwave.model.dto.PublishSongRequestDto;
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/NonAdminUserController.java

    rf5bc95e r85512ff  
    11package com.ukim.finki.develop.finkwave.controller;
    22
    3 import com.ukim.finki.develop.finkwave.model.dto.FollowStatusDto;
     3import com.ukim.finki.develop.finkwave.model.dto.statusDto.FollowStatusDto;
    44import com.ukim.finki.develop.finkwave.model.dto.NonAdminUserDto;
    55import com.ukim.finki.develop.finkwave.service.FollowService;
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/PlaylistController.java

    rf5bc95e r85512ff  
    33import com.ukim.finki.develop.finkwave.model.dto.BasicPlaylistDto;
    44import com.ukim.finki.develop.finkwave.model.dto.PlaylistDto;
     5import com.ukim.finki.develop.finkwave.model.dto.statusDto.SavePlaylistStatusDto;
    56import com.ukim.finki.develop.finkwave.service.PlaylistService;
    67import lombok.AllArgsConstructor;
    78import org.springframework.http.HttpEntity;
    89import org.springframework.http.ResponseEntity;
    9 import org.springframework.web.bind.annotation.GetMapping;
    10 import org.springframework.web.bind.annotation.PathVariable;
    11 import org.springframework.web.bind.annotation.RequestMapping;
    12 import org.springframework.web.bind.annotation.RestController;
     10import org.springframework.web.bind.annotation.*;
    1311
    1412import java.util.List;
     
    3028    }
    3129
     30    @PostMapping("/{id}/save")
     31    public HttpEntity<SavePlaylistStatusDto>savePlaylist(@PathVariable Long id) throws Exception {
     32        return ResponseEntity.ok(playlistService.savePlaylist(id));
     33    }
     34
    3235}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/SavedPlaylist.java

    rf5bc95e r85512ff  
    33import jakarta.persistence.*;
    44import lombok.Getter;
     5import lombok.NoArgsConstructor;
    56import lombok.Setter;
    67import org.hibernate.annotations.OnDelete;
     
    1011@Setter
    1112@Entity
     13@NoArgsConstructor
    1214@Table(name = "saved_playlists", schema = "project")
    1315public class SavedPlaylist {
     
    2729    private Playlist playlist;
    2830
     31    public SavedPlaylist( Listener listener, Playlist playlist) {
     32        this.id=new SavedPlaylistId(listener.getId(),playlist.getId());
     33        this.listener = listener;
     34        this.playlist = playlist;
     35    }
    2936}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/SavedPlaylistId.java

    rf5bc95e r85512ff  
    44import jakarta.persistence.Embeddable;
    55import lombok.Getter;
     6import lombok.NoArgsConstructor;
    67import lombok.Setter;
    78import org.hibernate.Hibernate;
     
    1314@Setter
    1415@Embeddable
     16@NoArgsConstructor
    1517public class SavedPlaylistId implements Serializable {
    1618    private static final long serialVersionUID = 678748242066896235L;
     
    2022    @Column(name = "playlist_id", nullable = false)
    2123    private Long playlistId;
     24
     25    public SavedPlaylistId(Long listenerId, Long playlistId) {
     26        this.listenerId = listenerId;
     27        this.playlistId = playlistId;
     28    }
    2229
    2330    @Override
     
    3037    }
    3138
     39
     40
    3241    @Override
    3342    public int hashCode() {
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/dto/PlaylistDto.java

    rf5bc95e r85512ff  
    1515    private String cover;
    1616    private String creatorName;
     17    private String creatorUsername;
    1718    private List<SongWithLinkDto>songsInPlaylist;
    1819    private Boolean isSavedByCurrentUser;
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/dto/statusDto/FollowStatusDto.java

    rf5bc95e r85512ff  
    1 package com.ukim.finki.develop.finkwave.model.dto;
     1package com.ukim.finki.develop.finkwave.model.dto.statusDto;
    22
    33public record FollowStatusDto(boolean isFollowing, Long followerCount, Long followingCount)
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/dto/statusDto/LikeStatusDto.java

    rf5bc95e r85512ff  
    1 package com.ukim.finki.develop.finkwave.model.dto;
     1package com.ukim.finki.develop.finkwave.model.dto.statusDto;
    22
    33public record LikeStatusDto(Long entityId,Boolean isLiked, String type) {
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/FollowService.java

    rf5bc95e r85512ff  
    77import com.ukim.finki.develop.finkwave.model.FollowId;
    88import com.ukim.finki.develop.finkwave.model.NonAdminUser;
    9 import com.ukim.finki.develop.finkwave.model.dto.FollowStatusDto;
     9import com.ukim.finki.develop.finkwave.model.dto.statusDto.FollowStatusDto;
    1010import com.ukim.finki.develop.finkwave.model.dto.NonAdminUserDto;
    1111import com.ukim.finki.develop.finkwave.repository.ArtistRepository;
     
    1616import org.springframework.stereotype.Service;
    1717
    18 import java.util.ArrayList;
    1918import java.util.List;
    2019import java.util.Optional;
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/LikeService.java

    rf5bc95e r85512ff  
    77import com.ukim.finki.develop.finkwave.model.Listener;
    88import com.ukim.finki.develop.finkwave.model.MusicalEntity;
    9 import com.ukim.finki.develop.finkwave.model.dto.LikeStatusDto;
     9import com.ukim.finki.develop.finkwave.model.dto.statusDto.LikeStatusDto;
    1010import com.ukim.finki.develop.finkwave.model.dto.MusicalEntityDto;
    1111import com.ukim.finki.develop.finkwave.repository.*;
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/NonAdminUserService.java

    rf5bc95e r85512ff  
    44import com.ukim.finki.develop.finkwave.model.*;
    55import com.ukim.finki.develop.finkwave.model.dto.*;
     6import com.ukim.finki.develop.finkwave.model.dto.statusDto.FollowStatusDto;
    67import com.ukim.finki.develop.finkwave.repository.*;
    78import com.ukim.finki.develop.finkwave.service.mappers.NonAdminUserMapper;
     
    123124
    124125
    125     private PlaylistDto mapToPlaylistDto(Playlist p, Set<Long> savedBy) {
     126    private PlaylistDto mapToPlaylistDto(Playlist p, Set<Long> visitorSavedPlaylistIds) {
    126127        return new PlaylistDto(
    127128                p.getId(),
     
    129130                p.getCover(),
    130131                p.getCreatedBy().getNonAdminUser().getUser().getFullName(),
     132                p.getCreatedBy().getNonAdminUser().getUser().getUsername(),
    131133                null,
    132                 savedBy.contains(p.getId())
     134                visitorSavedPlaylistIds.contains(p.getId())
    133135        );
    134136    }
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/PlaylistService.java

    rf5bc95e r85512ff  
    22
    33import com.ukim.finki.develop.finkwave.exceptions.PlaylistNotFoundException;
     4import com.ukim.finki.develop.finkwave.exceptions.UserNotFoundException;
     5import com.ukim.finki.develop.finkwave.model.Listener;
    46import com.ukim.finki.develop.finkwave.model.Playlist;
     7import com.ukim.finki.develop.finkwave.model.SavedPlaylist;
     8import com.ukim.finki.develop.finkwave.model.SavedPlaylistId;
    59import com.ukim.finki.develop.finkwave.model.dto.BasicPlaylistDto;
    610import com.ukim.finki.develop.finkwave.model.dto.PlaylistDto;
    711import com.ukim.finki.develop.finkwave.model.dto.SongWithLinkDto;
     12import com.ukim.finki.develop.finkwave.model.dto.statusDto.SavePlaylistStatusDto;
     13import com.ukim.finki.develop.finkwave.repository.ListenerRepository;
    814import com.ukim.finki.develop.finkwave.repository.PlaylistRepository;
    915import com.ukim.finki.develop.finkwave.repository.SavedPlaylistRepository;
    1016import com.ukim.finki.develop.finkwave.repository.SongRepository;
    1117import lombok.AllArgsConstructor;
     18import org.springframework.http.HttpStatus;
    1219import org.springframework.stereotype.Service;
    1320import org.springframework.transaction.annotation.Transactional;
     21import org.springframework.web.server.ResponseStatusException;
    1422
    1523import java.util.List;
     24import java.util.Optional;
    1625import java.util.Set;
    1726import java.util.stream.Collectors;
     
    1928@Service
    2029@AllArgsConstructor
    21 @Transactional(readOnly = true)
     30
    2231public class PlaylistService {
    2332    private final PlaylistRepository playlistRepository;
     
    2534    private final AuthService authService;
    2635    private final SavedPlaylistRepository savedPlaylistRepository;
     36    private final ListenerRepository listenerRepository;
    2737
    2838    public List<Playlist>findByCreatorId(Long id){
     
    4555                playlist.getCover(),
    4656                playlist.getCreatedBy().getNonAdminUser().getUser().getFullName(),
     57                playlist.getCreatedBy().getNonAdminUser().getUser().getUsername(),
    4758                songsInPlaylist,
    4859                savedIds.contains(playlist.getId())
     
    5263
    5364    public List<BasicPlaylistDto> getBasicPlaylists(){
    54         Long userId = authService.getCurrentUserID();
     65        Long userId = authService.getCurrentUserIDOptional().orElse(null);
    5566        return playlistRepository.getPlaylistsByIdIs(userId);
     67    }
     68
     69    @Transactional
     70    public SavePlaylistStatusDto savePlaylist(Long playlistId) throws Exception {
     71        Long currentUserId=authService.getCurrentUserID();
     72        SavedPlaylistId savedPlaylistId=new SavedPlaylistId(currentUserId,playlistId);
     73        Playlist playlist=playlistRepository.findById(playlistId).orElseThrow(
     74                ()->new PlaylistNotFoundException(playlistId)
     75        );
     76        if (playlist.getCreatedBy().getId().equals(currentUserId)) {
     77            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"Cannot save playlist created by the current user!");
     78        }
     79
     80
     81
     82        boolean isSaved;
     83
     84        if (savedPlaylistRepository.existsById(savedPlaylistId)){
     85            savedPlaylistRepository.deleteById(savedPlaylistId);
     86            isSaved=false;
     87        }
     88        else{
     89            Listener listener=listenerRepository.findById(currentUserId).orElseThrow(
     90                    ()->new UserNotFoundException("Listener not found")
     91            );
     92
     93            savedPlaylistRepository.save(new SavedPlaylist(listener,playlist));
     94            isSaved=true;
     95        }
     96        return new SavePlaylistStatusDto(playlistId,isSaved);
     97
    5698    }
    5799
    58100
    59101
    60 
    61102}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/mappers/NonAdminUserMapper.java

    rf5bc95e r85512ff  
    77import com.ukim.finki.develop.finkwave.model.dto.*;
    88import com.ukim.finki.develop.finkwave.model.NonAdminUser;
     9import com.ukim.finki.develop.finkwave.model.dto.statusDto.FollowStatusDto;
    910import org.springframework.stereotype.Component;
    1011
  • 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
  • frontend/src/main.tsx

    rf5bc95e r85512ff  
    55import { PlayerProvider } from "./context/playerContext.tsx";
    66import "./index.css";
     7import PlaylistProvider from "./context/playlistContext.tsx";
    78
    89createRoot(document.getElementById("root")!).render(
    9         <StrictMode>
    10                 <AuthProvider>
    11                         <PlayerProvider>
    12                                 <App />
    13                         </PlayerProvider>
    14                 </AuthProvider>
    15         </StrictMode>,
     10  <StrictMode>
     11    <AuthProvider>
     12      <PlayerProvider>
     13        <PlaylistProvider>
     14          <App />
     15        </PlaylistProvider>
     16      </PlayerProvider>
     17    </AuthProvider>
     18  </StrictMode>,
    1619);
  • frontend/src/pages/MusicalCollection.tsx

    rf5bc95e r85512ff  
    33import axiosInstance, { baseURL } from "../api/axiosInstance";
    44import SongItem from "../components/SongItem";
    5 import { handleError } from "../utils/error";
     5import { getErrorMessage } from "../utils/error";
    66import type { Album, Playlist, Song } from "../utils/types";
    77interface CollectionView {
    8         id: number;
    9         title: string;
    10         cover?: string | null;
    11         genre?: string;
    12         type: string;
    13         releasedBy: string;
    14         isLikedByCurrentUser?: boolean;
    15         songs: Song[];
     8  id: number;
     9  title: string;
     10  cover?: string | null;
     11  genre?: string;
     12  type: string;
     13  releasedBy: string;
     14  isLikedByCurrentUser?: boolean;
     15  songs: Song[];
    1616}
    1717
    1818const MusicalCollection = () => {
    19         const { type, id } = useParams();
    20         const [collection, setCollection] = useState<CollectionView | null>(null);
    21         const [isLoading, setIsLoading] = useState(true);
    22         const [error, setError] = useState<string | null>(null);
    23 
    24         const normalizeCollection = (
    25                 data: Album | Playlist,
    26                 type: string,
    27         ): CollectionView => {
    28                 if (type === "album") {
    29                         const album = data as Album;
    30                         return {
    31                                 id: album.id,
    32                                 title: album.title,
    33                                 cover: album.cover,
    34                                 genre: album.genre,
    35                                 type: album.type,
    36                                 releasedBy: album.releasedBy,
    37                                 isLikedByCurrentUser: album.isLikedByCurrentUser,
    38                                 songs: album.songs,
    39                         };
    40                 } else {
    41                         const playlist = data as Playlist;
    42                         return {
    43                                 id: playlist.id,
    44                                 title: playlist.name,
    45                                 cover: playlist.cover,
    46                                 genre: undefined,
    47                                 type: "PLAYLIST",
    48                                 releasedBy: playlist.creatorName,
    49                                 isLikedByCurrentUser: undefined,
    50                                 songs: playlist.songsInPlaylist,
    51                         };
    52                 }
    53         };
    54 
    55         const toggleLike = async (songId: number) => {
    56                 try {
    57                         await axiosInstance.post(`/musical-entity/${songId}/like`);
    58                         setCollection((prev) => {
    59                                 if (!prev) return null;
    60                                 return {
    61                                         ...prev,
    62                                         songs: prev.songs.map((s) =>
    63                                                 s.id === songId
    64                                                         ? { ...s, isLikedByCurrentUser: !s.isLikedByCurrentUser }
    65                                                         : s,
    66                                         ),
    67                                 };
    68                         });
    69                 } catch (err) {
    70                         console.error("Error toggling like:", err);
    71                 }
    72         };
    73 
    74         const toggleCollectionLike = async () => {
    75                 if (!collection) return;
    76                 try {
    77                         await axiosInstance.post(`/musical-entity/${collection.id}/like`);
    78                         setCollection((prev) => {
    79                                 if (!prev) return null;
    80                                 return { ...prev, isLikedByCurrentUser: !prev.isLikedByCurrentUser };
    81                         });
    82                 } catch (err) {
    83                         console.error("Error toggling collection like:", err);
    84                 }
    85         };
    86 
    87         useEffect(() => {
    88                 const fetchData = async () => {
    89                         setIsLoading(true);
    90                         setError(null);
    91                         try {
    92                                 const endpoint =
    93                                         type === "album" ? `/albums/${id}` : `/playlists/${id}`;
    94                                 const response = await axiosInstance.get(endpoint);
    95 
    96                                 const normalized = normalizeCollection(response.data, type!);
    97                                 setCollection(normalized);
    98                         } catch (err: any) {
    99                                 setError(handleError(err));
    100                         } finally {
    101                                 setIsLoading(false);
    102                         }
    103                 };
    104                 fetchData();
    105         }, [id, type]);
    106 
    107         if (isLoading) {
    108                 return (
    109                         <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
    110                                 <div className="flex flex-col items-center gap-4">
    111                                         <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin" />
    112                                         <p className="text-gray-400 text-lg">Loading collection…</p>
    113                                 </div>
    114                         </div>
    115                 );
    116         }
    117 
    118         if (error) {
    119                 return (
    120                         <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
    121                                 <div className="text-center">
    122                                         <p className="text-red-400 text-xl mb-4">{error}</p>
    123                                         <Link to="/" className="text-[#1db954] hover:underline text-sm">
    124                                                 ← Back to Home
    125                                         </Link>
    126                                 </div>
    127                         </div>
    128                 );
    129         }
    130 
    131         if (!collection) return null;
    132 
    133         return (
    134                 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white">
    135                         <div className="max-w-5xl mx-auto px-6 py-10">
    136                                 {/* Back link */}
    137                                 <Link
    138                                         to="/"
    139                                         className="inline-flex items-center gap-1 text-gray-400 hover:text-white text-sm mb-8 transition-colors"
    140                                 >
    141                                         <svg
    142                                                 className="w-4 h-4"
    143                                                 fill="none"
    144                                                 stroke="currentColor"
    145                                                 viewBox="0 0 24 24"
    146                                         >
    147                                                 <path
    148                                                         strokeLinecap="round"
    149                                                         strokeLinejoin="round"
    150                                                         strokeWidth={2}
    151                                                         d="M15 19l-7-7 7-7"
    152                                                 />
    153                                         </svg>
    154                                         Back to Home
    155                                 </Link>
    156 
    157                                 {/* Hero section */}
    158                                 <div className="flex flex-col md:flex-row gap-8 mb-10">
    159                                         {/* Cover art */}
    160                                         <div className="w-full md:w-72 shrink-0">
    161                                                 <div className="relative w-full pt-[100%] rounded-xl overflow-hidden shadow-2xl bg-[#181818]">
    162                                                         <img
    163                                                                 src={
    164                                                                         collection.cover
    165                                                                                 ? `${baseURL}/${collection.cover}`
    166                                                                                 : "/favicon.png"
    167                                                                 }
    168                                                                 alt={collection.title}
    169                                                                 className="absolute inset-0 w-full h-full object-cover"
    170                                                                 onError={(e) => {
    171                                                                         (e.target as HTMLImageElement).src = "/favicon.png";
    172                                                                 }}
    173                                                         />
    174                                                 </div>
    175                                         </div>
    176 
    177                                         {/* Collection info */}
    178                                         <div className="flex flex-col justify-end gap-3 min-w-0">
    179                                                 <span className="text-xs uppercase tracking-widest text-gray-400 font-medium">
    180                                                         {collection.genre ? `${collection.genre} • ` : ""}
    181                                                         {collection.type === "PLAYLIST" ? "Playlist" : "Album"}
    182                                                 </span>
    183                                                 <h1 className="text-4xl md:text-5xl font-extrabold leading-tight truncate">
    184                                                         {collection.title}
    185                                                 </h1>
    186 
    187                                                 <p className="text-xl text-gray-300 font-semibold">
    188                                                         {collection.releasedBy}
    189                                                 </p>
    190 
    191                                                 {collection.songs && (
    192                                                         <p className="text-sm text-gray-500">
    193                                                                 {collection.songs.length} song
    194                                                                 {collection.songs.length !== 1 ? "s" : ""}
    195                                                         </p>
    196                                                 )}
    197 
    198                                                 {/* Action buttons */}
    199                                                 <div className="flex items-center gap-3 mt-4">
    200                                                         {type === "album" && (
    201                                                                 <button
    202                                                                         onClick={toggleCollectionLike}
    203                                                                         className={`flex items-center gap-2 px-5 py-3 rounded-full text-sm font-semibold transition-colors cursor-pointer ${
    204                                                                                 collection.isLikedByCurrentUser
    205                                                                                         ? "bg-[#1db954] text-black"
    206                                                                                         : "bg-white/10 text-white hover:bg-white/20"
    207                                                                         }`}
    208                                                                 >
    209                                                                         <svg
    210                                                                                 className="w-5 h-5"
    211                                                                                 fill={
    212                                                                                         collection.isLikedByCurrentUser ? "currentColor" : "none"
    213                                                                                 }
    214                                                                                 stroke="currentColor"
    215                                                                                 viewBox="0 0 24 24"
    216                                                                         >
    217                                                                                 <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" />
    218                                                                         </svg>
    219                                                                         {collection.isLikedByCurrentUser ? "Liked" : "Like"}
    220                                                                 </button>
    221                                                         )}
    222                                                 </div>
    223                                         </div>
    224                                 </div>
    225 
    226                                 {/* Songs list */}
    227                                 <div className="border-t border-white/10 pt-6">
    228                                         <h2 className="text-2xl font-bold mb-4">Songs</h2>
    229 
    230                                         {collection.songs && collection.songs.length > 0 ? (
    231                                                 <div className="space-y-1">
    232                                                         {collection.songs.map((song, index) => (
    233                                                                 <SongItem
    234                                                                         key={song.id}
    235                                                                         song={song}
    236                                                                         index={index + 1}
    237                                                                         onLikeToggle={() => toggleLike(song.id)}
    238                                                                 />
    239                                                         ))}
    240                                                 </div>
    241                                         ) : (
    242                                                 <div className="text-center py-12 text-gray-400">
    243                                                         <p className="text-lg">No songs available</p>
    244                                                 </div>
    245                                         )}
    246                                 </div>
    247                         </div>
    248                 </div>
    249         );
     19  const { type, id } = useParams();
     20  const [collection, setCollection] = useState<CollectionView | null>(null);
     21  const [isLoading, setIsLoading] = useState(true);
     22  const [error, setError] = useState<string | null>(null);
     23
     24  const normalizeCollection = (
     25    data: Album | Playlist,
     26    type: string,
     27  ): CollectionView => {
     28    if (type === "album") {
     29      const album = data as Album;
     30      return {
     31        id: album.id,
     32        title: album.title,
     33        cover: album.cover,
     34        genre: album.genre,
     35        type: album.type,
     36        releasedBy: album.releasedBy,
     37        isLikedByCurrentUser: album.isLikedByCurrentUser,
     38        songs: album.songs,
     39      };
     40    } else {
     41      const playlist = data as Playlist;
     42      return {
     43        id: playlist.id,
     44        title: playlist.name,
     45        cover: playlist.cover,
     46        genre: undefined,
     47        type: "PLAYLIST",
     48        releasedBy: playlist.creatorName,
     49        isLikedByCurrentUser: undefined,
     50        songs: playlist.songsInPlaylist,
     51      };
     52    }
     53  };
     54
     55  const toggleLike = async (songId: number) => {
     56    try {
     57      await axiosInstance.post(`/musical-entity/${songId}/like`);
     58      setCollection((prev) => {
     59        if (!prev) return null;
     60        return {
     61          ...prev,
     62          songs: prev.songs.map((s) =>
     63            s.id === songId
     64              ? { ...s, isLikedByCurrentUser: !s.isLikedByCurrentUser }
     65              : s,
     66          ),
     67        };
     68      });
     69    } catch (err) {
     70      console.error("Error toggling like:", err);
     71    }
     72  };
     73
     74  const toggleCollectionLike = async () => {
     75    if (!collection) return;
     76    try {
     77      await axiosInstance.post(`/musical-entity/${collection.id}/like`);
     78      setCollection((prev) => {
     79        if (!prev) return null;
     80        return { ...prev, isLikedByCurrentUser: !prev.isLikedByCurrentUser };
     81      });
     82    } catch (err) {
     83      console.error("Error toggling collection like:", err);
     84    }
     85  };
     86
     87  useEffect(() => {
     88    const fetchData = async () => {
     89      setIsLoading(true);
     90      setError(null);
     91      try {
     92        const endpoint =
     93          type === "album" ? `/albums/${id}` : `/playlists/${id}`;
     94        const response = await axiosInstance.get(endpoint);
     95
     96        const normalized = normalizeCollection(response.data, type!);
     97        setCollection(normalized);
     98      } catch (err: any) {
     99        setError(getErrorMessage(err));
     100      } finally {
     101        setIsLoading(false);
     102      }
     103    };
     104    fetchData();
     105  }, [id, type]);
     106
     107  if (isLoading) {
     108    return (
     109      <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
     110        <div className="flex flex-col items-center gap-4">
     111          <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin" />
     112          <p className="text-gray-400 text-lg">Loading collection…</p>
     113        </div>
     114      </div>
     115    );
     116  }
     117
     118  if (error) {
     119    return (
     120      <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
     121        <div className="text-center">
     122          <p className="text-red-400 text-xl mb-4">{error}</p>
     123          <Link to="/" className="text-[#1db954] hover:underline text-sm">
     124            ← Back to Home
     125          </Link>
     126        </div>
     127      </div>
     128    );
     129  }
     130
     131  if (!collection) return null;
     132
     133  return (
     134    <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white">
     135      <div className="max-w-5xl mx-auto px-6 py-10">
     136        {/* Back link */}
     137        <Link
     138          to="/"
     139          className="inline-flex items-center gap-1 text-gray-400 hover:text-white text-sm mb-8 transition-colors"
     140        >
     141          <svg
     142            className="w-4 h-4"
     143            fill="none"
     144            stroke="currentColor"
     145            viewBox="0 0 24 24"
     146          >
     147            <path
     148              strokeLinecap="round"
     149              strokeLinejoin="round"
     150              strokeWidth={2}
     151              d="M15 19l-7-7 7-7"
     152            />
     153          </svg>
     154          Back to Home
     155        </Link>
     156
     157        {/* Hero section */}
     158        <div className="flex flex-col md:flex-row gap-8 mb-10">
     159          {/* Cover art */}
     160          <div className="w-full md:w-72 shrink-0">
     161            <div className="relative w-full pt-[100%] rounded-xl overflow-hidden shadow-2xl bg-[#181818]">
     162              <img
     163                src={
     164                  collection.cover
     165                    ? `${baseURL}/${collection.cover}`
     166                    : "/favicon.png"
     167                }
     168                alt={collection.title}
     169                className="absolute inset-0 w-full h-full object-cover"
     170                onError={(e) => {
     171                  (e.target as HTMLImageElement).src = "/favicon.png";
     172                }}
     173              />
     174            </div>
     175          </div>
     176
     177          {/* Collection info */}
     178          <div className="flex flex-col justify-end gap-3 min-w-0">
     179            <span className="text-xs uppercase tracking-widest text-gray-400 font-medium">
     180              {collection.genre ? `${collection.genre} • ` : ""}
     181              {collection.type === "PLAYLIST" ? "Playlist" : "Album"}
     182            </span>
     183            <h1 className="text-4xl md:text-5xl font-extrabold leading-tight truncate">
     184              {collection.title}
     185            </h1>
     186
     187            <p className="text-xl text-gray-300 font-semibold">
     188              {collection.releasedBy}
     189            </p>
     190
     191            {collection.songs && (
     192              <p className="text-sm text-gray-500">
     193                {collection.songs.length} song
     194                {collection.songs.length !== 1 ? "s" : ""}
     195              </p>
     196            )}
     197
     198            {/* Action buttons */}
     199            <div className="flex items-center gap-3 mt-4">
     200              {type === "album" && (
     201                <button
     202                  onClick={toggleCollectionLike}
     203                  className={`flex items-center gap-2 px-5 py-3 rounded-full text-sm font-semibold transition-colors cursor-pointer ${
     204                    collection.isLikedByCurrentUser
     205                      ? "bg-[#1db954] text-black"
     206                      : "bg-white/10 text-white hover:bg-white/20"
     207                  }`}
     208                >
     209                  <svg
     210                    className="w-5 h-5"
     211                    fill={
     212                      collection.isLikedByCurrentUser ? "currentColor" : "none"
     213                    }
     214                    stroke="currentColor"
     215                    viewBox="0 0 24 24"
     216                  >
     217                    <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" />
     218                  </svg>
     219                  {collection.isLikedByCurrentUser ? "Liked" : "Like"}
     220                </button>
     221              )}
     222            </div>
     223          </div>
     224        </div>
     225
     226        {/* Songs list */}
     227        <div className="border-t border-white/10 pt-6">
     228          <h2 className="text-2xl font-bold mb-4">Songs</h2>
     229
     230          {collection.songs && collection.songs.length > 0 ? (
     231            <div className="space-y-1">
     232              {collection.songs.map((song, index) => (
     233                <SongItem
     234                  key={song.id}
     235                  song={song}
     236                  index={index + 1}
     237                  onLikeToggle={() => toggleLike(song.id)}
     238                />
     239              ))}
     240            </div>
     241          ) : (
     242            <div className="text-center py-12 text-gray-400">
     243              <p className="text-lg">No songs available</p>
     244            </div>
     245          )}
     246        </div>
     247      </div>
     248    </div>
     249  );
    250250};
    251251
  • frontend/src/pages/UserDetail.tsx

    rf5bc95e r85512ff  
    77import UserListModal from "../components/userProfile/UserListModal";
    88import { useAuth } from "../context/authContext";
    9 import { handleError } from "../utils/error";
     9import { getErrorMessage } from "../utils/error";
    1010import type {
    11         ArtistContribution,
    12         BaseNonAdminUser,
    13         MusicalEntity,
    14         Playlist,
     11  ArtistContribution,
     12  BaseNonAdminUser,
     13  MusicalEntity,
     14  Playlist,
    1515} from "../utils/types";
     16import { useCreatedPlaylists } from "../context/playlistContext";
    1617
    1718interface FollowStatus {
    18         isFollowing: boolean;
    19         followerCount: number;
    20         followingCount: number;
     19  isFollowing: boolean;
     20  followerCount: number;
     21  followingCount: number;
    2122}
    2223
    2324interface Artist extends BaseNonAdminUser {
    24         userType: "ARTIST";
    25         contributions: ArtistContribution[];
     25  userType: "ARTIST";
     26  contributions: ArtistContribution[];
    2627}
    2728interface Listener extends BaseNonAdminUser {
    28         userType: "LISTENER";
    29         likedEntities: MusicalEntity[];
    30         createdPlaylists: Playlist[];
    31         savedPlaylists: Playlist[];
     29  userType: "LISTENER";
     30  likedEntities: MusicalEntity[];
     31  createdPlaylists: Playlist[];
     32  savedPlaylists: Playlist[];
    3233}
    3334
     
    3536
    3637const UserDetail = () => {
    37         const { username: usernameParam } = useParams();
    38         const { user: currentUser } = useAuth();
    39         const navigate = useNavigate();
    40         const [user, setUser] = useState<UserProfile | null>(null);
    41         const [error, setError] = useState<string | null>(null);
    42         const [showModal, setShowModal] = useState(false);
    43         const [modalTitle, setModalTitle] = useState("");
    44         const [modalUsers, setModalUsers] = useState<any[]>([]);
    45         const [isLoadingModal, setIsLoadingModal] = useState(false);
    46         const [isFollowing, setIsFollowing] = useState(false);
    47 
    48         const username = usernameParam || currentUser?.username;
    49         const isOwnProfile = currentUser?.username === username;
    50 
    51         if (!usernameParam && !currentUser) {
    52                 return (
    53                         <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
    54                                 <div className="text-center">
    55                                         <p className="text-red-400 text-xl mb-4">
    56                                                 You must be logged in to view your profile.
    57                                         </p>
    58                                         <button
    59                                                 onClick={() => navigate("/login")}
    60                                                 className="text-[#1db954] hover:underline text-sm cursor-pointer"
    61                                         >
    62                                                 Go to Login
    63                                         </button>
    64                                 </div>
    65                         </div>
    66                 );
    67         }
    68 
    69         const handleFollow = async () => {
    70                 if (!user) return;
    71 
    72                 setIsFollowing(true);
    73                 try {
    74                         const response = await axiosInstance.post<FollowStatus>(
    75                                 `/users/na/${username}/follow`,
    76                         );
    77                         setUser((prev) => {
    78                                 if (!prev) return null;
    79                                 return {
    80                                         ...prev,
    81                                         isFollowedByCurrentUser: response.data.isFollowing,
    82                                         followers: response.data.followerCount,
    83                                         following: response.data.followingCount,
    84                                 };
    85                         });
    86                 } catch (err: any) {
    87                         setError(handleError(err));
    88                 } finally {
    89                         setIsFollowing(false);
    90                 }
    91         };
    92 
    93         const handleFollowInModal = async (targetUsername: string) => {
    94                 try {
    95                         const response = await axiosInstance.post<FollowStatus>(
    96                                 `/users/na/${targetUsername}/follow`,
    97                         );
    98 
    99                         setModalUsers((prevUsers) =>
    100                                 prevUsers.map((u) =>
    101                                         u.username === targetUsername
    102                                                 ? { ...u, isFollowedByCurrentUser: response.data.isFollowing }
    103                                                 : u,
    104                                 ),
    105                         );
    106                 } catch (err: any) {
    107                         setError(handleError(err));
    108                 }
    109         };
    110 
    111         const displayFollowers = async () => {
    112                 setIsLoadingModal(true);
    113                 try {
    114                         const response = await axiosInstance.get(
    115                                 `/users/na/${username}/followers`,
    116                         );
    117                         setModalUsers(response.data);
    118                         setModalTitle("Followers");
    119                         setShowModal(true);
    120                 } catch (err) {
    121                         setError(handleError(err));
    122                 } finally {
    123                         setIsLoadingModal(false);
    124                 }
    125         };
    126         const displayFollowing = async () => {
    127                 setIsLoadingModal(true);
    128                 try {
    129                         const response = await axiosInstance.get(
    130                                 `/users/na/${username}/following`,
    131                         );
    132                         setModalUsers(response.data);
    133                         setModalTitle("Following");
    134                         setShowModal(true);
    135                 } catch (err: any) {
    136                         setError(handleError(err));
    137                 } finally {
    138                         setIsLoadingModal(false);
    139                 }
    140         };
    141 
    142         useEffect(() => {
    143                 const fetchUser = async () => {
    144                         setError(null);
    145                         try {
    146                                 const response = await axiosInstance.get(`/users/na/${username}`);
    147                                 setUser(response.data);
    148                         } catch (err: any) {
    149                                 setError(handleError(err));
    150                         }
    151                 };
    152                 fetchUser();
    153         }, [username]);
    154 
    155         if (error) {
    156                 return (
    157                         <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
    158                                 <div className="text-center">
    159                                         <p className="text-red-400 text-xl mb-4">{error}</p>
    160                                         <Link to="/" className="text-[#1db954] hover:underline text-sm">
    161                                                 ← Back to Home
    162                                         </Link>
    163                                 </div>
    164                         </div>
    165                 );
    166         }
    167 
    168         if (!user) {
    169                 return <LoadingSpinner />;
    170         }
    171 
    172         return (
    173                 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white">
    174                         {isLoadingModal && (
    175                                 <div className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm flex items-center justify-center">
    176                                         <div className="w-10 h-10 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin" />
    177                                 </div>
    178                         )}
    179 
    180                         <div className="max-w-5xl mx-auto px-6 py-10">
    181                                 {/* Back link */}
    182                                 <Link
    183                                         to="/"
    184                                         className="inline-flex items-center gap-1 text-gray-400 hover:text-white text-sm mb-8 transition-colors"
    185                                 >
    186                                         <svg
    187                                                 className="w-4 h-4"
    188                                                 fill="none"
    189                                                 stroke="currentColor"
    190                                                 viewBox="0 0 24 24"
    191                                         >
    192                                                 <path
    193                                                         strokeLinecap="round"
    194                                                         strokeLinejoin="round"
    195                                                         strokeWidth={2}
    196                                                         d="M15 19l-7-7 7-7"
    197                                                 />
    198                                         </svg>
    199                                         Back to Home
    200                                 </Link>
    201 
    202                                 {/* Hero section */}
    203                                 <div className="flex flex-col md:flex-row gap-8 mb-10">
    204                                         {/* Profile photo */}
    205                                         <div className="w-full md:w-48 shrink-0">
    206                                                 <div className="relative w-48 h-48 rounded-full overflow-hidden shadow-2xl bg-[#181818] mx-auto md:mx-0">
    207                                                         {user.profilePhoto ? (
    208                                                                 <img
    209                                                                         src={`${baseURL}/${user.profilePhoto}`}
    210                                                                         alt={user.fullName}
    211                                                                         className="w-full h-full object-cover"
    212                                                                 />
    213                                                         ) : (
    214                                                                 <div className="w-full h-full bg-linear-to-br from-[#1db954] to-[#1ed760] flex items-center justify-center text-white text-5xl font-bold">
    215                                                                         {user.fullName.charAt(0).toUpperCase()}
    216                                                                 </div>
    217                                                         )}
    218                                                 </div>
    219                                         </div>
    220 
    221                                         {/* User info */}
    222                                         <div className="flex flex-col justify-end gap-3 min-w-0">
    223                                                 <span className="text-xs uppercase tracking-widest text-gray-400 font-medium">
    224                                                         {user.userType === "ARTIST" ? "Artist" : "Listener"} • Profile
    225                                                 </span>
    226                                                 <h1 className="text-4xl md:text-5xl font-extrabold leading-tight">
    227                                                         {user.fullName}
    228                                                 </h1>
    229                                                 <p className="text-gray-400">@{user.username}</p>
    230 
    231                                                 {/* Stats */}
    232                                                 <div className="flex items-center gap-6 mt-2">
    233                                                         <div
    234                                                                 className={`${user.userType === "LISTENER" ? "cursor-pointer hover:text-white" : ""} transition-colors`}
    235                                                                 onClick={
    236                                                                         user.userType === "LISTENER" ? displayFollowers : undefined
    237                                                                 }
    238                                                         >
    239                                                                 <span className="text-xl font-bold text-white">
    240                                                                         {user.followers}
    241                                                                 </span>
    242                                                                 <span className="text-sm text-gray-400 ml-1">Followers</span>
    243                                                         </div>
    244                                                         <div
    245                                                                 className={`${user.userType === "LISTENER" ? "cursor-pointer hover:text-white" : ""} transition-colors`}
    246                                                                 onClick={
    247                                                                         user.userType === "LISTENER" ? displayFollowing : undefined
    248                                                                 }
    249                                                         >
    250                                                                 <span className="text-xl font-bold text-white">
    251                                                                         {user.following}
    252                                                                 </span>
    253                                                                 <span className="text-sm text-gray-400 ml-1">Following</span>
    254                                                         </div>
    255                                                 </div>
    256 
    257                                                 {/* Follow button - hidden on own profile */}
    258                                                 {!isOwnProfile && (
    259                                                         <div className="mt-4">
    260                                                                 <button
    261                                                                         onClick={handleFollow}
    262                                                                         disabled={isFollowing}
    263                                                                         className={`flex items-center gap-2 px-6 py-3 rounded-full text-sm font-semibold transition-all cursor-pointer ${
    264                                                                                 isFollowing
    265                                                                                         ? "bg-gray-700 text-gray-400 cursor-not-allowed"
    266                                                                                         : user.isFollowedByCurrentUser
    267                                                                                                 ? "bg-white/10 text-white hover:bg-white/20"
    268                                                                                                 : "bg-[#1db954] text-black hover:bg-[#1ed760] hover:scale-105"
    269                                                                         }`}
    270                                                                 >
    271                                                                         {user.isFollowedByCurrentUser ? (
    272                                                                                 <>
    273                                                                                         <svg
    274                                                                                                 className="w-5 h-5"
    275                                                                                                 fill="none"
    276                                                                                                 stroke="currentColor"
    277                                                                                                 viewBox="0 0 24 24"
    278                                                                                         >
    279                                                                                                 <path
    280                                                                                                         strokeLinecap="round"
    281                                                                                                         strokeLinejoin="round"
    282                                                                                                         strokeWidth={2}
    283                                                                                                         d="M5 13l4 4L19 7"
    284                                                                                                 />
    285                                                                                         </svg>
    286                                                                                         Following
    287                                                                                 </>
    288                                                                         ) : (
    289                                                                                 <>
    290                                                                                         <svg
    291                                                                                                 className="w-5 h-5"
    292                                                                                                 fill="none"
    293                                                                                                 stroke="currentColor"
    294                                                                                                 viewBox="0 0 24 24"
    295                                                                                         >
    296                                                                                                 <path
    297                                                                                                         strokeLinecap="round"
    298                                                                                                         strokeLinejoin="round"
    299                                                                                                         strokeWidth={2}
    300                                                                                                         d="M12 4v16m8-8H4"
    301                                                                                                 />
    302                                                                                         </svg>
    303                                                                                         Follow
    304                                                                                 </>
    305                                                                         )}
    306                                                                 </button>
    307                                                         </div>
    308                                                 )}
    309                                         </div>
    310                                 </div>
    311 
    312                                 {/* Content */}
    313                                 {user.userType === "ARTIST" ? (
    314                                         <ArtistView contributions={user.contributions} />
    315                                 ) : (
    316                                         <ListenerView
    317                                                 likedEntities={user.likedEntities}
    318                                                 createdPlaylists={user.createdPlaylists}
    319                                                 savedPlaylists={user.savedPlaylists}
    320                                         />
    321                                 )}
    322 
    323                                 {showModal && (
    324                                         <UserListModal
    325                                                 title={modalTitle}
    326                                                 users={modalUsers}
    327                                                 onClose={() => setShowModal(false)}
    328                                                 onFollowToggle={handleFollowInModal}
    329                                         />
    330                                 )}
    331                         </div>
    332                 </div>
    333         );
     38  const { username: usernameParam } = useParams();
     39  const { user: currentUser } = useAuth();
     40  const { createdPlaylists: currentUserCreatedPlaylists } =
     41    useCreatedPlaylists();
     42  const navigate = useNavigate();
     43  const [user, setUser] = useState<UserProfile | null>(null);
     44  const [error, setError] = useState<string | null>(null);
     45  const [showModal, setShowModal] = useState(false);
     46  const [modalTitle, setModalTitle] = useState("");
     47  const [modalUsers, setModalUsers] = useState<any[]>([]);
     48  const [isLoadingModal, setIsLoadingModal] = useState(false);
     49  const [isFollowing, setIsFollowing] = useState(false);
     50
     51  const username = usernameParam || currentUser?.username;
     52  const isOwnProfile = currentUser?.username === username;
     53
     54  if (!usernameParam && !currentUser) {
     55    return (
     56      <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
     57        <div className="text-center">
     58          <p className="text-red-400 text-xl mb-4">
     59            You must be logged in to view your profile.
     60          </p>
     61          <button
     62            onClick={() => navigate("/login")}
     63            className="text-[#1db954] hover:underline text-sm cursor-pointer"
     64          >
     65            Go to Login
     66          </button>
     67        </div>
     68      </div>
     69    );
     70  }
     71
     72  const handleFollow = async () => {
     73    if (!user) return;
     74
     75    setIsFollowing(true);
     76    try {
     77      const response = await axiosInstance.post<FollowStatus>(
     78        `/users/na/${username}/follow`,
     79      );
     80      setUser((prev) => {
     81        if (!prev) return null;
     82        return {
     83          ...prev,
     84          isFollowedByCurrentUser: response.data.isFollowing,
     85          followers: response.data.followerCount,
     86          following: response.data.followingCount,
     87        };
     88      });
     89    } catch (err: any) {
     90      setError(getErrorMessage(err));
     91    } finally {
     92      setIsFollowing(false);
     93    }
     94  };
     95
     96  const handleFollowInModal = async (targetUsername: string) => {
     97    try {
     98      const response = await axiosInstance.post<FollowStatus>(
     99        `/users/na/${targetUsername}/follow`,
     100      );
     101
     102      setModalUsers((prevUsers) =>
     103        prevUsers.map((u) =>
     104          u.username === targetUsername
     105            ? { ...u, isFollowedByCurrentUser: response.data.isFollowing }
     106            : u,
     107        ),
     108      );
     109    } catch (err: any) {
     110      setError(getErrorMessage(err));
     111    }
     112  };
     113
     114  const displayFollowers = async () => {
     115    setIsLoadingModal(true);
     116    try {
     117      const response = await axiosInstance.get(
     118        `/users/na/${username}/followers`,
     119      );
     120      setModalUsers(response.data);
     121      setModalTitle("Followers");
     122      setShowModal(true);
     123    } catch (err) {
     124      setError(getErrorMessage(err));
     125    } finally {
     126      setIsLoadingModal(false);
     127    }
     128  };
     129  const displayFollowing = async () => {
     130    setIsLoadingModal(true);
     131    try {
     132      const response = await axiosInstance.get(
     133        `/users/na/${username}/following`,
     134      );
     135      setModalUsers(response.data);
     136      setModalTitle("Following");
     137      setShowModal(true);
     138    } catch (err: any) {
     139      setError(getErrorMessage(err));
     140    } finally {
     141      setIsLoadingModal(false);
     142    }
     143  };
     144
     145  useEffect(() => {
     146    const fetchUser = async () => {
     147      setError(null);
     148      setUser(null);
     149      try {
     150        const response = await axiosInstance.get(`/users/na/${username}`);
     151
     152        setUser(response.data);
     153      } catch (err: any) {
     154        setError(getErrorMessage(err));
     155      }
     156    };
     157    fetchUser();
     158  }, [username]);
     159
     160  if (error) {
     161    return (
     162      <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
     163        <div className="text-center">
     164          <p className="text-red-400 text-xl mb-4">{error}</p>
     165          <Link to="/" className="text-[#1db954] hover:underline text-sm">
     166            ← Back to Home
     167          </Link>
     168        </div>
     169      </div>
     170    );
     171  }
     172
     173  if (!user) {
     174    return <LoadingSpinner />;
     175  }
     176
     177  return (
     178    <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white">
     179      {isLoadingModal && (
     180        <div className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm flex items-center justify-center">
     181          <div className="w-10 h-10 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin" />
     182        </div>
     183      )}
     184
     185      <div className="max-w-5xl mx-auto px-6 py-10">
     186        {/* Back link */}
     187        <Link
     188          to="/"
     189          className="inline-flex items-center gap-1 text-gray-400 hover:text-white text-sm mb-8 transition-colors"
     190        >
     191          <svg
     192            className="w-4 h-4"
     193            fill="none"
     194            stroke="currentColor"
     195            viewBox="0 0 24 24"
     196          >
     197            <path
     198              strokeLinecap="round"
     199              strokeLinejoin="round"
     200              strokeWidth={2}
     201              d="M15 19l-7-7 7-7"
     202            />
     203          </svg>
     204          Back to Home
     205        </Link>
     206
     207        {/* Hero section */}
     208        <div className="flex flex-col md:flex-row gap-8 mb-10">
     209          {/* Profile photo */}
     210          <div className="w-full md:w-48 shrink-0">
     211            <div className="relative w-48 h-48 rounded-full overflow-hidden shadow-2xl bg-[#181818] mx-auto md:mx-0">
     212              {user.profilePhoto ? (
     213                <img
     214                  src={`${baseURL}/${user.profilePhoto}`}
     215                  alt={user.fullName}
     216                  className="w-full h-full object-cover"
     217                />
     218              ) : (
     219                <div className="w-full h-full bg-linear-to-br from-[#1db954] to-[#1ed760] flex items-center justify-center text-white text-5xl font-bold">
     220                  {user.fullName.charAt(0).toUpperCase()}
     221                </div>
     222              )}
     223            </div>
     224          </div>
     225
     226          {/* User info */}
     227          <div className="flex flex-col justify-end gap-3 min-w-0">
     228            <span className="text-xs uppercase tracking-widest text-gray-400 font-medium">
     229              {user.userType === "ARTIST" ? "Artist" : "Listener"} • Profile
     230            </span>
     231            <h1 className="text-4xl md:text-5xl font-extrabold leading-tight">
     232              {user.fullName}
     233            </h1>
     234            <p className="text-gray-400">@{user.username}</p>
     235
     236            {/* Stats */}
     237            <div className="flex items-center gap-6 mt-2">
     238              <div
     239                className={`${user.userType === "LISTENER" ? "cursor-pointer hover:text-white" : ""} transition-colors`}
     240                onClick={
     241                  user.userType === "LISTENER" ? displayFollowers : undefined
     242                }
     243              >
     244                <span className="text-xl font-bold text-white">
     245                  {user.followers}
     246                </span>
     247                <span className="text-sm text-gray-400 ml-1">Followers</span>
     248              </div>
     249              <div
     250                className={`${user.userType === "LISTENER" ? "cursor-pointer hover:text-white" : ""} transition-colors`}
     251                onClick={
     252                  user.userType === "LISTENER" ? displayFollowing : undefined
     253                }
     254              >
     255                <span className="text-xl font-bold text-white">
     256                  {user.following}
     257                </span>
     258                <span className="text-sm text-gray-400 ml-1">Following</span>
     259              </div>
     260            </div>
     261
     262            {/* Follow button - hidden on own profile */}
     263            {!isOwnProfile && (
     264              <div className="mt-4">
     265                <button
     266                  onClick={handleFollow}
     267                  disabled={isFollowing}
     268                  className={`flex items-center gap-2 px-6 py-3 rounded-full text-sm font-semibold transition-all cursor-pointer ${
     269                    isFollowing
     270                      ? "bg-gray-700 text-gray-400 cursor-not-allowed"
     271                      : user.isFollowedByCurrentUser
     272                        ? "bg-white/10 text-white hover:bg-white/20"
     273                        : "bg-[#1db954] text-black hover:bg-[#1ed760] hover:scale-105"
     274                  }`}
     275                >
     276                  {user.isFollowedByCurrentUser ? (
     277                    <>
     278                      <svg
     279                        className="w-5 h-5"
     280                        fill="none"
     281                        stroke="currentColor"
     282                        viewBox="0 0 24 24"
     283                      >
     284                        <path
     285                          strokeLinecap="round"
     286                          strokeLinejoin="round"
     287                          strokeWidth={2}
     288                          d="M5 13l4 4L19 7"
     289                        />
     290                      </svg>
     291                      Following
     292                    </>
     293                  ) : (
     294                    <>
     295                      <svg
     296                        className="w-5 h-5"
     297                        fill="none"
     298                        stroke="currentColor"
     299                        viewBox="0 0 24 24"
     300                      >
     301                        <path
     302                          strokeLinecap="round"
     303                          strokeLinejoin="round"
     304                          strokeWidth={2}
     305                          d="M12 4v16m8-8H4"
     306                        />
     307                      </svg>
     308                      Follow
     309                    </>
     310                  )}
     311                </button>
     312              </div>
     313            )}
     314          </div>
     315        </div>
     316
     317        {/* Content */}
     318        {user.userType === "ARTIST" ? (
     319          <ArtistView contributions={user.contributions} />
     320        ) : (
     321          <ListenerView
     322            likedEntities={user.likedEntities}
     323            createdPlaylists={user.createdPlaylists}
     324            savedPlaylists={user.savedPlaylists}
     325          />
     326        )}
     327
     328        {showModal && (
     329          <UserListModal
     330            title={modalTitle}
     331            users={modalUsers}
     332            onClose={() => setShowModal(false)}
     333            onFollowToggle={handleFollowInModal}
     334          />
     335        )}
     336      </div>
     337    </div>
     338  );
    334339};
    335340
  • frontend/src/utils/error.ts

    rf5bc95e r85512ff  
    1 export const handleError = (err: any) => {
     1export const getErrorMessage = (err: any) => {
    22  const errorMessage = err.response?.data?.error || "Failed to fetch user";
    33  return errorMessage;
  • frontend/src/utils/types.ts

    rf5bc95e r85512ff  
    11export interface User {
    2         username: string;
    3         fullName: string;
    4         email?: string;
    5         profilePhoto?: string | null;
    6         isAdmin: boolean;
    7         isArtist: boolean;
     2  username: string;
     3  fullName: string;
     4  email?: string;
     5  profilePhoto?: string | null;
     6  isAdmin: boolean;
     7  isArtist: boolean;
    88}
    99
    1010export interface ArtistContribution {
    11         id: number;
    12         title: string;
    13         genre: string;
    14         role: string;
    15         entityType: string;
    16         isLikedByCurrentUser: boolean;
    17         cover?: string | null;
    18         link?: string | null;
     11  id: number;
     12  title: string;
     13  genre: string;
     14  role: string;
     15  entityType: string;
     16  isLikedByCurrentUser: boolean;
     17  cover?: string | null;
     18  link?: string | null;
    1919}
    2020
    2121export interface MusicalEntity {
    22         id: number;
    23         title: string;
    24         genre: string;
    25         type: string;
    26         releasedBy: string;
    27         artistUsername?: string;
    28         cover?: string | null;
    29         isLikedByCurrentUser?: boolean;
     22  id: number;
     23  title: string;
     24  genre: string;
     25  type: string;
     26  releasedBy: string;
     27  artistUsername?: string;
     28  cover?: string | null;
     29  isLikedByCurrentUser?: boolean;
    3030}
    3131
    3232export interface Song extends MusicalEntity {
    33         type: "SONG";
    34         album?: string;
    35         albumId?: number;
    36         link?: string;
     33  type: "SONG";
     34  album?: string;
     35  albumId?: number;
     36  link?: string;
    3737}
    3838
    3939export interface Album extends MusicalEntity {
    40         type: "ALBUM";
    41         songs: Song[];
     40  type: "ALBUM";
     41  songs: Song[];
    4242}
    4343
    4444export interface Playlist {
    45         id: number;
    46         name: string;
    47         cover: string;
    48         creatorName: string;
    49         songsInPlaylist: Song[];
    50         isSavedByCurrentUser: boolean;
     45  id: number;
     46  name: string;
     47  cover: string;
     48  creatorName: string;
     49  creatorUsername: string;
     50  songsInPlaylist: Song[];
     51  isSavedByCurrentUser: boolean;
    5152}
    5253
    5354export interface BaseNonAdminUser {
    54         username: string;
    55         fullName: string;
    56         userType: string;
    57         profilePhoto: string;
    58         followers: number;
    59         following: number;
    60         isFollowedByCurrentUser: boolean;
     55  username: string;
     56  fullName: string;
     57  userType: string;
     58  profilePhoto: string;
     59  followers: number;
     60  following: number;
     61  isFollowedByCurrentUser: boolean;
    6162}
    6263
     
    6667
    6768export interface SidebarProps {
    68         isOpen: boolean;
    69         onClose: () => void;
     69  isOpen: boolean;
     70  onClose: () => void;
     71}
     72
     73export interface CreatePlaylistModalProps {
     74  isOpen: boolean;
     75  onClose: () => void;
     76  onSubmit: (playlistName: string) => void;
    7077}
    7178
    7279export interface SongContribution {
    73         artistName: string;
    74         role: string;
     80  artistName: string;
     81  role: string;
    7582}
    7683
    7784export interface SongReview {
    78         id: {
    79                 listenerId: number;
    80                 musicalEntityId: number;
    81         };
    82         author: string;
    83         authorUsername: string;
    84         grade: number;
    85         comment: string;
     85  id: {
     86    listenerId: number;
     87    musicalEntityId: number;
     88  };
     89  author: string;
     90  authorUsername: string;
     91  grade: number;
     92  comment: string;
    8693}
    8794
    8895export interface SongDetail extends MusicalEntity {
    89         type: "SONG";
    90         album?: string | null;
    91         link?: string | null;
    92         contributions: SongContribution[];
    93         reviews: SongReview[];
     96  type: "SONG";
     97  album?: string | null;
     98  link?: string | null;
     99  contributions: SongContribution[];
     100  reviews: SongReview[];
    94101}
    95102
    96103export interface BasicSong {
    97         id: number;
    98         title: string;
    99         artist: string;
    100         artistUsername?: string;
    101         cover?: string;
    102         link?: string;
    103         album?: string;
    104         albumId?: number;
     104  id: number;
     105  title: string;
     106  artist: string;
     107  artistUsername?: string;
     108  cover?: string;
     109  link?: string;
     110  album?: string;
     111  albumId?: number;
    105112}
    106113
    107114export interface BasicPlaylist {
    108         id: number;
    109         name: string;
    110         songCount: number;
     115  id: number;
     116  name: string;
     117  songCount: number;
    111118}
    112119
    113120export interface CatalogItem {
    114         id: number;
    115         title: string;
    116         genre: string;
    117         cover: string | null;
    118         type: "SONG" | "ALBUM";
    119         releaseDate: string;
     121  id: number;
     122  title: string;
     123  genre: string;
     124  cover: string | null;
     125  type: "SONG" | "ALBUM";
     126  releaseDate: string;
    120127}
    121128
    122129export interface Contributor {
    123         id: number;
    124         fullName: string;
    125         role: string;
     130  id: number;
     131  fullName: string;
     132  role: string;
    126133}
    127134
    128135export interface ArtistSearchResult {
    129         id: number;
    130         username: string;
    131         fullName: string;
    132         profilePhoto?: string;
     136  id: number;
     137  username: string;
     138  fullName: string;
     139  profilePhoto?: string;
    133140}
    134141
    135142export interface SongEntry {
    136         title: string;
    137         link: string;
    138         contributors: Contributor[];
     143  title: string;
     144  link: string;
     145  contributors: Contributor[];
    139146}
Note: See TracChangeset for help on using the changeset viewer.