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