Changeset 85512ff for frontend/src/pages


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

save playlist

Location:
frontend/src/pages
Files:
2 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
  • frontend/src/pages/UserDetail.tsx

    rf5bc95e r85512ff  
    77import UserListModal from "../components/userProfile/UserListModal";
    88import { useAuth } from "../context/authContext";
    9 import { handleError } from "../utils/error";
     9import { getErrorMessage } from "../utils/error";
    1010import type {
    11         ArtistContribution,
    12         BaseNonAdminUser,
    13         MusicalEntity,
    14         Playlist,
     11  ArtistContribution,
     12  BaseNonAdminUser,
     13  MusicalEntity,
     14  Playlist,
    1515} from "../utils/types";
     16import { useCreatedPlaylists } from "../context/playlistContext";
    1617
    1718interface FollowStatus {
    18         isFollowing: boolean;
    19         followerCount: number;
    20         followingCount: number;
     19  isFollowing: boolean;
     20  followerCount: number;
     21  followingCount: number;
    2122}
    2223
    2324interface Artist extends BaseNonAdminUser {
    24         userType: "ARTIST";
    25         contributions: ArtistContribution[];
     25  userType: "ARTIST";
     26  contributions: ArtistContribution[];
    2627}
    2728interface Listener extends BaseNonAdminUser {
    28         userType: "LISTENER";
    29         likedEntities: MusicalEntity[];
    30         createdPlaylists: Playlist[];
    31         savedPlaylists: Playlist[];
     29  userType: "LISTENER";
     30  likedEntities: MusicalEntity[];
     31  createdPlaylists: Playlist[];
     32  savedPlaylists: Playlist[];
    3233}
    3334
     
    3536
    3637const UserDetail = () => {
    37         const { username: usernameParam } = useParams();
    38         const { user: currentUser } = useAuth();
    39         const navigate = useNavigate();
    40         const [user, setUser] = useState<UserProfile | null>(null);
    41         const [error, setError] = useState<string | null>(null);
    42         const [showModal, setShowModal] = useState(false);
    43         const [modalTitle, setModalTitle] = useState("");
    44         const [modalUsers, setModalUsers] = useState<any[]>([]);
    45         const [isLoadingModal, setIsLoadingModal] = useState(false);
    46         const [isFollowing, setIsFollowing] = useState(false);
    47 
    48         const username = usernameParam || currentUser?.username;
    49         const isOwnProfile = currentUser?.username === username;
    50 
    51         if (!usernameParam && !currentUser) {
    52                 return (
    53                         <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
    54                                 <div className="text-center">
    55                                         <p className="text-red-400 text-xl mb-4">
    56                                                 You must be logged in to view your profile.
    57                                         </p>
    58                                         <button
    59                                                 onClick={() => navigate("/login")}
    60                                                 className="text-[#1db954] hover:underline text-sm cursor-pointer"
    61                                         >
    62                                                 Go to Login
    63                                         </button>
    64                                 </div>
    65                         </div>
    66                 );
    67         }
    68 
    69         const handleFollow = async () => {
    70                 if (!user) return;
    71 
    72                 setIsFollowing(true);
    73                 try {
    74                         const response = await axiosInstance.post<FollowStatus>(
    75                                 `/users/na/${username}/follow`,
    76                         );
    77                         setUser((prev) => {
    78                                 if (!prev) return null;
    79                                 return {
    80                                         ...prev,
    81                                         isFollowedByCurrentUser: response.data.isFollowing,
    82                                         followers: response.data.followerCount,
    83                                         following: response.data.followingCount,
    84                                 };
    85                         });
    86                 } catch (err: any) {
    87                         setError(handleError(err));
    88                 } finally {
    89                         setIsFollowing(false);
    90                 }
    91         };
    92 
    93         const handleFollowInModal = async (targetUsername: string) => {
    94                 try {
    95                         const response = await axiosInstance.post<FollowStatus>(
    96                                 `/users/na/${targetUsername}/follow`,
    97                         );
    98 
    99                         setModalUsers((prevUsers) =>
    100                                 prevUsers.map((u) =>
    101                                         u.username === targetUsername
    102                                                 ? { ...u, isFollowedByCurrentUser: response.data.isFollowing }
    103                                                 : u,
    104                                 ),
    105                         );
    106                 } catch (err: any) {
    107                         setError(handleError(err));
    108                 }
    109         };
    110 
    111         const displayFollowers = async () => {
    112                 setIsLoadingModal(true);
    113                 try {
    114                         const response = await axiosInstance.get(
    115                                 `/users/na/${username}/followers`,
    116                         );
    117                         setModalUsers(response.data);
    118                         setModalTitle("Followers");
    119                         setShowModal(true);
    120                 } catch (err) {
    121                         setError(handleError(err));
    122                 } finally {
    123                         setIsLoadingModal(false);
    124                 }
    125         };
    126         const displayFollowing = async () => {
    127                 setIsLoadingModal(true);
    128                 try {
    129                         const response = await axiosInstance.get(
    130                                 `/users/na/${username}/following`,
    131                         );
    132                         setModalUsers(response.data);
    133                         setModalTitle("Following");
    134                         setShowModal(true);
    135                 } catch (err: any) {
    136                         setError(handleError(err));
    137                 } finally {
    138                         setIsLoadingModal(false);
    139                 }
    140         };
    141 
    142         useEffect(() => {
    143                 const fetchUser = async () => {
    144                         setError(null);
    145                         try {
    146                                 const response = await axiosInstance.get(`/users/na/${username}`);
    147                                 setUser(response.data);
    148                         } catch (err: any) {
    149                                 setError(handleError(err));
    150                         }
    151                 };
    152                 fetchUser();
    153         }, [username]);
    154 
    155         if (error) {
    156                 return (
    157                         <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
    158                                 <div className="text-center">
    159                                         <p className="text-red-400 text-xl mb-4">{error}</p>
    160                                         <Link to="/" className="text-[#1db954] hover:underline text-sm">
    161                                                 ← Back to Home
    162                                         </Link>
    163                                 </div>
    164                         </div>
    165                 );
    166         }
    167 
    168         if (!user) {
    169                 return <LoadingSpinner />;
    170         }
    171 
    172         return (
    173                 <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white">
    174                         {isLoadingModal && (
    175                                 <div className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm flex items-center justify-center">
    176                                         <div className="w-10 h-10 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin" />
    177                                 </div>
    178                         )}
    179 
    180                         <div className="max-w-5xl mx-auto px-6 py-10">
    181                                 {/* Back link */}
    182                                 <Link
    183                                         to="/"
    184                                         className="inline-flex items-center gap-1 text-gray-400 hover:text-white text-sm mb-8 transition-colors"
    185                                 >
    186                                         <svg
    187                                                 className="w-4 h-4"
    188                                                 fill="none"
    189                                                 stroke="currentColor"
    190                                                 viewBox="0 0 24 24"
    191                                         >
    192                                                 <path
    193                                                         strokeLinecap="round"
    194                                                         strokeLinejoin="round"
    195                                                         strokeWidth={2}
    196                                                         d="M15 19l-7-7 7-7"
    197                                                 />
    198                                         </svg>
    199                                         Back to Home
    200                                 </Link>
    201 
    202                                 {/* Hero section */}
    203                                 <div className="flex flex-col md:flex-row gap-8 mb-10">
    204                                         {/* Profile photo */}
    205                                         <div className="w-full md:w-48 shrink-0">
    206                                                 <div className="relative w-48 h-48 rounded-full overflow-hidden shadow-2xl bg-[#181818] mx-auto md:mx-0">
    207                                                         {user.profilePhoto ? (
    208                                                                 <img
    209                                                                         src={`${baseURL}/${user.profilePhoto}`}
    210                                                                         alt={user.fullName}
    211                                                                         className="w-full h-full object-cover"
    212                                                                 />
    213                                                         ) : (
    214                                                                 <div className="w-full h-full bg-linear-to-br from-[#1db954] to-[#1ed760] flex items-center justify-center text-white text-5xl font-bold">
    215                                                                         {user.fullName.charAt(0).toUpperCase()}
    216                                                                 </div>
    217                                                         )}
    218                                                 </div>
    219                                         </div>
    220 
    221                                         {/* User info */}
    222                                         <div className="flex flex-col justify-end gap-3 min-w-0">
    223                                                 <span className="text-xs uppercase tracking-widest text-gray-400 font-medium">
    224                                                         {user.userType === "ARTIST" ? "Artist" : "Listener"} • Profile
    225                                                 </span>
    226                                                 <h1 className="text-4xl md:text-5xl font-extrabold leading-tight">
    227                                                         {user.fullName}
    228                                                 </h1>
    229                                                 <p className="text-gray-400">@{user.username}</p>
    230 
    231                                                 {/* Stats */}
    232                                                 <div className="flex items-center gap-6 mt-2">
    233                                                         <div
    234                                                                 className={`${user.userType === "LISTENER" ? "cursor-pointer hover:text-white" : ""} transition-colors`}
    235                                                                 onClick={
    236                                                                         user.userType === "LISTENER" ? displayFollowers : undefined
    237                                                                 }
    238                                                         >
    239                                                                 <span className="text-xl font-bold text-white">
    240                                                                         {user.followers}
    241                                                                 </span>
    242                                                                 <span className="text-sm text-gray-400 ml-1">Followers</span>
    243                                                         </div>
    244                                                         <div
    245                                                                 className={`${user.userType === "LISTENER" ? "cursor-pointer hover:text-white" : ""} transition-colors`}
    246                                                                 onClick={
    247                                                                         user.userType === "LISTENER" ? displayFollowing : undefined
    248                                                                 }
    249                                                         >
    250                                                                 <span className="text-xl font-bold text-white">
    251                                                                         {user.following}
    252                                                                 </span>
    253                                                                 <span className="text-sm text-gray-400 ml-1">Following</span>
    254                                                         </div>
    255                                                 </div>
    256 
    257                                                 {/* Follow button - hidden on own profile */}
    258                                                 {!isOwnProfile && (
    259                                                         <div className="mt-4">
    260                                                                 <button
    261                                                                         onClick={handleFollow}
    262                                                                         disabled={isFollowing}
    263                                                                         className={`flex items-center gap-2 px-6 py-3 rounded-full text-sm font-semibold transition-all cursor-pointer ${
    264                                                                                 isFollowing
    265                                                                                         ? "bg-gray-700 text-gray-400 cursor-not-allowed"
    266                                                                                         : user.isFollowedByCurrentUser
    267                                                                                                 ? "bg-white/10 text-white hover:bg-white/20"
    268                                                                                                 : "bg-[#1db954] text-black hover:bg-[#1ed760] hover:scale-105"
    269                                                                         }`}
    270                                                                 >
    271                                                                         {user.isFollowedByCurrentUser ? (
    272                                                                                 <>
    273                                                                                         <svg
    274                                                                                                 className="w-5 h-5"
    275                                                                                                 fill="none"
    276                                                                                                 stroke="currentColor"
    277                                                                                                 viewBox="0 0 24 24"
    278                                                                                         >
    279                                                                                                 <path
    280                                                                                                         strokeLinecap="round"
    281                                                                                                         strokeLinejoin="round"
    282                                                                                                         strokeWidth={2}
    283                                                                                                         d="M5 13l4 4L19 7"
    284                                                                                                 />
    285                                                                                         </svg>
    286                                                                                         Following
    287                                                                                 </>
    288                                                                         ) : (
    289                                                                                 <>
    290                                                                                         <svg
    291                                                                                                 className="w-5 h-5"
    292                                                                                                 fill="none"
    293                                                                                                 stroke="currentColor"
    294                                                                                                 viewBox="0 0 24 24"
    295                                                                                         >
    296                                                                                                 <path
    297                                                                                                         strokeLinecap="round"
    298                                                                                                         strokeLinejoin="round"
    299                                                                                                         strokeWidth={2}
    300                                                                                                         d="M12 4v16m8-8H4"
    301                                                                                                 />
    302                                                                                         </svg>
    303                                                                                         Follow
    304                                                                                 </>
    305                                                                         )}
    306                                                                 </button>
    307                                                         </div>
    308                                                 )}
    309                                         </div>
    310                                 </div>
    311 
    312                                 {/* Content */}
    313                                 {user.userType === "ARTIST" ? (
    314                                         <ArtistView contributions={user.contributions} />
    315                                 ) : (
    316                                         <ListenerView
    317                                                 likedEntities={user.likedEntities}
    318                                                 createdPlaylists={user.createdPlaylists}
    319                                                 savedPlaylists={user.savedPlaylists}
    320                                         />
    321                                 )}
    322 
    323                                 {showModal && (
    324                                         <UserListModal
    325                                                 title={modalTitle}
    326                                                 users={modalUsers}
    327                                                 onClose={() => setShowModal(false)}
    328                                                 onFollowToggle={handleFollowInModal}
    329                                         />
    330                                 )}
    331                         </div>
    332                 </div>
    333         );
     38  const { username: usernameParam } = useParams();
     39  const { user: currentUser } = useAuth();
     40  const { createdPlaylists: currentUserCreatedPlaylists } =
     41    useCreatedPlaylists();
     42  const navigate = useNavigate();
     43  const [user, setUser] = useState<UserProfile | null>(null);
     44  const [error, setError] = useState<string | null>(null);
     45  const [showModal, setShowModal] = useState(false);
     46  const [modalTitle, setModalTitle] = useState("");
     47  const [modalUsers, setModalUsers] = useState<any[]>([]);
     48  const [isLoadingModal, setIsLoadingModal] = useState(false);
     49  const [isFollowing, setIsFollowing] = useState(false);
     50
     51  const username = usernameParam || currentUser?.username;
     52  const isOwnProfile = currentUser?.username === username;
     53
     54  if (!usernameParam && !currentUser) {
     55    return (
     56      <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
     57        <div className="text-center">
     58          <p className="text-red-400 text-xl mb-4">
     59            You must be logged in to view your profile.
     60          </p>
     61          <button
     62            onClick={() => navigate("/login")}
     63            className="text-[#1db954] hover:underline text-sm cursor-pointer"
     64          >
     65            Go to Login
     66          </button>
     67        </div>
     68      </div>
     69    );
     70  }
     71
     72  const handleFollow = async () => {
     73    if (!user) return;
     74
     75    setIsFollowing(true);
     76    try {
     77      const response = await axiosInstance.post<FollowStatus>(
     78        `/users/na/${username}/follow`,
     79      );
     80      setUser((prev) => {
     81        if (!prev) return null;
     82        return {
     83          ...prev,
     84          isFollowedByCurrentUser: response.data.isFollowing,
     85          followers: response.data.followerCount,
     86          following: response.data.followingCount,
     87        };
     88      });
     89    } catch (err: any) {
     90      setError(getErrorMessage(err));
     91    } finally {
     92      setIsFollowing(false);
     93    }
     94  };
     95
     96  const handleFollowInModal = async (targetUsername: string) => {
     97    try {
     98      const response = await axiosInstance.post<FollowStatus>(
     99        `/users/na/${targetUsername}/follow`,
     100      );
     101
     102      setModalUsers((prevUsers) =>
     103        prevUsers.map((u) =>
     104          u.username === targetUsername
     105            ? { ...u, isFollowedByCurrentUser: response.data.isFollowing }
     106            : u,
     107        ),
     108      );
     109    } catch (err: any) {
     110      setError(getErrorMessage(err));
     111    }
     112  };
     113
     114  const displayFollowers = async () => {
     115    setIsLoadingModal(true);
     116    try {
     117      const response = await axiosInstance.get(
     118        `/users/na/${username}/followers`,
     119      );
     120      setModalUsers(response.data);
     121      setModalTitle("Followers");
     122      setShowModal(true);
     123    } catch (err) {
     124      setError(getErrorMessage(err));
     125    } finally {
     126      setIsLoadingModal(false);
     127    }
     128  };
     129  const displayFollowing = async () => {
     130    setIsLoadingModal(true);
     131    try {
     132      const response = await axiosInstance.get(
     133        `/users/na/${username}/following`,
     134      );
     135      setModalUsers(response.data);
     136      setModalTitle("Following");
     137      setShowModal(true);
     138    } catch (err: any) {
     139      setError(getErrorMessage(err));
     140    } finally {
     141      setIsLoadingModal(false);
     142    }
     143  };
     144
     145  useEffect(() => {
     146    const fetchUser = async () => {
     147      setError(null);
     148      setUser(null);
     149      try {
     150        const response = await axiosInstance.get(`/users/na/${username}`);
     151
     152        setUser(response.data);
     153      } catch (err: any) {
     154        setError(getErrorMessage(err));
     155      }
     156    };
     157    fetchUser();
     158  }, [username]);
     159
     160  if (error) {
     161    return (
     162      <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
     163        <div className="text-center">
     164          <p className="text-red-400 text-xl mb-4">{error}</p>
     165          <Link to="/" className="text-[#1db954] hover:underline text-sm">
     166            ← Back to Home
     167          </Link>
     168        </div>
     169      </div>
     170    );
     171  }
     172
     173  if (!user) {
     174    return <LoadingSpinner />;
     175  }
     176
     177  return (
     178    <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white">
     179      {isLoadingModal && (
     180        <div className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm flex items-center justify-center">
     181          <div className="w-10 h-10 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin" />
     182        </div>
     183      )}
     184
     185      <div className="max-w-5xl mx-auto px-6 py-10">
     186        {/* Back link */}
     187        <Link
     188          to="/"
     189          className="inline-flex items-center gap-1 text-gray-400 hover:text-white text-sm mb-8 transition-colors"
     190        >
     191          <svg
     192            className="w-4 h-4"
     193            fill="none"
     194            stroke="currentColor"
     195            viewBox="0 0 24 24"
     196          >
     197            <path
     198              strokeLinecap="round"
     199              strokeLinejoin="round"
     200              strokeWidth={2}
     201              d="M15 19l-7-7 7-7"
     202            />
     203          </svg>
     204          Back to Home
     205        </Link>
     206
     207        {/* Hero section */}
     208        <div className="flex flex-col md:flex-row gap-8 mb-10">
     209          {/* Profile photo */}
     210          <div className="w-full md:w-48 shrink-0">
     211            <div className="relative w-48 h-48 rounded-full overflow-hidden shadow-2xl bg-[#181818] mx-auto md:mx-0">
     212              {user.profilePhoto ? (
     213                <img
     214                  src={`${baseURL}/${user.profilePhoto}`}
     215                  alt={user.fullName}
     216                  className="w-full h-full object-cover"
     217                />
     218              ) : (
     219                <div className="w-full h-full bg-linear-to-br from-[#1db954] to-[#1ed760] flex items-center justify-center text-white text-5xl font-bold">
     220                  {user.fullName.charAt(0).toUpperCase()}
     221                </div>
     222              )}
     223            </div>
     224          </div>
     225
     226          {/* User info */}
     227          <div className="flex flex-col justify-end gap-3 min-w-0">
     228            <span className="text-xs uppercase tracking-widest text-gray-400 font-medium">
     229              {user.userType === "ARTIST" ? "Artist" : "Listener"} • Profile
     230            </span>
     231            <h1 className="text-4xl md:text-5xl font-extrabold leading-tight">
     232              {user.fullName}
     233            </h1>
     234            <p className="text-gray-400">@{user.username}</p>
     235
     236            {/* Stats */}
     237            <div className="flex items-center gap-6 mt-2">
     238              <div
     239                className={`${user.userType === "LISTENER" ? "cursor-pointer hover:text-white" : ""} transition-colors`}
     240                onClick={
     241                  user.userType === "LISTENER" ? displayFollowers : undefined
     242                }
     243              >
     244                <span className="text-xl font-bold text-white">
     245                  {user.followers}
     246                </span>
     247                <span className="text-sm text-gray-400 ml-1">Followers</span>
     248              </div>
     249              <div
     250                className={`${user.userType === "LISTENER" ? "cursor-pointer hover:text-white" : ""} transition-colors`}
     251                onClick={
     252                  user.userType === "LISTENER" ? displayFollowing : undefined
     253                }
     254              >
     255                <span className="text-xl font-bold text-white">
     256                  {user.following}
     257                </span>
     258                <span className="text-sm text-gray-400 ml-1">Following</span>
     259              </div>
     260            </div>
     261
     262            {/* Follow button - hidden on own profile */}
     263            {!isOwnProfile && (
     264              <div className="mt-4">
     265                <button
     266                  onClick={handleFollow}
     267                  disabled={isFollowing}
     268                  className={`flex items-center gap-2 px-6 py-3 rounded-full text-sm font-semibold transition-all cursor-pointer ${
     269                    isFollowing
     270                      ? "bg-gray-700 text-gray-400 cursor-not-allowed"
     271                      : user.isFollowedByCurrentUser
     272                        ? "bg-white/10 text-white hover:bg-white/20"
     273                        : "bg-[#1db954] text-black hover:bg-[#1ed760] hover:scale-105"
     274                  }`}
     275                >
     276                  {user.isFollowedByCurrentUser ? (
     277                    <>
     278                      <svg
     279                        className="w-5 h-5"
     280                        fill="none"
     281                        stroke="currentColor"
     282                        viewBox="0 0 24 24"
     283                      >
     284                        <path
     285                          strokeLinecap="round"
     286                          strokeLinejoin="round"
     287                          strokeWidth={2}
     288                          d="M5 13l4 4L19 7"
     289                        />
     290                      </svg>
     291                      Following
     292                    </>
     293                  ) : (
     294                    <>
     295                      <svg
     296                        className="w-5 h-5"
     297                        fill="none"
     298                        stroke="currentColor"
     299                        viewBox="0 0 24 24"
     300                      >
     301                        <path
     302                          strokeLinecap="round"
     303                          strokeLinejoin="round"
     304                          strokeWidth={2}
     305                          d="M12 4v16m8-8H4"
     306                        />
     307                      </svg>
     308                      Follow
     309                    </>
     310                  )}
     311                </button>
     312              </div>
     313            )}
     314          </div>
     315        </div>
     316
     317        {/* Content */}
     318        {user.userType === "ARTIST" ? (
     319          <ArtistView contributions={user.contributions} />
     320        ) : (
     321          <ListenerView
     322            likedEntities={user.likedEntities}
     323            createdPlaylists={user.createdPlaylists}
     324            savedPlaylists={user.savedPlaylists}
     325          />
     326        )}
     327
     328        {showModal && (
     329          <UserListModal
     330            title={modalTitle}
     331            users={modalUsers}
     332            onClose={() => setShowModal(false)}
     333            onFollowToggle={handleFollowInModal}
     334          />
     335        )}
     336      </div>
     337    </div>
     338  );
    334339};
    335340
Note: See TracChangeset for help on using the changeset viewer.