Ignore:
Timestamp:
02/10/26 21:58:39 (5 months ago)
Author:
Filip Gavrilovski <filipgavrilovski28@…>
Branches:
main
Children:
92db381
Parents:
0808ef2
Message:

refactor artist, collection, listener components to have similar style as other components; refactor some dto-s

File:
1 edited

Legend:

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

    r0808ef2 r1579b4f  
    11import { useEffect, useState } from "react";
    2 import { useNavigate, useParams } from "react-router-dom";
     2import { Link, useParams } from "react-router-dom";
    33import axiosInstance from "../api/axiosInstance";
    4 import LoadingSpinner from "../components/LoadingSpinner";
    5 import type { Song, Album, Playlist } from "../utils/types";
     4import SongItem from "../components/SongItem";
    65import { handleError } from "../utils/error";
     6import type { Album, Playlist, Song } from "../utils/types";
    77interface CollectionView {
    8   id: number;
    9   title: string;
    10   genre?: string;
    11   type: string;
    12   releasedBy: string;
    13   isLikedByCurrentUser?: boolean;
    14   songs: Song[];
     8        id: number;
     9        title: string;
     10        cover?: string | null;
     11        genre?: string;
     12        type: string;
     13        releasedBy: string;
     14        isLikedByCurrentUser?: boolean;
     15        songs: Song[];
    1516}
    1617
    1718const MusicalCollection = () => {
    18   const { type, id } = useParams();
    19   const navigate = useNavigate();
    20   const [collection, setCollection] = useState<CollectionView | null>(null);
    21   const [isLoading, setIsLoading] = useState(true);
    22   const [error, setError] = useState<string | null>(null);
    23 
    24   const normalizeCollection = (
    25     data: Album | Playlist,
    26     type: string,
    27   ): CollectionView => {
    28     if (type === "album") {
    29       const album = data as Album;
    30       return {
    31         id: album.id,
    32         title: album.title,
    33         genre: album.genre,
    34         type: album.type,
    35         releasedBy: album.releasedBy,
    36         isLikedByCurrentUser: album.isLikedByCurrentUser,
    37         songs: album.songs,
    38       };
    39     } else {
    40       const playlist = data as Playlist;
    41       return {
    42         id: playlist.id,
    43         title: playlist.name,
    44         genre: undefined,
    45         type: "PLAYLIST",
    46         releasedBy: playlist.creatorName,
    47         isLikedByCurrentUser: undefined,
    48         songs: playlist.songsInPlaylist,
    49       };
    50     }
    51   };
    52 
    53   useEffect(() => {
    54     const fetchData = async () => {
    55       setIsLoading(true);
    56       setError(null);
    57       try {
    58         const endpoint =
    59           type === "album" ? `/albums/${id}` : `/playlists/${id}`;
    60         const response = await axiosInstance.get(endpoint);
    61 
    62         const normalized = normalizeCollection(response.data, type!);
    63         setCollection(normalized);
    64       } catch (err: any) {
    65         setError(handleError(err));
    66       } finally {
    67         setIsLoading(false);
    68       }
    69     };
    70     fetchData();
    71   }, [id, type]);
    72 
    73   if (isLoading) {
    74     return <LoadingSpinner />;
    75   }
    76 
    77   if (error) {
    78     return (
    79       <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg">
    80         <h2 className="font-bold">Error</h2>
    81         <p>{error}</p>
    82       </div>
    83     );
    84   }
    85 
    86   if (!collection) return null;
    87 
    88   return (
    89     <div className="container mx-auto p-6">
    90       <button
    91         onClick={() => navigate(-1)}
    92         className="mb-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors duration-200 cursor-pointer"
    93       >
    94         ← Back
    95       </button>
    96 
    97       <div className="bg-white shadow-lg rounded-lg p-8">
    98         <div className="flex items-start gap-6 mb-8">
    99           <div className="shrink-0">
    100             <div className="w-40 h-40 rounded-lg bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center text-white text-5xl font-bold shadow-lg">
    101               {collection.title.charAt(0).toUpperCase()}
    102             </div>
    103           </div>
    104 
    105           <div className="flex-1">
    106             <span className="inline-block px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium mb-2">
    107               {collection.type}
    108             </span>
    109             <h1 className="text-4xl font-bold mb-3">{collection.title}</h1>
    110 
    111             <div className="flex items-center gap-3 text-gray-700 mb-4">
    112               <span className="font-semibold">{collection.releasedBy}</span>
    113               {collection.genre && (
    114                 <>
    115                   <span>•</span>
    116                   <span className="text-gray-600">{collection.genre}</span>
    117                 </>
    118               )}
    119               {collection.songs && (
    120                 <>
    121                   <span>•</span>
    122                   <span className="text-gray-600">
    123                     {collection.songs.length} song
    124                     {collection.songs.length !== 1 ? "s" : ""}
    125                   </span>
    126                 </>
    127               )}
    128             </div>
    129 
    130             {type === "album" && (
    131               <button
    132                 className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors duration-200"
    133                 aria-label={collection.isLikedByCurrentUser ? "Unlike" : "Like"}
    134               >
    135                 <svg
    136                   className="w-5 h-5"
    137                   fill={collection.isLikedByCurrentUser ? "#ef4444" : "none"}
    138                   stroke={
    139                     collection.isLikedByCurrentUser ? "#ef4444" : "#6b7280"
    140                   }
    141                   strokeWidth="2"
    142                   viewBox="0 0 24 24"
    143                 >
    144                   <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
    145                 </svg>
    146                 <span className="text-sm font-medium text-gray-700">
    147                   {collection.isLikedByCurrentUser ? "Liked" : "Like"}
    148                 </span>
    149               </button>
    150             )}
    151           </div>
    152         </div>
    153 
    154         <div className="border-t pt-6">
    155           <h2 className="text-2xl font-bold mb-4 text-gray-800">Songs</h2>
    156 
    157           {collection.songs && collection.songs.length > 0 ? (
    158             <div className="space-y-2">
    159               {collection.songs.map((song, index) => (
    160                 <div
    161                   key={song.id}
    162                   className="flex items-center gap-4 p-3 rounded-lg hover:bg-gray-50 transition-colors duration-150"
    163                 >
    164                   <span className="text-gray-500 font-medium w-8 text-center">
    165                     {index + 1}
    166                   </span>
    167 
    168                   <div className="flex-1 min-w-0">
    169                     <p className="font-semibold text-gray-900 truncate">
    170                       {song.title}
    171                     </p>
    172                     <p className="text-sm text-gray-600 truncate">
    173                       {song.releasedBy}
    174                     </p>
    175                   </div>
    176 
    177                   <span className="text-sm text-gray-600 px-3 py-1 bg-gray-100 rounded-full">
    178                     {song.genre}
    179                   </span>
    180 
    181                   <button
    182                     className="p-2 hover:bg-gray-100 rounded-full transition-colors duration-200"
    183                     aria-label={song.isLikedByCurrentUser ? "Unlike" : "Like"}
    184                   >
    185                     <svg
    186                       className="w-5 h-5"
    187                       fill={song.isLikedByCurrentUser ? "#ef4444" : "none"}
    188                       stroke={song.isLikedByCurrentUser ? "#ef4444" : "#6b7280"}
    189                       strokeWidth="2"
    190                       viewBox="0 0 24 24"
    191                     >
    192                       <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
    193                     </svg>
    194                   </button>
    195                 </div>
    196               ))}
    197             </div>
    198           ) : (
    199             <p className="text-center text-gray-500 py-8">No songs available</p>
    200           )}
    201         </div>
    202       </div>
    203     </div>
    204   );
     19        const { type, id } = useParams();
     20        const [collection, setCollection] = useState<CollectionView | null>(null);
     21        const [isLoading, setIsLoading] = useState(true);
     22        const [error, setError] = useState<string | null>(null);
     23
     24        const normalizeCollection = (
     25                data: Album | Playlist,
     26                type: string,
     27        ): CollectionView => {
     28                if (type === "album") {
     29                        const album = data as Album;
     30                        return {
     31                                id: album.id,
     32                                title: album.title,
     33                                cover: album.cover,
     34                                genre: album.genre,
     35                                type: album.type,
     36                                releasedBy: album.releasedBy,
     37                                isLikedByCurrentUser: album.isLikedByCurrentUser,
     38                                songs: album.songs,
     39                        };
     40                } else {
     41                        const playlist = data as Playlist;
     42                        return {
     43                                id: playlist.id,
     44                                title: playlist.name,
     45                                cover: playlist.cover,
     46                                genre: undefined,
     47                                type: "PLAYLIST",
     48                                releasedBy: playlist.creatorName,
     49                                isLikedByCurrentUser: undefined,
     50                                songs: playlist.songsInPlaylist,
     51                        };
     52                }
     53        };
     54
     55        const toggleLike = async (songId: number) => {
     56                try {
     57                        await axiosInstance.post(`/musical-entity/${songId}/like`);
     58                        setCollection((prev) => {
     59                                if (!prev) return null;
     60                                return {
     61                                        ...prev,
     62                                        songs: prev.songs.map((s) =>
     63                                                s.id === songId
     64                                                        ? { ...s, isLikedByCurrentUser: !s.isLikedByCurrentUser }
     65                                                        : s,
     66                                        ),
     67                                };
     68                        });
     69                } catch (err) {
     70                        console.error("Error toggling like:", err);
     71                }
     72        };
     73
     74        const toggleCollectionLike = async () => {
     75                if (!collection) return;
     76                try {
     77                        await axiosInstance.post(`/musical-entity/${collection.id}/like`);
     78                        setCollection((prev) => {
     79                                if (!prev) return null;
     80                                return { ...prev, isLikedByCurrentUser: !prev.isLikedByCurrentUser };
     81                        });
     82                } catch (err) {
     83                        console.error("Error toggling collection like:", err);
     84                }
     85        };
     86
     87        useEffect(() => {
     88                const fetchData = async () => {
     89                        setIsLoading(true);
     90                        setError(null);
     91                        try {
     92                                const endpoint =
     93                                        type === "album" ? `/albums/${id}` : `/playlists/${id}`;
     94                                const response = await axiosInstance.get(endpoint);
     95
     96                                const normalized = normalizeCollection(response.data, type!);
     97                                setCollection(normalized);
     98                        } catch (err: any) {
     99                                setError(handleError(err));
     100                        } finally {
     101                                setIsLoading(false);
     102                        }
     103                };
     104                fetchData();
     105        }, [id, type]);
     106
     107        if (isLoading) {
     108                return (
     109                        <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
     110                                <div className="flex flex-col items-center gap-4">
     111                                        <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin" />
     112                                        <p className="text-gray-400 text-lg">Loading collection…</p>
     113                                </div>
     114                        </div>
     115                );
     116        }
     117
     118        if (error) {
     119                return (
     120                        <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
     121                                <div className="text-center">
     122                                        <p className="text-red-400 text-xl mb-4">{error}</p>
     123                                        <Link to="/" className="text-[#1db954] hover:underline text-sm">
     124                                                ← Back to Home
     125                                        </Link>
     126                                </div>
     127                        </div>
     128                );
     129        }
     130
     131        if (!collection) return null;
     132
     133        return (
     134                <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white">
     135                        <div className="max-w-5xl mx-auto px-6 py-10">
     136                                {/* Back link */}
     137                                <Link
     138                                        to="/"
     139                                        className="inline-flex items-center gap-1 text-gray-400 hover:text-white text-sm mb-8 transition-colors"
     140                                >
     141                                        <svg
     142                                                className="w-4 h-4"
     143                                                fill="none"
     144                                                stroke="currentColor"
     145                                                viewBox="0 0 24 24"
     146                                        >
     147                                                <path
     148                                                        strokeLinecap="round"
     149                                                        strokeLinejoin="round"
     150                                                        strokeWidth={2}
     151                                                        d="M15 19l-7-7 7-7"
     152                                                />
     153                                        </svg>
     154                                        Back to Home
     155                                </Link>
     156
     157                                {/* Hero section */}
     158                                <div className="flex flex-col md:flex-row gap-8 mb-10">
     159                                        {/* Cover art */}
     160                                        <div className="w-full md:w-72 shrink-0">
     161                                                <div className="relative w-full pt-[100%] rounded-xl overflow-hidden shadow-2xl bg-[#181818]">
     162                                                        <img
     163                                                                src={collection.cover || "/favicon.png"}
     164                                                                alt={collection.title}
     165                                                                className="absolute inset-0 w-full h-full object-cover"
     166                                                                onError={(e) => {
     167                                                                        (e.target as HTMLImageElement).src = "/favicon.png";
     168                                                                }}
     169                                                        />
     170                                                </div>
     171                                        </div>
     172
     173                                        {/* Collection info */}
     174                                        <div className="flex flex-col justify-end gap-3 min-w-0">
     175                                                <span className="text-xs uppercase tracking-widest text-gray-400 font-medium">
     176                                                        {collection.genre ? `${collection.genre} • ` : ""}
     177                                                        {collection.type === "PLAYLIST" ? "Playlist" : "Album"}
     178                                                </span>
     179                                                <h1 className="text-4xl md:text-5xl font-extrabold leading-tight truncate">
     180                                                        {collection.title}
     181                                                </h1>
     182
     183                                                <p className="text-xl text-gray-300 font-semibold">
     184                                                        {collection.releasedBy}
     185                                                </p>
     186
     187                                                {collection.songs && (
     188                                                        <p className="text-sm text-gray-500">
     189                                                                {collection.songs.length} song
     190                                                                {collection.songs.length !== 1 ? "s" : ""}
     191                                                        </p>
     192                                                )}
     193
     194                                                {/* Action buttons */}
     195                                                <div className="flex items-center gap-3 mt-4">
     196                                                        {type === "album" && (
     197                                                                <button
     198                                                                        onClick={toggleCollectionLike}
     199                                                                        className={`flex items-center gap-2 px-5 py-3 rounded-full text-sm font-semibold transition-colors cursor-pointer ${
     200                                                                                collection.isLikedByCurrentUser
     201                                                                                        ? "bg-[#1db954] text-black"
     202                                                                                        : "bg-white/10 text-white hover:bg-white/20"
     203                                                                        }`}
     204                                                                >
     205                                                                        <svg
     206                                                                                className="w-5 h-5"
     207                                                                                fill={
     208                                                                                        collection.isLikedByCurrentUser ? "currentColor" : "none"
     209                                                                                }
     210                                                                                stroke="currentColor"
     211                                                                                viewBox="0 0 24 24"
     212                                                                        >
     213                                                                                <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
     214                                                                        </svg>
     215                                                                        {collection.isLikedByCurrentUser ? "Liked" : "Like"}
     216                                                                </button>
     217                                                        )}
     218                                                </div>
     219                                        </div>
     220                                </div>
     221
     222                                {/* Songs list */}
     223                                <div className="border-t border-white/10 pt-6">
     224                                        <h2 className="text-2xl font-bold mb-4">Songs</h2>
     225
     226                                        {collection.songs && collection.songs.length > 0 ? (
     227                                                <div className="space-y-1">
     228                                                        {collection.songs.map((song, index) => (
     229                                                                <SongItem
     230                                                                        key={song.id}
     231                                                                        song={song}
     232                                                                        index={index + 1}
     233                                                                        onLikeToggle={() => toggleLike(song.id)}
     234                                                                />
     235                                                        ))}
     236                                                </div>
     237                                        ) : (
     238                                                <div className="text-center py-12 text-gray-400">
     239                                                        <p className="text-lg">No songs available</p>
     240                                                </div>
     241                                        )}
     242                                </div>
     243                        </div>
     244                </div>
     245        );
    205246};
    206247
Note: See TracChangeset for help on using the changeset viewer.