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

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

add loading spinner; don't show sidebar for unauthenticated users

  • Property mode set to 100644
File size: 8.2 KB
Line 
1import { useEffect, useState } from "react";
2import { useNavigate, useParams } from "react-router-dom";
3import axiosInstance 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 // user refers to the selected user NOT to the user from context
38 const baseURL = import.meta.env.VITE_API_BASE_URL;
39 const { username: usernameParam } = useParams();
40 // sintaksava dole znaci zemi go user od auth context i preimenuvaj go vo currentUser, za da ne se izmesa so user-ot dole
41 const { user: currentUser } = useAuth();
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 // determine which username to use: URL param or current user's username
52 const username = usernameParam || currentUser?.username;
53
54 // if we're on /me route and no user is logged in, show error
55 if (!usernameParam && !currentUser) {
56 return (
57 <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg">
58 <h2 className="font-bold">Authentication Required</h2>
59 <p>You must be logged in to view your profile.</p>
60 <button
61 onClick={() => navigate("/login")}
62 className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors duration-200 cursor-pointer"
63 >
64 Go to Login
65 </button>
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(handleError(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/${username}/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
108 // if (user && user.id === targetId) {
109 // setUser((prev) => {
110 // if (!prev) return null;
111 // return {
112 // ...prev,
113 // isFollowedByCurrentUser: response.data.isFollowing,
114 // followers: response.data.followerCount,
115 // };
116 // });
117 // }
118 } catch (err: any) {
119 setError(handleError(err));
120 }
121 };
122
123 const displayFollowers = async () => {
124 setIsLoadingModal(true);
125 try {
126 const response = await axiosInstance.get(
127 `/users/na/${username}/followers`,
128 );
129 setModalUsers(response.data);
130 setModalTitle("Followers");
131 setShowModal(true);
132 } catch (err) {
133 setError(handleError(err));
134 } finally {
135 setIsLoadingModal(false);
136 }
137 };
138 const displayFollowing = async () => {
139 setIsLoadingModal(true);
140 try {
141 const response = await axiosInstance.get(
142 `/users/na/${username}/following`,
143 );
144 setModalUsers(response.data);
145 setModalTitle("Following");
146 setShowModal(true);
147 } catch (err: any) {
148 setError(handleError(err));
149 } finally {
150 setIsLoadingModal(false);
151 }
152 };
153
154 useEffect(() => {
155 const fetchUser = async () => {
156 setError(null);
157 try {
158 const response = await axiosInstance.get(`/users/na/${username}`);
159 console.log(response.data);
160 setUser(response.data);
161 } catch (err: any) {
162 setError(handleError(err));
163 }
164 };
165 fetchUser();
166 }, [username]);
167
168 if (error) {
169 return (
170 <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg">
171 <h2 className="font-bold">Error</h2>
172 <p>{error}</p>
173 </div>
174 );
175 }
176
177 if (!user) {
178 return <LoadingSpinner />;
179 }
180
181 return (
182 <div className="container mx-auto p-6">
183 {isLoadingModal && (
184 <div className="fixed inset-0 z-40 bg-black/30 backdrop-blur-sm flex items-center justify-center">
185 <div className="flex items-center gap-3">
186 <div className="w-6 h-6 border-3 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
187 </div>
188 </div>
189 )}
190 <button
191 onClick={() => navigate(-1)}
192 className="mb-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors duration-200 cursor-pointer"
193 >
194 ← Back
195 </button>
196
197 <div className="bg-white shadow-lg rounded-lg p-8">
198 <div className="flex items-start gap-6 mb-8">
199 <div className="shrink-0">
200 <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">
201 {user.profilePhoto ? (
202 <img
203 src={`${baseURL}/${user.profilePhoto}`}
204 alt={user.fullName}
205 className="w-full h-full object-cover"
206 />
207 ) : (
208 user.fullName.charAt(0).toUpperCase()
209 )}
210 </div>
211 </div>
212
213 <div className="flex-1">
214 <h1 className="text-4xl font-bold mb-2">{user.fullName}</h1>
215 <span className="inline-block px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium mb-4">
216 {user.userType}
217 </span>
218
219 <div className="flex gap-6 mb-4 text-gray-700">
220 <div
221 className={`flex flex-col ${user.userType == "LISTENER" ? "cursor-pointer" : "cursor-default"}`}
222 onClick={
223 user.userType === "LISTENER" ? displayFollowers : undefined
224 }
225 >
226 <span className="text-2xl font-bold">{user.followers}</span>
227 <span className="text-sm text-gray-500">Followers</span>
228 </div>
229 <div
230 className={`flex flex-col ${user.userType == "LISTENER" ? "cursor-pointer" : "cursor-default"}`}
231 onClick={
232 user.userType === "LISTENER" ? displayFollowing : undefined
233 }
234 >
235 <span className="text-2xl font-bold">{user.following}</span>
236 <span className="text-sm text-gray-500">Following</span>
237 </div>
238 </div>
239
240 <button
241 onClick={handleFollow}
242 disabled={isFollowing}
243 className={`
244 px-6 py-2 font-semibold rounded-lg shadow-md
245 transition-colors duration-200
246 ${
247 isFollowing
248 ? "bg-gray-400 text-gray-200 cursor-not-allowed"
249 : user.isFollowedByCurrentUser
250 ? "bg-gray-200 text-gray-700 hover:bg-gray-300 cursor-pointer"
251 : "bg-blue-500 text-white hover:bg-blue-600 cursor-pointer"
252 }
253 `}
254 >
255 {user.isFollowedByCurrentUser ? "Unfollow" : "Follow"}
256 </button>
257 </div>
258 </div>
259
260 {user.userType === "ARTIST" ? (
261 <ArtistView contributions={user.contributions} />
262 ) : (
263 <ListenerView
264 likedEntities={user.likedEntities}
265 createdPlaylists={user.createdPlaylists}
266 savedPlaylists={user.savedPlaylists}
267 />
268 )}
269
270 {showModal && (
271 <UserListModal
272 title={modalTitle}
273 users={modalUsers}
274 onClose={() => setShowModal(false)}
275 onFollowToggle={handleFollowInModal}
276 />
277 )}
278 </div>
279 </div>
280 );
281};
282
283export default UserDetail;
Note: See TracBrowser for help on using the repository browser.