diff --git a/backend/src/main/java/hr/algebra/socialnetwork/controller/FriendsController.java b/backend/src/main/java/hr/algebra/socialnetwork/controller/FriendsController.java index 29e2439..92ceb43 100644 --- a/backend/src/main/java/hr/algebra/socialnetwork/controller/FriendsController.java +++ b/backend/src/main/java/hr/algebra/socialnetwork/controller/FriendsController.java @@ -1,17 +1,13 @@ package hr.algebra.socialnetwork.controller; import hr.algebra.socialnetwork.dto.FriendRequestDTO; +import hr.algebra.socialnetwork.dto.UserSummaryDTO; import hr.algebra.socialnetwork.service.FriendService; import java.security.Principal; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/friends") @@ -48,4 +44,14 @@ public ResponseEntity removeFriend(@PathVariable Long userId, Principal pr public ResponseEntity> getPendingRequests(Principal principal) { return ResponseEntity.ok(friendService.getPendingRequests(principal.getName())); } + + @GetMapping("/all") + public ResponseEntity> getFriends(Principal principal) { + return ResponseEntity.ok(friendService.getFriends(principal.getName())); + } + + @GetMapping("/non-friends") + public ResponseEntity> getNonFriends(Principal principal) { + return ResponseEntity.ok(friendService.getNonFriends(principal.getName())); + } } diff --git a/backend/src/main/java/hr/algebra/socialnetwork/repository/FriendRequestRepository.java b/backend/src/main/java/hr/algebra/socialnetwork/repository/FriendRequestRepository.java index 907ed27..193fd28 100644 --- a/backend/src/main/java/hr/algebra/socialnetwork/repository/FriendRequestRepository.java +++ b/backend/src/main/java/hr/algebra/socialnetwork/repository/FriendRequestRepository.java @@ -32,4 +32,15 @@ boolean existsBySenderAndReceiverAndStatus( """) List findByReceiverAndStatus( @Param("receiver") User receiver, @Param("status") RequestStatus status); + + @Query( + """ + SELECT fr FROM FriendRequest fr + WHERE (fr.sender = :requester OR fr.receiver = :requester) + AND fr.status = :requestStatus +""") + List findBySenderOrReceiverAndStatus( + @Param("requester") User requester, + @Param("receiver") User receiver, // Can be omitted if unused + @Param("requestStatus") RequestStatus requestStatus); } diff --git a/backend/src/main/java/hr/algebra/socialnetwork/service/FriendService.java b/backend/src/main/java/hr/algebra/socialnetwork/service/FriendService.java index b00c70a..105e32d 100644 --- a/backend/src/main/java/hr/algebra/socialnetwork/service/FriendService.java +++ b/backend/src/main/java/hr/algebra/socialnetwork/service/FriendService.java @@ -1,8 +1,10 @@ package hr.algebra.socialnetwork.service; import hr.algebra.socialnetwork.dto.FriendRequestDTO; +import hr.algebra.socialnetwork.dto.UserSummaryDTO; import hr.algebra.socialnetwork.exception.ResourceNotFoundException; import hr.algebra.socialnetwork.mapper.FriendRequestDTOMapper; +import hr.algebra.socialnetwork.mapper.UserSummaryDTOMapper; import hr.algebra.socialnetwork.model.FriendRequest; import hr.algebra.socialnetwork.model.RequestStatus; import hr.algebra.socialnetwork.model.User; @@ -10,6 +12,8 @@ import hr.algebra.socialnetwork.repository.UserRepository; import java.time.LocalDateTime; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -20,6 +24,7 @@ public class FriendService { private final UserRepository userRepository; private final FriendRequestRepository friendRequestRepository; private final FriendRequestDTOMapper friendRequestDTOMapper; + private final UserSummaryDTOMapper userSummaryDTOMapper; public void sendFriendRequest(String senderEmail, Long receiverId) { User sender = @@ -123,6 +128,47 @@ private FriendRequest getFriendRequestById(Long requestId) { "Friend request with ID [%d] not found.".formatted(requestId))); } + public List getFriends(String email) { + User user = getUserByEmail(email, "User with email [%s] not found.".formatted(email)); + return user.getFriends().stream().map(userSummaryDTOMapper).toList(); + } + + public List getNonFriends(String requesterEmail) { + User requester = + getUserByEmail( + requesterEmail, + "User (requester) with email [%s] not found.".formatted(requesterEmail)); + + Set friends = requester.getFriends(); + + List allUsers = userRepository.findAll(); + List potentialFriends = + allUsers.stream() + .filter(user -> !user.getId().equals(requester.getId())) + .filter(user -> !friends.contains(user)) + .collect(Collectors.toList()); + + List sentOrReceivedRequests = + friendRequestRepository.findBySenderOrReceiverAndStatus( + requester, requester, RequestStatus.PENDING); + + Set requestedUserIds = + sentOrReceivedRequests.stream() + .map( + req -> { + if (req.getSender().equals(requester)) { + return req.getReceiver().getId(); + } else { + return req.getSender().getId(); + } + }) + .collect(Collectors.toSet()); + + potentialFriends.removeIf(user -> requestedUserIds.contains(user.getId())); + + return potentialFriends.stream().map(userSummaryDTOMapper).toList(); + } + private void validateNotSelfRequest(Long senderId, Long receiverId) { if (senderId.equals(receiverId)) { throw new IllegalArgumentException("Cannot send a friend request to yourself."); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 88ef1d3..ab6cef7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -9,6 +9,7 @@ import PrivateRoute from "./routes/PrivateRoute.jsx"; import { AuthProvider } from "./context/AuthContext"; import "./styles/App.css"; import EditProfile from "./pages/EditProfilePage.jsx"; +import FriendsPage from "./pages/FriendsPage.jsx"; function App() { return ( @@ -45,7 +46,7 @@ function App() { path="/friends" element={ - + } /> diff --git a/frontend/src/pages/FriendsPage.jsx b/frontend/src/pages/FriendsPage.jsx index 42e902d..c4e4291 100644 --- a/frontend/src/pages/FriendsPage.jsx +++ b/frontend/src/pages/FriendsPage.jsx @@ -1,68 +1,58 @@ import React, { useEffect, useState } from "react"; -import { - Box, - Heading, - VStack, - Text, - Button, - HStack, - Spinner, - Flex, -} from "@chakra-ui/react"; -import { - getPendingFriendRequests, - approveFriendRequest, - declineFriendRequest, -} from "../services/friendsService.js"; - +import { Box, Button, Flex, Spinner, Text, VStack } from "@chakra-ui/react"; import Navbar from "../components/layout/Navbar.jsx"; import Sidebar from "../components/layout/Sidebar.jsx"; import AlgBG from "../assets/alg_wd_blur.svg"; +import { getFriends, removeFriend } from "../services/friendsService.js"; -const RequestsPage = () => { - const [requests, setRequests] = useState([]); +const FriendsPage = () => { + const [friends, setFriends] = useState([]); const [loading, setLoading] = useState(true); + const [removing, setRemoving] = useState({}); - const fetchRequests = async () => { + useEffect(() => { + document.body.style.background = `url(${AlgBG})`; + document.body.style.backgroundSize = "cover"; + document.body.style.backgroundPosition = "center"; + return () => { + document.body.style.background = ""; + }; + }, []); + + const fetchFriends = async () => { + setLoading(true); try { - const res = await getPendingFriendRequests(); - setRequests(res.data || []); - } catch (err) { - alert("Error fetching requests."); + const res = await getFriends(); + setFriends(res?.data || []); + } catch (e) { + console.error("Failed to fetch friends", e); } finally { setLoading(false); } }; - const handleApprove = async (id) => { - try { - await approveFriendRequest(id); - alert("Friend request approved."); - fetchRequests(); - } catch (err) { - alert("Error approving request."); - } - }; + useEffect(() => { + fetchFriends(); + }, []); - const handleDecline = async (id) => { + const handleRemoveFriend = async (id) => { + setRemoving((prev) => ({ ...prev, [id]: true })); try { - await declineFriendRequest(id); - alert("Friend request declined."); - fetchRequests(); - } catch (err) { - alert("Error declining request."); + await removeFriend(id); + setFriends((prev) => prev.filter((f) => f.id !== id)); + alert("Friend removed."); + } catch (e) { + console.error("Failed to remove friend", e); + alert("Failed to remove friend."); + } finally { + setRemoving((prev) => ({ ...prev, [id]: false })); } }; - useEffect(() => { - fetchRequests(); - }, []); - - if (loading) return ; - return ( <> + { maxW="1580px" height="80vh" > - + - - - Pending Friend Requests - - - {requests.length === 0 ? ( - No pending friend requests - ) : ( - requests.map((req) => ( - - - - {req.senderFullName || "Unknown User"} - - - + + {loading ? ( + + ) : ( + + {friends.length === 0 ? ( + No friends found. + ) : ( + friends.map((friend) => ( + + + + + {friend.firstName} {friend.lastName} + + + {friend.email} + + + - - - - )) - )} - + + + )) + )} + + )} @@ -136,4 +130,4 @@ const RequestsPage = () => { ); }; -export default RequestsPage; +export default FriendsPage; diff --git a/frontend/src/pages/StudentsPage.jsx b/frontend/src/pages/StudentsPage.jsx index 15c09cf..a27e020 100644 --- a/frontend/src/pages/StudentsPage.jsx +++ b/frontend/src/pages/StudentsPage.jsx @@ -1,18 +1,9 @@ import React, { useEffect, useState } from "react"; -import { - Box, - Button, - Flex, - Heading, - Spinner, - Text, - VStack, -} from "@chakra-ui/react"; +import { Box, Button, Flex, Spinner, Text, VStack } from "@chakra-ui/react"; import Navbar from "../components/layout/Navbar.jsx"; import Sidebar from "../components/layout/Sidebar.jsx"; import AlgBG from "../assets/alg_wd_blur.svg"; -import { sendFriendRequest } from "../services/friendsService"; -import { getAllUsers } from "../services/usersService"; +import { sendFriendRequest, getNonFriends } from "../services/friendsService"; const StudentsPage = () => { const [students, setStudents] = useState([]); @@ -28,17 +19,18 @@ const StudentsPage = () => { }; }, []); + const fetchStudents = async () => { + try { + const res = await getNonFriends(); + setStudents(res?.data || []); + } catch (e) { + console.error("Failed to fetch non-friends", e); + } finally { + setLoading(false); + } + }; + useEffect(() => { - const fetchStudents = async () => { - try { - const res = await getAllUsers(); - setStudents(res.data.content || []); - } catch (e) { - console.error("Failed to fetch students", e); - } finally { - setLoading(false); - } - }; fetchStudents(); }, []); @@ -47,6 +39,7 @@ const StudentsPage = () => { try { await sendFriendRequest(id); alert("Friend request sent."); + fetchStudents(); // Refresh list } catch (e) { console.error("Error sending friend request", e); alert("Failed to send request."); @@ -84,7 +77,7 @@ const StudentsPage = () => { ) : ( {students.length === 0 ? ( - No students found. + No users to add. ) : ( students.map((student) => ( {