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

save playlist

File:
1 edited

Legend:

Unmodified
Added
Removed
  • frontend/src/pages/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.