Changeset 0792d02 for frontend/src


Ignore:
Timestamp:
02/05/26 10:29:09 (5 months ago)
Author:
Dimitar Arsov <dimitararsov04@…>
Branches:
main
Children:
3de009b
Parents:
d2af1a6
Message:

show saved playlists by user

Location:
frontend/src
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • frontend/src/App.tsx

    rd2af1a6 r0792d02  
    5151      },
    5252      {
    53         path: "/users/:userId",
     53        path: "/users/:username",
    5454        element: <UserDetail />,
    5555      },
  • frontend/src/components/userProfile/ListenerView.tsx

    rd2af1a6 r0792d02  
    44import axiosInstance from "../../api/axiosInstance";
    55import { useState } from "react";
     6
    67interface ListenerViewProps {
    78  likedEntities: MusicalEntity[] | [];
    8   playlists: Playlist[] | [];
     9  createdPlaylists: Playlist[] | [];
     10  savedPlaylists: Playlist[] | [];
    911}
    1012
    11 const ListenerView = ({ likedEntities, playlists }: ListenerViewProps) => {
     13const ListenerView = ({
     14  likedEntities,
     15  createdPlaylists,
     16  savedPlaylists,
     17}: ListenerViewProps) => {
    1218  const navigate = useNavigate();
    1319  const [items, setItems] = useState(likedEntities);
     20  const [savedItems, setSavedItems] = useState(savedPlaylists);
    1421  const [toast, setToast] = useState<{ message: string; show: boolean }>({
    1522    message: "",
     
    2734  const likedAlbums = items.filter((e) => e.type === "ALBUM");
    2835
    29   const handleSavePlaylist = (e: React.MouseEvent, playlistId: number) => {
     36  const handleSavePlaylist = async (
     37    e: React.MouseEvent,
     38    playlistId: number,
     39    playlistName: string,
     40  ) => {
    3041    e.stopPropagation();
    3142
    32     console.log("Save playlist:", playlistId);
     43    try {
     44      const response = await axiosInstance.post(`/playlist/${playlistId}/save`);
     45      const data = response.data;
     46
     47      setSavedItems((prevItems) =>
     48        data.isSaved
     49          ? [...prevItems, prevItems.find((p) => p.id === playlistId)!]
     50          : prevItems.filter((p) => p.id !== playlistId),
     51      );
     52
     53      showToast(
     54        data.isSaved
     55          ? `Saved "${playlistName}"`
     56          : `Removed "${playlistName}" from saved playlists`,
     57      );
     58    } catch (err: any) {
     59      showToast(err.response?.data?.error || "Failed to save playlist");
     60    }
    3361  };
    3462
     
    6391        </div>
    6492      )}
    65       {playlists && playlists.length > 0 && (
     93
     94      {createdPlaylists && createdPlaylists.length > 0 && (
    6695        <section>
    6796          <div className="flex items-center gap-3 mb-6">
     
    7099              Created Playlists
    71100            </h3>
    72             <span className="text-sm text-gray-500">({playlists.length})</span>
     101            <span className="text-sm text-gray-500">
     102              ({createdPlaylists.length})
     103            </span>
    73104          </div>
    74105
    75106          <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
    76             {playlists.map((playlist) => (
     107            {createdPlaylists.map((playlist) => (
    77108              <div
    78109                key={playlist.id}
     
    93124                  )}
    94125
    95                   {/* Save button overlay */}
    96126                  <button
    97                     onClick={(e) => handleSavePlaylist(e, playlist.id)}
     127                    onClick={(e) =>
     128                      handleSavePlaylist(e, playlist.id, playlist.name)
     129                    }
    98130                    className="absolute top-2 right-2 p-2 bg-white/90 hover:bg-white rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100"
    99131                    title={playlist.isSavedByCurrentUser ? "Unsave" : "Save"}
     
    106138                      }`}
    107139                    />
     140                  </button>
     141                </div>
     142                <p className="text-sm font-semibold text-gray-900 truncate group-hover:text-blue-600 transition-colors">
     143                  {playlist.name}
     144                </p>
     145                <p className="text-xs text-gray-500 truncate">
     146                  {playlist.creatorName}
     147                </p>
     148              </div>
     149            ))}
     150          </div>
     151        </section>
     152      )}
     153
     154      {savedItems && savedItems.length > 0 && (
     155        <section>
     156          <div className="flex items-center gap-3 mb-6">
     157            <Bookmark className="w-6 h-6 text-blue-600 fill-blue-600" />
     158            <h3 className="text-2xl font-bold text-gray-800">
     159              Saved Playlists
     160            </h3>
     161            <span className="text-sm text-gray-500">({savedItems.length})</span>
     162          </div>
     163
     164          <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
     165            {savedItems.map((playlist) => (
     166              <div
     167                key={playlist.id}
     168                className="group cursor-pointer"
     169                onClick={() => navigate(`/collection/playlist/${playlist.id}`)}
     170              >
     171                <div className="relative aspect-square rounded-lg overflow-hidden bg-gray-100 mb-3 shadow-md group-hover:shadow-lg transition-all">
     172                  {playlist.cover ? (
     173                    <img
     174                      src={playlist.cover}
     175                      alt={playlist.name}
     176                      className="w-full h-full object-cover"
     177                    />
     178                  ) : (
     179                    <div className="w-full h-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center">
     180                      <ListMusic className="w-16 h-16 text-white opacity-30" />
     181                    </div>
     182                  )}
     183
     184                  <button
     185                    onClick={(e) =>
     186                      handleSavePlaylist(e, playlist.id, playlist.name)
     187                    }
     188                    className="absolute top-2 right-2 p-2 bg-white/90 hover:bg-white rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100"
     189                    title="Unsave"
     190                  >
     191                    <Bookmark className="w-5 h-5 fill-blue-600 text-blue-600" />
    108192                  </button>
    109193                </div>
     
    234318      {likedEntities &&
    235319        likedEntities.length === 0 &&
    236         (!playlists || playlists.length === 0) && (
     320        (!createdPlaylists || createdPlaylists.length === 0) &&
     321        (!savedItems || savedItems.length === 0) && (
    237322          <div className="flex flex-col items-center justify-center py-16 text-gray-400">
    238323            <Music className="w-20 h-20 mb-4 opacity-20" />
  • frontend/src/components/userProfile/UserListModal.tsx

    rd2af1a6 r0792d02  
    66  users: BaseNonAdminUser[];
    77  onClose: () => void;
    8   onFollowToggle: (targetId: number) => Promise<void>;
     8  onFollowToggle: (targetUsername: string) => Promise<void>;
    99}
    1010
     
    3737            users.map((u) => (
    3838              <div
    39                 key={u.id}
     39                key={u.username}
    4040                className="flex items-center justify-between p-3 hover:bg-gray-50 rounded-lg transition-colors"
    4141              >
     
    4444                  onClick={() => {
    4545                    onClose();
    46                     navigate(`/users/${u.id}`);
     46                    navigate(`/users/${u.username}`);
    4747                  }}
    4848                >
     
    6464
    6565                <button
    66                   onClick={() => onFollowToggle(u.id)}
     66                  onClick={() => onFollowToggle(u.username)}
    6767                  className={`px-4 py-1 text-sm font-medium rounded-md transition-colors cursor-pointer ${
    6868                    u.isFollowedByCurrentUser
  • frontend/src/pages/AllUsers.tsx

    rd2af1a6 r0792d02  
    44
    55interface User {
    6         id: number;
    7         username: string;
    8         fullName: string;
     6  username: string;
     7  fullName: string;
    98}
    109
    1110const AllUsers = () => {
    12         const [users, setUsers] = useState<User[]>([]);
    13         const [searchedUser, setSearchedUser] = useState<string>("");
    14         const [error, setError] = useState<string | null>(null);
    15         const navigate = useNavigate();
     11  const [users, setUsers] = useState<User[]>([]);
     12  const [searchedUser, setSearchedUser] = useState<string>("");
     13  const [error, setError] = useState<string | null>(null);
     14  const navigate = useNavigate();
    1615
    17         useEffect(() => {
    18                 const fetchUsers = async () => {
    19                         const response = axiosInstance.get("/users/all");
    20                         setUsers((await response).data);
    21                 };
    22                 fetchUsers();
    23         }, []);
     16  useEffect(() => {
     17    const fetchUsers = async () => {
     18      const response = axiosInstance.get("/users/all");
     19      setUsers((await response).data);
     20    };
     21    fetchUsers();
     22  }, []);
    2423
    25         const handleUserClick = (userId: number) => {
    26                 navigate(`/users/${userId}`);
    27         };
     24  const handleUserClick = (username: string) => {
     25    navigate(`/users/${username}`);
     26  };
    2827
    29         const handleSearch = async (e: React.FormEvent) => {
    30                 e.preventDefault();
    31                 try {
    32                         const response = await axiosInstance.get(`/users/search`, {
    33                                 params: { name: searchedUser },
    34                         });
    35                         setUsers(response.data);
    36                 } catch (err: any) {
    37                         const errorMessage = err.response?.data?.error || "Search failed";
    38                         setError(errorMessage);
    39                 }
    40         };
     28  const handleSearch = async (e: React.FormEvent) => {
     29    e.preventDefault();
     30    try {
     31      const response = await axiosInstance.get(`/users/search`, {
     32        params: { name: searchedUser },
     33      });
     34      setUsers(response.data);
     35    } catch (err: any) {
     36      const errorMessage = err.response?.data?.error || "Search failed";
     37      setError(errorMessage);
     38    }
     39  };
    4140
    42         if (users.length == 0) return <div className="p-6">Loading...</div>;
    43         if (error) {
    44                 return (
    45                         <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg">
    46                                 <h2 className="font-bold">Error</h2>
    47                                 <p>{error}</p>
    48                         </div>
    49                 );
    50         }
     41  if (users.length == 0) return <div className="p-6">Loading...</div>;
     42  if (error) {
     43    return (
     44      <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg">
     45        <h2 className="font-bold">Error</h2>
     46        <p>{error}</p>
     47      </div>
     48    );
     49  }
    5150
    52         return (
    53                 <div className="container mx-auto p-6">
    54                         <h1 className="text-3xl font-bold mb-6">All Users</h1>
    55                         <form onSubmit={handleSearch} className="flex gap-2 mb-10">
    56                                 <input
    57                                         type="text"
    58                                         value={searchedUser}
    59                                         onChange={(e) => setSearchedUser(e.target.value)}
    60                                         className="border p-2 rounded w-full"
    61                                         placeholder="Search for a user..."
    62                                 />
    63                                 <button
    64                                         type="submit"
    65                                         className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 transition-colors"
    66                                 >
    67                                         Search
    68                                 </button>
    69                         </form>
     51  return (
     52    <div className="container mx-auto p-6">
     53      <h1 className="text-3xl font-bold mb-6">All Users</h1>
     54      <form onSubmit={handleSearch} className="flex gap-2 mb-10">
     55        <input
     56          type="text"
     57          value={searchedUser}
     58          onChange={(e) => setSearchedUser(e.target.value)}
     59          className="border p-2 rounded w-full"
     60          placeholder="Search for a user..."
     61        />
     62        <button
     63          type="submit"
     64          className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 transition-colors"
     65        >
     66          Search
     67        </button>
     68      </form>
    7069
    71                         <div className="grid gap-4">
    72                                 {users.map((u) => (
    73                                         <div
    74                                                 key={u.id}
    75                                                 onClick={() => handleUserClick(u.id)}
    76                                                 className="bg-white shadow-md rounded-lg p-4 hover:shadow-lg hover:bg-gray-50 cursor-pointer transition-all"
    77                                         >
    78                                                 <h2 className="text-xl font-semibold">{u.fullName}</h2>
    79                                                 <p className="text-gray-600">@{u.username}</p>
    80                                                 <p className="text-gray-600">{u.id}</p>
    81                                         </div>
    82                                 ))}
    83                         </div>
    84                 </div>
    85         );
     70      <div className="grid gap-4">
     71        {users.map((u) => (
     72          <div
     73            key={u.username}
     74            onClick={() => handleUserClick(u.username)}
     75            className="bg-white shadow-md rounded-lg p-4 hover:shadow-lg hover:bg-gray-50 cursor-pointer transition-all"
     76          >
     77            <h2 className="text-xl font-semibold">{u.fullName}</h2>
     78            <p className="text-gray-600">@{u.username}</p>
     79          </div>
     80        ))}
     81      </div>
     82    </div>
     83  );
    8684};
    8785
  • frontend/src/pages/UserDetail.tsx

    rd2af1a6 r0792d02  
    2727  likedEntities: MusicalEntity[];
    2828  createdPlaylists: Playlist[];
     29  savedPlaylists: Playlist[];
    2930}
    3031
     
    3435  // user refers to the selected user NOT to the user from context
    3536  const baseURL = import.meta.env.VITE_API_BASE_URL;
    36   const { userId } = useParams();
     37  const { username } = useParams();
    3738  const navigate = useNavigate();
    3839  const [user, setUser] = useState<UserProfile | null>(null);
     
    5051    try {
    5152      const response = await axiosInstance.post<FollowStatus>(
    52         `/users/${userId}/follow`,
     53        `/users/${username}/follow`,
    5354      );
    5455      setUser((prev) => {
     
    6869  };
    6970
    70   const handleFollowInModal = async (targetId: number) => {
     71  const handleFollowInModal = async (targetUsername: string) => {
    7172    try {
    7273      const response = await axiosInstance.post<FollowStatus>(
    73         `/users/${targetId}/follow`,
     74        `/users/${username}/follow`,
    7475      );
    7576
    7677      setModalUsers((prevUsers) =>
    7778        prevUsers.map((u) =>
    78           u.id === targetId
     79          u.username === targetUsername
    7980            ? { ...u, isFollowedByCurrentUser: response.data.isFollowing }
    8081            : u,
     
    100101    setIsLoadingModal(true);
    101102    try {
    102       const response = await axiosInstance.get(`/users/${userId}/followers`);
     103      const response = await axiosInstance.get(`/users/${username}/followers`);
    103104      setModalUsers(response.data);
    104105      setModalTitle("Followers");
     
    113114    setIsLoadingModal(true);
    114115    try {
    115       const response = await axiosInstance.get(`/users/${userId}/following`);
     116      const response = await axiosInstance.get(`/users/${username}/following`);
    116117      setModalUsers(response.data);
    117118      setModalTitle("Following");
     
    128129      setError(null);
    129130      try {
    130         const response = await axiosInstance.get(`/users/${userId}`);
     131        const response = await axiosInstance.get(`/users/${username}`);
    131132        console.log(response.data);
    132133        setUser(response.data);
     
    136137    };
    137138    fetchUser();
    138   }, [userId]);
     139  }, [username]);
    139140
    140141  if (error) {
     
    233234          <ListenerView
    234235            likedEntities={user.likedEntities}
    235             playlists={user.createdPlaylists}
     236            createdPlaylists={user.createdPlaylists}
     237            savedPlaylists={user.savedPlaylists}
    236238          />
    237239        )}
  • frontend/src/utils/types.ts

    rd2af1a6 r0792d02  
    4444
    4545export interface BaseNonAdminUser {
    46   id: number;
     46  username: string;
    4747  fullName: string;
    4848  userType: string;
Note: See TracChangeset for help on using the changeset viewer.