Ignore:
Timestamp:
02/15/26 00:39:55 (5 months ago)
Author:
Dimitar Arsov <dimitararsov04@…>
Branches:
main
Children:
c8baad1
Parents:
85512ff
Message:

add song to playlist

File:
1 edited

Legend:

Unmodified
Added
Removed
  • frontend/src/pages/LandingPage.tsx

    r85512ff rce45c7a  
    1 import { useEffect, useRef, useState } from "react";
     1import { useEffect, useState } from "react";
    22import { useNavigate } from "react-router-dom";
    33import axiosInstance, { baseURL } from "../api/axiosInstance";
     4import PlaylistDropdown from "../components/PlaylistDropdown";
    45import AlbumResult from "../components/search/AlbumResult";
    56import SongResult from "../components/search/SongResult";
     
    78import { usePlayer } from "../context/playerContext";
    89import type {
    9         Album,
    10         BaseNonAdminUser,
    11         SearchCategory,
    12         Song,
     10  Album,
     11  BaseNonAdminUser,
     12  SearchCategory,
     13  Song,
    1314} from "../utils/types";
    1415import { toEmbedUrl } from "../utils/utils";
    1516
    1617const CATEGORIES: { value: SearchCategory; label: string }[] = [
    17         { value: "songs", label: "Songs" },
    18         { value: "albums", label: "Albums" },
    19         { value: "artists", label: "Artists" },
    20         { value: "users", label: "Users" },
     18  { value: "songs", label: "Songs" },
     19  { value: "albums", label: "Albums" },
     20  { value: "artists", label: "Artists" },
     21  { value: "users", label: "Users" },
    2122];
    2223
    2324const LandingPage = () => {
    24         const navigate = useNavigate();
    25         const { play, currentSong } = usePlayer();
    26         const [songs, setSongs] = useState<Song[]>([]);
    27         const [loading, setLoading] = useState<boolean>(true);
    28 
    29         // search state
    30         const [searchInput, setSearchInput] = useState("");
    31         const [activeQuery, setActiveQuery] = useState("");
    32         const [searchCategory, setSearchCategory] = useState<SearchCategory>("songs");
    33         const [searchResults, setSearchResults] = useState<unknown[]>([]);
    34         const [searchLoading, setSearchLoading] = useState(false);
    35         const [hasSearched, setHasSearched] = useState(false);
    36 
    37         // playlist dropdown state
    38         const [openPlaylistDropdown, setOpenPlaylistDropdown] = useState<
    39                 number | null
    40         >(null);
    41         const playlistDropdownRefs = useRef<{ [key: number]: HTMLDivElement | null }>(
    42                 {},
    43         );
    44 
    45         useEffect(() => {
    46                 const fetchSongs = async () => {
    47                         try {
    48                                 const response = await axiosInstance.get("/songs/top");
    49                                 setSongs(response.data);
    50                         } catch (error) {
    51                                 console.error("Error fetching songs:", error);
    52                         } finally {
    53                                 setLoading(false);
    54                         }
    55                 };
    56                 fetchSongs();
    57         }, []);
    58 
    59         // Handle click outside for playlist dropdown
    60         useEffect(() => {
    61                 const handleClickOutside = (event: MouseEvent) => {
    62                         if (openPlaylistDropdown !== null) {
    63                                 const ref = playlistDropdownRefs.current[openPlaylistDropdown];
    64                                 if (ref && !ref.contains(event.target as Node)) {
    65                                         setOpenPlaylistDropdown(null);
    66                                 }
    67                         }
    68                 };
    69 
    70                 document.addEventListener("mousedown", handleClickOutside);
    71                 return () => {
    72                         document.removeEventListener("mousedown", handleClickOutside);
    73                 };
    74         }, [openPlaylistDropdown]);
    75 
    76         const toggleLike = async (songId: number) => {
    77                 try {
    78                         await axiosInstance.post(`/musical-entity/${songId}/like`);
    79                         setSongs((prevSongs) =>
    80                                 prevSongs.map((song) =>
    81                                         song.id === songId
    82                                                 ? { ...song, isLikedByCurrentUser: !song.isLikedByCurrentUser }
    83                                                 : song,
    84                                 ),
    85                         );
    86                 } catch (error) {
    87                         console.error("Error toggling like:", error);
    88                 }
    89         };
    90 
    91         const togglePlaylistDropdown = (songId: number) => {
    92                 setOpenPlaylistDropdown((prev) => (prev === songId ? null : songId));
    93         };
    94 
    95         const handleAddToPlaylist = (songId: number, playlistName: string) => {
    96                 console.log(`Adding song ${songId} to ${playlistName}`);
    97                 // TODO: Implement actual API call
    98                 setOpenPlaylistDropdown(null);
    99         };
    100 
    101         const handleCreateNewPlaylist = (songId: number) => {
    102                 console.log(`Creating new playlist for song ${songId}`);
    103                 // TODO: Implement actual playlist creation
    104                 setOpenPlaylistDropdown(null);
    105         };
    106 
    107         const performSearch = async (query: string, category: SearchCategory) => {
    108                 if (!query.trim()) return;
    109 
    110                 setSearchLoading(true);
    111                 setHasSearched(true);
    112                 setActiveQuery(query);
    113                 setSearchCategory(category);
    114 
    115                 try {
    116                         let endpoint = "";
    117                         switch (category) {
    118                                 case "songs":
    119                                         endpoint = `/songs/search?q=${encodeURIComponent(query)}`;
    120                                         break;
    121                                 case "albums":
    122                                         endpoint = `/albums/search?q=${encodeURIComponent(query)}`;
    123                                         break;
    124                                 case "artists":
    125                                         endpoint = `/users/search?type=ARTIST&q=${encodeURIComponent(query)}`;
    126                                         break;
    127                                 case "users":
    128                                         endpoint = `/users/search?type=LISTENER&q=${encodeURIComponent(query)}`;
    129                                         break;
    130                         }
    131 
    132                         const response = await axiosInstance.get(endpoint);
    133                         setSearchResults(response.data);
    134                 } catch (error) {
    135                         console.error("Search error:", error);
    136                         setSearchResults([]);
    137                 } finally {
    138                         setSearchLoading(false);
    139                 }
    140         };
    141 
    142         const handleSearch = () => {
    143                 performSearch(searchInput, searchCategory);
    144         };
    145 
    146         const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    147                 if (e.key === "Enter") {
    148                         handleSearch();
    149                 }
    150         };
    151 
    152         const handleCategorySwitch = (category: SearchCategory) => {
    153                 performSearch(activeQuery, category);
    154         };
    155 
    156         const clearSearch = () => {
    157                 setSearchInput("");
    158                 setActiveQuery("");
    159                 setHasSearched(false);
    160                 setSearchResults([]);
    161         };
    162 
    163         const renderResults = () => {
    164                 if (searchLoading) {
    165                         return (
    166                                 <div className="flex justify-center py-12">
    167                                         <div className="w-10 h-10 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin"></div>
    168                                 </div>
    169                         );
    170                 }
    171 
    172                 if (searchResults.length === 0) {
    173                         return (
    174                                 <div className="text-center py-12 text-gray-400">
    175                                         <p className="text-lg">
    176                                                 No {searchCategory} found for "{activeQuery}"
    177                                         </p>
    178                                 </div>
    179                         );
    180                 }
    181 
    182                 return (
    183                         <div className="divide-y divide-white/5">
    184                                 {searchResults.map((result, index) => {
    185                                         switch (searchCategory) {
    186                                                 case "songs":
    187                                                         return (
    188                                                                 <SongResult key={(result as Song).id} song={result as Song} />
    189                                                         );
    190                                                 case "albums":
    191                                                         return (
    192                                                                 <AlbumResult
    193                                                                         key={(result as Album).id}
    194                                                                         album={result as Album}
    195                                                                 />
    196                                                         );
    197                                                 case "artists":
    198                                                         return (
    199                                                                 <UserResult
    200                                                                         key={(result as BaseNonAdminUser).username ?? index}
    201                                                                         user={result as BaseNonAdminUser}
    202                                                                         label="Artist"
    203                                                                 />
    204                                                         );
    205                                                 case "users":
    206                                                         return (
    207                                                                 <UserResult
    208                                                                         key={(result as BaseNonAdminUser).username ?? index}
    209                                                                         user={result as BaseNonAdminUser}
    210                                                                         label="User"
    211                                                                 />
    212                                                         );
    213                                         }
    214                                 })}
    215                         </div>
    216                 );
    217         };
    218 
    219         return (
    220                 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white flex">
    221                         <div className="flex-1">
    222                                 <div className="p-8">
    223                                         <div className="max-w-7xl mx-auto">
    224                                                 {/* Search Bar */}
    225                                                 <div className="mb-8 flex flex-col md:flex-row gap-3">
    226                                                         <div className="flex flex-1 gap-0">
    227                                                                 {/* Category Dropdown */}
    228                                                                 <select
    229                                                                         value={searchCategory}
    230                                                                         onChange={(e) =>
    231                                                                                 setSearchCategory(e.target.value as SearchCategory)
    232                                                                         }
    233                                                                         className="bg-[#282828] border border-white/10 border-r-0 rounded-l-full py-2 px-4 text-white text-sm focus:outline-none focus:border-[#1db954] appearance-none cursor-pointer"
    234                                                                 >
    235                                                                         {CATEGORIES.map((cat) => (
    236                                                                                 <option key={cat.value} value={cat.value}>
    237                                                                                         {cat.label}
    238                                                                                 </option>
    239                                                                         ))}
    240                                                                 </select>
    241 
    242                                                                 {/* Search Input */}
    243                                                                 <input
    244                                                                         type="text"
    245                                                                         placeholder={`Search ${searchCategory}...`}
    246                                                                         value={searchInput}
    247                                                                         onChange={(e) => setSearchInput(e.target.value)}
    248                                                                         onKeyDown={handleKeyDown}
    249                                                                         className="flex-1 bg-[#282828] border border-white/10 border-r-0 py-2 px-4 text-white focus:outline-none focus:border-[#1db954] focus:ring-1 focus:ring-[#1db954] transition-all"
    250                                                                 />
    251 
    252                                                                 {/* Search Button */}
    253                                                                 <button
    254                                                                         onClick={handleSearch}
    255                                                                         className="bg-[#1db954] hover:bg-[#1ed760] text-black font-medium px-6 rounded-r-full transition-colors flex items-center gap-2"
    256                                                                 >
    257                                                                         <svg
    258                                                                                 className="w-5 h-5"
    259                                                                                 fill="none"
    260                                                                                 stroke="currentColor"
    261                                                                                 viewBox="0 0 24 24"
    262                                                                         >
    263                                                                                 <path
    264                                                                                         strokeLinecap="round"
    265                                                                                         strokeLinejoin="round"
    266                                                                                         strokeWidth={2}
    267                                                                                         d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
    268                                                                                 />
    269                                                                         </svg>
    270                                                                         <span className="hidden sm:inline">Search</span>
    271                                                                 </button>
    272                                                         </div>
    273                                                 </div>
    274 
    275                                                 {/* Search Results Section */}
    276                                                 {hasSearched ? (
    277                                                         <div>
    278                                                                 <div className="flex items-center justify-between mb-4">
    279                                                                         <h2 className="text-2xl font-bold text-white">
    280                                                                                 Results for "{activeQuery}"
    281                                                                         </h2>
    282                                                                         <button
    283                                                                                 onClick={clearSearch}
    284                                                                                 className="text-sm text-gray-400 hover:text-white transition-colors"
    285                                                                         >
    286                                                                                 Clear search
    287                                                                         </button>
    288                                                                 </div>
    289 
    290                                                                 {/* Quick category switch buttons */}
    291                                                                 <div className="flex gap-2 mb-6">
    292                                                                         {CATEGORIES.map((cat) => (
    293                                                                                 <button
    294                                                                                         key={cat.value}
    295                                                                                         onClick={() => handleCategorySwitch(cat.value)}
    296                                                                                         className={`px-4 py-1.5 rounded-full text-sm font-medium transition-colors ${
    297                                                                                                 searchCategory === cat.value
    298                                                                                                         ? "bg-[#1db954] text-black"
    299                                                                                                         : "bg-white/10 text-white hover:bg-white/20"
    300                                                                                         }`}
    301                                                                                 >
    302                                                                                         {cat.label}
    303                                                                                 </button>
    304                                                                         ))}
    305                                                                 </div>
    306 
    307                                                                 {/* Results list */}
    308                                                                 <div className="bg-[#1a1a2e]/50 rounded-xl overflow-hidden">
    309                                                                         {renderResults()}
    310                                                                 </div>
    311                                                         </div>
    312                                                 ) : (
    313                                                         /* Default song grid */
    314                                                         <>
    315                                                                 <div className="mb-8 border-b border-white/10 pb-6">
    316                                                                         <h1 className="text-5xl font-bold mb-3 pb-1 bg-linear-to-r from-[#1db954] to-[#1ed760] bg-clip-text text-transparent">
    317                                                                                 Top Songs
    318                                                                         </h1>
    319                                                                         <p className="text-xl text-gray-400">
    320                                                                                 Listen to the newest tracks on FinkWave
    321                                                                         </p>
    322                                                                 </div>
    323 
    324                                                                 {loading ? (
    325                                                                         <div className="flex flex-col items-center justify-center min-h-[40vh] gap-6">
    326                                                                                 <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin"></div>
    327                                                                                 <p className="text-xl text-gray-400">Loading songs...</p>
    328                                                                         </div>
    329                                                                 ) : (
    330                                                                         <div className="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-8 py-4">
    331                                                                                 {songs.map((song) => (
    332                                                                                         <div
    333                                                                                                 key={song.id}
    334                                                                                                 onClick={() => navigate(`/songs/${song.id}`)}
    335                                                                                                 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"
    336                                                                                         >
    337                                                                                                 <div className="relative w-full pt-[100%] overflow-hidden bg-[#181818]">
    338                                                                                                         <img
    339                                                                                                                 src={
    340                                                                                                                         song.cover
    341                                                                                                                                 ? `${baseURL}/${song.cover}`
    342                                                                                                                                 : "/favicon.png"
    343                                                                                                                 }
    344                                                                                                                 alt={song.title}
    345                                                                                                                 className="absolute top-0 left-0 w-full h-full object-cover"
    346                                                                                                                 onError={(e) => {
    347                                                                                                                         (e.target as HTMLImageElement).src =
    348                                                                                                                                 "/favicon.png";
    349                                                                                                                 }}
    350                                                                                                         />
    351                                                                                                         <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">
    352                                                                                                                 {song.link ? (
    353                                                                                                                         <button
    354                                                                                                                                 onClick={(e) => {
    355                                                                                                                                         e.stopPropagation();
    356                                                                                                                                         play({
    357                                                                                                                                                 id: song.id,
    358                                                                                                                                                 title: song.title,
    359                                                                                                                                                 artist: song.releasedBy,
    360                                                                                                                                                 cover: song.cover,
    361                                                                                                                                                 embedUrl: toEmbedUrl(song.link!),
    362                                                                                                                                         });
    363                                                                                                                                 }}
    364                                                                                                                                 className={`w-14 h-14 rounded-full flex items-center justify-center text-xl shadow-[0_4px_12px_rgba(29,185,84,0.5)] transition-all cursor-pointer ${
    365                                                                                                                                         currentSong?.id === song.id
    366                                                                                                                                                 ? "bg-white text-[#1db954]"
    367                                                                                                                                                 : "bg-[#1db954] text-black hover:scale-105"
    368                                                                                                                                 }`}
    369                                                                                                                                 aria-label={
    370                                                                                                                                         currentSong?.id === song.id
    371                                                                                                                                                 ? "Now playing"
    372                                                                                                                                                 : "Play song"
    373                                                                                                                                 }
    374                                                                                                                         >
    375                                                                                                                                 {currentSong?.id === song.id ? (
    376                                                                                                                                         <svg
    377                                                                                                                                                 className="w-6 h-6"
    378                                                                                                                                                 fill="currentColor"
    379                                                                                                                                                 viewBox="0 0 24 24"
    380                                                                                                                                         >
    381                                                                                                                                                 <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
    382                                                                                                                                         </svg>
    383                                                                                                                                 ) : (
    384                                                                                                                                         <svg
    385                                                                                                                                                 className="w-6 h-6"
    386                                                                                                                                                 fill="currentColor"
    387                                                                                                                                                 viewBox="0 0 24 24"
    388                                                                                                                                         >
    389                                                                                                                                                 <path d="M8 5v14l11-7z" />
    390                                                                                                                                         </svg>
    391                                                                                                                                 )}
    392                                                                                                                         </button>
    393                                                                                                                 ) : (
    394                                                                                                                         <div className="w-14 h-14 rounded-full bg-[#1db954] flex items-center justify-center text-xl text-black shadow-[0_4px_12px_rgba(29,185,84,0.5)]">
    395                                                                                                                                 ▶
    396                                                                                                                         </div>
    397                                                                                                                 )}
    398                                                                                                         </div>
    399                                                                                                 </div>
    400                                                                                                 <div className="p-4">
    401                                                                                                         <h3 className="text-lg font-semibold mb-2 text-white overflow-hidden text-ellipsis whitespace-nowrap">
    402                                                                                                                 {song.title}
    403                                                                                                         </h3>
    404                                                                                                         {song.album && (
    405                                                                                                                 <p
    406                                                                                                                         onClick={(e) => {
    407                                                                                                                                 e.stopPropagation();
    408                                                                                                                                 if (song.albumId) {
    409                                                                                                                                         navigate(`/collection/album/${song.albumId}`);
    410                                                                                                                                 }
    411                                                                                                                         }}
    412                                                                                                                         className={`text-sm text-gray-400 mb-3 overflow-hidden text-ellipsis whitespace-nowrap ${
    413                                                                                                                                 song.albumId
    414                                                                                                                                         ? "hover:underline cursor-pointer hover:text-white"
    415                                                                                                                                         : ""
    416                                                                                                                         }`}
    417                                                                                                                 >
    418                                                                                                                         {song.album}
    419                                                                                                                 </p>
    420                                                                                                         )}
    421                                                                                                         <p
    422                                                                                                                 onClick={(e) => {
    423                                                                                                                         e.stopPropagation();
    424                                                                                                                         if (song.artistUsername) {
    425                                                                                                                                 navigate(`/users/${song.artistUsername}`);
    426                                                                                                                         }
    427                                                                                                                 }}
    428                                                                                                                 className={`text-sm text-gray-400 mb-3 overflow-hidden text-ellipsis whitespace-nowrap ${
    429                                                                                                                         song.artistUsername
    430                                                                                                                                 ? "hover:underline cursor-pointer hover:text-white"
    431                                                                                                                                 : ""
    432                                                                                                                 }`}
    433                                                                                                         >
    434                                                                                                                 {song.releasedBy}
    435                                                                                                         </p>
    436                                                                                                         <div className="flex items-center justify-between mt-4 pt-3 border-t border-white/10">
    437                                                                                                                 {/* Like button */}
    438                                                                                                                 <button
    439                                                                                                                         onClick={(e) => {
    440                                                                                                                                 e.stopPropagation();
    441                                                                                                                                 toggleLike(song.id);
    442                                                                                                                         }}
    443                                                                                                                         className="text-gray-400 hover:text-[#1db954] transition-colors cursor-pointer"
    444                                                                                                                         aria-label={
    445                                                                                                                                 song.isLikedByCurrentUser
    446                                                                                                                                         ? "Unlike song"
    447                                                                                                                                         : "Like song"
    448                                                                                                                         }
    449                                                                                                                 >
    450                                                                                                                         {song.isLikedByCurrentUser ? (
    451                                                                                                                                 <svg
    452                                                                                                                                         className="w-6 h-6 fill-[#1db954]"
    453                                                                                                                                         viewBox="0 0 24 24"
    454                                                                                                                                 >
    455                                                                                                                                         <path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
    456                                                                                                                                 </svg>
    457                                                                                                                         ) : (
    458                                                                                                                                 <svg
    459                                                                                                                                         className="w-6 h-6"
    460                                                                                                                                         fill="none"
    461                                                                                                                                         stroke="currentColor"
    462                                                                                                                                         viewBox="0 0 24 24"
    463                                                                                                                                 >
    464                                                                                                                                         <path
    465                                                                                                                                                 strokeLinecap="round"
    466                                                                                                                                                 strokeLinejoin="round"
    467                                                                                                                                                 strokeWidth={2}
    468                                                                                                                                                 d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
    469                                                                                                                                         />
    470                                                                                                                                 </svg>
    471                                                                                                                         )}
    472                                                                                                                 </button>
    473 
    474                                                                                                                 {/* Add to playlist button */}
    475                                                                                                                 <div
    476                                                                                                                         className="relative"
    477                                                                                                                         ref={(el) => {
    478                                                                                                                                 if (el)
    479                                                                                                                                         playlistDropdownRefs.current[song.id] = el;
    480                                                                                                                         }}
    481                                                                                                                 >
    482                                                                                                                         <button
    483                                                                                                                                 onClick={(e) => {
    484                                                                                                                                         e.stopPropagation();
    485                                                                                                                                         togglePlaylistDropdown(song.id);
    486                                                                                                                                 }}
    487                                                                                                                                 className="text-gray-400 hover:text-[#1db954] transition-colors cursor-pointer"
    488                                                                                                                                 aria-label="Add to playlist"
    489                                                                                                                         >
    490                                                                                                                                 <svg
    491                                                                                                                                         className="w-6 h-6"
    492                                                                                                                                         fill="none"
    493                                                                                                                                         stroke="currentColor"
    494                                                                                                                                         viewBox="0 0 24 24"
    495                                                                                                                                 >
    496                                                                                                                                         <path
    497                                                                                                                                                 strokeLinecap="round"
    498                                                                                                                                                 strokeLinejoin="round"
    499                                                                                                                                                 strokeWidth={2}
    500                                                                                                                                                 d="M12 4v16m8-8H4"
    501                                                                                                                                         />
    502                                                                                                                                 </svg>
    503                                                                                                                         </button>
    504 
    505                                                                                                                         {/* Playlist dropdown */}
    506                                                                                                                         {openPlaylistDropdown === song.id && (
    507                                                                                                                                 <div className="absolute right-0 bottom-full mb-2 w-48 bg-gray-700 rounded-lg shadow-lg py-1 z-50">
    508                                                                                                                                         <div className="px-3 py-2 text-xs text-gray-400 border-b border-white/10">
    509                                                                                                                                                 Add to playlist
    510                                                                                                                                         </div>
    511                                                                                                                                         <button
    512                                                                                                                                                 onClick={(e) => {
    513                                                                                                                                                         e.stopPropagation();
    514                                                                                                                                                         handleAddToPlaylist(
    515                                                                                                                                                                 song.id,
    516                                                                                                                                                                 "Playlist 1",
    517                                                                                                                                                         );
    518                                                                                                                                                 }}
    519                                                                                                                                                 className="w-full text-left px-4 py-2 text-sm text-white hover:bg-gray-600 transition-colors"
    520                                                                                                                                         >
    521                                                                                                                                                 Playlist 1
    522                                                                                                                                         </button>
    523                                                                                                                                         <button
    524                                                                                                                                                 onClick={(e) => {
    525                                                                                                                                                         e.stopPropagation();
    526                                                                                                                                                         handleAddToPlaylist(
    527                                                                                                                                                                 song.id,
    528                                                                                                                                                                 "Playlist 2",
    529                                                                                                                                                         );
    530                                                                                                                                                 }}
    531                                                                                                                                                 className="w-full text-left px-4 py-2 text-sm text-white hover:bg-gray-600 transition-colors"
    532                                                                                                                                         >
    533                                                                                                                                                 Playlist 2
    534                                                                                                                                         </button>
    535                                                                                                                                         <button
    536                                                                                                                                                 onClick={(e) => {
    537                                                                                                                                                         e.stopPropagation();
    538                                                                                                                                                         handleCreateNewPlaylist(song.id);
    539                                                                                                                                                 }}
    540                                                                                                                                                 className="w-full text-left px-4 py-2 text-sm text-[#1db954] hover:bg-gray-600 transition-colors border-t border-white/10 flex items-center gap-2"
    541                                                                                                                                         >
    542                                                                                                                                                 <svg
    543                                                                                                                                                         className="w-4 h-4"
    544                                                                                                                                                         fill="none"
    545                                                                                                                                                         stroke="currentColor"
    546                                                                                                                                                         viewBox="0 0 24 24"
    547                                                                                                                                                 >
    548                                                                                                                                                         <path
    549                                                                                                                                                                 strokeLinecap="round"
    550                                                                                                                                                                 strokeLinejoin="round"
    551                                                                                                                                                                 strokeWidth={2}
    552                                                                                                                                                                 d="M12 4v16m8-8H4"
    553                                                                                                                                                         />
    554                                                                                                                                                 </svg>
    555                                                                                                                                                 Create new playlist
    556                                                                                                                                         </button>
    557                                                                                                                                 </div>
    558                                                                                                                         )}
    559                                                                                                                 </div>
    560                                                                                                         </div>
    561                                                                                                 </div>
    562                                                                                         </div>
    563                                                                                 ))}
    564                                                                         </div>
    565                                                                 )}
    566                                                         </>
    567                                                 )}
    568                                         </div>
    569                                 </div>
    570                         </div>
    571                 </div>
    572         );
     25  const navigate = useNavigate();
     26  const { play, currentSong } = usePlayer();
     27  const [songs, setSongs] = useState<Song[]>([]);
     28  const [loading, setLoading] = useState<boolean>(true);
     29
     30  // search state
     31  const [searchInput, setSearchInput] = useState("");
     32  const [activeQuery, setActiveQuery] = useState("");
     33  const [searchCategory, setSearchCategory] = useState<SearchCategory>("songs");
     34  const [searchResults, setSearchResults] = useState<unknown[]>([]);
     35  const [searchLoading, setSearchLoading] = useState(false);
     36  const [hasSearched, setHasSearched] = useState(false);
     37
     38  // playlist dropdown state
     39  const [openPlaylistDropdown, setOpenPlaylistDropdown] = useState<
     40    number | null
     41  >(null);
     42
     43  useEffect(() => {
     44    const fetchSongs = async () => {
     45      try {
     46        const response = await axiosInstance.get("/songs/top");
     47        setSongs(response.data);
     48      } catch (error) {
     49        console.error("Error fetching songs:", error);
     50      } finally {
     51        setLoading(false);
     52      }
     53    };
     54    fetchSongs();
     55  }, []);
     56
     57  const toggleLike = async (songId: number) => {
     58    try {
     59      await axiosInstance.post(`/musical-entity/${songId}/like`);
     60      setSongs((prevSongs) =>
     61        prevSongs.map((song) =>
     62          song.id === songId
     63            ? { ...song, isLikedByCurrentUser: !song.isLikedByCurrentUser }
     64            : song,
     65        ),
     66      );
     67    } catch (error) {
     68      console.error("Error toggling like:", error);
     69    }
     70  };
     71
     72  const togglePlaylistDropdown = (songId: number) => {
     73    if (openPlaylistDropdown === songId) {
     74      setOpenPlaylistDropdown(null);
     75    } else {
     76      setOpenPlaylistDropdown(songId);
     77    }
     78  };
     79
     80  const handleCreateNewPlaylist = (songId: number) => {
     81    console.log(`Creating new playlist for song ${songId}`);
     82    // TODO: Implement actual playlist creation
     83    setOpenPlaylistDropdown(null);
     84  };
     85
     86  const performSearch = async (query: string, category: SearchCategory) => {
     87    if (!query.trim()) return;
     88
     89    setSearchLoading(true);
     90    setHasSearched(true);
     91    setActiveQuery(query);
     92    setSearchCategory(category);
     93
     94    try {
     95      let endpoint = "";
     96      switch (category) {
     97        case "songs":
     98          endpoint = `/songs/search?q=${encodeURIComponent(query)}`;
     99          break;
     100        case "albums":
     101          endpoint = `/albums/search?q=${encodeURIComponent(query)}`;
     102          break;
     103        case "artists":
     104          endpoint = `/users/search?type=ARTIST&q=${encodeURIComponent(query)}`;
     105          break;
     106        case "users":
     107          endpoint = `/users/search?type=LISTENER&q=${encodeURIComponent(query)}`;
     108          break;
     109      }
     110
     111      const response = await axiosInstance.get(endpoint);
     112      setSearchResults(response.data);
     113    } catch (error) {
     114      console.error("Search error:", error);
     115      setSearchResults([]);
     116    } finally {
     117      setSearchLoading(false);
     118    }
     119  };
     120
     121  const handleSearch = () => {
     122    performSearch(searchInput, searchCategory);
     123  };
     124
     125  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
     126    if (e.key === "Enter") {
     127      handleSearch();
     128    }
     129  };
     130
     131  const handleCategorySwitch = (category: SearchCategory) => {
     132    performSearch(activeQuery, category);
     133  };
     134
     135  const clearSearch = () => {
     136    setSearchInput("");
     137    setActiveQuery("");
     138    setHasSearched(false);
     139    setSearchResults([]);
     140  };
     141
     142  const renderResults = () => {
     143    if (searchLoading) {
     144      return (
     145        <div className="flex justify-center py-12">
     146          <div className="w-10 h-10 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin"></div>
     147        </div>
     148      );
     149    }
     150
     151    if (searchResults.length === 0) {
     152      return (
     153        <div className="text-center py-12 text-gray-400">
     154          <p className="text-lg">
     155            No {searchCategory} found for "{activeQuery}"
     156          </p>
     157        </div>
     158      );
     159    }
     160
     161    return (
     162      <div className="divide-y divide-white/5">
     163        {searchResults.map((result, index) => {
     164          switch (searchCategory) {
     165            case "songs":
     166              return (
     167                <SongResult key={(result as Song).id} song={result as Song} />
     168              );
     169            case "albums":
     170              return (
     171                <AlbumResult
     172                  key={(result as Album).id}
     173                  album={result as Album}
     174                />
     175              );
     176            case "artists":
     177              return (
     178                <UserResult
     179                  key={(result as BaseNonAdminUser).username ?? index}
     180                  user={result as BaseNonAdminUser}
     181                  label="Artist"
     182                />
     183              );
     184            case "users":
     185              return (
     186                <UserResult
     187                  key={(result as BaseNonAdminUser).username ?? index}
     188                  user={result as BaseNonAdminUser}
     189                  label="User"
     190                />
     191              );
     192          }
     193        })}
     194      </div>
     195    );
     196  };
     197
     198  return (
     199    <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white flex">
     200      <div className="flex-1">
     201        <div className="p-8">
     202          <div className="max-w-7xl mx-auto">
     203            {/* Search Bar */}
     204            <div className="mb-8 flex flex-col md:flex-row gap-3">
     205              <div className="flex flex-1 gap-0">
     206                {/* Category Dropdown */}
     207                <select
     208                  value={searchCategory}
     209                  onChange={(e) =>
     210                    setSearchCategory(e.target.value as SearchCategory)
     211                  }
     212                  className="bg-[#282828] border border-white/10 border-r-0 rounded-l-full py-2 px-4 text-white text-sm focus:outline-none focus:border-[#1db954] appearance-none cursor-pointer"
     213                >
     214                  {CATEGORIES.map((cat) => (
     215                    <option key={cat.value} value={cat.value}>
     216                      {cat.label}
     217                    </option>
     218                  ))}
     219                </select>
     220
     221                {/* Search Input */}
     222                <input
     223                  type="text"
     224                  placeholder={`Search ${searchCategory}...`}
     225                  value={searchInput}
     226                  onChange={(e) => setSearchInput(e.target.value)}
     227                  onKeyDown={handleKeyDown}
     228                  className="flex-1 bg-[#282828] border border-white/10 border-r-0 py-2 px-4 text-white focus:outline-none focus:border-[#1db954] focus:ring-1 focus:ring-[#1db954] transition-all"
     229                />
     230
     231                {/* Search Button */}
     232                <button
     233                  onClick={handleSearch}
     234                  className="bg-[#1db954] hover:bg-[#1ed760] text-black font-medium px-6 rounded-r-full transition-colors flex items-center gap-2"
     235                >
     236                  <svg
     237                    className="w-5 h-5"
     238                    fill="none"
     239                    stroke="currentColor"
     240                    viewBox="0 0 24 24"
     241                  >
     242                    <path
     243                      strokeLinecap="round"
     244                      strokeLinejoin="round"
     245                      strokeWidth={2}
     246                      d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
     247                    />
     248                  </svg>
     249                  <span className="hidden sm:inline">Search</span>
     250                </button>
     251              </div>
     252            </div>
     253
     254            {/* Search Results Section */}
     255            {hasSearched ? (
     256              <div>
     257                <div className="flex items-center justify-between mb-4">
     258                  <h2 className="text-2xl font-bold text-white">
     259                    Results for "{activeQuery}"
     260                  </h2>
     261                  <button
     262                    onClick={clearSearch}
     263                    className="text-sm text-gray-400 hover:text-white transition-colors"
     264                  >
     265                    Clear search
     266                  </button>
     267                </div>
     268
     269                {/* Quick category switch buttons */}
     270                <div className="flex gap-2 mb-6">
     271                  {CATEGORIES.map((cat) => (
     272                    <button
     273                      key={cat.value}
     274                      onClick={() => handleCategorySwitch(cat.value)}
     275                      className={`px-4 py-1.5 rounded-full text-sm font-medium transition-colors ${
     276                        searchCategory === cat.value
     277                          ? "bg-[#1db954] text-black"
     278                          : "bg-white/10 text-white hover:bg-white/20"
     279                      }`}
     280                    >
     281                      {cat.label}
     282                    </button>
     283                  ))}
     284                </div>
     285
     286                {/* Results list */}
     287                <div className="bg-[#1a1a2e]/50 rounded-xl overflow-hidden">
     288                  {renderResults()}
     289                </div>
     290              </div>
     291            ) : (
     292              /* Default song grid */
     293              <>
     294                <div className="mb-8 border-b border-white/10 pb-6">
     295                  <h1 className="text-5xl font-bold mb-3 pb-1 bg-linear-to-r from-[#1db954] to-[#1ed760] bg-clip-text text-transparent">
     296                    Top Songs
     297                  </h1>
     298                  <p className="text-xl text-gray-400">
     299                    Listen to the newest tracks on FinkWave
     300                  </p>
     301                </div>
     302
     303                {loading ? (
     304                  <div className="flex flex-col items-center justify-center min-h-[40vh] gap-6">
     305                    <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin"></div>
     306                    <p className="text-xl text-gray-400">Loading songs...</p>
     307                  </div>
     308                ) : (
     309                  <div className="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-8 py-4">
     310                    {songs.map((song) => (
     311                      <div
     312                        key={song.id}
     313                        onClick={() => navigate(`/songs/${song.id}`)}
     314                        onMouseEnter={() => {
     315                          if (
     316                            openPlaylistDropdown !== null &&
     317                            openPlaylistDropdown !== song.id
     318                          ) {
     319                            setOpenPlaylistDropdown(null);
     320                          }
     321                        }}
     322                        className="bg-[#282828] rounded-xl cursor-pointer shadow-lg transition-all duration-300 ease-in-out hover:-translate-y-2 hover:shadow-2xl group relative"
     323                      >
     324                        <div className="relative w-full pt-[100%] overflow-hidden bg-[#181818] rounded-t-xl">
     325                          <img
     326                            src={
     327                              song.cover
     328                                ? `${baseURL}/${song.cover}`
     329                                : "/favicon.png"
     330                            }
     331                            alt={song.title}
     332                            className="absolute top-0 left-0 w-full h-full object-cover"
     333                            onError={(e) => {
     334                              (e.target as HTMLImageElement).src =
     335                                "/favicon.png";
     336                            }}
     337                          />
     338                          <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">
     339                            {song.link ? (
     340                              <button
     341                                onClick={(e) => {
     342                                  e.stopPropagation();
     343                                  play({
     344                                    id: song.id,
     345                                    title: song.title,
     346                                    artist: song.releasedBy,
     347                                    cover: song.cover,
     348                                    embedUrl: toEmbedUrl(song.link!),
     349                                  });
     350                                }}
     351                                className={`w-14 h-14 rounded-full flex items-center justify-center text-xl shadow-[0_4px_12px_rgba(29,185,84,0.5)] transition-all cursor-pointer ${
     352                                  currentSong?.id === song.id
     353                                    ? "bg-white text-[#1db954]"
     354                                    : "bg-[#1db954] text-black hover:scale-105"
     355                                }`}
     356                                aria-label={
     357                                  currentSong?.id === song.id
     358                                    ? "Now playing"
     359                                    : "Play song"
     360                                }
     361                              >
     362                                {currentSong?.id === song.id ? (
     363                                  <svg
     364                                    className="w-6 h-6"
     365                                    fill="currentColor"
     366                                    viewBox="0 0 24 24"
     367                                  >
     368                                    <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
     369                                  </svg>
     370                                ) : (
     371                                  <svg
     372                                    className="w-6 h-6"
     373                                    fill="currentColor"
     374                                    viewBox="0 0 24 24"
     375                                  >
     376                                    <path d="M8 5v14l11-7z" />
     377                                  </svg>
     378                                )}
     379                              </button>
     380                            ) : (
     381                              <div className="w-14 h-14 rounded-full bg-[#1db954] flex items-center justify-center text-xl text-black shadow-[0_4px_12px_rgba(29,185,84,0.5)]">
     382                                ▶
     383                              </div>
     384                            )}
     385                          </div>
     386                        </div>
     387                        <div className="p-4">
     388                          <h3 className="text-lg font-semibold mb-2 text-white overflow-hidden text-ellipsis whitespace-nowrap">
     389                            {song.title}
     390                          </h3>
     391                          {song.album && (
     392                            <p
     393                              onClick={(e) => {
     394                                e.stopPropagation();
     395                                if (song.albumId) {
     396                                  navigate(`/collection/album/${song.albumId}`);
     397                                }
     398                              }}
     399                              className={`text-sm text-gray-400 mb-3 overflow-hidden text-ellipsis whitespace-nowrap ${
     400                                song.albumId
     401                                  ? "hover:underline cursor-pointer hover:text-white"
     402                                  : ""
     403                              }`}
     404                            >
     405                              {song.album}
     406                            </p>
     407                          )}
     408                          <p
     409                            onClick={(e) => {
     410                              e.stopPropagation();
     411                              if (song.artistUsername) {
     412                                navigate(`/users/${song.artistUsername}`);
     413                              }
     414                            }}
     415                            className={`text-sm text-gray-400 mb-3 overflow-hidden text-ellipsis whitespace-nowrap ${
     416                              song.artistUsername
     417                                ? "hover:underline cursor-pointer hover:text-white"
     418                                : ""
     419                            }`}
     420                          >
     421                            {song.releasedBy}
     422                          </p>
     423                          <div className="flex items-center justify-between mt-4 pt-3 border-t border-white/10">
     424                            {/* Like button */}
     425                            <button
     426                              onClick={(e) => {
     427                                e.stopPropagation();
     428                                toggleLike(song.id);
     429                              }}
     430                              className="text-gray-400 hover:text-[#1db954] transition-colors cursor-pointer"
     431                              aria-label={
     432                                song.isLikedByCurrentUser
     433                                  ? "Unlike song"
     434                                  : "Like song"
     435                              }
     436                            >
     437                              {song.isLikedByCurrentUser ? (
     438                                <svg
     439                                  className="w-6 h-6 fill-[#1db954]"
     440                                  viewBox="0 0 24 24"
     441                                >
     442                                  <path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
     443                                </svg>
     444                              ) : (
     445                                <svg
     446                                  className="w-6 h-6"
     447                                  fill="none"
     448                                  stroke="currentColor"
     449                                  viewBox="0 0 24 24"
     450                                >
     451                                  <path
     452                                    strokeLinecap="round"
     453                                    strokeLinejoin="round"
     454                                    strokeWidth={2}
     455                                    d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
     456                                  />
     457                                </svg>
     458                              )}
     459                            </button>
     460
     461                            {/* Add to playlist button */}
     462                            <div className="relative">
     463                              <button
     464                                onClick={(e) => {
     465                                  e.stopPropagation();
     466                                  togglePlaylistDropdown(song.id);
     467                                }}
     468                                className="text-gray-400 hover:text-[#1db954] transition-colors cursor-pointer"
     469                                aria-label="Add to playlist"
     470                              >
     471                                <svg
     472                                  className="w-6 h-6"
     473                                  fill="none"
     474                                  stroke="currentColor"
     475                                  viewBox="0 0 24 24"
     476                                >
     477                                  <path
     478                                    strokeLinecap="round"
     479                                    strokeLinejoin="round"
     480                                    strokeWidth={2}
     481                                    d="M12 4v16m8-8H4"
     482                                  />
     483                                </svg>
     484                              </button>
     485
     486                              <PlaylistDropdown
     487                                songId={song.id}
     488                                isOpen={openPlaylistDropdown === song.id}
     489                                onClose={() => setOpenPlaylistDropdown(null)}
     490                                onCreateNewPlaylist={() =>
     491                                  handleCreateNewPlaylist(song.id)
     492                                }
     493                                direction="above"
     494                              />
     495                            </div>
     496                          </div>
     497                        </div>
     498                      </div>
     499                    ))}
     500                  </div>
     501                )}
     502              </>
     503            )}
     504          </div>
     505        </div>
     506      </div>
     507    </div>
     508  );
    573509};
    574510
Note: See TracChangeset for help on using the changeset viewer.