Changeset cb4a1da


Ignore:
Timestamp:
02/05/26 20:52:11 (5 months ago)
Author:
Filip Gavrilovski <filipgavrilovski28@…>
Branches:
main
Children:
2ce7c1e
Parents:
694fc25
Message:

add working server side search to landing page; change some endpoints

Files:
5 added
16 edited

Legend:

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

    r694fc25 rcb4a1da  
    66import org.springframework.http.HttpEntity;
    77import org.springframework.http.ResponseEntity;
    8 import org.springframework.web.bind.annotation.GetMapping;
    9 import org.springframework.web.bind.annotation.PathVariable;
    10 import org.springframework.web.bind.annotation.RequestMapping;
    11 import org.springframework.web.bind.annotation.RestController;
     8import org.springframework.web.bind.annotation.*;
     9
     10import java.util.List;
    1211
    1312@RestController
     
    1514@RequestMapping("/albums")
    1615public class AlbumController {
    17 
    1816    private final AlbumService albumService;
    1917
     
    2321    }
    2422
     23    @GetMapping("/search")
     24    public HttpEntity<List<MusicalEntityDto>> searchAlbums(@RequestParam(name = "q") String searchTerm){
     25        return ResponseEntity.ok(albumService.searchAlbums(searchTerm));
     26    }
     27
    2528}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/NonAdminUserController.java

    r694fc25 rcb4a1da  
    1515@CrossOrigin("*")
    1616@RequiredArgsConstructor
    17 @RequestMapping("/users")
     17@RequestMapping("/users/na/")
    1818public class NonAdminUserController {
    1919
     
    2727
    2828    @GetMapping("/{username}")
    29     public HttpEntity<NonAdminUserDto>getById(@PathVariable String username){
     29    public HttpEntity<NonAdminUserDto>getNonAdminUserById(@PathVariable String username){
    3030        return ResponseEntity.ok(nonAdminUserService.getNonAdminUserProfile(username));
    3131    }
    3232
    3333    @GetMapping("/search")
    34     public HttpEntity<List<NonAdminUserDto>>searchUsers(@RequestParam String name){
     34    public HttpEntity<List<NonAdminUserDto>>searchNonAdminUsers(@RequestParam String name){
    3535        return ResponseEntity.ok(nonAdminUserService.searchUsers(name));
    3636    }
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/SongController.java

    r694fc25 rcb4a1da  
    1010import org.springframework.web.bind.annotation.GetMapping;
    1111import org.springframework.web.bind.annotation.RequestMapping;
     12import org.springframework.web.bind.annotation.RequestParam;
    1213import org.springframework.web.bind.annotation.RestController;
    1314
     
    1819@AllArgsConstructor
    1920public class SongController {
    20     private final SongRepository songRepository;
    2121    private final AuthService authService;
    2222    private final SongService songService;
    2323
    24     @GetMapping
     24    @GetMapping("/top")
    2525    private HttpEntity<List<MusicalEntityDto>> getSongs(){
    2626        Long userId = authService.getCurrentUserID();
    2727        return ResponseEntity.ok(songService.getTopSongs(userId));
    2828    }
     29
     30    @GetMapping("/search")
     31    private HttpEntity<List<MusicalEntityDto>> searchSongs(
     32            @RequestParam(name = "q") String searchTerm){
     33        return ResponseEntity.ok(songService.searchSongs(null, searchTerm));
     34    }
    2935}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/UserController.java

    r694fc25 rcb4a1da  
    22
    33import com.ukim.finki.develop.finkwave.model.User;
     4import com.ukim.finki.develop.finkwave.model.enums.UserType;
    45import com.ukim.finki.develop.finkwave.service.UserService;
    56import lombok.RequiredArgsConstructor;
    67import org.springframework.http.HttpEntity;
    78import org.springframework.http.ResponseEntity;
    8 import org.springframework.web.bind.annotation.CrossOrigin;
    9 import org.springframework.web.bind.annotation.GetMapping;
    10 import org.springframework.web.bind.annotation.RestController;
     9import org.springframework.web.bind.annotation.*;
    1110
    1211import java.util.List;
     
    1514@CrossOrigin("*")
    1615@RequiredArgsConstructor
     16@RequestMapping("/users")
    1717public class UserController {
    1818    private final UserService usersService;
     
    2323    }
    2424
    25 
     25    @GetMapping("/search")
     26    public HttpEntity<List<User>> searchArtists(
     27            @RequestParam(name = "type") UserType userType,
     28            @RequestParam(name = "q") String searchTerm){
     29        return ResponseEntity.ok(usersService.search(userType, searchTerm));
     30    }
    2631}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/AlbumRepository.java

    r694fc25 rcb4a1da  
    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
     
    1215
    1316    List<Album>findAllByIdIn(List<Long>ids);
     17
     18
     19    @Query("""
     20            SELECT NEW com.ukim.finki.develop.finkwave.model.dto.MusicalEntityDto(
     21            a.id, me.title, me.genre, 'ALBUM', u.fullName, me.cover, FALSE )
     22            FROM Album a
     23            JOIN a.musicalEntities me
     24            JOIN me.releasedBy rb
     25            JOIN rb.nonAdminUser nau
     26            JOIN nau.user u
     27            WHERE me.title ILIKE %:searchTerm%
     28            """
     29    )
     30    List<MusicalEntityDto> searchAlbums(@Param("currentUserId")Long currentUserId, @Param("searchTerm") String searchTerm);
     31
    1432}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/SongRepository.java

    r694fc25 rcb4a1da  
    1515
    1616
    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             "me.cover," +
    24             "(CASE WHEN :currentUserId IS NOT NULL AND l.id IS NOT NULL THEN true ELSE false END)) " +
    25             "FROM Song s " +
    26             "JOIN s.musicalEntities me " +
    27             "JOIN me.releasedBy a " +
    28             "JOIN a.nonAdminUser nau " +
    29             "JOIN nau.user u "+
    30             "LEFT JOIN Like l ON l.musicalEntity.id = s.id AND l.listener.id = :currentUserId " +
    31             "WHERE s.album.id = :albumId"
     17    @Query("""
     18            SELECT NEW com.ukim.finki.develop.finkwave.model.dto.MusicalEntityDto(
     19                s.id, me.title, me.genre, 'SONG', u.fullName, me.cover,
     20                (CASE WHEN :currentUserId IS NOT NULL AND l.id IS NOT NULL THEN true ELSE false END)
     21            )
     22            FROM Song s
     23            JOIN s.musicalEntities me
     24            JOIN me.releasedBy a
     25            JOIN a.nonAdminUser nau
     26            JOIN nau.user u
     27            LEFT JOIN Like l ON l.musicalEntity.id = s.id AND l.listener.id = :currentUserId
     28            WHERE s.album.id = :albumId
     29            """
    3230    )
    3331    List<MusicalEntityDto>findSongsByAlbum(@Param("albumId") Long albumId, @Param("currentUserId")Long currentUserId);
    3432
    3533
    36     @Query("SELECT NEW com.ukim.finki.develop.finkwave.model.dto.MusicalEntityDto(" +
    37             "s.id, " +
    38             "me.title, " +
    39             "me.genre, " +
    40             "'SONG', " +
    41             "u.fullName, " +
    42             "me.cover," +
    43             "(CASE WHEN :currentUserId IS NOT NULL AND l.id IS NOT NULL THEN true ELSE false END)) " +
    44             "FROM Song s "+
    45             "JOIN s.musicalEntities me "+
    46             "JOIN me.releasedBy a "+
    47             "JOIN a.nonAdminUser nau "+
    48             "JOIN nau.user u "+
    49             "JOIN PlaylistSong ps ON ps.song.id=s.id "+
    50             "LEFT JOIN Like l ON l.musicalEntity.id=s.id AND l.listener.id=:currentUserId "+
    51             "WHERE ps.playlist.id=:playlistId")
     34    @Query("""
     35            SELECT NEW com.ukim.finki.develop.finkwave.model.dto.MusicalEntityDto(
     36            s.id, me.title, me.genre, 'SONG', u.fullName, me.cover,
     37            (CASE WHEN :currentUserId IS NOT NULL AND l.id IS NOT NULL THEN true ELSE false END))
     38            FROM Song s
     39            JOIN s.musicalEntities me
     40            JOIN me.releasedBy a
     41            JOIN a.nonAdminUser nau
     42            JOIN nau.user u
     43            JOIN PlaylistSong ps ON ps.song.id=s.id
     44            LEFT JOIN Like l ON l.musicalEntity.id=s.id AND l.listener.id=:currentUserId
     45            WHERE ps.playlist.id=:playlistId
     46    """)
    5247    List<MusicalEntityDto>findSongsByPlaylistId(@Param("playlistId")Long playlistId, @Param("currentUserId")Long currentUserId);
    5348
    54     @Query("SELECT NEW com.ukim.finki.develop.finkwave.model.dto.MusicalEntityDto(" +
    55             "s.id, " +
    56             "me.title, " +
    57             "me.genre, " +
    58             "'SONG', " +
    59             "u.fullName, " +
    60             "me.cover," +
    61             "FALSE ) " +
    62 //            "(CASE WHEN :currentUserId IS NOT NULL AND l.id IS NOT NULL THEN true ELSE false END)) " +
    63 //            "COUNT(*) " +
    64             "FROM Song s "+
    65             "JOIN s.musicalEntities me " +
    66             "JOIN me.releasedBy a " +
    67             "JOIN a.nonAdminUser nau "+
    68             "JOIN nau.user u " +
    69             "JOIN Listen l on l.song.id = s.id " +
    70             "GROUP BY s.id, me.title, me.genre, u.fullName, me.cover " +
    71             "ORDER BY count(*) desc "
     49    @Query("""
     50            SELECT NEW com.ukim.finki.develop.finkwave.model.dto.MusicalEntityDto(
     51            s.id, me.title, me.genre, 'SONG', u.fullName, me.cover, FALSE)
     52            FROM Song s
     53            JOIN s.musicalEntities me
     54            JOIN me.releasedBy a
     55            JOIN a.nonAdminUser nau
     56            JOIN nau.user u
     57            JOIN Listen l on l.song.id = s.id
     58            GROUP BY s.id, me.title, me.genre, u.fullName, me.cover
     59            ORDER BY count(*) desc
     60    """
    7261    )
    7362    // todo: handle likes, momentalno site se false za da raboti joinot
    7463    // todo: add paging
    7564    List<MusicalEntityDto> findTopByListens(@Param("currentUserId")Long currentUserId);
     65
     66
     67    // todo: fix is liked by user, currently returns hard coded false
     68    @Query("""
     69            SELECT NEW com.ukim.finki.develop.finkwave.model.dto.MusicalEntityDto(
     70            s.id, me.title, me.genre, 'SONG', u.fullName, me.cover, FALSE )
     71            FROM Song s
     72            JOIN s.musicalEntities me
     73            JOIN me.releasedBy a
     74            JOIN a.nonAdminUser nau
     75            JOIN nau.user u
     76            WHERE me.title ILIKE %:searchTerm%
     77            """
     78    )
     79    List<MusicalEntityDto> searchSongs(@Param("currentUserId")Long currentUserId, @Param("searchTerm") String searchTerm);
     80
    7681}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/UserRepository.java

    r694fc25 rcb4a1da  
    33import com.ukim.finki.develop.finkwave.model.User;
    44import org.springframework.data.jpa.repository.JpaRepository;
     5import org.springframework.data.jpa.repository.Query;
    56import org.springframework.stereotype.Repository;
    67
     8import java.util.List;
    79import java.util.Optional;
    810
     
    1012public interface UserRepository extends JpaRepository<User, Long> {
    1113    Optional<User> findByUsername(String username);
     14
     15
     16    @Query("""
     17        SELECT u from User u
     18        WHERE (u.fullName ilike %:searchTerm% or u.username ilike %:searchTerm%)
     19            and u.listener = true and u.artist = false
     20        """)
     21    List<User> searchListeners(String searchTerm);
     22
     23    @Query("""
     24        SELECT u from User u
     25        WHERE (u.fullName ilike %:searchTerm% or u.username ilike %:searchTerm%)
     26            and u.artist = true
     27        """)
     28    List<User> searchArtists(String searchTerm);
    1229}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/AlbumService.java

    r694fc25 rcb4a1da  
    3939        dto.setSongs(songsFromAlbum);
    4040        return dto;
     41    }
    4142
     43    public List<MusicalEntityDto> searchAlbums(String searchTerm){
     44        return albumRepository.searchAlbums(authService.getCurrentUserID(), searchTerm);
    4245    }
    4346
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/SongService.java

    r694fc25 rcb4a1da  
    1616        return songRepository.findTopByListens(userId);
    1717    }
     18
     19    public List<MusicalEntityDto> searchSongs(Long userId, String searchTerm){
     20        return songRepository.searchSongs(userId, searchTerm);
     21    }
    1822}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/UserService.java

    r694fc25 rcb4a1da  
    22
    33import com.ukim.finki.develop.finkwave.model.User;
     4import com.ukim.finki.develop.finkwave.model.enums.UserType;
    45import com.ukim.finki.develop.finkwave.repository.UserRepository;
    56import lombok.AllArgsConstructor;
     
    1718    }
    1819
     20    // todo: add dto-s
     21
     22    public List<User> search(UserType userType, String searchTerm){
     23
     24        return switch (userType) {
     25            case ARTIST -> usersRepository.searchArtists(searchTerm);
     26            case LISTENER -> usersRepository.searchListeners(searchTerm);
     27            case null -> throw new IllegalArgumentException("Invalid userType");
     28        };
     29    }
     30
    1931}
  • frontend/src/App.tsx

    r694fc25 rcb4a1da  
     1import { useState } from "react";
    12import { createBrowserRouter, Outlet, RouterProvider } from "react-router-dom";
    23import { ToastContainer } from "react-toastify";
    34import "react-toastify/dist/ReactToastify.css";
     5import Sidebar from "./components/Sidebar";
    46import AllUsers from "./pages/AllUsers";
    57import LandingPage from "./pages/LandingPage";
    68import Login from "./pages/Login";
     9import MusicalCollection from "./pages/MusicalCollection";
    710import Nav from "./pages/Nav";
    811import Register from "./pages/Register";
    912import UserDetail from "./pages/UserDetail";
    10 import MusicalCollection from "./pages/MusicalCollection";
    1113
    1214const Layout = () => {
    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   );
     15        const [isSidebarOpen, setIsSidebarOpen] = useState(true);
     16
     17        return (
     18                <div className="flex flex-col min-h-screen bg-[#121212]">
     19                        <Nav
     20                                isSidebarOpen={isSidebarOpen}
     21                                onToggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)}
     22                        />
     23                        <Sidebar isOpen={isSidebarOpen} onClose={() => setIsSidebarOpen(false)} />
     24                        <ToastContainer
     25                                position="top-right"
     26                                autoClose={2000}
     27                                hideProgressBar={false}
     28                                newestOnTop={false}
     29                                closeOnClick
     30                                rtl={false}
     31                                pauseOnFocusLoss
     32                                draggable
     33                                pauseOnHover
     34                                theme="light"
     35                        />
     36                        <main
     37                                className={`grow transition-all duration-300 pt-20 ${
     38                                        isSidebarOpen ? "ml-64" : "ml-0"
     39                                }`}
     40                        >
     41                                <Outlet />
     42                        </main>
     43                </div>
     44        );
    3345};
    3446
    3547const router = createBrowserRouter([
    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/:username",
    54         element: <UserDetail />,
    55       },
    56       {
    57         path: "/register",
    58         element: <Register />,
    59       },
    60       {
    61         path: "/collection/:type/:id",
    62         element: <MusicalCollection />,
    63       },
    64     ],
    65   },
     48        {
     49                path: "",
     50                element: <Layout />,
     51                children: [
     52                        {
     53                                path: "/",
     54                                element: <LandingPage />,
     55                        },
     56                        {
     57                                path: "/login",
     58                                element: <Login />,
     59                        },
     60                        {
     61                                path: "/users",
     62                                element: <AllUsers />,
     63                        },
     64                        {
     65                                path: "/users/:username",
     66                                element: <UserDetail />,
     67                        },
     68                        {
     69                                path: "/register",
     70                                element: <Register />,
     71                        },
     72                        {
     73                                path: "/collection/:type/:id",
     74                                element: <MusicalCollection />,
     75                        },
     76                ],
     77        },
    6678]);
    6779
    6880function App() {
    69   return <RouterProvider router={router} />;
     81        return <RouterProvider router={router} />;
    7082}
    7183
  • frontend/src/components/userProfile/UserListModal.tsx

    r694fc25 rcb4a1da  
    33
    44interface ModalProps {
    5   title: string;
    6   users: BaseNonAdminUser[];
    7   onClose: () => void;
    8   onFollowToggle: (targetUsername: string) => Promise<void>;
     5        title: string;
     6        users: BaseNonAdminUser[];
     7        onClose: () => void;
     8        onFollowToggle: (targetUsername: string) => Promise<void>;
    99}
    1010
    1111const UserListModal = ({
    12   title,
    13   users,
    14   onClose,
    15   onFollowToggle,
     12        title,
     13        users,
     14        onClose,
     15        onFollowToggle,
    1616}: ModalProps) => {
    17   const navigate = useNavigate();
    18   const baseURL = import.meta.env.VITE_API_BASE_URL;
     17        const navigate = useNavigate();
     18        const baseURL = import.meta.env.VITE_API_BASE_URL;
    1919
    20   return (
    21     <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
    22       <div className="bg-white rounded-xl shadow-2xl w-full max-w-md max-h-[70vh] flex flex-col">
    23         <div className="p-4 border-b flex justify-between items-center">
    24           <h2 className="text-xl font-bold">{title}</h2>
    25           <button
    26             onClick={onClose}
    27             className="text-gray-500 hover:text-black text-2xl cursor-pointer"
    28           >
    29             &times;
    30           </button>
    31         </div>
     20        return (
     21                <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
     22                        <div className="bg-white rounded-xl shadow-2xl w-full max-w-md max-h-[70vh] flex flex-col">
     23                                <div className="p-4 border-b flex justify-between items-center">
     24                                        <h2 className="text-xl font-bold">{title}</h2>
     25                                        <button
     26                                                onClick={onClose}
     27                                                className="text-gray-500 hover:text-black text-2xl cursor-pointer"
     28                                        >
     29                                                &times;
     30                                        </button>
     31                                </div>
    3232
    33         <div className="overflow-y-auto p-4 flex-1">
    34           {users.length === 0 ? (
    35             <p className="text-center text-gray-500 py-8">No users found.</p>
    36           ) : (
    37             users.map((u) => (
    38               <div
    39                 key={u.username}
    40                 className="flex items-center justify-between p-3 hover:bg-gray-50 rounded-lg transition-colors"
    41               >
    42                 <div
    43                   className="flex items-center gap-4 cursor-pointer flex-1"
    44                   onClick={() => {
    45                     onClose();
    46                     navigate(`/users/${u.username}`);
    47                   }}
    48                 >
    49                   <div className="w-10 h-10 rounded-full bg-blue-100 overflow-hidden shrink-0 flex items-center justify-center">
    50                     {u.profilePhoto ? (
    51                       <img
    52                         src={`${baseURL}/${u.profilePhoto}`}
    53                         className="w-full h-full object-cover"
    54                         alt=""
    55                       />
    56                     ) : (
    57                       <span className="text-blue-600 font-bold">
    58                         {u.fullName.charAt(0)}
    59                       </span>
    60                     )}
    61                   </div>
    62                   <p className="font-semibold text-gray-900">{u.fullName}</p>
    63                 </div>
     33                                <div className="overflow-y-auto p-4 flex-1">
     34                                        {users.length === 0 ? (
     35                                                <p className="text-center text-gray-500 py-8">No users found.</p>
     36                                        ) : (
     37                                                users.map((u) => (
     38                                                        <div
     39                                                                key={u.username}
     40                                                                className="flex items-center justify-between p-3 hover:bg-gray-50 rounded-lg transition-colors"
     41                                                        >
     42                                                                <div
     43                                                                        className="flex items-center gap-4 cursor-pointer flex-1"
     44                                                                        onClick={() => {
     45                                                                                onClose();
     46                                                                                navigate(`/users/${u.username}`);
     47                                                                        }}
     48                                                                >
     49                                                                        <div className="w-10 h-10 rounded-full bg-blue-100 overflow-hidden shrink-0 flex items-center justify-center">
     50                                                                                {u.profilePhoto ? (
     51                                                                                        <img
     52                                                                                                src={`${baseURL}/${u.profilePhoto}`}
     53                                                                                                className="w-full h-full object-cover"
     54                                                                                                alt=""
     55                                                                                        />
     56                                                                                ) : (
     57                                                                                        <span className="text-blue-600 font-bold">
     58                                                                                                {u.fullName.charAt(0)}
     59                                                                                        </span>
     60                                                                                )}
     61                                                                        </div>
     62                                                                        <p className="font-semibold text-gray-900">{u.fullName}</p>
     63                                                                </div>
    6464
    65                 <button
    66                   onClick={() => onFollowToggle(u.username)}
    67                   className={`px-4 py-1 text-sm font-medium rounded-md transition-colors cursor-pointer ${
    68                     u.isFollowedByCurrentUser
    69                       ? "bg-gray-200 text-gray-700 hover:bg-gray-300"
    70                       : "bg-blue-500 text-white hover:bg-blue-600"
    71                   }`}
    72                 >
    73                   {u.isFollowedByCurrentUser ? "Unfollow" : "Follow"}
    74                 </button>
    75               </div>
    76             ))
    77           )}
    78         </div>
    79       </div>
    80       <div className="absolute inset-0 -z-10" onClick={onClose}></div>
    81     </div>
    82   );
     65                                                                <button
     66                                                                        onClick={() => onFollowToggle(u.username)}
     67                                                                        className={`px-4 py-1 text-sm font-medium rounded-md transition-colors cursor-pointer ${
     68                                                                                u.isFollowedByCurrentUser
     69                                                                                        ? "bg-gray-200 text-gray-700 hover:bg-gray-300"
     70                                                                                        : "bg-blue-500 text-white hover:bg-blue-600"
     71                                                                        }`}
     72                                                                >
     73                                                                        {u.isFollowedByCurrentUser ? "Unfollow" : "Follow"}
     74                                                                </button>
     75                                                        </div>
     76                                                ))
     77                                        )}
     78                                </div>
     79                        </div>
     80                        <div className="absolute inset-0 -z-10" onClick={onClose}></div>
     81                </div>
     82        );
    8383};
    8484
  • frontend/src/pages/AllUsers.tsx

    r694fc25 rcb4a1da  
    44
    55interface User {
    6   username: string;
    7   fullName: string;
     6        username: string;
     7        fullName: string;
    88}
    99
    1010const AllUsers = () => {
    11   const [users, setUsers] = useState<User[]>([]);
    12   const [searchedUser, setSearchedUser] = useState<string>("");
    13   const [error, setError] = useState<string | null>(null);
    14   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();
    1515
    16   useEffect(() => {
    17     const fetchUsers = async () => {
    18       const response = axiosInstance.get("/users/all");
    19       setUsers((await response).data);
    20     };
    21     fetchUsers();
    22   }, []);
     16        useEffect(() => {
     17                const fetchUsers = async () => {
     18                        const response = axiosInstance.get("/users/na/all");
     19                        setUsers((await response).data);
     20                };
     21                fetchUsers();
     22        }, []);
    2323
    24   const handleUserClick = (username: string) => {
    25     navigate(`/users/${username}`);
    26   };
     24        const handleUserClick = (username: string) => {
     25                navigate(`/users/${username}`);
     26        };
    2727
    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   };
     28        const handleSearch = async (e: React.FormEvent) => {
     29                e.preventDefault();
     30                try {
     31                        const response = await axiosInstance.get(`/users/na/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        };
    4040
    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   }
     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        }
    5050
    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>
     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>
    6969
    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   );
     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        );
    8484};
    8585
  • frontend/src/pages/LandingPage.tsx

    r694fc25 rcb4a1da  
    11import { useEffect, useState } from "react";
    22import axiosInstance from "../api/axiosInstance";
    3 import type { Song } from "../utils/types";
    4 import Nav from "./Nav";
     3import AlbumResult from "../components/search/AlbumResult";
     4import SongResult from "../components/search/SongResult";
     5import UserResult from "../components/search/UserResult";
     6import type {
     7        Album,
     8        BaseNonAdminUser,
     9        SearchCategory,
     10        Song,
     11} from "../utils/types";
     12
     13const CATEGORIES: { value: SearchCategory; label: string }[] = [
     14        { value: "songs", label: "Songs" },
     15        { value: "albums", label: "Albums" },
     16        { value: "artists", label: "Artists" },
     17        { value: "users", label: "Users" },
     18];
    519
    620const LandingPage = () => {
    721        const [songs, setSongs] = useState<Song[]>([]);
    822        const [loading, setLoading] = useState<boolean>(true);
    9         const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(true);
    10 
    11         // mock data for recently listened songs
    12         const recentlyListened = [
    13                 { id: 1, title: "Song One", artist: "Artist A", cover: "/favicon.png" },
    14                 { id: 2, title: "Song Two", artist: "Artist B", cover: "/favicon.png" },
    15                 { id: 3, title: "Song Three", artist: "Artist C", cover: "/favicon.png" },
    16                 { id: 4, title: "Song Four", artist: "Artist D", cover: "/favicon.png" },
    17                 { id: 5, title: "Song Five", artist: "Artist E", cover: "/favicon.png" },
    18         ];
    19 
    20         // mock data for my playlists
    21         const playlists = [
    22                 { id: 1, name: "My Favorites", songCount: 25 },
    23                 { id: 2, name: "Workout Mix", songCount: 18 },
    24                 { id: 3, name: "Chill Vibes", songCount: 32 },
    25                 { id: 4, name: "Party Hits", songCount: 45 },
    26         ];
     23
     24        // search state
     25        const [searchInput, setSearchInput] = useState("");
     26        const [activeQuery, setActiveQuery] = useState("");
     27        const [searchCategory, setSearchCategory] = useState<SearchCategory>("songs");
     28        const [searchResults, setSearchResults] = useState<unknown[]>([]);
     29        const [searchLoading, setSearchLoading] = useState(false);
     30        const [hasSearched, setHasSearched] = useState(false);
    2731
    2832        useEffect(() => {
    2933                const fetchSongs = async () => {
    3034                        try {
    31                                 const response = await axiosInstance.get("/songs");
    32                                 const data = response.data;
    33                                 console.log("Fetched songs:", data);
    34                                 setSongs(data);
     35                                const response = await axiosInstance.get("/songs/top");
     36                                setSongs(response.data);
    3537                        } catch (error) {
    3638                                console.error("Error fetching songs:", error);
     
    4244        }, []);
    4345
     46        const performSearch = async (query: string, category: SearchCategory) => {
     47                if (!query.trim()) return;
     48
     49                setSearchLoading(true);
     50                setHasSearched(true);
     51                setActiveQuery(query);
     52                setSearchCategory(category);
     53
     54                try {
     55                        let endpoint = "";
     56                        switch (category) {
     57                                case "songs":
     58                                        endpoint = `/songs/search?q=${encodeURIComponent(query)}`;
     59                                        break;
     60                                case "albums":
     61                                        endpoint = `/albums/search?q=${encodeURIComponent(query)}`;
     62                                        break;
     63                                case "artists":
     64                                        endpoint = `/users/search?type=ARTIST&q=${encodeURIComponent(query)}`;
     65                                        break;
     66                                case "users":
     67                                        endpoint = `/users/search?type=LISTENER&q=${encodeURIComponent(query)}`;
     68                                        break;
     69                        }
     70
     71                        const response = await axiosInstance.get(endpoint);
     72                        setSearchResults(response.data);
     73                } catch (error) {
     74                        console.error("Search error:", error);
     75                        setSearchResults([]);
     76                } finally {
     77                        setSearchLoading(false);
     78                }
     79        };
     80
     81        const handleSearch = () => {
     82                performSearch(searchInput, searchCategory);
     83        };
     84
     85        const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
     86                if (e.key === "Enter") {
     87                        handleSearch();
     88                }
     89        };
     90
     91        const handleCategorySwitch = (category: SearchCategory) => {
     92                performSearch(activeQuery, category);
     93        };
     94
     95        const clearSearch = () => {
     96                setSearchInput("");
     97                setActiveQuery("");
     98                setHasSearched(false);
     99                setSearchResults([]);
     100        };
     101
     102        const renderResults = () => {
     103                if (searchLoading) {
     104                        return (
     105                                <div className="flex justify-center py-12">
     106                                        <div className="w-10 h-10 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin"></div>
     107                                </div>
     108                        );
     109                }
     110
     111                if (searchResults.length === 0) {
     112                        return (
     113                                <div className="text-center py-12 text-gray-400">
     114                                        <p className="text-lg">
     115                                                No {searchCategory} found for "{activeQuery}"
     116                                        </p>
     117                                </div>
     118                        );
     119                }
     120
     121                return (
     122                        <div className="divide-y divide-white/5">
     123                                {searchResults.map((result, index) => {
     124                                        switch (searchCategory) {
     125                                                case "songs":
     126                                                        return (
     127                                                                <SongResult key={(result as Song).id} song={result as Song} />
     128                                                        );
     129                                                case "albums":
     130                                                        return (
     131                                                                <AlbumResult
     132                                                                        key={(result as Album).id}
     133                                                                        album={result as Album}
     134                                                                />
     135                                                        );
     136                                                case "artists":
     137                                                        return (
     138                                                                <UserResult
     139                                                                        key={(result as BaseNonAdminUser).username ?? index}
     140                                                                        user={result as BaseNonAdminUser}
     141                                                                        label="Artist"
     142                                                                />
     143                                                        );
     144                                                case "users":
     145                                                        return (
     146                                                                <UserResult
     147                                                                        key={(result as BaseNonAdminUser).username ?? index}
     148                                                                        user={result as BaseNonAdminUser}
     149                                                                        label="User"
     150                                                                />
     151                                                        );
     152                                        }
     153                                })}
     154                        </div>
     155                );
     156        };
     157
    44158        return (
    45                 <>
    46                         <Nav
    47                                 isSidebarOpen={isSidebarOpen}
    48                                 onToggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)}
    49                         />
    50                         <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white flex pt-20">
    51                                 {/* Sidebar */}
    52                                 <div
    53                                         className={`fixed left-0 top-0 h-full bg-[#121212] border-r border-white/10 transition-transform duration-300 ease-in-out z-100 ${
    54                                                 isSidebarOpen ? "translate-x-0" : "-translate-x-full"
    55                                         } w-64 overflow-y-auto`}
    56                                 >
    57                                         <div className="p-6">
    58                                                 {/* Sidebar Header */}
    59                                                 <div className="flex justify-between items-center mb-6">
    60                                                         <h2 className="text-xl font-bold text-white">Library</h2>
    61                                                         <button
    62                                                                 onClick={() => setIsSidebarOpen(false)}
    63                                                                 className="text-gray-400 hover:text-white transition-colors"
    64                                                                 aria-label="Close sidebar"
    65                                                         >
    66                                                                 <svg
    67                                                                         className="w-6 h-6"
    68                                                                         fill="none"
    69                                                                         stroke="currentColor"
    70                                                                         viewBox="0 0 24 24"
     159                <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white flex">
     160                        <div className="flex-1">
     161                                <div className="p-8">
     162                                        <div className="max-w-7xl mx-auto">
     163                                                {/* Search Bar */}
     164                                                <div className="mb-8 flex flex-col md:flex-row gap-3">
     165                                                        <div className="flex flex-1 gap-0">
     166                                                                {/* Category Dropdown */}
     167                                                                <select
     168                                                                        value={searchCategory}
     169                                                                        onChange={(e) =>
     170                                                                                setSearchCategory(e.target.value as SearchCategory)
     171                                                                        }
     172                                                                        className="bg-[#282828] border border-white/10 border-r-0 rounded-l-full py-2 px-4 text-white text-sm focus:outline-none focus:border-[#1db954] appearance-none cursor-pointer"
    71173                                                                >
    72                                                                         <path
    73                                                                                 strokeLinecap="round"
    74                                                                                 strokeLinejoin="round"
    75                                                                                 strokeWidth={2}
    76                                                                                 d="M6 18L18 6M6 6l12 12"
    77                                                                         />
    78                                                                 </svg>
    79                                                         </button>
    80                                                 </div>
    81 
    82                                                 {/* Recently Listened */}
    83                                                 <div className="mb-8">
    84                                                         <h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4">
    85                                                                 Recently Played
    86                                                         </h3>
    87                                                         <div className="space-y-3">
    88                                                                 {recentlyListened.map((song) => (
    89                                                                         <div
    90                                                                                 key={song.id}
    91                                                                                 className="flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 cursor-pointer transition-colors"
     174                                                                        {CATEGORIES.map((cat) => (
     175                                                                                <option key={cat.value} value={cat.value}>
     176                                                                                        {cat.label}
     177                                                                                </option>
     178                                                                        ))}
     179                                                                </select>
     180
     181                                                                {/* Search Input */}
     182                                                                <input
     183                                                                        type="text"
     184                                                                        placeholder={`Search ${searchCategory}...`}
     185                                                                        value={searchInput}
     186                                                                        onChange={(e) => setSearchInput(e.target.value)}
     187                                                                        onKeyDown={handleKeyDown}
     188                                                                        className="flex-1 bg-[#282828] border border-white/10 border-r-0 py-2 px-4 text-white focus:outline-none focus:border-[#1db954] focus:ring-1 focus:ring-[#1db954] transition-all"
     189                                                                />
     190
     191                                                                {/* Search Button */}
     192                                                                <button
     193                                                                        onClick={handleSearch}
     194                                                                        className="bg-[#1db954] hover:bg-[#1ed760] text-black font-medium px-6 rounded-r-full transition-colors flex items-center gap-2"
     195                                                                >
     196                                                                        <svg
     197                                                                                className="w-5 h-5"
     198                                                                                fill="none"
     199                                                                                stroke="currentColor"
     200                                                                                viewBox="0 0 24 24"
    92201                                                                        >
    93                                                                                 <img
    94                                                                                         src={song.cover}
    95                                                                                         alt={song.title}
    96                                                                                         className="w-10 h-10 rounded object-cover"
     202                                                                                <path
     203                                                                                        strokeLinecap="round"
     204                                                                                        strokeLinejoin="round"
     205                                                                                        strokeWidth={2}
     206                                                                                        d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
    97207                                                                                />
    98                                                                                 <div className="flex-1 min-w-0">
    99                                                                                         <p className="text-sm font-medium text-white truncate">
    100                                                                                                 {song.title}
    101                                                                                         </p>
    102                                                                                         <p className="text-xs text-gray-400 truncate">
    103                                                                                                 {song.artist}
    104                                                                                         </p>
    105                                                                                 </div>
    106                                                                         </div>
    107                                                                 ))}
     208                                                                        </svg>
     209                                                                        <span className="hidden sm:inline">Search</span>
     210                                                                </button>
    108211                                                        </div>
    109212                                                </div>
    110213
    111                                                 {/* Playlists */}
    112                                                 <div>
    113                                                         <h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4">
    114                                                                 Your Playlists
    115                                                         </h3>
    116                                                         <div className="space-y-2">
    117                                                                 {playlists.map((playlist) => (
    118                                                                         <div
    119                                                                                 key={playlist.id}
    120                                                                                 className="flex items-center justify-between p-3 rounded-lg hover:bg-white/5 cursor-pointer transition-colors"
     214                                                {/* Search Results Section */}
     215                                                {hasSearched ? (
     216                                                        <div>
     217                                                                <div className="flex items-center justify-between mb-4">
     218                                                                        <h2 className="text-2xl font-bold text-white">
     219                                                                                Results for "{activeQuery}"
     220                                                                        </h2>
     221                                                                        <button
     222                                                                                onClick={clearSearch}
     223                                                                                className="text-sm text-gray-400 hover:text-white transition-colors"
    121224                                                                        >
    122                                                                                 <div className="flex items-center gap-3">
    123                                                                                         <div className="w-10 h-10 bg-linear-to-br from-purple-500 to-pink-500 rounded flex items-center justify-center">
    124                                                                                                 <svg
    125                                                                                                         className="w-5 h-5 text-white"
    126                                                                                                         fill="currentColor"
    127                                                                                                         viewBox="0 0 20 20"
    128                                                                                                 >
    129                                                                                                         <path d="M18 3a1 1 0 00-1.196-.98l-10 2A1 1 0 006 5v9.114A4.369 4.369 0 005 14c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V7.82l8-1.6v5.894A4.37 4.37 0 0015 12c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V3z" />
    130                                                                                                 </svg>
    131                                                                                         </div>
    132                                                                                         <div>
    133                                                                                                 <p className="text-sm font-medium text-white">
    134                                                                                                         {playlist.name}
    135                                                                                                 </p>
    136                                                                                                 <p className="text-xs text-gray-400">
    137                                                                                                         {playlist.songCount} songs
    138                                                                                                 </p>
    139                                                                                         </div>
    140                                                                                 </div>
    141                                                                         </div>
    142                                                                 ))}
    143                                                         </div>
    144                                                 </div>
    145                                         </div>
    146                                 </div>
    147 
    148                                 {/* Main Content */}
    149                                 <div
    150                                         className={`flex-1 transition-all duration-300 ${
    151                                                 isSidebarOpen ? "ml-64" : "ml-0"
    152                                         }`}
    153                                 >
    154                                         <div className="p-8">
    155                                                 {loading ? (
    156                                                         <div className="flex flex-col items-center justify-center min-h-[60vh] gap-6">
    157                                                                 <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin"></div>
    158                                                                 <p className="text-xl text-gray-400">Loading songs...</p>
     225                                                                                Clear search
     226                                                                        </button>
     227                                                                </div>
     228
     229                                                                {/* Quick category switch buttons */}
     230                                                                <div className="flex gap-2 mb-6">
     231                                                                        {CATEGORIES.map((cat) => (
     232                                                                                <button
     233                                                                                        key={cat.value}
     234                                                                                        onClick={() => handleCategorySwitch(cat.value)}
     235                                                                                        className={`px-4 py-1.5 rounded-full text-sm font-medium transition-colors ${
     236                                                                                                searchCategory === cat.value
     237                                                                                                        ? "bg-[#1db954] text-black"
     238                                                                                                        : "bg-white/10 text-white hover:bg-white/20"
     239                                                                                        }`}
     240                                                                                >
     241                                                                                        {cat.label}
     242                                                                                </button>
     243                                                                        ))}
     244                                                                </div>
     245
     246                                                                {/* Results list */}
     247                                                                <div className="bg-[#1a1a2e]/50 rounded-xl overflow-hidden">
     248                                                                        {renderResults()}
     249                                                                </div>
    159250                                                        </div>
    160251                                                ) : (
    161                                                         <div className="max-w-7xl mx-auto">
    162                                                                 <div className="mb-12 pb-6 border-b border-white/10">
     252                                                        /* Default song grid */
     253                                                        <>
     254                                                                <div className="mb-8 border-b border-white/10 pb-6">
    163255                                                                        <h1 className="text-5xl font-bold mb-3 pb-1 bg-linear-to-r from-[#1db954] to-[#1ed760] bg-clip-text text-transparent">
    164256                                                                                Top Songs
     
    168260                                                                        </p>
    169261                                                                </div>
    170                                                                 <div className="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-8 py-4">
    171                                                                         {songs.map((song) => (
    172                                                                                 <div
    173                                                                                         key={song.id}
    174                                                                                         className="bg-[#282828] rounded-xl overflow-hidden cursor-pointer shadow-lg transition-all duration-300 ease-in-out hover:-translate-y-2 hover:shadow-2xl group"
    175                                                                                 >
    176                                                                                         <div className="relative w-full pt-[100%] overflow-hidden bg-[#181818]">
    177                                                                                                 <img
    178                                                                                                         src={song.cover || "/favicon.png"}
    179                                                                                                         alt={song.title}
    180                                                                                                         className="absolute top-0 left-0 w-full h-full object-cover"
    181                                                                                                         onError={(e) => {
    182                                                                                                                 (e.target as HTMLImageElement).src = "/favicon.png";
    183                                                                                                         }}
    184                                                                                                 />
    185                                                                                                 <div className="absolute top-0 left-0 w-full h-full bg-black/60 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
    186                                                                                                         <div className="w-15 h-15 rounded-full bg-[#1db954] flex items-center justify-center text-2xl text-black shadow-[0_4px_12px_rgba(29,185,84,0.5)]">
    187                                                                                                                 ▶
     262
     263                                                                {loading ? (
     264                                                                        <div className="flex flex-col items-center justify-center min-h-[40vh] gap-6">
     265                                                                                <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin"></div>
     266                                                                                <p className="text-xl text-gray-400">Loading songs...</p>
     267                                                                        </div>
     268                                                                ) : (
     269                                                                        <div className="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-8 py-4">
     270                                                                                {songs.map((song) => (
     271                                                                                        <div
     272                                                                                                key={song.id}
     273                                                                                                className="bg-[#282828] rounded-xl overflow-hidden cursor-pointer shadow-lg transition-all duration-300 ease-in-out hover:-translate-y-2 hover:shadow-2xl group"
     274                                                                                        >
     275                                                                                                <div className="relative w-full pt-[100%] overflow-hidden bg-[#181818]">
     276                                                                                                        <img
     277                                                                                                                src={song.cover || "/favicon.png"}
     278                                                                                                                alt={song.title}
     279                                                                                                                className="absolute top-0 left-0 w-full h-full object-cover"
     280                                                                                                                onError={(e) => {
     281                                                                                                                        (e.target as HTMLImageElement).src =
     282                                                                                                                                "/favicon.png";
     283                                                                                                                }}
     284                                                                                                        />
     285                                                                                                        <div className="absolute top-0 left-0 w-full h-full bg-black/60 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
     286                                                                                                                <div className="w-15 h-15 rounded-full bg-[#1db954] flex items-center justify-center text-2xl text-black shadow-[0_4px_12px_rgba(29,185,84,0.5)]">
     287                                                                                                                        ▶
     288                                                                                                                </div>
    188289                                                                                                        </div>
    189290                                                                                                </div>
     291                                                                                                <div className="p-4 flex flex-col items-center">
     292                                                                                                        <h3 className="text-lg font-semibold mb-2 text-white overflow-hidden text-ellipsis whitespace-nowrap">
     293                                                                                                                {song.title}
     294                                                                                                        </h3>
     295                                                                                                        <p className="text-sm text-gray-400 mb-3 overflow-hidden text-ellipsis whitespace-nowrap">
     296                                                                                                                {"<album>"}
     297                                                                                                        </p>
     298                                                                                                        <p className="text-sm text-gray-400 mb-3 overflow-hidden text-ellipsis whitespace-nowrap">
     299                                                                                                                {song.releasedBy}
     300                                                                                                        </p>
     301                                                                                                </div>
    190302                                                                                        </div>
    191                                                                                         <div className="p-4 flex flex-col items-center">
    192                                                                                                 <h3 className="text-lg font-semibold mb-2 text-white overflow-hidden text-ellipsis whitespace-nowrap">
    193                                                                                                         {song.title}
    194                                                                                                 </h3>
    195                                                                                                 <p className="text-sm text-gray-400 mb-3 overflow-hidden text-ellipsis whitespace-nowrap">
    196                                                                                                         {"<album>"}
    197                                                                                                 </p>
    198                                                                                                 <p className="text-sm text-gray-400 mb-3 overflow-hidden text-ellipsis whitespace-nowrap">
    199                                                                                                         {song.releasedBy}
    200                                                                                                 </p>
    201                                                                                                 {/* <div className="inline-block bg-[#1db954]/20 px-3 py-1 rounded-xl border border-[#1db954]/40">
    202                                                                                                         <span className="text-xs text-[#1db954] font-medium uppercase tracking-wider">
    203                                                                                                                 {song.genre}
    204                                                                                                         </span>
    205                                                                                                 </div> */}
    206                                                                                         </div>
    207                                                                                 </div>
    208                                                                         ))}
    209                                                                 </div>
    210                                                         </div>
     303                                                                                ))}
     304                                                                        </div>
     305                                                                )}
     306                                                        </>
    211307                                                )}
    212308                                        </div>
    213309                                </div>
    214310                        </div>
    215                 </>
     311                </div>
    216312        );
    217313};
  • frontend/src/pages/UserDetail.tsx

    r694fc25 rcb4a1da  
    77import { handleError } from "../utils/error";
    88import type {
    9   MusicalEntity,
    10   Playlist,
    11   ArtistContribution,
    12   BaseNonAdminUser,
     9        ArtistContribution,
     10        BaseNonAdminUser,
     11        MusicalEntity,
     12        Playlist,
    1313} from "../utils/types";
    1414
    1515interface FollowStatus {
    16   isFollowing: boolean;
    17   followerCount: number;
    18   followingCount: number;
     16        isFollowing: boolean;
     17        followerCount: number;
     18        followingCount: number;
    1919}
    2020
    2121interface Artist extends BaseNonAdminUser {
    22   userType: "ARTIST";
    23   contributions: ArtistContribution[];
     22        userType: "ARTIST";
     23        contributions: ArtistContribution[];
    2424}
    2525interface Listener extends BaseNonAdminUser {
    26   userType: "LISTENER";
    27   likedEntities: MusicalEntity[];
    28   createdPlaylists: Playlist[];
    29   savedPlaylists: Playlist[];
     26        userType: "LISTENER";
     27        likedEntities: MusicalEntity[];
     28        createdPlaylists: Playlist[];
     29        savedPlaylists: Playlist[];
    3030}
    3131
     
    3333
    3434const UserDetail = () => {
    35   // user refers to the selected user NOT to the user from context
    36   const baseURL = import.meta.env.VITE_API_BASE_URL;
    37   const { username } = useParams();
    38   const navigate = useNavigate();
    39   const [user, setUser] = useState<UserProfile | null>(null);
    40   const [error, setError] = useState<string | null>(null);
    41   const [showModal, setShowModal] = useState(false);
    42   const [modalTitle, setModalTitle] = useState("");
    43   const [modalUsers, setModalUsers] = useState<any[]>([]);
    44   const [isLoadingModal, setIsLoadingModal] = useState(false);
    45   const [isFollowing, setIsFollowing] = useState(false);
    46 
    47   const handleFollow = async () => {
    48     if (!user) return;
    49 
    50     setIsFollowing(true);
    51     try {
    52       const response = await axiosInstance.post<FollowStatus>(
    53         `/users/${username}/follow`,
    54       );
    55       setUser((prev) => {
    56         if (!prev) return null;
    57         return {
    58           ...prev,
    59           isFollowedByCurrentUser: response.data.isFollowing,
    60           followers: response.data.followerCount,
    61           following: response.data.followingCount,
    62         };
    63       });
    64     } catch (err: any) {
    65       setError(handleError(err));
    66     } finally {
    67       setIsFollowing(false);
    68     }
    69   };
    70 
    71   const handleFollowInModal = async (targetUsername: string) => {
    72     try {
    73       const response = await axiosInstance.post<FollowStatus>(
    74         `/users/${username}/follow`,
    75       );
    76 
    77       setModalUsers((prevUsers) =>
    78         prevUsers.map((u) =>
    79           u.username === targetUsername
    80             ? { ...u, isFollowedByCurrentUser: response.data.isFollowing }
    81             : u,
    82         ),
    83       );
    84 
    85       // if (user && user.id === targetId) {
    86       //   setUser((prev) => {
    87       //     if (!prev) return null;
    88       //     return {
    89       //       ...prev,
    90       //       isFollowedByCurrentUser: response.data.isFollowing,
    91       //       followers: response.data.followerCount,
    92       //     };
    93       //   });
    94       // }
    95     } catch (err: any) {
    96       setError(handleError(err));
    97     }
    98   };
    99 
    100   const displayFollowers = async () => {
    101     setIsLoadingModal(true);
    102     try {
    103       const response = await axiosInstance.get(`/users/${username}/followers`);
    104       setModalUsers(response.data);
    105       setModalTitle("Followers");
    106       setShowModal(true);
    107     } catch (err) {
    108       setError(handleError(err));
    109     } finally {
    110       setIsLoadingModal(false);
    111     }
    112   };
    113   const displayFollowing = async () => {
    114     setIsLoadingModal(true);
    115     try {
    116       const response = await axiosInstance.get(`/users/${username}/following`);
    117       setModalUsers(response.data);
    118       setModalTitle("Following");
    119       setShowModal(true);
    120     } catch (err: any) {
    121       setError(handleError(err));
    122     } finally {
    123       setIsLoadingModal(false);
    124     }
    125   };
    126 
    127   useEffect(() => {
    128     const fetchUser = async () => {
    129       setError(null);
    130       try {
    131         const response = await axiosInstance.get(`/users/${username}`);
    132         console.log(response.data);
    133         setUser(response.data);
    134       } catch (err: any) {
    135         setError(handleError(err));
    136       }
    137     };
    138     fetchUser();
    139   }, [username]);
    140 
    141   if (error) {
    142     return (
    143       <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg">
    144         <h2 className="font-bold">Error</h2>
    145         <p>{error}</p>
    146       </div>
    147     );
    148   }
    149 
    150   if (!user) return <div className="p-6">Loading...</div>;
    151 
    152   return (
    153     <div className="container mx-auto p-6">
    154       {isLoadingModal && (
    155         <div className="fixed inset-0 z-40 bg-black/30 backdrop-blur-sm flex items-center justify-center">
    156           <div className="flex items-center gap-3">
    157             <div className="w-6 h-6 border-3 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
    158           </div>
    159         </div>
    160       )}
    161       <button
    162         onClick={() => navigate(-1)}
    163         className="mb-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors duration-200 cursor-pointer"
    164       >
    165         ← Back
    166       </button>
    167 
    168       <div className="bg-white shadow-lg rounded-lg p-8">
    169         <div className="flex items-start gap-6 mb-8">
    170           <div className="shrink-0">
    171             <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">
    172               {user.profilePhoto ? (
    173                 <img
    174                   src={`${baseURL}/${user.profilePhoto}`}
    175                   alt={user.fullName}
    176                   className="w-full h-full object-cover"
    177                 />
    178               ) : (
    179                 user.fullName.charAt(0).toUpperCase()
    180               )}
    181             </div>
    182           </div>
    183 
    184           <div className="flex-1">
    185             <h1 className="text-4xl font-bold mb-2">{user.fullName}</h1>
    186             <span className="inline-block px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium mb-4">
    187               {user.userType}
    188             </span>
    189 
    190             <div className="flex gap-6 mb-4 text-gray-700">
    191               <div
    192                 className={`flex flex-col ${user.userType == "LISTENER" ? "cursor-pointer" : "cursor-default"}`}
    193                 onClick={
    194                   user.userType === "LISTENER" ? displayFollowers : undefined
    195                 }
    196               >
    197                 <span className="text-2xl font-bold">{user.followers}</span>
    198                 <span className="text-sm text-gray-500">Followers</span>
    199               </div>
    200               <div
    201                 className={`flex flex-col ${user.userType == "LISTENER" ? "cursor-pointer" : "cursor-default"}`}
    202                 onClick={
    203                   user.userType === "LISTENER" ? displayFollowing : undefined
    204                 }
    205               >
    206                 <span className="text-2xl font-bold">{user.following}</span>
    207                 <span className="text-sm text-gray-500">Following</span>
    208               </div>
    209             </div>
    210 
    211             <button
    212               onClick={handleFollow}
    213               disabled={isFollowing}
    214               className={`
     35        // user refers to the selected user NOT to the user from context
     36        const baseURL = import.meta.env.VITE_API_BASE_URL;
     37        const { username } = useParams();
     38        const navigate = useNavigate();
     39        const [user, setUser] = useState<UserProfile | null>(null);
     40        const [error, setError] = useState<string | null>(null);
     41        const [showModal, setShowModal] = useState(false);
     42        const [modalTitle, setModalTitle] = useState("");
     43        const [modalUsers, setModalUsers] = useState<any[]>([]);
     44        const [isLoadingModal, setIsLoadingModal] = useState(false);
     45        const [isFollowing, setIsFollowing] = useState(false);
     46
     47        const handleFollow = async () => {
     48                if (!user) return;
     49
     50                setIsFollowing(true);
     51                try {
     52                        const response = await axiosInstance.post<FollowStatus>(
     53                                `/users/na/${username}/follow`,
     54                        );
     55                        setUser((prev) => {
     56                                if (!prev) return null;
     57                                return {
     58                                        ...prev,
     59                                        isFollowedByCurrentUser: response.data.isFollowing,
     60                                        followers: response.data.followerCount,
     61                                        following: response.data.followingCount,
     62                                };
     63                        });
     64                } catch (err: any) {
     65                        setError(handleError(err));
     66                } finally {
     67                        setIsFollowing(false);
     68                }
     69        };
     70
     71        const handleFollowInModal = async (targetUsername: string) => {
     72                try {
     73                        const response = await axiosInstance.post<FollowStatus>(
     74                                `/users/na/${username}/follow`,
     75                        );
     76
     77                        setModalUsers((prevUsers) =>
     78                                prevUsers.map((u) =>
     79                                        u.username === targetUsername
     80                                                ? { ...u, isFollowedByCurrentUser: response.data.isFollowing }
     81                                                : u,
     82                                ),
     83                        );
     84
     85                        // if (user && user.id === targetId) {
     86                        //   setUser((prev) => {
     87                        //     if (!prev) return null;
     88                        //     return {
     89                        //       ...prev,
     90                        //       isFollowedByCurrentUser: response.data.isFollowing,
     91                        //       followers: response.data.followerCount,
     92                        //     };
     93                        //   });
     94                        // }
     95                } catch (err: any) {
     96                        setError(handleError(err));
     97                }
     98        };
     99
     100        const displayFollowers = async () => {
     101                setIsLoadingModal(true);
     102                try {
     103                        const response = await axiosInstance.get(
     104                                `/users/na/${username}/followers`,
     105                        );
     106                        setModalUsers(response.data);
     107                        setModalTitle("Followers");
     108                        setShowModal(true);
     109                } catch (err) {
     110                        setError(handleError(err));
     111                } finally {
     112                        setIsLoadingModal(false);
     113                }
     114        };
     115        const displayFollowing = async () => {
     116                setIsLoadingModal(true);
     117                try {
     118                        const response = await axiosInstance.get(
     119                                `/users/na/${username}/following`,
     120                        );
     121                        setModalUsers(response.data);
     122                        setModalTitle("Following");
     123                        setShowModal(true);
     124                } catch (err: any) {
     125                        setError(handleError(err));
     126                } finally {
     127                        setIsLoadingModal(false);
     128                }
     129        };
     130
     131        useEffect(() => {
     132                const fetchUser = async () => {
     133                        setError(null);
     134                        try {
     135                                const response = await axiosInstance.get(`/users/na/${username}`);
     136                                console.log(response.data);
     137                                setUser(response.data);
     138                        } catch (err: any) {
     139                                setError(handleError(err));
     140                        }
     141                };
     142                fetchUser();
     143        }, [username]);
     144
     145        if (error) {
     146                return (
     147                        <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg">
     148                                <h2 className="font-bold">Error</h2>
     149                                <p>{error}</p>
     150                        </div>
     151                );
     152        }
     153
     154        if (!user) return <div className="p-6">Loading...</div>;
     155
     156        return (
     157                <div className="container mx-auto p-6">
     158                        {isLoadingModal && (
     159                                <div className="fixed inset-0 z-40 bg-black/30 backdrop-blur-sm flex items-center justify-center">
     160                                        <div className="flex items-center gap-3">
     161                                                <div className="w-6 h-6 border-3 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
     162                                        </div>
     163                                </div>
     164                        )}
     165                        <button
     166                                onClick={() => navigate(-1)}
     167                                className="mb-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors duration-200 cursor-pointer"
     168                        >
     169                                ← Back
     170                        </button>
     171
     172                        <div className="bg-white shadow-lg rounded-lg p-8">
     173                                <div className="flex items-start gap-6 mb-8">
     174                                        <div className="shrink-0">
     175                                                <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">
     176                                                        {user.profilePhoto ? (
     177                                                                <img
     178                                                                        src={`${baseURL}/${user.profilePhoto}`}
     179                                                                        alt={user.fullName}
     180                                                                        className="w-full h-full object-cover"
     181                                                                />
     182                                                        ) : (
     183                                                                user.fullName.charAt(0).toUpperCase()
     184                                                        )}
     185                                                </div>
     186                                        </div>
     187
     188                                        <div className="flex-1">
     189                                                <h1 className="text-4xl font-bold mb-2">{user.fullName}</h1>
     190                                                <span className="inline-block px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium mb-4">
     191                                                        {user.userType}
     192                                                </span>
     193
     194                                                <div className="flex gap-6 mb-4 text-gray-700">
     195                                                        <div
     196                                                                className={`flex flex-col ${user.userType == "LISTENER" ? "cursor-pointer" : "cursor-default"}`}
     197                                                                onClick={
     198                                                                        user.userType === "LISTENER" ? displayFollowers : undefined
     199                                                                }
     200                                                        >
     201                                                                <span className="text-2xl font-bold">{user.followers}</span>
     202                                                                <span className="text-sm text-gray-500">Followers</span>
     203                                                        </div>
     204                                                        <div
     205                                                                className={`flex flex-col ${user.userType == "LISTENER" ? "cursor-pointer" : "cursor-default"}`}
     206                                                                onClick={
     207                                                                        user.userType === "LISTENER" ? displayFollowing : undefined
     208                                                                }
     209                                                        >
     210                                                                <span className="text-2xl font-bold">{user.following}</span>
     211                                                                <span className="text-sm text-gray-500">Following</span>
     212                                                        </div>
     213                                                </div>
     214
     215                                                <button
     216                                                        onClick={handleFollow}
     217                                                        disabled={isFollowing}
     218                                                        className={`
    215219                px-6 py-2 font-semibold rounded-lg shadow-md
    216220                transition-colors duration-200
    217221                ${
    218                   isFollowing
    219                     ? "bg-gray-400 text-gray-200 cursor-not-allowed"
    220                     : user.isFollowedByCurrentUser
    221                       ? "bg-gray-200 text-gray-700 hover:bg-gray-300 cursor-pointer"
    222                       : "bg-blue-500 text-white hover:bg-blue-600 cursor-pointer"
    223                 }
     222                                                                        isFollowing
     223                                                                                ? "bg-gray-400 text-gray-200 cursor-not-allowed"
     224                                                                                : user.isFollowedByCurrentUser
     225                                                                                        ? "bg-gray-200 text-gray-700 hover:bg-gray-300 cursor-pointer"
     226                                                                                        : "bg-blue-500 text-white hover:bg-blue-600 cursor-pointer"
     227                                                                }
    224228              `}
    225             >
    226               {user.isFollowedByCurrentUser ? "Unfollow" : "Follow"}
    227             </button>
    228           </div>
    229         </div>
    230 
    231         {user.userType === "ARTIST" ? (
    232           <ArtistView contributions={user.contributions} />
    233         ) : (
    234           <ListenerView
    235             likedEntities={user.likedEntities}
    236             createdPlaylists={user.createdPlaylists}
    237             savedPlaylists={user.savedPlaylists}
    238           />
    239         )}
    240 
    241         {showModal && (
    242           <UserListModal
    243             title={modalTitle}
    244             users={modalUsers}
    245             onClose={() => setShowModal(false)}
    246             onFollowToggle={handleFollowInModal}
    247           />
    248         )}
    249       </div>
    250     </div>
    251   );
     229                                                >
     230                                                        {user.isFollowedByCurrentUser ? "Unfollow" : "Follow"}
     231                                                </button>
     232                                        </div>
     233                                </div>
     234
     235                                {user.userType === "ARTIST" ? (
     236                                        <ArtistView contributions={user.contributions} />
     237                                ) : (
     238                                        <ListenerView
     239                                                likedEntities={user.likedEntities}
     240                                                createdPlaylists={user.createdPlaylists}
     241                                                savedPlaylists={user.savedPlaylists}
     242                                        />
     243                                )}
     244
     245                                {showModal && (
     246                                        <UserListModal
     247                                                title={modalTitle}
     248                                                users={modalUsers}
     249                                                onClose={() => setShowModal(false)}
     250                                                onFollowToggle={handleFollowInModal}
     251                                        />
     252                                )}
     253                        </div>
     254                </div>
     255        );
    252256};
    253257
  • frontend/src/utils/types.ts

    r694fc25 rcb4a1da  
    5555
    5656export type UserRegisterType = "ARTIST" | "LISTENER";
     57
     58export type SearchCategory = "songs" | "albums" | "artists" | "users";
Note: See TracChangeset for help on using the changeset viewer.