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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • 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.