Changeset 85512ff for frontend/src/pages/UserDetail.tsx
- Timestamp:
- 02/14/26 18:21:18 (5 months ago)
- Branches:
- main
- Children:
- ce45c7a
- Parents:
- f5bc95e
- File:
-
- 1 edited
-
frontend/src/pages/UserDetail.tsx (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
frontend/src/pages/UserDetail.tsx
rf5bc95e r85512ff 7 7 import UserListModal from "../components/userProfile/UserListModal"; 8 8 import { useAuth } from "../context/authContext"; 9 import { handleError} from "../utils/error";9 import { getErrorMessage } from "../utils/error"; 10 10 import type { 11 ArtistContribution,12 BaseNonAdminUser,13 MusicalEntity,14 Playlist,11 ArtistContribution, 12 BaseNonAdminUser, 13 MusicalEntity, 14 Playlist, 15 15 } from "../utils/types"; 16 import { useCreatedPlaylists } from "../context/playlistContext"; 16 17 17 18 interface FollowStatus { 18 isFollowing: boolean;19 followerCount: number;20 followingCount: number;19 isFollowing: boolean; 20 followerCount: number; 21 followingCount: number; 21 22 } 22 23 23 24 interface Artist extends BaseNonAdminUser { 24 userType: "ARTIST";25 contributions: ArtistContribution[];25 userType: "ARTIST"; 26 contributions: ArtistContribution[]; 26 27 } 27 28 interface Listener extends BaseNonAdminUser { 28 userType: "LISTENER";29 likedEntities: MusicalEntity[];30 createdPlaylists: Playlist[];31 savedPlaylists: Playlist[];29 userType: "LISTENER"; 30 likedEntities: MusicalEntity[]; 31 createdPlaylists: Playlist[]; 32 savedPlaylists: Playlist[]; 32 33 } 33 34 … … 35 36 36 37 const UserDetail = () => { 37 const { username: usernameParam } = useParams(); 38 const { user: currentUser } = useAuth(); 39 const navigate = useNavigate(); 40 const [user, setUser] = useState<UserProfile | null>(null); 41 const [error, setError] = useState<string | null>(null); 42 const [showModal, setShowModal] = useState(false); 43 const [modalTitle, setModalTitle] = useState(""); 44 const [modalUsers, setModalUsers] = useState<any[]>([]); 45 const [isLoadingModal, setIsLoadingModal] = useState(false); 46 const [isFollowing, setIsFollowing] = useState(false); 47 48 const username = usernameParam || currentUser?.username; 49 const isOwnProfile = currentUser?.username === username; 50 51 if (!usernameParam && !currentUser) { 52 return ( 53 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center"> 54 <div className="text-center"> 55 <p className="text-red-400 text-xl mb-4"> 56 You must be logged in to view your profile. 57 </p> 58 <button 59 onClick={() => navigate("/login")} 60 className="text-[#1db954] hover:underline text-sm cursor-pointer" 61 > 62 Go to Login 63 </button> 64 </div> 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/${targetUsername}/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 } catch (err: any) { 107 setError(handleError(err)); 108 } 109 }; 110 111 const displayFollowers = async () => { 112 setIsLoadingModal(true); 113 try { 114 const response = await axiosInstance.get( 115 `/users/na/${username}/followers`, 116 ); 117 setModalUsers(response.data); 118 setModalTitle("Followers"); 119 setShowModal(true); 120 } catch (err) { 121 setError(handleError(err)); 122 } finally { 123 setIsLoadingModal(false); 124 } 125 }; 126 const displayFollowing = async () => { 127 setIsLoadingModal(true); 128 try { 129 const response = await axiosInstance.get( 130 `/users/na/${username}/following`, 131 ); 132 setModalUsers(response.data); 133 setModalTitle("Following"); 134 setShowModal(true); 135 } catch (err: any) { 136 setError(handleError(err)); 137 } finally { 138 setIsLoadingModal(false); 139 } 140 }; 141 142 useEffect(() => { 143 const fetchUser = async () => { 144 setError(null); 145 try { 146 const response = await axiosInstance.get(`/users/na/${username}`); 147 setUser(response.data); 148 } catch (err: any) { 149 setError(handleError(err)); 150 } 151 }; 152 fetchUser(); 153 }, [username]); 154 155 if (error) { 156 return ( 157 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center"> 158 <div className="text-center"> 159 <p className="text-red-400 text-xl mb-4">{error}</p> 160 <Link to="/" className="text-[#1db954] hover:underline text-sm"> 161 ← Back to Home 162 </Link> 163 </div> 164 </div> 165 ); 166 } 167 168 if (!user) { 169 return <LoadingSpinner />; 170 } 171 172 return ( 173 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white"> 174 {isLoadingModal && ( 175 <div className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm flex items-center justify-center"> 176 <div className="w-10 h-10 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin" /> 177 </div> 178 )} 179 180 <div className="max-w-5xl mx-auto px-6 py-10"> 181 {/* Back link */} 182 <Link 183 to="/" 184 className="inline-flex items-center gap-1 text-gray-400 hover:text-white text-sm mb-8 transition-colors" 185 > 186 <svg 187 className="w-4 h-4" 188 fill="none" 189 stroke="currentColor" 190 viewBox="0 0 24 24" 191 > 192 <path 193 strokeLinecap="round" 194 strokeLinejoin="round" 195 strokeWidth={2} 196 d="M15 19l-7-7 7-7" 197 /> 198 </svg> 199 Back to Home 200 </Link> 201 202 {/* Hero section */} 203 <div className="flex flex-col md:flex-row gap-8 mb-10"> 204 {/* Profile photo */} 205 <div className="w-full md:w-48 shrink-0"> 206 <div className="relative w-48 h-48 rounded-full overflow-hidden shadow-2xl bg-[#181818] mx-auto md:mx-0"> 207 {user.profilePhoto ? ( 208 <img 209 src={`${baseURL}/${user.profilePhoto}`} 210 alt={user.fullName} 211 className="w-full h-full object-cover" 212 /> 213 ) : ( 214 <div className="w-full h-full bg-linear-to-br from-[#1db954] to-[#1ed760] flex items-center justify-center text-white text-5xl font-bold"> 215 {user.fullName.charAt(0).toUpperCase()} 216 </div> 217 )} 218 </div> 219 </div> 220 221 {/* User info */} 222 <div className="flex flex-col justify-end gap-3 min-w-0"> 223 <span className="text-xs uppercase tracking-widest text-gray-400 font-medium"> 224 {user.userType === "ARTIST" ? "Artist" : "Listener"} • Profile 225 </span> 226 <h1 className="text-4xl md:text-5xl font-extrabold leading-tight"> 227 {user.fullName} 228 </h1> 229 <p className="text-gray-400">@{user.username}</p> 230 231 {/* Stats */} 232 <div className="flex items-center gap-6 mt-2"> 233 <div 234 className={`${user.userType === "LISTENER" ? "cursor-pointer hover:text-white" : ""} transition-colors`} 235 onClick={ 236 user.userType === "LISTENER" ? displayFollowers : undefined 237 } 238 > 239 <span className="text-xl font-bold text-white"> 240 {user.followers} 241 </span> 242 <span className="text-sm text-gray-400 ml-1">Followers</span> 243 </div> 244 <div 245 className={`${user.userType === "LISTENER" ? "cursor-pointer hover:text-white" : ""} transition-colors`} 246 onClick={ 247 user.userType === "LISTENER" ? displayFollowing : undefined 248 } 249 > 250 <span className="text-xl font-bold text-white"> 251 {user.following} 252 </span> 253 <span className="text-sm text-gray-400 ml-1">Following</span> 254 </div> 255 </div> 256 257 {/* Follow button - hidden on own profile */} 258 {!isOwnProfile && ( 259 <div className="mt-4"> 260 <button 261 onClick={handleFollow} 262 disabled={isFollowing} 263 className={`flex items-center gap-2 px-6 py-3 rounded-full text-sm font-semibold transition-all cursor-pointer ${ 264 isFollowing 265 ? "bg-gray-700 text-gray-400 cursor-not-allowed" 266 : user.isFollowedByCurrentUser 267 ? "bg-white/10 text-white hover:bg-white/20" 268 : "bg-[#1db954] text-black hover:bg-[#1ed760] hover:scale-105" 269 }`} 270 > 271 {user.isFollowedByCurrentUser ? ( 272 <> 273 <svg 274 className="w-5 h-5" 275 fill="none" 276 stroke="currentColor" 277 viewBox="0 0 24 24" 278 > 279 <path 280 strokeLinecap="round" 281 strokeLinejoin="round" 282 strokeWidth={2} 283 d="M5 13l4 4L19 7" 284 /> 285 </svg> 286 Following 287 </> 288 ) : ( 289 <> 290 <svg 291 className="w-5 h-5" 292 fill="none" 293 stroke="currentColor" 294 viewBox="0 0 24 24" 295 > 296 <path 297 strokeLinecap="round" 298 strokeLinejoin="round" 299 strokeWidth={2} 300 d="M12 4v16m8-8H4" 301 /> 302 </svg> 303 Follow 304 </> 305 )} 306 </button> 307 </div> 308 )} 309 </div> 310 </div> 311 312 {/* Content */} 313 {user.userType === "ARTIST" ? ( 314 <ArtistView contributions={user.contributions} /> 315 ) : ( 316 <ListenerView 317 likedEntities={user.likedEntities} 318 createdPlaylists={user.createdPlaylists} 319 savedPlaylists={user.savedPlaylists} 320 /> 321 )} 322 323 {showModal && ( 324 <UserListModal 325 title={modalTitle} 326 users={modalUsers} 327 onClose={() => setShowModal(false)} 328 onFollowToggle={handleFollowInModal} 329 /> 330 )} 331 </div> 332 </div> 333 ); 38 const { username: usernameParam } = useParams(); 39 const { user: currentUser } = useAuth(); 40 const { createdPlaylists: currentUserCreatedPlaylists } = 41 useCreatedPlaylists(); 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 const username = usernameParam || currentUser?.username; 52 const isOwnProfile = currentUser?.username === username; 53 54 if (!usernameParam && !currentUser) { 55 return ( 56 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center"> 57 <div className="text-center"> 58 <p className="text-red-400 text-xl mb-4"> 59 You must be logged in to view your profile. 60 </p> 61 <button 62 onClick={() => navigate("/login")} 63 className="text-[#1db954] hover:underline text-sm cursor-pointer" 64 > 65 Go to Login 66 </button> 67 </div> 68 </div> 69 ); 70 } 71 72 const handleFollow = async () => { 73 if (!user) return; 74 75 setIsFollowing(true); 76 try { 77 const response = await axiosInstance.post<FollowStatus>( 78 `/users/na/${username}/follow`, 79 ); 80 setUser((prev) => { 81 if (!prev) return null; 82 return { 83 ...prev, 84 isFollowedByCurrentUser: response.data.isFollowing, 85 followers: response.data.followerCount, 86 following: response.data.followingCount, 87 }; 88 }); 89 } catch (err: any) { 90 setError(getErrorMessage(err)); 91 } finally { 92 setIsFollowing(false); 93 } 94 }; 95 96 const handleFollowInModal = async (targetUsername: string) => { 97 try { 98 const response = await axiosInstance.post<FollowStatus>( 99 `/users/na/${targetUsername}/follow`, 100 ); 101 102 setModalUsers((prevUsers) => 103 prevUsers.map((u) => 104 u.username === targetUsername 105 ? { ...u, isFollowedByCurrentUser: response.data.isFollowing } 106 : u, 107 ), 108 ); 109 } catch (err: any) { 110 setError(getErrorMessage(err)); 111 } 112 }; 113 114 const displayFollowers = async () => { 115 setIsLoadingModal(true); 116 try { 117 const response = await axiosInstance.get( 118 `/users/na/${username}/followers`, 119 ); 120 setModalUsers(response.data); 121 setModalTitle("Followers"); 122 setShowModal(true); 123 } catch (err) { 124 setError(getErrorMessage(err)); 125 } finally { 126 setIsLoadingModal(false); 127 } 128 }; 129 const displayFollowing = async () => { 130 setIsLoadingModal(true); 131 try { 132 const response = await axiosInstance.get( 133 `/users/na/${username}/following`, 134 ); 135 setModalUsers(response.data); 136 setModalTitle("Following"); 137 setShowModal(true); 138 } catch (err: any) { 139 setError(getErrorMessage(err)); 140 } finally { 141 setIsLoadingModal(false); 142 } 143 }; 144 145 useEffect(() => { 146 const fetchUser = async () => { 147 setError(null); 148 setUser(null); 149 try { 150 const response = await axiosInstance.get(`/users/na/${username}`); 151 152 setUser(response.data); 153 } catch (err: any) { 154 setError(getErrorMessage(err)); 155 } 156 }; 157 fetchUser(); 158 }, [username]); 159 160 if (error) { 161 return ( 162 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center"> 163 <div className="text-center"> 164 <p className="text-red-400 text-xl mb-4">{error}</p> 165 <Link to="/" className="text-[#1db954] hover:underline text-sm"> 166 ← Back to Home 167 </Link> 168 </div> 169 </div> 170 ); 171 } 172 173 if (!user) { 174 return <LoadingSpinner />; 175 } 176 177 return ( 178 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white"> 179 {isLoadingModal && ( 180 <div className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm flex items-center justify-center"> 181 <div className="w-10 h-10 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin" /> 182 </div> 183 )} 184 185 <div className="max-w-5xl mx-auto px-6 py-10"> 186 {/* Back link */} 187 <Link 188 to="/" 189 className="inline-flex items-center gap-1 text-gray-400 hover:text-white text-sm mb-8 transition-colors" 190 > 191 <svg 192 className="w-4 h-4" 193 fill="none" 194 stroke="currentColor" 195 viewBox="0 0 24 24" 196 > 197 <path 198 strokeLinecap="round" 199 strokeLinejoin="round" 200 strokeWidth={2} 201 d="M15 19l-7-7 7-7" 202 /> 203 </svg> 204 Back to Home 205 </Link> 206 207 {/* Hero section */} 208 <div className="flex flex-col md:flex-row gap-8 mb-10"> 209 {/* Profile photo */} 210 <div className="w-full md:w-48 shrink-0"> 211 <div className="relative w-48 h-48 rounded-full overflow-hidden shadow-2xl bg-[#181818] mx-auto md:mx-0"> 212 {user.profilePhoto ? ( 213 <img 214 src={`${baseURL}/${user.profilePhoto}`} 215 alt={user.fullName} 216 className="w-full h-full object-cover" 217 /> 218 ) : ( 219 <div className="w-full h-full bg-linear-to-br from-[#1db954] to-[#1ed760] flex items-center justify-center text-white text-5xl font-bold"> 220 {user.fullName.charAt(0).toUpperCase()} 221 </div> 222 )} 223 </div> 224 </div> 225 226 {/* User info */} 227 <div className="flex flex-col justify-end gap-3 min-w-0"> 228 <span className="text-xs uppercase tracking-widest text-gray-400 font-medium"> 229 {user.userType === "ARTIST" ? "Artist" : "Listener"} • Profile 230 </span> 231 <h1 className="text-4xl md:text-5xl font-extrabold leading-tight"> 232 {user.fullName} 233 </h1> 234 <p className="text-gray-400">@{user.username}</p> 235 236 {/* Stats */} 237 <div className="flex items-center gap-6 mt-2"> 238 <div 239 className={`${user.userType === "LISTENER" ? "cursor-pointer hover:text-white" : ""} transition-colors`} 240 onClick={ 241 user.userType === "LISTENER" ? displayFollowers : undefined 242 } 243 > 244 <span className="text-xl font-bold text-white"> 245 {user.followers} 246 </span> 247 <span className="text-sm text-gray-400 ml-1">Followers</span> 248 </div> 249 <div 250 className={`${user.userType === "LISTENER" ? "cursor-pointer hover:text-white" : ""} transition-colors`} 251 onClick={ 252 user.userType === "LISTENER" ? displayFollowing : undefined 253 } 254 > 255 <span className="text-xl font-bold text-white"> 256 {user.following} 257 </span> 258 <span className="text-sm text-gray-400 ml-1">Following</span> 259 </div> 260 </div> 261 262 {/* Follow button - hidden on own profile */} 263 {!isOwnProfile && ( 264 <div className="mt-4"> 265 <button 266 onClick={handleFollow} 267 disabled={isFollowing} 268 className={`flex items-center gap-2 px-6 py-3 rounded-full text-sm font-semibold transition-all cursor-pointer ${ 269 isFollowing 270 ? "bg-gray-700 text-gray-400 cursor-not-allowed" 271 : user.isFollowedByCurrentUser 272 ? "bg-white/10 text-white hover:bg-white/20" 273 : "bg-[#1db954] text-black hover:bg-[#1ed760] hover:scale-105" 274 }`} 275 > 276 {user.isFollowedByCurrentUser ? ( 277 <> 278 <svg 279 className="w-5 h-5" 280 fill="none" 281 stroke="currentColor" 282 viewBox="0 0 24 24" 283 > 284 <path 285 strokeLinecap="round" 286 strokeLinejoin="round" 287 strokeWidth={2} 288 d="M5 13l4 4L19 7" 289 /> 290 </svg> 291 Following 292 </> 293 ) : ( 294 <> 295 <svg 296 className="w-5 h-5" 297 fill="none" 298 stroke="currentColor" 299 viewBox="0 0 24 24" 300 > 301 <path 302 strokeLinecap="round" 303 strokeLinejoin="round" 304 strokeWidth={2} 305 d="M12 4v16m8-8H4" 306 /> 307 </svg> 308 Follow 309 </> 310 )} 311 </button> 312 </div> 313 )} 314 </div> 315 </div> 316 317 {/* Content */} 318 {user.userType === "ARTIST" ? ( 319 <ArtistView contributions={user.contributions} /> 320 ) : ( 321 <ListenerView 322 likedEntities={user.likedEntities} 323 createdPlaylists={user.createdPlaylists} 324 savedPlaylists={user.savedPlaylists} 325 /> 326 )} 327 328 {showModal && ( 329 <UserListModal 330 title={modalTitle} 331 users={modalUsers} 332 onClose={() => setShowModal(false)} 333 onFollowToggle={handleFollowInModal} 334 /> 335 )} 336 </div> 337 </div> 338 ); 334 339 }; 335 340
Note:
See TracChangeset
for help on using the changeset viewer.
