source: frontend/src/pages/UserDetail.tsx@ 0792d02

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

show saved playlists by user

  • Property mode set to 100644
File size: 8.0 KB
RevLine 
[4e5cf92]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";
[2b08bed]6import UserListModal from "../components/userProfile/UserListModal";
[2730163]7import { handleError } from "../utils/error";
[4e5cf92]8import type {
[98992cf]9 MusicalEntity,
[6de2873]10 Playlist,
[98992cf]11 ArtistContribution,
[2b08bed]12 BaseNonAdminUser,
[4e5cf92]13} from "../utils/types";
14
[d2af1a6]15interface FollowStatus {
16 isFollowing: boolean;
17 followerCount: number;
18 followingCount: number;
19}
20
[2b08bed]21interface Artist extends BaseNonAdminUser {
[98992cf]22 userType: "ARTIST";
23 contributions: ArtistContribution[];
[4e5cf92]24}
[2b08bed]25interface Listener extends BaseNonAdminUser {
[98992cf]26 userType: "LISTENER";
27 likedEntities: MusicalEntity[];
28 createdPlaylists: Playlist[];
[0792d02]29 savedPlaylists: Playlist[];
[98992cf]30}
31
32type UserProfile = Artist | Listener;
[4e5cf92]33
34const UserDetail = () => {
[6de2873]35 // user refers to the selected user NOT to the user from context
[2b08bed]36 const baseURL = import.meta.env.VITE_API_BASE_URL;
[0792d02]37 const { username } = useParams();
[6de2873]38 const navigate = useNavigate();
[98992cf]39 const [user, setUser] = useState<UserProfile | null>(null);
[6de2873]40 const [error, setError] = useState<string | null>(null);
[2b08bed]41 const [showModal, setShowModal] = useState(false);
42 const [modalTitle, setModalTitle] = useState("");
43 const [modalUsers, setModalUsers] = useState<any[]>([]);
44 const [isLoadingModal, setIsLoadingModal] = useState(false);
[6de2873]45 const [isFollowing, setIsFollowing] = useState(false);
46
47 const handleFollow = async () => {
48 if (!user) return;
49
50 setIsFollowing(true);
51 try {
[d2af1a6]52 const response = await axiosInstance.post<FollowStatus>(
[0792d02]53 `/users/${username}/follow`,
[6de2873]54 );
[d2af1a6]55 setUser((prev) => {
56 if (!prev) return null;
57 return {
58 ...prev,
59 isFollowedByCurrentUser: response.data.isFollowing,
60 followers: response.data.followerCount,
61 following: response.data.followingCount,
62 };
63 });
[6de2873]64 } catch (err: any) {
[2730163]65 setError(handleError(err));
[6de2873]66 } finally {
67 setIsFollowing(false);
68 }
69 };
70
[0792d02]71 const handleFollowInModal = async (targetUsername: string) => {
[d2af1a6]72 try {
73 const response = await axiosInstance.post<FollowStatus>(
[0792d02]74 `/users/${username}/follow`,
[d2af1a6]75 );
76
77 setModalUsers((prevUsers) =>
78 prevUsers.map((u) =>
[0792d02]79 u.username === targetUsername
[d2af1a6]80 ? { ...u, isFollowedByCurrentUser: response.data.isFollowing }
81 : u,
82 ),
83 );
84
85 // if (user && user.id === targetId) {
86 // setUser((prev) => {
87 // if (!prev) return null;
88 // return {
89 // ...prev,
90 // isFollowedByCurrentUser: response.data.isFollowing,
91 // followers: response.data.followerCount,
92 // };
93 // });
94 // }
95 } catch (err: any) {
96 setError(handleError(err));
97 }
98 };
99
[2b08bed]100 const displayFollowers = async () => {
101 setIsLoadingModal(true);
102 try {
[0792d02]103 const response = await axiosInstance.get(`/users/${username}/followers`);
[2b08bed]104 setModalUsers(response.data);
105 setModalTitle("Followers");
106 setShowModal(true);
107 } catch (err) {
[2730163]108 setError(handleError(err));
[2b08bed]109 } finally {
110 setIsLoadingModal(false);
111 }
112 };
113 const displayFollowing = async () => {
114 setIsLoadingModal(true);
115 try {
[0792d02]116 const response = await axiosInstance.get(`/users/${username}/following`);
[2b08bed]117 setModalUsers(response.data);
118 setModalTitle("Following");
119 setShowModal(true);
[2730163]120 } catch (err: any) {
121 setError(handleError(err));
[2b08bed]122 } finally {
123 setIsLoadingModal(false);
124 }
125 };
126
[6de2873]127 useEffect(() => {
128 const fetchUser = async () => {
129 setError(null);
130 try {
[0792d02]131 const response = await axiosInstance.get(`/users/${username}`);
[d2af1a6]132 console.log(response.data);
[6de2873]133 setUser(response.data);
134 } catch (err: any) {
[2730163]135 setError(handleError(err));
[6de2873]136 }
137 };
138 fetchUser();
[0792d02]139 }, [username]);
[6de2873]140
141 if (error) {
142 return (
143 <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg">
144 <h2 className="font-bold">Error</h2>
145 <p>{error}</p>
146 </div>
147 );
148 }
149
150 if (!user) return <div className="p-6">Loading...</div>;
151
152 return (
153 <div className="container mx-auto p-6">
[2b08bed]154 {isLoadingModal && (
155 <div className="fixed inset-0 z-40 bg-black/30 backdrop-blur-sm flex items-center justify-center">
156 <div className="flex items-center gap-3">
157 <div className="w-6 h-6 border-3 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
158 </div>
159 </div>
160 )}
[6de2873]161 <button
162 onClick={() => navigate(-1)}
[2730163]163 className="mb-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors duration-200 cursor-pointer"
[6de2873]164 >
165 ← Back
166 </button>
167
168 <div className="bg-white shadow-lg rounded-lg p-8">
169 <div className="flex items-start gap-6 mb-8">
170 <div className="shrink-0">
[2b08bed]171 <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">
172 {user.profilePhoto ? (
173 <img
174 src={`${baseURL}/${user.profilePhoto}`}
175 alt={user.fullName}
176 className="w-full h-full object-cover"
177 />
178 ) : (
179 user.fullName.charAt(0).toUpperCase()
180 )}
[6de2873]181 </div>
182 </div>
183
184 <div className="flex-1">
185 <h1 className="text-4xl font-bold mb-2">{user.fullName}</h1>
186 <span className="inline-block px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium mb-4">
187 {user.userType}
188 </span>
189
190 <div className="flex gap-6 mb-4 text-gray-700">
[2b08bed]191 <div
192 className={`flex flex-col ${user.userType == "LISTENER" ? "cursor-pointer" : "cursor-default"}`}
193 onClick={
194 user.userType === "LISTENER" ? displayFollowers : undefined
195 }
196 >
[6de2873]197 <span className="text-2xl font-bold">{user.followers}</span>
198 <span className="text-sm text-gray-500">Followers</span>
199 </div>
[2b08bed]200 <div
201 className={`flex flex-col ${user.userType == "LISTENER" ? "cursor-pointer" : "cursor-default"}`}
202 onClick={
203 user.userType === "LISTENER" ? displayFollowing : undefined
204 }
205 >
[6de2873]206 <span className="text-2xl font-bold">{user.following}</span>
207 <span className="text-sm text-gray-500">Following</span>
208 </div>
209 </div>
210
211 <button
212 onClick={handleFollow}
213 disabled={isFollowing}
214 className={`
215 px-6 py-2 font-semibold rounded-lg shadow-md
216 transition-colors duration-200
217 ${
218 isFollowing
219 ? "bg-gray-400 text-gray-200 cursor-not-allowed"
220 : user.isFollowedByCurrentUser
221 ? "bg-gray-200 text-gray-700 hover:bg-gray-300 cursor-pointer"
222 : "bg-blue-500 text-white hover:bg-blue-600 cursor-pointer"
223 }
224 `}
225 >
226 {user.isFollowedByCurrentUser ? "Unfollow" : "Follow"}
227 </button>
228 </div>
229 </div>
230
[98992cf]231 {user.userType === "ARTIST" ? (
232 <ArtistView contributions={user.contributions} />
233 ) : (
[6de2873]234 <ListenerView
[98992cf]235 likedEntities={user.likedEntities}
[0792d02]236 createdPlaylists={user.createdPlaylists}
237 savedPlaylists={user.savedPlaylists}
[6de2873]238 />
239 )}
[2b08bed]240
241 {showModal && (
242 <UserListModal
243 title={modalTitle}
244 users={modalUsers}
245 onClose={() => setShowModal(false)}
246 onFollowToggle={handleFollowInModal}
247 />
248 )}
[6de2873]249 </div>
250 </div>
251 );
[4e5cf92]252};
253
254export default UserDetail;
Note: See TracBrowser for help on using the repository browser.