Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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")
Expand Down Expand Up @@ -48,4 +44,14 @@ public ResponseEntity<Void> removeFriend(@PathVariable Long userId, Principal pr
public ResponseEntity<List<FriendRequestDTO>> getPendingRequests(Principal principal) {
return ResponseEntity.ok(friendService.getPendingRequests(principal.getName()));
}

@GetMapping("/all")
public ResponseEntity<List<UserSummaryDTO>> getFriends(Principal principal) {
return ResponseEntity.ok(friendService.getFriends(principal.getName()));
}

@GetMapping("/non-friends")
public ResponseEntity<List<UserSummaryDTO>> getNonFriends(Principal principal) {
return ResponseEntity.ok(friendService.getNonFriends(principal.getName()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,15 @@ boolean existsBySenderAndReceiverAndStatus(
""")
List<FriendRequest> 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<FriendRequest> findBySenderOrReceiverAndStatus(
@Param("requester") User requester,
@Param("receiver") User receiver, // Can be omitted if unused
@Param("requestStatus") RequestStatus requestStatus);
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
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;
import hr.algebra.socialnetwork.repository.FriendRequestRepository;
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;

Expand All @@ -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 =
Expand Down Expand Up @@ -123,6 +128,47 @@ private FriendRequest getFriendRequestById(Long requestId) {
"Friend request with ID [%d] not found.".formatted(requestId)));
}

public List<UserSummaryDTO> getFriends(String email) {
User user = getUserByEmail(email, "User with email [%s] not found.".formatted(email));
return user.getFriends().stream().map(userSummaryDTOMapper).toList();
}

public List<UserSummaryDTO> getNonFriends(String requesterEmail) {
User requester =
getUserByEmail(
requesterEmail,
"User (requester) with email [%s] not found.".formatted(requesterEmail));

Set<User> friends = requester.getFriends();

List<User> allUsers = userRepository.findAll();
List<User> potentialFriends =
allUsers.stream()
.filter(user -> !user.getId().equals(requester.getId()))
.filter(user -> !friends.contains(user))
.collect(Collectors.toList());

List<FriendRequest> sentOrReceivedRequests =
friendRequestRepository.findBySenderOrReceiverAndStatus(
requester, requester, RequestStatus.PENDING);

Set<Long> 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.");
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -45,7 +46,7 @@ function App() {
path="/friends"
element={
<PrivateRoute>
<StudentsPage />
<FriendsPage />
</PrivateRoute>
}
/>
Expand Down
172 changes: 83 additions & 89 deletions frontend/src/pages/FriendsPage.jsx
Original file line number Diff line number Diff line change
@@ -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 <Spinner size="xl" mt={10} />;

return (
<>
<Navbar />

<Flex
justify="center"
align="center"
Expand All @@ -78,62 +68,66 @@ const RequestsPage = () => {
maxW="1580px"
height="80vh"
>
<Box width={{ base: "100%", md: "20%" }} marginRight={5}>
<Box width={{ base: "100%", md: "20%" }} mr={5}>
<Sidebar />
</Box>

<Box flex="1" p={6} overflowY="auto">
<Heading size="lg" mb={4} color="white">
Pending Friend Requests
</Heading>
<VStack spacing={4} align="stretch">
{requests.length === 0 ? (
<Text color="white">No pending friend requests</Text>
) : (
requests.map((req) => (
<Box
key={req.id}
p={4}
shadow="md"
borderWidth="1px"
rounded="md"
bg="whiteAlpha.800"
>
<HStack justifyContent="space-between">
<Text
fontWeight="bold"
bg="linear-gradient(45deg, var(--alg-gradient-color-1), var(--alg-gradient-color-2))"
bgClip="text"
color="transparent"
>
{req.senderFullName || "Unknown User"}
</Text>
<HStack>
<Button
colorScheme="green"
size="sm"
onClick={() => handleApprove(req.id)}
>
Approve
</Button>
<Box flex="1" overflowY="auto">
{loading ? (
<Spinner size="xl" mt={10} />
) : (
<VStack spacing={4}>
{friends.length === 0 ? (
<Text>No friends found.</Text>
) : (
friends.map((friend) => (
<Box
key={friend.id}
p={4}
width="100%"
shadow="md"
borderWidth="1px"
bg="whiteAlpha.800"
rounded="lg"
>
<Flex justifyContent="space-between" alignItems="center">
<Box>
<Text
fontWeight="bold"
bg="linear-gradient(45deg, var(--alg-gradient-color-1), var(--alg-gradient-color-2))"
bgClip="text"
color="transparent"
>
{friend.firstName} {friend.lastName}
</Text>
<Text fontSize="sm" color="gray.600">
{friend.email}
</Text>
</Box>

<Button
colorScheme="red"
style={{
background:
"linear-gradient(45deg, #f56565, #e53e3e)",
color: "white",
}}
size="sm"
onClick={() => handleDecline(req.id)}
isLoading={removing[friend.id]}
onClick={() => handleRemoveFriend(friend.id)}
>
Decline
Unfriend
</Button>
</HStack>
</HStack>
</Box>
))
)}
</VStack>
</Flex>
</Box>
))
)}
</VStack>
)}
</Box>
</Flex>
</Flex>
</>
);
};

export default RequestsPage;
export default FriendsPage;
Loading