Changeset d2af1a6
- Timestamp:
- 02/01/26 17:29:49 (5 months ago)
- Branches:
- main
- Children:
- 0792d02
- Parents:
- 2730163
- Files:
-
- 4 added
- 17 edited
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/GlobalExceptionHandler.java (modified) (4 diffs)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/MusicalEntityController.java (added)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/NonAdminUserController.java (modified) (2 diffs)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/exceptions/MusicalEntityNotFoundException.java (added)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/Like.java (modified) (3 diffs)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/LikeId.java (modified) (2 diffs)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/dto/ArtistContributionDto.java (modified) (1 diff)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/dto/FollowStatusDto.java (added)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/dto/LikeStatusDto.java (added)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/dto/PlaylistDto.java (modified) (1 diff)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/LikeRepository.java (modified) (1 diff)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/SavedPlaylistRepository.java (modified) (1 diff)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/FollowService.java (modified) (4 diffs)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/LikeService.java (modified) (2 diffs)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/ListenerService.java (modified) (1 diff)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/NonAdminUserService.java (modified) (5 diffs)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/PlaylistService.java (modified) (5 diffs)
-
frontend/src/components/userProfile/ArtistView.tsx (modified) (5 diffs)
-
frontend/src/components/userProfile/ListenerView.tsx (modified) (7 diffs)
-
frontend/src/pages/UserDetail.tsx (modified) (4 diffs)
-
frontend/src/utils/types.ts (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/GlobalExceptionHandler.java
r2730163 rd2af1a6 3 3 import java.util.Map; 4 4 5 import com.ukim.finki.develop.finkwave.exceptions.AlbumNotFoundException; 6 import com.ukim.finki.develop.finkwave.exceptions.FollowException; 7 import com.ukim.finki.develop.finkwave.exceptions.UserNotFoundException; 5 import com.ukim.finki.develop.finkwave.exceptions.*; 8 6 import org.slf4j.Logger; 9 7 import org.slf4j.LoggerFactory; … … 15 13 import org.springframework.web.multipart.MaxUploadSizeExceededException; 16 14 import org.springframework.web.server.ResponseStatusException; 17 18 import com.ukim.finki.develop.finkwave.exceptions.AuthException;19 15 20 16 @RestControllerAdvice … … 39 35 public ResponseEntity<Map<String, String>> handleUserNotFound(UserNotFoundException ex) { 40 36 return ResponseEntity 41 .status(HttpStatus. BAD_REQUEST)37 .status(HttpStatus.NOT_FOUND) 42 38 .body(Map.of("error", ex.getMessage())); 43 39 } … … 53 49 public ResponseEntity<Map<String, String>> handleAlbumNotFound(AlbumNotFoundException ex) { 54 50 return ResponseEntity 55 .status(HttpStatus.BAD_REQUEST) 51 .status(HttpStatus.NOT_FOUND) 52 .body(Map.of("error", ex.getMessage())); 53 } 54 55 @ExceptionHandler(MusicalEntityNotFoundException.class) 56 public ResponseEntity<Map<String, String>> handleMusicalEntityNotFound(MusicalEntityNotFoundException ex) { 57 return ResponseEntity 58 .status(HttpStatus.NOT_FOUND) 56 59 .body(Map.of("error", ex.getMessage())); 57 60 } -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/NonAdminUserController.java
r2730163 rd2af1a6 1 1 package com.ukim.finki.develop.finkwave.controller; 2 2 3 import com.ukim.finki.develop.finkwave.model.dto.FollowStatusDto; 3 4 import com.ukim.finki.develop.finkwave.model.dto.NonAdminUserDto; 4 5 import com.ukim.finki.develop.finkwave.service.FollowService; … … 46 47 47 48 @PostMapping("/{id}/follow") 48 public HttpEntity<NonAdminUserDto>followUser(@PathVariable Long id){ 49 followService.toggleFollow(id); 49 public HttpEntity<FollowStatusDto>followUser(@PathVariable Long id){ 50 50 51 return ResponseEntity.ok( nonAdminUserService.getById(id));51 return ResponseEntity.ok(followService.toggleFollow(id)); 52 52 } 53 53 -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/Like.java
r2730163 rd2af1a6 3 3 import jakarta.persistence.*; 4 4 import lombok.Getter; 5 import lombok.NoArgsConstructor; 5 6 import lombok.Setter; 6 7 import org.hibernate.annotations.OnDelete; … … 10 11 @Setter 11 12 @Entity 13 @NoArgsConstructor 12 14 @Table(name = "likes", schema = "project") 13 15 public class Like { … … 27 29 private MusicalEntity musicalEntity; 28 30 31 public Like(MusicalEntity musicalEntity, Listener listener) { 32 this.musicalEntity = musicalEntity; 33 this.listener = listener; 34 this.id=new LikeId(listener.getId(),musicalEntity.getId()); 35 } 29 36 } -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/LikeId.java
r2730163 rd2af1a6 3 3 import jakarta.persistence.Column; 4 4 import jakarta.persistence.Embeddable; 5 import lombok.AllArgsConstructor; 5 6 import lombok.Getter; 7 import lombok.NoArgsConstructor; 6 8 import lombok.Setter; 7 9 import org.hibernate.Hibernate; … … 13 15 @Setter 14 16 @Embeddable 17 @AllArgsConstructor 18 @NoArgsConstructor 15 19 public class LikeId implements Serializable { 16 20 private static final long serialVersionUID = -2521356135522333223L; -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/dto/ArtistContributionDto.java
r2730163 rd2af1a6 9 9 @AllArgsConstructor 10 10 public class ArtistContributionDto { 11 private Long musicalEntityId;11 private Long id; 12 12 private String title; 13 13 private String genre; -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/dto/PlaylistDto.java
r2730163 rd2af1a6 15 15 private String cover; 16 16 private String creatorName; 17 18 17 private List<MusicalEntityDto>songsInPlaylist; 18 private Boolean isSavedByCurrentUser; 19 19 20 20 } -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/LikeRepository.java
r2730163 rd2af1a6 15 15 @Repository 16 16 public interface LikeRepository extends JpaRepository<Like, LikeId> { 17 @Query("SELECT NEW com.ukim.finki.develop.finkwave.model.dto.MusicalEntityDto(l.musicalEntity.id, " + 18 "l.musicalEntity.title," + 19 "l.musicalEntity.genre, " + 20 "CASE WHEN s.id IS NOT NULL THEN 'SONG' " + 21 "WHEN a.id IS NOT NULL THEN 'ALBUM' " + 22 "ELSE 'UNKNOWN' END," + 23 "u.fullName," + 24 "true)" + 25 "FROM Like l " + 26 "LEFT JOIN Song s ON s.musicalEntities.id = l.musicalEntity.id " + 27 "LEFT JOIN Album a ON a.musicalEntities.id = l.musicalEntity.id " + 28 "LEFT JOIN User u ON u.id=l.musicalEntity.releasedBy.id "+ 29 "WHERE l.listener.id = :listenerId") 30 List<MusicalEntityDto> findLikedEntitiesWithTypeByListenerId(@Param("listenerId") Long listenerId); 17 @Query("SELECT NEW com.ukim.finki.develop.finkwave.model.dto.MusicalEntityDto(" + 18 "me.id, " + 19 "me.title, " + 20 "me.genre, " + 21 "CASE WHEN s.id IS NOT NULL THEN 'SONG' " + 22 " WHEN a.id IS NOT NULL THEN 'ALBUM' " + 23 " ELSE 'UNKNOWN' END, " + 24 "u.fullName, " + 25 "(CASE WHEN currentUserLike.id IS NOT NULL THEN true ELSE false END)" + 26 ") " + 27 "FROM Like l " + 28 "JOIN l.musicalEntity me " + 29 "JOIN me.releasedBy nu " + 30 "JOIN nu.nonAdminUser.user u " + 31 "LEFT JOIN Song s ON s.musicalEntities.id = me.id " + 32 "LEFT JOIN Album a ON a.musicalEntities.id = me.id " + 33 "LEFT JOIN Like currentUserLike ON currentUserLike.musicalEntity.id = me.id " + 34 "AND currentUserLike.listener.id = :currentUserId " + 35 "WHERE l.listener.id = :listenerId") 36 List<MusicalEntityDto> findLikedEntitiesWithTypeByListenerId(@Param("currentUserId")Long currentUserId,@Param("listenerId") Long listenerId); 31 37 32 38 -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/SavedPlaylistRepository.java
r2730163 rd2af1a6 7 7 import com.ukim.finki.develop.finkwave.model.SavedPlaylistId; 8 8 9 import java.util.List; 10 9 11 @Repository 10 12 public interface SavedPlaylistRepository extends JpaRepository<SavedPlaylist, SavedPlaylistId> { 13 14 List<SavedPlaylist>findAllByListener_Id(Long currentUserId); 11 15 } -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/FollowService.java
r2730163 rd2af1a6 6 6 import com.ukim.finki.develop.finkwave.model.FollowId; 7 7 import com.ukim.finki.develop.finkwave.model.NonAdminUser; 8 import com.ukim.finki.develop.finkwave.model.dto.FollowStatusDto; 8 9 import com.ukim.finki.develop.finkwave.model.dto.NonAdminUserDto; 9 10 import com.ukim.finki.develop.finkwave.repository.ArtistRepository; … … 75 76 76 77 77 public voidtoggleFollow(Long id){78 public FollowStatusDto toggleFollow(Long id){ 78 79 Long currentUserId=authService.getCurrentUserID(); 80 boolean isFollowing; 79 81 80 82 if (currentUserId.equals(id)){ … … 87 89 if (followRepository.existsById(followId)){ 88 90 followRepository.deleteById(followId); 91 isFollowing=false; 89 92 90 93 }else{ … … 95 98 96 99 followRepository.save(new Follow(follower, followee)); 100 isFollowing=true; 97 101 98 102 } 103 return new FollowStatusDto( 104 isFollowing, 105 followRepository.countByFolloweeId(id), 106 followRepository.countByFollowerId(id)); 99 107 100 108 } -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/LikeService.java
r2730163 rd2af1a6 1 1 package com.ukim.finki.develop.finkwave.service; 2 2 3 import com.ukim.finki.develop.finkwave.exceptions.MusicalEntityNotFoundException; 4 import com.ukim.finki.develop.finkwave.exceptions.UserNotFoundException; 5 import com.ukim.finki.develop.finkwave.model.Like; 6 import com.ukim.finki.develop.finkwave.model.LikeId; 7 import com.ukim.finki.develop.finkwave.model.Listener; 8 import com.ukim.finki.develop.finkwave.model.MusicalEntity; 9 import com.ukim.finki.develop.finkwave.model.dto.LikeStatusDto; 3 10 import com.ukim.finki.develop.finkwave.model.dto.MusicalEntityDto; 4 import com.ukim.finki.develop.finkwave.repository. LikeRepository;11 import com.ukim.finki.develop.finkwave.repository.*; 5 12 import lombok.AllArgsConstructor; 6 13 import org.springframework.stereotype.Service; … … 11 18 @AllArgsConstructor 12 19 public class LikeService { 20 private final LikeRepository likeRepository; 21 private final AuthService authService; 22 private final MusicalEntityRepository musicalEntityRepository; 23 private AlbumRepository albumRepository; 24 private final SongRepository songRepository; 25 private final ListenerRepository listenerRepository; 13 26 14 private final LikeRepository likeRepository; 15 public List<MusicalEntityDto> findLikedEntitiesWithTypeByListenerId(Long listenerId){ 16 return likeRepository.findLikedEntitiesWithTypeByListenerId(listenerId); 27 public List<MusicalEntityDto> findLikedEntitiesWithTypeByListenerId(Long currentUserId,Long listenerId){ 28 return likeRepository.findLikedEntitiesWithTypeByListenerId(currentUserId,listenerId); 17 29 } 30 31 public LikeStatusDto toggleLike(Long entityId){ 32 Long currentUserId=authService.getCurrentUserID(); 33 34 35 LikeId likeId=new LikeId(currentUserId,entityId); 36 boolean isLiked; 37 if (likeRepository.existsById(likeId)){ 38 likeRepository.deleteById(likeId); 39 isLiked=false; 40 } 41 else{ 42 Listener listener=listenerRepository.findById(currentUserId).orElseThrow( 43 ()->new UserNotFoundException("Listener not found") 44 ); 45 MusicalEntity entity=musicalEntityRepository.findById(entityId).orElseThrow( 46 ()->new MusicalEntityNotFoundException("Entity not found") 47 ); 48 likeRepository.save(new Like(entity,listener)); 49 isLiked=true; 50 } 51 return new LikeStatusDto(entityId,isLiked,determineType(entityId)); 52 53 } 54 55 private String determineType(Long id){ 56 if (albumRepository.existsById(id)){ 57 return "ALBUM"; 58 }else if(songRepository.existsById(id)){ 59 return "SONG"; 60 }return "UNDEFINED"; 61 } 62 18 63 } -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/ListenerService.java
r2730163 rd2af1a6 18 18 private final PlaylistService playlistService; 19 19 20 public List<MusicalEntityDto> getLikedEntities(Long listenerId) {21 return likeService.findLikedEntitiesWithTypeByListenerId( listenerId);20 public List<MusicalEntityDto> getLikedEntities(Long currentUserId,Long listenerId) { 21 return likeService.findLikedEntitiesWithTypeByListenerId(currentUserId,listenerId); 22 22 23 23 -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/NonAdminUserService.java
r2730163 rd2af1a6 10 10 import org.springframework.transaction.annotation.Transactional; 11 11 import java.util.List; 12 import java.util.Set; 12 13 import java.util.stream.Collectors; 13 14 … … 23 24 private final FollowRepository followRepository; 24 25 private final AuthService authService; 26 private final SavedPlaylistRepository savedPlaylistRepository; 25 27 26 28 @Transactional(readOnly = true) … … 53 55 54 56 if (listenerRepository.existsById(id)) { 55 dto=getListenerProfile(id, followers, following);57 dto=getListenerProfile(id,currentUserId, followers, following); 56 58 dto.setIsFollowedByCurrentUser(isFollowedByCurrentUser); 57 59 return dto; … … 71 73 } 72 74 73 private NonAdminUserDto getListenerProfile(Long listenerId, Long followers, Long following) {75 private NonAdminUserDto getListenerProfile(Long listenerId,Long currentUserId, Long followers, Long following) { 74 76 Listener listener = listenerRepository.findByIdWithUser(listenerId) 75 77 .orElseThrow(()->new UserNotFoundException("Listener not found with id: " + listenerId)); 76 78 77 79 78 List<MusicalEntityDto>musicalEntityDtos=listenerService.getLikedEntities(listenerId); 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()); 79 83 List<PlaylistDto>playlists=listenerService.getPlaylistsCreatedByUser(listenerId).stream() 80 84 .map(p->new PlaylistDto( … … 83 87 p.getCover(), 84 88 p.getCreatedBy().getNonAdminUser().getUser().getFullName(), 85 null 89 null, 90 savedIds.contains(p.getId()) 86 91 )).toList(); 87 92 -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/PlaylistService.java
r2730163 rd2af1a6 6 6 import com.ukim.finki.develop.finkwave.model.dto.PlaylistDto; 7 7 import com.ukim.finki.develop.finkwave.repository.PlaylistRepository; 8 import com.ukim.finki.develop.finkwave.repository.SavedPlaylistRepository; 8 9 import com.ukim.finki.develop.finkwave.repository.SongRepository; 9 10 import lombok.AllArgsConstructor; … … 12 13 13 14 import java.util.List; 15 import java.util.Set; 16 import java.util.stream.Collectors; 14 17 15 18 @Service … … 20 23 private final SongRepository songRepository; 21 24 private final AuthService authService; 25 private final SavedPlaylistRepository savedPlaylistRepository; 22 26 23 27 public List<Playlist>findByCreatorId(Long id){ … … 29 33 Playlist playlist = playlistRepository.findById(id).orElseThrow(()-> new PlaylistNotFoundException(id)); 30 34 List<MusicalEntityDto>songsInPlaylist=songRepository.findSongsByPlaylistId(id,currentUserId); 35 Set<Long> savedIds = savedPlaylistRepository.findAllByListener_Id(currentUserId) 36 .stream().map(sp -> sp.getPlaylist().getId()).collect(Collectors.toSet()); 31 37 return new PlaylistDto( 32 38 playlist.getId(), … … 34 40 playlist.getCover(), 35 41 playlist.getCreatedBy().getNonAdminUser().getUser().getFullName(), 36 songsInPlaylist 42 songsInPlaylist, 43 savedIds.contains(playlist.getId()) 44 37 45 ); 38 46 } -
frontend/src/components/userProfile/ArtistView.tsx
r2730163 rd2af1a6 1 1 import { useNavigate } from "react-router-dom"; 2 import { Music, Disc3 } from "lucide-react"; 2 import { useState } from "react"; 3 import { Music, Disc3, Play, Plus } from "lucide-react"; 3 4 import type { ArtistContribution } from "../../utils/types"; 5 import axiosInstance from "../../api/axiosInstance"; 4 6 5 7 interface ArtistViewProps { … … 9 11 const ArtistView = ({ contributions }: ArtistViewProps) => { 10 12 const navigate = useNavigate(); 11 12 const albums = contributions.filter((c) => c.entityType === "ALBUM"); 13 const songs = contributions.filter((c) => c.entityType === "SONG"); 13 const [items, setItems] = useState(contributions); 14 const [toast, setToast] = useState<{ message: string; show: boolean }>({ 15 message: "", 16 show: false, 17 }); 18 19 const albums = items.filter((c) => c.entityType === "ALBUM"); 20 const songs = items.filter((c) => c.entityType === "SONG"); 14 21 15 22 const getRoleColor = (role: string) => { 16 23 const colors: { [key: string]: string } = { 17 COMPOSER: "bg-purple- 500",18 PERFORMER: "bg-blue- 500",19 PRODUCER: "bg-green- 500",20 MAIN_VOCAL: "bg-pink- 500",24 COMPOSER: "bg-purple-100 text-purple-700", 25 PERFORMER: "bg-blue-100 text-blue-700", 26 PRODUCER: "bg-green-100 text-green-700", 27 MAIN_VOCAL: "bg-pink-100 text-pink-700", 21 28 }; 22 return colors[role] || "bg-gray-500"; 29 return colors[role] || "bg-gray-100 text-gray-700"; 30 }; 31 32 const showToast = (message: string) => { 33 setToast({ message, show: true }); 34 setTimeout(() => { 35 setToast({ message: "", show: false }); 36 }, 2000); 37 }; 38 39 const handleLike = async (id: number, title: string) => { 40 try { 41 const response = await axiosInstance.post(`/musical-entity/${id}/like`); 42 const data = response.data; 43 44 setItems((prevItems) => 45 prevItems.map((item) => 46 item.id === data.entityId 47 ? { ...item, isLikedByCurrentUser: data.isLiked } 48 : item, 49 ), 50 ); 51 52 showToast( 53 data.isLiked ? `Liked "${title}"` : `Removed ${title} from likes`, 54 ); 55 } catch (err: any) { 56 showToast(err.response?.data?.error); 57 } 23 58 }; 24 59 25 60 return ( 26 61 <div className="mt-8"> 62 {toast.show && ( 63 <div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 animate-fade-in-up"> 64 <div className="bg-gray-900 text-white px-6 py-3 rounded-full shadow-lg text-sm font-medium"> 65 {toast.message} 66 </div> 67 </div> 68 )} 69 27 70 {albums.length > 0 && ( 28 71 <div className="mb-12"> 29 72 <div className="flex items-center gap-3 mb-6"> 30 <Disc3 className="w- 8 h-8 text-purple-600" />31 <h2 className="text- 3xl font-bold">Albums</h2>73 <Disc3 className="w-6 h-6 text-gray-700" /> 74 <h2 className="text-2xl font-bold text-gray-800">Albums</h2> 32 75 </div> 33 76 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> 34 {albums.map((album , index) => (77 {albums.map((album) => ( 35 78 <div 36 key={ index}79 key={album.id} 37 80 className="group cursor-pointer" 38 onClick={() => 39 navigate(`/collection/album/${album.musicalEntityId}`) 40 } 81 onClick={() => navigate(`/collection/album/${album.id}`)} 41 82 > 42 <div className="relative aspect-square bg- linear-to-br from-purple-400 via-pink-400 to-blue-400 rounded-lg mb-3 overflow-hidden shadow-lg group-hover:shadow-2xl transition-all duration-300 group-hover:scale-105">83 <div className="relative aspect-square bg-gradient-to-br from-blue-400 to-purple-500 rounded-lg mb-3 overflow-hidden shadow-md group-hover:shadow-lg transition-all duration-300"> 43 84 <div className="absolute inset-0 flex items-center justify-center"> 44 <Disc3 className="w-20 h-20 text-white opacity-40" /> 45 </div> 46 <div className="absolute bottom-0 left-0 right-0 bg-linear-to-t from-black/60 to-transparent p-4"> 85 <Disc3 className="w-16 h-16 text-white opacity-30" /> 86 </div> 87 88 <button 89 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 cursor-pointer" 90 title={album.isLikedByCurrentUser ? "Unlike" : "Like"} 91 onClick={(e) => { 92 e.stopPropagation(); 93 handleLike(album.id, album.title); 94 }} 95 > 96 <svg 97 className="w-5 h-5" 98 fill={album.isLikedByCurrentUser ? "#ef4444" : "none"} 99 stroke={ 100 album.isLikedByCurrentUser ? "#ef4444" : "#6b7280" 101 } 102 strokeWidth="2" 103 viewBox="0 0 24 24" 104 > 105 <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" /> 106 </svg> 107 </button> 108 109 <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent p-3"> 47 110 <span 48 className={`text-xs px-2 py-1 rounded-full text-white${getRoleColor(album.role)}`}111 className={`text-xs px-2 py-1 rounded-full font-medium ${getRoleColor(album.role)}`} 49 112 > 50 113 {album.role.replace("_", " ")} … … 52 115 </div> 53 116 </div> 54 <h3 className="font-semibold text- base line-clamp-2group-hover:text-blue-600 transition-colors">117 <h3 className="font-semibold text-sm line-clamp-2 text-gray-900 group-hover:text-blue-600 transition-colors"> 55 118 {album.title} 56 119 </h3> … … 64 127 <div className="mb-12"> 65 128 <div className="flex items-center gap-3 mb-6"> 66 <Music className="w- 8 h-8 text-blue-600" />67 <h2 className="text- 3xl font-bold">Songs</h2>129 <Music className="w-6 h-6 text-gray-700" /> 130 <h2 className="text-2xl font-bold text-gray-800">Songs</h2> 68 131 </div> 69 132 <div className="space-y-2"> 70 {songs.map((song , index) => (133 {songs.map((song) => ( 71 134 <div 72 key={index} 73 className="group flex items-center gap-4 p-4 rounded-lg hover:bg-gray-50 transition-all cursor-pointer border border-transparent hover:border-gray-200" 74 onClick={() => 75 navigate(`/musical-entity/${song.musicalEntityId}`) 76 } 135 key={song.id} 136 className="group relative flex items-center gap-4 p-3 rounded-lg hover:bg-gray-50 transition-all cursor-pointer" 137 onClick={() => navigate(`/musical-entity/${song.id}`)} 77 138 > 78 <div className="shrink-0 w-14 h-14 bg-linear-to-br from-blue-400 to-cyan-400 rounded flex items-center justify-center group-hover:scale-110 transition-transform shadow-md"> 79 <Music className="w-7 h-7 text-white" /> 80 </div> 139 <div className="relative shrink-0"> 140 <div className="w-12 h-12 bg-gradient-to-br from-blue-400 to-purple-500 rounded flex items-center justify-center shadow-sm"> 141 <Music className="w-6 h-6 text-white" /> 142 </div> 143 <button 144 className="absolute inset-0 bg-black/60 rounded flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200" 145 aria-label="Play song" 146 > 147 <Play className="w-6 h-6 text-white fill-white" /> 148 </button> 149 </div> 150 81 151 <div className="flex-1 min-w-0"> 82 <h3 className="font-semibold text- lggroup-hover:text-blue-600 transition-colors truncate">152 <h3 className="font-semibold text-base text-gray-900 group-hover:text-blue-600 transition-colors truncate"> 83 153 {song.title} 84 154 </h3> 85 155 <div className="flex items-center gap-2 mt-1"> 86 {song.genre} 87 </div> 88 </div> 156 <span className="text-sm text-gray-600">{song.genre}</span> 157 </div> 158 </div> 159 89 160 <span 90 className={`px- 4 py-2 rounded-full text-white text-sm font-medium ${getRoleColor(song.role)} shadow-md`}161 className={`px-3 py-1 rounded-full text-sm font-medium ${getRoleColor(song.role)}`} 91 162 > 92 163 {song.role.replace("_", " ")} 93 164 </span> 165 166 <div className="flex items-center gap-2"> 167 <button 168 className="p-2 hover:bg-gray-200 rounded-full transition-colors duration-200 cursor-pointer" 169 title="Add to playlist" 170 > 171 <Plus className="w-5 h-5 text-gray-600" /> 172 </button> 173 174 <button 175 className="p-2 hover:bg-gray-200 rounded-full transition-colors duration-200 cursor-pointer" 176 title={song.isLikedByCurrentUser ? "Unlike" : "Like"} 177 onClick={(e) => { 178 e.stopPropagation(); 179 handleLike(song.id, song.title); 180 }} 181 > 182 <svg 183 className="w-5 h-5" 184 fill={song.isLikedByCurrentUser ? "#ef4444" : "none"} 185 stroke={song.isLikedByCurrentUser ? "#ef4444" : "#6b7280"} 186 strokeWidth="2" 187 viewBox="0 0 24 24" 188 > 189 <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" /> 190 </svg> 191 </button> 192 </div> 94 193 </div> 95 194 ))} … … 100 199 {contributions.length === 0 && ( 101 200 <div className="flex flex-col items-center justify-center py-16 text-gray-400"> 102 <Music className="w-2 4 h-24mb-4 opacity-20" />103 <p className="text- xlfont-medium">No contributions yet</p>201 <Music className="w-20 h-20 mb-4 opacity-20" /> 202 <p className="text-lg font-medium">No contributions yet</p> 104 203 <p className="text-sm mt-2">Start creating music to see it here</p> 105 204 </div> -
frontend/src/components/userProfile/ListenerView.tsx
r2730163 rd2af1a6 1 1 import { useNavigate } from "react-router-dom"; 2 import { Heart, ListMusic, Music, Album } from "lucide-react";2 import { Heart, ListMusic, Music, Album, Bookmark } from "lucide-react"; 3 3 import type { Playlist, MusicalEntity } from "../../utils/types"; 4 4 import axiosInstance from "../../api/axiosInstance"; 5 import { useState } from "react"; 5 6 interface ListenerViewProps { 6 7 likedEntities: MusicalEntity[] | []; … … 10 11 const ListenerView = ({ likedEntities, playlists }: ListenerViewProps) => { 11 12 const navigate = useNavigate(); 12 13 const likedSongs = likedEntities?.filter((e) => e.type === "SONG"); 14 const likedAlbums = likedEntities?.filter((e) => e.type === "ALBUM"); 13 const [items, setItems] = useState(likedEntities); 14 const [toast, setToast] = useState<{ message: string; show: boolean }>({ 15 message: "", 16 show: false, 17 }); 18 19 const showToast = (message: string) => { 20 setToast({ message, show: true }); 21 setTimeout(() => { 22 setToast({ message: "", show: false }); 23 }, 2000); 24 }; 25 26 const likedSongs = items.filter((e) => e.type === "SONG"); 27 const likedAlbums = items.filter((e) => e.type === "ALBUM"); 28 29 const handleSavePlaylist = (e: React.MouseEvent, playlistId: number) => { 30 e.stopPropagation(); 31 32 console.log("Save playlist:", playlistId); 33 }; 34 35 const handleLike = async (id: number, title: string) => { 36 try { 37 const response = await axiosInstance.post(`/musical-entity/${id}/like`); 38 const data = response.data; 39 40 setItems((prevItems) => 41 prevItems.map((item) => 42 item.id === data.entityId 43 ? { ...item, isLikedByCurrentUser: data.isLiked } 44 : item, 45 ), 46 ); 47 48 showToast( 49 data.isLiked ? `Liked "${title}"` : `Removed "${title}" from likes`, 50 ); 51 } catch (err: any) { 52 showToast(err.response?.data?.error); 53 } 54 }; 15 55 16 56 return ( 17 57 <div className="mt-8 space-y-12"> 58 {toast.show && ( 59 <div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 animate-fade-in-up"> 60 <div className="bg-gray-900 text-white px-6 py-3 rounded-full shadow-lg text-sm font-medium"> 61 {toast.message} 62 </div> 63 </div> 64 )} 18 65 {playlists && playlists.length > 0 && ( 19 66 <section> 20 <div className="flex items-center gap- 2 mb-6 pb-2 border-b-2 border-gray-200">67 <div className="flex items-center gap-3 mb-6"> 21 68 <ListMusic className="w-6 h-6 text-gray-700" /> 22 69 <h3 className="text-2xl font-bold text-gray-800"> 23 70 Created Playlists 24 71 </h3> 25 <span className="text-sm text-gray-400 ml-1"> 26 ({playlists.length}) 27 </span> 28 </div> 29 30 <div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4"> 72 <span className="text-sm text-gray-500">({playlists.length})</span> 73 </div> 74 75 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> 31 76 {playlists.map((playlist) => ( 32 77 <div … … 35 80 onClick={() => navigate(`/collection/playlist/${playlist.id}`)} 36 81 > 37 <div className=" aspect-square rounded-md overflow-hidden bg-gray-100 mb-2 relative shadow-smgroup-hover:shadow-lg transition-all">82 <div className="relative aspect-square rounded-lg overflow-hidden bg-gray-100 mb-3 shadow-md group-hover:shadow-lg transition-all"> 38 83 {playlist.cover ? ( 39 84 <img 40 85 src={playlist.cover} 41 86 alt={playlist.name} 42 className="w-full h-full object-cover group-hover:opacity-90 transition-opacity"87 className="w-full h-full object-cover" 43 88 /> 44 89 ) : ( 45 <div className="w-full h-full bg-gradient-to-br from- gray-200 to-gray-300 flex items-center justify-center">46 <ListMusic className="w-1 0 h-10 text-gray-400" />90 <div className="w-full h-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center"> 91 <ListMusic className="w-16 h-16 text-white opacity-30" /> 47 92 </div> 48 93 )} 49 </div> 50 <p className="text-sm font-medium text-gray-800 truncate group-hover:text-blue-600 transition-colors"> 94 95 {/* Save button overlay */} 96 <button 97 onClick={(e) => handleSavePlaylist(e, playlist.id)} 98 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" 99 title={playlist.isSavedByCurrentUser ? "Unsave" : "Save"} 100 > 101 <Bookmark 102 className={`w-5 h-5 ${ 103 playlist.isSavedByCurrentUser 104 ? "fill-blue-600 text-blue-600" 105 : "text-gray-600" 106 }`} 107 /> 108 </button> 109 </div> 110 <p className="text-sm font-semibold text-gray-900 truncate group-hover:text-blue-600 transition-colors"> 51 111 {playlist.name} 112 </p> 113 <p className="text-xs text-gray-500 truncate"> 114 {playlist.creatorName} 52 115 </p> 53 116 </div> … … 59 122 {likedSongs && likedSongs.length > 0 && ( 60 123 <section> 61 <div className="flex items-center gap- 2 mb-6 pb-2 border-b-2 border-gray-200">124 <div className="flex items-center gap-3 mb-6"> 62 125 <Heart className="w-6 h-6 text-red-500 fill-red-500" /> 63 126 <h3 className="text-2xl font-bold text-gray-800">Liked Songs</h3> 64 <span className="text-sm text-gray- 400 ml-1">127 <span className="text-sm text-gray-500"> 65 128 ({likedSongs?.length}) 66 129 </span> 67 130 </div> 68 131 69 <div className="space-y- 1">132 <div className="space-y-2"> 70 133 {likedSongs.map((song, index) => ( 71 134 <div 72 135 key={song.id} 73 className="flex items-center gap-4 p-3 rounded hover:bg-gray-50 cursor-pointer group transition-colors"136 className="flex items-center gap-4 p-3 rounded-lg hover:bg-gray-50 cursor-pointer group transition-colors" 74 137 onClick={() => navigate(`/musical-entity/${song.id}`)} 75 138 > 76 <span className="text- sm text-gray-400 w-8 text-right font-medium">139 <span className="text-gray-500 font-medium w-8 text-center"> 77 140 {index + 1} 78 141 </span> 79 <div className="w-12 h-12 rounded bg-gradient-to-br from- red-100 to-pink-100 flex items-center justify-center flex-shrink-0">80 <Music className="w-6 h-6 text- red-600" />142 <div className="w-12 h-12 rounded bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center flex-shrink-0 shadow-sm"> 143 <Music className="w-6 h-6 text-white" /> 81 144 </div> 82 145 <div className="flex-1 min-w-0"> 83 <p className="font- medium text-gray-800 truncate group-hover:text-blue-600 transition-colors">146 <p className="font-semibold text-gray-900 truncate group-hover:text-blue-600 transition-colors"> 84 147 {song.title} 85 148 </p> 86 <p className="text-xs text-gray-500">{song.genre}</p> 87 </div> 149 <p className="text-sm text-gray-600 truncate"> 150 {song.releasedBy} 151 </p> 152 </div> 153 <span className="text-sm text-gray-600 px-3 py-1 bg-gray-100 rounded-full"> 154 {song.genre} 155 </span> 156 157 <button 158 onClick={(e) => { 159 e.stopPropagation(); 160 handleLike(song.id, song.title); 161 }} 162 className="p-2 hover:bg-gray-200 rounded-full transition-colors duration-200 cursor-pointer" 163 title={song.isLikedByCurrentUser ? "Unlike" : "Like"} 164 > 165 <svg 166 className="w-5 h-5" 167 fill={song.isLikedByCurrentUser ? "#ef4444" : "none"} 168 stroke={song.isLikedByCurrentUser ? "#ef4444" : "#6b7280"} 169 strokeWidth="2" 170 viewBox="0 0 24 24" 171 > 172 <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" /> 173 </svg> 174 </button> 88 175 </div> 89 176 ))} … … 94 181 {likedAlbums && likedAlbums.length > 0 && ( 95 182 <section> 96 <div className="flex items-center gap- 2 mb-6 pb-2 border-b-2 border-gray-200">97 <Album className="w-6 h-6 text- blue-600" />183 <div className="flex items-center gap-3 mb-6"> 184 <Album className="w-6 h-6 text-gray-700" /> 98 185 <h3 className="text-2xl font-bold text-gray-800">Liked Albums</h3> 99 <span className="text-sm text-gray- 400 ml-1">186 <span className="text-sm text-gray-500"> 100 187 ({likedAlbums.length}) 101 188 </span> 102 189 </div> 103 190 104 <div className="grid grid-cols- 3 md:grid-cols-4 lg:grid-cols-5 gap-5">191 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> 105 192 {likedAlbums.map((album) => ( 106 193 <div … … 109 196 onClick={() => navigate(`/collection/album/${album.id}`)} 110 197 > 111 <div className="aspect-square rounded-lg overflow-hidden bg-gradient-to-br from-blue-100 to-indigo-100 mb-3 flex items-center justify-center shadow-sm group-hover:shadow-md transition-all"> 112 <Album className="w-16 h-16 text-blue-600 opacity-60" /> 113 </div> 114 <p className="font-medium text-sm text-gray-800 truncate group-hover:text-blue-600 transition-colors"> 198 <div className="relative aspect-square rounded-lg overflow-hidden bg-gradient-to-br from-blue-400 to-purple-500 mb-3 flex items-center justify-center shadow-md group-hover:shadow-lg transition-all"> 199 <Album className="w-16 h-16 text-white opacity-30" /> 200 201 <button 202 onClick={(e) => { 203 e.stopPropagation(); 204 handleLike(album.id, album.title); 205 }} 206 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 cursor-pointer" 207 title={album.isLikedByCurrentUser ? "Unlike" : "Like"} 208 > 209 <svg 210 className="w-5 h-5" 211 fill={album.isLikedByCurrentUser ? "#ef4444" : "none"} 212 stroke={ 213 album.isLikedByCurrentUser ? "#ef4444" : "#6b7280" 214 } 215 strokeWidth="2" 216 viewBox="0 0 24 24" 217 > 218 <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" /> 219 </svg> 220 </button> 221 </div> 222 <p className="font-semibold text-sm text-gray-900 truncate group-hover:text-blue-600 transition-colors"> 115 223 {album.title} 116 224 </p> 117 <p className="text-xs text-gray-500 mt-0.5">{album.genre}</p> 225 <p className="text-xs text-gray-600 truncate mt-1"> 226 {album.releasedBy} 227 </p> 118 228 </div> 119 229 ))} … … 125 235 likedEntities.length === 0 && 126 236 (!playlists || playlists.length === 0) && ( 127 <div className="flex flex-col items-center justify-center py-16 text-gray-300"> 128 <p className="text-lg font-medium text-gray-400"> 129 Nothing here yet 130 </p> 131 <p className="text-sm text-gray-400 mt-1"> 237 <div className="flex flex-col items-center justify-center py-16 text-gray-400"> 238 <Music className="w-20 h-20 mb-4 opacity-20" /> 239 <p className="text-lg font-medium">Nothing here yet</p> 240 <p className="text-sm mt-2"> 132 241 Start exploring music to build your collection 133 242 </p> -
frontend/src/pages/UserDetail.tsx
r2730163 rd2af1a6 13 13 } from "../utils/types"; 14 14 15 interface FollowStatus { 16 isFollowing: boolean; 17 followerCount: number; 18 followingCount: number; 19 } 20 15 21 interface Artist extends BaseNonAdminUser { 16 22 userType: "ARTIST"; … … 43 49 setIsFollowing(true); 44 50 try { 45 const response = await axiosInstance.post< UserProfile>(51 const response = await axiosInstance.post<FollowStatus>( 46 52 `/users/${userId}/follow`, 47 53 ); 48 setUser(response.data); 54 setUser((prev) => { 55 if (!prev) return null; 56 return { 57 ...prev, 58 isFollowedByCurrentUser: response.data.isFollowing, 59 followers: response.data.followerCount, 60 following: response.data.followingCount, 61 }; 62 }); 49 63 } catch (err: any) { 50 64 setError(handleError(err)); 51 65 } finally { 52 66 setIsFollowing(false); 67 } 68 }; 69 70 const handleFollowInModal = async (targetId: number) => { 71 try { 72 const response = await axiosInstance.post<FollowStatus>( 73 `/users/${targetId}/follow`, 74 ); 75 76 setModalUsers((prevUsers) => 77 prevUsers.map((u) => 78 u.id === targetId 79 ? { ...u, isFollowedByCurrentUser: response.data.isFollowing } 80 : u, 81 ), 82 ); 83 84 // if (user && user.id === targetId) { 85 // setUser((prev) => { 86 // if (!prev) return null; 87 // return { 88 // ...prev, 89 // isFollowedByCurrentUser: response.data.isFollowing, 90 // followers: response.data.followerCount, 91 // }; 92 // }); 93 // } 94 } catch (err: any) { 95 setError(handleError(err)); 53 96 } 54 97 }; … … 81 124 }; 82 125 83 const handleFollowInModal = async (targetId: number) => {84 try {85 await axiosInstance.post(`/users/${targetId}/follow`);86 setModalUsers((prevUsers) =>87 prevUsers.map((u) => {88 if (u.id === targetId) {89 const isNowFollowing = !u.isFollowedByCurrentUser;90 return {91 ...u,92 isFollowedByCurrentUser: isNowFollowing,93 };94 }95 return u;96 }),97 );98 99 // if (user && user.id === targetId) {100 // const response = await axiosInstance.get(`/users/${targetId}`);101 // setUser(response.data);102 // }103 } catch (err: any) {104 setError(handleError(err));105 }106 };107 108 126 useEffect(() => { 109 127 const fetchUser = async () => { … … 111 129 try { 112 130 const response = await axiosInstance.get(`/users/${userId}`); 131 console.log(response.data); 113 132 setUser(response.data); 114 133 } catch (err: any) { -
frontend/src/utils/types.ts
r2730163 rd2af1a6 8 8 9 9 export interface ArtistContribution { 10 musicalEntityId: number;10 id: number; 11 11 title: string; 12 12 genre: string; … … 40 40 creatorName: string; 41 41 songsInPlaylist: Song[]; 42 isSavedByCurrentUser: boolean; 42 43 } 43 44
Note:
See TracChangeset
for help on using the changeset viewer.
