Changeset a3ae097
- Timestamp:
- 01/26/26 20:19:20 (6 months ago)
- Branches:
- main
- Children:
- d47e225
- Parents:
- fd81a18
- Files:
-
- 1 added
- 9 edited
- 1 moved
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/GlobalExceptionHandler.java (added)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/NonAdminUserController.java (modified) (1 diff)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/NonAdminUserRepository.java (modified) (1 diff)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/NonAdminUserService.java (modified) (1 diff)
-
frontend/package-lock.json (modified) (2 diffs)
-
frontend/package.json (modified) (1 diff)
-
frontend/src/App.tsx (modified) (1 diff)
-
frontend/src/components/userProfile/AllUsersHelper.tsx (modified) (3 diffs)
-
frontend/src/components/userProfile/ArtistView.tsx (modified) (2 diffs)
-
frontend/src/components/userProfile/ListenerView.tsx (modified) (2 diffs)
-
frontend/src/components/userProfile/UserDetailView.tsx (moved) (moved from frontend/src/components/userProfile/UserDetail.tsx ) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/NonAdminUserController.java
rfd81a18 ra3ae097 27 27 return ResponseEntity.ok(nonAdminUserService.getById(id)); 28 28 } 29 30 @GetMapping("/search") 31 public HttpEntity<List<NonAdminUserDTO>>searchUsers(@RequestParam String name){ 32 return ResponseEntity.ok(nonAdminUserService.searchUsers(name)); 33 } 29 34 } -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/NonAdminUserRepository.java
rfd81a18 ra3ae097 16 16 List<NonAdminUser> findAllWithUser(); 17 17 18 @Query("SELECT u FROM NonAdminUser u " + 19 "WHERE LOWER(u.user.fullName) LIKE LOWER(CONCAT('%', :name, '%') )"+ 20 "OR LOWER(u.user.username) LIKE LOWER(CONCAT('%', :name, '%' )) ") 21 List<NonAdminUser> searchByName(String name); 22 18 23 19 24 -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/NonAdminUserService.java
rfd81a18 ra3ae097 84 84 } 85 85 86 87 public List<NonAdminUserDTO>searchUsers(String name){ 88 return nonAdminUserRepository.searchByName(name).stream() 89 .map(user -> { 90 String type = determineType(user.getId()); 91 Long followers = followRepository.countByFolloweeId(user.getId()); 92 Long following = followRepository.countByFollowerId(user.getId()); 93 return mapper.toDTO(user, type, followers, following); 94 }) 95 .collect(Collectors.toList()); 96 } 97 86 98 private String determineType(Long id) { 87 99 if (artistRepository.existsById(id)) { -
frontend/package-lock.json
rfd81a18 ra3ae097 10 10 "dependencies": { 11 11 "@tailwindcss/vite": "^4.1.18", 12 "lucide-react": "^0.563.0", 12 13 "react": "^19.2.0", 13 14 "react-dom": "^19.2.0", … … 3114 3115 } 3115 3116 }, 3117 "node_modules/lucide-react": { 3118 "version": "0.563.0", 3119 "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", 3120 "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", 3121 "license": "ISC", 3122 "peerDependencies": { 3123 "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" 3124 } 3125 }, 3116 3126 "node_modules/magic-string": { 3117 3127 "version": "0.30.21", -
frontend/package.json
rfd81a18 ra3ae097 12 12 "dependencies": { 13 13 "@tailwindcss/vite": "^4.1.18", 14 "lucide-react": "^0.563.0", 14 15 "react": "^19.2.0", 15 16 "react-dom": "^19.2.0", -
frontend/src/App.tsx
rfd81a18 ra3ae097 2 2 import { BrowserRouter, Routes, Route } from "react-router-dom"; 3 3 import AllUsers from "./components/userProfile/AllUsersHelper"; 4 import UserDetail from "./components/userProfile/UserDetail ";4 import UserDetail from "./components/userProfile/UserDetailView"; 5 5 6 6 function Home() { -
frontend/src/components/userProfile/AllUsersHelper.tsx
rfd81a18 ra3ae097 1 import { useEffect, useState } from "react";1 import React, { useEffect, useState } from "react"; 2 2 import { useNavigate } from "react-router-dom"; 3 3 … … 10 10 const AllUsers = () => { 11 11 const [users, setUsers] = useState<User[]>([]); 12 const [searchedUser, setSearchedUser] = useState<string>(""); 13 const [error, setError] = useState<string | null>(null); 12 14 const navigate = useNavigate(); 13 15 … … 25 27 }; 26 28 29 const handleSearch = async (e: React.FormEvent) => { 30 e.preventDefault(); 31 try { 32 const response = await fetch( 33 `http://localhost:8080/users/search?name=${searchedUser}`, 34 ); 35 if (!response.ok) throw new Error("Search failed"); 36 37 const data = await response.json(); 38 setUsers(data); 39 } catch (err: any) { 40 setError(err.message); 41 } 42 }; 43 44 if (users.length == 0) return <div className="p-6">Loading...</div>; 45 27 46 return ( 28 47 <div className="container mx-auto p-6"> 29 48 <h1 className="text-3xl font-bold mb-6">All Users</h1> 49 <form onSubmit={handleSearch} className="flex gap-2 mb-10"> 50 <input 51 type="text" 52 value={searchedUser} 53 onChange={(e) => setSearchedUser(e.target.value)} 54 className="border p-2 rounded w-full" 55 placeholder="Search for a user..." 56 /> 57 <button 58 type="submit" 59 className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 transition-colors" 60 > 61 Search 62 </button> 63 </form> 64 30 65 <div className="grid gap-4"> 31 66 {users.map((u) => ( -
frontend/src/components/userProfile/ArtistView.tsx
rfd81a18 ra3ae097 1 1 import { useNavigate } from "react-router-dom"; 2 import { Music, Disc3, Mic2 } from "lucide-react"; 2 3 3 4 interface ArtistContributionDTO { … … 15 16 const navigate = useNavigate(); 16 17 18 const albums = contributions.filter((c) => c.entityType === "Album"); 19 const songs = contributions.filter((c) => c.entityType === "Song"); 20 21 const getRoleColor = (role: string) => { 22 const colors: { [key: string]: string } = { 23 COMPOSER: "bg-purple-500", 24 PERFORMER: "bg-blue-500", 25 PRODUCER: "bg-green-500", 26 MAIN_VOCAL: "bg-pink-500", 27 }; 28 return colors[role] || "bg-gray-500"; 29 }; 30 17 31 return ( 18 <div className="mt-8 border-t pt-6"> 19 <h2 className="text-2xl font-bold mb-4">Contributions</h2> 20 <div className="grid gap-3"> 21 {contributions.map((contribution, index) => ( 22 <div 23 key={index} 24 className="p-4 border rounded hover:shadow-md transition-shadow cursor-pointer" 25 onClick={() => 26 navigate(`/musical-entity/${contribution.musicalEntityId}`) 27 } 28 > 29 <div className="flex justify-between items-center"> 30 <div className="flex-1"> 31 <p className="font-semibold text-lg">{contribution.title}</p> 32 <p className="text-sm text-gray-500 mt-1"> 33 ID: {contribution.musicalEntityId} 34 </p> 32 <div className="mt-8"> 33 {/* Albums Section */} 34 {albums.length > 0 && ( 35 <div className="mb-12"> 36 <div className="flex items-center gap-3 mb-6"> 37 <Disc3 className="w-8 h-8 text-purple-600" /> 38 <h2 className="text-3xl font-bold">Albums</h2> 39 </div> 40 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> 41 {albums.map((album, index) => ( 42 <div 43 key={index} 44 className="group cursor-pointer" 45 onClick={() => 46 navigate(`/musical-entity/${album.musicalEntityId}`) 47 } 48 > 49 <div className="relative aspect-square bg-linear-to-br from-purple-400 via-pink-400 to-blue-400 rounded-lg mb-3 overflow-hidden shadow-lg group-hover:shadow-2xl transition-all duration-300 group-hover:scale-105"> 50 <div className="absolute inset-0 flex items-center justify-center"> 51 <Disc3 className="w-20 h-20 text-white opacity-40" /> 52 </div> 53 <div className="absolute bottom-0 left-0 right-0 bg-linear-to-t from-black/60 to-transparent p-4"> 54 <span 55 className={`text-xs px-2 py-1 rounded-full text-white ${getRoleColor(album.role)}`} 56 > 57 {album.role.replace("_", " ")} 58 </span> 59 </div> 60 </div> 61 <h3 className="font-semibold text-base line-clamp-2 group-hover:text-blue-600 transition-colors"> 62 {album.title} 63 </h3> 35 64 </div> 36 <div className="flex gap-2"> 37 <span className="bg-purple-100 text-purple-800 px-3 py-1 rounded-full text-sm font-medium"> 38 {contribution.role} 39 </span> 40 <span className="bg-gray-100 text-gray-800 px-3 py-1 rounded-full text-sm font-medium"> 41 {contribution.entityType} 65 ))} 66 </div> 67 </div> 68 )} 69 70 {songs.length > 0 && ( 71 <div className="mb-12"> 72 <div className="flex items-center gap-3 mb-6"> 73 <Music className="w-8 h-8 text-blue-600" /> 74 <h2 className="text-3xl font-bold">Songs</h2> 75 </div> 76 <div className="space-y-2"> 77 {songs.map((song, index) => ( 78 <div 79 key={index} 80 className="group flex items-center gap-4 p-4 rounded-lg hover:bg-gray-50 transition-all cursor-pointer border border-transparent hover:border-gray-200" 81 onClick={() => 82 navigate(`/musical-entity/${song.musicalEntityId}`) 83 } 84 > 85 <div className="shrink-0 w-14 h-14 bg-linear-to-br from-blue-400 to-cyan-400 rounded flex items-center justify-center group-hover:scale-110 transition-transform shadow-md"> 86 <Music className="w-7 h-7 text-white" /> 87 </div> 88 <div className="flex-1 min-w-0"> 89 <h3 className="font-semibold text-lg group-hover:text-blue-600 transition-colors truncate"> 90 {song.title} 91 </h3> 92 <div className="flex items-center gap-2 mt-1"> 93 <Mic2 className="w-4 h-4 text-gray-400" /> 94 </div> 95 </div> 96 <span 97 className={`px-4 py-2 rounded-full text-white text-sm font-medium ${getRoleColor(song.role)} shadow-md`} 98 > 99 {song.role.replace("_", " ")} 42 100 </span> 43 101 </div> 44 </div>102 ))} 45 103 </div> 46 ))} 47 </div> 104 </div> 105 )} 106 48 107 {contributions.length === 0 && ( 49 <p className="text-gray-500 text-center py-4">No contributions yet</p> 108 <div className="flex flex-col items-center justify-center py-16 text-gray-400"> 109 <Music className="w-24 h-24 mb-4 opacity-20" /> 110 <p className="text-xl font-medium">No contributions yet</p> 111 <p className="text-sm mt-2">Start creating music to see it here</p> 112 </div> 50 113 )} 51 114 </div> -
frontend/src/components/userProfile/ListenerView.tsx
rfd81a18 ra3ae097 1 1 import { useNavigate } from "react-router-dom"; 2 import { Heart, ListMusic, Music, Album } from "lucide-react"; 2 3 3 4 interface MusicalEntityDTO { … … 23 24 const navigate = useNavigate(); 24 25 26 const likedSongs = likedEntities.filter((e) => e.type === "SONG"); 27 const likedAlbums = likedEntities.filter((e) => e.type === "ALBUM"); 28 25 29 return ( 26 <div className="mt-8 border-t pt-6 space-y-10"> 27 {/* --- PLAYLISTS SECTION --- */} 28 <section> 29 <h3 className="text-2xl font-bold mb-6 flex items-center gap-2"> 30 <span>My Playlists</span> 31 <span className="text-sm font-normal text-gray-400"> 32 ({playlists?.length}) 33 </span> 34 </h3> 30 <div className="mt-8 space-y-12"> 31 {playlists && playlists.length > 0 && ( 32 <section> 33 <div className="flex items-center gap-2 mb-6 pb-2 border-b-2 border-gray-200"> 34 <ListMusic className="w-6 h-6 text-gray-700" /> 35 <h3 className="text-2xl font-bold text-gray-800">My Playlists</h3> 36 <span className="text-sm text-gray-400 ml-1"> 37 ({playlists.length}) 38 </span> 39 </div> 35 40 36 <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-6"> 37 {playlists?.map((playlist) => ( 38 <div 39 key={playlist.id} 40 className="group cursor-pointer" 41 onClick={() => navigate(`/playlist/${playlist.id}`)} 42 > 43 {/* Playlist Cover Container */} 44 <div className="aspect-square relative overflow-hidden rounded-xl shadow-md bg-gray-200 mb-3"> 45 {playlist.cover ? ( 46 <img 47 src={playlist.cover} 48 alt={playlist.name} 49 className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110" 50 /> 51 ) : ( 52 <div className="w-full h-full flex items-center justify-center bg-linear-to-br from-gray-300 to-gray-400 text-white"> 53 <svg 54 xmlns="http://www.w3.org/2000/svg" 55 className="h-12 w-12" 56 fill="none" 57 viewBox="0 0 24 24" 58 stroke="currentColor" 59 > 60 <path 61 strokeLinecap="round" 62 strokeLinejoin="round" 63 strokeWidth={2} 64 d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" 65 /> 66 </svg> 67 </div> 68 )} 69 {/* Hover Overlay */} 70 <div className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"> 71 <div className="bg-blue-500 p-3 rounded-full text-white shadow-xl transform translate-y-4 group-hover:translate-y-0 transition-transform"> 72 <svg 73 xmlns="http://www.w3.org/2000/svg" 74 className="h-6 w-6" 75 fill="none" 76 viewBox="0 0 24 24" 77 stroke="currentColor" 78 > 79 <path 80 strokeLinecap="round" 81 strokeLinejoin="round" 82 strokeWidth={2} 83 d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" 84 /> 85 </svg> 86 </div> 41 <div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4"> 42 {playlists.map((playlist) => ( 43 <div 44 key={playlist.id} 45 className="group cursor-pointer" 46 onClick={() => navigate(`/playlist/${playlist.id}`)} 47 > 48 <div className="aspect-square rounded-md overflow-hidden bg-gray-100 mb-2 relative shadow-sm group-hover:shadow-lg transition-all"> 49 {playlist.cover ? ( 50 <img 51 src={playlist.cover} 52 alt={playlist.name} 53 className="w-full h-full object-cover group-hover:opacity-90 transition-opacity" 54 /> 55 ) : ( 56 <div className="w-full h-full bg-gradient-to-br from-gray-200 to-gray-300 flex items-center justify-center"> 57 <ListMusic className="w-10 h-10 text-gray-400" /> 58 </div> 59 )} 60 </div> 61 <p className="text-sm font-medium text-gray-800 truncate group-hover:text-blue-600 transition-colors"> 62 {playlist.name} 63 </p> 64 </div> 65 ))} 66 </div> 67 </section> 68 )} 69 70 {likedSongs.length > 0 && ( 71 <section> 72 <div className="flex items-center gap-2 mb-6 pb-2 border-b-2 border-gray-200"> 73 <Heart className="w-6 h-6 text-red-500 fill-red-500" /> 74 <h3 className="text-2xl font-bold text-gray-800">Liked Songs</h3> 75 <span className="text-sm text-gray-400 ml-1"> 76 ({likedSongs.length}) 77 </span> 78 </div> 79 80 <div className="space-y-1"> 81 {likedSongs.map((song, index) => ( 82 <div 83 key={song.id} 84 className="flex items-center gap-4 p-3 rounded hover:bg-gray-50 cursor-pointer group transition-colors" 85 onClick={() => navigate(`/musical-entity/${song.id}`)} 86 > 87 <span className="text-sm text-gray-400 w-8 text-right font-medium"> 88 {index + 1} 89 </span> 90 <div className="w-12 h-12 rounded bg-gradient-to-br from-red-100 to-pink-100 flex items-center justify-center flex-shrink-0"> 91 <Music className="w-6 h-6 text-red-600" /> 92 </div> 93 <div className="flex-1 min-w-0"> 94 <p className="font-medium text-gray-800 truncate group-hover:text-blue-600 transition-colors"> 95 {song.title} 96 </p> 97 <p className="text-xs text-gray-500">{song.genre}</p> 87 98 </div> 88 99 </div> 100 ))} 101 </div> 102 </section> 103 )} 89 104 90 <h4 className="font-bold text-gray-900 truncate group-hover:text-blue-600 transition-colors"> 91 {playlist.name} 92 </h4> 93 <p className="text-xs text-gray-500 uppercase tracking-wider"> 94 Playlist 95 </p> 96 </div> 97 ))} 105 {likedAlbums.length > 0 && ( 106 <section> 107 <div className="flex items-center gap-2 mb-6 pb-2 border-b-2 border-gray-200"> 108 <Album className="w-6 h-6 text-blue-600" /> 109 <h3 className="text-2xl font-bold text-gray-800">Liked Albums</h3> 110 <span className="text-sm text-gray-400 ml-1"> 111 ({likedAlbums.length}) 112 </span> 113 </div> 98 114 99 {playlists?.length === 0 && ( 100 <div className="col-span-full py-8 border-2 border-dashed border-gray-200 rounded-xl text-center"> 101 <p className="text-gray-400">No playlists created yet</p> 102 </div> 103 )} 115 <div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-5"> 116 {likedAlbums.map((album) => ( 117 <div 118 key={album.id} 119 className="group cursor-pointer" 120 onClick={() => navigate(`/musical-entity/${album.id}`)} 121 > 122 <div className="aspect-square rounded-lg overflow-hidden bg-gradient-to-br from-blue-100 to-indigo-100 mb-3 flex items-center justify-center shadow-sm group-hover:shadow-md transition-all"> 123 <Album className="w-16 h-16 text-blue-600 opacity-60" /> 124 </div> 125 <p className="font-medium text-sm text-gray-800 truncate group-hover:text-blue-600 transition-colors"> 126 {album.title} 127 </p> 128 <p className="text-xs text-gray-500 mt-0.5">{album.genre}</p> 129 </div> 130 ))} 131 </div> 132 </section> 133 )} 134 135 {likedEntities.length === 0 && (!playlists || playlists.length === 0) && ( 136 <div className="flex flex-col items-center justify-center py-16 text-gray-300"> 137 <p className="text-lg font-medium text-gray-400">Nothing here yet</p> 138 <p className="text-sm text-gray-400 mt-1"> 139 Start exploring music to build your collection 140 </p> 104 141 </div> 105 </section> 106 107 {/* --- LIKED MUSIC SECTION --- */} 108 <section> 109 <h3 className="text-2xl font-bold mb-4">Liked Music</h3> 110 <div className="grid gap-2"> 111 {likedEntities.map((entity) => ( 112 <div 113 key={entity.id} 114 className="p-4 bg-gray-50 rounded-lg border border-transparent hover:border-gray-200 hover:bg-white transition-all cursor-pointer group shadow-xs" 115 onClick={() => navigate(`/musical-entity/${entity.id}`)} 116 > 117 <div className="flex justify-between items-center"> 118 <div className="flex-1"> 119 <span className="font-semibold text-gray-800 group-hover:text-blue-600"> 120 {entity.title} 121 </span> 122 <p className="text-sm text-gray-500 mt-1">{entity.genre}</p> 123 </div> 124 <span 125 className={`px-3 py-1 rounded-full text-xs font-bold tracking-wider ${ 126 entity.type === "SONG" 127 ? "bg-green-100 text-green-700" 128 : "bg-blue-100 text-blue-700" 129 }`} 130 > 131 {entity.type} 132 </span> 133 </div> 134 </div> 135 ))} 136 </div> 137 {likedEntities.length === 0 && ( 138 <p className="text-gray-500 text-center py-4 italic"> 139 No liked music yet 140 </p> 141 )} 142 </section> 142 )} 143 143 </div> 144 144 ); -
frontend/src/components/userProfile/UserDetailView.tsx
rfd81a18 ra3ae097 48 48 const navigate = useNavigate(); 49 49 const [user, setUser] = useState<User | null>(null); 50 const [error, setError] = useState<string | null>(null); 50 51 51 52 useEffect(() => { 52 53 const fetchUser = async () => { 53 console.log(userId); 54 const response = await fetch(`http://localhost:8080/users/${userId}`); 55 const data = await response.json(); 56 console.log("Fetched user data:", data); 57 setUser(data); 54 setError(null); 55 try { 56 const response = await fetch(`http://localhost:8080/users/${userId}`); 57 58 if (!response.ok) { 59 const errorData = await response.json(); 60 throw new Error(errorData.error || "Failed to fetch user"); 61 } 62 63 const data = await response.json(); 64 setUser(data); 65 } catch (err: any) { 66 setError(err.message); 67 } 58 68 }; 59 69 fetchUser(); 60 70 }, [userId]); 71 72 if (error) { 73 return ( 74 <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg"> 75 <h2 className="font-bold">Error</h2> 76 <p>{error}</p> 77 </div> 78 ); 79 } 61 80 62 81 if (!user) return <div className="p-6">Loading...</div>; … … 72 91 73 92 <div className="bg-white shadow-lg rounded-lg p-8"> 74 {/* Profile Header */}75 93 <div className="flex items-start gap-6 mb-8"> 76 {/* Profile Photo */}77 94 <div className="shrink-0"> 78 95 <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"> … … 81 98 </div> 82 99 83 {/* User Info */}84 100 <div className="flex-1"> 85 101 <h1 className="text-4xl font-bold mb-2">{user.fullName}</h1> … … 88 104 </span> 89 105 90 {/* Followers/Following Stats */}91 106 <div className="flex gap-6 mb-4 text-gray-700"> 92 107 <div className="flex flex-col">
Note:
See TracChangeset
for help on using the changeset viewer.
