source: frontend/src/pages/UserDetail.tsx@ cb4a1da

main
Last change on this file since cb4a1da was cb4a1da, checked in by Filip Gavrilovski <filipgavrilovski28@…>, 5 months ago

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

  • Property mode set to 100644
File size: 7.2 KB
Line 
1import { useEffect, useState } from "react";
2import { useNavigate, useParams } from "react-router-dom";
3import axiosInstance from "../api/axiosInstance";
4import ArtistView from "../components/userProfile/ArtistView";
5import ListenerView from "../components/userProfile/ListenerView";
6import UserListModal from "../components/userProfile/UserListModal";
7import { handleError } from "../utils/error";
8import type {
9 ArtistContribution,
10 BaseNonAdminUser,
11 MusicalEntity,
12 Playlist,
13} from "../utils/types";
14
15interface FollowStatus {
16 isFollowing: boolean;
17 followerCount: number;
18 followingCount: number;
19}
20
21interface Artist extends BaseNonAdminUser {
22 userType: "ARTIST";
23 contributions: ArtistContribution[];
24}
25interface Listener extends BaseNonAdminUser {
26 userType: "LISTENER";
27 likedEntities: MusicalEntity[];
28 createdPlaylists: Playlist[];
29 savedPlaylists: Playlist[];
30}
31
32type UserProfile = Artist | Listener;
33
34const 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/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={`
219 px-6 py-2 font-semibold rounded-lg shadow-md
220 transition-colors duration-200
221 ${
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 }
228 `}
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 );
256};
257
258export default UserDetail;
Note: See TracBrowser for help on using the repository browser.