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

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

add song/album to favourites

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