Changeset 947fcaa for frontend/src/pages


Ignore:
Timestamp:
02/21/26 19:19:51 (5 months ago)
Author:
Filip Gavrilovski <filipgavrilovski28@…>
Branches:
main
Parents:
765e166
Message:

add cursor pointer to buttons

Location:
frontend/src/pages
Files:
3 edited

Legend:

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

    r765e166 r947fcaa  
    66import SongResult from "../components/search/SongResult";
    77import UserResult from "../components/search/UserResult";
     8import { useAuth } from "../context/authContext";
    89import { usePlayer } from "../context/playerContext";
    910import type {
    10   Album,
    11   BaseNonAdminUser,
    12   SearchCategory,
    13   Song,
     11        Album,
     12        BaseNonAdminUser,
     13        SearchCategory,
     14        Song,
    1415} from "../utils/types";
    1516import { toEmbedUrl } from "../utils/utils";
    16 import { useAuth } from "../context/authContext";
    1717
    1818const CATEGORIES: { value: SearchCategory; label: string }[] = [
    19   { value: "songs", label: "Songs" },
    20   { value: "albums", label: "Albums" },
    21   { value: "artists", label: "Artists" },
    22   { value: "users", label: "Users" },
     19        { value: "songs", label: "Songs" },
     20        { value: "albums", label: "Albums" },
     21        { value: "artists", label: "Artists" },
     22        { value: "users", label: "Users" },
    2323];
    2424
    2525const LandingPage = () => {
    26   const navigate = useNavigate();
    27   const { play, currentSong } = usePlayer();
    28   const { user } = useAuth();
    29   const [songs, setSongs] = useState<Song[]>([]);
    30   const [loading, setLoading] = useState<boolean>(true);
    31 
    32   const [searchInput, setSearchInput] = useState("");
    33   const [activeQuery, setActiveQuery] = useState("");
    34   const [searchCategory, setSearchCategory] = useState<SearchCategory>("songs");
    35   const [searchResults, setSearchResults] = useState<unknown[]>([]);
    36   const [searchLoading, setSearchLoading] = useState(false);
    37   const [hasSearched, setHasSearched] = useState(false);
    38 
    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 performSearch = async (query: string, category: SearchCategory) => {
    81     if (!query.trim()) return;
    82 
    83     setSearchLoading(true);
    84     setHasSearched(true);
    85     setActiveQuery(query);
    86     setSearchCategory(category);
    87 
    88     try {
    89       let endpoint = "";
    90       switch (category) {
    91         case "songs":
    92           endpoint = `/songs/search?q=${encodeURIComponent(query)}`;
    93           break;
    94         case "albums":
    95           endpoint = `/albums/search?q=${encodeURIComponent(query)}`;
    96           break;
    97         case "artists":
    98           endpoint = `/users/search?type=ARTIST&q=${encodeURIComponent(query)}`;
    99           break;
    100         case "users":
    101           endpoint = `/users/search?type=LISTENER&q=${encodeURIComponent(query)}`;
    102           break;
    103       }
    104 
    105       const response = await axiosInstance.get(endpoint);
    106       setSearchResults(response.data);
    107     } catch (error) {
    108       console.error("Search error:", error);
    109       setSearchResults([]);
    110     } finally {
    111       setSearchLoading(false);
    112     }
    113   };
    114 
    115   const handleSearch = () => {
    116     performSearch(searchInput, searchCategory);
    117   };
    118 
    119   const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    120     if (e.key === "Enter") {
    121       handleSearch();
    122     }
    123   };
    124 
    125   const handleCategorySwitch = (category: SearchCategory) => {
    126     performSearch(activeQuery, category);
    127   };
    128 
    129   const clearSearch = () => {
    130     setSearchInput("");
    131     setActiveQuery("");
    132     setHasSearched(false);
    133     setSearchResults([]);
    134   };
    135 
    136   const renderResults = () => {
    137     if (searchLoading) {
    138       return (
    139         <div className="flex justify-center py-12">
    140           <div className="w-10 h-10 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin"></div>
    141         </div>
    142       );
    143     }
    144 
    145     if (searchResults.length === 0) {
    146       return (
    147         <div className="text-center py-12 text-gray-400">
    148           <p className="text-lg">
    149             No {searchCategory} found for "{activeQuery}"
    150           </p>
    151         </div>
    152       );
    153     }
    154 
    155     return (
    156       <div className="divide-y divide-white/5">
    157         {searchResults.map((result, index) => {
    158           switch (searchCategory) {
    159             case "songs":
    160               return (
    161                 <SongResult key={(result as Song).id} song={result as Song} />
    162               );
    163             case "albums":
    164               return (
    165                 <AlbumResult
    166                   key={(result as Album).id}
    167                   album={result as Album}
    168                 />
    169               );
    170             case "artists":
    171               return (
    172                 <UserResult
    173                   key={(result as BaseNonAdminUser).username ?? index}
    174                   user={result as BaseNonAdminUser}
    175                   label="Artist"
    176                 />
    177               );
    178             case "users":
    179               return (
    180                 <UserResult
    181                   key={(result as BaseNonAdminUser).username ?? index}
    182                   user={result as BaseNonAdminUser}
    183                   label="User"
    184                 />
    185               );
    186           }
    187         })}
    188       </div>
    189     );
    190   };
    191 
    192   return (
    193     <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white flex">
    194       <div className="flex-1">
    195         <div className="p-8">
    196           <div className="max-w-7xl mx-auto">
    197             {/* Search Bar */}
    198             <div className="mb-8 flex flex-col md:flex-row gap-3">
    199               <div className="flex flex-1 gap-0">
    200                 {/* Category Dropdown */}
    201                 <select
    202                   value={searchCategory}
    203                   onChange={(e) =>
    204                     setSearchCategory(e.target.value as SearchCategory)
    205                   }
    206                   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"
    207                 >
    208                   {CATEGORIES.map((cat) => (
    209                     <option key={cat.value} value={cat.value}>
    210                       {cat.label}
    211                     </option>
    212                   ))}
    213                 </select>
    214 
    215                 {/* Search Input */}
    216                 <input
    217                   type="text"
    218                   placeholder={`Search ${searchCategory}...`}
    219                   value={searchInput}
    220                   onChange={(e) => setSearchInput(e.target.value)}
    221                   onKeyDown={handleKeyDown}
    222                   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"
    223                 />
    224 
    225                 {/* Search Button */}
    226                 <button
    227                   onClick={handleSearch}
    228                   className="bg-[#1db954] hover:bg-[#1ed760] text-black font-medium px-6 rounded-r-full transition-colors flex items-center gap-2"
    229                 >
    230                   <svg
    231                     className="w-5 h-5"
    232                     fill="none"
    233                     stroke="currentColor"
    234                     viewBox="0 0 24 24"
    235                   >
    236                     <path
    237                       strokeLinecap="round"
    238                       strokeLinejoin="round"
    239                       strokeWidth={2}
    240                       d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
    241                     />
    242                   </svg>
    243                   <span className="hidden sm:inline">Search</span>
    244                 </button>
    245               </div>
    246             </div>
    247 
    248             {/* Search Results Section */}
    249             {hasSearched ? (
    250               <div>
    251                 <div className="flex items-center justify-between mb-4">
    252                   <h2 className="text-2xl font-bold text-white">
    253                     Results for "{activeQuery}"
    254                   </h2>
    255                   <button
    256                     onClick={clearSearch}
    257                     className="text-sm text-gray-400 hover:text-white transition-colors"
    258                   >
    259                     Clear search
    260                   </button>
    261                 </div>
    262 
    263                 {/* Quick category switch buttons */}
    264                 <div className="flex gap-2 mb-6">
    265                   {CATEGORIES.map((cat) => (
    266                     <button
    267                       key={cat.value}
    268                       onClick={() => handleCategorySwitch(cat.value)}
    269                       className={`px-4 py-1.5 rounded-full text-sm font-medium transition-colors cursor-pointer ${
    270                         searchCategory === cat.value
    271                           ? "bg-[#1db954] text-black"
    272                           : "bg-white/10 text-white hover:bg-white/20"
    273                       }`}
    274                     >
    275                       {cat.label}
    276                     </button>
    277                   ))}
    278                 </div>
    279 
    280                 {/* Results list */}
    281                 <div className="bg-[#1a1a2e]/50 rounded-xl overflow-hidden">
    282                   {renderResults()}
    283                 </div>
    284               </div>
    285             ) : (
    286               /* Default song grid */
    287               <>
    288                 <div className="mb-8 border-b border-white/10 pb-6">
    289                   <h1 className="text-5xl font-bold mb-3 pb-1 bg-linear-to-r from-[#1db954] to-[#1ed760] bg-clip-text text-transparent">
    290                     Top Songs
    291                   </h1>
    292                   <p className="text-xl text-gray-400">
    293                     Listen to the newest tracks on FinkWave
    294                   </p>
    295                 </div>
    296 
    297                 {loading ? (
    298                   <div className="flex flex-col items-center justify-center min-h-[40vh] gap-6">
    299                     <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin"></div>
    300                     <p className="text-xl text-gray-400">Loading songs...</p>
    301                   </div>
    302                 ) : (
    303                   <div className="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-8 py-4">
    304                     {songs.map((song) => (
    305                       <div
    306                         key={song.id}
    307                         onClick={() => navigate(`/songs/${song.id}`)}
    308                         onMouseEnter={() => {
    309                           if (
    310                             openPlaylistDropdown !== null &&
    311                             openPlaylistDropdown !== song.id
    312                           ) {
    313                             setOpenPlaylistDropdown(null);
    314                           }
    315                         }}
    316                         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"
    317                       >
    318                         <div className="relative w-full pt-[100%] overflow-hidden bg-[#181818] rounded-t-xl">
    319                           <img
    320                             src={
    321                               song.cover
    322                                 ? `${baseURL}/${song.cover}`
    323                                 : "/favicon.png"
    324                             }
    325                             alt={song.title}
    326                             className="absolute top-0 left-0 w-full h-full object-cover"
    327                             onError={(e) => {
    328                               (e.target as HTMLImageElement).src =
    329                                 "/favicon.png";
    330                             }}
    331                           />
    332                           <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">
    333                             {song.link ? (
    334                               <button
    335                                 onClick={(e) => {
    336                                   e.stopPropagation();
    337                                   play({
    338                                     id: song.id,
    339                                     title: song.title,
    340                                     artist: song.releasedBy,
    341                                     cover: song.cover,
    342                                     embedUrl: toEmbedUrl(song.link!),
    343                                   });
    344                                 }}
    345                                 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 ${
    346                                   currentSong?.id === song.id
    347                                     ? "bg-white text-[#1db954]"
    348                                     : "bg-[#1db954] text-black hover:scale-105"
    349                                 }`}
    350                                 aria-label={
    351                                   currentSong?.id === song.id
    352                                     ? "Now playing"
    353                                     : "Play song"
    354                                 }
    355                               >
    356                                 {currentSong?.id === song.id ? (
    357                                   <svg
    358                                     className="w-6 h-6"
    359                                     fill="currentColor"
    360                                     viewBox="0 0 24 24"
    361                                   >
    362                                     <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
    363                                   </svg>
    364                                 ) : (
    365                                   <svg
    366                                     className="w-6 h-6"
    367                                     fill="currentColor"
    368                                     viewBox="0 0 24 24"
    369                                   >
    370                                     <path d="M8 5v14l11-7z" />
    371                                   </svg>
    372                                 )}
    373                               </button>
    374                             ) : (
    375                               <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)]">
    376                                
    377                               </div>
    378                             )}
    379                           </div>
    380                         </div>
    381                         <div className="p-4">
    382                           <h3 className="text-lg font-semibold mb-2 text-white overflow-hidden text-ellipsis whitespace-nowrap">
    383                             {song.title}
    384                           </h3>
    385                           {song.album && (
    386                             <p
    387                               onClick={(e) => {
    388                                 e.stopPropagation();
    389                                 if (song.albumId) {
    390                                   navigate(`/collection/album/${song.albumId}`);
    391                                 }
    392                               }}
    393                               className={`text-sm text-gray-400 mb-3 overflow-hidden text-ellipsis whitespace-nowrap ${
    394                                 song.albumId
    395                                   ? "hover:underline cursor-pointer hover:text-white"
    396                                   : ""
    397                               }`}
    398                             >
    399                               {song.album}
    400                             </p>
    401                           )}
    402                           <p
    403                             onClick={(e) => {
    404                               e.stopPropagation();
    405                               if (song.artistUsername) {
    406                                 navigate(`/users/${song.artistUsername}`);
    407                               }
    408                             }}
    409                             className={`text-sm text-gray-400 mb-3 overflow-hidden text-ellipsis whitespace-nowrap ${
    410                               song.artistUsername
    411                                 ? "hover:underline cursor-pointer hover:text-white"
    412                                 : ""
    413                             }`}
    414                           >
    415                             {song.releasedBy}
    416                           </p>
    417                           {user && (
    418                             <div className="flex items-center justify-between mt-4 pt-3 border-t border-white/10">
    419                               {/* Like button */}
    420                               <button
    421                                 onClick={(e) => {
    422                                   e.stopPropagation();
    423                                   toggleLike(song.id);
    424                                 }}
    425                                 className="text-gray-400 hover:text-[#1db954] transition-colors cursor-pointer"
    426                                 aria-label={
    427                                   song.isLikedByCurrentUser
    428                                     ? "Unlike song"
    429                                     : "Like song"
    430                                 }
    431                               >
    432                                 {song.isLikedByCurrentUser ? (
    433                                   <svg
    434                                     className="w-6 h-6 fill-[#1db954]"
    435                                     viewBox="0 0 24 24"
    436                                   >
    437                                     <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" />
    438                                   </svg>
    439                                 ) : (
    440                                   <svg
    441                                     className="w-6 h-6"
    442                                     fill="none"
    443                                     stroke="currentColor"
    444                                     viewBox="0 0 24 24"
    445                                   >
    446                                     <path
    447                                       strokeLinecap="round"
    448                                       strokeLinejoin="round"
    449                                       strokeWidth={2}
    450                                       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"
    451                                     />
    452                                   </svg>
    453                                 )}
    454                               </button>
    455 
    456                               {/* Add to playlist button */}
    457                               <div className="relative">
    458                                 <button
    459                                   onClick={(e) => {
    460                                     e.stopPropagation();
    461                                     togglePlaylistDropdown(song.id);
    462                                   }}
    463                                   className="text-gray-400 hover:text-[#1db954] transition-colors cursor-pointer"
    464                                   aria-label="Add to playlist"
    465                                 >
    466                                   <svg
    467                                     className="w-6 h-6"
    468                                     fill="none"
    469                                     stroke="currentColor"
    470                                     viewBox="0 0 24 24"
    471                                   >
    472                                     <path
    473                                       strokeLinecap="round"
    474                                       strokeLinejoin="round"
    475                                       strokeWidth={2}
    476                                       d="M12 4v16m8-8H4"
    477                                     />
    478                                   </svg>
    479                                 </button>
    480 
    481                                 <PlaylistDropdown
    482                                   songId={song.id}
    483                                   isOpen={openPlaylistDropdown === song.id}
    484                                   onClose={() => setOpenPlaylistDropdown(null)}
    485                                   direction="above"
    486                                 />
    487                               </div>
    488                             </div>
    489                           )}
    490                         </div>
    491                       </div>
    492                     ))}
    493                   </div>
    494                 )}
    495               </>
    496             )}
    497           </div>
    498         </div>
    499       </div>
    500     </div>
    501   );
     26        const navigate = useNavigate();
     27        const { play, currentSong } = usePlayer();
     28        const { user } = useAuth();
     29        const [songs, setSongs] = useState<Song[]>([]);
     30        const [loading, setLoading] = useState<boolean>(true);
     31
     32        const [searchInput, setSearchInput] = useState("");
     33        const [activeQuery, setActiveQuery] = useState("");
     34        const [searchCategory, setSearchCategory] = useState<SearchCategory>("songs");
     35        const [searchResults, setSearchResults] = useState<unknown[]>([]);
     36        const [searchLoading, setSearchLoading] = useState(false);
     37        const [hasSearched, setHasSearched] = useState(false);
     38
     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 performSearch = async (query: string, category: SearchCategory) => {
     81                if (!query.trim()) return;
     82
     83                setSearchLoading(true);
     84                setHasSearched(true);
     85                setActiveQuery(query);
     86                setSearchCategory(category);
     87
     88                try {
     89                        let endpoint = "";
     90                        switch (category) {
     91                                case "songs":
     92                                        endpoint = `/songs/search?q=${encodeURIComponent(query)}`;
     93                                        break;
     94                                case "albums":
     95                                        endpoint = `/albums/search?q=${encodeURIComponent(query)}`;
     96                                        break;
     97                                case "artists":
     98                                        endpoint = `/users/search?type=ARTIST&q=${encodeURIComponent(query)}`;
     99                                        break;
     100                                case "users":
     101                                        endpoint = `/users/search?type=LISTENER&q=${encodeURIComponent(query)}`;
     102                                        break;
     103                        }
     104
     105                        const response = await axiosInstance.get(endpoint);
     106                        setSearchResults(response.data);
     107                } catch (error) {
     108                        console.error("Search error:", error);
     109                        setSearchResults([]);
     110                } finally {
     111                        setSearchLoading(false);
     112                }
     113        };
     114
     115        const handleSearch = () => {
     116                performSearch(searchInput, searchCategory);
     117        };
     118
     119        const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
     120                if (e.key === "Enter") {
     121                        handleSearch();
     122                }
     123        };
     124
     125        const handleCategorySwitch = (category: SearchCategory) => {
     126                performSearch(activeQuery, category);
     127        };
     128
     129        const clearSearch = () => {
     130                setSearchInput("");
     131                setActiveQuery("");
     132                setHasSearched(false);
     133                setSearchResults([]);
     134        };
     135
     136        const renderResults = () => {
     137                if (searchLoading) {
     138                        return (
     139                                <div className="flex justify-center py-12">
     140                                        <div className="w-10 h-10 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin"></div>
     141                                </div>
     142                        );
     143                }
     144
     145                if (searchResults.length === 0) {
     146                        return (
     147                                <div className="text-center py-12 text-gray-400">
     148                                        <p className="text-lg">
     149                                                No {searchCategory} found for "{activeQuery}"
     150                                        </p>
     151                                </div>
     152                        );
     153                }
     154
     155                return (
     156                        <div className="divide-y divide-white/5">
     157                                {searchResults.map((result, index) => {
     158                                        switch (searchCategory) {
     159                                                case "songs":
     160                                                        return (
     161                                                                <SongResult key={(result as Song).id} song={result as Song} />
     162                                                        );
     163                                                case "albums":
     164                                                        return (
     165                                                                <AlbumResult
     166                                                                        key={(result as Album).id}
     167                                                                        album={result as Album}
     168                                                                />
     169                                                        );
     170                                                case "artists":
     171                                                        return (
     172                                                                <UserResult
     173                                                                        key={(result as BaseNonAdminUser).username ?? index}
     174                                                                        user={result as BaseNonAdminUser}
     175                                                                        label="Artist"
     176                                                                />
     177                                                        );
     178                                                case "users":
     179                                                        return (
     180                                                                <UserResult
     181                                                                        key={(result as BaseNonAdminUser).username ?? index}
     182                                                                        user={result as BaseNonAdminUser}
     183                                                                        label="User"
     184                                                                />
     185                                                        );
     186                                        }
     187                                })}
     188                        </div>
     189                );
     190        };
     191
     192        return (
     193                <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white flex">
     194                        <div className="flex-1">
     195                                <div className="p-8">
     196                                        <div className="max-w-7xl mx-auto">
     197                                                {/* Search Bar */}
     198                                                <div className="mb-8 flex flex-col md:flex-row gap-3">
     199                                                        <div className="flex flex-1 gap-0">
     200                                                                {/* Category Dropdown */}
     201                                                                <select
     202                                                                        value={searchCategory}
     203                                                                        onChange={(e) =>
     204                                                                                setSearchCategory(e.target.value as SearchCategory)
     205                                                                        }
     206                                                                        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"
     207                                                                >
     208                                                                        {CATEGORIES.map((cat) => (
     209                                                                                <option key={cat.value} value={cat.value}>
     210                                                                                        {cat.label}
     211                                                                                </option>
     212                                                                        ))}
     213                                                                </select>
     214
     215                                                                {/* Search Input */}
     216                                                                <input
     217                                                                        type="text"
     218                                                                        placeholder={`Search ${searchCategory}...`}
     219                                                                        value={searchInput}
     220                                                                        onChange={(e) => setSearchInput(e.target.value)}
     221                                                                        onKeyDown={handleKeyDown}
     222                                                                        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"
     223                                                                />
     224
     225                                                                {/* Search Button */}
     226                                                                <button
     227                                                                        onClick={handleSearch}
     228                                                                        className="bg-[#1db954] hover:bg-[#1ed760] text-black font-medium px-6 rounded-r-full transition-colors flex items-center gap-2 cursor-pointer"
     229                                                                >
     230                                                                        <svg
     231                                                                                className="w-5 h-5"
     232                                                                                fill="none"
     233                                                                                stroke="currentColor"
     234                                                                                viewBox="0 0 24 24"
     235                                                                        >
     236                                                                                <path
     237                                                                                        strokeLinecap="round"
     238                                                                                        strokeLinejoin="round"
     239                                                                                        strokeWidth={2}
     240                                                                                        d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
     241                                                                                />
     242                                                                        </svg>
     243                                                                        <span className="hidden sm:inline">Search</span>
     244                                                                </button>
     245                                                        </div>
     246                                                </div>
     247
     248                                                {/* Search Results Section */}
     249                                                {hasSearched ? (
     250                                                        <div>
     251                                                                <div className="flex items-center justify-between mb-4">
     252                                                                        <h2 className="text-2xl font-bold text-white">
     253                                                                                Results for "{activeQuery}"
     254                                                                        </h2>
     255                                                                        <button
     256                                                                                onClick={clearSearch}
     257                                                                                className="text-sm text-gray-400 hover:text-white transition-colors"
     258                                                                        >
     259                                                                                Clear search
     260                                                                        </button>
     261                                                                </div>
     262
     263                                                                {/* Quick category switch buttons */}
     264                                                                <div className="flex gap-2 mb-6">
     265                                                                        {CATEGORIES.map((cat) => (
     266                                                                                <button
     267                                                                                        key={cat.value}
     268                                                                                        onClick={() => handleCategorySwitch(cat.value)}
     269                                                                                        className={`px-4 py-1.5 rounded-full text-sm font-medium transition-colors cursor-pointer ${
     270                                                                                                searchCategory === cat.value
     271                                                                                                        ? "bg-[#1db954] text-black"
     272                                                                                                        : "bg-white/10 text-white hover:bg-white/20"
     273                                                                                        }`}
     274                                                                                >
     275                                                                                        {cat.label}
     276                                                                                </button>
     277                                                                        ))}
     278                                                                </div>
     279
     280                                                                {/* Results list */}
     281                                                                <div className="bg-[#1a1a2e]/50 rounded-xl overflow-hidden">
     282                                                                        {renderResults()}
     283                                                                </div>
     284                                                        </div>
     285                                                ) : (
     286                                                        /* Default song grid */
     287                                                        <>
     288                                                                <div className="mb-8 border-b border-white/10 pb-6">
     289                                                                        <h1 className="text-5xl font-bold mb-3 pb-1 bg-linear-to-r from-[#1db954] to-[#1ed760] bg-clip-text text-transparent">
     290                                                                                Top Songs
     291                                                                        </h1>
     292                                                                        <p className="text-xl text-gray-400">
     293                                                                                Listen to the newest tracks on FinkWave
     294                                                                        </p>
     295                                                                </div>
     296
     297                                                                {loading ? (
     298                                                                        <div className="flex flex-col items-center justify-center min-h-[40vh] gap-6">
     299                                                                                <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin"></div>
     300                                                                                <p className="text-xl text-gray-400">Loading songs...</p>
     301                                                                        </div>
     302                                                                ) : (
     303                                                                        <div className="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-8 py-4">
     304                                                                                {songs.map((song) => (
     305                                                                                        <div
     306                                                                                                key={song.id}
     307                                                                                                onClick={() => navigate(`/songs/${song.id}`)}
     308                                                                                                onMouseEnter={() => {
     309                                                                                                        if (
     310                                                                                                                openPlaylistDropdown !== null &&
     311                                                                                                                openPlaylistDropdown !== song.id
     312                                                                                                        ) {
     313                                                                                                                setOpenPlaylistDropdown(null);
     314                                                                                                        }
     315                                                                                                }}
     316                                                                                                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"
     317                                                                                        >
     318                                                                                                <div className="relative w-full pt-[100%] overflow-hidden bg-[#181818] rounded-t-xl">
     319                                                                                                        <img
     320                                                                                                                src={
     321                                                                                                                        song.cover
     322                                                                                                                                ? `${baseURL}/${song.cover}`
     323                                                                                                                                : "/favicon.png"
     324                                                                                                                }
     325                                                                                                                alt={song.title}
     326                                                                                                                className="absolute top-0 left-0 w-full h-full object-cover"
     327                                                                                                                onError={(e) => {
     328                                                                                                                        (e.target as HTMLImageElement).src =
     329                                                                                                                                "/favicon.png";
     330                                                                                                                }}
     331                                                                                                        />
     332                                                                                                        <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">
     333                                                                                                                {song.link ? (
     334                                                                                                                        <button
     335                                                                                                                                onClick={(e) => {
     336                                                                                                                                        e.stopPropagation();
     337                                                                                                                                        play({
     338                                                                                                                                                id: song.id,
     339                                                                                                                                                title: song.title,
     340                                                                                                                                                artist: song.releasedBy,
     341                                                                                                                                                cover: song.cover,
     342                                                                                                                                                embedUrl: toEmbedUrl(song.link!),
     343                                                                                                                                        });
     344                                                                                                                                }}
     345                                                                                                                                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 ${
     346                                                                                                                                        currentSong?.id === song.id
     347                                                                                                                                                ? "bg-white text-[#1db954]"
     348                                                                                                                                                : "bg-[#1db954] text-black hover:scale-105"
     349                                                                                                                                }`}
     350                                                                                                                                aria-label={
     351                                                                                                                                        currentSong?.id === song.id
     352                                                                                                                                                ? "Now playing"
     353                                                                                                                                                : "Play song"
     354                                                                                                                                }
     355                                                                                                                        >
     356                                                                                                                                {currentSong?.id === song.id ? (
     357                                                                                                                                        <svg
     358                                                                                                                                                className="w-6 h-6"
     359                                                                                                                                                fill="currentColor"
     360                                                                                                                                                viewBox="0 0 24 24"
     361                                                                                                                                        >
     362                                                                                                                                                <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
     363                                                                                                                                        </svg>
     364                                                                                                                                ) : (
     365                                                                                                                                        <svg
     366                                                                                                                                                className="w-6 h-6"
     367                                                                                                                                                fill="currentColor"
     368                                                                                                                                                viewBox="0 0 24 24"
     369                                                                                                                                        >
     370                                                                                                                                                <path d="M8 5v14l11-7z" />
     371                                                                                                                                        </svg>
     372                                                                                                                                )}
     373                                                                                                                        </button>
     374                                                                                                                ) : (
     375                                                                                                                        <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)]">
     376                                                                                                                               
     377                                                                                                                        </div>
     378                                                                                                                )}
     379                                                                                                        </div>
     380                                                                                                </div>
     381                                                                                                <div className="p-4">
     382                                                                                                        <h3 className="text-lg font-semibold mb-2 text-white overflow-hidden text-ellipsis whitespace-nowrap">
     383                                                                                                                {song.title}
     384                                                                                                        </h3>
     385                                                                                                        {song.album && (
     386                                                                                                                <p
     387                                                                                                                        onClick={(e) => {
     388                                                                                                                                e.stopPropagation();
     389                                                                                                                                if (song.albumId) {
     390                                                                                                                                        navigate(`/collection/album/${song.albumId}`);
     391                                                                                                                                }
     392                                                                                                                        }}
     393                                                                                                                        className={`text-sm text-gray-400 mb-3 overflow-hidden text-ellipsis whitespace-nowrap ${
     394                                                                                                                                song.albumId
     395                                                                                                                                        ? "hover:underline cursor-pointer hover:text-white"
     396                                                                                                                                        : ""
     397                                                                                                                        }`}
     398                                                                                                                >
     399                                                                                                                        {song.album}
     400                                                                                                                </p>
     401                                                                                                        )}
     402                                                                                                        <p
     403                                                                                                                onClick={(e) => {
     404                                                                                                                        e.stopPropagation();
     405                                                                                                                        if (song.artistUsername) {
     406                                                                                                                                navigate(`/users/${song.artistUsername}`);
     407                                                                                                                        }
     408                                                                                                                }}
     409                                                                                                                className={`text-sm text-gray-400 mb-3 overflow-hidden text-ellipsis whitespace-nowrap ${
     410                                                                                                                        song.artistUsername
     411                                                                                                                                ? "hover:underline cursor-pointer hover:text-white"
     412                                                                                                                                : ""
     413                                                                                                                }`}
     414                                                                                                        >
     415                                                                                                                {song.releasedBy}
     416                                                                                                        </p>
     417                                                                                                        {user && (
     418                                                                                                                <div className="flex items-center justify-between mt-4 pt-3 border-t border-white/10">
     419                                                                                                                        {/* Like button */}
     420                                                                                                                        <button
     421                                                                                                                                onClick={(e) => {
     422                                                                                                                                        e.stopPropagation();
     423                                                                                                                                        toggleLike(song.id);
     424                                                                                                                                }}
     425                                                                                                                                className="text-gray-400 hover:text-[#1db954] transition-colors cursor-pointer"
     426                                                                                                                                aria-label={
     427                                                                                                                                        song.isLikedByCurrentUser
     428                                                                                                                                                ? "Unlike song"
     429                                                                                                                                                : "Like song"
     430                                                                                                                                }
     431                                                                                                                        >
     432                                                                                                                                {song.isLikedByCurrentUser ? (
     433                                                                                                                                        <svg
     434                                                                                                                                                className="w-6 h-6 fill-[#1db954]"
     435                                                                                                                                                viewBox="0 0 24 24"
     436                                                                                                                                        >
     437                                                                                                                                                <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" />
     438                                                                                                                                        </svg>
     439                                                                                                                                ) : (
     440                                                                                                                                        <svg
     441                                                                                                                                                className="w-6 h-6"
     442                                                                                                                                                fill="none"
     443                                                                                                                                                stroke="currentColor"
     444                                                                                                                                                viewBox="0 0 24 24"
     445                                                                                                                                        >
     446                                                                                                                                                <path
     447                                                                                                                                                        strokeLinecap="round"
     448                                                                                                                                                        strokeLinejoin="round"
     449                                                                                                                                                        strokeWidth={2}
     450                                                                                                                                                        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"
     451                                                                                                                                                />
     452                                                                                                                                        </svg>
     453                                                                                                                                )}
     454                                                                                                                        </button>
     455
     456                                                                                                                        {/* Add to playlist button */}
     457                                                                                                                        <div className="relative">
     458                                                                                                                                <button
     459                                                                                                                                        onClick={(e) => {
     460                                                                                                                                                e.stopPropagation();
     461                                                                                                                                                togglePlaylistDropdown(song.id);
     462                                                                                                                                        }}
     463                                                                                                                                        className="text-gray-400 hover:text-[#1db954] transition-colors cursor-pointer"
     464                                                                                                                                        aria-label="Add to playlist"
     465                                                                                                                                >
     466                                                                                                                                        <svg
     467                                                                                                                                                className="w-6 h-6"
     468                                                                                                                                                fill="none"
     469                                                                                                                                                stroke="currentColor"
     470                                                                                                                                                viewBox="0 0 24 24"
     471                                                                                                                                        >
     472                                                                                                                                                <path
     473                                                                                                                                                        strokeLinecap="round"
     474                                                                                                                                                        strokeLinejoin="round"
     475                                                                                                                                                        strokeWidth={2}
     476                                                                                                                                                        d="M12 4v16m8-8H4"
     477                                                                                                                                                />
     478                                                                                                                                        </svg>
     479                                                                                                                                </button>
     480
     481                                                                                                                                <PlaylistDropdown
     482                                                                                                                                        songId={song.id}
     483                                                                                                                                        isOpen={openPlaylistDropdown === song.id}
     484                                                                                                                                        onClose={() => setOpenPlaylistDropdown(null)}
     485                                                                                                                                        direction="above"
     486                                                                                                                                />
     487                                                                                                                        </div>
     488                                                                                                                </div>
     489                                                                                                        )}
     490                                                                                                </div>
     491                                                                                        </div>
     492                                                                                ))}
     493                                                                        </div>
     494                                                                )}
     495                                                        </>
     496                                                )}
     497                                        </div>
     498                                </div>
     499                        </div>
     500                </div>
     501        );
    502502};
    503503
  • frontend/src/pages/Nav.tsx

    r765e166 r947fcaa  
    5656                                        <button
    5757                                                onClick={onToggleSidebar}
    58                                                 className="text-white hover:text-[#1db954] transition-colors p-2"
     58                                                className="text-white hover:text-[#1db954] transition-colors p-2 cursor-pointer"
    5959                                                aria-label="Toggle sidebar"
    6060                                        >
  • frontend/src/pages/SongDetail.tsx

    r765e166 r947fcaa  
    1010
    1111const ROLE_LABELS: Record<string, string> = {
    12   MAIN_VOCAL: "Main Vocal",
    13   FEATURED: "Featured",
    14   PRODUCER: "Producer",
    15   SONGWRITER: "Songwriter",
    16   COMPOSER: "Composer",
    17   MIXER: "Mixer",
    18   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",
    1919};
    2020
    2121const formatRole = (role: string): string => {
    22   return ROLE_LABELS[role] || role.replace(/_/g, " ");
     22        return ROLE_LABELS[role] || role.replace(/_/g, " ");
    2323};
    2424
    2525const renderStars = (grade: number) => {
    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   );
     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        );
    4040};
    4141
    4242const SongDetail = () => {
    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   if (loading) {
    122     return (
    123       <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
    124         <div className="flex flex-col items-center gap-4">
    125           <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin" />
    126           <p className="text-gray-400 text-lg">Loading song…</p>
    127         </div>
    128       </div>
    129     );
    130   }
    131 
    132   if (error || !song) {
    133     return (
    134       <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
    135         <div className="text-center">
    136           <p className="text-red-400 text-xl mb-4">
    137             {error ?? "Song not found"}
    138           </p>
    139           <Link to="/" className="text-[#1db954] hover:underline text-sm">
    140             ← Back to Home
    141           </Link>
    142         </div>
    143       </div>
    144     );
    145   }
    146 
    147   const otherContributors = song.contributions.filter(
    148     (c) => c.artistName !== song.releasedBy,
    149   );
    150 
    151   const avgRating =
    152     song.reviews.length > 0
    153       ? (
    154           song.reviews.reduce((sum, r) => sum + r.grade, 0) /
    155           song.reviews.length
    156         ).toFixed(1)
    157       : null;
    158 
    159   return (
    160     <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white">
    161       <div className="max-w-5xl mx-auto px-6 py-10">
    162         {/* Back link */}
    163         <Link
    164           to="/"
    165           className="inline-flex items-center gap-1 text-gray-400 hover:text-white text-sm mb-8 transition-colors"
    166         >
    167           <svg
    168             className="w-4 h-4"
    169             fill="none"
    170             stroke="currentColor"
    171             viewBox="0 0 24 24"
    172           >
    173             <path
    174               strokeLinecap="round"
    175               strokeLinejoin="round"
    176               strokeWidth={2}
    177               d="M15 19l-7-7 7-7"
    178             />
    179           </svg>
    180           Back to Home
    181         </Link>
    182 
    183         {/* Hero section */}
    184         <div className="flex flex-col md:flex-row gap-8 mb-10">
    185           {/* Cover art */}
    186           <div className="w-full md:w-72 shrink-0">
    187             <div className="relative w-full pt-[100%] rounded-xl overflow-hidden shadow-2xl bg-[#181818]">
    188               <img
    189                 src={song.cover ? `${baseURL}/${song.cover}` : "/favicon.png"}
    190                 alt={song.title}
    191                 className="absolute inset-0 w-full h-full object-cover"
    192                 onError={(e) => {
    193                   (e.target as HTMLImageElement).src = "/favicon.png";
    194                 }}
    195               />
    196             </div>
    197           </div>
    198 
    199           {/* Song info */}
    200           <div className="flex flex-col justify-end gap-3 min-w-0">
    201             <span className="text-xs uppercase tracking-widest text-gray-400 font-medium">
    202               {song.genre} • Song
    203             </span>
    204             <h1 className="text-4xl md:text-5xl font-extrabold leading-tight truncate">
    205               {song.title}
    206             </h1>
    207 
    208             {/* Main artist */}
    209             <p className="text-xl text-gray-300 font-semibold">
    210               {song.releasedBy}
    211             </p>
    212 
    213             {/* Album */}
    214             {song.album && (
    215               <p className="text-sm text-gray-500">
    216                 Album: <span className="text-gray-300">{song.album}</span>
    217               </p>
    218             )}
    219 
    220             {/* Rating summary */}
    221             {avgRating && (
    222               <div className="flex items-center gap-2 mt-1">
    223                 {renderStars(Math.round(Number(avgRating)))}
    224                 <span className="text-sm text-gray-400">
    225                   {avgRating} · {song.reviews.length}{" "}
    226                   {song.reviews.length === 1 ? "review" : "reviews"}
    227                 </span>
    228               </div>
    229             )}
    230 
    231             {/* Action buttons */}
    232             <div className="flex items-center gap-3 mt-4">
    233               {/* Play button */}
    234               {song.link && (
    235                 <button
    236                   onClick={() =>
    237                     play({
    238                       id: song.id,
    239                       title: song.title,
    240                       artist: song.releasedBy,
    241                       cover: song.cover,
    242                       embedUrl: toEmbedUrl(song.link!),
    243                     })
    244                   }
    245                   className={`flex items-center gap-2 px-6 py-3 rounded-full text-sm font-semibold transition-all cursor-pointer ${
    246                     currentSong?.id === song.id
    247                       ? "bg-white text-[#1db954]"
    248                       : "bg-[#1db954] text-black hover:bg-[#1ed760] hover:scale-105"
    249                   }`}
    250                 >
    251                   {currentSong?.id === song.id ? (
    252                     <>
    253                       <svg
    254                         className="w-5 h-5"
    255                         fill="currentColor"
    256                         viewBox="0 0 24 24"
    257                       >
    258                         <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
    259                       </svg>
    260                       Now Playing
    261                     </>
    262                   ) : (
    263                     <>
    264                       <svg
    265                         className="w-5 h-5"
    266                         fill="currentColor"
    267                         viewBox="0 0 24 24"
    268                       >
    269                         <path d="M8 5v14l11-7z" />
    270                       </svg>
    271                       Play Song
    272                     </>
    273                   )}
    274                 </button>
    275               )}
    276 
    277               {user && (
    278                 <>
    279                   <button
    280                     onClick={toggleLike}
    281                     className={`flex items-center gap-2 px-5 py-3 rounded-full text-sm font-semibold transition-colors cursor-pointer ${
    282                       song.isLikedByCurrentUser
    283                         ? "bg-[#1db954] text-black"
    284                         : "bg-white/10 text-white hover:bg-white/20"
    285                     }`}
    286                   >
    287                     {song.isLikedByCurrentUser ? (
    288                       <svg
    289                         className="w-5 h-5"
    290                         fill="currentColor"
    291                         viewBox="0 0 24 24"
    292                       >
    293                         <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" />
    294                       </svg>
    295                     ) : (
    296                       <svg
    297                         className="w-5 h-5"
    298                         fill="none"
    299                         stroke="currentColor"
    300                         viewBox="0 0 24 24"
    301                       >
    302                         <path
    303                           strokeLinecap="round"
    304                           strokeLinejoin="round"
    305                           strokeWidth={2}
    306                           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"
    307                         />
    308                       </svg>
    309                     )}
    310                     {song.isLikedByCurrentUser ? "Liked" : "Like"}
    311                   </button>
    312 
    313                   {/* Add to Playlist button */}
    314                   <div className="relative">
    315                     <button
    316                       onClick={() => setShowPlaylistDropdown((prev) => !prev)}
    317                       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"
    318                     >
    319                       <svg
    320                         className="w-5 h-5"
    321                         fill="none"
    322                         stroke="currentColor"
    323                         viewBox="0 0 24 24"
    324                       >
    325                         <path
    326                           strokeLinecap="round"
    327                           strokeLinejoin="round"
    328                           strokeWidth={2}
    329                           d="M12 4v16m8-8H4"
    330                         />
    331                       </svg>
    332                       Add to Playlist
    333                     </button>
    334 
    335                     <PlaylistDropdown
    336                       songId={song.id}
    337                       isOpen={showPlaylistDropdown}
    338                       onClose={() => setShowPlaylistDropdown(false)}
    339                       direction="below"
    340                     />
    341                   </div>
    342                 </>
    343               )}
    344             </div>
    345           </div>
    346         </div>
    347 
    348         {/* Credits / Contributions */}
    349         {song.contributions.length > 0 && (
    350           <section className="mb-10">
    351             <h2 className="text-2xl font-bold mb-4">Credits</h2>
    352             <div className="bg-[#1a1a2e]/60 rounded-xl p-5 divide-y divide-white/5">
    353               {/* Main artist first */}
    354               {song.contributions
    355                 .filter((c) => c.artistName === song.releasedBy)
    356                 .map((c, i) => (
    357                   <div
    358                     key={`main-${i}`}
    359                     className="flex items-center justify-between py-3 first:pt-0 last:pb-0"
    360                   >
    361                     <div className="flex items-center gap-3">
    362                       <div className="w-10 h-10 bg-[#1db954] rounded-full flex items-center justify-center text-black font-bold text-sm">
    363                         {c.artistName.charAt(0).toUpperCase()}
    364                       </div>
    365                       <div>
    366                         <p className="text-white font-semibold text-lg">
    367                           {c.artistName}
    368                         </p>
    369                       </div>
    370                     </div>
    371                     <span className="text-sm text-gray-400 bg-white/5 px-3 py-1 rounded-full">
    372                       {formatRole(c.role)}
    373                     </span>
    374                   </div>
    375                 ))}
    376 
    377               {/* Other contributors */}
    378               {otherContributors.map((c, i) => (
    379                 <div
    380                   key={`contrib-${i}`}
    381                   className="flex items-center justify-between py-3 first:pt-0 last:pb-0"
    382                 >
    383                   <div className="flex items-center gap-3">
    384                     <div className="w-9 h-9 bg-white/10 rounded-full flex items-center justify-center text-white font-medium text-sm">
    385                       {c.artistName.charAt(0).toUpperCase()}
    386                     </div>
    387                     <p className="text-gray-300 font-medium">{c.artistName}</p>
    388                   </div>
    389                   <span className="text-sm text-gray-500 bg-white/5 px-3 py-1 rounded-full">
    390                     {formatRole(c.role)}
    391                   </span>
    392                 </div>
    393               ))}
    394             </div>
    395           </section>
    396         )}
    397 
    398         {/* Reviews */}
    399         <section>
    400           <div className="flex items-center justify-between mb-4">
    401             <h2 className="text-2xl font-bold">
    402               Reviews
    403               {song.reviews.length > 0 && (
    404                 <span className="text-base font-normal text-gray-500 ml-2">
    405                   ({song.reviews.length})
    406                 </span>
    407               )}
    408             </h2>
    409             {user != null && (
    410               <button
    411                 onClick={() => setShowReviewModal(true)}
    412                 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"
    413               >
    414                 <svg
    415                   className="w-4 h-4"
    416                   fill="none"
    417                   stroke="currentColor"
    418                   viewBox="0 0 24 24"
    419                 >
    420                   <path
    421                     strokeLinecap="round"
    422                     strokeLinejoin="round"
    423                     strokeWidth={2}
    424                     d="M12 4v16m8-8H4"
    425                   />
    426                 </svg>
    427                 Add Review
    428               </button>
    429             )}
    430           </div>
    431 
    432           {song.reviews.length === 0 ? (
    433             <div className="bg-[#1a1a2e]/60 rounded-xl p-10 text-center">
    434               <p className="text-gray-500 text-lg">No reviews yet.</p>
    435               <p className="text-gray-600 text-sm mt-1">
    436                 Be the first to share your thoughts!
    437               </p>
    438             </div>
    439           ) : (
    440             <div className="space-y-4">
    441               {song.reviews.map((review) => (
    442                 <div
    443                   key={`${review.id.listenerId}-${review.id.musicalEntityId}`}
    444                   className="bg-[#1a1a2e]/60 rounded-xl p-5 hover:bg-[#1a1a2e]/80 transition-colors"
    445                 >
    446                   <div className="flex items-start justify-between mb-2">
    447                     <div className="flex items-center gap-3">
    448                       <div className="w-9 h-9 bg-blue-500/20 rounded-full flex items-center justify-center">
    449                         <span className="text-blue-400 font-semibold text-sm">
    450                           {review.author.charAt(0).toUpperCase()}
    451                         </span>
    452                       </div>
    453                       <div>
    454                         <p className="text-white font-medium">
    455                           {review.author}
    456                         </p>
    457                         {renderStars(review.grade)}
    458                       </div>
    459                     </div>
    460                     {user?.username === review.authorUsername && (
    461                       <button
    462                         onClick={deleteReview}
    463                         className="text-gray-500 hover:text-red-400 transition-colors p-2 rounded-lg hover:bg-red-400/10"
    464                         title="Delete review"
    465                       >
    466                         <svg
    467                           className="w-5 h-5"
    468                           fill="none"
    469                           stroke="currentColor"
    470                           viewBox="0 0 24 24"
    471                         >
    472                           <path
    473                             strokeLinecap="round"
    474                             strokeLinejoin="round"
    475                             strokeWidth={2}
    476                             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"
    477                           />
    478                         </svg>
    479                       </button>
    480                     )}
    481                   </div>
    482                   <p className="text-gray-300 text-sm leading-relaxed mt-3 pl-12">
    483                     {review.comment}
    484                   </p>
    485                 </div>
    486               ))}
    487             </div>
    488           )}
    489         </section>
    490       </div>
    491 
    492       {/* Review Modal */}
    493       {showReviewModal && (
    494         <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm">
    495           <div className="bg-[#1a1a2e] rounded-2xl p-6 max-w-md w-full shadow-2xl">
    496             <div className="flex items-center justify-between mb-6">
    497               <h3 className="text-2xl font-bold">Add Review</h3>
    498               <button
    499                 onClick={() => {
    500                   setShowReviewModal(false);
    501                   setReviewRating(0);
    502                   setReviewComment("");
    503                   setHoverRating(0);
    504                 }}
    505                 className="text-gray-400 hover:text-white transition-colors"
    506               >
    507                 <svg
    508                   className="w-6 h-6"
    509                   fill="none"
    510                   stroke="currentColor"
    511                   viewBox="0 0 24 24"
    512                 >
    513                   <path
    514                     strokeLinecap="round"
    515                     strokeLinejoin="round"
    516                     strokeWidth={2}
    517                     d="M6 18L18 6M6 6l12 12"
    518                   />
    519                 </svg>
    520               </button>
    521             </div>
    522 
    523             {/* Star Rating */}
    524             <div className="mb-6">
    525               <label className="block text-sm font-medium mb-3">
    526                 Rating <span className="text-red-400">*</span>
    527               </label>
    528               <div className="flex gap-2">
    529                 {[1, 2, 3, 4, 5].map((star) => (
    530                   <button
    531                     key={star}
    532                     onClick={() => setReviewRating(star)}
    533                     onMouseEnter={() => setHoverRating(star)}
    534                     onMouseLeave={() => setHoverRating(0)}
    535                     className="transition-transform hover:scale-110"
    536                   >
    537                     <svg
    538                       className={`w-10 h-10 ${
    539                         star <= (hoverRating || reviewRating)
    540                           ? "text-yellow-400"
    541                           : "text-gray-600"
    542                       }`}
    543                       fill="currentColor"
    544                       viewBox="0 0 20 20"
    545                     >
    546                       <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" />
    547                     </svg>
    548                   </button>
    549                 ))}
    550               </div>
    551             </div>
    552 
    553             {/* Comment */}
    554             <div className="mb-6">
    555               <label className="block text-sm font-medium mb-2">
    556                 Comment <span className="text-gray-500">(optional)</span>
    557               </label>
    558               <textarea
    559                 value={reviewComment}
    560                 onChange={(e) => setReviewComment(e.target.value)}
    561                 placeholder="Share your thoughts about this song..."
    562                 rows={4}
    563                 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"
    564               />
    565             </div>
    566 
    567             {/* Actions */}
    568             <div className="flex gap-3 justify-end">
    569               <button
    570                 onClick={() => {
    571                   setShowReviewModal(false);
    572                   setReviewRating(0);
    573                   setReviewComment("");
    574                   setHoverRating(0);
    575                 }}
    576                 className="px-5 py-2.5 rounded-full text-sm font-semibold bg-white/10 hover:bg-white/20 transition-colors"
    577               >
    578                 Cancel
    579               </button>
    580               <button
    581                 onClick={handleSubmitReview}
    582                 disabled={reviewRating === 0}
    583                 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"
    584               >
    585                 Submit
    586               </button>
    587             </div>
    588           </div>
    589         </div>
    590       )}
    591     </div>
    592   );
     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        if (loading) {
     122                return (
     123                        <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
     124                                <div className="flex flex-col items-center gap-4">
     125                                        <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin" />
     126                                        <p className="text-gray-400 text-lg">Loading song…</p>
     127                                </div>
     128                        </div>
     129                );
     130        }
     131
     132        if (error || !song) {
     133                return (
     134                        <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
     135                                <div className="text-center">
     136                                        <p className="text-red-400 text-xl mb-4">
     137                                                {error ?? "Song not found"}
     138                                        </p>
     139                                        <Link to="/" className="text-[#1db954] hover:underline text-sm">
     140                                                ← Back to Home
     141                                        </Link>
     142                                </div>
     143                        </div>
     144                );
     145        }
     146
     147        const otherContributors = song.contributions.filter(
     148                (c) => c.artistName !== song.releasedBy,
     149        );
     150
     151        const avgRating =
     152                song.reviews.length > 0
     153                        ? (
     154                                        song.reviews.reduce((sum, r) => sum + r.grade, 0) /
     155                                        song.reviews.length
     156                                ).toFixed(1)
     157                        : null;
     158
     159        return (
     160                <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white">
     161                        <div className="max-w-5xl mx-auto px-6 py-10">
     162                                {/* Back link */}
     163                                <Link
     164                                        to="/"
     165                                        className="inline-flex items-center gap-1 text-gray-400 hover:text-white text-sm mb-8 transition-colors"
     166                                >
     167                                        <svg
     168                                                className="w-4 h-4"
     169                                                fill="none"
     170                                                stroke="currentColor"
     171                                                viewBox="0 0 24 24"
     172                                        >
     173                                                <path
     174                                                        strokeLinecap="round"
     175                                                        strokeLinejoin="round"
     176                                                        strokeWidth={2}
     177                                                        d="M15 19l-7-7 7-7"
     178                                                />
     179                                        </svg>
     180                                        Back to Home
     181                                </Link>
     182
     183                                {/* Hero section */}
     184                                <div className="flex flex-col md:flex-row gap-8 mb-10">
     185                                        {/* Cover art */}
     186                                        <div className="w-full md:w-72 shrink-0">
     187                                                <div className="relative w-full pt-[100%] rounded-xl overflow-hidden shadow-2xl bg-[#181818]">
     188                                                        <img
     189                                                                src={song.cover ? `${baseURL}/${song.cover}` : "/favicon.png"}
     190                                                                alt={song.title}
     191                                                                className="absolute inset-0 w-full h-full object-cover"
     192                                                                onError={(e) => {
     193                                                                        (e.target as HTMLImageElement).src = "/favicon.png";
     194                                                                }}
     195                                                        />
     196                                                </div>
     197                                        </div>
     198
     199                                        {/* Song info */}
     200                                        <div className="flex flex-col justify-end gap-3 min-w-0">
     201                                                <span className="text-xs uppercase tracking-widest text-gray-400 font-medium">
     202                                                        {song.genre} • Song
     203                                                </span>
     204                                                <h1 className="text-4xl md:text-5xl font-extrabold leading-tight truncate">
     205                                                        {song.title}
     206                                                </h1>
     207
     208                                                {/* Main artist */}
     209                                                <p className="text-xl text-gray-300 font-semibold">
     210                                                        {song.releasedBy}
     211                                                </p>
     212
     213                                                {/* Album */}
     214                                                {song.album && (
     215                                                        <p className="text-sm text-gray-500">
     216                                                                Album: <span className="text-gray-300">{song.album}</span>
     217                                                        </p>
     218                                                )}
     219
     220                                                {/* Rating summary */}
     221                                                {avgRating && (
     222                                                        <div className="flex items-center gap-2 mt-1">
     223                                                                {renderStars(Math.round(Number(avgRating)))}
     224                                                                <span className="text-sm text-gray-400">
     225                                                                        {avgRating} · {song.reviews.length}{" "}
     226                                                                        {song.reviews.length === 1 ? "review" : "reviews"}
     227                                                                </span>
     228                                                        </div>
     229                                                )}
     230
     231                                                {/* Action buttons */}
     232                                                <div className="flex items-center gap-3 mt-4">
     233                                                        {/* Play button */}
     234                                                        {song.link && (
     235                                                                <button
     236                                                                        onClick={() =>
     237                                                                                play({
     238                                                                                        id: song.id,
     239                                                                                        title: song.title,
     240                                                                                        artist: song.releasedBy,
     241                                                                                        cover: song.cover,
     242                                                                                        embedUrl: toEmbedUrl(song.link!),
     243                                                                                })
     244                                                                        }
     245                                                                        className={`flex items-center gap-2 px-6 py-3 rounded-full text-sm font-semibold transition-all cursor-pointer ${
     246                                                                                currentSong?.id === song.id
     247                                                                                        ? "bg-white text-[#1db954]"
     248                                                                                        : "bg-[#1db954] text-black hover:bg-[#1ed760] hover:scale-105"
     249                                                                        }`}
     250                                                                >
     251                                                                        {currentSong?.id === song.id ? (
     252                                                                                <>
     253                                                                                        <svg
     254                                                                                                className="w-5 h-5"
     255                                                                                                fill="currentColor"
     256                                                                                                viewBox="0 0 24 24"
     257                                                                                        >
     258                                                                                                <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
     259                                                                                        </svg>
     260                                                                                        Now Playing
     261                                                                                </>
     262                                                                        ) : (
     263                                                                                <>
     264                                                                                        <svg
     265                                                                                                className="w-5 h-5"
     266                                                                                                fill="currentColor"
     267                                                                                                viewBox="0 0 24 24"
     268                                                                                        >
     269                                                                                                <path d="M8 5v14l11-7z" />
     270                                                                                        </svg>
     271                                                                                        Play Song
     272                                                                                </>
     273                                                                        )}
     274                                                                </button>
     275                                                        )}
     276
     277                                                        {user && (
     278                                                                <>
     279                                                                        <button
     280                                                                                onClick={toggleLike}
     281                                                                                className={`flex items-center gap-2 px-5 py-3 rounded-full text-sm font-semibold transition-colors cursor-pointer ${
     282                                                                                        song.isLikedByCurrentUser
     283                                                                                                ? "bg-[#1db954] text-black"
     284                                                                                                : "bg-white/10 text-white hover:bg-white/20"
     285                                                                                }`}
     286                                                                        >
     287                                                                                {song.isLikedByCurrentUser ? (
     288                                                                                        <svg
     289                                                                                                className="w-5 h-5"
     290                                                                                                fill="currentColor"
     291                                                                                                viewBox="0 0 24 24"
     292                                                                                        >
     293                                                                                                <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" />
     294                                                                                        </svg>
     295                                                                                ) : (
     296                                                                                        <svg
     297                                                                                                className="w-5 h-5"
     298                                                                                                fill="none"
     299                                                                                                stroke="currentColor"
     300                                                                                                viewBox="0 0 24 24"
     301                                                                                        >
     302                                                                                                <path
     303                                                                                                        strokeLinecap="round"
     304                                                                                                        strokeLinejoin="round"
     305                                                                                                        strokeWidth={2}
     306                                                                                                        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"
     307                                                                                                />
     308                                                                                        </svg>
     309                                                                                )}
     310                                                                                {song.isLikedByCurrentUser ? "Liked" : "Like"}
     311                                                                        </button>
     312
     313                                                                        {/* Add to Playlist button */}
     314                                                                        <div className="relative">
     315                                                                                <button
     316                                                                                        onClick={() => setShowPlaylistDropdown((prev) => !prev)}
     317                                                                                        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"
     318                                                                                >
     319                                                                                        <svg
     320                                                                                                className="w-5 h-5"
     321                                                                                                fill="none"
     322                                                                                                stroke="currentColor"
     323                                                                                                viewBox="0 0 24 24"
     324                                                                                        >
     325                                                                                                <path
     326                                                                                                        strokeLinecap="round"
     327                                                                                                        strokeLinejoin="round"
     328                                                                                                        strokeWidth={2}
     329                                                                                                        d="M12 4v16m8-8H4"
     330                                                                                                />
     331                                                                                        </svg>
     332                                                                                        Add to Playlist
     333                                                                                </button>
     334
     335                                                                                <PlaylistDropdown
     336                                                                                        songId={song.id}
     337                                                                                        isOpen={showPlaylistDropdown}
     338                                                                                        onClose={() => setShowPlaylistDropdown(false)}
     339                                                                                        direction="below"
     340                                                                                />
     341                                                                        </div>
     342                                                                </>
     343                                                        )}
     344                                                </div>
     345                                        </div>
     346                                </div>
     347
     348                                {/* Credits / Contributions */}
     349                                {song.contributions.length > 0 && (
     350                                        <section className="mb-10">
     351                                                <h2 className="text-2xl font-bold mb-4">Credits</h2>
     352                                                <div className="bg-[#1a1a2e]/60 rounded-xl p-5 divide-y divide-white/5">
     353                                                        {/* Main artist first */}
     354                                                        {song.contributions
     355                                                                .filter((c) => c.artistName === song.releasedBy)
     356                                                                .map((c, i) => (
     357                                                                        <div
     358                                                                                key={`main-${i}`}
     359                                                                                className="flex items-center justify-between py-3 first:pt-0 last:pb-0"
     360                                                                        >
     361                                                                                <div className="flex items-center gap-3">
     362                                                                                        <div className="w-10 h-10 bg-[#1db954] rounded-full flex items-center justify-center text-black font-bold text-sm">
     363                                                                                                {c.artistName.charAt(0).toUpperCase()}
     364                                                                                        </div>
     365                                                                                        <div>
     366                                                                                                <p className="text-white font-semibold text-lg">
     367                                                                                                        {c.artistName}
     368                                                                                                </p>
     369                                                                                        </div>
     370                                                                                </div>
     371                                                                                <span className="text-sm text-gray-400 bg-white/5 px-3 py-1 rounded-full">
     372                                                                                        {formatRole(c.role)}
     373                                                                                </span>
     374                                                                        </div>
     375                                                                ))}
     376
     377                                                        {/* Other contributors */}
     378                                                        {otherContributors.map((c, i) => (
     379                                                                <div
     380                                                                        key={`contrib-${i}`}
     381                                                                        className="flex items-center justify-between py-3 first:pt-0 last:pb-0"
     382                                                                >
     383                                                                        <div className="flex items-center gap-3">
     384                                                                                <div className="w-9 h-9 bg-white/10 rounded-full flex items-center justify-center text-white font-medium text-sm">
     385                                                                                        {c.artistName.charAt(0).toUpperCase()}
     386                                                                                </div>
     387                                                                                <p className="text-gray-300 font-medium">{c.artistName}</p>
     388                                                                        </div>
     389                                                                        <span className="text-sm text-gray-500 bg-white/5 px-3 py-1 rounded-full">
     390                                                                                {formatRole(c.role)}
     391                                                                        </span>
     392                                                                </div>
     393                                                        ))}
     394                                                </div>
     395                                        </section>
     396                                )}
     397
     398                                {/* Reviews */}
     399                                <section>
     400                                        <div className="flex items-center justify-between mb-4">
     401                                                <h2 className="text-2xl font-bold">
     402                                                        Reviews
     403                                                        {song.reviews.length > 0 && (
     404                                                                <span className="text-base font-normal text-gray-500 ml-2">
     405                                                                        ({song.reviews.length})
     406                                                                </span>
     407                                                        )}
     408                                                </h2>
     409                                                {user != null && (
     410                                                        <button
     411                                                                onClick={() => setShowReviewModal(true)}
     412                                                                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 cursor-pointer"
     413                                                        >
     414                                                                <svg
     415                                                                        className="w-4 h-4"
     416                                                                        fill="none"
     417                                                                        stroke="currentColor"
     418                                                                        viewBox="0 0 24 24"
     419                                                                >
     420                                                                        <path
     421                                                                                strokeLinecap="round"
     422                                                                                strokeLinejoin="round"
     423                                                                                strokeWidth={2}
     424                                                                                d="M12 4v16m8-8H4"
     425                                                                        />
     426                                                                </svg>
     427                                                                Add Review
     428                                                        </button>
     429                                                )}
     430                                        </div>
     431
     432                                        {song.reviews.length === 0 ? (
     433                                                <div className="bg-[#1a1a2e]/60 rounded-xl p-10 text-center">
     434                                                        <p className="text-gray-500 text-lg">No reviews yet.</p>
     435                                                        <p className="text-gray-600 text-sm mt-1">
     436                                                                Be the first to share your thoughts!
     437                                                        </p>
     438                                                </div>
     439                                        ) : (
     440                                                <div className="space-y-4">
     441                                                        {song.reviews.map((review) => (
     442                                                                <div
     443                                                                        key={`${review.id.listenerId}-${review.id.musicalEntityId}`}
     444                                                                        className="bg-[#1a1a2e]/60 rounded-xl p-5 hover:bg-[#1a1a2e]/80 transition-colors"
     445                                                                >
     446                                                                        <div className="flex items-start justify-between mb-2">
     447                                                                                <div className="flex items-center gap-3">
     448                                                                                        <div className="w-9 h-9 bg-blue-500/20 rounded-full flex items-center justify-center">
     449                                                                                                <span className="text-blue-400 font-semibold text-sm">
     450                                                                                                        {review.author.charAt(0).toUpperCase()}
     451                                                                                                </span>
     452                                                                                        </div>
     453                                                                                        <div>
     454                                                                                                <p className="text-white font-medium">
     455                                                                                                        {review.author}
     456                                                                                                </p>
     457                                                                                                {renderStars(review.grade)}
     458                                                                                        </div>
     459                                                                                </div>
     460                                                                                {user?.username === review.authorUsername && (
     461                                                                                        <button
     462                                                                                                onClick={deleteReview}
     463                                                                                                className="text-gray-500 hover:text-red-400 transition-colors p-2 rounded-lg hover:bg-red-400/10 cursor-pointer"
     464                                                                                                title="Delete review"
     465                                                                                        >
     466                                                                                                <svg
     467                                                                                                        className="w-5 h-5"
     468                                                                                                        fill="none"
     469                                                                                                        stroke="currentColor"
     470                                                                                                        viewBox="0 0 24 24"
     471                                                                                                >
     472                                                                                                        <path
     473                                                                                                                strokeLinecap="round"
     474                                                                                                                strokeLinejoin="round"
     475                                                                                                                strokeWidth={2}
     476                                                                                                                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"
     477                                                                                                        />
     478                                                                                                </svg>
     479                                                                                        </button>
     480                                                                                )}
     481                                                                        </div>
     482                                                                        <p className="text-gray-300 text-sm leading-relaxed mt-3 pl-12">
     483                                                                                {review.comment}
     484                                                                        </p>
     485                                                                </div>
     486                                                        ))}
     487                                                </div>
     488                                        )}
     489                                </section>
     490                        </div>
     491
     492                        {/* Review Modal */}
     493                        {showReviewModal && (
     494                                <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm">
     495                                        <div className="bg-[#1a1a2e] rounded-2xl p-6 max-w-md w-full shadow-2xl">
     496                                                <div className="flex items-center justify-between mb-6">
     497                                                        <h3 className="text-2xl font-bold">Add Review</h3>
     498                                                        <button
     499                                                                onClick={() => {
     500                                                                        setShowReviewModal(false);
     501                                                                        setReviewRating(0);
     502                                                                        setReviewComment("");
     503                                                                        setHoverRating(0);
     504                                                                }}
     505                                                                className="text-gray-400 hover:text-white transition-colors cursor-pointer"
     506                                                        >
     507                                                                <svg
     508                                                                        className="w-6 h-6"
     509                                                                        fill="none"
     510                                                                        stroke="currentColor"
     511                                                                        viewBox="0 0 24 24"
     512                                                                >
     513                                                                        <path
     514                                                                                strokeLinecap="round"
     515                                                                                strokeLinejoin="round"
     516                                                                                strokeWidth={2}
     517                                                                                d="M6 18L18 6M6 6l12 12"
     518                                                                        />
     519                                                                </svg>
     520                                                        </button>
     521                                                </div>
     522
     523                                                {/* Star Rating */}
     524                                                <div className="mb-6">
     525                                                        <label className="block text-sm font-medium mb-3">
     526                                                                Rating <span className="text-red-400">*</span>
     527                                                        </label>
     528                                                        <div className="flex gap-2">
     529                                                                {[1, 2, 3, 4, 5].map((star) => (
     530                                                                        <button
     531                                                                                key={star}
     532                                                                                onClick={() => setReviewRating(star)}
     533                                                                                onMouseEnter={() => setHoverRating(star)}
     534                                                                                onMouseLeave={() => setHoverRating(0)}
     535                                                                                className="transition-transform hover:scale-110 cursor-pointer"
     536                                                                        >
     537                                                                                <svg
     538                                                                                        className={`w-10 h-10 ${
     539                                                                                                star <= (hoverRating || reviewRating)
     540                                                                                                        ? "text-yellow-400"
     541                                                                                                        : "text-gray-600"
     542                                                                                        }`}
     543                                                                                        fill="currentColor"
     544                                                                                        viewBox="0 0 20 20"
     545                                                                                >
     546                                                                                        <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" />
     547                                                                                </svg>
     548                                                                        </button>
     549                                                                ))}
     550                                                        </div>
     551                                                </div>
     552
     553                                                {/* Comment */}
     554                                                <div className="mb-6">
     555                                                        <label className="block text-sm font-medium mb-2">
     556                                                                Comment <span className="text-gray-500">(optional)</span>
     557                                                        </label>
     558                                                        <textarea
     559                                                                value={reviewComment}
     560                                                                onChange={(e) => setReviewComment(e.target.value)}
     561                                                                placeholder="Share your thoughts about this song..."
     562                                                                rows={4}
     563                                                                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"
     564                                                        />
     565                                                </div>
     566
     567                                                {/* Actions */}
     568                                                <div className="flex gap-3 justify-end">
     569                                                        <button
     570                                                                onClick={() => {
     571                                                                        setShowReviewModal(false);
     572                                                                        setReviewRating(0);
     573                                                                        setReviewComment("");
     574                                                                        setHoverRating(0);
     575                                                                }}
     576                                                                className="px-5 py-2.5 rounded-full text-sm font-semibold bg-white/10 hover:bg-white/20 transition-colors cursor-pointer"
     577                                                        >
     578                                                                Cancel
     579                                                        </button>
     580                                                        <button
     581                                                                onClick={handleSubmitReview}
     582                                                                disabled={reviewRating === 0}
     583                                                                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 cursor-pointer"
     584                                                        >
     585                                                                Submit
     586                                                        </button>
     587                                                </div>
     588                                        </div>
     589                                </div>
     590                        )}
     591                </div>
     592        );
    593593};
    594594
Note: See TracChangeset for help on using the changeset viewer.