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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • 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
Note: See TracChangeset for help on using the changeset viewer.