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

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

add route for viewing own profile

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