source: frontend/src/pages/UserDetail.tsx@ 1579b4f

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

refactor artist, collection, listener components to have similar style as other components; refactor some dto-s

  • Property mode set to 100644
File size: 9.3 KB
Line 
1import { useEffect, useState } from "react";
2import { Link, useNavigate, useParams } from "react-router-dom";
3import axiosInstance, { baseURL } from "../api/axiosInstance";
4import LoadingSpinner from "../components/LoadingSpinner";
5import ArtistView from "../components/userProfile/ArtistView";
6import ListenerView from "../components/userProfile/ListenerView";
7import UserListModal from "../components/userProfile/UserListModal";
8import { useAuth } from "../context/authContext";
9import { handleError } from "../utils/error";
10import type {
11 ArtistContribution,
12 BaseNonAdminUser,
13 MusicalEntity,
14 Playlist,
15} from "../utils/types";
16
17interface FollowStatus {
18 isFollowing: boolean;
19 followerCount: number;
20 followingCount: number;
21}
22
23interface Artist extends BaseNonAdminUser {
24 userType: "ARTIST";
25 contributions: ArtistContribution[];
26}
27interface Listener extends BaseNonAdminUser {
28 userType: "LISTENER";
29 likedEntities: MusicalEntity[];
30 createdPlaylists: Playlist[];
31 savedPlaylists: Playlist[];
32}
33
34type UserProfile = Artist | Listener;
35
36const UserDetail = () => {
37 const { username: usernameParam } = useParams();
38 const { user: currentUser } = useAuth();
39 const navigate = useNavigate();
40 const [user, setUser] = useState<UserProfile | null>(null);
41 const [error, setError] = useState<string | null>(null);
42 const [showModal, setShowModal] = useState(false);
43 const [modalTitle, setModalTitle] = useState("");
44 const [modalUsers, setModalUsers] = useState<any[]>([]);
45 const [isLoadingModal, setIsLoadingModal] = useState(false);
46 const [isFollowing, setIsFollowing] = useState(false);
47
48 const username = usernameParam || currentUser?.username;
49 const isOwnProfile = currentUser?.username === username;
50
51 if (!usernameParam && !currentUser) {
52 return (
53 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
54 <div className="text-center">
55 <p className="text-red-400 text-xl mb-4">
56 You must be logged in to view your profile.
57 </p>
58 <button
59 onClick={() => navigate("/login")}
60 className="text-[#1db954] hover:underline text-sm cursor-pointer"
61 >
62 Go to Login
63 </button>
64 </div>
65 </div>
66 );
67 }
68
69 const handleFollow = async () => {
70 if (!user) return;
71
72 setIsFollowing(true);
73 try {
74 const response = await axiosInstance.post<FollowStatus>(
75 `/users/na/${username}/follow`,
76 );
77 setUser((prev) => {
78 if (!prev) return null;
79 return {
80 ...prev,
81 isFollowedByCurrentUser: response.data.isFollowing,
82 followers: response.data.followerCount,
83 following: response.data.followingCount,
84 };
85 });
86 } catch (err: any) {
87 setError(handleError(err));
88 } finally {
89 setIsFollowing(false);
90 }
91 };
92
93 const handleFollowInModal = async (targetUsername: string) => {
94 try {
95 const response = await axiosInstance.post<FollowStatus>(
96 `/users/na/${targetUsername}/follow`,
97 );
98
99 setModalUsers((prevUsers) =>
100 prevUsers.map((u) =>
101 u.username === targetUsername
102 ? { ...u, isFollowedByCurrentUser: response.data.isFollowing }
103 : u,
104 ),
105 );
106 } catch (err: any) {
107 setError(handleError(err));
108 }
109 };
110
111 const displayFollowers = async () => {
112 setIsLoadingModal(true);
113 try {
114 const response = await axiosInstance.get(
115 `/users/na/${username}/followers`,
116 );
117 setModalUsers(response.data);
118 setModalTitle("Followers");
119 setShowModal(true);
120 } catch (err) {
121 setError(handleError(err));
122 } finally {
123 setIsLoadingModal(false);
124 }
125 };
126 const displayFollowing = async () => {
127 setIsLoadingModal(true);
128 try {
129 const response = await axiosInstance.get(
130 `/users/na/${username}/following`,
131 );
132 setModalUsers(response.data);
133 setModalTitle("Following");
134 setShowModal(true);
135 } catch (err: any) {
136 setError(handleError(err));
137 } finally {
138 setIsLoadingModal(false);
139 }
140 };
141
142 useEffect(() => {
143 const fetchUser = async () => {
144 setError(null);
145 try {
146 const response = await axiosInstance.get(`/users/na/${username}`);
147 setUser(response.data);
148 } catch (err: any) {
149 setError(handleError(err));
150 }
151 };
152 fetchUser();
153 }, [username]);
154
155 if (error) {
156 return (
157 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
158 <div className="text-center">
159 <p className="text-red-400 text-xl mb-4">{error}</p>
160 <Link to="/" className="text-[#1db954] hover:underline text-sm">
161 ← Back to Home
162 </Link>
163 </div>
164 </div>
165 );
166 }
167
168 if (!user) {
169 return <LoadingSpinner />;
170 }
171
172 return (
173 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white">
174 {isLoadingModal && (
175 <div className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm flex items-center justify-center">
176 <div className="w-10 h-10 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin" />
177 </div>
178 )}
179
180 <div className="max-w-5xl mx-auto px-6 py-10">
181 {/* Back link */}
182 <Link
183 to="/"
184 className="inline-flex items-center gap-1 text-gray-400 hover:text-white text-sm mb-8 transition-colors"
185 >
186 <svg
187 className="w-4 h-4"
188 fill="none"
189 stroke="currentColor"
190 viewBox="0 0 24 24"
191 >
192 <path
193 strokeLinecap="round"
194 strokeLinejoin="round"
195 strokeWidth={2}
196 d="M15 19l-7-7 7-7"
197 />
198 </svg>
199 Back to Home
200 </Link>
201
202 {/* Hero section */}
203 <div className="flex flex-col md:flex-row gap-8 mb-10">
204 {/* Profile photo */}
205 <div className="w-full md:w-48 shrink-0">
206 <div className="relative w-48 h-48 rounded-full overflow-hidden shadow-2xl bg-[#181818] mx-auto md:mx-0">
207 {user.profilePhoto ? (
208 <img
209 src={`${baseURL}/${user.profilePhoto}`}
210 alt={user.fullName}
211 className="w-full h-full object-cover"
212 />
213 ) : (
214 <div className="w-full h-full bg-linear-to-br from-[#1db954] to-[#1ed760] flex items-center justify-center text-white text-5xl font-bold">
215 {user.fullName.charAt(0).toUpperCase()}
216 </div>
217 )}
218 </div>
219 </div>
220
221 {/* User info */}
222 <div className="flex flex-col justify-end gap-3 min-w-0">
223 <span className="text-xs uppercase tracking-widest text-gray-400 font-medium">
224 {user.userType === "ARTIST" ? "Artist" : "Listener"} • Profile
225 </span>
226 <h1 className="text-4xl md:text-5xl font-extrabold leading-tight">
227 {user.fullName}
228 </h1>
229 <p className="text-gray-400">@{user.username}</p>
230
231 {/* Stats */}
232 <div className="flex items-center gap-6 mt-2">
233 <div
234 className={`${user.userType === "LISTENER" ? "cursor-pointer hover:text-white" : ""} transition-colors`}
235 onClick={
236 user.userType === "LISTENER" ? displayFollowers : undefined
237 }
238 >
239 <span className="text-xl font-bold text-white">
240 {user.followers}
241 </span>
242 <span className="text-sm text-gray-400 ml-1">Followers</span>
243 </div>
244 <div
245 className={`${user.userType === "LISTENER" ? "cursor-pointer hover:text-white" : ""} transition-colors`}
246 onClick={
247 user.userType === "LISTENER" ? displayFollowing : undefined
248 }
249 >
250 <span className="text-xl font-bold text-white">
251 {user.following}
252 </span>
253 <span className="text-sm text-gray-400 ml-1">Following</span>
254 </div>
255 </div>
256
257 {/* Follow button - hidden on own profile */}
258 {!isOwnProfile && (
259 <div className="mt-4">
260 <button
261 onClick={handleFollow}
262 disabled={isFollowing}
263 className={`flex items-center gap-2 px-6 py-3 rounded-full text-sm font-semibold transition-all cursor-pointer ${
264 isFollowing
265 ? "bg-gray-700 text-gray-400 cursor-not-allowed"
266 : user.isFollowedByCurrentUser
267 ? "bg-white/10 text-white hover:bg-white/20"
268 : "bg-[#1db954] text-black hover:bg-[#1ed760] hover:scale-105"
269 }`}
270 >
271 {user.isFollowedByCurrentUser ? (
272 <>
273 <svg
274 className="w-5 h-5"
275 fill="none"
276 stroke="currentColor"
277 viewBox="0 0 24 24"
278 >
279 <path
280 strokeLinecap="round"
281 strokeLinejoin="round"
282 strokeWidth={2}
283 d="M5 13l4 4L19 7"
284 />
285 </svg>
286 Following
287 </>
288 ) : (
289 <>
290 <svg
291 className="w-5 h-5"
292 fill="none"
293 stroke="currentColor"
294 viewBox="0 0 24 24"
295 >
296 <path
297 strokeLinecap="round"
298 strokeLinejoin="round"
299 strokeWidth={2}
300 d="M12 4v16m8-8H4"
301 />
302 </svg>
303 Follow
304 </>
305 )}
306 </button>
307 </div>
308 )}
309 </div>
310 </div>
311
312 {/* Content */}
313 {user.userType === "ARTIST" ? (
314 <ArtistView contributions={user.contributions} />
315 ) : (
316 <ListenerView
317 likedEntities={user.likedEntities}
318 createdPlaylists={user.createdPlaylists}
319 savedPlaylists={user.savedPlaylists}
320 />
321 )}
322
323 {showModal && (
324 <UserListModal
325 title={modalTitle}
326 users={modalUsers}
327 onClose={() => setShowModal(false)}
328 onFollowToggle={handleFollowInModal}
329 />
330 )}
331 </div>
332 </div>
333 );
334};
335
336export default UserDetail;
Note: See TracBrowser for help on using the repository browser.