Ignore:
Timestamp:
02/14/26 18:21:18 (5 months ago)
Author:
Dimitar Arsov <dimitararsov04@…>
Branches:
main
Children:
ce45c7a
Parents:
f5bc95e
Message:

save playlist

File:
1 edited

Legend:

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

    rf5bc95e r85512ff  
    33import axiosInstance, { baseURL } from "../api/axiosInstance";
    44import SongItem from "../components/SongItem";
    5 import { handleError } from "../utils/error";
     5import { getErrorMessage } from "../utils/error";
    66import type { Album, Playlist, Song } from "../utils/types";
    77interface CollectionView {
    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[];
     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[];
    1616}
    1717
    1818const MusicalCollection = () => {
    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={
    164                                                                         collection.cover
    165                                                                                 ? `${baseURL}/${collection.cover}`
    166                                                                                 : "/favicon.png"
    167                                                                 }
    168                                                                 alt={collection.title}
    169                                                                 className="absolute inset-0 w-full h-full object-cover"
    170                                                                 onError={(e) => {
    171                                                                         (e.target as HTMLImageElement).src = "/favicon.png";
    172                                                                 }}
    173                                                         />
    174                                                 </div>
    175                                         </div>
    176 
    177                                         {/* Collection info */}
    178                                         <div className="flex flex-col justify-end gap-3 min-w-0">
    179                                                 <span className="text-xs uppercase tracking-widest text-gray-400 font-medium">
    180                                                         {collection.genre ? `${collection.genre} • ` : ""}
    181                                                         {collection.type === "PLAYLIST" ? "Playlist" : "Album"}
    182                                                 </span>
    183                                                 <h1 className="text-4xl md:text-5xl font-extrabold leading-tight truncate">
    184                                                         {collection.title}
    185                                                 </h1>
    186 
    187                                                 <p className="text-xl text-gray-300 font-semibold">
    188                                                         {collection.releasedBy}
    189                                                 </p>
    190 
    191                                                 {collection.songs && (
    192                                                         <p className="text-sm text-gray-500">
    193                                                                 {collection.songs.length} song
    194                                                                 {collection.songs.length !== 1 ? "s" : ""}
    195                                                         </p>
    196                                                 )}
    197 
    198                                                 {/* Action buttons */}
    199                                                 <div className="flex items-center gap-3 mt-4">
    200                                                         {type === "album" && (
    201                                                                 <button
    202                                                                         onClick={toggleCollectionLike}
    203                                                                         className={`flex items-center gap-2 px-5 py-3 rounded-full text-sm font-semibold transition-colors cursor-pointer ${
    204                                                                                 collection.isLikedByCurrentUser
    205                                                                                         ? "bg-[#1db954] text-black"
    206                                                                                         : "bg-white/10 text-white hover:bg-white/20"
    207                                                                         }`}
    208                                                                 >
    209                                                                         <svg
    210                                                                                 className="w-5 h-5"
    211                                                                                 fill={
    212                                                                                         collection.isLikedByCurrentUser ? "currentColor" : "none"
    213                                                                                 }
    214                                                                                 stroke="currentColor"
    215                                                                                 viewBox="0 0 24 24"
    216                                                                         >
    217                                                                                 <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" />
    218                                                                         </svg>
    219                                                                         {collection.isLikedByCurrentUser ? "Liked" : "Like"}
    220                                                                 </button>
    221                                                         )}
    222                                                 </div>
    223                                         </div>
    224                                 </div>
    225 
    226                                 {/* Songs list */}
    227                                 <div className="border-t border-white/10 pt-6">
    228                                         <h2 className="text-2xl font-bold mb-4">Songs</h2>
    229 
    230                                         {collection.songs && collection.songs.length > 0 ? (
    231                                                 <div className="space-y-1">
    232                                                         {collection.songs.map((song, index) => (
    233                                                                 <SongItem
    234                                                                         key={song.id}
    235                                                                         song={song}
    236                                                                         index={index + 1}
    237                                                                         onLikeToggle={() => toggleLike(song.id)}
    238                                                                 />
    239                                                         ))}
    240                                                 </div>
    241                                         ) : (
    242                                                 <div className="text-center py-12 text-gray-400">
    243                                                         <p className="text-lg">No songs available</p>
    244                                                 </div>
    245                                         )}
    246                                 </div>
    247                         </div>
    248                 </div>
    249         );
     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(getErrorMessage(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={
     164                  collection.cover
     165                    ? `${baseURL}/${collection.cover}`
     166                    : "/favicon.png"
     167                }
     168                alt={collection.title}
     169                className="absolute inset-0 w-full h-full object-cover"
     170                onError={(e) => {
     171                  (e.target as HTMLImageElement).src = "/favicon.png";
     172                }}
     173              />
     174            </div>
     175          </div>
     176
     177          {/* Collection info */}
     178          <div className="flex flex-col justify-end gap-3 min-w-0">
     179            <span className="text-xs uppercase tracking-widest text-gray-400 font-medium">
     180              {collection.genre ? `${collection.genre} • ` : ""}
     181              {collection.type === "PLAYLIST" ? "Playlist" : "Album"}
     182            </span>
     183            <h1 className="text-4xl md:text-5xl font-extrabold leading-tight truncate">
     184              {collection.title}
     185            </h1>
     186
     187            <p className="text-xl text-gray-300 font-semibold">
     188              {collection.releasedBy}
     189            </p>
     190
     191            {collection.songs && (
     192              <p className="text-sm text-gray-500">
     193                {collection.songs.length} song
     194                {collection.songs.length !== 1 ? "s" : ""}
     195              </p>
     196            )}
     197
     198            {/* Action buttons */}
     199            <div className="flex items-center gap-3 mt-4">
     200              {type === "album" && (
     201                <button
     202                  onClick={toggleCollectionLike}
     203                  className={`flex items-center gap-2 px-5 py-3 rounded-full text-sm font-semibold transition-colors cursor-pointer ${
     204                    collection.isLikedByCurrentUser
     205                      ? "bg-[#1db954] text-black"
     206                      : "bg-white/10 text-white hover:bg-white/20"
     207                  }`}
     208                >
     209                  <svg
     210                    className="w-5 h-5"
     211                    fill={
     212                      collection.isLikedByCurrentUser ? "currentColor" : "none"
     213                    }
     214                    stroke="currentColor"
     215                    viewBox="0 0 24 24"
     216                  >
     217                    <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" />
     218                  </svg>
     219                  {collection.isLikedByCurrentUser ? "Liked" : "Like"}
     220                </button>
     221              )}
     222            </div>
     223          </div>
     224        </div>
     225
     226        {/* Songs list */}
     227        <div className="border-t border-white/10 pt-6">
     228          <h2 className="text-2xl font-bold mb-4">Songs</h2>
     229
     230          {collection.songs && collection.songs.length > 0 ? (
     231            <div className="space-y-1">
     232              {collection.songs.map((song, index) => (
     233                <SongItem
     234                  key={song.id}
     235                  song={song}
     236                  index={index + 1}
     237                  onLikeToggle={() => toggleLike(song.id)}
     238                />
     239              ))}
     240            </div>
     241          ) : (
     242            <div className="text-center py-12 text-gray-400">
     243              <p className="text-lg">No songs available</p>
     244            </div>
     245          )}
     246        </div>
     247      </div>
     248    </div>
     249  );
    250250};
    251251
Note: See TracChangeset for help on using the changeset viewer.