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

Location:
frontend/src/components
Files:
1 added
5 edited

Legend:

Unmodified
Added
Removed
  • frontend/src/components/Sidebar.tsx

    r0808ef2 r1579b4f  
    55import { usePlayer } from "../context/playerContext";
    66import type { BasicPlaylist, BasicSong, SidebarProps } from "../utils/types";
    7 
    8 const toEmbedUrl = (url: string): string => {
    9         try {
    10                 const parsed = new URL(url);
    11                 if (
    12                         (parsed.hostname === "www.youtube.com" ||
    13                                 parsed.hostname === "youtube.com") &&
    14                         parsed.searchParams.has("v")
    15                 ) {
    16                         return `https://www.youtube.com/embed/${parsed.searchParams.get("v")}`;
    17                 }
    18                 if (parsed.hostname === "youtu.be") {
    19                         return `https://www.youtube.com/embed${parsed.pathname}`;
    20                 }
    21                 return url;
    22         } catch {
    23                 return url;
    24         }
    25 };
     7import { toEmbedUrl } from "../utils/utils";
    268
    279const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
  • frontend/src/components/search/SongResult.tsx

    r0808ef2 r1579b4f  
    1 import { useNavigate } from "react-router-dom";
    2 import { usePlayer } from "../../context/playerContext";
     1import axiosInstance from "../../api/axiosInstance";
    32import type { Song } from "../../utils/types";
     3import SongItem from "../SongItem";
    44
    55interface SongResultProps {
    66        song: Song;
     7        /** Propagate like state change upward if needed */
     8        onLikeToggled?: (songId: number, isLiked: boolean) => void;
    79}
    810
    9 const toEmbedUrl = (url: string): string => {
    10         try {
    11                 const parsed = new URL(url);
    12                 if (
    13                         (parsed.hostname === "www.youtube.com" ||
    14                                 parsed.hostname === "youtube.com") &&
    15                         parsed.searchParams.has("v")
    16                 ) {
    17                         return `https://www.youtube.com/embed/${parsed.searchParams.get("v")}`;
     11const SongResult = ({ song, onLikeToggled }: SongResultProps) => {
     12        const handleLike = async (songId: number) => {
     13                try {
     14                        const response = await axiosInstance.post(
     15                                `/musical-entity/${songId}/like`,
     16                        );
     17                        onLikeToggled?.(songId, response.data.isLiked);
     18                } catch (err) {
     19                        console.error("Error toggling like:", err);
    1820                }
    19                 if (parsed.hostname === "youtu.be") {
    20                         return `https://www.youtube.com/embed${parsed.pathname}`;
    21                 }
    22                 return url;
    23         } catch {
    24                 return url;
    25         }
    26 };
     21        };
    2722
    28 const SongResult = ({ song }: SongResultProps) => {
    29         const navigate = useNavigate();
    30         const { play, currentSong } = usePlayer();
    31 
    32         return (
    33                 <div
    34                         onClick={() => navigate(`/songs/${song.id}`)}
    35                         className="flex items-center gap-4 p-3 rounded-lg hover:bg-white/5 cursor-pointer transition-colors group"
    36                 >
    37                         <img
    38                                 src={song.cover || "/favicon.png"}
    39                                 alt={song.title}
    40                                 className="w-12 h-12 rounded object-cover"
    41                                 onError={(e) => {
    42                                         (e.target as HTMLImageElement).src = "/favicon.png";
    43                                 }}
    44                         />
    45                         <div className="flex-1 min-w-0">
    46                                 <p className="text-white font-medium truncate">{song.title}</p>
    47                                 <p className="text-sm text-gray-400 truncate">
    48                                         Song • {song.releasedBy}
    49                                 </p>
    50                         </div>
    51                         <span className="text-xs text-gray-500 uppercase tracking-wider mr-2">
    52                                 {song.genre}
    53                         </span>
    54                         {song.link && (
    55                                 <button
    56                                         onClick={(e) => {
    57                                                 e.stopPropagation();
    58                                                 play({
    59                                                         id: song.id,
    60                                                         title: song.title,
    61                                                         artist: song.releasedBy,
    62                                                         cover: song.cover,
    63                                                         embedUrl: toEmbedUrl(song.link!),
    64                                                 });
    65                                         }}
    66                                         className={`p-2 rounded-full transition-all opacity-0 group-hover:opacity-100 ${
    67                                                 currentSong?.id === song.id
    68                                                         ? "bg-white text-[#1db954]"
    69                                                         : "bg-[#1db954] text-black hover:scale-110"
    70                                         }`}
    71                                         aria-label={currentSong?.id === song.id ? "Now playing" : "Play song"}
    72                                 >
    73                                         {currentSong?.id === song.id ? (
    74                                                 <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
    75                                                         <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
    76                                                 </svg>
    77                                         ) : (
    78                                                 <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
    79                                                         <path d="M8 5v14l11-7z" />
    80                                                 </svg>
    81                                         )}
    82                                 </button>
    83                         )}
    84                 </div>
    85         );
     23        return <SongItem song={song} label="Song" onLikeToggle={handleLike} />;
    8624};
    8725
  • frontend/src/components/userProfile/ArtistView.tsx

    r0808ef2 r1579b4f  
     1import { Disc3, Music } from "lucide-react";
     2import { useState } from "react";
    13import { useNavigate } from "react-router-dom";
    2 import { useState } from "react";
    3 import { Music, Disc3, Play, Plus } from "lucide-react";
     4import { toast } from "react-toastify";
     5import axiosInstance from "../../api/axiosInstance";
    46import type { ArtistContribution } from "../../utils/types";
    5 import axiosInstance from "../../api/axiosInstance";
     7import SongItem from "../SongItem";
    68
    79interface ArtistViewProps {
    8   contributions: ArtistContribution[];
     10        contributions: ArtistContribution[];
    911}
    1012
    1113const ArtistView = ({ contributions }: ArtistViewProps) => {
    12   const navigate = useNavigate();
    13   const [items, setItems] = useState(contributions);
    14   const [toast, setToast] = useState<{ message: string; show: boolean }>({
    15     message: "",
    16     show: false,
    17   });
     14        const navigate = useNavigate();
     15        const [items, setItems] = useState(contributions);
    1816
    19   const albums = items.filter((c) => c.entityType === "ALBUM");
    20   const songs = items.filter((c) => c.entityType === "SONG");
     17        const albums = items.filter((c) => c.entityType === "ALBUM");
     18        const songs = items.filter((c) => c.entityType === "SONG");
    2119
    22   const getRoleColor = (role: string) => {
    23     const colors: { [key: string]: string } = {
    24       COMPOSER: "bg-purple-100 text-purple-700",
    25       PERFORMER: "bg-blue-100 text-blue-700",
    26       PRODUCER: "bg-green-100 text-green-700",
    27       MAIN_VOCAL: "bg-pink-100 text-pink-700",
    28     };
    29     return colors[role] || "bg-gray-100 text-gray-700";
    30   };
     20        const getRoleColor = (role: string) => {
     21                const colors: { [key: string]: string } = {
     22                        COMPOSER: "bg-purple-500/20 text-purple-300",
     23                        PERFORMER: "bg-blue-500/20 text-blue-300",
     24                        PRODUCER: "bg-green-500/20 text-green-300",
     25                        MAIN_VOCAL: "bg-pink-500/20 text-pink-300",
     26                };
     27                return colors[role] || "bg-white/10 text-gray-300";
     28        };
    3129
    32   const showToast = (message: string) => {
    33     setToast({ message, show: true });
    34     setTimeout(() => {
    35       setToast({ message: "", show: false });
    36     }, 2000);
    37   };
     30        const handleLike = async (id: number, title: string) => {
     31                try {
     32                        const response = await axiosInstance.post(`/musical-entity/${id}/like`);
     33                        const data = response.data;
    3834
    39   const handleLike = async (id: number, title: string) => {
    40     try {
    41       const response = await axiosInstance.post(`/musical-entity/${id}/like`);
    42       const data = response.data;
     35                        setItems((prevItems) =>
     36                                prevItems.map((item) =>
     37                                        item.id === data.entityId
     38                                                ? { ...item, isLikedByCurrentUser: data.isLiked }
     39                                                : item,
     40                                ),
     41                        );
     42                        toast.success(
     43                                data.isLiked ? `Liked "${title}"` : `Removed "${title}" from likes`,
     44                        );
     45                } catch (err: any) {
     46                        toast.error(err.response?.data?.error || "Failed to like the item");
     47                }
     48        };
    4349
    44       setItems((prevItems) =>
    45         prevItems.map((item) =>
    46           item.id === data.entityId
    47             ? { ...item, isLikedByCurrentUser: data.isLiked }
    48             : item,
    49         ),
    50       );
     50        return (
     51                <div className="mt-8">
     52                        {albums.length > 0 && (
     53                                <div className="mb-12">
     54                                        <div className="flex items-center gap-3 mb-6">
     55                                                <Disc3 className="w-6 h-6 text-[#1db954]" />
     56                                                <h2 className="text-2xl font-bold text-white">Albums</h2>
     57                                        </div>
     58                                        <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
     59                                                {albums.map((album) => (
     60                                                        <div
     61                                                                key={album.id}
     62                                                                className="group cursor-pointer"
     63                                                                onClick={() => navigate(`/collection/album/${album.id}`)}
     64                                                        >
     65                                                                <div className="relative aspect-square rounded-lg mb-3 overflow-hidden shadow-md group-hover:shadow-xl transition-all duration-300 bg-[#181818]">
     66                                                                        <img
     67                                                                                src={album.cover || "/favicon.png"}
     68                                                                                alt={album.title}
     69                                                                                className="w-full h-full object-cover"
     70                                                                                onError={(e) => {
     71                                                                                        (e.target as HTMLImageElement).src = "/favicon.png";
     72                                                                                }}
     73                                                                        />
    5174
    52       showToast(
    53         data.isLiked ? `Liked "${title}"` : `Removed ${title} from likes`,
    54       );
    55     } catch (err: any) {
    56       showToast(err.response?.data?.error);
    57     }
    58   };
     75                                                                        <button
     76                                                                                className="absolute top-2 right-2 p-2 bg-black/70 hover:bg-black/90 rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100 cursor-pointer"
     77                                                                                title={album.isLikedByCurrentUser ? "Unlike" : "Like"}
     78                                                                                onClick={(e) => {
     79                                                                                        e.stopPropagation();
     80                                                                                        handleLike(album.id, album.title);
     81                                                                                }}
     82                                                                        >
     83                                                                                <svg
     84                                                                                        className="w-5 h-5"
     85                                                                                        fill={album.isLikedByCurrentUser ? "#ef4444" : "none"}
     86                                                                                        stroke={
     87                                                                                                album.isLikedByCurrentUser ? "#ef4444" : "#9ca3af"
     88                                                                                        }
     89                                                                                        strokeWidth="2"
     90                                                                                        viewBox="0 0 24 24"
     91                                                                                >
     92                                                                                        <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" />
     93                                                                                </svg>
     94                                                                        </button>
    5995
    60   return (
    61     <div className="mt-8">
    62       {toast.show && (
    63         <div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 animate-fade-in-up">
    64           <div className="bg-gray-900 text-white px-6 py-3 rounded-full shadow-lg text-sm font-medium">
    65             {toast.message}
    66           </div>
    67         </div>
    68       )}
     96                                                                        <div className="absolute bottom-0 left-0 right-0 bg-linear-to-t from-black/80 to-transparent p-3">
     97                                                                                <span
     98                                                                                        className={`text-xs px-2 py-1 rounded-full font-medium ${getRoleColor(album.role)}`}
     99                                                                                >
     100                                                                                        {album.role.replace("_", " ")}
     101                                                                                </span>
     102                                                                        </div>
     103                                                                </div>
     104                                                                <h3 className="font-semibold text-sm line-clamp-2 text-white group-hover:text-[#1db954] transition-colors">
     105                                                                        {album.title}
     106                                                                </h3>
     107                                                        </div>
     108                                                ))}
     109                                        </div>
     110                                </div>
     111                        )}
    69112
    70       {albums.length > 0 && (
    71         <div className="mb-12">
    72           <div className="flex items-center gap-3 mb-6">
    73             <Disc3 className="w-6 h-6 text-gray-700" />
    74             <h2 className="text-2xl font-bold text-gray-800">Albums</h2>
    75           </div>
    76           <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
    77             {albums.map((album) => (
    78               <div
    79                 key={album.id}
    80                 className="group cursor-pointer"
    81                 onClick={() => navigate(`/collection/album/${album.id}`)}
    82               >
    83                 <div className="relative aspect-square bg-gradient-to-br from-blue-400 to-purple-500 rounded-lg mb-3 overflow-hidden shadow-md group-hover:shadow-lg transition-all duration-300">
    84                   <div className="absolute inset-0 flex items-center justify-center">
    85                     <Disc3 className="w-16 h-16 text-white opacity-30" />
    86                   </div>
     113                        {songs.length > 0 && (
     114                                <div className="mb-12">
     115                                        <div className="flex items-center gap-3 mb-6">
     116                                                <Music className="w-6 h-6 text-[#1db954]" />
     117                                                <h2 className="text-2xl font-bold text-white">Songs</h2>
     118                                        </div>
     119                                        <div className="space-y-1">
     120                                                {songs.map((song) => (
     121                                                        <SongItem
     122                                                                key={song.id}
     123                                                                song={{
     124                                                                        id: song.id,
     125                                                                        title: song.title,
     126                                                                        cover: song.cover,
     127                                                                        genre: song.genre,
     128                                                                        link: song.link,
     129                                                                        isLikedByCurrentUser: song.isLikedByCurrentUser,
     130                                                                }}
     131                                                                role={song.role}
     132                                                                onLikeToggle={() => handleLike(song.id, song.title)}
     133                                                        />
     134                                                ))}
     135                                        </div>
     136                                </div>
     137                        )}
    87138
    88                   <button
    89                     className="absolute top-2 right-2 p-2 bg-white/90 hover:bg-white rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100 cursor-pointer"
    90                     title={album.isLikedByCurrentUser ? "Unlike" : "Like"}
    91                     onClick={(e) => {
    92                       e.stopPropagation();
    93                       handleLike(album.id, album.title);
    94                     }}
    95                   >
    96                     <svg
    97                       className="w-5 h-5"
    98                       fill={album.isLikedByCurrentUser ? "#ef4444" : "none"}
    99                       stroke={
    100                         album.isLikedByCurrentUser ? "#ef4444" : "#6b7280"
    101                       }
    102                       strokeWidth="2"
    103                       viewBox="0 0 24 24"
    104                     >
    105                       <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" />
    106                     </svg>
    107                   </button>
    108 
    109                   <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent p-3">
    110                     <span
    111                       className={`text-xs px-2 py-1 rounded-full font-medium ${getRoleColor(album.role)}`}
    112                     >
    113                       {album.role.replace("_", " ")}
    114                     </span>
    115                   </div>
    116                 </div>
    117                 <h3 className="font-semibold text-sm line-clamp-2 text-gray-900 group-hover:text-blue-600 transition-colors">
    118                   {album.title}
    119                 </h3>
    120               </div>
    121             ))}
    122           </div>
    123         </div>
    124       )}
    125 
    126       {songs.length > 0 && (
    127         <div className="mb-12">
    128           <div className="flex items-center gap-3 mb-6">
    129             <Music className="w-6 h-6 text-gray-700" />
    130             <h2 className="text-2xl font-bold text-gray-800">Songs</h2>
    131           </div>
    132           <div className="space-y-2">
    133             {songs.map((song) => (
    134               <div
    135                 key={song.id}
    136                 className="group relative flex items-center gap-4 p-3 rounded-lg hover:bg-gray-50 transition-all cursor-pointer"
    137                 onClick={() => navigate(`/musical-entity/${song.id}`)}
    138               >
    139                 <div className="relative shrink-0">
    140                   <div className="w-12 h-12 bg-gradient-to-br from-blue-400 to-purple-500 rounded flex items-center justify-center shadow-sm">
    141                     <Music className="w-6 h-6 text-white" />
    142                   </div>
    143                   <button
    144                     className="absolute inset-0 bg-black/60 rounded flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200"
    145                     aria-label="Play song"
    146                   >
    147                     <Play className="w-6 h-6 text-white fill-white" />
    148                   </button>
    149                 </div>
    150 
    151                 <div className="flex-1 min-w-0">
    152                   <h3 className="font-semibold text-base text-gray-900 group-hover:text-blue-600 transition-colors truncate">
    153                     {song.title}
    154                   </h3>
    155                   <div className="flex items-center gap-2 mt-1">
    156                     <span className="text-sm text-gray-600">{song.genre}</span>
    157                   </div>
    158                 </div>
    159 
    160                 <span
    161                   className={`px-3 py-1 rounded-full text-sm font-medium ${getRoleColor(song.role)}`}
    162                 >
    163                   {song.role.replace("_", " ")}
    164                 </span>
    165 
    166                 <div className="flex items-center gap-2">
    167                   <button
    168                     className="p-2 hover:bg-gray-200 rounded-full transition-colors duration-200 cursor-pointer"
    169                     title="Add to playlist"
    170                   >
    171                     <Plus className="w-5 h-5 text-gray-600" />
    172                   </button>
    173 
    174                   <button
    175                     className="p-2 hover:bg-gray-200 rounded-full transition-colors duration-200 cursor-pointer"
    176                     title={song.isLikedByCurrentUser ? "Unlike" : "Like"}
    177                     onClick={(e) => {
    178                       e.stopPropagation();
    179                       handleLike(song.id, song.title);
    180                     }}
    181                   >
    182                     <svg
    183                       className="w-5 h-5"
    184                       fill={song.isLikedByCurrentUser ? "#ef4444" : "none"}
    185                       stroke={song.isLikedByCurrentUser ? "#ef4444" : "#6b7280"}
    186                       strokeWidth="2"
    187                       viewBox="0 0 24 24"
    188                     >
    189                       <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" />
    190                     </svg>
    191                   </button>
    192                 </div>
    193               </div>
    194             ))}
    195           </div>
    196         </div>
    197       )}
    198 
    199       {contributions.length === 0 && (
    200         <div className="flex flex-col items-center justify-center py-16 text-gray-400">
    201           <Music className="w-20 h-20 mb-4 opacity-20" />
    202           <p className="text-lg font-medium">No contributions yet</p>
    203           <p className="text-sm mt-2">Start creating music to see it here</p>
    204         </div>
    205       )}
    206     </div>
    207   );
     139                        {contributions.length === 0 && (
     140                                <div className="flex flex-col items-center justify-center py-16 text-gray-500">
     141                                        <Music className="w-20 h-20 mb-4 opacity-20" />
     142                                        <p className="text-lg font-medium">No contributions yet</p>
     143                                        <p className="text-sm mt-2">Start creating music to see it here</p>
     144                                </div>
     145                        )}
     146                </div>
     147        );
    208148};
    209149
  • frontend/src/components/userProfile/ListenerView.tsx

    r0808ef2 r1579b4f  
     1import { Album, Bookmark, Heart, ListMusic, Music } from "lucide-react";
     2import { useState } from "react";
    13import { useNavigate } from "react-router-dom";
    2 import { Heart, ListMusic, Music, Album, Bookmark } from "lucide-react";
    3 import type { Playlist, MusicalEntity } from "../../utils/types";
     4import { toast } from "react-toastify";
    45import axiosInstance from "../../api/axiosInstance";
    5 import { useState } from "react";
     6import type { MusicalEntity, Playlist } from "../../utils/types";
     7import SongItem from "../SongItem";
    68
    79interface ListenerViewProps {
    8   likedEntities: MusicalEntity[] | [];
    9   createdPlaylists: Playlist[] | [];
    10   savedPlaylists: Playlist[] | [];
     10        likedEntities: MusicalEntity[] | [];
     11        createdPlaylists: Playlist[] | [];
     12        savedPlaylists: Playlist[] | [];
    1113}
    1214
    1315const ListenerView = ({
    14   likedEntities,
    15   createdPlaylists,
    16   savedPlaylists,
     16        likedEntities,
     17        createdPlaylists,
     18        savedPlaylists,
    1719}: ListenerViewProps) => {
    18   const navigate = useNavigate();
    19   const [items, setItems] = useState(likedEntities);
    20   const [savedItems, setSavedItems] = useState(savedPlaylists);
    21   const [toast, setToast] = useState<{ message: string; show: boolean }>({
    22     message: "",
    23     show: false,
    24   });
    25 
    26   const showToast = (message: string) => {
    27     setToast({ message, show: true });
    28     setTimeout(() => {
    29       setToast({ message: "", show: false });
    30     }, 2000);
    31   };
    32 
    33   const likedSongs = items.filter((e) => e.type === "SONG");
    34   const likedAlbums = items.filter((e) => e.type === "ALBUM");
    35 
    36   const handleSavePlaylist = async (
    37     e: React.MouseEvent,
    38     playlistId: number,
    39     playlistName: string,
    40   ) => {
    41     e.stopPropagation();
    42 
    43     try {
    44       const response = await axiosInstance.post(`/playlist/${playlistId}/save`);
    45       const data = response.data;
    46 
    47       setSavedItems((prevItems) =>
    48         data.isSaved
    49           ? [...prevItems, prevItems.find((p) => p.id === playlistId)!]
    50           : prevItems.filter((p) => p.id !== playlistId),
    51       );
    52 
    53       showToast(
    54         data.isSaved
    55           ? `Saved "${playlistName}"`
    56           : `Removed "${playlistName}" from saved playlists`,
    57       );
    58     } catch (err: any) {
    59       showToast(err.response?.data?.error || "Failed to save playlist");
    60     }
    61   };
    62 
    63   const handleLike = async (id: number, title: string) => {
    64     try {
    65       const response = await axiosInstance.post(`/musical-entity/${id}/like`);
    66       const data = response.data;
    67 
    68       setItems((prevItems) =>
    69         prevItems.map((item) =>
    70           item.id === data.entityId
    71             ? { ...item, isLikedByCurrentUser: data.isLiked }
    72             : item,
    73         ),
    74       );
    75 
    76       showToast(
    77         data.isLiked ? `Liked "${title}"` : `Removed "${title}" from likes`,
    78       );
    79     } catch (err: any) {
    80       showToast(err.response?.data?.error);
    81     }
    82   };
    83 
    84   return (
    85     <div className="mt-8 space-y-12">
    86       {toast.show && (
    87         <div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 animate-fade-in-up">
    88           <div className="bg-gray-900 text-white px-6 py-3 rounded-full shadow-lg text-sm font-medium">
    89             {toast.message}
    90           </div>
    91         </div>
    92       )}
    93 
    94       {createdPlaylists && createdPlaylists.length > 0 && (
    95         <section>
    96           <div className="flex items-center gap-3 mb-6">
    97             <ListMusic className="w-6 h-6 text-gray-700" />
    98             <h3 className="text-2xl font-bold text-gray-800">
    99               Created Playlists
    100             </h3>
    101             <span className="text-sm text-gray-500">
    102               ({createdPlaylists.length})
    103             </span>
    104           </div>
    105 
    106           <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
    107             {createdPlaylists.map((playlist) => (
    108               <div
    109                 key={playlist.id}
    110                 className="group cursor-pointer"
    111                 onClick={() => navigate(`/collection/playlist/${playlist.id}`)}
    112               >
    113                 <div className="relative aspect-square rounded-lg overflow-hidden bg-gray-100 mb-3 shadow-md group-hover:shadow-lg transition-all">
    114                   {playlist.cover ? (
    115                     <img
    116                       src={playlist.cover}
    117                       alt={playlist.name}
    118                       className="w-full h-full object-cover"
    119                     />
    120                   ) : (
    121                     <div className="w-full h-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center">
    122                       <ListMusic className="w-16 h-16 text-white opacity-30" />
    123                     </div>
    124                   )}
    125 
    126                   <button
    127                     onClick={(e) =>
    128                       handleSavePlaylist(e, playlist.id, playlist.name)
    129                     }
    130                     className="absolute top-2 right-2 p-2 bg-white/90 hover:bg-white rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100"
    131                     title={playlist.isSavedByCurrentUser ? "Unsave" : "Save"}
    132                   >
    133                     <Bookmark
    134                       className={`w-5 h-5 ${
    135                         playlist.isSavedByCurrentUser
    136                           ? "fill-blue-600 text-blue-600"
    137                           : "text-gray-600"
    138                       }`}
    139                     />
    140                   </button>
    141                 </div>
    142                 <p className="text-sm font-semibold text-gray-900 truncate group-hover:text-blue-600 transition-colors">
    143                   {playlist.name}
    144                 </p>
    145                 <p className="text-xs text-gray-500 truncate">
    146                   {playlist.creatorName}
    147                 </p>
    148               </div>
    149             ))}
    150           </div>
    151         </section>
    152       )}
    153 
    154       {savedItems && savedItems.length > 0 && (
    155         <section>
    156           <div className="flex items-center gap-3 mb-6">
    157             <Bookmark className="w-6 h-6 text-blue-600 fill-blue-600" />
    158             <h3 className="text-2xl font-bold text-gray-800">
    159               Saved Playlists
    160             </h3>
    161             <span className="text-sm text-gray-500">({savedItems.length})</span>
    162           </div>
    163 
    164           <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
    165             {savedItems.map((playlist) => (
    166               <div
    167                 key={playlist.id}
    168                 className="group cursor-pointer"
    169                 onClick={() => navigate(`/collection/playlist/${playlist.id}`)}
    170               >
    171                 <div className="relative aspect-square rounded-lg overflow-hidden bg-gray-100 mb-3 shadow-md group-hover:shadow-lg transition-all">
    172                   {playlist.cover ? (
    173                     <img
    174                       src={playlist.cover}
    175                       alt={playlist.name}
    176                       className="w-full h-full object-cover"
    177                     />
    178                   ) : (
    179                     <div className="w-full h-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center">
    180                       <ListMusic className="w-16 h-16 text-white opacity-30" />
    181                     </div>
    182                   )}
    183 
    184                   <button
    185                     onClick={(e) =>
    186                       handleSavePlaylist(e, playlist.id, playlist.name)
    187                     }
    188                     className="absolute top-2 right-2 p-2 bg-white/90 hover:bg-white rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100"
    189                     title="Unsave"
    190                   >
    191                     <Bookmark className="w-5 h-5 fill-blue-600 text-blue-600" />
    192                   </button>
    193                 </div>
    194                 <p className="text-sm font-semibold text-gray-900 truncate group-hover:text-blue-600 transition-colors">
    195                   {playlist.name}
    196                 </p>
    197                 <p className="text-xs text-gray-500 truncate">
    198                   {playlist.creatorName}
    199                 </p>
    200               </div>
    201             ))}
    202           </div>
    203         </section>
    204       )}
    205 
    206       {likedSongs && likedSongs.length > 0 && (
    207         <section>
    208           <div className="flex items-center gap-3 mb-6">
    209             <Heart className="w-6 h-6 text-red-500 fill-red-500" />
    210             <h3 className="text-2xl font-bold text-gray-800">Liked Songs</h3>
    211             <span className="text-sm text-gray-500">
    212               ({likedSongs?.length})
    213             </span>
    214           </div>
    215 
    216           <div className="space-y-2">
    217             {likedSongs.map((song, index) => (
    218               <div
    219                 key={song.id}
    220                 className="flex items-center gap-4 p-3 rounded-lg hover:bg-gray-50 cursor-pointer group transition-colors"
    221                 onClick={() => navigate(`/musical-entity/${song.id}`)}
    222               >
    223                 <span className="text-gray-500 font-medium w-8 text-center">
    224                   {index + 1}
    225                 </span>
    226                 <div className="w-12 h-12 rounded bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center flex-shrink-0 shadow-sm">
    227                   <Music className="w-6 h-6 text-white" />
    228                 </div>
    229                 <div className="flex-1 min-w-0">
    230                   <p className="font-semibold text-gray-900 truncate group-hover:text-blue-600 transition-colors">
    231                     {song.title}
    232                   </p>
    233                   <p className="text-sm text-gray-600 truncate">
    234                     {song.releasedBy}
    235                   </p>
    236                 </div>
    237                 <span className="text-sm text-gray-600 px-3 py-1 bg-gray-100 rounded-full">
    238                   {song.genre}
    239                 </span>
    240 
    241                 <button
    242                   onClick={(e) => {
    243                     e.stopPropagation();
    244                     handleLike(song.id, song.title);
    245                   }}
    246                   className="p-2 hover:bg-gray-200 rounded-full transition-colors duration-200 cursor-pointer"
    247                   title={song.isLikedByCurrentUser ? "Unlike" : "Like"}
    248                 >
    249                   <svg
    250                     className="w-5 h-5"
    251                     fill={song.isLikedByCurrentUser ? "#ef4444" : "none"}
    252                     stroke={song.isLikedByCurrentUser ? "#ef4444" : "#6b7280"}
    253                     strokeWidth="2"
    254                     viewBox="0 0 24 24"
    255                   >
    256                     <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" />
    257                   </svg>
    258                 </button>
    259               </div>
    260             ))}
    261           </div>
    262         </section>
    263       )}
    264 
    265       {likedAlbums && likedAlbums.length > 0 && (
    266         <section>
    267           <div className="flex items-center gap-3 mb-6">
    268             <Album className="w-6 h-6 text-gray-700" />
    269             <h3 className="text-2xl font-bold text-gray-800">Liked Albums</h3>
    270             <span className="text-sm text-gray-500">
    271               ({likedAlbums.length})
    272             </span>
    273           </div>
    274 
    275           <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
    276             {likedAlbums.map((album) => (
    277               <div
    278                 key={album.id}
    279                 className="group cursor-pointer"
    280                 onClick={() => navigate(`/collection/album/${album.id}`)}
    281               >
    282                 <div className="relative aspect-square rounded-lg overflow-hidden bg-gradient-to-br from-blue-400 to-purple-500 mb-3 flex items-center justify-center shadow-md group-hover:shadow-lg transition-all">
    283                   <Album className="w-16 h-16 text-white opacity-30" />
    284 
    285                   <button
    286                     onClick={(e) => {
    287                       e.stopPropagation();
    288                       handleLike(album.id, album.title);
    289                     }}
    290                     className="absolute top-2 right-2 p-2 bg-white/90 hover:bg-white rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100 cursor-pointer"
    291                     title={album.isLikedByCurrentUser ? "Unlike" : "Like"}
    292                   >
    293                     <svg
    294                       className="w-5 h-5"
    295                       fill={album.isLikedByCurrentUser ? "#ef4444" : "none"}
    296                       stroke={
    297                         album.isLikedByCurrentUser ? "#ef4444" : "#6b7280"
    298                       }
    299                       strokeWidth="2"
    300                       viewBox="0 0 24 24"
    301                     >
    302                       <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" />
    303                     </svg>
    304                   </button>
    305                 </div>
    306                 <p className="font-semibold text-sm text-gray-900 truncate group-hover:text-blue-600 transition-colors">
    307                   {album.title}
    308                 </p>
    309                 <p className="text-xs text-gray-600 truncate mt-1">
    310                   {album.releasedBy}
    311                 </p>
    312               </div>
    313             ))}
    314           </div>
    315         </section>
    316       )}
    317 
    318       {likedEntities &&
    319         likedEntities.length === 0 &&
    320         (!createdPlaylists || createdPlaylists.length === 0) &&
    321         (!savedItems || savedItems.length === 0) && (
    322           <div className="flex flex-col items-center justify-center py-16 text-gray-400">
    323             <Music className="w-20 h-20 mb-4 opacity-20" />
    324             <p className="text-lg font-medium">Nothing here yet</p>
    325             <p className="text-sm mt-2">
    326               Start exploring music to build your collection
    327             </p>
    328           </div>
    329         )}
    330     </div>
    331   );
     20        const navigate = useNavigate();
     21        const [items, setItems] = useState(likedEntities);
     22        const [savedItems, setSavedItems] = useState(savedPlaylists);
     23
     24        const likedSongs = items.filter((e) => e.type === "SONG");
     25        const likedAlbums = items.filter((e) => e.type === "ALBUM");
     26
     27        const handleSavePlaylist = async (
     28                e: React.MouseEvent,
     29                playlistId: number,
     30                playlistName: string,
     31        ) => {
     32                e.stopPropagation();
     33
     34                try {
     35                        const response = await axiosInstance.post(`/playlist/${playlistId}/save`);
     36                        const data = response.data;
     37
     38                        setSavedItems((prevItems) =>
     39                                data.isSaved
     40                                        ? [...prevItems, prevItems.find((p) => p.id === playlistId)!]
     41                                        : prevItems.filter((p) => p.id !== playlistId),
     42                        );
     43
     44                        toast.success(
     45                                data.isSaved
     46                                        ? `Saved "${playlistName}" to your library`
     47                                        : `Removed "${playlistName}" from your library`,
     48                        );
     49                } catch (err: any) {
     50                        toast.error(err.response?.data?.error || "Failed to save the playlist");
     51                }
     52        };
     53
     54        const handleLike = async (id: number, title: string) => {
     55                try {
     56                        const response = await axiosInstance.post(`/musical-entity/${id}/like`);
     57                        const data = response.data;
     58
     59                        setItems((prevItems) =>
     60                                prevItems.map((item) =>
     61                                        item.id === data.entityId
     62                                                ? { ...item, isLikedByCurrentUser: data.isLiked }
     63                                                : item,
     64                                ),
     65                        );
     66
     67                        toast.success(
     68                                data.isLiked ? `Liked "${title}"` : `Removed "${title}" from likes`,
     69                        );
     70                } catch (err: any) {
     71                        toast.error(err.response?.data?.error || "Failed to like the item");
     72                }
     73        };
     74
     75        return (
     76                <div className="mt-8 space-y-12">
     77                        {createdPlaylists && createdPlaylists.length > 0 && (
     78                                <section>
     79                                        <div className="flex items-center gap-3 mb-6">
     80                                                <ListMusic className="w-6 h-6 text-[#1db954]" />
     81                                                <h3 className="text-2xl font-bold text-white">Created Playlists</h3>
     82                                                <span className="text-sm text-gray-500">
     83                                                        ({createdPlaylists.length})
     84                                                </span>
     85                                        </div>
     86
     87                                        <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
     88                                                {createdPlaylists.map((playlist) => (
     89                                                        <div
     90                                                                key={playlist.id}
     91                                                                className="group cursor-pointer"
     92                                                                onClick={() => navigate(`/collection/playlist/${playlist.id}`)}
     93                                                        >
     94                                                                <div className="relative aspect-square rounded-lg overflow-hidden bg-[#181818] mb-3 shadow-md group-hover:shadow-xl transition-all">
     95                                                                        <img
     96                                                                                src={playlist.cover || "/favicon.png"}
     97                                                                                alt={playlist.name}
     98                                                                                className="w-full h-full object-cover"
     99                                                                                onError={(e) => {
     100                                                                                        (e.target as HTMLImageElement).src = "/favicon.png";
     101                                                                                }}
     102                                                                        />
     103
     104                                                                        <button
     105                                                                                onClick={(e) =>
     106                                                                                        handleSavePlaylist(e, playlist.id, playlist.name)
     107                                                                                }
     108                                                                                className="absolute top-2 right-2 p-2 bg-black/70 hover:bg-black/90 rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100"
     109                                                                                title={playlist.isSavedByCurrentUser ? "Unsave" : "Save"}
     110                                                                        >
     111                                                                                <Bookmark
     112                                                                                        className={`w-5 h-5 ${
     113                                                                                                playlist.isSavedByCurrentUser
     114                                                                                                        ? "fill-[#1db954] text-[#1db954]"
     115                                                                                                        : "text-gray-400"
     116                                                                                        }`}
     117                                                                                />
     118                                                                        </button>
     119                                                                </div>
     120                                                                <p className="text-sm font-semibold text-white truncate group-hover:text-[#1db954] transition-colors">
     121                                                                        {playlist.name}
     122                                                                </p>
     123                                                                <p className="text-xs text-gray-400 truncate">
     124                                                                        {playlist.creatorName}
     125                                                                </p>
     126                                                        </div>
     127                                                ))}
     128                                        </div>
     129                                </section>
     130                        )}
     131
     132                        {savedItems && savedItems.length > 0 && (
     133                                <section>
     134                                        <div className="flex items-center gap-3 mb-6">
     135                                                <Bookmark className="w-6 h-6 text-[#1db954] fill-[#1db954]" />
     136                                                <h3 className="text-2xl font-bold text-white">Saved Playlists</h3>
     137                                                <span className="text-sm text-gray-500">({savedItems.length})</span>
     138                                        </div>
     139
     140                                        <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
     141                                                {savedItems.map((playlist) => (
     142                                                        <div
     143                                                                key={playlist.id}
     144                                                                className="group cursor-pointer"
     145                                                                onClick={() => navigate(`/collection/playlist/${playlist.id}`)}
     146                                                        >
     147                                                                <div className="relative aspect-square rounded-lg overflow-hidden bg-[#181818] mb-3 shadow-md group-hover:shadow-xl transition-all">
     148                                                                        <img
     149                                                                                src={playlist.cover || "/favicon.png"}
     150                                                                                alt={playlist.name}
     151                                                                                className="w-full h-full object-cover"
     152                                                                                onError={(e) => {
     153                                                                                        (e.target as HTMLImageElement).src = "/favicon.png";
     154                                                                                }}
     155                                                                        />
     156
     157                                                                        <button
     158                                                                                onClick={(e) =>
     159                                                                                        handleSavePlaylist(e, playlist.id, playlist.name)
     160                                                                                }
     161                                                                                className="absolute top-2 right-2 p-2 bg-black/70 hover:bg-black/90 rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100"
     162                                                                                title="Unsave"
     163                                                                        >
     164                                                                                <Bookmark className="w-5 h-5 fill-[#1db954] text-[#1db954]" />
     165                                                                        </button>
     166                                                                </div>
     167                                                                <p className="text-sm font-semibold text-white truncate group-hover:text-[#1db954] transition-colors">
     168                                                                        {playlist.name}
     169                                                                </p>
     170                                                                <p className="text-xs text-gray-400 truncate">
     171                                                                        {playlist.creatorName}
     172                                                                </p>
     173                                                        </div>
     174                                                ))}
     175                                        </div>
     176                                </section>
     177                        )}
     178
     179                        {likedSongs && likedSongs.length > 0 && (
     180                                <section>
     181                                        <div className="flex items-center gap-3 mb-6">
     182                                                <Heart className="w-6 h-6 text-red-500 fill-red-500" />
     183                                                <h3 className="text-2xl font-bold text-white">Liked Songs</h3>
     184                                                <span className="text-sm text-gray-500">
     185                                                        ({likedSongs?.length})
     186                                                </span>
     187                                        </div>
     188
     189                                        <div className="space-y-1">
     190                                                {likedSongs.map((song, index) => (
     191                                                        <SongItem
     192                                                                key={song.id}
     193                                                                song={{
     194                                                                        id: song.id,
     195                                                                        title: song.title,
     196                                                                        cover: song.cover,
     197                                                                        genre: song.genre,
     198                                                                        link: (song as any).link,
     199                                                                        releasedBy: song.releasedBy,
     200                                                                        isLikedByCurrentUser: song.isLikedByCurrentUser,
     201                                                                }}
     202                                                                index={index + 1}
     203                                                                onLikeToggle={() => handleLike(song.id, song.title)}
     204                                                        />
     205                                                ))}
     206                                        </div>
     207                                </section>
     208                        )}
     209
     210                        {likedAlbums && likedAlbums.length > 0 && (
     211                                <section>
     212                                        <div className="flex items-center gap-3 mb-6">
     213                                                <Album className="w-6 h-6 text-[#1db954]" />
     214                                                <h3 className="text-2xl font-bold text-white">Liked Albums</h3>
     215                                                <span className="text-sm text-gray-500">
     216                                                        ({likedAlbums.length})
     217                                                </span>
     218                                        </div>
     219
     220                                        <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
     221                                                {likedAlbums.map((album) => (
     222                                                        <div
     223                                                                key={album.id}
     224                                                                className="group cursor-pointer"
     225                                                                onClick={() => navigate(`/collection/album/${album.id}`)}
     226                                                        >
     227                                                                <div className="relative aspect-square rounded-lg overflow-hidden bg-[#181818] mb-3 shadow-md group-hover:shadow-xl transition-all">
     228                                                                        <img
     229                                                                                src={album.cover || "/favicon.png"}
     230                                                                                alt={album.title}
     231                                                                                className="w-full h-full object-cover"
     232                                                                                onError={(e) => {
     233                                                                                        (e.target as HTMLImageElement).src = "/favicon.png";
     234                                                                                }}
     235                                                                        />
     236
     237                                                                        <button
     238                                                                                onClick={(e) => {
     239                                                                                        e.stopPropagation();
     240                                                                                        handleLike(album.id, album.title);
     241                                                                                }}
     242                                                                                className="absolute top-2 right-2 p-2 bg-black/70 hover:bg-black/90 rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100 cursor-pointer"
     243                                                                                title={album.isLikedByCurrentUser ? "Unlike" : "Like"}
     244                                                                        >
     245                                                                                <svg
     246                                                                                        className="w-5 h-5"
     247                                                                                        fill={album.isLikedByCurrentUser ? "#ef4444" : "none"}
     248                                                                                        stroke={
     249                                                                                                album.isLikedByCurrentUser ? "#ef4444" : "#9ca3af"
     250                                                                                        }
     251                                                                                        strokeWidth="2"
     252                                                                                        viewBox="0 0 24 24"
     253                                                                                >
     254                                                                                        <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" />
     255                                                                                </svg>
     256                                                                        </button>
     257                                                                </div>
     258                                                                <p className="font-semibold text-sm text-white truncate group-hover:text-[#1db954] transition-colors">
     259                                                                        {album.title}
     260                                                                </p>
     261                                                                <p className="text-xs text-gray-400 truncate mt-1">
     262                                                                        {album.releasedBy}
     263                                                                </p>
     264                                                        </div>
     265                                                ))}
     266                                        </div>
     267                                </section>
     268                        )}
     269
     270                        {likedEntities &&
     271                                likedEntities.length === 0 &&
     272                                (!createdPlaylists || createdPlaylists.length === 0) &&
     273                                (!savedItems || savedItems.length === 0) && (
     274                                        <div className="flex flex-col items-center justify-center py-16 text-gray-500">
     275                                                <Music className="w-20 h-20 mb-4 opacity-20" />
     276                                                <p className="text-lg font-medium">Nothing here yet</p>
     277                                                <p className="text-sm mt-2">
     278                                                        Start exploring music to build your collection
     279                                                </p>
     280                                        </div>
     281                                )}
     282                </div>
     283        );
    332284};
    333285
  • frontend/src/components/userProfile/UserListModal.tsx

    r0808ef2 r1579b4f  
    11import { useNavigate } from "react-router-dom";
     2import { baseURL } from "../../api/axiosInstance";
    23import type { BaseNonAdminUser } from "../../utils/types";
    34
     
    1617}: ModalProps) => {
    1718        const navigate = useNavigate();
    18         const baseURL = import.meta.env.VITE_API_BASE_URL;
    1919
    2020        return (
    21                 <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
    22                         <div className="bg-white rounded-xl shadow-2xl w-full max-w-md max-h-[70vh] flex flex-col">
    23                                 <div className="p-4 border-b flex justify-between items-center">
    24                                         <h2 className="text-xl font-bold">{title}</h2>
     21                <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
     22                        <div className="bg-[#1a1a2e] border border-white/10 rounded-xl shadow-2xl w-full max-w-md max-h-[70vh] flex flex-col">
     23                                <div className="p-4 border-b border-white/10 flex justify-between items-center">
     24                                        <h2 className="text-xl font-bold text-white">{title}</h2>
    2525                                        <button
    2626                                                onClick={onClose}
    27                                                 className="text-gray-500 hover:text-black text-2xl cursor-pointer"
     27                                                className="text-gray-400 hover:text-white text-2xl cursor-pointer transition-colors"
    2828                                        >
    2929                                                &times;
     
    3838                                                        <div
    3939                                                                key={u.username}
    40                                                                 className="flex items-center justify-between p-3 hover:bg-gray-50 rounded-lg transition-colors"
     40                                                                className="flex items-center justify-between p-3 hover:bg-white/5 rounded-lg transition-colors"
    4141                                                        >
    4242                                                                <div
     
    4747                                                                        }}
    4848                                                                >
    49                                                                         <div className="w-10 h-10 rounded-full bg-blue-100 overflow-hidden shrink-0 flex items-center justify-center">
     49                                                                        <div className="w-10 h-10 rounded-full bg-[#282828] overflow-hidden shrink-0 flex items-center justify-center">
    5050                                                                                {u.profilePhoto ? (
    5151                                                                                        <img
     
    5555                                                                                        />
    5656                                                                                ) : (
    57                                                                                         <span className="text-blue-600 font-bold">
     57                                                                                        <span className="text-[#1db954] font-bold">
    5858                                                                                                {u.fullName.charAt(0)}
    5959                                                                                        </span>
    6060                                                                                )}
    6161                                                                        </div>
    62                                                                         <p className="font-semibold text-gray-900">{u.fullName}</p>
     62                                                                        <div>
     63                                                                                <p className="font-semibold text-white">{u.fullName}</p>
     64                                                                                <p className="text-sm text-gray-400">@{u.username}</p>
     65                                                                        </div>
    6366                                                                </div>
    6467
    6568                                                                <button
    6669                                                                        onClick={() => onFollowToggle(u.username)}
    67                                                                         className={`px-4 py-1 text-sm font-medium rounded-md transition-colors cursor-pointer ${
     70                                                                        className={`px-4 py-1.5 text-sm font-medium rounded-full transition-colors cursor-pointer ${
    6871                                                                                u.isFollowedByCurrentUser
    69                                                                                         ? "bg-gray-200 text-gray-700 hover:bg-gray-300"
    70                                                                                         : "bg-blue-500 text-white hover:bg-blue-600"
     72                                                                                        ? "bg-white/10 text-white hover:bg-white/20"
     73                                                                                        : "bg-[#1db954] text-black hover:bg-[#1ed760]"
    7174                                                                        }`}
    7275                                                                >
    73                                                                         {u.isFollowedByCurrentUser ? "Unfollow" : "Follow"}
     76                                                                        {u.isFollowedByCurrentUser ? "Following" : "Follow"}
    7477                                                                </button>
    7578                                                        </div>
Note: See TracChangeset for help on using the changeset viewer.