Changeset 6de2873


Ignore:
Timestamp:
01/27/26 17:30:07 (6 months ago)
Author:
Dimitar Arsov <dimitararsov04@…>
Branches:
main
Children:
98992cf
Parents:
c1d2f07
Message:

implement following a user

Files:
2 added
6 edited

Legend:

Unmodified
Added
Removed
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/GlobalExceptionHandler.java

    rc1d2f07 r6de2873  
    33import java.util.Map;
    44
     5import com.ukim.finki.develop.finkwave.exceptions.FollowException;
     6import com.ukim.finki.develop.finkwave.exceptions.UserNotFoundException;
    57import org.slf4j.Logger;
    68import org.slf4j.LoggerFactory;
     
    3335    }
    3436
     37    @ExceptionHandler(UserNotFoundException.class)
     38    public ResponseEntity<Map<String, String>> handleUserNotFound(UserNotFoundException ex) {
     39        return ResponseEntity
     40                .status(HttpStatus.BAD_REQUEST)
     41                .body(Map.of("error", ex.getMessage()));
     42    }
     43
     44    @ExceptionHandler(FollowException.class)
     45    public ResponseEntity<Map<String, String>> handleSelfFollow(FollowException ex) {
     46        return ResponseEntity
     47                .status(HttpStatus.BAD_REQUEST)
     48                .body(Map.of("error", ex.getMessage()));
     49    }
     50
     51
     52
    3553    @ExceptionHandler(ResponseStatusException.class)
    3654    public ResponseEntity<Map<String, String>> handleResponseStatusException(ResponseStatusException ex) {
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/NonAdminUserController.java

    rc1d2f07 r6de2873  
    22
    33import com.ukim.finki.develop.finkwave.model.dto.NonAdminUserDTO;
     4import com.ukim.finki.develop.finkwave.service.FollowService;
    45import com.ukim.finki.develop.finkwave.service.NonAdminUserService;
    56import lombok.RequiredArgsConstructor;
     
    1718
    1819    private final NonAdminUserService nonAdminUserService;
     20    private final FollowService followService;
    1921
    2022    @GetMapping("/all")
     
    3234        return ResponseEntity.ok(nonAdminUserService.searchUsers(name));
    3335    }
     36
     37    @PostMapping("/follow/{id}")
     38    public HttpEntity<NonAdminUserDTO>followUser(@PathVariable Long id){
     39        followService.toggleFollow(id);
     40
     41        return ResponseEntity.ok(nonAdminUserService.getById(id));
     42    }
    3443}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/Follow.java

    rc1d2f07 r6de2873  
    33import jakarta.persistence.*;
    44import lombok.Getter;
     5import lombok.NoArgsConstructor;
    56import lombok.Setter;
    67import org.hibernate.annotations.OnDelete;
     
    1011@Setter
    1112@Entity
     13@NoArgsConstructor
    1214@Table(name = "follows", schema = "project")
    1315public class Follow {
     
    2729    private NonAdminUser followee;
    2830
     31    public Follow(NonAdminUser follower,NonAdminUser followee){
     32        this.follower=follower;
     33        this.followee=followee;
     34        this.id=new FollowId(follower.getId(),followee.getId());
     35    }
     36
    2937}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/FollowId.java

    rc1d2f07 r6de2873  
    33import jakarta.persistence.Column;
    44import jakarta.persistence.Embeddable;
     5import lombok.AllArgsConstructor;
    56import lombok.Getter;
     7import lombok.NoArgsConstructor;
    68import lombok.Setter;
    79import org.hibernate.Hibernate;
     
    1315@Setter
    1416@Embeddable
     17@NoArgsConstructor
     18@AllArgsConstructor
    1519public class FollowId implements Serializable {
    1620    private static final long serialVersionUID = -915670330063692538L;
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/NonAdminUserService.java

    rc1d2f07 r6de2873  
    11package com.ukim.finki.develop.finkwave.service;
    22
     3import com.ukim.finki.develop.finkwave.exceptions.UserNotFoundException;
    34import com.ukim.finki.develop.finkwave.model.*;
    45import com.ukim.finki.develop.finkwave.model.dto.*;
     
    6667    private NonAdminUserDTO getArtistProfile(Long artistId, Long followers,Long following) {
    6768        Artist artist = artistRepository.findByIdWithUser(artistId)
    68             .orElseThrow(() -> new RuntimeException("Artist not found with id: " + artistId));
     69            .orElseThrow(()->new UserNotFoundException("Artist not found with id: " + artistId));
    6970
    7071
     
    7778    private NonAdminUserDTO getListenerProfile(Long listenerId, Long followers,Long following) {
    7879        Listener listener = listenerRepository.findByIdWithUser(listenerId)
    79             .orElseThrow(() -> new RuntimeException("Listener not found with id: " + listenerId));
     80            .orElseThrow(()->new UserNotFoundException("Listener not found with id: " + listenerId));
    8081
    8182
     
    9798        List<NonAdminUser>nonAdminUsers=nonAdminUserRepository.searchByName(name);
    9899        if (nonAdminUsers.isEmpty()){
    99             throw new RuntimeException("No user found");
     100            throw new UserNotFoundException("No users matched the criteria");
    100101        }
    101102
  • frontend/src/pages/UserDetailView.tsx

    rc1d2f07 r6de2873  
    55import ListenerView from "../components/userProfile/ListenerView";
    66import type {
    7         ArtistContributionDTO,
    8         MusicalEntityDTO,
    9         Playlist,
     7  ArtistContributionDTO,
     8  MusicalEntityDTO,
     9  Playlist,
    1010} from "../utils/types";
    1111
    1212interface User {
    13         id: number;
    14         username: string;
    15         fullName: string;
    16         userType: string;
    17         followers: number;
    18         following: number;
    19         isFollowedByCurrentUser: boolean;
     13  id: number;
     14  username: string;
     15  fullName: string;
     16  userType: string;
     17  followers: number;
     18  following: number;
     19  isFollowedByCurrentUser: boolean;
    2020
    21         musicalEntities?: {
    22                 contributions: ArtistContributionDTO[];
    23         };
     21  musicalEntities?: {
     22    contributions: ArtistContributionDTO[];
     23  };
    2424
    25         likes?: {
    26                 likedEntities: MusicalEntityDTO[];
    27         };
     25  likes?: {
     26    likedEntities: MusicalEntityDTO[];
     27  };
    2828
    29         createdPlaylists?: Playlist[];
     29  createdPlaylists?: Playlist[];
    3030}
    3131
    3232const UserDetail = () => {
    33         // user refers to the selected user NOT to the user from context
    34         const { userId } = useParams();
    35         const navigate = useNavigate();
    36         const [user, setUser] = useState<User | null>(null);
    37         const [error, setError] = useState<string | null>(null);
     33  // user refers to the selected user NOT to the user from context
     34  const { userId } = useParams();
     35  const navigate = useNavigate();
     36  const [user, setUser] = useState<User | null>(null);
     37  const [error, setError] = useState<string | null>(null);
     38  const [isFollowing, setIsFollowing] = useState(false);
    3839
    39         useEffect(() => {
    40                 const fetchUser = async () => {
    41                         setError(null);
    42                         try {
    43                                 const response = await axiosInstance.get(`/users/${userId}`);
     40  const handleFollow = async () => {
     41    if (!user) return;
    4442
    45                                 setUser(response.data);
    46                         } catch (err: any) {
    47                                 const errorMessage =
    48                                         err.response?.data?.error || "Failed to fetch user";
    49                                 setError(errorMessage);
    50                         }
    51                 };
    52                 fetchUser();
    53         }, [userId]);
     43    setIsFollowing(true);
     44    try {
     45      const response = await axiosInstance.post<User>(
     46        `/users/follow/${userId}`,
     47      );
     48      setUser(response.data);
     49    } catch (err: any) {
     50      console.error(err.response?.data?.error);
     51    } finally {
     52      setIsFollowing(false);
     53    }
     54  };
    5455
    55         if (error) {
    56                 return (
    57                         <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg">
    58                                 <h2 className="font-bold">Error</h2>
    59                                 <p>{error}</p>
    60                         </div>
    61                 );
    62         }
     56  useEffect(() => {
     57    const fetchUser = async () => {
     58      setError(null);
     59      try {
     60        const response = await axiosInstance.get(`/users/${userId}`);
    6361
    64         if (!user) return <div className="p-6">Loading...</div>;
     62        setUser(response.data);
     63      } catch (err: any) {
     64        const errorMessage =
     65          err.response?.data?.error || "Failed to fetch user";
     66        setError(errorMessage);
     67      }
     68    };
     69    fetchUser();
     70  }, [userId]);
    6571
    66         return (
    67                 <div className="container mx-auto p-6">
    68                         <button
    69                                 onClick={() => navigate(-1)}
    70                                 className="mb-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
    71                         >
    72                                 ← Back
    73                         </button>
     72  if (error) {
     73    return (
     74      <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg">
     75        <h2 className="font-bold">Error</h2>
     76        <p>{error}</p>
     77      </div>
     78    );
     79  }
    7480
    75                         <div className="bg-white shadow-lg rounded-lg p-8">
    76                                 <div className="flex items-start gap-6 mb-8">
    77                                         <div className="shrink-0">
    78                                                 <div className="w-32 h-32 rounded-full bg-linear-to-br from-blue-400 to-purple-500 flex items-center justify-center text-white text-4xl font-bold shadow-lg">
    79                                                         {user.fullName.charAt(0).toUpperCase()}
    80                                                 </div>
    81                                         </div>
     81  if (!user) return <div className="p-6">Loading...</div>;
    8282
    83                                         <div className="flex-1">
    84                                                 <h1 className="text-4xl font-bold mb-2">{user.fullName}</h1>
    85                                                 <span className="inline-block px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium mb-4">
    86                                                         {user.userType}
    87                                                 </span>
     83  return (
     84    <div className="container mx-auto p-6">
     85      <button
     86        onClick={() => navigate(-1)}
     87        className="mb-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors duration-200"
     88      >
     89        ← Back
     90      </button>
    8891
    89                                                 <div className="flex gap-6 mb-4 text-gray-700">
    90                                                         <div className="flex flex-col">
    91                                                                 <span className="text-2xl font-bold">{user.followers}</span>
    92                                                                 <span className="text-sm text-gray-500">Followers</span>
    93                                                         </div>
    94                                                         <div className="flex flex-col">
    95                                                                 <span className="text-2xl font-bold">{user.following}</span>
    96                                                                 <span className="text-sm text-gray-500">Following</span>
    97                                                         </div>
    98                                                 </div>
     92      <div className="bg-white shadow-lg rounded-lg p-8">
     93        <div className="flex items-start gap-6 mb-8">
     94          <div className="shrink-0">
     95            <div className="w-32 h-32 rounded-full bg-linear-to-br from-blue-400 to-purple-500 flex items-center justify-center text-white text-4xl font-bold shadow-lg">
     96              {user.fullName.charAt(0).toUpperCase()}
     97            </div>
     98          </div>
    9999
    100                                                 <button className="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-lg shadow-md transition-colors duration-200 cursor-pointer">
    101                                                         {user.isFollowedByCurrentUser ? "Unfollow" : "Follow"}
    102                                                 </button>
    103                                         </div>
    104                                 </div>
     100          <div className="flex-1">
     101            <h1 className="text-4xl font-bold mb-2">{user.fullName}</h1>
     102            <span className="inline-block px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium mb-4">
     103              {user.userType}
     104            </span>
    105105
    106                                 {user.userType === "ARTIST" && user.musicalEntities?.contributions && (
    107                                         <ArtistView contributions={user.musicalEntities.contributions} />
    108                                 )}
     106            <div className="flex gap-6 mb-4 text-gray-700">
     107              <div className="flex flex-col">
     108                <span className="text-2xl font-bold">{user.followers}</span>
     109                <span className="text-sm text-gray-500">Followers</span>
     110              </div>
     111              <div className="flex flex-col">
     112                <span className="text-2xl font-bold">{user.following}</span>
     113                <span className="text-sm text-gray-500">Following</span>
     114              </div>
     115            </div>
    109116
    110                                 {user.userType === "LISTENER" && user.likes?.likedEntities && (
    111                                         <ListenerView
    112                                                 likedEntities={user.likes.likedEntities}
    113                                                 playlists={user.createdPlaylists}
    114                                         />
    115                                 )}
    116                         </div>
    117                 </div>
    118         );
     117            <button
     118              onClick={handleFollow}
     119              disabled={isFollowing}
     120              className={`
     121                px-6 py-2 font-semibold rounded-lg shadow-md
     122                transition-colors duration-200
     123                ${
     124                  isFollowing
     125                    ? "bg-gray-400 text-gray-200 cursor-not-allowed"
     126                    : user.isFollowedByCurrentUser
     127                      ? "bg-gray-200 text-gray-700 hover:bg-gray-300 cursor-pointer"
     128                      : "bg-blue-500 text-white hover:bg-blue-600 cursor-pointer"
     129                }
     130              `}
     131            >
     132              {user.isFollowedByCurrentUser ? "Unfollow" : "Follow"}
     133            </button>
     134          </div>
     135        </div>
     136
     137        {user.userType === "ARTIST" && user.musicalEntities?.contributions && (
     138          <ArtistView contributions={user.musicalEntities.contributions} />
     139        )}
     140
     141        {user.userType === "LISTENER" && user.likes?.likedEntities && (
     142          <ListenerView
     143            likedEntities={user.likes.likedEntities}
     144            playlists={user.createdPlaylists}
     145          />
     146        )}
     147      </div>
     148    </div>
     149  );
    119150};
    120151
Note: See TracChangeset for help on using the changeset viewer.