source: frontend/src/pages/UserDetail.tsx@ 85512ff

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

save playlist

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