Changeset 2b08bed


Ignore:
Timestamp:
01/30/26 22:14:36 (5 months ago)
Author:
Dimitar Arsov <dimitararsov04@…>
Branches:
main
Children:
2730163
Parents:
98992cf
Message:

show followers, following and songs in album

Files:
5 added
14 edited

Legend:

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

    r98992cf r2b08bed  
    3535    }
    3636
     37    @GetMapping("/followers/{id}")
     38    public HttpEntity<List<NonAdminUserDto>>getFollowersForUser(@PathVariable Long id){
     39        return ResponseEntity.ok(followService.getFollowersForUser(id));
     40    }
     41
     42    @GetMapping("/following/{id}")
     43    public HttpEntity<List<NonAdminUserDto>>getFollowingForUser(@PathVariable Long id){
     44        return ResponseEntity.ok(followService.getFollowingForUser(id));
     45    }
     46
    3747    @PostMapping("/follow/{id}")
    3848    public HttpEntity<NonAdminUserDto>followUser(@PathVariable Long id){
     
    4151        return ResponseEntity.ok(nonAdminUserService.getById(id));
    4252    }
     53
     54
    4355}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/dto/MusicalEntityDto.java

    r98992cf r2b08bed  
    1313    private String genre;
    1414    private String type;
     15    private String releasedBy;
     16    private Boolean isLikedByCurrentUser;
    1517}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/dto/NonAdminUserDto.java

    r98992cf r2b08bed  
    1212    private String username;
    1313    private String fullName;
     14    private String profilePhoto;
    1415    private String userType;
    1516    private Long followers;
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/FollowRepository.java

    r98992cf r2b08bed  
    88import com.ukim.finki.develop.finkwave.model.Follow;
    99import com.ukim.finki.develop.finkwave.model.FollowId;
     10
     11import java.util.List;
    1012
    1113@Repository
     
    2325    boolean isFollowing(@Param("currentUserId") Long currentUSerId,@Param("selectedUserId")Long selectedUserId);
    2426
     27    List<Follow>getFollowsByFollowee_Id(Long id);
     28
     29    List<Follow>getFollowsByFollower_Id(Long id);
     30
    2531}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/LikeRepository.java

    r98992cf r2b08bed  
    11package com.ukim.finki.develop.finkwave.repository;
    22
     3import com.ukim.finki.develop.finkwave.model.MusicalEntity;
    34import com.ukim.finki.develop.finkwave.model.dto.MusicalEntityDto;
    45import org.springframework.data.jpa.repository.JpaRepository;
     
    1920           "CASE WHEN s.id IS NOT NULL THEN 'SONG' " +
    2021           "WHEN a.id IS NOT NULL THEN 'ALBUM' " +
    21            "ELSE 'UNKNOWN' END)" +
     22           "ELSE 'UNKNOWN' END," +
     23            "u.fullName," +
     24            "true)" +
    2225           "FROM Like l " +
    2326           "LEFT JOIN Song s ON s.musicalEntities.id = l.musicalEntity.id " +
    2427           "LEFT JOIN Album a ON a.musicalEntities.id = l.musicalEntity.id " +
     28            "LEFT JOIN User u ON u.id=l.musicalEntity.releasedBy.id "+
    2529           "WHERE l.listener.id = :listenerId")
    2630    List<MusicalEntityDto> findLikedEntitiesWithTypeByListenerId(@Param("listenerId") Long listenerId);
     31
     32
     33    @Query("SELECT CASE WHEN COUNT (l)>0 THEN true ELSE false END " +
     34            "FROM MusicalEntity  me " +
     35            "JOIN Like  l on l.musicalEntity.id=me.id " +
     36            "WHERE l.listener.id=:userId"
     37
     38    )
     39    boolean isLikedByUser(@Param("userId") Long userId);
    2740}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/SongRepository.java

    r98992cf r2b08bed  
    11package com.ukim.finki.develop.finkwave.repository;
    22
     3import com.ukim.finki.develop.finkwave.model.dto.MusicalEntityDto;
    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
     
    1114public interface SongRepository extends JpaRepository<Song, Long> {
    1215
    13     List<Song>findAllByIdIn(List<Long>ids);
     16
     17    @Query("SELECT NEW com.ukim.finki.develop.finkwave.model.dto.MusicalEntityDto( "+
     18            "s.id, " +
     19            "me.title, " +
     20            "me.genre, " +
     21            "'SONG'," +
     22            "u.fullName, " +
     23            "(CASE WHEN l.id IS NOT NULL THEN true ELSE false END)) "+
     24            "FROM Song s "+
     25            "JOIN MusicalEntity  me on me.id=s.musicalEntities.id "+
     26            "JOIN User u on u.id=me.releasedBy.id "+
     27            "LEFT JOIN Like  l on l.musicalEntity.id=s.id AND l.listener.id=:currentUserId "+
     28            "WHERE s.album.id=:albumId"
     29
     30    )
     31    List<MusicalEntityDto>findSongsByAlbum(@Param("albumId") Long albumId, @Param("currentUserId")Long currentUserId);
    1432}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/FollowService.java

    r98992cf r2b08bed  
    66import com.ukim.finki.develop.finkwave.model.FollowId;
    77import com.ukim.finki.develop.finkwave.model.NonAdminUser;
     8import com.ukim.finki.develop.finkwave.model.dto.NonAdminUserDto;
     9import com.ukim.finki.develop.finkwave.repository.ArtistRepository;
    810import com.ukim.finki.develop.finkwave.repository.FollowRepository;
    911import com.ukim.finki.develop.finkwave.repository.NonAdminUserRepository;
     12import com.ukim.finki.develop.finkwave.service.mappers.NonAdminUserMapper;
    1013import lombok.AllArgsConstructor;
    1114import org.springframework.stereotype.Service;
     15
     16import java.util.ArrayList;
     17import java.util.List;
     18import java.util.stream.Collectors;
    1219
    1320@Service
     
    1825    private final NonAdminUserRepository nonAdminUserRepository;
    1926    private final AuthService authService;
     27    private final ArtistRepository artistRepository;
     28    private final NonAdminUserMapper mapper;
     29
     30
     31
     32    public List<NonAdminUserDto>getFollowersForUser(Long id){
     33        if (artistRepository.existsById(id)){
     34            throw new FollowException("Cannot view for artist");
     35        }
     36        List<Follow>followers=followRepository.getFollowsByFollowee_Id(id);
     37        Long currentUserId=authService.getCurrentUserID();
     38        return followers.stream()
     39                .map(f->{
     40                   NonAdminUserDto dto=mapper.toDto(f.getFollower(),null,null,null);
     41                   dto.setIsFollowedByCurrentUser(followRepository.isFollowing(currentUserId,f.getFollower().getId()));
     42                   return dto;
     43                })
     44                .toList();
     45    }
     46
     47    public List<NonAdminUserDto>getFollowingForUser(Long id){
     48        if (artistRepository.existsById(id)){
     49            throw new FollowException("Cannot view for artist");
     50        }
     51        List<Follow>followers=followRepository.getFollowsByFollower_Id(id);
     52        Long currentUserId=authService.getCurrentUserID();
     53        return followers.stream()
     54                .map(f->{
     55                    NonAdminUserDto dto=mapper.toDto(f.getFollowee(),null,null,null);
     56                    dto.setIsFollowedByCurrentUser(followRepository.isFollowing(currentUserId,f.getFollowee().getId()));
     57                    return dto;
     58                })
     59                .toList();
     60    }
     61
    2062
    2163
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/NonAdminUserService.java

    r98992cf r2b08bed  
    2929                .map(user -> {
    3030                    String type = determineType(user.getId());
    31                     Long followers = followRepository.countByFolloweeId(user.getId());
    32                     Long following = followRepository.countByFollowerId(user.getId());
    33                     return mapper.toDto(user, type, followers, following);
     31                    return mapper.toDto(user, type, null, null);
    3432                })
    3533                .collect(Collectors.toList());
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/mappers/NonAdminUserMapper.java

    r98992cf r2b08bed  
    3636    }
    3737
     38
     39
    3840    private void populateBaseClassFields(NonAdminUserDto dto,NonAdminUser user,String type,Long followers,Long following){
     41
    3942        dto.setId(user.getId());
    4043        dto.setUsername(user.getUser().getUsername());
    4144        dto.setFullName(user.getUser().getFullName());
     45        dto.setProfilePhoto(user.getUser().getProfilePhoto());
    4246        dto.setUserType(type);
    4347        dto.setFollowers(followers);
  • frontend/src/App.tsx

    r98992cf r2b08bed  
    88import Register from "./pages/Register";
    99import UserDetailView from "./pages/UserDetailView";
     10import MusicalCollection from "./pages/MusicalCollection";
    1011
    1112const Layout = () => {
    12         return (
    13                 <div className="flex flex-col min-h-screen">
    14                         <Nav />
    15                         <ToastContainer
    16                                 position="top-right"
    17                                 autoClose={2000}
    18                                 hideProgressBar={false}
    19                                 newestOnTop={false}
    20                                 closeOnClick
    21                                 rtl={false}
    22                                 pauseOnFocusLoss
    23                                 draggable
    24                                 pauseOnHover
    25                                 theme="light"
    26                         />
    27                         <main className="grow">
    28                                 <Outlet />
    29                         </main>
    30                 </div>
    31         );
     13  return (
     14    <div className="flex flex-col min-h-screen">
     15      <Nav />
     16      <ToastContainer
     17        position="top-right"
     18        autoClose={2000}
     19        hideProgressBar={false}
     20        newestOnTop={false}
     21        closeOnClick
     22        rtl={false}
     23        pauseOnFocusLoss
     24        draggable
     25        pauseOnHover
     26        theme="light"
     27      />
     28      <main className="grow">
     29        <Outlet />
     30      </main>
     31    </div>
     32  );
    3233};
    3334
    3435const router = createBrowserRouter([
    35         {
    36                 path: "",
    37                 element: <Layout />,
    38                 children: [
    39                         {
    40                                 path: "/",
    41                                 element: <LandingPage />,
    42                         },
    43                         {
    44                                 path: "/login",
    45                                 element: <Login />,
    46                         },
    47                         {
    48                                 path: "/users",
    49                                 element: <AllUsers />,
    50                         },
    51                         {
    52                                 path: "/users/:userId",
    53                                 element: <UserDetailView />,
    54                         },
    55                         {
    56                                 path: "/register",
    57                                 element: <Register />,
    58                         },
    59                 ],
    60         },
     36  {
     37    path: "",
     38    element: <Layout />,
     39    children: [
     40      {
     41        path: "/",
     42        element: <LandingPage />,
     43      },
     44      {
     45        path: "/login",
     46        element: <Login />,
     47      },
     48      {
     49        path: "/users",
     50        element: <AllUsers />,
     51      },
     52      {
     53        path: "/users/:userId",
     54        element: <UserDetailView />,
     55      },
     56      {
     57        path: "/register",
     58        element: <Register />,
     59      },
     60      {
     61        path: "/collection/:type/:id",
     62        element: <MusicalCollection />,
     63      },
     64    ],
     65  },
    6166]);
    6267
    6368function App() {
    64         return <RouterProvider router={router} />;
     69  return <RouterProvider router={router} />;
    6570}
    6671
  • frontend/src/components/userProfile/ArtistView.tsx

    r98992cf r2b08bed  
    3737                className="group cursor-pointer"
    3838                onClick={() =>
    39                   navigate(`/musical-entity/${album.musicalEntityId}`)
     39                  navigate(`/collection/album/${album.musicalEntityId}`)
    4040                }
    4141              >
  • frontend/src/components/userProfile/ListenerView.tsx

    r98992cf r2b08bed  
    3131                key={playlist.id}
    3232                className="group cursor-pointer"
    33                 onClick={() => navigate(`/playlist/${playlist.id}`)}
     33                onClick={() => navigate(`/playlists/${playlist.id}`)}
    3434              >
    3535                <div className="aspect-square rounded-md overflow-hidden bg-gray-100 mb-2 relative shadow-sm group-hover:shadow-lg transition-all">
  • frontend/src/pages/UserDetailView.tsx

    r98992cf r2b08bed  
    44import ArtistView from "../components/userProfile/ArtistView";
    55import ListenerView from "../components/userProfile/ListenerView";
     6import UserListModal from "../components/userProfile/UserListModal";
    67import type {
    78  MusicalEntity,
    89  Playlist,
    910  ArtistContribution,
     11  BaseNonAdminUser,
    1012} from "../utils/types";
    1113
    12 interface BaseUser {
    13   id: number;
    14   fullName: string;
    15   userType: string;
    16   followers: number;
    17   following: number;
    18   isFollowedByCurrentUser: boolean;
    19 }
    20 
    21 interface Artist extends BaseUser {
     14interface Artist extends BaseNonAdminUser {
    2215  userType: "ARTIST";
    2316  contributions: ArtistContribution[];
    2417}
    25 interface Listener extends BaseUser {
     18interface Listener extends BaseNonAdminUser {
    2619  userType: "LISTENER";
    2720  likedEntities: MusicalEntity[];
     
    3326const UserDetail = () => {
    3427  // user refers to the selected user NOT to the user from context
     28  const baseURL = import.meta.env.VITE_API_BASE_URL;
    3529  const { userId } = useParams();
    3630  const navigate = useNavigate();
    3731  const [user, setUser] = useState<UserProfile | null>(null);
    3832  const [error, setError] = useState<string | null>(null);
     33  const [showModal, setShowModal] = useState(false);
     34  const [modalTitle, setModalTitle] = useState("");
     35  const [modalUsers, setModalUsers] = useState<any[]>([]);
     36  const [isLoadingModal, setIsLoadingModal] = useState(false);
    3937  const [isFollowing, setIsFollowing] = useState(false);
    4038
     
    4745        `/users/follow/${userId}`,
    4846      );
     47      setUser(response.data);
    4948    } catch (err: any) {
    5049      console.error(err.response?.data?.error);
    5150    } finally {
    5251      setIsFollowing(false);
     52    }
     53  };
     54
     55  const displayFollowers = async () => {
     56    setIsLoadingModal(true);
     57    try {
     58      const response = await axiosInstance.get(`/users/followers/${userId}`);
     59      setModalUsers(response.data);
     60      setModalTitle("Followers");
     61      setShowModal(true);
     62    } catch (err) {
     63      console.error("Failed to fetch followers");
     64    } finally {
     65      setIsLoadingModal(false);
     66    }
     67  };
     68  const displayFollowing = async () => {
     69    setIsLoadingModal(true);
     70    try {
     71      const response = await axiosInstance.get(`/users/following/${userId}`);
     72      setModalUsers(response.data);
     73      setModalTitle("Following");
     74      setShowModal(true);
     75    } catch (err) {
     76      console.error("Failed to fetch following users");
     77    } finally {
     78      setIsLoadingModal(false);
     79    }
     80  };
     81
     82  const handleFollowInModal = async (targetId: number) => {
     83    try {
     84      await axiosInstance.post(`/users/follow/${targetId}`);
     85      setModalUsers((prevUsers) =>
     86        prevUsers.map((u) => {
     87          if (u.id === targetId) {
     88            const isNowFollowing = !u.isFollowedByCurrentUser;
     89            return {
     90              ...u,
     91              isFollowedByCurrentUser: isNowFollowing,
     92            };
     93          }
     94          return u;
     95        }),
     96      );
     97
     98      if (user && user.id === targetId) {
     99        const response = await axiosInstance.get(`/users/${targetId}`);
     100        setUser(response.data);
     101      }
     102    } catch (err) {
     103      console.error("Failed to toggle follow in modal", err);
    53104    }
    54105  };
     
    60111        const response = await axiosInstance.get(`/users/${userId}`);
    61112        console.log(response.data);
    62 
    63113        setUser(response.data);
    64114      } catch (err: any) {
     
    84134  return (
    85135    <div className="container mx-auto p-6">
     136      {isLoadingModal && (
     137        <div className="fixed inset-0 z-40 bg-black/30 backdrop-blur-sm flex items-center justify-center">
     138          <div className="flex items-center gap-3">
     139            <div className="w-6 h-6 border-3 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
     140          </div>
     141        </div>
     142      )}
    86143      <button
    87144        onClick={() => navigate(-1)}
     
    94151        <div className="flex items-start gap-6 mb-8">
    95152          <div className="shrink-0">
    96             <div className="w-32 h-32 rounded-full bg-linear-to-br from-blue-400 to-purple-500 flex items-center justify-center text-white text-4xl font-bold shadow-lg">
    97               {user.fullName.charAt(0).toUpperCase()}
     153            <div className="w-32 h-32 rounded-full bg-linear-to-br from-blue-400 to-purple-500 flex items-center justify-center text-white text-4xl font-bold shadow-lg overflow-hidden">
     154              {user.profilePhoto ? (
     155                <img
     156                  src={`${baseURL}/${user.profilePhoto}`}
     157                  alt={user.fullName}
     158                  className="w-full h-full object-cover"
     159                />
     160              ) : (
     161                user.fullName.charAt(0).toUpperCase()
     162              )}
    98163            </div>
    99164          </div>
     
    106171
    107172            <div className="flex gap-6 mb-4 text-gray-700">
    108               <div className="flex flex-col">
     173              <div
     174                className={`flex flex-col ${user.userType == "LISTENER" ? "cursor-pointer" : "cursor-default"}`}
     175                onClick={
     176                  user.userType === "LISTENER" ? displayFollowers : undefined
     177                }
     178              >
    109179                <span className="text-2xl font-bold">{user.followers}</span>
    110180                <span className="text-sm text-gray-500">Followers</span>
    111181              </div>
    112               <div className="flex flex-col">
     182              <div
     183                className={`flex flex-col ${user.userType == "LISTENER" ? "cursor-pointer" : "cursor-default"}`}
     184                onClick={
     185                  user.userType === "LISTENER" ? displayFollowing : undefined
     186                }
     187              >
    113188                <span className="text-2xl font-bold">{user.following}</span>
    114189                <span className="text-sm text-gray-500">Following</span>
     
    144219          />
    145220        )}
     221
     222        {showModal && (
     223          <UserListModal
     224            title={modalTitle}
     225            users={modalUsers}
     226            onClose={() => setShowModal(false)}
     227            onFollowToggle={handleFollowInModal}
     228          />
     229        )}
    146230      </div>
    147231    </div>
  • frontend/src/utils/types.ts

    r98992cf r2b08bed  
    1919  genre: string;
    2020  type: string;
     21  releasedBy: string;
    2122}
    2223
     
    2728  creatorName: string;
    2829}
     30
     31export interface BaseNonAdminUser {
     32  id: number;
     33  fullName: string;
     34  userType: string;
     35  profilePhoto: string;
     36  followers: number;
     37  following: number;
     38  isFollowedByCurrentUser: boolean;
     39}
Note: See TracChangeset for help on using the changeset viewer.