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
Expand Up @@ -32,6 +32,15 @@ public ResponseEntity<Page<PostDTO>> getAllPosts(
return ResponseEntity.ok(postService.getAllPosts(pageable));
}

@GetMapping("/friends")
public ResponseEntity<Page<PostDTO>> getFriendsPosts(
Principal principal,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
return ResponseEntity.ok(postService.getFriendsPosts(principal.getName(), pageable));
}

@GetMapping("/{id}")
public ResponseEntity<PostDTO> getPostById(@PathVariable Long id) {
return ResponseEntity.ok(postService.getPostById(id));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package hr.algebra.socialnetwork.repository;

import hr.algebra.socialnetwork.model.Post;
import hr.algebra.socialnetwork.model.User;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
Expand Down Expand Up @@ -32,4 +35,10 @@ SELECT COUNT(p) > 0 FROM Post p
SELECT p FROM Post p
""")
List<Post> findAll();

@Query("""
SELECT p FROM Post p
WHERE p.user IN :friends
""")
Page<Post> findAllByUserIn(@Param("friends") List<User> friends, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import hr.algebra.socialnetwork.s3.S3Service;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
Expand Down Expand Up @@ -211,4 +213,21 @@ public byte[] getPostImage(Long postId) {
String key = "post-images/%s/%s.jpg".formatted(postId, post.getImageId());
return s3Service.getObject(key);
}

public Page<PostDTO> getFriendsPosts(String requesterEmail, Pageable pageable) {
User requester =
userRepository
.findByEmail(requesterEmail)
.orElseThrow(() -> new ResourceNotFoundException("User not found: " + requesterEmail));

Set<User> friendSet = requester.getFriends();
if (friendSet.isEmpty()) {
return Page.empty(pageable);
}

List<User> friends = new ArrayList<>(friendSet);

Page<Post> posts = postRepository.findAllByUserIn(friends, pageable);
return posts.map(postDTOMapper);
}
}
8 changes: 8 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ function App() {
</PrivateRoute>
}
/>
<Route
path="/friends"
element={
<PrivateRoute>
<StudentsPage />
</PrivateRoute>
}
/>
<Route
path="/profile"
element={
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/layout/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ function Navbar() {
{ label: "Profile", path: "/profile" },
{ label: "Students", path: "/students" },
{ label: "Requests", path: "/requests" },
{ label: "Friends", path: "/friends" },
{ label: "Logout", action: "logout" },
];

Expand Down
139 changes: 139 additions & 0 deletions frontend/src/pages/FriendsPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
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 Navbar from "../components/layout/Navbar.jsx";
import Sidebar from "../components/layout/Sidebar.jsx";
import AlgBG from "../assets/alg_wd_blur.svg";

const RequestsPage = () => {
const [requests, setRequests] = useState([]);
const [loading, setLoading] = useState(true);

const fetchRequests = async () => {
try {
const res = await getPendingFriendRequests();
setRequests(res.data || []);
} catch (err) {
alert("Error fetching requests.");
} finally {
setLoading(false);
}
};

const handleApprove = async (id) => {
try {
await approveFriendRequest(id);
alert("Friend request approved.");
fetchRequests();
} catch (err) {
alert("Error approving request.");
}
};

const handleDecline = async (id) => {
try {
await declineFriendRequest(id);
alert("Friend request declined.");
fetchRequests();
} catch (err) {
alert("Error declining request.");
}
};

useEffect(() => {
fetchRequests();
}, []);

if (loading) return <Spinner size="xl" mt={10} />;

return (
<>
<Navbar />
<Flex
justify="center"
align="center"
bg={`url(${AlgBG})`}
bgRepeat="no-repeat"
bgSize="cover"
backgroundPosition="center"
h="95vh"
>
<Flex
direction={{ base: "column", md: "row" }}
width="100%"
maxW="1580px"
height="80vh"
>
<Box width={{ base: "100%", md: "20%" }} marginRight={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>
<Button
colorScheme="red"
size="sm"
onClick={() => handleDecline(req.id)}
>
Decline
</Button>
</HStack>
</HStack>
</Box>
))
)}
</VStack>
</Box>
</Flex>
</Flex>
</>
);
};

export default RequestsPage;
70 changes: 50 additions & 20 deletions frontend/src/pages/HomePage.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import React, { useEffect, useState } from "react";
import { Flex, Box } from "@chakra-ui/react";
import { Flex, Box, Button, HStack } from "@chakra-ui/react";
import Navbar from "../components/layout/Navbar.jsx";
import Sidebar from "../components/layout/Sidebar.jsx";
import PostFeed from "../components/posts/PostFeed.jsx";
import PostItem from "../components/posts/PostItem.jsx";
import { getAllPosts } from "../services/postsService.js";
import { getAllPosts, getFriendsPosts } from "../services/postsService.js";

function HomePage() {
const [posts, setPosts] = useState([]);
const [showFriendsPosts, setShowFriendsPosts] = useState(false);

const fetchPosts = async () => {
try {
const response = await getAllPosts();
const response = showFriendsPosts
? await getFriendsPosts()
: await getAllPosts();
setPosts(response?.data?.content || []);
} catch (err) {
console.error("Failed to load posts:", err);
Expand All @@ -20,7 +23,14 @@ function HomePage() {

useEffect(() => {
fetchPosts();
}, []);
}, [showFriendsPosts]);

const handlePostCreated = () => {
fetchPosts();
};

const activeButtonBg = "#3182CE";
const inactiveButtonBg = "#EDF2F7";

return (
<>
Expand All @@ -46,24 +56,44 @@ function HomePage() {
<Sidebar />
</Box>

<Box
flex="1"
bg="white"
borderRadius="lg"
boxShadow="lg"
border="1px solid rgba(255,255,255,0.2)"
minH="80vh"
overflowY="auto"
className="feed-scroll"
p={6}
>
<PostFeed onPostCreated={fetchPosts} />
<Box flex="1" minH="80vh">
{/* Post Feed (Create new post) */}
<Box
bg="white"
borderRadius="lg"
boxShadow="lg"
border="1px solid rgba(255,255,255,0.2)"
p={6}
mb={4}
>
<PostFeed onPostCreated={handlePostCreated} />
</Box>

{/* Toggle Buttons */}
<HStack mb={4} spacing={4}>
<Button
colorScheme="blue"
bg={!showFriendsPosts ? activeButtonBg : inactiveButtonBg}
color={!showFriendsPosts ? "white" : "black"}
onClick={() => setShowFriendsPosts(false)}
>
All posts
</Button>
<Button
colorScheme="blue"
bg={showFriendsPosts ? activeButtonBg : inactiveButtonBg}
color={showFriendsPosts ? "white" : "black"}
onClick={() => setShowFriendsPosts(true)}
>
Friends posts
</Button>
</HStack>

{/* Posts Feed */}
<Box
mt={6}
maxH="calc(100vh - 200px)"
overflowY="auto"
pr={2}
className="feed-scroll"
maxH="calc(100vh - 300px)"
overflowY="auto"
>
{posts.map((post) => (
<PostItem key={post.id} post={post} />
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/services/postsService.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ export const getAllPosts = async (page = 0, size = 10) => {
}
};

export const getFriendsPosts = async (page = 0, size = 10) => {
try {
return await axios.get(
`${API_BASE}/api/v1/posts/friends?page=${page}&size=${size}`,
getAuthConfig(),
);
} catch (e) {
console.error(`Error: ${e}`);
}
};

export const getPostById = async (id) => {
try {
return await axios.get(`${API_BASE}/api/v1/posts/${id}`, getAuthConfig());
Expand Down