Changeset ce45c7a for frontend/src/pages


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

Location:
frontend/src/pages
Files:
4 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
  • frontend/src/pages/MusicalCollection.tsx

    r85512ff rce45c7a  
    2121  const [isLoading, setIsLoading] = useState(true);
    2222  const [error, setError] = useState<string | null>(null);
     23  const [openDropdownSongId, setOpenDropdownSongId] = useState<number | null>(
     24    null,
     25  );
    2326
    2427  const normalizeCollection = (
     
    236239                  index={index + 1}
    237240                  onLikeToggle={() => toggleLike(song.id)}
     241                  isDropdownOpen={openDropdownSongId === song.id}
     242                  onDropdownToggle={setOpenDropdownSongId}
    238243                />
    239244              ))}
  • frontend/src/pages/SongDetail.tsx

    r85512ff rce45c7a  
    1 import { useEffect, useRef, useState } from "react";
     1import { useEffect, useState } from "react";
    22import { Link, useParams } from "react-router-dom";
    33import { toast } from "react-toastify";
    44import axiosInstance, { baseURL } from "../api/axiosInstance";
     5import PlaylistDropdown from "../components/PlaylistDropdown";
    56import { useAuth } from "../context/authContext";
    67import { usePlayer } from "../context/playerContext";
     
    910
    1011const ROLE_LABELS: Record<string, string> = {
    11         MAIN_VOCAL: "Main Vocal",
    12         FEATURED: "Featured",
    13         PRODUCER: "Producer",
    14         SONGWRITER: "Songwriter",
    15         COMPOSER: "Composer",
    16         MIXER: "Mixer",
    17         ENGINEER: "Engineer",
     12  MAIN_VOCAL: "Main Vocal",
     13  FEATURED: "Featured",
     14  PRODUCER: "Producer",
     15  SONGWRITER: "Songwriter",
     16  COMPOSER: "Composer",
     17  MIXER: "Mixer",
     18  ENGINEER: "Engineer",
    1819};
    1920
    2021const formatRole = (role: string): string => {
    21         return ROLE_LABELS[role] || role.replace(/_/g, " ");
     22  return ROLE_LABELS[role] || role.replace(/_/g, " ");
    2223};
    2324
    2425const renderStars = (grade: number) => {
    25         return (
    26                 <div className="flex gap-0.5">
    27                         {[1, 2, 3, 4, 5].map((star) => (
    28                                 <svg
    29                                         key={star}
    30                                         className={`w-4 h-4 ${star <= grade ? "text-yellow-400" : "text-gray-600"}`}
    31                                         fill="currentColor"
    32                                         viewBox="0 0 20 20"
    33                                 >
    34                                         <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
    35                                 </svg>
    36                         ))}
    37                 </div>
    38         );
     26  return (
     27    <div className="flex gap-0.5">
     28      {[1, 2, 3, 4, 5].map((star) => (
     29        <svg
     30          key={star}
     31          className={`w-4 h-4 ${star <= grade ? "text-yellow-400" : "text-gray-600"}`}
     32          fill="currentColor"
     33          viewBox="0 0 20 20"
     34        >
     35          <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
     36        </svg>
     37      ))}
     38    </div>
     39  );
    3940};
    4041
    4142const SongDetail = () => {
    42         const { id } = useParams<{ id: string }>();
    43         const { user } = useAuth();
    44         const { play, currentSong } = usePlayer();
    45         const [song, setSong] = useState<SongDetailType | null>(null);
    46         const [loading, setLoading] = useState(true);
    47         const [error, setError] = useState<string | null>(null);
    48         const [showReviewModal, setShowReviewModal] = useState(false);
    49         const [reviewRating, setReviewRating] = useState(0);
    50         const [reviewComment, setReviewComment] = useState("");
    51         const [hoverRating, setHoverRating] = useState(0);
    52         const [showPlaylistDropdown, setShowPlaylistDropdown] = useState(false);
    53         const playlistDropdownRef = useRef<HTMLDivElement>(null);
    54         console.log(user);
    55         useEffect(() => {
    56                 const fetchSong = async () => {
    57                         try {
    58                                 setLoading(true);
    59                                 const response = await axiosInstance.get(`/songs/${id}/details`);
    60                                 setSong(response.data);
    61                         } catch (err) {
    62                                 console.error("Error fetching song details:", err);
    63                                 setError("Failed to load song details.");
    64                         } finally {
    65                                 setLoading(false);
    66                         }
    67                 };
    68                 if (id) fetchSong();
    69         }, [id]);
    70 
    71         // handle click outside for playlist dropdown
    72         useEffect(() => {
    73                 const handleClickOutside = (event: MouseEvent) => {
    74                         if (
    75                                 playlistDropdownRef.current &&
    76                                 !playlistDropdownRef.current.contains(event.target as Node)
    77                         ) {
    78                                 setShowPlaylistDropdown(false);
    79                         }
    80                 };
    81 
    82                 document.addEventListener("mousedown", handleClickOutside);
    83                 return () => {
    84                         document.removeEventListener("mousedown", handleClickOutside);
    85                 };
    86         }, []);
    87 
    88         const handleSubmitReview = async () => {
    89                 if (reviewRating === 0) {
    90                         // todo: replace with toast
    91                         alert("Please select a rating");
    92                         return;
    93                 }
    94                 try {
    95                         await axiosInstance.post(`reviews/${song?.id}`, {
    96                                 grade: reviewRating,
    97                                 comment: reviewComment,
    98                         });
    99                         const response = await axiosInstance.get(`/songs/${id}/details`);
    100                         setSong(response.data);
    101                 } catch (err) {
    102                         // todo: replace with toast
    103                         console.error("Error submitting review:", err);
    104                 } finally {
    105                         setShowReviewModal(false);
    106                         setReviewRating(0);
    107                         setReviewComment("");
    108                         setHoverRating(0);
    109                 }
    110         };
    111 
    112         const deleteReview = async () => {
    113                 try {
    114                         await axiosInstance.delete(`reviews/${song?.id}`);
    115                         const response = await axiosInstance.get(`/songs/${id}/details`);
    116                         setSong(response.data);
    117                 } catch (err) {
    118                         toast.error("Failed to delete review");
    119                         console.error("Error deleting review:", err);
    120                 }
    121         };
    122 
    123         const toggleLike = async () => {
    124                 if (!song) return;
    125                 try {
    126                         await axiosInstance.post(`/musical-entity/${song.id}/like`);
    127                         setSong((prev) =>
    128                                 prev
    129                                         ? { ...prev, isLikedByCurrentUser: !prev.isLikedByCurrentUser }
    130                                         : prev,
    131                         );
    132                 } catch (err) {
    133                         toast.error("Failed to toggle like");
    134                         console.error("Error toggling like:", err);
    135                 }
    136         };
    137 
    138         const handleAddToPlaylist = (playlistName: string) => {
    139                 console.log(`Adding song ${song?.id} to ${playlistName}`);
    140                 // TODO: actual API call
    141                 setShowPlaylistDropdown(false);
    142         };
    143 
    144         const handleCreateNewPlaylist = () => {
    145                 console.log(`Creating new playlist for song ${song?.id}`);
    146                 // TODO: actual playlist creation
    147                 setShowPlaylistDropdown(false);
    148         };
    149 
    150         if (loading) {
    151                 return (
    152                         <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
    153                                 <div className="flex flex-col items-center gap-4">
    154                                         <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin" />
    155                                         <p className="text-gray-400 text-lg">Loading song…</p>
    156                                 </div>
    157                         </div>
    158                 );
    159         }
    160 
    161         if (error || !song) {
    162                 return (
    163                         <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
    164                                 <div className="text-center">
    165                                         <p className="text-red-400 text-xl mb-4">
    166                                                 {error ?? "Song not found"}
    167                                         </p>
    168                                         <Link to="/" className="text-[#1db954] hover:underline text-sm">
    169                                                 ← Back to Home
    170                                         </Link>
    171                                 </div>
    172                         </div>
    173                 );
    174         }
    175 
    176         const otherContributors = song.contributions.filter(
    177                 (c) => c.artistName !== song.releasedBy,
    178         );
    179 
    180         const avgRating =
    181                 song.reviews.length > 0
    182                         ? (
    183                                         song.reviews.reduce((sum, r) => sum + r.grade, 0) /
    184                                         song.reviews.length
    185                                 ).toFixed(1)
    186                         : null;
    187 
    188         return (
    189                 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white">
    190                         <div className="max-w-5xl mx-auto px-6 py-10">
    191                                 {/* Back link */}
    192                                 <Link
    193                                         to="/"
    194                                         className="inline-flex items-center gap-1 text-gray-400 hover:text-white text-sm mb-8 transition-colors"
    195                                 >
    196                                         <svg
    197                                                 className="w-4 h-4"
    198                                                 fill="none"
    199                                                 stroke="currentColor"
    200                                                 viewBox="0 0 24 24"
    201                                         >
    202                                                 <path
    203                                                         strokeLinecap="round"
    204                                                         strokeLinejoin="round"
    205                                                         strokeWidth={2}
    206                                                         d="M15 19l-7-7 7-7"
    207                                                 />
    208                                         </svg>
    209                                         Back to Home
    210                                 </Link>
    211 
    212                                 {/* Hero section */}
    213                                 <div className="flex flex-col md:flex-row gap-8 mb-10">
    214                                         {/* Cover art */}
    215                                         <div className="w-full md:w-72 shrink-0">
    216                                                 <div className="relative w-full pt-[100%] rounded-xl overflow-hidden shadow-2xl bg-[#181818]">
    217                                                         <img
    218                                                                 src={song.cover ? `${baseURL}/${song.cover}` : "/favicon.png"}
    219                                                                 alt={song.title}
    220                                                                 className="absolute inset-0 w-full h-full object-cover"
    221                                                                 onError={(e) => {
    222                                                                         (e.target as HTMLImageElement).src = "/favicon.png";
    223                                                                 }}
    224                                                         />
    225                                                 </div>
    226                                         </div>
    227 
    228                                         {/* Song info */}
    229                                         <div className="flex flex-col justify-end gap-3 min-w-0">
    230                                                 <span className="text-xs uppercase tracking-widest text-gray-400 font-medium">
    231                                                         {song.genre} • Song
    232                                                 </span>
    233                                                 <h1 className="text-4xl md:text-5xl font-extrabold leading-tight truncate">
    234                                                         {song.title}
    235                                                 </h1>
    236 
    237                                                 {/* Main artist */}
    238                                                 <p className="text-xl text-gray-300 font-semibold">
    239                                                         {song.releasedBy}
    240                                                 </p>
    241 
    242                                                 {/* Album */}
    243                                                 {song.album && (
    244                                                         <p className="text-sm text-gray-500">
    245                                                                 Album: <span className="text-gray-300">{song.album}</span>
    246                                                         </p>
    247                                                 )}
    248 
    249                                                 {/* Rating summary */}
    250                                                 {avgRating && (
    251                                                         <div className="flex items-center gap-2 mt-1">
    252                                                                 {renderStars(Math.round(Number(avgRating)))}
    253                                                                 <span className="text-sm text-gray-400">
    254                                                                         {avgRating} · {song.reviews.length}{" "}
    255                                                                         {song.reviews.length === 1 ? "review" : "reviews"}
    256                                                                 </span>
    257                                                         </div>
    258                                                 )}
    259 
    260                                                 {/* Action buttons */}
    261                                                 <div className="flex items-center gap-3 mt-4">
    262                                                         {/* Play button */}
    263                                                         {song.link && (
    264                                                                 <button
    265                                                                         onClick={() =>
    266                                                                                 play({
    267                                                                                         id: song.id,
    268                                                                                         title: song.title,
    269                                                                                         artist: song.releasedBy,
    270                                                                                         cover: song.cover,
    271                                                                                         embedUrl: toEmbedUrl(song.link!),
    272                                                                                 })
    273                                                                         }
    274                                                                         className={`flex items-center gap-2 px-6 py-3 rounded-full text-sm font-semibold transition-all cursor-pointer ${
    275                                                                                 currentSong?.id === song.id
    276                                                                                         ? "bg-white text-[#1db954]"
    277                                                                                         : "bg-[#1db954] text-black hover:bg-[#1ed760] hover:scale-105"
    278                                                                         }`}
    279                                                                 >
    280                                                                         {currentSong?.id === song.id ? (
    281                                                                                 <>
    282                                                                                         <svg
    283                                                                                                 className="w-5 h-5"
    284                                                                                                 fill="currentColor"
    285                                                                                                 viewBox="0 0 24 24"
    286                                                                                         >
    287                                                                                                 <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
    288                                                                                         </svg>
    289                                                                                         Now Playing
    290                                                                                 </>
    291                                                                         ) : (
    292                                                                                 <>
    293                                                                                         <svg
    294                                                                                                 className="w-5 h-5"
    295                                                                                                 fill="currentColor"
    296                                                                                                 viewBox="0 0 24 24"
    297                                                                                         >
    298                                                                                                 <path d="M8 5v14l11-7z" />
    299                                                                                         </svg>
    300                                                                                         Play Song
    301                                                                                 </>
    302                                                                         )}
    303                                                                 </button>
    304                                                         )}
    305 
    306                                                         {user && (
    307                                                                 <>
    308                                                                         <button
    309                                                                                 onClick={toggleLike}
    310                                                                                 className={`flex items-center gap-2 px-5 py-3 rounded-full text-sm font-semibold transition-colors cursor-pointer ${
    311                                                                                         song.isLikedByCurrentUser
    312                                                                                                 ? "bg-[#1db954] text-black"
    313                                                                                                 : "bg-white/10 text-white hover:bg-white/20"
    314                                                                                 }`}
    315                                                                         >
    316                                                                                 {song.isLikedByCurrentUser ? (
    317                                                                                         <svg
    318                                                                                                 className="w-5 h-5"
    319                                                                                                 fill="currentColor"
    320                                                                                                 viewBox="0 0 24 24"
    321                                                                                         >
    322                                                                                                 <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" />
    323                                                                                         </svg>
    324                                                                                 ) : (
    325                                                                                         <svg
    326                                                                                                 className="w-5 h-5"
    327                                                                                                 fill="none"
    328                                                                                                 stroke="currentColor"
    329                                                                                                 viewBox="0 0 24 24"
    330                                                                                         >
    331                                                                                                 <path
    332                                                                                                         strokeLinecap="round"
    333                                                                                                         strokeLinejoin="round"
    334                                                                                                         strokeWidth={2}
    335                                                                                                         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"
    336                                                                                                 />
    337                                                                                         </svg>
    338                                                                                 )}
    339                                                                                 {song.isLikedByCurrentUser ? "Liked" : "Like"}
    340                                                                         </button>
    341 
    342                                                                         {/* Add to Playlist button */}
    343                                                                         <div className="relative" ref={playlistDropdownRef}>
    344                                                                                 <button
    345                                                                                         onClick={() =>
    346                                                                                                 setShowPlaylistDropdown(!showPlaylistDropdown)
    347                                                                                         }
    348                                                                                         className="flex items-center gap-2 px-5 py-3 rounded-full text-sm font-semibold bg-white/10 text-white hover:bg-white/20 transition-colors cursor-pointer"
    349                                                                                 >
    350                                                                                         <svg
    351                                                                                                 className="w-5 h-5"
    352                                                                                                 fill="none"
    353                                                                                                 stroke="currentColor"
    354                                                                                                 viewBox="0 0 24 24"
    355                                                                                         >
    356                                                                                                 <path
    357                                                                                                         strokeLinecap="round"
    358                                                                                                         strokeLinejoin="round"
    359                                                                                                         strokeWidth={2}
    360                                                                                                         d="M12 4v16m8-8H4"
    361                                                                                                 />
    362                                                                                         </svg>
    363                                                                                         Add to Playlist
    364                                                                                 </button>
    365 
    366                                                                                 {/* Playlist dropdown */}
    367                                                                                 {showPlaylistDropdown && (
    368                                                                                         <div className="absolute left-0 top-full mt-2 w-56 bg-[#282828] rounded-lg shadow-2xl py-1 z-50 border border-white/10">
    369                                                                                                 <div className="px-3 py-2 text-xs text-gray-400 border-b border-white/10">
    370                                                                                                         Add to playlist
    371                                                                                                 </div>
    372                                                                                                 <button
    373                                                                                                         onClick={() => handleAddToPlaylist("Hello")}
    374                                                                                                         className="w-full text-left px-4 py-2.5 text-sm text-white hover:bg-white/10 transition-colors"
    375                                                                                                 >
    376                                                                                                         Hello
    377                                                                                                 </button>
    378                                                                                                 <button
    379                                                                                                         onClick={() => handleAddToPlaylist("Dimi")}
    380                                                                                                         className="w-full text-left px-4 py-2.5 text-sm text-white hover:bg-white/10 transition-colors"
    381                                                                                                 >
    382                                                                                                         Dimi
    383                                                                                                 </button>
    384                                                                                                 <button
    385                                                                                                         onClick={() => handleAddToPlaylist("lmao")}
    386                                                                                                         className="w-full text-left px-4 py-2.5 text-sm text-white hover:bg-white/10 transition-colors"
    387                                                                                                 >
    388                                                                                                         Kako hello world ama ti si mojot world
    389                                                                                                 </button>
    390                                                                                                 <button
    391                                                                                                         onClick={handleCreateNewPlaylist}
    392                                                                                                         className="w-full text-left px-4 py-2.5 text-sm text-[#1db954] hover:bg-white/10 transition-colors border-t border-white/10 flex items-center gap-2"
    393                                                                                                 >
    394                                                                                                         <svg
    395                                                                                                                 className="w-4 h-4"
    396                                                                                                                 fill="none"
    397                                                                                                                 stroke="currentColor"
    398                                                                                                                 viewBox="0 0 24 24"
    399                                                                                                         >
    400                                                                                                                 <path
    401                                                                                                                         strokeLinecap="round"
    402                                                                                                                         strokeLinejoin="round"
    403                                                                                                                         strokeWidth={2}
    404                                                                                                                         d="M12 4v16m8-8H4"
    405                                                                                                                 />
    406                                                                                                         </svg>
    407                                                                                                         Create new playlist
    408                                                                                                 </button>
    409                                                                                         </div>
    410                                                                                 )}
    411                                                                         </div>
    412                                                                 </>
    413                                                         )}
    414                                                 </div>
    415                                         </div>
    416                                 </div>
    417 
    418                                 {/* Credits / Contributions */}
    419                                 {song.contributions.length > 0 && (
    420                                         <section className="mb-10">
    421                                                 <h2 className="text-2xl font-bold mb-4">Credits</h2>
    422                                                 <div className="bg-[#1a1a2e]/60 rounded-xl p-5 divide-y divide-white/5">
    423                                                         {/* Main artist first */}
    424                                                         {song.contributions
    425                                                                 .filter((c) => c.artistName === song.releasedBy)
    426                                                                 .map((c, i) => (
    427                                                                         <div
    428                                                                                 key={`main-${i}`}
    429                                                                                 className="flex items-center justify-between py-3 first:pt-0 last:pb-0"
    430                                                                         >
    431                                                                                 <div className="flex items-center gap-3">
    432                                                                                         <div className="w-10 h-10 bg-[#1db954] rounded-full flex items-center justify-center text-black font-bold text-sm">
    433                                                                                                 {c.artistName.charAt(0).toUpperCase()}
    434                                                                                         </div>
    435                                                                                         <div>
    436                                                                                                 <p className="text-white font-semibold text-lg">
    437                                                                                                         {c.artistName}
    438                                                                                                 </p>
    439                                                                                         </div>
    440                                                                                 </div>
    441                                                                                 <span className="text-sm text-gray-400 bg-white/5 px-3 py-1 rounded-full">
    442                                                                                         {formatRole(c.role)}
    443                                                                                 </span>
    444                                                                         </div>
    445                                                                 ))}
    446 
    447                                                         {/* Other contributors */}
    448                                                         {otherContributors.map((c, i) => (
    449                                                                 <div
    450                                                                         key={`contrib-${i}`}
    451                                                                         className="flex items-center justify-between py-3 first:pt-0 last:pb-0"
    452                                                                 >
    453                                                                         <div className="flex items-center gap-3">
    454                                                                                 <div className="w-9 h-9 bg-white/10 rounded-full flex items-center justify-center text-white font-medium text-sm">
    455                                                                                         {c.artistName.charAt(0).toUpperCase()}
    456                                                                                 </div>
    457                                                                                 <p className="text-gray-300 font-medium">{c.artistName}</p>
    458                                                                         </div>
    459                                                                         <span className="text-sm text-gray-500 bg-white/5 px-3 py-1 rounded-full">
    460                                                                                 {formatRole(c.role)}
    461                                                                         </span>
    462                                                                 </div>
    463                                                         ))}
    464                                                 </div>
    465                                         </section>
    466                                 )}
    467 
    468                                 {/* Reviews */}
    469                                 <section>
    470                                         <div className="flex items-center justify-between mb-4">
    471                                                 <h2 className="text-2xl font-bold">
    472                                                         Reviews
    473                                                         {song.reviews.length > 0 && (
    474                                                                 <span className="text-base font-normal text-gray-500 ml-2">
    475                                                                         ({song.reviews.length})
    476                                                                 </span>
    477                                                         )}
    478                                                 </h2>
    479                                                 <button
    480                                                         onClick={() => setShowReviewModal(true)}
    481                                                         className="flex items-center gap-2 bg-[#1db954] hover:bg-[#1ed760] text-black text-sm font-semibold px-4 py-2 rounded-full transition-colors"
    482                                                 >
    483                                                         <svg
    484                                                                 className="w-4 h-4"
    485                                                                 fill="none"
    486                                                                 stroke="currentColor"
    487                                                                 viewBox="0 0 24 24"
    488                                                         >
    489                                                                 <path
    490                                                                         strokeLinecap="round"
    491                                                                         strokeLinejoin="round"
    492                                                                         strokeWidth={2}
    493                                                                         d="M12 4v16m8-8H4"
    494                                                                 />
    495                                                         </svg>
    496                                                         Add Review
    497                                                 </button>
    498                                         </div>
    499 
    500                                         {song.reviews.length === 0 ? (
    501                                                 <div className="bg-[#1a1a2e]/60 rounded-xl p-10 text-center">
    502                                                         <p className="text-gray-500 text-lg">No reviews yet.</p>
    503                                                         <p className="text-gray-600 text-sm mt-1">
    504                                                                 Be the first to share your thoughts!
    505                                                         </p>
    506                                                 </div>
    507                                         ) : (
    508                                                 <div className="space-y-4">
    509                                                         {song.reviews.map((review) => (
    510                                                                 <div
    511                                                                         key={`${review.id.listenerId}-${review.id.musicalEntityId}`}
    512                                                                         className="bg-[#1a1a2e]/60 rounded-xl p-5 hover:bg-[#1a1a2e]/80 transition-colors"
    513                                                                 >
    514                                                                         <div className="flex items-start justify-between mb-2">
    515                                                                                 <div className="flex items-center gap-3">
    516                                                                                         <div className="w-9 h-9 bg-blue-500/20 rounded-full flex items-center justify-center">
    517                                                                                                 <span className="text-blue-400 font-semibold text-sm">
    518                                                                                                         {review.author.charAt(0).toUpperCase()}
    519                                                                                                 </span>
    520                                                                                         </div>
    521                                                                                         <div>
    522                                                                                                 <p className="text-white font-medium">
    523                                                                                                         {review.author}
    524                                                                                                 </p>
    525                                                                                                 {renderStars(review.grade)}
    526                                                                                         </div>
    527                                                                                 </div>
    528                                                                                 {user?.username === review.authorUsername && (
    529                                                                                         <button
    530                                                                                                 onClick={deleteReview}
    531                                                                                                 className="text-gray-500 hover:text-red-400 transition-colors p-2 rounded-lg hover:bg-red-400/10"
    532                                                                                                 title="Delete review"
    533                                                                                         >
    534                                                                                                 <svg
    535                                                                                                         className="w-5 h-5"
    536                                                                                                         fill="none"
    537                                                                                                         stroke="currentColor"
    538                                                                                                         viewBox="0 0 24 24"
    539                                                                                                 >
    540                                                                                                         <path
    541                                                                                                                 strokeLinecap="round"
    542                                                                                                                 strokeLinejoin="round"
    543                                                                                                                 strokeWidth={2}
    544                                                                                                                 d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
    545                                                                                                         />
    546                                                                                                 </svg>
    547                                                                                         </button>
    548                                                                                 )}
    549                                                                         </div>
    550                                                                         <p className="text-gray-300 text-sm leading-relaxed mt-3 pl-12">
    551                                                                                 {review.comment}
    552                                                                         </p>
    553                                                                 </div>
    554                                                         ))}
    555                                                 </div>
    556                                         )}
    557                                 </section>
    558                         </div>
    559 
    560                         {/* Review Modal */}
    561                         {showReviewModal && (
    562                                 <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm">
    563                                         <div className="bg-[#1a1a2e] rounded-2xl p-6 max-w-md w-full shadow-2xl">
    564                                                 <div className="flex items-center justify-between mb-6">
    565                                                         <h3 className="text-2xl font-bold">Add Review</h3>
    566                                                         <button
    567                                                                 onClick={() => {
    568                                                                         setShowReviewModal(false);
    569                                                                         setReviewRating(0);
    570                                                                         setReviewComment("");
    571                                                                         setHoverRating(0);
    572                                                                 }}
    573                                                                 className="text-gray-400 hover:text-white transition-colors"
    574                                                         >
    575                                                                 <svg
    576                                                                         className="w-6 h-6"
    577                                                                         fill="none"
    578                                                                         stroke="currentColor"
    579                                                                         viewBox="0 0 24 24"
    580                                                                 >
    581                                                                         <path
    582                                                                                 strokeLinecap="round"
    583                                                                                 strokeLinejoin="round"
    584                                                                                 strokeWidth={2}
    585                                                                                 d="M6 18L18 6M6 6l12 12"
    586                                                                         />
    587                                                                 </svg>
    588                                                         </button>
    589                                                 </div>
    590 
    591                                                 {/* Star Rating */}
    592                                                 <div className="mb-6">
    593                                                         <label className="block text-sm font-medium mb-3">
    594                                                                 Rating <span className="text-red-400">*</span>
    595                                                         </label>
    596                                                         <div className="flex gap-2">
    597                                                                 {[1, 2, 3, 4, 5].map((star) => (
    598                                                                         <button
    599                                                                                 key={star}
    600                                                                                 onClick={() => setReviewRating(star)}
    601                                                                                 onMouseEnter={() => setHoverRating(star)}
    602                                                                                 onMouseLeave={() => setHoverRating(0)}
    603                                                                                 className="transition-transform hover:scale-110"
    604                                                                         >
    605                                                                                 <svg
    606                                                                                         className={`w-10 h-10 ${
    607                                                                                                 star <= (hoverRating || reviewRating)
    608                                                                                                         ? "text-yellow-400"
    609                                                                                                         : "text-gray-600"
    610                                                                                         }`}
    611                                                                                         fill="currentColor"
    612                                                                                         viewBox="0 0 20 20"
    613                                                                                 >
    614                                                                                         <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
    615                                                                                 </svg>
    616                                                                         </button>
    617                                                                 ))}
    618                                                         </div>
    619                                                 </div>
    620 
    621                                                 {/* Comment */}
    622                                                 <div className="mb-6">
    623                                                         <label className="block text-sm font-medium mb-2">
    624                                                                 Comment <span className="text-gray-500">(optional)</span>
    625                                                         </label>
    626                                                         <textarea
    627                                                                 value={reviewComment}
    628                                                                 onChange={(e) => setReviewComment(e.target.value)}
    629                                                                 placeholder="Share your thoughts about this song..."
    630                                                                 rows={4}
    631                                                                 className="w-full bg-[#0f0f1e] border border-white/10 rounded-lg px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-[#1db954] transition-colors resize-none"
    632                                                         />
    633                                                 </div>
    634 
    635                                                 {/* Actions */}
    636                                                 <div className="flex gap-3 justify-end">
    637                                                         <button
    638                                                                 onClick={() => {
    639                                                                         setShowReviewModal(false);
    640                                                                         setReviewRating(0);
    641                                                                         setReviewComment("");
    642                                                                         setHoverRating(0);
    643                                                                 }}
    644                                                                 className="px-5 py-2.5 rounded-full text-sm font-semibold bg-white/10 hover:bg-white/20 transition-colors"
    645                                                         >
    646                                                                 Cancel
    647                                                         </button>
    648                                                         <button
    649                                                                 onClick={handleSubmitReview}
    650                                                                 disabled={reviewRating === 0}
    651                                                                 className="px-5 py-2.5 rounded-full text-sm font-semibold bg-[#1db954] hover:bg-[#1ed760] text-black transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
    652                                                         >
    653                                                                 Submit
    654                                                         </button>
    655                                                 </div>
    656                                         </div>
    657                                 </div>
    658                         )}
    659                 </div>
    660         );
     43  const { id } = useParams<{ id: string }>();
     44  const { user } = useAuth();
     45  const { play, currentSong } = usePlayer();
     46  const [song, setSong] = useState<SongDetailType | null>(null);
     47  const [loading, setLoading] = useState(true);
     48  const [error, setError] = useState<string | null>(null);
     49  const [showReviewModal, setShowReviewModal] = useState(false);
     50  const [reviewRating, setReviewRating] = useState(0);
     51  const [reviewComment, setReviewComment] = useState("");
     52  const [hoverRating, setHoverRating] = useState(0);
     53  const [showPlaylistDropdown, setShowPlaylistDropdown] = useState(false);
     54  console.log(user);
     55  useEffect(() => {
     56    const fetchSong = async () => {
     57      try {
     58        setLoading(true);
     59        const response = await axiosInstance.get(`/songs/${id}/details`);
     60        setSong(response.data);
     61      } catch (err) {
     62        console.error("Error fetching song details:", err);
     63        setError("Failed to load song details.");
     64      } finally {
     65        setLoading(false);
     66      }
     67    };
     68    if (id) fetchSong();
     69  }, [id]);
     70
     71  const handleSubmitReview = async () => {
     72    if (reviewRating === 0) {
     73      // todo: replace with toast
     74      alert("Please select a rating");
     75      return;
     76    }
     77    try {
     78      await axiosInstance.post(`reviews/${song?.id}`, {
     79        grade: reviewRating,
     80        comment: reviewComment,
     81      });
     82      const response = await axiosInstance.get(`/songs/${id}/details`);
     83      setSong(response.data);
     84    } catch (err) {
     85      // todo: replace with toast
     86      console.error("Error submitting review:", err);
     87    } finally {
     88      setShowReviewModal(false);
     89      setReviewRating(0);
     90      setReviewComment("");
     91      setHoverRating(0);
     92    }
     93  };
     94
     95  const deleteReview = async () => {
     96    try {
     97      await axiosInstance.delete(`reviews/${song?.id}`);
     98      const response = await axiosInstance.get(`/songs/${id}/details`);
     99      setSong(response.data);
     100    } catch (err) {
     101      toast.error("Failed to delete review");
     102      console.error("Error deleting review:", err);
     103    }
     104  };
     105
     106  const toggleLike = async () => {
     107    if (!song) return;
     108    try {
     109      await axiosInstance.post(`/musical-entity/${song.id}/like`);
     110      setSong((prev) =>
     111        prev
     112          ? { ...prev, isLikedByCurrentUser: !prev.isLikedByCurrentUser }
     113          : prev,
     114      );
     115    } catch (err) {
     116      toast.error("Failed to toggle like");
     117      console.error("Error toggling like:", err);
     118    }
     119  };
     120
     121  const handleCreateNewPlaylist = () => {
     122    console.log(`Creating new playlist for song ${song?.id}`);
     123    // TODO: actual playlist creation
     124    setShowPlaylistDropdown(false);
     125  };
     126
     127  if (loading) {
     128    return (
     129      <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
     130        <div className="flex flex-col items-center gap-4">
     131          <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin" />
     132          <p className="text-gray-400 text-lg">Loading song…</p>
     133        </div>
     134      </div>
     135    );
     136  }
     137
     138  if (error || !song) {
     139    return (
     140      <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
     141        <div className="text-center">
     142          <p className="text-red-400 text-xl mb-4">
     143            {error ?? "Song not found"}
     144          </p>
     145          <Link to="/" className="text-[#1db954] hover:underline text-sm">
     146            ← Back to Home
     147          </Link>
     148        </div>
     149      </div>
     150    );
     151  }
     152
     153  const otherContributors = song.contributions.filter(
     154    (c) => c.artistName !== song.releasedBy,
     155  );
     156
     157  const avgRating =
     158    song.reviews.length > 0
     159      ? (
     160          song.reviews.reduce((sum, r) => sum + r.grade, 0) /
     161          song.reviews.length
     162        ).toFixed(1)
     163      : null;
     164
     165  return (
     166    <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white">
     167      <div className="max-w-5xl mx-auto px-6 py-10">
     168        {/* Back link */}
     169        <Link
     170          to="/"
     171          className="inline-flex items-center gap-1 text-gray-400 hover:text-white text-sm mb-8 transition-colors"
     172        >
     173          <svg
     174            className="w-4 h-4"
     175            fill="none"
     176            stroke="currentColor"
     177            viewBox="0 0 24 24"
     178          >
     179            <path
     180              strokeLinecap="round"
     181              strokeLinejoin="round"
     182              strokeWidth={2}
     183              d="M15 19l-7-7 7-7"
     184            />
     185          </svg>
     186          Back to Home
     187        </Link>
     188
     189        {/* Hero section */}
     190        <div className="flex flex-col md:flex-row gap-8 mb-10">
     191          {/* Cover art */}
     192          <div className="w-full md:w-72 shrink-0">
     193            <div className="relative w-full pt-[100%] rounded-xl overflow-hidden shadow-2xl bg-[#181818]">
     194              <img
     195                src={song.cover ? `${baseURL}/${song.cover}` : "/favicon.png"}
     196                alt={song.title}
     197                className="absolute inset-0 w-full h-full object-cover"
     198                onError={(e) => {
     199                  (e.target as HTMLImageElement).src = "/favicon.png";
     200                }}
     201              />
     202            </div>
     203          </div>
     204
     205          {/* Song info */}
     206          <div className="flex flex-col justify-end gap-3 min-w-0">
     207            <span className="text-xs uppercase tracking-widest text-gray-400 font-medium">
     208              {song.genre} • Song
     209            </span>
     210            <h1 className="text-4xl md:text-5xl font-extrabold leading-tight truncate">
     211              {song.title}
     212            </h1>
     213
     214            {/* Main artist */}
     215            <p className="text-xl text-gray-300 font-semibold">
     216              {song.releasedBy}
     217            </p>
     218
     219            {/* Album */}
     220            {song.album && (
     221              <p className="text-sm text-gray-500">
     222                Album: <span className="text-gray-300">{song.album}</span>
     223              </p>
     224            )}
     225
     226            {/* Rating summary */}
     227            {avgRating && (
     228              <div className="flex items-center gap-2 mt-1">
     229                {renderStars(Math.round(Number(avgRating)))}
     230                <span className="text-sm text-gray-400">
     231                  {avgRating} · {song.reviews.length}{" "}
     232                  {song.reviews.length === 1 ? "review" : "reviews"}
     233                </span>
     234              </div>
     235            )}
     236
     237            {/* Action buttons */}
     238            <div className="flex items-center gap-3 mt-4">
     239              {/* Play button */}
     240              {song.link && (
     241                <button
     242                  onClick={() =>
     243                    play({
     244                      id: song.id,
     245                      title: song.title,
     246                      artist: song.releasedBy,
     247                      cover: song.cover,
     248                      embedUrl: toEmbedUrl(song.link!),
     249                    })
     250                  }
     251                  className={`flex items-center gap-2 px-6 py-3 rounded-full text-sm font-semibold transition-all cursor-pointer ${
     252                    currentSong?.id === song.id
     253                      ? "bg-white text-[#1db954]"
     254                      : "bg-[#1db954] text-black hover:bg-[#1ed760] hover:scale-105"
     255                  }`}
     256                >
     257                  {currentSong?.id === song.id ? (
     258                    <>
     259                      <svg
     260                        className="w-5 h-5"
     261                        fill="currentColor"
     262                        viewBox="0 0 24 24"
     263                      >
     264                        <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
     265                      </svg>
     266                      Now Playing
     267                    </>
     268                  ) : (
     269                    <>
     270                      <svg
     271                        className="w-5 h-5"
     272                        fill="currentColor"
     273                        viewBox="0 0 24 24"
     274                      >
     275                        <path d="M8 5v14l11-7z" />
     276                      </svg>
     277                      Play Song
     278                    </>
     279                  )}
     280                </button>
     281              )}
     282
     283              {user && (
     284                <>
     285                  <button
     286                    onClick={toggleLike}
     287                    className={`flex items-center gap-2 px-5 py-3 rounded-full text-sm font-semibold transition-colors cursor-pointer ${
     288                      song.isLikedByCurrentUser
     289                        ? "bg-[#1db954] text-black"
     290                        : "bg-white/10 text-white hover:bg-white/20"
     291                    }`}
     292                  >
     293                    {song.isLikedByCurrentUser ? (
     294                      <svg
     295                        className="w-5 h-5"
     296                        fill="currentColor"
     297                        viewBox="0 0 24 24"
     298                      >
     299                        <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" />
     300                      </svg>
     301                    ) : (
     302                      <svg
     303                        className="w-5 h-5"
     304                        fill="none"
     305                        stroke="currentColor"
     306                        viewBox="0 0 24 24"
     307                      >
     308                        <path
     309                          strokeLinecap="round"
     310                          strokeLinejoin="round"
     311                          strokeWidth={2}
     312                          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"
     313                        />
     314                      </svg>
     315                    )}
     316                    {song.isLikedByCurrentUser ? "Liked" : "Like"}
     317                  </button>
     318
     319                  {/* Add to Playlist button */}
     320                  <div className="relative">
     321                    <button
     322                      onClick={() => setShowPlaylistDropdown((prev) => !prev)}
     323                      className="flex items-center gap-2 px-5 py-3 rounded-full text-sm font-semibold bg-white/10 text-white hover:bg-white/20 transition-colors cursor-pointer"
     324                    >
     325                      <svg
     326                        className="w-5 h-5"
     327                        fill="none"
     328                        stroke="currentColor"
     329                        viewBox="0 0 24 24"
     330                      >
     331                        <path
     332                          strokeLinecap="round"
     333                          strokeLinejoin="round"
     334                          strokeWidth={2}
     335                          d="M12 4v16m8-8H4"
     336                        />
     337                      </svg>
     338                      Add to Playlist
     339                    </button>
     340
     341                    <PlaylistDropdown
     342                      songId={song.id}
     343                      isOpen={showPlaylistDropdown}
     344                      onClose={() => setShowPlaylistDropdown(false)}
     345                      onCreateNewPlaylist={handleCreateNewPlaylist}
     346                      direction="below"
     347                    />
     348                  </div>
     349                </>
     350              )}
     351            </div>
     352          </div>
     353        </div>
     354
     355        {/* Credits / Contributions */}
     356        {song.contributions.length > 0 && (
     357          <section className="mb-10">
     358            <h2 className="text-2xl font-bold mb-4">Credits</h2>
     359            <div className="bg-[#1a1a2e]/60 rounded-xl p-5 divide-y divide-white/5">
     360              {/* Main artist first */}
     361              {song.contributions
     362                .filter((c) => c.artistName === song.releasedBy)
     363                .map((c, i) => (
     364                  <div
     365                    key={`main-${i}`}
     366                    className="flex items-center justify-between py-3 first:pt-0 last:pb-0"
     367                  >
     368                    <div className="flex items-center gap-3">
     369                      <div className="w-10 h-10 bg-[#1db954] rounded-full flex items-center justify-center text-black font-bold text-sm">
     370                        {c.artistName.charAt(0).toUpperCase()}
     371                      </div>
     372                      <div>
     373                        <p className="text-white font-semibold text-lg">
     374                          {c.artistName}
     375                        </p>
     376                      </div>
     377                    </div>
     378                    <span className="text-sm text-gray-400 bg-white/5 px-3 py-1 rounded-full">
     379                      {formatRole(c.role)}
     380                    </span>
     381                  </div>
     382                ))}
     383
     384              {/* Other contributors */}
     385              {otherContributors.map((c, i) => (
     386                <div
     387                  key={`contrib-${i}`}
     388                  className="flex items-center justify-between py-3 first:pt-0 last:pb-0"
     389                >
     390                  <div className="flex items-center gap-3">
     391                    <div className="w-9 h-9 bg-white/10 rounded-full flex items-center justify-center text-white font-medium text-sm">
     392                      {c.artistName.charAt(0).toUpperCase()}
     393                    </div>
     394                    <p className="text-gray-300 font-medium">{c.artistName}</p>
     395                  </div>
     396                  <span className="text-sm text-gray-500 bg-white/5 px-3 py-1 rounded-full">
     397                    {formatRole(c.role)}
     398                  </span>
     399                </div>
     400              ))}
     401            </div>
     402          </section>
     403        )}
     404
     405        {/* Reviews */}
     406        <section>
     407          <div className="flex items-center justify-between mb-4">
     408            <h2 className="text-2xl font-bold">
     409              Reviews
     410              {song.reviews.length > 0 && (
     411                <span className="text-base font-normal text-gray-500 ml-2">
     412                  ({song.reviews.length})
     413                </span>
     414              )}
     415            </h2>
     416            <button
     417              onClick={() => setShowReviewModal(true)}
     418              className="flex items-center gap-2 bg-[#1db954] hover:bg-[#1ed760] text-black text-sm font-semibold px-4 py-2 rounded-full transition-colors"
     419            >
     420              <svg
     421                className="w-4 h-4"
     422                fill="none"
     423                stroke="currentColor"
     424                viewBox="0 0 24 24"
     425              >
     426                <path
     427                  strokeLinecap="round"
     428                  strokeLinejoin="round"
     429                  strokeWidth={2}
     430                  d="M12 4v16m8-8H4"
     431                />
     432              </svg>
     433              Add Review
     434            </button>
     435          </div>
     436
     437          {song.reviews.length === 0 ? (
     438            <div className="bg-[#1a1a2e]/60 rounded-xl p-10 text-center">
     439              <p className="text-gray-500 text-lg">No reviews yet.</p>
     440              <p className="text-gray-600 text-sm mt-1">
     441                Be the first to share your thoughts!
     442              </p>
     443            </div>
     444          ) : (
     445            <div className="space-y-4">
     446              {song.reviews.map((review) => (
     447                <div
     448                  key={`${review.id.listenerId}-${review.id.musicalEntityId}`}
     449                  className="bg-[#1a1a2e]/60 rounded-xl p-5 hover:bg-[#1a1a2e]/80 transition-colors"
     450                >
     451                  <div className="flex items-start justify-between mb-2">
     452                    <div className="flex items-center gap-3">
     453                      <div className="w-9 h-9 bg-blue-500/20 rounded-full flex items-center justify-center">
     454                        <span className="text-blue-400 font-semibold text-sm">
     455                          {review.author.charAt(0).toUpperCase()}
     456                        </span>
     457                      </div>
     458                      <div>
     459                        <p className="text-white font-medium">
     460                          {review.author}
     461                        </p>
     462                        {renderStars(review.grade)}
     463                      </div>
     464                    </div>
     465                    {user?.username === review.authorUsername && (
     466                      <button
     467                        onClick={deleteReview}
     468                        className="text-gray-500 hover:text-red-400 transition-colors p-2 rounded-lg hover:bg-red-400/10"
     469                        title="Delete review"
     470                      >
     471                        <svg
     472                          className="w-5 h-5"
     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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
     482                          />
     483                        </svg>
     484                      </button>
     485                    )}
     486                  </div>
     487                  <p className="text-gray-300 text-sm leading-relaxed mt-3 pl-12">
     488                    {review.comment}
     489                  </p>
     490                </div>
     491              ))}
     492            </div>
     493          )}
     494        </section>
     495      </div>
     496
     497      {/* Review Modal */}
     498      {showReviewModal && (
     499        <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm">
     500          <div className="bg-[#1a1a2e] rounded-2xl p-6 max-w-md w-full shadow-2xl">
     501            <div className="flex items-center justify-between mb-6">
     502              <h3 className="text-2xl font-bold">Add Review</h3>
     503              <button
     504                onClick={() => {
     505                  setShowReviewModal(false);
     506                  setReviewRating(0);
     507                  setReviewComment("");
     508                  setHoverRating(0);
     509                }}
     510                className="text-gray-400 hover:text-white transition-colors"
     511              >
     512                <svg
     513                  className="w-6 h-6"
     514                  fill="none"
     515                  stroke="currentColor"
     516                  viewBox="0 0 24 24"
     517                >
     518                  <path
     519                    strokeLinecap="round"
     520                    strokeLinejoin="round"
     521                    strokeWidth={2}
     522                    d="M6 18L18 6M6 6l12 12"
     523                  />
     524                </svg>
     525              </button>
     526            </div>
     527
     528            {/* Star Rating */}
     529            <div className="mb-6">
     530              <label className="block text-sm font-medium mb-3">
     531                Rating <span className="text-red-400">*</span>
     532              </label>
     533              <div className="flex gap-2">
     534                {[1, 2, 3, 4, 5].map((star) => (
     535                  <button
     536                    key={star}
     537                    onClick={() => setReviewRating(star)}
     538                    onMouseEnter={() => setHoverRating(star)}
     539                    onMouseLeave={() => setHoverRating(0)}
     540                    className="transition-transform hover:scale-110"
     541                  >
     542                    <svg
     543                      className={`w-10 h-10 ${
     544                        star <= (hoverRating || reviewRating)
     545                          ? "text-yellow-400"
     546                          : "text-gray-600"
     547                      }`}
     548                      fill="currentColor"
     549                      viewBox="0 0 20 20"
     550                    >
     551                      <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
     552                    </svg>
     553                  </button>
     554                ))}
     555              </div>
     556            </div>
     557
     558            {/* Comment */}
     559            <div className="mb-6">
     560              <label className="block text-sm font-medium mb-2">
     561                Comment <span className="text-gray-500">(optional)</span>
     562              </label>
     563              <textarea
     564                value={reviewComment}
     565                onChange={(e) => setReviewComment(e.target.value)}
     566                placeholder="Share your thoughts about this song..."
     567                rows={4}
     568                className="w-full bg-[#0f0f1e] border border-white/10 rounded-lg px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-[#1db954] transition-colors resize-none"
     569              />
     570            </div>
     571
     572            {/* Actions */}
     573            <div className="flex gap-3 justify-end">
     574              <button
     575                onClick={() => {
     576                  setShowReviewModal(false);
     577                  setReviewRating(0);
     578                  setReviewComment("");
     579                  setHoverRating(0);
     580                }}
     581                className="px-5 py-2.5 rounded-full text-sm font-semibold bg-white/10 hover:bg-white/20 transition-colors"
     582              >
     583                Cancel
     584              </button>
     585              <button
     586                onClick={handleSubmitReview}
     587                disabled={reviewRating === 0}
     588                className="px-5 py-2.5 rounded-full text-sm font-semibold bg-[#1db954] hover:bg-[#1ed760] text-black transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
     589              >
     590                Submit
     591              </button>
     592            </div>
     593          </div>
     594        </div>
     595      )}
     596    </div>
     597  );
    661598};
    662599
  • frontend/src/pages/UserDetail.tsx

    r85512ff rce45c7a  
    1414  Playlist,
    1515} from "../utils/types";
    16 import { useCreatedPlaylists } from "../context/playlistContext";
    1716
    1817interface FollowStatus {
     
    3837  const { username: usernameParam } = useParams();
    3938  const { user: currentUser } = useAuth();
    40   const { createdPlaylists: currentUserCreatedPlaylists } =
    41     useCreatedPlaylists();
     39
    4240  const navigate = useNavigate();
    4341  const [user, setUser] = useState<UserProfile | null>(null);
Note: See TracChangeset for help on using the changeset viewer.