source: frontend/src/pages/UserDetail.tsx@ 615dcee

main
Last change on this file since 615dcee was 615dcee, checked in by Dimitar Arsov <dimitararsov04@…>, 5 months ago

hide actions from unauthenticated users

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