Changeset 694fc25 for frontend/src
- Timestamp:
- 02/05/26 18:59:39 (5 months ago)
- Branches:
- main
- Children:
- cb4a1da
- Parents:
- 15782a6
- Location:
- frontend/src
- Files:
-
- 3 edited
-
pages/LandingPage.tsx (modified) (1 diff)
-
pages/Nav.tsx (modified) (5 diffs)
-
utils/types.ts (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
frontend/src/pages/LandingPage.tsx
r15782a6 r694fc25 1 import { useEffect, useState } from "react"; 2 import axiosInstance from "../api/axiosInstance"; 3 import type { Song } from "../utils/types"; 4 import Nav from "./Nav"; 5 1 6 const LandingPage = () => { 2 return <div>landing page</div>; 7 const [songs, setSongs] = useState<Song[]>([]); 8 const [loading, setLoading] = useState<boolean>(true); 9 const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(true); 10 11 // mock data for recently listened songs 12 const recentlyListened = [ 13 { id: 1, title: "Song One", artist: "Artist A", cover: "/favicon.png" }, 14 { id: 2, title: "Song Two", artist: "Artist B", cover: "/favicon.png" }, 15 { id: 3, title: "Song Three", artist: "Artist C", cover: "/favicon.png" }, 16 { id: 4, title: "Song Four", artist: "Artist D", cover: "/favicon.png" }, 17 { id: 5, title: "Song Five", artist: "Artist E", cover: "/favicon.png" }, 18 ]; 19 20 // mock data for my playlists 21 const playlists = [ 22 { id: 1, name: "My Favorites", songCount: 25 }, 23 { id: 2, name: "Workout Mix", songCount: 18 }, 24 { id: 3, name: "Chill Vibes", songCount: 32 }, 25 { id: 4, name: "Party Hits", songCount: 45 }, 26 ]; 27 28 useEffect(() => { 29 const fetchSongs = async () => { 30 try { 31 const response = await axiosInstance.get("/songs"); 32 const data = response.data; 33 console.log("Fetched songs:", data); 34 setSongs(data); 35 } catch (error) { 36 console.error("Error fetching songs:", error); 37 } finally { 38 setLoading(false); 39 } 40 }; 41 fetchSongs(); 42 }, []); 43 44 return ( 45 <> 46 <Nav 47 isSidebarOpen={isSidebarOpen} 48 onToggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)} 49 /> 50 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white flex pt-20"> 51 {/* Sidebar */} 52 <div 53 className={`fixed left-0 top-0 h-full bg-[#121212] border-r border-white/10 transition-transform duration-300 ease-in-out z-100 ${ 54 isSidebarOpen ? "translate-x-0" : "-translate-x-full" 55 } w-64 overflow-y-auto`} 56 > 57 <div className="p-6"> 58 {/* Sidebar Header */} 59 <div className="flex justify-between items-center mb-6"> 60 <h2 className="text-xl font-bold text-white">Library</h2> 61 <button 62 onClick={() => setIsSidebarOpen(false)} 63 className="text-gray-400 hover:text-white transition-colors" 64 aria-label="Close sidebar" 65 > 66 <svg 67 className="w-6 h-6" 68 fill="none" 69 stroke="currentColor" 70 viewBox="0 0 24 24" 71 > 72 <path 73 strokeLinecap="round" 74 strokeLinejoin="round" 75 strokeWidth={2} 76 d="M6 18L18 6M6 6l12 12" 77 /> 78 </svg> 79 </button> 80 </div> 81 82 {/* Recently Listened */} 83 <div className="mb-8"> 84 <h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4"> 85 Recently Played 86 </h3> 87 <div className="space-y-3"> 88 {recentlyListened.map((song) => ( 89 <div 90 key={song.id} 91 className="flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 cursor-pointer transition-colors" 92 > 93 <img 94 src={song.cover} 95 alt={song.title} 96 className="w-10 h-10 rounded object-cover" 97 /> 98 <div className="flex-1 min-w-0"> 99 <p className="text-sm font-medium text-white truncate"> 100 {song.title} 101 </p> 102 <p className="text-xs text-gray-400 truncate"> 103 {song.artist} 104 </p> 105 </div> 106 </div> 107 ))} 108 </div> 109 </div> 110 111 {/* Playlists */} 112 <div> 113 <h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4"> 114 Your Playlists 115 </h3> 116 <div className="space-y-2"> 117 {playlists.map((playlist) => ( 118 <div 119 key={playlist.id} 120 className="flex items-center justify-between p-3 rounded-lg hover:bg-white/5 cursor-pointer transition-colors" 121 > 122 <div className="flex items-center gap-3"> 123 <div className="w-10 h-10 bg-linear-to-br from-purple-500 to-pink-500 rounded flex items-center justify-center"> 124 <svg 125 className="w-5 h-5 text-white" 126 fill="currentColor" 127 viewBox="0 0 20 20" 128 > 129 <path d="M18 3a1 1 0 00-1.196-.98l-10 2A1 1 0 006 5v9.114A4.369 4.369 0 005 14c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V7.82l8-1.6v5.894A4.37 4.37 0 0015 12c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V3z" /> 130 </svg> 131 </div> 132 <div> 133 <p className="text-sm font-medium text-white"> 134 {playlist.name} 135 </p> 136 <p className="text-xs text-gray-400"> 137 {playlist.songCount} songs 138 </p> 139 </div> 140 </div> 141 </div> 142 ))} 143 </div> 144 </div> 145 </div> 146 </div> 147 148 {/* Main Content */} 149 <div 150 className={`flex-1 transition-all duration-300 ${ 151 isSidebarOpen ? "ml-64" : "ml-0" 152 }`} 153 > 154 <div className="p-8"> 155 {loading ? ( 156 <div className="flex flex-col items-center justify-center min-h-[60vh] gap-6"> 157 <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin"></div> 158 <p className="text-xl text-gray-400">Loading songs...</p> 159 </div> 160 ) : ( 161 <div className="max-w-7xl mx-auto"> 162 <div className="mb-12 pb-6 border-b border-white/10"> 163 <h1 className="text-5xl font-bold mb-3 pb-1 bg-linear-to-r from-[#1db954] to-[#1ed760] bg-clip-text text-transparent"> 164 Top Songs 165 </h1> 166 <p className="text-xl text-gray-400"> 167 Listen to the newest tracks on FinkWave 168 </p> 169 </div> 170 <div className="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-8 py-4"> 171 {songs.map((song) => ( 172 <div 173 key={song.id} 174 className="bg-[#282828] rounded-xl overflow-hidden cursor-pointer shadow-lg transition-all duration-300 ease-in-out hover:-translate-y-2 hover:shadow-2xl group" 175 > 176 <div className="relative w-full pt-[100%] overflow-hidden bg-[#181818]"> 177 <img 178 src={song.cover || "/favicon.png"} 179 alt={song.title} 180 className="absolute top-0 left-0 w-full h-full object-cover" 181 onError={(e) => { 182 (e.target as HTMLImageElement).src = "/favicon.png"; 183 }} 184 /> 185 <div className="absolute top-0 left-0 w-full h-full bg-black/60 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300"> 186 <div className="w-15 h-15 rounded-full bg-[#1db954] flex items-center justify-center text-2xl text-black shadow-[0_4px_12px_rgba(29,185,84,0.5)]"> 187 ▶ 188 </div> 189 </div> 190 </div> 191 <div className="p-4 flex flex-col items-center"> 192 <h3 className="text-lg font-semibold mb-2 text-white overflow-hidden text-ellipsis whitespace-nowrap"> 193 {song.title} 194 </h3> 195 <p className="text-sm text-gray-400 mb-3 overflow-hidden text-ellipsis whitespace-nowrap"> 196 {"<album>"} 197 </p> 198 <p className="text-sm text-gray-400 mb-3 overflow-hidden text-ellipsis whitespace-nowrap"> 199 {song.releasedBy} 200 </p> 201 {/* <div className="inline-block bg-[#1db954]/20 px-3 py-1 rounded-xl border border-[#1db954]/40"> 202 <span className="text-xs text-[#1db954] font-medium uppercase tracking-wider"> 203 {song.genre} 204 </span> 205 </div> */} 206 </div> 207 </div> 208 ))} 209 </div> 210 </div> 211 )} 212 </div> 213 </div> 214 </div> 215 </> 216 ); 3 217 }; 4 218 -
frontend/src/pages/Nav.tsx
r15782a6 r694fc25 1 import { useEffect, useRef, useState } from "react"; 1 2 import { Link } from "react-router-dom"; 2 3 import { toast } from "react-toastify"; … … 5 6 import { useAuth } from "../context/authContext"; 6 7 7 const Nav = () => { 8 interface NavProps { 9 isSidebarOpen?: boolean; 10 onToggleSidebar?: () => void; 11 } 12 13 const Nav = ({ isSidebarOpen = false, onToggleSidebar }: NavProps) => { 8 14 const { user, setUser, isAuthLoading } = useAuth(); 15 const [isDropdownOpen, setIsDropdownOpen] = useState(false); 16 const dropdownRef = useRef<HTMLDivElement>(null); 9 17 10 18 const handleLogout = async (e: React.MouseEvent<HTMLButtonElement>) => { … … 13 21 await axiosInstance.post("/auth/logout"); 14 22 setUser(undefined); 23 setIsDropdownOpen(false); 15 24 toast.success("Logout successful!"); 16 25 } catch (error) { … … 20 29 }; 21 30 31 useEffect(() => { 32 const handleClickOutside = (event: MouseEvent) => { 33 if ( 34 dropdownRef.current && 35 !dropdownRef.current.contains(event.target as Node) 36 ) { 37 setIsDropdownOpen(false); 38 } 39 }; 40 41 document.addEventListener("mousedown", handleClickOutside); 42 return () => { 43 document.removeEventListener("mousedown", handleClickOutside); 44 }; 45 }, []); 46 22 47 return ( 23 <div className="bg-gray-800 p-4 flex justify-between items-center"> 24 <Link to="/" className="text-white text-lg font-semibold"> 25 <img src={Logo} alt="Finkwave Logo" className="h-12 w-auto" /> 26 </Link> 48 <div 49 className={`bg-gray-800 p-4 flex justify-between items-center fixed top-0 right-0 z-50 transition-all duration-300 ${ 50 isSidebarOpen ? "left-64" : "left-0" 51 }`} 52 > 53 <div className="flex items-center gap-4"> 54 {onToggleSidebar && ( 55 <button 56 onClick={onToggleSidebar} 57 className="text-white hover:text-[#1db954] transition-colors p-2" 58 aria-label="Toggle sidebar" 59 > 60 <svg 61 className="w-6 h-6" 62 fill="none" 63 stroke="currentColor" 64 viewBox="0 0 24 24" 65 > 66 <path 67 strokeLinecap="round" 68 strokeLinejoin="round" 69 strokeWidth={2} 70 d="M4 6h16M4 12h16M4 18h16" 71 /> 72 </svg> 73 </button> 74 )} 75 <Link to="/" className="text-white text-lg font-semibold"> 76 <img src={Logo} alt="Finkwave Logo" className="h-12 w-auto" /> 77 </Link> 78 </div> 27 79 28 80 <div className="flex items-center space-x-4"> … … 30 82 <div className="flex items-center space-x-3"> 31 83 {user ? ( 32 <div className="flex items-center space-x-3"> 33 <div className="flex items-center space-x-2 bg-slate-700 rounded-lg px-3 py-2"> 84 <div className="relative" ref={dropdownRef}> 85 <button 86 onClick={() => setIsDropdownOpen(!isDropdownOpen)} 87 className="focus:outline-none focus:ring-2 focus:ring-blue-500 rounded-full" 88 > 34 89 {user.profilePhoto ? ( 35 90 <img 36 91 src={`${baseURL}/${user.profilePhoto}`} 37 92 alt={`${user.username}'s profile`} 38 className="w- 8 h-8 rounded-full object-cover"93 className="w-10 h-10 rounded-full object-cover cursor-pointer hover:ring-2 hover:ring-blue-400 transition-all" 39 94 /> 40 95 ) : ( 41 <div className="w- 8 h-8 bg-blue-500 rounded-full flex items-center justify-center">42 <span className="text-white text- smfont-semibold">96 <div className="w-10 h-10 bg-blue-500 rounded-full flex items-center justify-center cursor-pointer hover:ring-2 hover:ring-blue-400 transition-all"> 97 <span className="text-white text-lg font-semibold"> 43 98 {user.username.charAt(0).toUpperCase()} 44 99 </span> 45 100 </div> 46 101 )} 102 </button> 47 103 48 <div className="text-white"> 49 <p className="text-sm font-medium">{user.username}</p> 50 <p className="text-xs text-gray-300 capitalize"> 51 {user.role} 52 </p> 104 {isDropdownOpen && ( 105 <div className="absolute right-0 mt-2 w-48 bg-gray-700 rounded-lg shadow-lg py-1 z-50"> 106 <Link 107 to="#" 108 className="block px-4 py-2 text-sm text-white hover:bg-gray-600 transition-colors" 109 onClick={() => setIsDropdownOpen(false)} 110 > 111 Account 112 </Link> 113 <button 114 onClick={handleLogout} 115 className="w-full text-left px-4 py-2 text-sm text-white hover:bg-gray-600 transition-colors" 116 > 117 Log out 118 </button> 53 119 </div> 54 </div> 55 <button 56 onClick={handleLogout} 57 className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg text-sm 58 font-medium transition-colors duration-200 flex items-center space-x-1 cursor-pointer" 59 > 60 Logout 61 </button> 120 )} 62 121 </div> 63 122 ) : ( -
frontend/src/utils/types.ts
r15782a6 r694fc25 22 22 type: string; 23 23 releasedBy: string; 24 cover?: string | null; 24 25 isLikedByCurrentUser?: boolean; 25 26 }
Note:
See TracChangeset
for help on using the changeset viewer.
