Changeset 0792d02 for frontend/src
- Timestamp:
- 02/05/26 10:29:09 (5 months ago)
- Branches:
- main
- Children:
- 3de009b
- Parents:
- d2af1a6
- Location:
- frontend/src
- Files:
-
- 6 edited
-
App.tsx (modified) (1 diff)
-
components/userProfile/ListenerView.tsx (modified) (7 diffs)
-
components/userProfile/UserListModal.tsx (modified) (4 diffs)
-
pages/AllUsers.tsx (modified) (1 diff)
-
pages/UserDetail.tsx (modified) (9 diffs)
-
utils/types.ts (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
frontend/src/App.tsx
rd2af1a6 r0792d02 51 51 }, 52 52 { 53 path: "/users/:user Id",53 path: "/users/:username", 54 54 element: <UserDetail />, 55 55 }, -
frontend/src/components/userProfile/ListenerView.tsx
rd2af1a6 r0792d02 4 4 import axiosInstance from "../../api/axiosInstance"; 5 5 import { useState } from "react"; 6 6 7 interface ListenerViewProps { 7 8 likedEntities: MusicalEntity[] | []; 8 playlists: Playlist[] | []; 9 createdPlaylists: Playlist[] | []; 10 savedPlaylists: Playlist[] | []; 9 11 } 10 12 11 const ListenerView = ({ likedEntities, playlists }: ListenerViewProps) => { 13 const ListenerView = ({ 14 likedEntities, 15 createdPlaylists, 16 savedPlaylists, 17 }: ListenerViewProps) => { 12 18 const navigate = useNavigate(); 13 19 const [items, setItems] = useState(likedEntities); 20 const [savedItems, setSavedItems] = useState(savedPlaylists); 14 21 const [toast, setToast] = useState<{ message: string; show: boolean }>({ 15 22 message: "", … … 27 34 const likedAlbums = items.filter((e) => e.type === "ALBUM"); 28 35 29 const handleSavePlaylist = (e: React.MouseEvent, playlistId: number) => { 36 const handleSavePlaylist = async ( 37 e: React.MouseEvent, 38 playlistId: number, 39 playlistName: string, 40 ) => { 30 41 e.stopPropagation(); 31 42 32 console.log("Save playlist:", playlistId); 43 try { 44 const response = await axiosInstance.post(`/playlist/${playlistId}/save`); 45 const data = response.data; 46 47 setSavedItems((prevItems) => 48 data.isSaved 49 ? [...prevItems, prevItems.find((p) => p.id === playlistId)!] 50 : prevItems.filter((p) => p.id !== playlistId), 51 ); 52 53 showToast( 54 data.isSaved 55 ? `Saved "${playlistName}"` 56 : `Removed "${playlistName}" from saved playlists`, 57 ); 58 } catch (err: any) { 59 showToast(err.response?.data?.error || "Failed to save playlist"); 60 } 33 61 }; 34 62 … … 63 91 </div> 64 92 )} 65 {playlists && playlists.length > 0 && ( 93 94 {createdPlaylists && createdPlaylists.length > 0 && ( 66 95 <section> 67 96 <div className="flex items-center gap-3 mb-6"> … … 70 99 Created Playlists 71 100 </h3> 72 <span className="text-sm text-gray-500">({playlists.length})</span> 101 <span className="text-sm text-gray-500"> 102 ({createdPlaylists.length}) 103 </span> 73 104 </div> 74 105 75 106 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> 76 { playlists.map((playlist) => (107 {createdPlaylists.map((playlist) => ( 77 108 <div 78 109 key={playlist.id} … … 93 124 )} 94 125 95 {/* Save button overlay */}96 126 <button 97 onClick={(e) => handleSavePlaylist(e, playlist.id)} 127 onClick={(e) => 128 handleSavePlaylist(e, playlist.id, playlist.name) 129 } 98 130 className="absolute top-2 right-2 p-2 bg-white/90 hover:bg-white rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100" 99 131 title={playlist.isSavedByCurrentUser ? "Unsave" : "Save"} … … 106 138 }`} 107 139 /> 140 </button> 141 </div> 142 <p className="text-sm font-semibold text-gray-900 truncate group-hover:text-blue-600 transition-colors"> 143 {playlist.name} 144 </p> 145 <p className="text-xs text-gray-500 truncate"> 146 {playlist.creatorName} 147 </p> 148 </div> 149 ))} 150 </div> 151 </section> 152 )} 153 154 {savedItems && savedItems.length > 0 && ( 155 <section> 156 <div className="flex items-center gap-3 mb-6"> 157 <Bookmark className="w-6 h-6 text-blue-600 fill-blue-600" /> 158 <h3 className="text-2xl font-bold text-gray-800"> 159 Saved Playlists 160 </h3> 161 <span className="text-sm text-gray-500">({savedItems.length})</span> 162 </div> 163 164 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> 165 {savedItems.map((playlist) => ( 166 <div 167 key={playlist.id} 168 className="group cursor-pointer" 169 onClick={() => navigate(`/collection/playlist/${playlist.id}`)} 170 > 171 <div className="relative aspect-square rounded-lg overflow-hidden bg-gray-100 mb-3 shadow-md group-hover:shadow-lg transition-all"> 172 {playlist.cover ? ( 173 <img 174 src={playlist.cover} 175 alt={playlist.name} 176 className="w-full h-full object-cover" 177 /> 178 ) : ( 179 <div className="w-full h-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center"> 180 <ListMusic className="w-16 h-16 text-white opacity-30" /> 181 </div> 182 )} 183 184 <button 185 onClick={(e) => 186 handleSavePlaylist(e, playlist.id, playlist.name) 187 } 188 className="absolute top-2 right-2 p-2 bg-white/90 hover:bg-white rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100" 189 title="Unsave" 190 > 191 <Bookmark className="w-5 h-5 fill-blue-600 text-blue-600" /> 108 192 </button> 109 193 </div> … … 234 318 {likedEntities && 235 319 likedEntities.length === 0 && 236 (!playlists || playlists.length === 0) && ( 320 (!createdPlaylists || createdPlaylists.length === 0) && 321 (!savedItems || savedItems.length === 0) && ( 237 322 <div className="flex flex-col items-center justify-center py-16 text-gray-400"> 238 323 <Music className="w-20 h-20 mb-4 opacity-20" /> -
frontend/src/components/userProfile/UserListModal.tsx
rd2af1a6 r0792d02 6 6 users: BaseNonAdminUser[]; 7 7 onClose: () => void; 8 onFollowToggle: (target Id: number) => Promise<void>;8 onFollowToggle: (targetUsername: string) => Promise<void>; 9 9 } 10 10 … … 37 37 users.map((u) => ( 38 38 <div 39 key={u. id}39 key={u.username} 40 40 className="flex items-center justify-between p-3 hover:bg-gray-50 rounded-lg transition-colors" 41 41 > … … 44 44 onClick={() => { 45 45 onClose(); 46 navigate(`/users/${u. id}`);46 navigate(`/users/${u.username}`); 47 47 }} 48 48 > … … 64 64 65 65 <button 66 onClick={() => onFollowToggle(u. id)}66 onClick={() => onFollowToggle(u.username)} 67 67 className={`px-4 py-1 text-sm font-medium rounded-md transition-colors cursor-pointer ${ 68 68 u.isFollowedByCurrentUser -
frontend/src/pages/AllUsers.tsx
rd2af1a6 r0792d02 4 4 5 5 interface User { 6 id: number; 7 username: string; 8 fullName: string; 6 username: string; 7 fullName: string; 9 8 } 10 9 11 10 const AllUsers = () => { 12 const [users, setUsers] = useState<User[]>([]);13 const [searchedUser, setSearchedUser] = useState<string>("");14 const [error, setError] = useState<string | null>(null);15 const navigate = useNavigate();11 const [users, setUsers] = useState<User[]>([]); 12 const [searchedUser, setSearchedUser] = useState<string>(""); 13 const [error, setError] = useState<string | null>(null); 14 const navigate = useNavigate(); 16 15 17 useEffect(() => {18 const fetchUsers = async () => {19 const response = axiosInstance.get("/users/all");20 setUsers((await response).data);21 };22 fetchUsers();23 }, []);16 useEffect(() => { 17 const fetchUsers = async () => { 18 const response = axiosInstance.get("/users/all"); 19 setUsers((await response).data); 20 }; 21 fetchUsers(); 22 }, []); 24 23 25 const handleUserClick = (userId: number) => {26 navigate(`/users/${userId}`);27 };24 const handleUserClick = (username: string) => { 25 navigate(`/users/${username}`); 26 }; 28 27 29 const handleSearch = async (e: React.FormEvent) => {30 e.preventDefault();31 try {32 const response = await axiosInstance.get(`/users/search`, {33 params: { name: searchedUser },34 });35 setUsers(response.data);36 } catch (err: any) {37 const errorMessage = err.response?.data?.error || "Search failed";38 setError(errorMessage);39 }40 };28 const handleSearch = async (e: React.FormEvent) => { 29 e.preventDefault(); 30 try { 31 const response = await axiosInstance.get(`/users/search`, { 32 params: { name: searchedUser }, 33 }); 34 setUsers(response.data); 35 } catch (err: any) { 36 const errorMessage = err.response?.data?.error || "Search failed"; 37 setError(errorMessage); 38 } 39 }; 41 40 42 if (users.length == 0) return <div className="p-6">Loading...</div>;43 if (error) {44 return (45 <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg">46 <h2 className="font-bold">Error</h2>47 <p>{error}</p>48 </div>49 );50 }41 if (users.length == 0) return <div className="p-6">Loading...</div>; 42 if (error) { 43 return ( 44 <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg"> 45 <h2 className="font-bold">Error</h2> 46 <p>{error}</p> 47 </div> 48 ); 49 } 51 50 52 return (53 <div className="container mx-auto p-6">54 <h1 className="text-3xl font-bold mb-6">All Users</h1>55 <form onSubmit={handleSearch} className="flex gap-2 mb-10">56 <input57 type="text"58 value={searchedUser}59 onChange={(e) => setSearchedUser(e.target.value)}60 className="border p-2 rounded w-full"61 placeholder="Search for a user..."62 />63 <button64 type="submit"65 className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 transition-colors"66 >67 Search68 </button>69 </form>51 return ( 52 <div className="container mx-auto p-6"> 53 <h1 className="text-3xl font-bold mb-6">All Users</h1> 54 <form onSubmit={handleSearch} className="flex gap-2 mb-10"> 55 <input 56 type="text" 57 value={searchedUser} 58 onChange={(e) => setSearchedUser(e.target.value)} 59 className="border p-2 rounded w-full" 60 placeholder="Search for a user..." 61 /> 62 <button 63 type="submit" 64 className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 transition-colors" 65 > 66 Search 67 </button> 68 </form> 70 69 71 <div className="grid gap-4"> 72 {users.map((u) => ( 73 <div 74 key={u.id} 75 onClick={() => handleUserClick(u.id)} 76 className="bg-white shadow-md rounded-lg p-4 hover:shadow-lg hover:bg-gray-50 cursor-pointer transition-all" 77 > 78 <h2 className="text-xl font-semibold">{u.fullName}</h2> 79 <p className="text-gray-600">@{u.username}</p> 80 <p className="text-gray-600">{u.id}</p> 81 </div> 82 ))} 83 </div> 84 </div> 85 ); 70 <div className="grid gap-4"> 71 {users.map((u) => ( 72 <div 73 key={u.username} 74 onClick={() => handleUserClick(u.username)} 75 className="bg-white shadow-md rounded-lg p-4 hover:shadow-lg hover:bg-gray-50 cursor-pointer transition-all" 76 > 77 <h2 className="text-xl font-semibold">{u.fullName}</h2> 78 <p className="text-gray-600">@{u.username}</p> 79 </div> 80 ))} 81 </div> 82 </div> 83 ); 86 84 }; 87 85 -
frontend/src/pages/UserDetail.tsx
rd2af1a6 r0792d02 27 27 likedEntities: MusicalEntity[]; 28 28 createdPlaylists: Playlist[]; 29 savedPlaylists: Playlist[]; 29 30 } 30 31 … … 34 35 // user refers to the selected user NOT to the user from context 35 36 const baseURL = import.meta.env.VITE_API_BASE_URL; 36 const { user Id} = useParams();37 const { username } = useParams(); 37 38 const navigate = useNavigate(); 38 39 const [user, setUser] = useState<UserProfile | null>(null); … … 50 51 try { 51 52 const response = await axiosInstance.post<FollowStatus>( 52 `/users/${user Id}/follow`,53 `/users/${username}/follow`, 53 54 ); 54 55 setUser((prev) => { … … 68 69 }; 69 70 70 const handleFollowInModal = async (target Id: number) => {71 const handleFollowInModal = async (targetUsername: string) => { 71 72 try { 72 73 const response = await axiosInstance.post<FollowStatus>( 73 `/users/${ targetId}/follow`,74 `/users/${username}/follow`, 74 75 ); 75 76 76 77 setModalUsers((prevUsers) => 77 78 prevUsers.map((u) => 78 u. id === targetId79 u.username === targetUsername 79 80 ? { ...u, isFollowedByCurrentUser: response.data.isFollowing } 80 81 : u, … … 100 101 setIsLoadingModal(true); 101 102 try { 102 const response = await axiosInstance.get(`/users/${user Id}/followers`);103 const response = await axiosInstance.get(`/users/${username}/followers`); 103 104 setModalUsers(response.data); 104 105 setModalTitle("Followers"); … … 113 114 setIsLoadingModal(true); 114 115 try { 115 const response = await axiosInstance.get(`/users/${user Id}/following`);116 const response = await axiosInstance.get(`/users/${username}/following`); 116 117 setModalUsers(response.data); 117 118 setModalTitle("Following"); … … 128 129 setError(null); 129 130 try { 130 const response = await axiosInstance.get(`/users/${user Id}`);131 const response = await axiosInstance.get(`/users/${username}`); 131 132 console.log(response.data); 132 133 setUser(response.data); … … 136 137 }; 137 138 fetchUser(); 138 }, [user Id]);139 }, [username]); 139 140 140 141 if (error) { … … 233 234 <ListenerView 234 235 likedEntities={user.likedEntities} 235 playlists={user.createdPlaylists} 236 createdPlaylists={user.createdPlaylists} 237 savedPlaylists={user.savedPlaylists} 236 238 /> 237 239 )} -
frontend/src/utils/types.ts
rd2af1a6 r0792d02 44 44 45 45 export interface BaseNonAdminUser { 46 id: number;46 username: string; 47 47 fullName: string; 48 48 userType: string;
Note:
See TracChangeset
for help on using the changeset viewer.
