Changeset 0792d02


Ignore:
Timestamp:
02/05/26 10:29:09 (5 months ago)
Author:
Dimitar Arsov <dimitararsov04@…>
Branches:
main
Children:
3de009b
Parents:
d2af1a6
Message:

show saved playlists by user

Files:
18 edited

Legend:

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

    rd2af1a6 r0792d02  
    2626    }
    2727
    28     @GetMapping("/{id}")
    29     public HttpEntity<NonAdminUserDto>getById(@PathVariable Long id){
    30         return ResponseEntity.ok(nonAdminUserService.getById(id));
     28    @GetMapping("/{username}")
     29    public HttpEntity<NonAdminUserDto>getById(@PathVariable String username){
     30        return ResponseEntity.ok(nonAdminUserService.getNonAdminUserProfile(username));
    3131    }
    3232
     
    3636    }
    3737
    38     @GetMapping("/{id}/followers")
    39     public HttpEntity<List<NonAdminUserDto>>getFollowersForUser(@PathVariable Long id){
    40         return ResponseEntity.ok(followService.getFollowersForUser(id));
     38    @GetMapping("/{username}/followers")
     39    public HttpEntity<List<NonAdminUserDto>>getFollowersForUser(@PathVariable String username){
     40        return ResponseEntity.ok(followService.getFollowersForUser(username));
    4141    }
    4242
    43     @GetMapping("/{id}/following")
    44     public HttpEntity<List<NonAdminUserDto>>getFollowingForUser(@PathVariable Long id){
    45         return ResponseEntity.ok(followService.getFollowingForUser(id));
     43    @GetMapping("/{username}/following")
     44    public HttpEntity<List<NonAdminUserDto>>getFollowingForUser(@PathVariable String username){
     45        return ResponseEntity.ok(followService.getFollowingForUser(username));
    4646    }
    4747
    48     @PostMapping("/{id}/follow")
    49     public HttpEntity<FollowStatusDto>followUser(@PathVariable Long id){
     48    @PostMapping("/{username}/follow")
     49    public HttpEntity<FollowStatusDto>followUser(@PathVariable String username){
    5050
    51         return ResponseEntity.ok(followService.toggleFollow(id));
     51        return ResponseEntity.ok(followService.toggleFollow(username));
    5252    }
    5353
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/dto/ListenerDto.java

    rd2af1a6 r0792d02  
    1111    private List<MusicalEntityDto> likedEntities;
    1212    private List<PlaylistDto> createdPlaylists;
     13    private List<PlaylistDto>savedPlaylists;
    1314}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/dto/NonAdminUserDto.java

    rd2af1a6 r0792d02  
    99
    1010public  class NonAdminUserDto {
    11     private Long id;
    1211    private String username;
    1312    private String fullName;
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/ArtistRepository.java

    rd2af1a6 r0792d02  
    1919    Optional<Artist>findByIdWithUser(@Param("artistId")Long artistId);
    2020
     21    @Query("SELECT a FROM Artist a " +
     22            "JOIN FETCH a.nonAdminUser nau " +
     23            "JOIN FETCH nau.user u " +
     24            "WHERE u.username = :username")
     25    Optional<Artist> findByUsername(@Param("username") String username);
     26
     27
    2128}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/ListenerRepository.java

    rd2af1a6 r0792d02  
    1717           "WHERE l.id = :listenerId")
    1818    Optional<Listener> findByIdWithUser(@Param("listenerId") Long listenerId);
     19
     20    @Query("SELECT l FROM Listener l " +
     21            "JOIN FETCH l.nonAdminUser nu " +
     22            "JOIN FETCH nu.user u " +
     23            "WHERE u.username=:username")
     24
     25    Optional<Listener>findByUsername(@Param("username")String username);
    1926}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/NonAdminUserRepository.java

    rd2af1a6 r0792d02  
    33import org.springframework.data.jpa.repository.JpaRepository;
    44import org.springframework.data.jpa.repository.Query;
     5import org.springframework.data.repository.query.Param;
    56import org.springframework.stereotype.Repository;
    67
     
    89
    910import java.util.List;
     11import java.util.Optional;
    1012
    1113@Repository
     
    2022    List<NonAdminUser> searchByName(String name);
    2123
     24    @Query("SELECT nau FROM NonAdminUser nau " +
     25            "JOIN FETCH nau.user u " +
     26            "WHERE u.username = :username")
     27    Optional<NonAdminUser> findByUsername(@Param("username") String username);
     28
    2229
    2330
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/SavedPlaylistRepository.java

    rd2af1a6 r0792d02  
    11package com.ukim.finki.develop.finkwave.repository;
    22
     3import com.ukim.finki.develop.finkwave.model.Playlist;
    34import org.springframework.data.jpa.repository.JpaRepository;
     5import org.springframework.data.jpa.repository.Query;
     6import org.springframework.data.repository.query.Param;
    47import org.springframework.stereotype.Repository;
    58
     
    1316
    1417    List<SavedPlaylist>findAllByListener_Id(Long currentUserId);
     18
     19    @Query("SELECT p FROM SavedPlaylist sp " +
     20            "JOIN sp.playlist p " +
     21            "JOIN FETCH p.createdBy l " +
     22            "JOIN FETCH l.nonAdminUser nau " +
     23            "JOIN FETCH nau.user u " +
     24            "WHERE sp.id.listenerId = :listenerId")
     25    List<Playlist> findSavedPlaylistsMetadata(@Param("listenerId") Long listenerId);
    1526}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/FollowService.java

    rd2af1a6 r0792d02  
    33import com.ukim.finki.develop.finkwave.exceptions.FollowException;
    44import com.ukim.finki.develop.finkwave.exceptions.UserNotFoundException;
     5import com.ukim.finki.develop.finkwave.model.Artist;
    56import com.ukim.finki.develop.finkwave.model.Follow;
    67import com.ukim.finki.develop.finkwave.model.FollowId;
     
    1718import java.util.ArrayList;
    1819import java.util.List;
     20import java.util.Optional;
    1921import java.util.Set;
    2022import java.util.stream.Collectors;
     
    3941    }
    4042
    41     public List<NonAdminUserDto>getFollowersForUser(Long id){
    42         if (artistRepository.existsById(id)){
     43    private  NonAdminUser getNonAdminUser(String username){
     44        Optional<Artist>artistOptional=artistRepository.findByUsername(username);
     45        if (artistOptional.isPresent()){
    4346            throw new FollowException("Cannot view for artist");
    4447        }
     48        return nonAdminUserRepository.findByUsername(username)
     49                .orElseThrow(()-> new UserNotFoundException("Cannot find user with username: "+ username));
     50    }
     51
     52    public List<NonAdminUserDto>getFollowersForUser(String username){
     53        NonAdminUser nonAdminUser=getNonAdminUser(username);
     54
     55        Long id=nonAdminUser.getId();
    4556        List<Follow> followers = followRepository.findFollowersWithProfile(id);
    4657        Long currentUserId=authService.getCurrentUserID();
     
    5061                .map(f -> {
    5162                    NonAdminUser follower = f.getFollower();
    52                     NonAdminUserDto dto = mapper.toDto(follower, null, null, null);
     63                    boolean isFollowedByCurrentUser=isFollowedByCurrentUser(currentUserId,follower.getId());
    5364
    54                     dto.setIsFollowedByCurrentUser(isFollowedByCurrentUser(currentUserId,follower.getId()));
    55                     return dto;
     65                    return mapper.toDto(follower, null, new FollowStatusDto(isFollowedByCurrentUser,null,null));
    5666                })
    5767                .toList();
    5868    }
    5969
    60     public List<NonAdminUserDto>getFollowingForUser(Long id){
    61         if (artistRepository.existsById(id)){
    62             throw new FollowException("Cannot view for artist");
    63         }
     70    public List<NonAdminUserDto>getFollowingForUser(String username){
     71        NonAdminUser nonAdminUser=getNonAdminUser(username);
     72        Long id=nonAdminUser.getId();
    6473        List<Follow>followings=followRepository.getFollowsByFollower_Id(id);
    6574        Long currentUserId=authService.getCurrentUserID();
     
    6776                .map(f->{
    6877                    NonAdminUser followee=f.getFollowee();
    69                     NonAdminUserDto dto=mapper.toDto(f.getFollowee(),null,null,null);
    70                     dto.setIsFollowedByCurrentUser(isFollowedByCurrentUser(currentUserId,followee.getId()));
    71                     return dto;
     78                    boolean isFollowedByCurrentUser=isFollowedByCurrentUser(currentUserId,followee.getId());
     79                    return mapper.toDto(f.getFollowee(),null,new FollowStatusDto(isFollowedByCurrentUser,null,null));
    7280                })
    7381                .toList();
     
    7684
    7785
    78     public FollowStatusDto toggleFollow(Long id){
     86    public FollowStatusDto toggleFollow(String username){
    7987        Long currentUserId=authService.getCurrentUserID();
    8088        boolean isFollowing;
     89        NonAdminUser nonAdminUser=nonAdminUserRepository.findByUsername(username)
     90                .orElseThrow(()-> new UserNotFoundException("Cannot find user with username: "+ username));
     91        Long id=nonAdminUser.getId();
    8192
    8293        if (currentUserId.equals(id)){
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/ListenerService.java

    rd2af1a6 r0792d02  
    2828        return playlistService.findByCreatorId(listenerId);
    2929    }
     30
     31    public List<Playlist>getPlaylistsSavedByUser(Long listenerId){
     32        return playlistService.findSavedByUser(listenerId);
     33    }
    3034}
    3135
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/NonAdminUserService.java

    rd2af1a6 r0792d02  
    99import org.springframework.stereotype.Service;
    1010import org.springframework.transaction.annotation.Transactional;
     11
     12import java.util.Collections;
    1113import java.util.List;
     14import java.util.Optional;
    1215import java.util.Set;
    1316import java.util.stream.Collectors;
     
    3134                .map(user -> {
    3235                    String type = determineType(user.getId());
    33                     return mapper.toDto(user, type, null, null);
     36                    return mapper.toDto(user, type, new FollowStatusDto(false,null,null));
    3437                })
    3538                .collect(Collectors.toList());
     
    3740
    3841    @Transactional(readOnly = true)
    39     public NonAdminUserDto getById(Long id) {
    40         Long followers=followRepository.countByFolloweeId(id);
    41         Long following=followRepository.countByFollowerId(id);
     42    public NonAdminUserDto getNonAdminUserProfile(String username) {
     43
    4244        Long currentUserId= authService.getCurrentUserID();
    4345
    44         boolean isFollowedByCurrentUser=false;
    45         if (currentUserId != null) {
    46             isFollowedByCurrentUser = followRepository.isFollowing(currentUserId, id);
     46
     47        Optional<Artist>artistOptional=artistRepository.findByUsername(username);
     48   
     49        if (artistOptional.isPresent()) {
     50            Artist artist = artistOptional.get();
     51            return getArtistProfile(artist, currentUserId);
    4752        }
    48         NonAdminUserDto dto;
    49    
    50         if (artistRepository.existsById(id)) {
    51              dto=getArtistProfile(id, followers, following);
    52              dto.setIsFollowedByCurrentUser(isFollowedByCurrentUser);
    53              return dto;
     53
     54        Optional<Listener>listenerOptional=listenerRepository.findByUsername(username);
     55     
     56        if (listenerOptional.isPresent()) {
     57            Listener listener=listenerOptional.get();
     58            return getListenerProfile(listener,currentUserId);
    5459        }
    55      
    56         if (listenerRepository.existsById(id)) {
    57             dto=getListenerProfile(id,currentUserId, followers, following);
    58             dto.setIsFollowedByCurrentUser(isFollowedByCurrentUser);
    59             return dto;
    60         }
    61        
    62         throw new RuntimeException("User not found with id: " + id);
     60
     61        throw new UserNotFoundException("User not found: " + username);
    6362    }
    6463
    65     private NonAdminUserDto getArtistProfile(Long artistId, Long followers, Long following) {
    66         Artist artist = artistRepository.findByIdWithUser(artistId)
    67             .orElseThrow(()->new UserNotFoundException("Artist not found with id: " + artistId));
     64    private FollowStatusDto getFollowingInfo(Long id,Long currentUserId){
     65        Long followers = followRepository.countByFolloweeId(id);
     66        Long following = followRepository.countByFollowerId(id);
     67        boolean isFollowing = followRepository.isFollowing(currentUserId, id);
     68        return new FollowStatusDto(isFollowing,followers,following);
     69    }
     70
     71    private NonAdminUserDto getArtistProfile(Artist artist,Long currentUserId) {
     72        Long artistId=artist.getId();
     73
     74        List<ArtistContributionDto>artistContributionDtos=artistService.getArtistContributions(artistId);
     75        FollowStatusDto followStatusDto=getFollowingInfo(artistId,currentUserId);
     76
     77        return mapper.toArtistDTO(artist, artistContributionDtos,followStatusDto);
     78    }
     79
     80    private NonAdminUserDto getListenerProfile(Listener listener, Long currentUserId) {
     81        Long profileId = listener.getId();
     82
     83        FollowStatusDto followStatusDto = getFollowingInfo(profileId, currentUserId);
    6884
    6985
    70         List<ArtistContributionDto>artistContributionDtos=artistService.getArtistContributions(artistId);
     86        List<MusicalEntityDto> likedEntities = listenerService.getLikedEntities(currentUserId, profileId);
    7187
    72         return mapper.toArtistDTO(artist, artistContributionDtos,followers,following);
    73     }
    74 
    75     private NonAdminUserDto getListenerProfile(Long listenerId,Long currentUserId, Long followers, Long following) {
    76         Listener listener = listenerRepository.findByIdWithUser(listenerId)
    77             .orElseThrow(()->new UserNotFoundException("Listener not found with id: " + listenerId));
     88        Set<Long> visitorSavedPlaylistIds = (currentUserId != null)
     89                ? savedPlaylistRepository.findAllByListener_Id(currentUserId).stream()
     90                .map(sp -> sp.getPlaylist().getId())
     91                .collect(Collectors.toSet())
     92                : Collections.emptySet();
    7893
    7994
    80         List<MusicalEntityDto>musicalEntityDtos=listenerService.getLikedEntities(currentUserId,listenerId);
    81         Set<Long> savedIds = savedPlaylistRepository.findAllByListener_Id(currentUserId)
    82                 .stream().map(sp -> sp.getPlaylist().getId()).collect(Collectors.toSet());
    83         List<PlaylistDto>playlists=listenerService.getPlaylistsCreatedByUser(listenerId).stream()
    84                 .map(p->new PlaylistDto(
    85                         p.getId(),
    86                         p.getName(),
    87                         p.getCover(),
    88                         p.getCreatedBy().getNonAdminUser().getUser().getFullName(),
    89                         null,
    90                         savedIds.contains(p.getId())
    91                 )).toList();
     95        List<PlaylistDto> createdPlaylists = listenerService.getPlaylistsCreatedByUser(profileId).stream()
     96                .map(p -> mapToPlaylistDto(p, visitorSavedPlaylistIds))
     97                .toList();
    9298
    93         return mapper.toListenerDTO(listener, musicalEntityDtos,followers,following,playlists);
     99
     100        List<PlaylistDto> savedPlaylists = savedPlaylistRepository.findSavedPlaylistsMetadata(profileId).stream()
     101                .map(p -> mapToPlaylistDto(p, visitorSavedPlaylistIds))
     102                .toList();
     103
     104        return mapper.toListenerDTO(listener, likedEntities, followStatusDto, createdPlaylists, savedPlaylists);
    94105    }
    95106
     
    104115        return nonAdminUserRepository.searchByName(name).stream()
    105116                .map(user -> {
    106                     String type = determineType(user.getId());
    107                     Long followers = followRepository.countByFolloweeId(user.getId());
    108                     Long following = followRepository.countByFollowerId(user.getId());
    109                     return mapper.toDto(user, type, followers, following);
     117                    Long userId= user.getId();
     118                    String type = determineType(userId);
     119                    FollowStatusDto followStatusDto=getFollowingInfo(userId,null);
     120                    return mapper.toDto(user, type,followStatusDto);
    110121                })
    111122                .collect(Collectors.toList());
     
    113124
    114125
    115 
     126    private PlaylistDto mapToPlaylistDto(Playlist p, Set<Long> savedBy) {
     127        return new PlaylistDto(
     128                p.getId(),
     129                p.getName(),
     130                p.getCover(),
     131                p.getCreatedBy().getNonAdminUser().getUser().getFullName(),
     132                null,
     133                savedBy.contains(p.getId())
     134        );
     135    }
    116136    private String determineType(Long id) {
    117137        if (artistRepository.existsById(id)) {
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/PlaylistService.java

    rd2af1a6 r0792d02  
    2929    }
    3030
     31    public List<Playlist>findSavedByUser(Long id){
     32        return savedPlaylistRepository.findSavedPlaylistsMetadata(id);
     33    }
     34
    3135    public PlaylistDto getPlaylist(Long id){
    3236        Long currentUserId=authService.getCurrentUserID();
     
    4852
    4953
     54
     55
    5056}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/mappers/NonAdminUserMapper.java

    rd2af1a6 r0792d02  
    1515public class NonAdminUserMapper {
    1616
    17     public NonAdminUserDto toArtistDTO(Artist artist, List<ArtistContributionDto> entitiesDTO, Long followers, Long following) {
     17    public NonAdminUserDto toArtistDTO(Artist artist, List<ArtistContributionDto> entitiesDTO, FollowStatusDto followStatusDto) {
    1818        ArtistDto dto=new ArtistDto();
    19         populateBaseClassFields(dto,artist.getNonAdminUser(),"ARTIST",followers,following);
     19        populateBaseClassFields(dto,artist.getNonAdminUser(),"ARTIST",followStatusDto);
    2020        dto.setContributions(entitiesDTO);
    2121        return dto;
    2222    }
    2323
    24     public NonAdminUserDto toListenerDTO(Listener listener, List<MusicalEntityDto> musicalEntityDtos, Long followers, Long following, List<PlaylistDto>playlists) {
     24    public NonAdminUserDto toListenerDTO(Listener listener,
     25                                         List<MusicalEntityDto> musicalEntityDtos,
     26                                         FollowStatusDto followStatusDto,
     27                                         List<PlaylistDto>createdPlaylists,
     28                                         List<PlaylistDto>savedPlaylists) {
    2529        ListenerDto dto=new ListenerDto();
    26         populateBaseClassFields(dto,listener.getNonAdminUser(),"LISTENER",followers,following);
     30        populateBaseClassFields(dto,listener.getNonAdminUser(),"LISTENER",followStatusDto);
    2731        dto.setLikedEntities(musicalEntityDtos);
    28         dto.setCreatedPlaylists(playlists);
     32        dto.setCreatedPlaylists(createdPlaylists);
     33        dto.setSavedPlaylists(savedPlaylists);
    2934        return dto;
    3035    }
    3136
    32     public NonAdminUserDto toDto(NonAdminUser user,String type, Long followers,Long following){
     37    public NonAdminUserDto toDto(NonAdminUser user,String type, FollowStatusDto followStatusDto){
    3338       NonAdminUserDto dto=new NonAdminUserDto();
    34        populateBaseClassFields(dto,user,type,followers,following);
     39       populateBaseClassFields(dto,user,type,followStatusDto);
    3540       return dto;
    3641    }
     
    3843
    3944
    40     private void populateBaseClassFields(NonAdminUserDto dto,NonAdminUser user,String type,Long followers,Long following){
    41 
    42         dto.setId(user.getId());
     45    private void populateBaseClassFields(NonAdminUserDto dto,NonAdminUser user,String type,FollowStatusDto followStatusDto){
    4346        dto.setUsername(user.getUser().getUsername());
    4447        dto.setFullName(user.getUser().getFullName());
    4548        dto.setProfilePhoto(user.getUser().getProfilePhoto());
    4649        dto.setUserType(type);
    47         dto.setFollowers(followers);
    48         dto.setFollowing(following);
     50        dto.setFollowers(followStatusDto.followerCount());
     51        dto.setFollowing(followStatusDto.followingCount());
     52        dto.setIsFollowedByCurrentUser(followStatusDto.isFollowing());
    4953    }
    5054}
  • frontend/src/App.tsx

    rd2af1a6 r0792d02  
    5151      },
    5252      {
    53         path: "/users/:userId",
     53        path: "/users/:username",
    5454        element: <UserDetail />,
    5555      },
  • frontend/src/components/userProfile/ListenerView.tsx

    rd2af1a6 r0792d02  
    44import axiosInstance from "../../api/axiosInstance";
    55import { useState } from "react";
     6
    67interface ListenerViewProps {
    78  likedEntities: MusicalEntity[] | [];
    8   playlists: Playlist[] | [];
     9  createdPlaylists: Playlist[] | [];
     10  savedPlaylists: Playlist[] | [];
    911}
    1012
    11 const ListenerView = ({ likedEntities, playlists }: ListenerViewProps) => {
     13const ListenerView = ({
     14  likedEntities,
     15  createdPlaylists,
     16  savedPlaylists,
     17}: ListenerViewProps) => {
    1218  const navigate = useNavigate();
    1319  const [items, setItems] = useState(likedEntities);
     20  const [savedItems, setSavedItems] = useState(savedPlaylists);
    1421  const [toast, setToast] = useState<{ message: string; show: boolean }>({
    1522    message: "",
     
    2734  const likedAlbums = items.filter((e) => e.type === "ALBUM");
    2835
    29   const handleSavePlaylist = (e: React.MouseEvent, playlistId: number) => {
     36  const handleSavePlaylist = async (
     37    e: React.MouseEvent,
     38    playlistId: number,
     39    playlistName: string,
     40  ) => {
    3041    e.stopPropagation();
    3142
    32     console.log("Save playlist:", playlistId);
     43    try {
     44      const response = await axiosInstance.post(`/playlist/${playlistId}/save`);
     45      const data = response.data;
     46
     47      setSavedItems((prevItems) =>
     48        data.isSaved
     49          ? [...prevItems, prevItems.find((p) => p.id === playlistId)!]
     50          : prevItems.filter((p) => p.id !== playlistId),
     51      );
     52
     53      showToast(
     54        data.isSaved
     55          ? `Saved "${playlistName}"`
     56          : `Removed "${playlistName}" from saved playlists`,
     57      );
     58    } catch (err: any) {
     59      showToast(err.response?.data?.error || "Failed to save playlist");
     60    }
    3361  };
    3462
     
    6391        </div>
    6492      )}
    65       {playlists && playlists.length > 0 && (
     93
     94      {createdPlaylists && createdPlaylists.length > 0 && (
    6695        <section>
    6796          <div className="flex items-center gap-3 mb-6">
     
    7099              Created Playlists
    71100            </h3>
    72             <span className="text-sm text-gray-500">({playlists.length})</span>
     101            <span className="text-sm text-gray-500">
     102              ({createdPlaylists.length})
     103            </span>
    73104          </div>
    74105
    75106          <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
    76             {playlists.map((playlist) => (
     107            {createdPlaylists.map((playlist) => (
    77108              <div
    78109                key={playlist.id}
     
    93124                  )}
    94125
    95                   {/* Save button overlay */}
    96126                  <button
    97                     onClick={(e) => handleSavePlaylist(e, playlist.id)}
     127                    onClick={(e) =>
     128                      handleSavePlaylist(e, playlist.id, playlist.name)
     129                    }
    98130                    className="absolute top-2 right-2 p-2 bg-white/90 hover:bg-white rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100"
    99131                    title={playlist.isSavedByCurrentUser ? "Unsave" : "Save"}
     
    106138                      }`}
    107139                    />
     140                  </button>
     141                </div>
     142                <p className="text-sm font-semibold text-gray-900 truncate group-hover:text-blue-600 transition-colors">
     143                  {playlist.name}
     144                </p>
     145                <p className="text-xs text-gray-500 truncate">
     146                  {playlist.creatorName}
     147                </p>
     148              </div>
     149            ))}
     150          </div>
     151        </section>
     152      )}
     153
     154      {savedItems && savedItems.length > 0 && (
     155        <section>
     156          <div className="flex items-center gap-3 mb-6">
     157            <Bookmark className="w-6 h-6 text-blue-600 fill-blue-600" />
     158            <h3 className="text-2xl font-bold text-gray-800">
     159              Saved Playlists
     160            </h3>
     161            <span className="text-sm text-gray-500">({savedItems.length})</span>
     162          </div>
     163
     164          <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
     165            {savedItems.map((playlist) => (
     166              <div
     167                key={playlist.id}
     168                className="group cursor-pointer"
     169                onClick={() => navigate(`/collection/playlist/${playlist.id}`)}
     170              >
     171                <div className="relative aspect-square rounded-lg overflow-hidden bg-gray-100 mb-3 shadow-md group-hover:shadow-lg transition-all">
     172                  {playlist.cover ? (
     173                    <img
     174                      src={playlist.cover}
     175                      alt={playlist.name}
     176                      className="w-full h-full object-cover"
     177                    />
     178                  ) : (
     179                    <div className="w-full h-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center">
     180                      <ListMusic className="w-16 h-16 text-white opacity-30" />
     181                    </div>
     182                  )}
     183
     184                  <button
     185                    onClick={(e) =>
     186                      handleSavePlaylist(e, playlist.id, playlist.name)
     187                    }
     188                    className="absolute top-2 right-2 p-2 bg-white/90 hover:bg-white rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100"
     189                    title="Unsave"
     190                  >
     191                    <Bookmark className="w-5 h-5 fill-blue-600 text-blue-600" />
    108192                  </button>
    109193                </div>
     
    234318      {likedEntities &&
    235319        likedEntities.length === 0 &&
    236         (!playlists || playlists.length === 0) && (
     320        (!createdPlaylists || createdPlaylists.length === 0) &&
     321        (!savedItems || savedItems.length === 0) && (
    237322          <div className="flex flex-col items-center justify-center py-16 text-gray-400">
    238323            <Music className="w-20 h-20 mb-4 opacity-20" />
  • frontend/src/components/userProfile/UserListModal.tsx

    rd2af1a6 r0792d02  
    66  users: BaseNonAdminUser[];
    77  onClose: () => void;
    8   onFollowToggle: (targetId: number) => Promise<void>;
     8  onFollowToggle: (targetUsername: string) => Promise<void>;
    99}
    1010
     
    3737            users.map((u) => (
    3838              <div
    39                 key={u.id}
     39                key={u.username}
    4040                className="flex items-center justify-between p-3 hover:bg-gray-50 rounded-lg transition-colors"
    4141              >
     
    4444                  onClick={() => {
    4545                    onClose();
    46                     navigate(`/users/${u.id}`);
     46                    navigate(`/users/${u.username}`);
    4747                  }}
    4848                >
     
    6464
    6565                <button
    66                   onClick={() => onFollowToggle(u.id)}
     66                  onClick={() => onFollowToggle(u.username)}
    6767                  className={`px-4 py-1 text-sm font-medium rounded-md transition-colors cursor-pointer ${
    6868                    u.isFollowedByCurrentUser
  • frontend/src/pages/AllUsers.tsx

    rd2af1a6 r0792d02  
    44
    55interface User {
    6         id: number;
    7         username: string;
    8         fullName: string;
     6  username: string;
     7  fullName: string;
    98}
    109
    1110const AllUsers = () => {
    12         const [users, setUsers] = useState<User[]>([]);
    13         const [searchedUser, setSearchedUser] = useState<string>("");
    14         const [error, setError] = useState<string | null>(null);
    15         const navigate = useNavigate();
     11  const [users, setUsers] = useState<User[]>([]);
     12  const [searchedUser, setSearchedUser] = useState<string>("");
     13  const [error, setError] = useState<string | null>(null);
     14  const navigate = useNavigate();
    1615
    17         useEffect(() => {
    18                 const fetchUsers = async () => {
    19                         const response = axiosInstance.get("/users/all");
    20                         setUsers((await response).data);
    21                 };
    22                 fetchUsers();
    23         }, []);
     16  useEffect(() => {
     17    const fetchUsers = async () => {
     18      const response = axiosInstance.get("/users/all");
     19      setUsers((await response).data);
     20    };
     21    fetchUsers();
     22  }, []);
    2423
    25         const handleUserClick = (userId: number) => {
    26                 navigate(`/users/${userId}`);
    27         };
     24  const handleUserClick = (username: string) => {
     25    navigate(`/users/${username}`);
     26  };
    2827
    29         const handleSearch = async (e: React.FormEvent) => {
    30                 e.preventDefault();
    31                 try {
    32                         const response = await axiosInstance.get(`/users/search`, {
    33                                 params: { name: searchedUser },
    34                         });
    35                         setUsers(response.data);
    36                 } catch (err: any) {
    37                         const errorMessage = err.response?.data?.error || "Search failed";
    38                         setError(errorMessage);
    39                 }
    40         };
     28  const handleSearch = async (e: React.FormEvent) => {
     29    e.preventDefault();
     30    try {
     31      const response = await axiosInstance.get(`/users/search`, {
     32        params: { name: searchedUser },
     33      });
     34      setUsers(response.data);
     35    } catch (err: any) {
     36      const errorMessage = err.response?.data?.error || "Search failed";
     37      setError(errorMessage);
     38    }
     39  };
    4140
    42         if (users.length == 0) return <div className="p-6">Loading...</div>;
    43         if (error) {
    44                 return (
    45                         <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg">
    46                                 <h2 className="font-bold">Error</h2>
    47                                 <p>{error}</p>
    48                         </div>
    49                 );
    50         }
     41  if (users.length == 0) return <div className="p-6">Loading...</div>;
     42  if (error) {
     43    return (
     44      <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg">
     45        <h2 className="font-bold">Error</h2>
     46        <p>{error}</p>
     47      </div>
     48    );
     49  }
    5150
    52         return (
    53                 <div className="container mx-auto p-6">
    54                         <h1 className="text-3xl font-bold mb-6">All Users</h1>
    55                         <form onSubmit={handleSearch} className="flex gap-2 mb-10">
    56                                 <input
    57                                         type="text"
    58                                         value={searchedUser}
    59                                         onChange={(e) => setSearchedUser(e.target.value)}
    60                                         className="border p-2 rounded w-full"
    61                                         placeholder="Search for a user..."
    62                                 />
    63                                 <button
    64                                         type="submit"
    65                                         className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 transition-colors"
    66                                 >
    67                                         Search
    68                                 </button>
    69                         </form>
     51  return (
     52    <div className="container mx-auto p-6">
     53      <h1 className="text-3xl font-bold mb-6">All Users</h1>
     54      <form onSubmit={handleSearch} className="flex gap-2 mb-10">
     55        <input
     56          type="text"
     57          value={searchedUser}
     58          onChange={(e) => setSearchedUser(e.target.value)}
     59          className="border p-2 rounded w-full"
     60          placeholder="Search for a user..."
     61        />
     62        <button
     63          type="submit"
     64          className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 transition-colors"
     65        >
     66          Search
     67        </button>
     68      </form>
    7069
    71                         <div className="grid gap-4">
    72                                 {users.map((u) => (
    73                                         <div
    74                                                 key={u.id}
    75                                                 onClick={() => handleUserClick(u.id)}
    76                                                 className="bg-white shadow-md rounded-lg p-4 hover:shadow-lg hover:bg-gray-50 cursor-pointer transition-all"
    77                                         >
    78                                                 <h2 className="text-xl font-semibold">{u.fullName}</h2>
    79                                                 <p className="text-gray-600">@{u.username}</p>
    80                                                 <p className="text-gray-600">{u.id}</p>
    81                                         </div>
    82                                 ))}
    83                         </div>
    84                 </div>
    85         );
     70      <div className="grid gap-4">
     71        {users.map((u) => (
     72          <div
     73            key={u.username}
     74            onClick={() => handleUserClick(u.username)}
     75            className="bg-white shadow-md rounded-lg p-4 hover:shadow-lg hover:bg-gray-50 cursor-pointer transition-all"
     76          >
     77            <h2 className="text-xl font-semibold">{u.fullName}</h2>
     78            <p className="text-gray-600">@{u.username}</p>
     79          </div>
     80        ))}
     81      </div>
     82    </div>
     83  );
    8684};
    8785
  • frontend/src/pages/UserDetail.tsx

    rd2af1a6 r0792d02  
    2727  likedEntities: MusicalEntity[];
    2828  createdPlaylists: Playlist[];
     29  savedPlaylists: Playlist[];
    2930}
    3031
     
    3435  // user refers to the selected user NOT to the user from context
    3536  const baseURL = import.meta.env.VITE_API_BASE_URL;
    36   const { userId } = useParams();
     37  const { username } = useParams();
    3738  const navigate = useNavigate();
    3839  const [user, setUser] = useState<UserProfile | null>(null);
     
    5051    try {
    5152      const response = await axiosInstance.post<FollowStatus>(
    52         `/users/${userId}/follow`,
     53        `/users/${username}/follow`,
    5354      );
    5455      setUser((prev) => {
     
    6869  };
    6970
    70   const handleFollowInModal = async (targetId: number) => {
     71  const handleFollowInModal = async (targetUsername: string) => {
    7172    try {
    7273      const response = await axiosInstance.post<FollowStatus>(
    73         `/users/${targetId}/follow`,
     74        `/users/${username}/follow`,
    7475      );
    7576
    7677      setModalUsers((prevUsers) =>
    7778        prevUsers.map((u) =>
    78           u.id === targetId
     79          u.username === targetUsername
    7980            ? { ...u, isFollowedByCurrentUser: response.data.isFollowing }
    8081            : u,
     
    100101    setIsLoadingModal(true);
    101102    try {
    102       const response = await axiosInstance.get(`/users/${userId}/followers`);
     103      const response = await axiosInstance.get(`/users/${username}/followers`);
    103104      setModalUsers(response.data);
    104105      setModalTitle("Followers");
     
    113114    setIsLoadingModal(true);
    114115    try {
    115       const response = await axiosInstance.get(`/users/${userId}/following`);
     116      const response = await axiosInstance.get(`/users/${username}/following`);
    116117      setModalUsers(response.data);
    117118      setModalTitle("Following");
     
    128129      setError(null);
    129130      try {
    130         const response = await axiosInstance.get(`/users/${userId}`);
     131        const response = await axiosInstance.get(`/users/${username}`);
    131132        console.log(response.data);
    132133        setUser(response.data);
     
    136137    };
    137138    fetchUser();
    138   }, [userId]);
     139  }, [username]);
    139140
    140141  if (error) {
     
    233234          <ListenerView
    234235            likedEntities={user.likedEntities}
    235             playlists={user.createdPlaylists}
     236            createdPlaylists={user.createdPlaylists}
     237            savedPlaylists={user.savedPlaylists}
    236238          />
    237239        )}
  • frontend/src/utils/types.ts

    rd2af1a6 r0792d02  
    4444
    4545export interface BaseNonAdminUser {
    46   id: number;
     46  username: string;
    4747  fullName: string;
    4848  userType: string;
Note: See TracChangeset for help on using the changeset viewer.