diff --git a/.gitignore b/.gitignore index c64b53754..ce53ff50a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ dependency-reduced-pom.xml .factorypath .project .settings/ +application.yml +.DS_Store diff --git a/README.md b/README.md index 395d736fa..969266918 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Acebook +https://trello.com/b/VidgaAkH/acebook-engineering-project-2 + The application uses: - `maven` to build the project - `thymeleaf` for templating diff --git a/pom.xml b/pom.xml index 32aeb8fc4..9ca00565e 100644 --- a/pom.xml +++ b/pom.xml @@ -64,6 +64,7 @@ org.projectlombok lombok + 1.18.32 provided @@ -102,6 +103,7 @@ org.springframework.boot spring-boot-maven-plugin + 3.3.2 org.apache.maven.plugins @@ -114,6 +116,7 @@ org.projectlombok lombok + 1.18.32 diff --git a/src/main/.DS_Store b/src/main/.DS_Store new file mode 100644 index 000000000..af482a9f4 Binary files /dev/null and b/src/main/.DS_Store differ diff --git a/src/main/java/.DS_Store b/src/main/java/.DS_Store new file mode 100644 index 000000000..de3a99e1e Binary files /dev/null and b/src/main/java/.DS_Store differ diff --git a/src/main/java/com/.DS_Store b/src/main/java/com/.DS_Store new file mode 100644 index 000000000..4ed73db6d Binary files /dev/null and b/src/main/java/com/.DS_Store differ diff --git a/src/main/java/com/makersacademy/.DS_Store b/src/main/java/com/makersacademy/.DS_Store new file mode 100644 index 000000000..6700e0985 Binary files /dev/null and b/src/main/java/com/makersacademy/.DS_Store differ diff --git a/src/main/java/com/makersacademy/acebook/.DS_Store b/src/main/java/com/makersacademy/acebook/.DS_Store new file mode 100644 index 000000000..78797ea11 Binary files /dev/null and b/src/main/java/com/makersacademy/acebook/.DS_Store differ diff --git a/src/main/java/com/makersacademy/acebook/config/SecurityConfiguration.java b/src/main/java/com/makersacademy/acebook/config/SecurityConfiguration.java index 542bce54c..25f4674f9 100644 --- a/src/main/java/com/makersacademy/acebook/config/SecurityConfiguration.java +++ b/src/main/java/com/makersacademy/acebook/config/SecurityConfiguration.java @@ -33,7 +33,10 @@ public SecurityFilterChain configure(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/", "/images/**").permitAll() + .requestMatchers("/", + "/main.css", + "/images/**", + "/favicon.png").permitAll() .anyRequest().authenticated() ) .oauth2Login(oauth2 -> oauth2 diff --git a/src/main/java/com/makersacademy/acebook/config/WebConfig.java b/src/main/java/com/makersacademy/acebook/config/WebConfig.java new file mode 100644 index 000000000..99fe743bd --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/config/WebConfig.java @@ -0,0 +1,24 @@ +package com.makersacademy.acebook.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// This class customises how the app serves static resources (e.g. images) from the file system +// Resource handlers map URL paths to filesystem directories so static files (e.g. images) can be served via HTTP +// This is required for image storage outside the classpath (src) on uploading + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + // Handler for profile images + registry.addResourceHandler("/uploads/user_profile/**") + .addResourceLocations("file:uploads/user_profile/"); + + // Handler for post images + registry.addResourceHandler("/uploads/post_images/**") + .addResourceLocations("file:uploads/post_images/"); + } +} diff --git a/src/main/java/com/makersacademy/acebook/controller/CommentController.java b/src/main/java/com/makersacademy/acebook/controller/CommentController.java new file mode 100644 index 000000000..e19af5560 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/CommentController.java @@ -0,0 +1,83 @@ +package com.makersacademy.acebook.controller; + +import com.makersacademy.acebook.dto.CommentRequest; +import com.makersacademy.acebook.model.Comment; +import com.makersacademy.acebook.model.Post; +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.CommentRepository; +import com.makersacademy.acebook.repository.NotificationRepository; +import com.makersacademy.acebook.repository.PostRepository; +import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.CommentService; +import com.makersacademy.acebook.service.NotificationService; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.view.RedirectView; + +import java.util.List; +import jakarta.transaction.Transactional; + + +@Controller +@RequestMapping("/posts/comments") +public class CommentController { + + private final CommentService commentService; + private final PostRepository postRepository; + private final UserRepository userRepository; + private final NotificationService notificationService; + private final CommentRepository commentRepository; + + public CommentController(CommentService commentService, PostRepository postRepository, UserRepository userRepository,NotificationRepository notificationRepository, NotificationService notificationService, CommentRepository commentRepository) { + this.commentService = commentService; + this.postRepository = postRepository; + this.userRepository = userRepository; + this.notificationService = notificationService; + this.commentRepository = commentRepository; + } + + + // Show comments for specific post + @GetMapping("/post/{postId}") + @ResponseBody + public List getCommentsByPost(@PathVariable Long postId) { + return commentService.getCommentsForPost(postId); + } + + // This will POST to add a comment and then redirect back to the post with the updated comment for that post + @PostMapping + public RedirectView createComment(@ModelAttribute CommentRequest request, + @AuthenticationPrincipal(expression = "attributes['email']") String email) { + Post post = postRepository.findById(request.getPostId()) + .orElseThrow(() -> new RuntimeException("Post not found")); + + User user = userRepository.findUserByUsername(email) + .orElseThrow(() -> new RuntimeException("User not found")); + + Comment comment = new Comment(); + comment.setPost(post); + comment.setUser(user); + comment.setContent(request.getContent()); + + commentService.addComment(comment); + notificationService.newNotification(user.getId(), "comment", comment, null, null); + return new RedirectView("/posts/" + post.getId()); + } + + + @Transactional + @PostMapping("/{commentId}/delete") + public RedirectView deleteComment(@PathVariable Long commentId, + @RequestParam Long postId, + @AuthenticationPrincipal(expression = "attributes['email']") String email) { + + if (userRepository.findUserByUsername(email).get().getId() == commentRepository.findById(commentId).get().getUser().getId()) { + commentService.deleteComment(commentId); + return new RedirectView("/posts/" + postId); + } else { + return new RedirectView("/profile"); + } + } +} + diff --git a/src/main/java/com/makersacademy/acebook/controller/CommentLikeController.java b/src/main/java/com/makersacademy/acebook/controller/CommentLikeController.java new file mode 100644 index 000000000..f80992a99 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/CommentLikeController.java @@ -0,0 +1,36 @@ +package com.makersacademy.acebook.controller; + +import com.makersacademy.acebook.service.CommentLikeService; +import jakarta.transaction.Transactional; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.view.RedirectView; + +@Controller +@RequestMapping("/posts/comments") +public class CommentLikeController { + private final CommentLikeService commentLikeService; + public CommentLikeController(CommentLikeService commentLikeService) { + this.commentLikeService = commentLikeService; + } + + @Transactional + @PostMapping("/{commentId}/like") + public RedirectView likeComment(@PathVariable Long commentId, + @RequestParam Long postId, + @AuthenticationPrincipal(expression = "attributes['email']") String email) { + commentLikeService.likeComment(commentId, email); + return new RedirectView("/posts/" + postId); + } + + @Transactional + @PostMapping("/{commentId}/unlike") + public RedirectView unlikeComment(@PathVariable Long commentId, + @RequestParam Long postId, + @AuthenticationPrincipal(expression = "attributes['email']") String email) { + commentLikeService.unlikeComment(commentId, email); + return new RedirectView("/posts/" + postId); + } +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/controller/FriendsController.java b/src/main/java/com/makersacademy/acebook/controller/FriendsController.java new file mode 100644 index 000000000..0c177836b --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/FriendsController.java @@ -0,0 +1,231 @@ +package com.makersacademy.acebook.controller; + +import com.makersacademy.acebook.model.Friend; +import com.makersacademy.acebook.model.FriendRequest; +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.FriendRepository; +import com.makersacademy.acebook.repository.FriendRequestRepository; +import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.NotificationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.RedirectView; + +import java.sql.Timestamp; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +@Controller +public class FriendsController { + + @Autowired + FriendRequestRepository friendRequestRepository; + @Autowired + FriendRepository friendRepository; + @Autowired + UserRepository userRepository; + @Autowired + NotificationService notificationService; + + @GetMapping("/friends") + public ModelAndView friendList(@AuthenticationPrincipal(expression = "attributes['email']") String email) { + ModelAndView modelAndView = new ModelAndView("friends/friends"); + + // User! + Optional userOptional = userRepository.findUserByUsername(email); + + User currentUser = userOptional.get(); + Long userId = currentUser.getId(); + + // Friends! + List friendsList = friendRepository.findAllByMainUserId(userId); + + // Objectify! + List friendUsers = new ArrayList<>(); + for (Friend friend : friendsList) { + Long friendId = friend.getFriendUserId(); + Optional friendUser = userRepository.findById(friendId); + if (friendUser.isPresent()) { + friendUsers.add(friendUser.get()); + } + } + + // Sort the users into alphabetical order! + friendUsers.sort(Comparator.comparing(User::getFirstName)); + + + // Friend Requests! + List pendingRequests = friendRequestRepository.findAllByReceiverIdAndStatusOrderByCreatedAtDesc(userId, "pending"); + + + // Objectify! + List requesterUsers = new ArrayList<>(); + for (FriendRequest request : pendingRequests) { + Long requesterId = request.getRequesterId(); + Optional requesterUser = userRepository.findById(requesterId); + if (requesterUser.isPresent()) { + requesterUsers.add(requesterUser.get()); + } + } + + + // Get notifications count for navbar + Integer notificationCount = notificationService.notificationCount(currentUser.getId()); + + modelAndView.addObject("notificationCount", notificationCount); + modelAndView.addObject("friendUsers", friendUsers); + modelAndView.addObject("requesterUsers", requesterUsers); + modelAndView.addObject("currentUser", currentUser); + + return modelAndView; + } + + + @PostMapping("/friend_request/{requesterId}") + public RedirectView respondToFriendRequest( + @PathVariable Long requesterId, + @RequestParam String decision, + @AuthenticationPrincipal(expression = "attributes['email']") String email) { + + // Get User! + Optional userOptional = userRepository.findUserByUsername(email); + + User currentUser = userOptional.get(); + Long currentUserId = currentUser.getId(); + + // Get Friend Request! + Optional friendRequestOptional = friendRequestRepository + .findByRequesterIdAndReceiverIdAndStatus(requesterId, currentUserId, "pending"); + + + FriendRequest friendRequest = friendRequestOptional.get(); + + if (decision.equals("accept")) { + friendRequest.setStatus("accepted"); + Instant instant = Instant.now(); + Timestamp now = Timestamp.from(instant); + friendRequest.setRespondedAt(now); + friendRequestRepository.save(friendRequest); + + + Friend friendship1 = new Friend(); + friendship1.setMainUserId(currentUserId); + friendship1.setFriendUserId(requesterId); + friendship1.setFriendsSince(now); + friendRepository.save(friendship1); + + Friend friendship2 = new Friend(); + friendship2.setMainUserId(requesterId); + friendship2.setFriendUserId(currentUserId); + friendship2.setFriendsSince(now); + friendRepository.save(friendship2); + + } else if (decision.equals("decline")) { + friendRequest.setStatus("rejected"); + Instant instant = Instant.now(); + Timestamp now = Timestamp.from(instant); + friendRequest.setRespondedAt(now); + friendRequestRepository.save(friendRequest); + } + + return new RedirectView("/friends"); + } + + @PostMapping("/remove_friend/{friendId}") + public RedirectView removeFriend( + @PathVariable Long friendId, + @AuthenticationPrincipal(expression = "attributes['email']") String email) { + + // Get User! + Optional userOptional = userRepository.findUserByUsername(email); + + User currentUser = userOptional.get(); + Long currentUserId = currentUser.getId(); + + //Find Friendships! + + Optional friendship1 = friendRepository.findByMainUserIdAndFriendUserId(currentUserId, friendId); + Optional friendship2 = friendRepository.findByMainUserIdAndFriendUserId(friendId, currentUserId); + + //Remove Friendships! + friendship1.ifPresent(friendRepository::delete); + friendship2.ifPresent(friendRepository::delete); + + //Redirect to Friends! + return new RedirectView("/friends"); + } + + @PostMapping("/add_friend/{userId}") + public RedirectView addFriend( + @PathVariable Long userId, + @AuthenticationPrincipal(expression = "attributes['email']") String email) { + + // Get User! + Optional userOptional = userRepository.findUserByUsername(email); + + User currentUser = userOptional.get(); + Long currentUserId = currentUser.getId(); + + Optional requestee = userRepository.findById(userId); + + User requesteeUser = requestee.get(); + Long requesteeId = requesteeUser.getId(); + + FriendRequest request = new FriendRequest(); + request.setRequesterId(currentUserId); + request.setReceiverId(requesteeId); + request.setStatus("pending"); // Set status + request.setCreatedAt(new Timestamp(System.currentTimeMillis())); // Set timestamp + friendRequestRepository.save(request); + + return new RedirectView("/profile/{userId}"); + } + + @GetMapping("/friends/{userId}") + public ModelAndView profileFriendList(@AuthenticationPrincipal(expression = "attributes['email']") String email, + @PathVariable("userId") Long id) { + ModelAndView modelAndView = new ModelAndView("friends/profile_friends"); + + // User! + Optional userOptional = userRepository.findUserByUsername(email); + User currentUser = userOptional.get(); + + // Profile User! + Optional profileUserOptional = userRepository.findById(id); + User profileUser = profileUserOptional.get(); + + // Friends! + List friendsList = friendRepository.findAllByMainUserId(id); + + // Objectify! + List friendUsers = new ArrayList<>(); + for (Friend friend : friendsList) { + Long friendId = friend.getFriendUserId(); + Optional friendUser = userRepository.findById(friendId); + if (friendUser.isPresent()) { + friendUsers.add(friendUser.get()); + } + } + + // Sort the users into alphabetical order! + friendUsers.sort(Comparator.comparing(User::getFirstName)); + + // Get notifications count for navbar + Integer notificationCount = notificationService.notificationCount(currentUser.getId()); + + modelAndView.addObject("notificationCount", notificationCount); + modelAndView.addObject("friendUsers", friendUsers); + modelAndView.addObject("currentUser", currentUser); + modelAndView.addObject("profileUser", profileUser); + + return modelAndView; + } + +} + diff --git a/src/main/java/com/makersacademy/acebook/controller/HomeController.java b/src/main/java/com/makersacademy/acebook/controller/HomeController.java index 2036ec7e0..1bad57528 100644 --- a/src/main/java/com/makersacademy/acebook/controller/HomeController.java +++ b/src/main/java/com/makersacademy/acebook/controller/HomeController.java @@ -2,12 +2,13 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.RedirectView; @Controller public class HomeController { @RequestMapping(value = "/") - public RedirectView index() { - return new RedirectView("/posts"); + public ModelAndView index() { + return new ModelAndView("/landing"); } } diff --git a/src/main/java/com/makersacademy/acebook/controller/NotificationsController.java b/src/main/java/com/makersacademy/acebook/controller/NotificationsController.java new file mode 100644 index 000000000..5685147f0 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/NotificationsController.java @@ -0,0 +1,57 @@ +package com.makersacademy.acebook.controller; + +import com.makersacademy.acebook.model.FriendRequest; +import com.makersacademy.acebook.model.Notification; +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.NotificationRepository; +import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.NotificationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.view.RedirectView; + +import java.util.*; + +@Controller +public class NotificationsController { + + @Autowired + NotificationRepository notificationRepository; + @Autowired + UserRepository userRepository; + @Autowired + NotificationService notificationService; + + + // Get notifications for current user + @GetMapping("/notifications") + public String index(Model notificationsPage, @AuthenticationPrincipal(expression = "attributes['email']") String email) { + User currentUser = userRepository.findUserByUsername(email) + .orElseThrow(() -> new RuntimeException("User not found")); + Integer notificationCount = notificationService.notificationCount(currentUser.getId()); + + Collection notifications = notificationService.getNotifications(currentUser.getId()); + Map senderNames = notificationService.getNotificationSenderNames(notifications); + List pendingFriendRequests = notificationService.getFriendRequests(currentUser.getId()); + + notificationsPage.addAttribute("pendingFriendRequests", pendingFriendRequests); + notificationsPage.addAttribute("notifications", notifications); + notificationsPage.addAttribute("senderNames", senderNames); + notificationsPage.addAttribute("currentUser", currentUser); + return "notifications/index"; + } + + + // Mark notification as read, redirect to specific post page + @PostMapping("/notifications") + public RedirectView markAsRead(@ModelAttribute Notification notification, @RequestParam String id) { + Long notificationId = Long.parseLong(id); + String postId = notificationService.readNotification(notificationId); + return new RedirectView("/posts/" + postId); + } +} diff --git a/src/main/java/com/makersacademy/acebook/controller/PostLikeController.java b/src/main/java/com/makersacademy/acebook/controller/PostLikeController.java new file mode 100644 index 000000000..840869b1e --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/PostLikeController.java @@ -0,0 +1,38 @@ +package com.makersacademy.acebook.controller; + +import com.makersacademy.acebook.service.PostLikeService; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.view.RedirectView; +import jakarta.transaction.Transactional; + + +@Controller +@RequestMapping("/posts") +public class PostLikeController { + private final PostLikeService postLikeService; + public PostLikeController(PostLikeService postLikeService) { + this.postLikeService = postLikeService; + } + + + @Transactional + @PostMapping("/{postId}/like") + public RedirectView likePost(@PathVariable Long postId, + @AuthenticationPrincipal(expression = "attributes['email']") String email) { + postLikeService.likePost(postId, email); + return new RedirectView("/posts/" + postId); + } + + + @Transactional + @PostMapping("/{postId}/unlike") + public RedirectView unlikePost(@PathVariable Long postId, + @AuthenticationPrincipal(expression = "attributes['email']") String email) { + postLikeService.unlikePost(postId, email); + return new RedirectView("/posts/" + postId); + } +} + diff --git a/src/main/java/com/makersacademy/acebook/controller/PostsController.java b/src/main/java/com/makersacademy/acebook/controller/PostsController.java index 57a7e5f4d..0dd51e55a 100644 --- a/src/main/java/com/makersacademy/acebook/controller/PostsController.java +++ b/src/main/java/com/makersacademy/acebook/controller/PostsController.java @@ -1,32 +1,244 @@ package com.makersacademy.acebook.controller; -import com.makersacademy.acebook.model.Post; +import com.makersacademy.acebook.dto.CommentDto; +import com.makersacademy.acebook.dto.PostDto; +import com.makersacademy.acebook.model.*; +import com.makersacademy.acebook.repository.FriendRepository; +import com.makersacademy.acebook.repository.PostLikeRepository; import com.makersacademy.acebook.repository.PostRepository; +import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.*; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.RedirectView; -import java.util.List; +import jakarta.transaction.Transactional; +import java.io.IOException; +import java.util.*; @Controller public class PostsController { @Autowired - PostRepository repository; + PostRepository postRepository; + @Autowired + PostLikeRepository postLikeRepository; + @Autowired + PostService postService; + @Autowired + PostLikeService postLikeService; + @Autowired + UserRepository userRepository; + @Autowired + ImageStorageService imageStorageService; + @Autowired + NotificationService notificationService; + @Autowired + FriendRepository friendRepository; + + private final CommentService commentService; + private final CommentLikeService commentLikeService; + + public PostsController(PostRepository postRepository, + UserRepository userRepository, + CommentService commentService, + CommentLikeService commentLikeService) { + this.postRepository = postRepository; + this.userRepository = userRepository; + this.commentService = commentService; + this.commentLikeService = commentLikeService; + } + @Value("${file.upload-dir.post-images}") + private String uploadDir; + + // View all posts @GetMapping("/posts") public String index(Model model) { - Iterable posts = repository.findAll(); + Iterable posts = postRepository.findByOrderByTimePostedDesc(); + + DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder + .getContext() + .getAuthentication() + .getPrincipal(); + + String username = (String) principal.getAttributes().get("email"); + User user = userRepository.findUserByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found")); + Integer notificationCount = notificationService.notificationCount(user.getId()); + Boolean globalWall = true; + + // ADD likeCounts and commentCounts + Map likeCounts = new HashMap<>(); + Map commentCounts = new HashMap<>(); + + for (Post post : posts) { + long postLikes = postService.getLikesCount(post.getId()); + likeCounts.put(post.getId(), postLikes); + + long postComments = commentService.getCommentsForPost(post.getId()).size(); + commentCounts.put(post.getId(), postComments); + } + model.addAttribute("posts", posts); model.addAttribute("post", new Post()); + model.addAttribute("likeCounts", likeCounts); + model.addAttribute("commentCounts", commentCounts); + model.addAttribute("user", user); + model.addAttribute("notificationCount", notificationCount); + model.addAttribute("globalWall", globalWall); + return "posts/index"; } + // View currentUsers friends' posts + @GetMapping("/posts/friends/{id}") + public String viewFriendsPosts(Model friendsWall, @AuthenticationPrincipal(expression = "attributes['email']") String email) { + User currentUser = userRepository.findUserByUsername(email) + .orElseThrow(() -> new RuntimeException("User not found")); + Integer notificationCount = notificationService.notificationCount(currentUser.getId()); + + List friendsPosts = postService.findFriendsPosts(currentUser.getId()); + Boolean globalWall = false; + + // ADD likeCounts and commentCounts + Map likeCounts = new HashMap<>(); + Map commentCounts = new HashMap<>(); + + for (Post post : friendsPosts) { + long postLikes = postService.getLikesCount(post.getId()); + likeCounts.put(post.getId(), postLikes); + + long postComments = commentService.getCommentsForPost(post.getId()).size(); + commentCounts.put(post.getId(), postComments); + } + + friendsWall.addAttribute("likeCounts", likeCounts); + friendsWall.addAttribute("commentCounts", commentCounts); + friendsWall.addAttribute("globalWall", globalWall); + friendsWall.addAttribute("posts", friendsPosts); + friendsWall.addAttribute("user", currentUser); + friendsWall.addAttribute("notificationCount", notificationCount); + friendsWall.addAttribute("post", new Post()); + + return "posts/index"; + } + + // Create new post @PostMapping("/posts") - public RedirectView create(@ModelAttribute Post post) { - repository.save(post); + public RedirectView create( + @RequestParam("globalWall") Boolean globalWall, + @RequestParam("content") String content, + @AuthenticationPrincipal(expression = "attributes['email']") String email, + @RequestParam("image") MultipartFile file + ) throws IOException { + User user = userRepository.findUserByUsername(email) + .orElseThrow(() -> new RuntimeException("User not found")); + Post post = new Post( + null, content, user.getId(), null, null, user); + post = postRepository.save(post); + + String fileName = imageStorageService.storePostImage(file, String.valueOf(post.getId())); + if (fileName != null) { + post.setImage(fileName); + postRepository.save(post); + } + + if (globalWall) { + return new RedirectView("/posts"); + } else { + return new RedirectView("/posts/friends/" + user.getId()); + } + } + + + @GetMapping("/posts/{id}") + public ModelAndView viewPost(@PathVariable("id") Long id, @AuthenticationPrincipal(expression = "attributes['email']") String email) { + ModelAndView modelAndView = new ModelAndView("posts/post"); + ModelAndView errorView = new ModelAndView("genericErrorPage"); + Optional currentPost = postRepository.findById(id); + if (currentPost.isEmpty()) { + return errorView; + } + else { + Post post = currentPost.get(); + String posterName = post.getUser().getFirstName() + " " + post.getUser().getLastName(); + long postLikesCount = postService.getLikesCount(post.getId()); + List likedBy = postLikeService.getLikersForPost(post.getId()); + PostDto postDto = new PostDto( + post.getId(), + post.getContent(), + posterName, + post.getTimePosted(), + post.getImage(), + postLikesCount, + likedBy, + post.getUser().getProfilePic(), + post.getUser().getId() + ); + List commentEntities = commentService.getCommentsForPost(id); + List commentDtos = commentEntities.stream() + .map(comment -> { + String displayName = comment.getUser().getFirstName() + " " + comment.getUser().getLastName(); + long likesCount = commentService.getLikesCount(comment.getId()); + List likers = commentLikeService.getLikersForComment(comment.getId()); + return new CommentDto( + comment.getId(), + comment.getContent(), + displayName, + comment.getCreatedAt(), + likesCount, + likers, + comment.getUser().getProfilePic(), + comment.getUser().getId() + ); + }) + .toList(); + + User currentUser = userRepository.findUserByUsername(email).orElse(null); + Integer notificationCount = notificationService.notificationCount(currentUser.getId()); + String userId = Long.toString(currentUser.getId()); + String userDisplayName = currentUser.getFirstName() + " " + currentUser.getLastName(); + + boolean likedBySignedInUser = likedByUser(userDisplayName, likedBy); + + modelAndView.addObject("notificationCount", notificationCount); + modelAndView.addObject("currentUser", currentUser); + modelAndView.addObject("userId", userId); + modelAndView.addObject("userDisplayName", userDisplayName); + modelAndView.addObject("post", postDto); + modelAndView.addObject("comments", commentDtos); + modelAndView.addObject("newComment", new Comment()); + modelAndView.addObject("likedBySignedInUser", likedBySignedInUser); + return modelAndView; + } + } + + @Transactional + @PostMapping("/posts/{postId}/delete") + public RedirectView deletePost(@PathVariable Long postId, + @AuthenticationPrincipal(expression = "attributes['email']") String email) { + if (Objects.equals(userRepository.findUserByUsername(email).get().getId(), postRepository.findById(postId).get().getUserId())) { + commentService.deleteCommentsByPostId(postId); + postService.deletePost(postId); + } return new RedirectView("/posts"); } + + private boolean likedByUser(String displayName, List likedBy){ + if (likedBy.contains(displayName)) { + return true; + } + else { + return false; + } + } } diff --git a/src/main/java/com/makersacademy/acebook/controller/ProfileController.java b/src/main/java/com/makersacademy/acebook/controller/ProfileController.java new file mode 100644 index 000000000..0d341f96f --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/ProfileController.java @@ -0,0 +1,180 @@ +package com.makersacademy.acebook.controller; + +import com.makersacademy.acebook.model.*; +import com.makersacademy.acebook.repository.FriendRepository; +import com.makersacademy.acebook.repository.FriendRequestRepository; +import com.makersacademy.acebook.repository.PostRepository; +import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.RedirectView; + +import java.sql.Timestamp; +import java.time.Instant; +import java.util.*; + +@Controller +public class ProfileController { + + @Autowired + PostService postService; + + @Autowired + CommentService commentService; + + @Autowired + PostRepository postRepository; + + @Autowired + UserRepository userRepository; + + @Autowired + FriendRepository friendRepository; + + @Autowired + FriendRequestRepository friendRequestRepository; + + @Autowired + NotificationService notificationService; + + @GetMapping("/profile/{userId}") + public ModelAndView profile(@PathVariable("userId") Long id) { + + // Get signed-in user from Auth0 + DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder + .getContext() + .getAuthentication() + .getPrincipal(); + + String username = (String) principal.getAttributes().get("email"); + User signedInUser = userRepository.findUserByUsername(username).get(); + Long userId = (Long) principal.getAttributes().get("id"); + User userForProfile = userRepository.findById(id).get(); + Iterable posts = postRepository.findByUserIdOrderByTimePostedDesc(id); + Iterable friendships = friendRepository.findAllByMainUserId(id); + + // Build friends list + List friends = new ArrayList<>(); + for (Friend friend : friendships) { + Long friendId = friend.getFriendUserId(); + Optional friendUser = userRepository.findById(friendId); + friendUser.ifPresent(friends::add); + } + + // Build likeCounts and commentCounts maps + Map likeCounts = new HashMap<>(); + Map commentCounts = new HashMap<>(); + for (Post post : posts) { + likeCounts.put(post.getId(), postService.getLikesCount(post.getId())); + commentCounts.put(post.getId(), (long) commentService.getCommentsForPost(post.getId()).size()); + } + + // Get notifications count for navbar + Integer notificationCount = notificationService.notificationCount(signedInUser.getId()); + + boolean isFriend = isFriend(signedInUser.getId(), userForProfile.getId()); + boolean incomingRequest = incomingFriendRequest(signedInUser.getId(), userForProfile.getId()); + boolean outgoingRequest = outgoingFriendRequest(signedInUser.getId(), userForProfile.getId()); + boolean pendingRequest = isFriendRequest(outgoingRequest, incomingRequest); + + // Build model + ModelAndView profile = new ModelAndView("users/profile"); + profile.addObject("notificationCount", notificationCount); + profile.addObject("userId", userId); + profile.addObject("user", userForProfile); + profile.addObject("signedInUser", signedInUser); + profile.addObject("posts", posts); + profile.addObject("friends", friends); + profile.addObject("isFriend", isFriend); + profile.addObject("pendingRequest", pendingRequest); + profile.addObject("incomingRequest", incomingRequest); + profile.addObject("outgoingRequest", outgoingRequest); + profile.addObject("likeCounts", likeCounts); + profile.addObject("commentCounts", commentCounts); + return profile; + } + + @PostMapping("/profile_friend_request/{requesterId}") + public RedirectView respondToFriendRequest( + @PathVariable Long requesterId, + @RequestParam String decision, + @AuthenticationPrincipal(expression = "attributes['email']") String email) { + + Optional userOptional = userRepository.findUserByUsername(email); + User currentUser = userOptional.get(); + Long currentUserId = currentUser.getId(); + + Optional friendRequestOptional = friendRequestRepository + .findByRequesterIdAndReceiverIdAndStatus(requesterId, currentUserId, "pending"); + FriendRequest friendRequest = friendRequestOptional.get(); + + if (decision.equals("accept")) { + friendRequest.setStatus("accepted"); + Instant instant = Instant.now(); + Timestamp now = Timestamp.from(instant); + friendRequest.setRespondedAt(now); + friendRequestRepository.save(friendRequest); + + Friend friendship1 = new Friend(); + friendship1.setMainUserId(currentUserId); + friendship1.setFriendUserId(requesterId); + friendship1.setFriendsSince(now); + friendRepository.save(friendship1); + + Friend friendship2 = new Friend(); + friendship2.setMainUserId(requesterId); + friendship2.setFriendUserId(currentUserId); + friendship2.setFriendsSince(now); + friendRepository.save(friendship2); + } else if (decision.equals("decline")) { + friendRequest.setStatus("rejected"); + Instant instant = Instant.now(); + Timestamp now = Timestamp.from(instant); + friendRequest.setRespondedAt(now); + friendRequestRepository.save(friendRequest); + } + + return new RedirectView("/profile/{requesterId}"); + } + + private boolean isFriend(Long userId, Long friendId) { + Iterable signedInFriendships = friendRepository.findAllByMainUserId(userId); + for (Friend friend : signedInFriendships) { + if (friend.getFriendUserId().equals(friendId)) { + return true; + } + } + return false; + } + + private boolean outgoingFriendRequest(Long userA, Long userB) { + Iterable pendingFriendRequests = friendRequestRepository.findAllByRequesterIdAndStatus(userA, "pending"); + for (FriendRequest request : pendingFriendRequests) { + if (request.getReceiverId().equals(userB)) { + return true; + } + } + return false; + } + + private boolean incomingFriendRequest(Long userA, Long userB) { + Iterable incomingPendingFriendRequests = friendRequestRepository.findAllByRequesterIdAndStatus(userB, "pending"); + for (FriendRequest request : incomingPendingFriendRequests) { + if (request.getReceiverId().equals(userA)) { + return true; + } + } + return false; + } + + private boolean isFriendRequest(Boolean sent, Boolean incoming) { + return sent && incoming; + } +} diff --git a/src/main/java/com/makersacademy/acebook/controller/UsersController.java b/src/main/java/com/makersacademy/acebook/controller/UsersController.java index a7c9db1d8..71171de0e 100644 --- a/src/main/java/com/makersacademy/acebook/controller/UsersController.java +++ b/src/main/java/com/makersacademy/acebook/controller/UsersController.java @@ -2,29 +2,164 @@ import com.makersacademy.acebook.model.User; import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.ImageStorageService; +import com.makersacademy.acebook.service.NotificationService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.repository.query.Param; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.RedirectView; +import java.io.IOException; +import java.util.List; +import java.util.Optional; + @RestController public class UsersController { @Autowired UserRepository userRepository; + @Autowired + private ImageStorageService imageStorageService; + + @Autowired + NotificationService notificationService; + + + // Inserts "uploads/user_profile" filepath (taken from application.properties) as uploadDir variable value + @Value("${file.upload-dir.user-profile}") + private String uploadDir; + + @GetMapping("/users/after-login") public RedirectView afterLogin() { + + // Creates principal variable of type Default0idcUser - Spring Security class which represents a user authenticated by Auth0 + // The get commands work together to extract user attributes from Auth0 (email, given_name, family_name) DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder .getContext() .getAuthentication() .getPrincipal(); + // Uses the principal variable above to extract email, given_name and family_name from Auth0 + // Sets a default profile picture String username = (String) principal.getAttributes().get("email"); + String first_name = (String) principal.getAttributes().get("given_name"); + String last_name = (String) principal.getAttributes().get("family_name"); + String profile_pic = "/images/profile/default.jpg"; + + // Uses the email address captured above to find the relevant user + // OR creates a new user if doesn't exist, utilizing the elements captured above userRepository .findUserByUsername(username) - .orElseGet(() -> userRepository.save(new User(username))); + .orElseGet(() -> userRepository.save(new User(username, first_name, last_name, profile_pic))); return new RedirectView("/posts"); } + + + @GetMapping("/settings") + public ModelAndView settings() { + + // Creates principal variable of type Default0idcUser - Spring Security class which represents a user authenticated by Auth0 + // The get commands work together to extract user attributes from Auth0 (email, given_name, family_name) + DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder + .getContext() + .getAuthentication() + .getPrincipal(); + + // Uses the principal variable above to extract email + // Then utilizes the 'username' variable to search the database and return matching User object + // Creates a new ModelandView and adds the User object to be referenced in Thymeleaf + String username = (String) principal.getAttributes().get("email"); + User userByEmail = userRepository.findUserByUsername(username).get(); + Optional user = userRepository.findById(userByEmail.getId()); + + // Get notification count for navbar + Integer notificationCount = notificationService.notificationCount(userByEmail.getId()); + + ModelAndView settings = new ModelAndView("/users/settings"); + settings.addObject("notificationCount", notificationCount); + settings.addObject("user", userByEmail); + return settings; + } + + + @PostMapping("/settings") + public RedirectView update(@ModelAttribute User userFromForm, @RequestParam("file") MultipartFile file) throws IOException { + + // Creates principal variable of type Default0idcUser - Spring Security class which represents a user authenticated by Auth0 + // The get commands work together to extract user attributes from Auth0 (email, given_name, family_name) + DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder + .getContext() + .getAuthentication() + .getPrincipal(); + + // Uses the principal variable above to extract email + String email = (String) principal.getAttributes().get("email"); + + // Creates a new User object representing the User extracted from the database, with matching email(username) + // Error mapping TBC + User userInDb = userRepository.findUserByUsername(email) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + + String profilePic = imageStorageService.storeProfileImage(file, String.valueOf(userInDb.getId())); + if (profilePic != null) { + userInDb.setProfilePic(profilePic); + } + + // Sets database user properties as values returned by User object from form + // The userFromForm is taken as an input to the function as a whole, generated by the form within the HTML template + userInDb.setFirstName(userFromForm.getFirstName()); + userInDb.setLastName(userFromForm.getLastName()); + + + // Save function will update, rather than create, if user already in existence - once userInDb attributes are + // updated, essentially overwrites existing name and image fields + userRepository.save(userInDb); + + return new RedirectView("/settings"); + } + + + // Shows user search page + @GetMapping("/friends/search") + public ModelAndView searchPage(@AuthenticationPrincipal(expression = "attributes['email']") String email) { + User currentUser = userRepository.findUserByUsername(email) + .orElseThrow(() -> new RuntimeException("User not found")); + Integer notificationCount = notificationService.notificationCount(currentUser.getId()); + + ModelAndView searchPage = new ModelAndView("friends/friendsSearch"); + + searchPage.addObject("notificationCount", notificationCount); + searchPage.addObject("currentUser", currentUser); + return searchPage; + } + + + // Search for friends via text input + @RequestMapping(value = "/friends/search", method = RequestMethod.POST) + public ModelAndView searchUsers(@RequestParam String searchInput, @AuthenticationPrincipal(expression = "attributes['email']") String email){ + User currentUser = userRepository.findUserByUsername(email) + .orElseThrow(() -> new RuntimeException("User not found")); + Integer notificationCount = notificationService.notificationCount(currentUser.getId()); + + ModelAndView searchPage = new ModelAndView("friends/friendsSearch"); + ModelAndView errorPage = new ModelAndView("genericErrorPage"); + + List searchResults = userRepository.findUsersBySearchInput(searchInput); + + searchPage.addObject("notificationCount", notificationCount); + searchPage.addObject("currentUser", currentUser); + searchPage.addObject("searchResults", searchResults); + searchPage.addObject("searchInput", searchInput); + return searchPage; + } } diff --git a/src/main/java/com/makersacademy/acebook/dto/CommentDto.java b/src/main/java/com/makersacademy/acebook/dto/CommentDto.java new file mode 100644 index 000000000..49f161310 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/dto/CommentDto.java @@ -0,0 +1,36 @@ +package com.makersacademy.acebook.dto; + + + +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Setter +public class CommentDto { + private Long id; + private String content; + private String displayName; + private LocalDateTime createdAt; + private long likesCount; + private List likers; + private String profilePic; + private Long commenterId; + + public CommentDto(Long id, String content, String displayName, + LocalDateTime createdAt, long likesCount, List likers, String profilePic, Long commenterId) { + this.id = id; + this.content = content; + this.displayName = displayName; + this.createdAt = createdAt; + this.likesCount = likesCount; + this.likers = likers; + this.profilePic = profilePic; + this.commenterId = commenterId; + } +} + + diff --git a/src/main/java/com/makersacademy/acebook/dto/CommentRequest.java b/src/main/java/com/makersacademy/acebook/dto/CommentRequest.java new file mode 100644 index 000000000..749ca67a6 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/dto/CommentRequest.java @@ -0,0 +1,17 @@ +package com.makersacademy.acebook.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter + + +public class CommentRequest { + private Long postId; + private Long userId; + private String content; + + +} + diff --git a/src/main/java/com/makersacademy/acebook/dto/PostDto.java b/src/main/java/com/makersacademy/acebook/dto/PostDto.java new file mode 100644 index 000000000..134871fa0 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/dto/PostDto.java @@ -0,0 +1,35 @@ +package com.makersacademy.acebook.dto; + +import lombok.Data; + +import java.sql.Timestamp; +import java.util.List; + + +@Data +public class PostDto { + private Long id; + private String content; + private String posterName; + private Timestamp timePosted; + private String image; + private Long postLikesCount; + private List likedBy; + private String profilePic; + private Long posterId; + + public PostDto(Long id, String content, String posterName, + Timestamp timePosted, String image, Long postLikesCount, List likedBy, String profilePic, Long posterId) { + this.id = id; + this.content = content; + this.posterName = posterName; + this.timePosted = timePosted; + this.image = image; + this.postLikesCount = postLikesCount; + this.likedBy = likedBy; + this.profilePic = profilePic; + this.posterId = posterId; + } +} + + diff --git a/src/main/java/com/makersacademy/acebook/model/Comment.java b/src/main/java/com/makersacademy/acebook/model/Comment.java new file mode 100644 index 000000000..f61482ed2 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/Comment.java @@ -0,0 +1,39 @@ +package com.makersacademy.acebook.model; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + + +@Data +@Entity +@Table(name = "comments") +@NoArgsConstructor +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name="post_id", insertable=false, updatable=false) + public Long postId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id", nullable = false) + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(nullable = false, columnDefinition = "TEXT") + private String content; + + @Column(name = "created_at") + private LocalDateTime createdAt = LocalDateTime.now(); + + public Comment(Long id) { + this.id = id; + } +} + diff --git a/src/main/java/com/makersacademy/acebook/model/CommentLike.java b/src/main/java/com/makersacademy/acebook/model/CommentLike.java new file mode 100644 index 000000000..034d3f8aa --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/CommentLike.java @@ -0,0 +1,25 @@ +package com.makersacademy.acebook.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "comment_likes", uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "comment_id"})) +@IdClass(CommentLikeId.class) +@Getter +@Setter +@NoArgsConstructor +public class CommentLike { + + @Id + @Column(name = "user_id") + private Long userId; + + @Id + @Column(name = "comment_id") + private Long commentId; + + +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/model/CommentLikeId.java b/src/main/java/com/makersacademy/acebook/model/CommentLikeId.java new file mode 100644 index 000000000..422aead38 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/CommentLikeId.java @@ -0,0 +1,31 @@ +package com.makersacademy.acebook.model; + +import java.io.Serializable; +import java.util.Objects; + +public class CommentLikeId implements Serializable { + + private Long userId; + private Long commentId; + + public CommentLikeId() {} + + public CommentLikeId(Long userId, Long commentId) { + this.userId = userId; + this.commentId = commentId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof CommentLikeId)) return false; + CommentLikeId that = (CommentLikeId) o; + return Objects.equals(userId, that.userId) && + Objects.equals(commentId, that.commentId); + } + + @Override + public int hashCode() { + return Objects.hash(userId, commentId); + } +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/model/Friend.java b/src/main/java/com/makersacademy/acebook/model/Friend.java new file mode 100644 index 000000000..a546e292d --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/Friend.java @@ -0,0 +1,35 @@ +package com.makersacademy.acebook.model; + +import jakarta.persistence.*; +import lombok.*; +import java.io.Serializable; +import java.sql.Timestamp; + +// Composite key class +@Data +@NoArgsConstructor +@AllArgsConstructor +class FriendId implements Serializable { + private Long mainUserId; + private Long friendUserId; +} + +@Data +@Entity +@Table(name = "friends") +@NoArgsConstructor +@AllArgsConstructor +@IdClass(FriendId.class) +public class Friend { + + @Id + @Column(name = "main_user_id") + private Long mainUserId; + + @Id + @Column(name = "friend_user_id") + private Long friendUserId; + + @Column(name = "friends_since") + private Timestamp friendsSince; +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/model/FriendRequest.java b/src/main/java/com/makersacademy/acebook/model/FriendRequest.java new file mode 100644 index 000000000..1b0e665ba --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/FriendRequest.java @@ -0,0 +1,33 @@ +package com.makersacademy.acebook.model; + +import jakarta.persistence.*; +import lombok.*; + +import java.sql.Timestamp; + +@Data +@Entity +@Table(name = "friend_requests") +@NoArgsConstructor +@AllArgsConstructor +public class FriendRequest { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "requester_id") + private Long requesterId; + + @Column(name = "receiver_id") + private Long receiverId; + + @Column(name = "status") + private String status; + + @Column(name = "created_at") + private Timestamp createdAt; + + @Column(name = "responded_at") + private Timestamp respondedAt; +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/model/Notification.java b/src/main/java/com/makersacademy/acebook/model/Notification.java new file mode 100644 index 000000000..6deac76b5 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/Notification.java @@ -0,0 +1,44 @@ +package com.makersacademy.acebook.model; + + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.DynamicUpdate; + +import java.sql.Timestamp; + +@Data +@Entity(name = "Notification") +@Table(name = "NOTIFICATIONS") +@NoArgsConstructor +@AllArgsConstructor +@DynamicUpdate +public class Notification { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name="receiving_user_id") + private Long receivingUserId; + + @Column(name="sending_user_id") + private Long sendingUserId; + + private String type; + + @Column(name= "post_id") + private Long postId; + + @Column(name="comment_id") + private Long commentId; + + @Column(name="is_read") + private boolean isRead; + + @CreationTimestamp + @Column(name="created_at") + private Timestamp createdAt; + +} diff --git a/src/main/java/com/makersacademy/acebook/model/Post.java b/src/main/java/com/makersacademy/acebook/model/Post.java index 33492c6b1..9bd3188d3 100644 --- a/src/main/java/com/makersacademy/acebook/model/Post.java +++ b/src/main/java/com/makersacademy/acebook/model/Post.java @@ -1,22 +1,45 @@ package com.makersacademy.acebook.model; +import org.hibernate.annotations.CreationTimestamp; + +import lombok.*; import jakarta.persistence.*; +import java.sql.Timestamp; -import lombok.Data; @Data @Entity @Table(name = "POSTS") -public class Post { +@AllArgsConstructor +@NoArgsConstructor +public class Post implements Comparable{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + private String content; - public Post() {} + @Column(name="user_id") + private Long userId; // current user is populated in PostController + + @CreationTimestamp + @Column(name="time_posted") + private Timestamp timePosted; + + private String image; - public Post(String content) { - this.content = content; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", insertable = false, updatable = false) + private User user; + + public Post(Long id) { + this.id = id; + } + + @Override + public int compareTo(Post p) { + return getTimePosted().compareTo(p.getTimePosted()); } } + diff --git a/src/main/java/com/makersacademy/acebook/model/PostLike.java b/src/main/java/com/makersacademy/acebook/model/PostLike.java new file mode 100644 index 000000000..ec2a392da --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/PostLike.java @@ -0,0 +1,23 @@ +package com.makersacademy.acebook.model; + +import jakarta.persistence.*; +import lombok.*; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "post_likes", uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "post_id"})) +@IdClass(PostLikeId.class) +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PostLike { + + @Id + @Column(name = "user_id") + private Long userId; + + @Id + @Column(name = "post_id") + private Long postId; + +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/model/PostLikeId.java b/src/main/java/com/makersacademy/acebook/model/PostLikeId.java new file mode 100644 index 000000000..76b3fc3eb --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/PostLikeId.java @@ -0,0 +1,31 @@ +package com.makersacademy.acebook.model; + +import java.io.Serializable; +import java.util.Objects; + +public class PostLikeId implements Serializable { + + private Long userId; + private Long postId; + + public PostLikeId() {} + + public PostLikeId(Long userId, Long postId) { + this.userId = userId; + this.postId = postId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostLikeId)) return false; + PostLikeId that = (PostLikeId) o; + return Objects.equals(userId, that.userId) && + Objects.equals(postId, that.postId); + } + + @Override + public int hashCode() { + return Objects.hash(userId, postId); + } +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/model/User.java b/src/main/java/com/makersacademy/acebook/model/User.java index 6013fbe23..3201195a1 100644 --- a/src/main/java/com/makersacademy/acebook/model/User.java +++ b/src/main/java/com/makersacademy/acebook/model/User.java @@ -1,7 +1,7 @@ package com.makersacademy.acebook.model; import jakarta.persistence.*; -import lombok.Data; +import lombok.*; import static java.lang.Boolean.TRUE; @@ -14,18 +14,36 @@ public class User { private Long id; private String username; private boolean enabled; + @Column(name = "firstName") + private String firstName; + @Column(name = "last_name") + private String lastName; + @Column(name = "profile_pic") + private String profilePic; - public User() { - this.enabled = TRUE; + public User(){ } public User(String username) { + this.enabled = TRUE; + } + + public User(String username, String firstName, String last_name, String profilePic) { this.username = username; this.enabled = TRUE; + this.firstName = firstName; + this.lastName = last_name; + this.profilePic = profilePic; } - public User(String username, boolean enabled) { + public User(String username, boolean enabled, String firstName, String last_name, String profilePic) { this.username = username; this.enabled = enabled; + this.firstName = firstName; + this.lastName = last_name; + this.profilePic = profilePic; + } + + public User(Long userId) { } -} +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/repository/CommentLikeRepository.java b/src/main/java/com/makersacademy/acebook/repository/CommentLikeRepository.java new file mode 100644 index 000000000..21ea7a385 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/CommentLikeRepository.java @@ -0,0 +1,16 @@ +package com.makersacademy.acebook.repository; +import com.makersacademy.acebook.model.CommentLike; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository + +public interface CommentLikeRepository extends JpaRepository { + Optional findByUserIdAndCommentId(Long userId, Long commentId); + List findByCommentId(Long commentId); + long countByCommentId(Long commentId); + void deleteByUserIdAndCommentId(Long userId, Long commentId); +} diff --git a/src/main/java/com/makersacademy/acebook/repository/CommentRepository.java b/src/main/java/com/makersacademy/acebook/repository/CommentRepository.java new file mode 100644 index 000000000..2623bbb06 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/CommentRepository.java @@ -0,0 +1,15 @@ +package com.makersacademy.acebook.repository; + +import com.makersacademy.acebook.model.Comment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.List; + +@Repository +public interface CommentRepository extends JpaRepository { + + + List findByPost_IdOrderByCreatedAtAsc(Long postId); + List findByUser_Id(Long userId); + void deleteAllByPostId(Long postId); +} diff --git a/src/main/java/com/makersacademy/acebook/repository/FriendRepository.java b/src/main/java/com/makersacademy/acebook/repository/FriendRepository.java new file mode 100644 index 000000000..014a77c13 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/FriendRepository.java @@ -0,0 +1,17 @@ +package com.makersacademy.acebook.repository; + +import com.makersacademy.acebook.model.Friend; +import com.makersacademy.acebook.model.User; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; + +import java.util.List; +import java.util.Optional; + + +public interface FriendRepository extends CrudRepository { + List findAllByMainUserId(Long mainUserId); + Optional findByMainUserIdAndFriendUserId(Long currentUserId, Long friendId); + @Query("SELECT a.friendUserId FROM Friend a WHERE a.mainUserId = :currentUserId") + List findFriendUserIdByMainUserId(Long currentUserId); +} diff --git a/src/main/java/com/makersacademy/acebook/repository/FriendRequestRepository.java b/src/main/java/com/makersacademy/acebook/repository/FriendRequestRepository.java new file mode 100644 index 000000000..d270b45ca --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/FriendRequestRepository.java @@ -0,0 +1,27 @@ +package com.makersacademy.acebook.repository; + +import com.makersacademy.acebook.model.FriendRequest; +import com.makersacademy.acebook.model.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; + +import java.util.List; +import java.util.Optional; + + +public interface FriendRequestRepository extends CrudRepository { + List findAllByReceiverIdAndStatus(Long receiverId, String status); + + Optional findByRequesterIdAndReceiverIdAndStatus(Long requesterId, Long currentUserId, String pending); + + List findAllByReceiverIdAndStatusOrderByCreatedAtDesc(Long userId, String pending); + + Integer countByReceiverIdAndStatus(Long userId, String pending); + + Iterable findAllByRequesterId(Long requesterId); + + Iterable findAllByReceiverId(Long receiverId); + + Iterable findAllByRequesterIdAndStatus(Long requesterId, String pending); + +} diff --git a/src/main/java/com/makersacademy/acebook/repository/NotificationRepository.java b/src/main/java/com/makersacademy/acebook/repository/NotificationRepository.java new file mode 100644 index 000000000..a24758f96 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/NotificationRepository.java @@ -0,0 +1,11 @@ +package com.makersacademy.acebook.repository; + +import com.makersacademy.acebook.model.Notification; +import org.springframework.data.repository.CrudRepository; +import java.util.List; + +public interface NotificationRepository extends CrudRepository { + List findAllByReceivingUserId(Long receivingUserId); + List findByReceivingUserIdOrderByCreatedAtDesc(Long receivingUserId); + Integer countByReceivingUserIdAndIsRead(Long receivingUserId, Boolean isRead); +} diff --git a/src/main/java/com/makersacademy/acebook/repository/PostLikeRepository.java b/src/main/java/com/makersacademy/acebook/repository/PostLikeRepository.java new file mode 100644 index 000000000..52ea29990 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/PostLikeRepository.java @@ -0,0 +1,16 @@ +package com.makersacademy.acebook.repository; +import com.makersacademy.acebook.model.PostLike; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository + +public interface PostLikeRepository extends JpaRepository { + Optional findByUserIdAndPostId(Long userId, Long postId); + List findByPostId(Long postId); + long countByPostId(Long postId); + void deleteByUserIdAndPostId(Long userId, Long postId); +} diff --git a/src/main/java/com/makersacademy/acebook/repository/PostRepository.java b/src/main/java/com/makersacademy/acebook/repository/PostRepository.java index d435e0ce1..0067fc2d0 100644 --- a/src/main/java/com/makersacademy/acebook/repository/PostRepository.java +++ b/src/main/java/com/makersacademy/acebook/repository/PostRepository.java @@ -1,9 +1,13 @@ package com.makersacademy.acebook.repository; import com.makersacademy.acebook.model.Post; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; +import java.util.List; + public interface PostRepository extends CrudRepository { + List findByUserIdOrderByTimePostedDesc(Long receivingUserId); + List findByOrderByTimePostedDesc(); + Iterable findAllByUserId(Long id); } diff --git a/src/main/java/com/makersacademy/acebook/repository/UserRepository.java b/src/main/java/com/makersacademy/acebook/repository/UserRepository.java index 4b4fd2b52..cba4d4d75 100644 --- a/src/main/java/com/makersacademy/acebook/repository/UserRepository.java +++ b/src/main/java/com/makersacademy/acebook/repository/UserRepository.java @@ -1,10 +1,21 @@ package com.makersacademy.acebook.repository; import com.makersacademy.acebook.model.User; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import java.util.List; import java.util.Optional; public interface UserRepository extends CrudRepository { + @Query("SELECT a FROM User a WHERE " + + "LOWER(CONCAT('%', a.firstName, ' ', a.lastName, '%')) LIKE LOWER(CONCAT('%', :searchInput, '%')) OR " + + "LOWER(CONCAT('%', a.lastName, ' ', a.firstName, '%')) LIKE LOWER(CONCAT('%', :searchInput, '%')) OR " + + "LOWER(CONCAT('%', a.lastName, a.firstName, '%')) LIKE LOWER(CONCAT('%', :searchInput, '%')) OR " + + "LOWER(CONCAT('%', a.firstName, a.lastName, '%')) LIKE LOWER(CONCAT('%', :searchInput, '%')) OR " + + "LOWER(CONCAT('%', a.username, '%')) LIKE LOWER(CONCAT('%', :searchInput, '%'))") + public List findUsersBySearchInput(@Param("searchInput") String searchInput); public Optional findUserByUsername(String username); + public List findUsersByFirstNameAndLastName(String firstName, String lastName); } diff --git a/src/main/java/com/makersacademy/acebook/service/CommentLikeService.java b/src/main/java/com/makersacademy/acebook/service/CommentLikeService.java new file mode 100644 index 000000000..977048442 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/service/CommentLikeService.java @@ -0,0 +1,69 @@ +package com.makersacademy.acebook.service; + +import com.makersacademy.acebook.model.CommentLike; +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.CommentLikeRepository; +import com.makersacademy.acebook.repository.UserRepository; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + + +@Service +public class CommentLikeService { + + private final CommentLikeRepository commentLikeRepository; + private final UserRepository userRepository; + private final NotificationService notificationService; + + public CommentLikeService(CommentLikeRepository commentLikeRepository, UserRepository userRepository, NotificationService notificationService) { + this.commentLikeRepository = commentLikeRepository; + this.userRepository = userRepository; + this.notificationService = notificationService; + } + + + public void likeComment(Long commentId, String userEmail) { + User user = userRepository.findUserByUsername(userEmail) + .orElseThrow(() -> new RuntimeException("User not found")); + + Optional existingLike = commentLikeRepository.findByUserIdAndCommentId(user.getId(), commentId); + if (existingLike.isEmpty()) { + CommentLike like = new CommentLike(); + like.setUserId(user.getId()); + like.setCommentId(commentId); + commentLikeRepository.save(like); + notificationService.newNotification(user.getId(), "commentLike", null, null, like); + } + + } + + + public long getLikesCount(Long commentId) { + return commentLikeRepository.countByCommentId(commentId); + } + + + public void unlikeComment(Long commentId, String userEmail) { + User user = userRepository.findUserByUsername(userEmail) + .orElseThrow(() -> new RuntimeException("User not found")); + + commentLikeRepository.deleteByUserIdAndCommentId(user.getId(), commentId); + } + + + public List getLikersForComment(Long commentId) { + List likes = commentLikeRepository.findByCommentId(commentId); + return likes.stream() + .map(like -> userRepository.findById(like.getUserId())) + .filter(Optional::isPresent) + .map(user -> { + User u = user.get(); + return u.getFirstName() + " " + u.getLastName(); + }) + .toList(); + } + +} + diff --git a/src/main/java/com/makersacademy/acebook/service/CommentService.java b/src/main/java/com/makersacademy/acebook/service/CommentService.java new file mode 100644 index 000000000..c2c823145 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/service/CommentService.java @@ -0,0 +1,46 @@ +package com.makersacademy.acebook.service; + +import com.makersacademy.acebook.model.Comment; +import com.makersacademy.acebook.repository.CommentLikeRepository; +import com.makersacademy.acebook.repository.CommentRepository; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + + +@Service +public class CommentService { + + private final CommentRepository commentRepository; + private final CommentLikeRepository commentLikeRepository; + + + public CommentService(CommentRepository commentRepository, + CommentLikeRepository commentLikeRepository) { + this.commentRepository = commentRepository; + this.commentLikeRepository = commentLikeRepository; + } + + public Comment addComment(Comment comment) { + comment.setCreatedAt(LocalDateTime.now()); + return commentRepository.save(comment); + } + + public List getCommentsForPost(Long postId) { + return commentRepository.findByPost_IdOrderByCreatedAtAsc(postId); + } + + public long getLikesCount(Long commentId) { + return commentLikeRepository.countByCommentId(commentId); + } + + public void deleteComment(Long commentId) { + commentRepository.deleteById(commentId); + } + + public void deleteCommentsByPostId(Long postId) { + commentRepository.deleteAllByPostId(postId); + } +} + diff --git a/src/main/java/com/makersacademy/acebook/service/ImageStorageService.java b/src/main/java/com/makersacademy/acebook/service/ImageStorageService.java new file mode 100644 index 000000000..b90beef9b --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/service/ImageStorageService.java @@ -0,0 +1,70 @@ +package com.makersacademy.acebook.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.file.*; +import java.util.Optional; +import java.util.UUID; + +@Service +public class ImageStorageService { + + // Locations for storing profile / post images within the app setup - configured within application.properties + @Value("${file.upload-dir.user-profile}") + private String uploadDirProfile; + + @Value("${file.upload-dir.post-images}") + private String uploadDirPost; + + // Segregated methods for storing the different types of images + public String storeProfileImage(MultipartFile file, String id) throws IOException { + return storeImage(file, uploadDirProfile, id); + } + + public String storePostImage(MultipartFile file, String id) throws IOException { + return storeImage(file, uploadDirPost, id); + } + + // Generic storeImage method which is called in both methods above, but fed different parameters for storage location + // Takes a file, a String representing target directory (for image storage) and a string id which helps configure filename + private String storeImage(MultipartFile file, String targetDir, String id) throws IOException { + // Checks whether file is empty + if (file.isEmpty()){ + return null; + } + + // Check file is of correct type + String contentType = file.getContentType(); + if (!contentType.equals("image/jpeg") && !contentType.equals("image/png")) { + throw new IllegalArgumentException("Only JPEG or PNG images are allowed"); + } + + // Extracts the file extension from the filename, and concatenates with + String extension = getFileExtension(file.getOriginalFilename()); + String newFileName = id + extension; + + // These two blocks handle setting the upload path and getting the file stored in right location + Path uploadPath = Paths.get(targetDir); + if (!Files.exists(uploadPath)) { + Files.createDirectories(uploadPath); + } + + Path filePath = uploadPath.resolve(newFileName); + Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING); + + return String.valueOf("/" + filePath); + } + + public static String getFileExtension(String fileName) { + return Optional.ofNullable(fileName) + .filter(f -> f.contains(".")) + .map(f -> f.substring(f.lastIndexOf("."))) + .orElse(""); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/service/NotificationService.java b/src/main/java/com/makersacademy/acebook/service/NotificationService.java new file mode 100644 index 000000000..60e43e900 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/service/NotificationService.java @@ -0,0 +1,106 @@ +package com.makersacademy.acebook.service; + +import com.makersacademy.acebook.model.*; +import com.makersacademy.acebook.repository.*; +import com.makersacademy.acebook.model.Comment; +import com.makersacademy.acebook.repository.CommentRepository; +import com.makersacademy.acebook.repository.NotificationRepository; +import com.makersacademy.acebook.model.Notification; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.stereotype.Service; + +import jakarta.annotation.Nullable; +import org.springframework.web.servlet.view.RedirectView; + +import java.util.*; + + +@Service +public class NotificationService { + + @Autowired + NotificationRepository notificationRepository; + @Autowired + CommentRepository commentRepository; + @Autowired + PostRepository postRepository; + @Autowired + FriendRequestRepository friendRequestRepository; + @Autowired + UserRepository userRepository; + + + // Creates a new notification for user, when someone comments on or likes their post or comment. + public void newNotification(Long userId, String type, @Nullable Comment comment, @Nullable PostLike postLike, @Nullable CommentLike commentLike) { + Post post = null; + Long commentId = null; + + if (type.equals("comment") && comment != null) { + commentId = comment.getId(); + post = comment.getPost(); + } else if (type.contains("Like")) { // && (postLike != null || commentLike != null) + if (commentLike != null) { + commentId = commentLike.getCommentId(); + post = commentRepository.findById(commentId).get().getPost(); + } else if (postLike != null) { + post = postRepository.findById(postLike.getPostId()).get(); + } + } + if (post != null && !(userId.equals(post.getUserId()))) { + Notification notification = new Notification(null, post.getUserId(), userId, type, post.getId(), commentId, false, null); + notificationRepository.save(notification); + } + } + + + // Gets count of all unread notifications and pending friend requests for current user + public Integer notificationCount(Long receivingUserId) { + Integer pendingRequests = friendRequestRepository.countByReceiverIdAndStatus(receivingUserId, "pending"); + Integer notificationsCount = notificationRepository.countByReceivingUserIdAndIsRead(receivingUserId, false); + Integer totalNotificationsCount = pendingRequests + notificationsCount; + return totalNotificationsCount; + } + + // Gets list of all notifications for current user, sorted by the newest ones first + public Collection getNotifications(Long userId) { + Collection notifications = new ArrayList<>(); + notifications.addAll(notificationRepository.findByReceivingUserIdOrderByCreatedAtDesc(userId)); + return notifications; + } + + // Gets list of notification senders, used in conjunction with getNotifications for formatting purposes. + public Map getNotificationSenderNames(Collection notifications) { + Map senderNames = new HashMap<>(); + for (Notification notification : notifications) { + Optional sender = userRepository.findById(notification.getSendingUserId()); + sender.ifPresent(user -> senderNames.put(notification.getId(), user.getFirstName())); + } + return senderNames; + } + + // Mark notification as read, redirect to specific post page + public String readNotification(Long notificationId) { + Notification activeNotification = notificationRepository.findById(notificationId).orElse(null); + if (activeNotification == null) { + return null; + } + activeNotification.setRead(true); + notificationRepository.save(activeNotification); + return Long.toString(activeNotification.getPostId()); + } + + // Gets list of pending friend requests, orders by newest first. + public List getFriendRequests(Long currentUserId) { + List pendingFriendRequests = friendRequestRepository.findAllByReceiverIdAndStatusOrderByCreatedAtDesc(currentUserId, "pending"); + List requesterUsers = new ArrayList<>(); + for (FriendRequest request : pendingFriendRequests) { + Long requesterId = request.getRequesterId(); + User requesterUser = userRepository.findById(requesterId).orElse(null); + requesterUsers.add(requesterUser); + } + return requesterUsers; + } +} + diff --git a/src/main/java/com/makersacademy/acebook/service/PostLikeService.java b/src/main/java/com/makersacademy/acebook/service/PostLikeService.java new file mode 100644 index 000000000..0ad6f2030 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/service/PostLikeService.java @@ -0,0 +1,63 @@ +package com.makersacademy.acebook.service; + +import com.makersacademy.acebook.model.PostLike; +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.PostLikeRepository; +import com.makersacademy.acebook.repository.UserRepository; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class PostLikeService { + + private final PostLikeRepository postLikeRepository; + private final UserRepository userRepository; + private final NotificationService notificationService; + + public PostLikeService(PostLikeRepository postLikeRepository, UserRepository userRepository, NotificationService notificationService) { + this.postLikeRepository = postLikeRepository; + this.userRepository = userRepository; + this.notificationService = notificationService; + } + + public void likePost(Long postId, String userEmail) { + User user = userRepository.findUserByUsername(userEmail) + .orElseThrow(() -> new RuntimeException("User not found")); + + Optional existingLike = postLikeRepository.findByUserIdAndPostId(user.getId(), postId); + + if (existingLike.isEmpty()) { + PostLike like = new PostLike(); + like.setUserId(user.getId()); + like.setPostId(postId); + postLikeRepository.save(like); + notificationService.newNotification(user.getId(), "postLike", null, like, null); + } + + } + + public long getLikesCount(Long postId) { + return postLikeRepository.countByPostId(postId); + } + + public void unlikePost(Long postId, String userEmail) { + User user = userRepository.findUserByUsername(userEmail) + .orElseThrow(() -> new RuntimeException("User not found")); + postLikeRepository.deleteByUserIdAndPostId(user.getId(), postId); + } + + public List getLikersForPost(Long postId) { + List likes = postLikeRepository.findByPostId(postId); + return likes.stream() + .map(like -> userRepository.findById(like.getUserId())) + .filter(Optional::isPresent) + .map(user -> { + User u = user.get(); + return u.getFirstName() + " " + u.getLastName(); + }) + .toList(); + } + +} diff --git a/src/main/java/com/makersacademy/acebook/service/PostService.java b/src/main/java/com/makersacademy/acebook/service/PostService.java new file mode 100644 index 000000000..a7920e685 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/service/PostService.java @@ -0,0 +1,82 @@ +package com.makersacademy.acebook.service; + +import com.makersacademy.acebook.model.Post; +import com.makersacademy.acebook.model.PostLike; +import com.makersacademy.acebook.repository.FriendRepository; +import com.makersacademy.acebook.repository.PostLikeRepository; +import com.makersacademy.acebook.repository.PostRepository; +import com.makersacademy.acebook.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + +@Service +public class PostService { + + @Autowired + PostRepository postRepository; + @Autowired + PostLikeRepository postLikeRepository; + @Autowired + UserRepository userRepository; + @Autowired + FriendRepository friendRepository; + @Autowired + PostLikeService postLikeService; + + + public PostService(PostRepository postRepository, + PostLikeRepository postLikeRepository) { + this.postRepository = postRepository; + this.postLikeRepository = postLikeRepository; + } + + public Post newPost(Post post) { + return postRepository.save(post); + } + + public void likePost(Long userId, Long postId) { + boolean alreadyLiked = postLikeRepository + .findByUserIdAndPostId(userId, postId) + .isPresent(); + if (!alreadyLiked) { + PostLike like = new PostLike(); + like.setUserId(userId); + like.setPostId(postId); + postLikeRepository.save(like); + } + } + + public void unlikePost(Long userId, Long postId) { + postLikeRepository.deleteByUserIdAndPostId(userId, postId); + } + + public long getLikesCount(Long postId) { + return postLikeRepository.countByPostId(postId); + } + + public void deletePost(Long postId) { + postRepository.deleteById(postId); + } + + public List findFriendsPosts(Long currentUserId) { + // Find the id's of each friend for current user + List friendsIds = (friendRepository.findFriendUserIdByMainUserId(currentUserId)); + Iterable currentUserPosts = postRepository.findAllByUserId(currentUserId); + List friendsPosts = new ArrayList<>(); + currentUserPosts.forEach(friendsPosts::add); + + // find the User objects for each friend of current user + for (Long friend : friendsIds) { + Iterable postsOfFriend = postRepository.findAllByUserId(friend); + postsOfFriend.forEach(friendsPosts::add); + } + friendsPosts.sort(Collections.reverseOrder()); + return friendsPosts; + } +} + diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index 865b41e1c..926b9edf4 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -3,4 +3,4 @@ spring.datasource.username= spring.datasource.password= flyway.baseline-on-migrate=true spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults = false -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7b2ed1a6b..eab4a7acc 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,3 +6,9 @@ logging.level.org.springframework.web=DEBUG logging.level.org.springframework.security=DEBUG logging.level.org.springframework.web.client.RestTemplate=DEBUG logging.level.org.apache.http=DEBUG +file.upload-dir.user-profile=uploads/user_profile +file.upload-dir.post-images=uploads/post_images +spring.web.resources.static-locations=classpath:/static/,file:uploads/ +spring.servlet.multipart.enabled=true +spring.servlet.multipart.max-file-size=5MB +spring.servlet.multipart.max-request-size=5MB \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index 699e8575a..000000000 --- a/src/main/resources/application.yml +++ /dev/null @@ -1,5 +0,0 @@ -okta: - oauth2: - issuer: https://dev-edward-andress.uk.auth0.com/ - client-id: ${OKTA_CLIENT_ID} - client-secret: ${OKTA_CLIENT_SECRET} diff --git a/src/main/resources/db/migration/V10__create_notifications_table.sql b/src/main/resources/db/migration/V10__create_notifications_table.sql new file mode 100644 index 000000000..ed3bc154e --- /dev/null +++ b/src/main/resources/db/migration/V10__create_notifications_table.sql @@ -0,0 +1,17 @@ +DROP TABLE IF EXISTS notifications; + +CREATE TABLE notifications ( + id BIGSERIAL PRIMARY KEY, + receiving_user_id BIGINT NOT NULL, + sending_user_id BIGINT, + type TEXT NOT NULL, + post_id BIGINT, -- means clicking on this could take you to related post + comment_id BIGINT, -- same but for comment. + is_read BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW(), + + FOREIGN KEY (receiving_user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (sending_user_id) REFERENCES users(id) ON DELETE SET NULL, + FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, + FOREIGN KEY (comment_id) REFERENCES comments(id) ON DELETE CASCADE +); diff --git a/src/main/resources/db/migration/V11__create_friend_requests_table.sql b/src/main/resources/db/migration/V11__create_friend_requests_table.sql new file mode 100644 index 000000000..c8745a6a0 --- /dev/null +++ b/src/main/resources/db/migration/V11__create_friend_requests_table.sql @@ -0,0 +1,18 @@ +DROP TABLE IF EXISTS friend_requests; + +CREATE TABLE friend_requests ( + id BIGSERIAL PRIMARY KEY, + requester_id BIGINT NOT NULL, + receiver_id BIGINT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending',-- 'pending', 'accepted', 'rejected' + created_at TIMESTAMP DEFAULT NOW(), + responded_at TIMESTAMP, + + FOREIGN KEY (requester_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (receiver_id) REFERENCES users(id) ON DELETE CASCADE, + + CHECK (requester_id <> receiver_id), + CHECK (status IN ('pending', 'accepted', 'rejected')), + + UNIQUE (requester_id, receiver_id) -- prevent duplicate requests +); diff --git a/src/main/resources/db/migration/V12__alter_friends_table_add_friends_since.sql b/src/main/resources/db/migration/V12__alter_friends_table_add_friends_since.sql new file mode 100644 index 000000000..a12b44b04 --- /dev/null +++ b/src/main/resources/db/migration/V12__alter_friends_table_add_friends_since.sql @@ -0,0 +1,15 @@ + + +DROP TABLE IF EXISTS friends; + +CREATE TABLE friends ( + main_user_id bigint NOT NULL, + friend_user_id bigint NOT NULL, + friends_since timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (main_user_id, friend_user_id), + + FOREIGN KEY (main_user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (friend_user_id) REFERENCES users(id) ON DELETE CASCADE, + + CHECK (main_user_id <> friend_user_id) +); diff --git a/src/main/resources/db/migration/V13__add_image_column_to_post_table.sql b/src/main/resources/db/migration/V13__add_image_column_to_post_table.sql new file mode 100644 index 000000000..00961bfe6 --- /dev/null +++ b/src/main/resources/db/migration/V13__add_image_column_to_post_table.sql @@ -0,0 +1,2 @@ +ALTER TABLE posts +ADD image text; \ No newline at end of file diff --git a/src/main/resources/db/migration/V3__alter_users_table_for_new_columns.sql b/src/main/resources/db/migration/V3__alter_users_table_for_new_columns.sql new file mode 100644 index 000000000..eee4c625d --- /dev/null +++ b/src/main/resources/db/migration/V3__alter_users_table_for_new_columns.sql @@ -0,0 +1,9 @@ +ALTER TABLE users +ADD first_name varchar(50), +ADD last_name varchar(50), +ADD profile_pic text; + + + + + diff --git a/src/main/resources/db/migration/V4__create_friends_table.sql b/src/main/resources/db/migration/V4__create_friends_table.sql new file mode 100644 index 000000000..7091351da --- /dev/null +++ b/src/main/resources/db/migration/V4__create_friends_table.sql @@ -0,0 +1,8 @@ +DROP TABLE IF EXISTS friends; + +CREATE TABLE friends ( + main_user_id bigserial, + friend_user_id bigserial, + PRIMARY KEY(main_user_id, friend_user_id) + +); \ No newline at end of file diff --git a/src/main/resources/db/migration/V5__alter_posts_table_for_new_columns.sql b/src/main/resources/db/migration/V5__alter_posts_table_for_new_columns.sql new file mode 100644 index 000000000..298c8b226 --- /dev/null +++ b/src/main/resources/db/migration/V5__alter_posts_table_for_new_columns.sql @@ -0,0 +1,5 @@ +ALTER TABLE posts +ADD user_id bigserial, +ADD CONSTRAINT post_user_id +FOREIGN KEY (user_id) REFERENCES users(id), +ADD time_posted timestamp; \ No newline at end of file diff --git a/src/main/resources/db/migration/V6__create_post_likes_table.sql b/src/main/resources/db/migration/V6__create_post_likes_table.sql new file mode 100644 index 000000000..0941de6c2 --- /dev/null +++ b/src/main/resources/db/migration/V6__create_post_likes_table.sql @@ -0,0 +1,12 @@ +DROP TABLE IF EXISTS postLikes; + +CREATE TABLE post_likes ( + user_id bigserial NOT NULL, + post_id bigserial NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + + PRIMARY KEY (user_id, post_id), + + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE +); diff --git a/src/main/resources/db/migration/V7__alter_friends_add_foreign_keys.sql b/src/main/resources/db/migration/V7__alter_friends_add_foreign_keys.sql new file mode 100644 index 000000000..34bd81b9b --- /dev/null +++ b/src/main/resources/db/migration/V7__alter_friends_add_foreign_keys.sql @@ -0,0 +1,14 @@ + + +DROP TABLE IF EXISTS friends; + +CREATE TABLE friends ( + main_user_id bigserial NOT NULL, + friend_user_id bigserial NOT NULL, + PRIMARY KEY (main_user_id, friend_user_id), + + FOREIGN KEY (main_user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (friend_user_id) REFERENCES users(id) ON DELETE CASCADE, + + CHECK (main_user_id <> friend_user_id) +); diff --git a/src/main/resources/db/migration/V8__create_comments_table.sql b/src/main/resources/db/migration/V8__create_comments_table.sql new file mode 100644 index 000000000..cd633704a --- /dev/null +++ b/src/main/resources/db/migration/V8__create_comments_table.sql @@ -0,0 +1,11 @@ +DROP TABLE IF EXISTS comments; +CREATE TABLE comments ( + id bigserial PRIMARY KEY, + post_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + content TEXT NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + + FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/src/main/resources/db/migration/V9__create_comment_likes_table.sql b/src/main/resources/db/migration/V9__create_comment_likes_table.sql new file mode 100644 index 000000000..d2a79910e --- /dev/null +++ b/src/main/resources/db/migration/V9__create_comment_likes_table.sql @@ -0,0 +1,12 @@ + + +CREATE TABLE comment_likes ( + user_id BIGINT NOT NULL, + comment_id BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + + PRIMARY KEY (user_id, comment_id), + + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (comment_id) REFERENCES comments(id) ON DELETE CASCADE +); diff --git a/src/main/resources/seeds/acebook_seeds.sql b/src/main/resources/seeds/acebook_seeds.sql new file mode 100644 index 000000000..7ecee9e46 --- /dev/null +++ b/src/main/resources/seeds/acebook_seeds.sql @@ -0,0 +1,510 @@ +-- Seed file for social media database with pop artists +-- Run this after all migrations are complete + +-- Clear existing data (in correct order to respect foreign keys) +DELETE FROM comment_likes; +DELETE FROM post_likes; +DELETE FROM notifications; +DELETE FROM comments; +DELETE FROM friend_requests; +DELETE FROM friends; +DELETE FROM posts; +DELETE FROM users; + +-- Reset sequences +ALTER SEQUENCE users_id_seq RESTART WITH 1; +ALTER SEQUENCE posts_id_seq RESTART WITH 1; +ALTER SEQUENCE comments_id_seq RESTART WITH 1; +ALTER SEQUENCE notifications_id_seq RESTART WITH 1; +ALTER SEQUENCE friend_requests_id_seq RESTART WITH 1; + +-- Insert Users (Pop Artists) +INSERT INTO users (username, enabled, first_name, last_name, profile_pic) VALUES +('taylorswift@swiftmail.com', true, 'Taylor', 'Swift', '/uploads/user_profile/taylor.jpg'), +('arianagrande@grandenotes.com', true, 'Ariana', 'Grande', '/uploads/user_profile/ariana.jpg'), +('justinbieber@beliebers.net', true, 'Justin', 'Bieber', '/uploads/user_profile/justin.jpg'), +('billieeilish@oceaneyes.org', true, 'Billie', 'Eilish', '/uploads/user_profile/billie.jpg'), +('dualipa@levitating.fm', true, 'Dua', 'Lipa', '/uploads/user_profile/dua.jpg'), +('edsheeran@shapeofyou.co', true, 'Ed', 'Sheeran', '/uploads/user_profile/ed.jpg'), +('oliviarodrigo@driverslicense.io', true, 'Olivia', 'Rodrigo', '/uploads/user_profile/olivia.jpg'), +('harrystyles@watermelonsugar.love', true, 'Harry', 'Styles', '/uploads/user_profile/harry.jpg'), +('selenagomez@rarebeauty.email', true, 'Selena', 'Gomez', '/uploads/user_profile/selena.jpg'), +('theweeknd@afterhours.zone', true, 'Abel', 'Tesfaye', '/uploads/user_profile/abel.jpg'), +('adele@helloagain.uk', true, 'Adele', 'Adkins', '/uploads/user_profile/adele.jpg'), +('brunomars@uptownfunk.space', true, 'Bruno', 'Mars', '/uploads/user_profile/bruno.jpg'), +('beyonce@queenbey.com', true, 'Beyoncé', 'Knowles', '/uploads/user_profile/beyonce.jpeg'), +('drake@ovo.ca', true, 'Aubrey', 'Graham', '/uploads/user_profile/drake.jpeg'), +('rihanna@fentyworld.com', true, 'Rihanna', 'Fenty', '/uploads/user_profile/rihanna.jpg'), +('ladygaga@monstermail.net', true, 'Lady', 'Gaga', '/uploads/user_profile/gaga.jpg'), +('khalid@location.live', true, 'Khalid', '', '/uploads/user_profile/khalid.jpeg'), +('lizzo@juice.fm', true, 'Lizzo', '', '/uploads/user_profile/lizzo.jpeg'), +('sza@ctrl.zone', true, 'SZA', '', '/uploads/user_profile/sza.jpg'), +('shawnmendes@stitches.ca', true, 'Shawn', 'Mendes', '/uploads/user_profile/shawn.jpg'), +('camila@havanamail.cu', true, 'Camila', 'Cabello', '/uploads/user_profile/camila.jpg'), +('charlieputh@attention.co', true, 'Charlie', 'Puth', '/uploads/user_profile/charlie.jpg'), +('bts@bangtan.kr', true, 'BTS', '', '/uploads/user_profile/bts.jpeg'), +('zayn@pillowtalk.uk', true, 'Zayn', 'Malik', '/uploads/user_profile/zayn.jpg'), +('halsey@badlands.com', true, 'Halsey', '', '/uploads/user_profile/halsey.jpg'), +('dojacat@planether.space', true, 'Doja', 'Cat', '/uploads/user_profile/doja.jpg'), +('nickiminaj@barbz.net', true, 'Nicki', 'Minaj', '/uploads/user_profile/nicki.jpg'); + +-- Insert Posts +INSERT INTO posts (content, user_id, time_posted, image) VALUES +-- Taylor Swift posts +('Just finished writing my 200th song this year 🎵 The stories keep coming!', 1, NOW() - INTERVAL '2 hours', '/uploads/post_images/taylor_post.jpg'), +('Cats are better than people and I will die on this hill 🐱', 1, NOW() - INTERVAL '1 day', NULL), +('13 is my lucky number for a reason ✨', 1, NOW() - INTERVAL '3 days', NULL), + +-- Ariana Grande posts +('thank u, next era was just the beginning 💫', 2, NOW() - INTERVAL '4 hours', NULL), +('My vocal range today: 4 octaves and counting 🎤', 2, NOW() - INTERVAL '2 days', '/uploads/post_images/ariana_post.jpeg'), +('Donut licking was a mistake but yuh 🍩', 2, NOW() - INTERVAL '1 week', NULL), + +-- Justin Bieber posts +('Sorry for all the apologies, but here''s another one 🙏', 3, NOW() - INTERVAL '6 hours', NULL), +('Hailey and I just adopted another puppy! 🐕', 3, NOW() - INTERVAL '1 day', '/uploads/post_images/justin_post.jpg'), +('Baby baby baby ohhh... still stuck in my head', 3, NOW() - INTERVAL '4 days', NULL), + +-- Billie Eilish posts +('wore color today and everyone lost their minds 🌈', 4, NOW() - INTERVAL '3 hours', '/uploads/post_images/billie_post.jpg'), +('bad guy but make it acoustic 😈', 4, NOW() - INTERVAL '2 days', NULL), +('oversized clothes = maximum comfort level achieved', 4, NOW() - INTERVAL '5 days', NULL), + +-- Dua Lipa posts +('Levitating to the studio for another banger 🚀', 5, NOW() - INTERVAL '5 hours', NULL), +('New rules: always dance like nobody''s watching 💃', 5, NOW() - INTERVAL '3 days', '/uploads/post_images/dua_post.jpg'), +('Physical training for the tour starts now 💪', 5, NOW() - INTERVAL '1 week', NULL), + +-- Ed Sheeran posts +('Mathematical precision in every melody 🔢🎵', 6, NOW() - INTERVAL '7 hours', NULL), +('Thinking out loud about my next album concept', 6, NOW() - INTERVAL '2 days', NULL), +('Perfect collaboration brewing with someone special 👀', 6, NOW() - INTERVAL '6 days', '/uploads/post_images/ed_post.jpg'), + +-- Olivia Rodrigo posts +('drivers license test: passed ✅ heartbreak songs: unlimited', 7, NOW() - INTERVAL '1 hour', NULL), +('good 4 u if you''re not crying to my music rn', 7, NOW() - INTERVAL '1 day', NULL), +('brutal honesty is my brand and I''m not sorry', 7, NOW() - INTERVAL '4 days', '/uploads/post_images/olivia_post.jpg'), + +-- Harry Styles posts +('Watermelon sugar high and loving life 🍉', 8, NOW() - INTERVAL '8 hours', NULL), +('Fine line between fashion and art, I choose both', 8, NOW() - INTERVAL '3 days', '/uploads/post_images/harry_post.jpg'), +('Treat people with kindness, always ❤️', 8, NOW() - INTERVAL '1 week', NULL), + +-- Selena Gomez posts +('Rare moments of self-love hit different 💕', 9, NOW() - INTERVAL '2 hours', NULL), +('Lose you to love me was just the beginning of my journey', 9, NOW() - INTERVAL '2 days', '/uploads/post_images/selena_post.png'), +('Mental health check: we''re all learning and growing 🌱', 9, NOW() - INTERVAL '5 days', NULL), + +-- The Weeknd posts +('Blinding lights, but make it emotional 🌟', 10, NOW() - INTERVAL '4 hours', NULL), +('Can''t feel my face when the music hits just right', 10, NOW() - INTERVAL '1 day', NULL), +('After hours creativity is when magic happens ✨', 10, NOW() - INTERVAL '3 days', '/uploads/post_images/abel_post.jpg'), + +-- Adele posts +('Hello, it''s me wondering if you''re ready for album 31 👋', 11, NOW() - INTERVAL '6 hours', NULL), +('Rolling in the deep... thoughts about life and love', 11, NOW() - INTERVAL '4 days', NULL), +('Someone like you is out there, just keep believing 💫', 11, NOW() - INTERVAL '1 week', '/uploads/post_images/adele_post.jpg'), + +-- Bruno Mars posts +('Just the way you are is exactly how you should be ✨', 12, NOW() - INTERVAL '3 hours', NULL), +('Uptown funk vibes all day, every day 🕺', 12, NOW() - INTERVAL '2 days', '/uploads/post_images/bruno_post.png'), +('Count on me to bring the groove to every situation', 12, NOW() - INTERVAL '6 days', NULL), + +-- Beyoncé posts +('Formation energy never fades 👑', 13, NOW() - INTERVAL '2 hours', NULL), +('Coachella was legendary, but wait till you see what''s next.', 13, NOW() - INTERVAL '2 days', NULL), +('Queen things only. 🐝', 13, NOW() - INTERVAL '3 days', NULL), +-- Drake posts +('Certified Lover Boy vibes 💘', 14, NOW() - INTERVAL '1 hour', NULL), +('Started from the bottom... still grinding.', 14, NOW() - INTERVAL '1 day', NULL), +('Toronto forever 🦉', 14, NOW() - INTERVAL '3 days', NULL), +-- Rihanna posts +('Makeup mogul by day, pop queen by night 💄🎤', 15, NOW() - INTERVAL '5 hours', NULL), +('New album? Maybe. Maybe not. 😏', 15, NOW() - INTERVAL '2 days', NULL), +('Umbrella ella ella ☔️', 15, NOW() - INTERVAL '4 days', NULL), +-- Lady Gaga posts +('Born this way, thriving this way 🌈', 16, NOW() - INTERVAL '1 hour', NULL), +('Little monsters forever 💖', 16, NOW() - INTERVAL '2 days', NULL), +('The Haus of Gaga is in session 🎭', 16, NOW() - INTERVAL '3 days', NULL), +-- Khalid posts +('Location 2.0 dropping soon 🌍', 17, NOW() - INTERVAL '3 hours', NULL), +('Grateful for the growth 🌱', 17, NOW() - INTERVAL '1 day', NULL), +('Soul music for the soul 🎶', 17, NOW() - INTERVAL '2 days', NULL), +-- Lizzo posts +('Feeling good as hell 💅', 18, NOW() - INTERVAL '30 minutes', NULL), +('Flute solos and body positivity 🌀', 18, NOW() - INTERVAL '1 day', NULL), +('Juice is forever 🧃', 18, NOW() - INTERVAL '3 days', NULL), +-- SZA posts +('Ctrl never left my rotation 🔁', 19, NOW() - INTERVAL '2 hours', NULL), +('Snooze is not just a song—it''s a mood.', 19, NOW() - INTERVAL '2 days', NULL), +('Real ones know the vibes 💫', 19, NOW() - INTERVAL '4 days', NULL), +-- Shawn Mendes posts +('Stitches healed, voice still strong 🎤', 20, NOW() - INTERVAL '1 hour', NULL), +('Wonder tour memories hitting hard 🌎', 20, NOW() - INTERVAL '1 day', NULL), +('Canadian maple beats 🍁', 20, NOW() - INTERVAL '3 days', NULL), +-- Camila Cabello posts +('Havana ooh na-na 💃', 21, NOW() - INTERVAL '3 hours', NULL), +('Familia over everything ❤️', 21, NOW() - INTERVAL '1 day', NULL), +('Finding new rhythm in life and music 🎶', 21, NOW() - INTERVAL '4 days', NULL), +-- Charlie Puth posts +('Attention to detail 🎧', 22, NOW() - INTERVAL '1 hour', NULL), +('Perfect pitch and awkward tweets 😅', 22, NOW() - INTERVAL '2 days', NULL), +('Light switch era is here 💡', 22, NOW() - INTERVAL '3 days', NULL), +-- BTS posts +('Bangtan power united 🌟', 23, NOW() - INTERVAL '2 hours', NULL), +('ARMY, you are everything 💜', 23, NOW() - INTERVAL '2 days', NULL), +('Mic drop! 🎤', 23, NOW() - INTERVAL '4 days', NULL), +-- Zayn posts +('Zquad assemble 🖤', 24, NOW() - INTERVAL '1 hour', NULL), +('Pillowtalk is a lifestyle', 24, NOW() - INTERVAL '2 days', NULL), +('Solo and thriving ✨', 24, NOW() - INTERVAL '3 days', NULL), +-- Halsey posts +('Colors of creativity 🔥', 25, NOW() - INTERVAL '1.5 hours', NULL), +('Without Me was never really about just me.', 25, NOW() - INTERVAL '2 days', NULL), +('Exploring new sonic palettes 🎨', 25, NOW() - INTERVAL '3 days', NULL), +-- Doja Cat posts +('Planet Her, population: me 🌍✨', 26, NOW() - INTERVAL '2 hours', NULL), +('Mooo! still my proudest track 🐄', 26, NOW() - INTERVAL '1 day', NULL), +('The internet wasn''t ready for me 🤖', 26, NOW() - INTERVAL '2 days', NULL), +-- Nicki Minaj posts +('Barbz stand up 💅', 27, NOW() - INTERVAL '1 hour', NULL), +('Queen Radio live again soon 🎙️', 27, NOW() - INTERVAL '2 days', NULL), +('Chun-Li energy 24/7 🥋', 27, NOW() - INTERVAL '3 days', NULL); + +-- Insert Friend Relationships (bidirectional) +INSERT INTO friends (main_user_id, friend_user_id, friends_since) VALUES +-- Taylor's friendships +(1, 2, NOW() - INTERVAL '2 years'), (2, 1, NOW() - INTERVAL '2 years'), -- Taylor & Ariana +(1, 9, NOW() - INTERVAL '3 years'), (9, 1, NOW() - INTERVAL '3 years'), -- Taylor & Selena +(1, 8, NOW() - INTERVAL '1 year'), (8, 1, NOW() - INTERVAL '1 year'), -- Taylor & Harry + +-- Ariana's additional friendships +(2, 3, NOW() - INTERVAL '4 years'), (3, 2, NOW() - INTERVAL '4 years'), -- Ariana & Justin +(2, 12, NOW() - INTERVAL '6 months'), (12, 2, NOW() - INTERVAL '6 months'), -- Ariana & Bruno + +-- Justin's friendships +(3, 6, NOW() - INTERVAL '5 years'), (6, 3, NOW() - INTERVAL '5 years'), -- Justin & Ed +(3, 8, NOW() - INTERVAL '2 years'), (8, 3, NOW() - INTERVAL '2 years'), -- Justin & Harry + +-- Billie's friendships +(4, 7, NOW() - INTERVAL '1 year'), (7, 4, NOW() - INTERVAL '1 year'), -- Billie & Olivia +(4, 5, NOW() - INTERVAL '8 months'), (5, 4, NOW() - INTERVAL '8 months'), -- Billie & Dua + +-- More cross-connections +(5, 8, NOW() - INTERVAL '1.5 years'), (8, 5, NOW() - INTERVAL '1.5 years'), -- Dua & Harry +(6, 11, NOW() - INTERVAL '3 years'), (11, 6, NOW() - INTERVAL '3 years'), -- Ed & Adele +(9, 7, NOW() - INTERVAL '6 months'), (7, 9, NOW() - INTERVAL '6 months'), -- Selena & Olivia +(10, 12, NOW() - INTERVAL '2 years'), (12, 10, NOW() - INTERVAL '2 years'), -- Weeknd & Bruno + +-- Beyoncé +(13, 15, NOW() - INTERVAL '4 years'), (15, 13, NOW() - INTERVAL '4 years'), +(13, 1, NOW() - INTERVAL '5 years'), (1, 13, NOW() - INTERVAL '5 years'), +(13, 3, NOW() - INTERVAL '3 years'), (3, 13, NOW() - INTERVAL '3 years'), +(13, 27, NOW() - INTERVAL '6 years'), (27, 13, NOW() - INTERVAL '6 years'), +(13, 16, NOW() - INTERVAL '2 years'), (16, 13, NOW() - INTERVAL '2 years'), +(13, 14, NOW() - INTERVAL '1 year'), (14, 13, NOW() - INTERVAL '1 year'), +(13, 18, NOW() - INTERVAL '2.5 years'), (18, 13, NOW() - INTERVAL '2.5 years'), +(13, 2, NOW() - INTERVAL '1.5 years'), (2, 13, NOW() - INTERVAL '1.5 years'), +(13, 6, NOW() - INTERVAL '3 years'), (6, 13, NOW() - INTERVAL '3 years'), + +-- Drake +(14, 27, NOW() - INTERVAL '8 years'), (27, 14, NOW() - INTERVAL '8 years'), +(14, 15, NOW() - INTERVAL '6 years'), (15, 14, NOW() - INTERVAL '6 years'), +(14, 3, NOW() - INTERVAL '4 years'), (3, 14, NOW() - INTERVAL '4 years'), +(14, 6, NOW() - INTERVAL '5 years'), (6, 14, NOW() - INTERVAL '5 years'), +(14, 23, NOW() - INTERVAL '1.5 years'), (23, 14, NOW() - INTERVAL '1.5 years'), +(14, 10, NOW() - INTERVAL '3 years'), (10, 14, NOW() - INTERVAL '3 years'), +(14, 22, NOW() - INTERVAL '1 year'), (22, 14, NOW() - INTERVAL '1 year'), + +-- Rihanna +(15, 27, NOW() - INTERVAL '7 years'), (27, 15, NOW() - INTERVAL '7 years'), +(15, 3, NOW() - INTERVAL '5 years'), (3, 15, NOW() - INTERVAL '5 years'), +(15, 16, NOW() - INTERVAL '3 years'), (16, 15, NOW() - INTERVAL '3 years'), +(15, 6, NOW() - INTERVAL '2 years'), (6, 15, NOW() - INTERVAL '2 years'), +(15, 18, NOW() - INTERVAL '4 years'), (18, 15, NOW() - INTERVAL '4 years'), +(15, 7, NOW() - INTERVAL '1 year'), (7, 15, NOW() - INTERVAL '1 year'), +(15, 25, NOW() - INTERVAL '2 years'), (25, 15, NOW() - INTERVAL '2 years'), + +-- Lady Gaga +(16, 18, NOW() - INTERVAL '1.5 years'), (18, 16, NOW() - INTERVAL '1.5 years'), +(16, 25, NOW() - INTERVAL '3 years'), (25, 16, NOW() - INTERVAL '3 years'), +(16, 4, NOW() - INTERVAL '2 years'), (4, 16, NOW() - INTERVAL '2 years'), +(16, 9, NOW() - INTERVAL '1 year'), (9, 16, NOW() - INTERVAL '1 year'), +(16, 26, NOW() - INTERVAL '1.5 years'), (26, 16, NOW() - INTERVAL '1.5 years'), +(16, 27, NOW() - INTERVAL '2 years'), (27, 16, NOW() - INTERVAL '2 years'), + +-- Khalid +(17, 5, NOW() - INTERVAL '2 years'), (5, 17, NOW() - INTERVAL '2 years'), +(17, 18, NOW() - INTERVAL '2.5 years'), (18, 17, NOW() - INTERVAL '2.5 years'), +(17, 6, NOW() - INTERVAL '3 years'), (6, 17, NOW() - INTERVAL '3 years'), +(17, 7, NOW() - INTERVAL '2 years'), (7, 17, NOW() - INTERVAL '2 years'), +(17, 21, NOW() - INTERVAL '2 years'), (21, 17, NOW() - INTERVAL '2 years'), +(17, 19, NOW() - INTERVAL '1 year'), (19, 17, NOW() - INTERVAL '1 year'), +(17, 24, NOW() - INTERVAL '1.5 years'), (24, 17, NOW() - INTERVAL '1.5 years'), + +-- Lizzo +(18, 27, NOW() - INTERVAL '2 years'), (27, 18, NOW() - INTERVAL '2 years'), +(18, 2, NOW() - INTERVAL '1.5 years'), (2, 18, NOW() - INTERVAL '1.5 years'), +(18, 25, NOW() - INTERVAL '2 years'), (25, 18, NOW() - INTERVAL '2 years'), +(18, 26, NOW() - INTERVAL '1.5 years'), (26, 18, NOW() - INTERVAL '1.5 years'), +(18, 19, NOW() - INTERVAL '1 year'), (19, 18, NOW() - INTERVAL '1 year'), + +-- SZA +(19, 25, NOW() - INTERVAL '2 years'), (25, 19, NOW() - INTERVAL '2 years'), +(19, 6, NOW() - INTERVAL '2 years'), (6, 19, NOW() - INTERVAL '2 years'), +(19, 4, NOW() - INTERVAL '1 year'), (4, 19, NOW() - INTERVAL '1 year'), +(19, 7, NOW() - INTERVAL '1.5 years'), (7, 19, NOW() - INTERVAL '1.5 years'), +(19, 20, NOW() - INTERVAL '2 years'), (20, 19, NOW() - INTERVAL '2 years'), +(19, 21, NOW() - INTERVAL '1.5 years'), (21, 19, NOW() - INTERVAL '1.5 years'), + +-- Shawn Mendes +(20, 21, NOW() - INTERVAL '3 years'), (21, 20, NOW() - INTERVAL '3 years'), +(20, 22, NOW() - INTERVAL '2 years'), (22, 20, NOW() - INTERVAL '2 years'), +(20, 3, NOW() - INTERVAL '2 years'), (3, 20, NOW() - INTERVAL '2 years'), +(20, 4, NOW() - INTERVAL '1.5 years'), (4, 20, NOW() - INTERVAL '1.5 years'), +(20, 24, NOW() - INTERVAL '2 years'), (24, 20, NOW() - INTERVAL '2 years'), +(20, 1, NOW() - INTERVAL '3 years'), (1, 20, NOW() - INTERVAL '3 years'), + +-- Camila Cabello +(21, 7, NOW() - INTERVAL '2 years'), (7, 21, NOW() - INTERVAL '2 years'), +(21, 5, NOW() - INTERVAL '3 years'), (5, 21, NOW() - INTERVAL '3 years'), +(21, 6, NOW() - INTERVAL '3 years'), (6, 21, NOW() - INTERVAL '3 years'), +(21, 4, NOW() - INTERVAL '2 years'), (4, 21, NOW() - INTERVAL '2 years'), + +-- Charlie Puth +(22, 6, NOW() - INTERVAL '2 years'), (6, 22, NOW() - INTERVAL '2 years'), +(22, 2, NOW() - INTERVAL '1 year'), (2, 22, NOW() - INTERVAL '1 year'), +(22, 1, NOW() - INTERVAL '2 years'), (1, 22, NOW() - INTERVAL '2 years'), +(22, 25, NOW() - INTERVAL '1.5 years'), (25, 22, NOW() - INTERVAL '1.5 years'), +(22, 3, NOW() - INTERVAL '2 years'), (3, 22, NOW() - INTERVAL '2 years'), + +-- BTS +(23, 1, NOW() - INTERVAL '2 years'), (1, 23, NOW() - INTERVAL '2 years'), +(23, 2, NOW() - INTERVAL '1 year'), (2, 23, NOW() - INTERVAL '1 year'), +(23, 9, NOW() - INTERVAL '1.5 years'), (9, 23, NOW() - INTERVAL '1.5 years'), +(23, 8, NOW() - INTERVAL '2 years'), (8, 23, NOW() - INTERVAL '2 years'), +(23, 6, NOW() - INTERVAL '2 years'), (6, 23, NOW() - INTERVAL '2 years'), + +-- Zayn +(24, 6, NOW() - INTERVAL '3 years'), (6, 24, NOW() - INTERVAL '3 years'), +(24, 27, NOW() - INTERVAL '2 years'), (27, 24, NOW() - INTERVAL '2 years'), +(24, 25, NOW() - INTERVAL '1 year'), (25, 24, NOW() - INTERVAL '1 year'); + + + + + +-- Insert Friend Requests (some pending, some accepted/rejected) +INSERT INTO friend_requests (requester_id, receiver_id, status, created_at, responded_at) VALUES +(4, 11, 'pending', NOW() - INTERVAL '2 days', NULL), -- Billie -> Adele (pending) +(10, 1, 'pending', NOW() - INTERVAL '1 day', NULL), -- Weeknd -> Taylor (pending) +(7, 3, 'rejected', NOW() - INTERVAL '1 week', NOW() - INTERVAL '5 days'), -- Olivia -> Justin (rejected) +(11, 5, 'accepted', NOW() - INTERVAL '2 weeks', NOW() - INTERVAL '10 days'), -- Adele -> Dua (accepted but not in friends yet) +(6, 10, 'pending', NOW() - INTERVAL '3 days', NULL); -- Ed -> Weeknd (pending) + +-- Insert Comments +INSERT INTO comments (post_id, user_id, content, created_at) VALUES +-- Comments on Taylor's posts +(1, 2, 'Your songwriting never ceases to amaze me! 💕', NOW() - INTERVAL '1 hour'), +(1, 9, 'Can''t wait to hear them all! 🎵', NOW() - INTERVAL '45 minutes'), +(2, 8, 'Cats > people, always 😸', NOW() - INTERVAL '12 hours'), +(3, 1, 'Lucky number 13 strikes again! ✨', NOW() - INTERVAL '2 days'), + +-- Comments on Ariana's posts +(4, 1, 'thank u, next for teaching us self-love 💫', NOW() - INTERVAL '3 hours'), +(5, 12, 'Those vocals are unmatched! 🎤', NOW() - INTERVAL '1 day'), +(6, 3, 'We all make mistakes, yuh! 😂', NOW() - INTERVAL '6 days'), + +-- Comments on Justin's posts +(7, 2, 'Growth looks good on you! 🙏', NOW() - INTERVAL '5 hours'), +(8, 8, 'Puppy pics or it didn''t happen! 🐕', NOW() - INTERVAL '20 hours'), +(9, 6, 'That song will never get old 😄', NOW() - INTERVAL '3 days'), + +-- Comments on Billie's posts +(10, 7, 'Color looks amazing on you! 🌈', NOW() - INTERVAL '2 hours'), +(11, 5, 'Acoustic version when?? 😈', NOW() - INTERVAL '1 day'), +(12, 4, 'Comfort over fashion, always! 👌', NOW() - INTERVAL '4 days'), + +-- Comments on other posts +(13, 4, 'Levitate me to your next concert! 🚀', NOW() - INTERVAL '4 hours'), +(16, 2, 'Mathematical genius meets musical genius! 🔢🎵', NOW() - INTERVAL '6 hours'), +(19, 1, 'Drivers license to break hearts ✅', NOW() - INTERVAL '30 minutes'), +(22, 9, 'Kindness is everything ❤️', NOW() - INTERVAL '7 hours'), +(25, 8, 'Blinding us with talent as always 🌟', NOW() - INTERVAL '3 hours'), +(28, 6, 'Ready for album 31, always! 👋', NOW() - INTERVAL '5 hours'), + +-- Beyoncé posts (post_id 37-39) +(37, 14, 'Legend inspiring legends 🔥', NOW() - INTERVAL '1 hour'), +(37, 15, 'You were born to lead. 👑', NOW() - INTERVAL '30 minutes'), +(38, 1, 'Still thinking about that Coachella set!', NOW() - INTERVAL '2 hours'), + +-- Drake posts (post_id 40-42) +(40, 27, 'Certified 🔥', NOW() - INTERVAL '1 hour'), +(42, 13, 'Toronto salute 🇨🇦', NOW() - INTERVAL '3 hours'), + +-- Rihanna posts (post_id 43-45) +(43, 18, 'Queen of ALL trades 💄🎤', NOW() - INTERVAL '1.5 hours'), +(44, 14, 'We need that album tho...', NOW() - INTERVAL '2 hours'), +(45, 6, 'Ella ella ella ☔️ never gets old', NOW() - INTERVAL '5 hours'), + +-- Gaga posts (post_id 46-48) +(46, 25, 'You’ve always been iconic 💅', NOW() - INTERVAL '3 hours'), +(48, 13, 'The Haus is in full force 🎭', NOW() - INTERVAL '4 hours'), + +-- SZA posts (post_id 55-57) +(55, 19, 'This is why we ctrl everything 🔁', NOW() - INTERVAL '1 hour'), + +-- Halsey posts (post_id 64-66) +(66, 19, 'Your palette never misses 🎨', NOW() - INTERVAL '1.5 hours'), + +-- Doja Cat posts (post_id 67-69) +(68, 18, 'MOoo is eternal 🐄', NOW() - INTERVAL '3 hours'), + +-- Nicki Minaj posts (post_id 70-72) +(70, 14, 'Barbz forever 💗', NOW() - INTERVAL '45 minutes'); + +-- Insert Post Likes +INSERT INTO post_likes (user_id, post_id, created_at) VALUES +-- Likes on Taylor's posts +(2, 1, NOW() - INTERVAL '1.5 hours'), (9, 1, NOW() - INTERVAL '1 hour'), (8, 1, NOW() - INTERVAL '30 minutes'), +(8, 2, NOW() - INTERVAL '15 hours'), (4, 2, NOW() - INTERVAL '10 hours'), (7, 2, NOW() - INTERVAL '8 hours'), +(2, 3, NOW() - INTERVAL '2 days'), (6, 3, NOW() - INTERVAL '1 day'), + +-- Likes on Ariana's posts +(1, 4, NOW() - INTERVAL '3.5 hours'), (3, 4, NOW() - INTERVAL '2 hours'), (12, 4, NOW() - INTERVAL '1 hour'), +(12, 5, NOW() - INTERVAL '1.5 days'), (1, 5, NOW() - INTERVAL '1 day'), (8, 5, NOW() - INTERVAL '12 hours'), +(3, 6, NOW() - INTERVAL '6 days'), (1, 6, NOW() - INTERVAL '5 days'), + +-- Likes on other posts +(2, 7, NOW() - INTERVAL '5.5 hours'), (8, 7, NOW() - INTERVAL '4 hours'), +(8, 8, NOW() - INTERVAL '18 hours'), (1, 8, NOW() - INTERVAL '16 hours'), (2, 8, NOW() - INTERVAL '14 hours'), +(7, 10, NOW() - INTERVAL '2.5 hours'), (5, 10, NOW() - INTERVAL '1.5 hours'), +(4, 13, NOW() - INTERVAL '4.5 hours'), (8, 13, NOW() - INTERVAL '3 hours'), +(1, 19, NOW() - INTERVAL '45 minutes'), (9, 19, NOW() - INTERVAL '25 minutes'), +(9, 22, NOW() - INTERVAL '6.5 hours'), (1, 22, NOW() - INTERVAL '5 hours'), +(6, 28, NOW() - INTERVAL '4.5 hours'), (5, 28, NOW() - INTERVAL '3 hours'), + +-- Beyoncé (37-39) +(14, 37, NOW() - INTERVAL '1 hour'), (15, 37, NOW() - INTERVAL '45 minutes'), (1, 37, NOW() - INTERVAL '30 minutes'), (27, 37, NOW() - INTERVAL '20 minutes'), +(2, 38, NOW() - INTERVAL '1 day'), (16, 39, NOW() - INTERVAL '2 hours'), (18, 38, NOW() - INTERVAL '3 hours'), + +-- Drake (40-42) +(13, 40, NOW() - INTERVAL '1.5 hours'), (27, 40, NOW() - INTERVAL '1 hour'), (15, 41, NOW() - INTERVAL '3 hours'), +(6, 42, NOW() - INTERVAL '2 hours'), (9, 42, NOW() - INTERVAL '1 hour'), (22, 42, NOW() - INTERVAL '30 minutes'), + +-- Rihanna (43-45) +(14, 43, NOW() - INTERVAL '2 hours'), (18, 44, NOW() - INTERVAL '1 hour'), (6, 45, NOW() - INTERVAL '5 hours'), +(1, 44, NOW() - INTERVAL '3 hours'), (25, 43, NOW() - INTERVAL '2 hours'), (26, 45, NOW() - INTERVAL '1 hour'), + +-- Gaga (46-48) +(13, 46, NOW() - INTERVAL '3 hours'), (18, 46, NOW() - INTERVAL '2 hours'), (25, 47, NOW() - INTERVAL '3 hours'), +(19, 48, NOW() - INTERVAL '1 hour'), (5, 46, NOW() - INTERVAL '1 day'), (4, 47, NOW() - INTERVAL '2 days'), + +-- Khalid (49-51) +(6, 49, NOW() - INTERVAL '30 minutes'), (18, 49, NOW() - INTERVAL '1 hour'), (21, 50, NOW() - INTERVAL '45 minutes'), +(7, 50, NOW() - INTERVAL '3 hours'), (20, 51, NOW() - INTERVAL '4 hours'), + +-- Lizzo (52-54) +(16, 52, NOW() - INTERVAL '30 minutes'), (25, 52, NOW() - INTERVAL '1 hour'), (13, 53, NOW() - INTERVAL '2 hours'), +(14, 54, NOW() - INTERVAL '1 hour'), (6, 54, NOW() - INTERVAL '2 hours'), + +-- SZA (55-57) +(18, 55, NOW() - INTERVAL '1.5 hours'), (25, 55, NOW() - INTERVAL '2 hours'), (19, 55, NOW() - INTERVAL '1 hour'), +(27, 56, NOW() - INTERVAL '2 hours'), (1, 57, NOW() - INTERVAL '3 hours'), + +-- Shawn Mendes (58-60) +(21, 58, NOW() - INTERVAL '1 hour'), (22, 58, NOW() - INTERVAL '2 hours'), (20, 59, NOW() - INTERVAL '1 hour'), +(19, 60, NOW() - INTERVAL '2 hours'), (7, 58, NOW() - INTERVAL '3 hours'), + +-- Camila Cabello (61-63) +(20, 61, NOW() - INTERVAL '1 hour'), (21, 62, NOW() - INTERVAL '1.5 hours'), (18, 63, NOW() - INTERVAL '3 hours'), +(5, 62, NOW() - INTERVAL '2 days'), (6, 63, NOW() - INTERVAL '1 day'), + +-- Charlie Puth (64-66) +(14, 64, NOW() - INTERVAL '1 hour'), (1, 64, NOW() - INTERVAL '1 hour'), (25, 65, NOW() - INTERVAL '1 hour'), +(3, 66, NOW() - INTERVAL '2 hours'), (6, 66, NOW() - INTERVAL '1 hour'), + +-- BTS (67-69) +(2, 67, NOW() - INTERVAL '2 hours'), (9, 68, NOW() - INTERVAL '3 hours'), (23, 68, NOW() - INTERVAL '2 hours'), +(14, 69, NOW() - INTERVAL '4 hours'), (4, 67, NOW() - INTERVAL '2 days'), + +-- Zayn (70-72) +(15, 70, NOW() - INTERVAL '1 hour'), (24, 70, NOW() - INTERVAL '1.5 hours'), (26, 71, NOW() - INTERVAL '1 hour'), +(10, 72, NOW() - INTERVAL '2 hours'), (7, 71, NOW() - INTERVAL '3 hours'), + +-- Halsey (73-75) +(19, 73, NOW() - INTERVAL '1 hour'), (25, 74, NOW() - INTERVAL '2 hours'), (14, 75, NOW() - INTERVAL '3 hours'), +(16, 73, NOW() - INTERVAL '4 hours'), + +-- Doja Cat (76-78) +(18, 76, NOW() - INTERVAL '2 hours'), (14, 76, NOW() - INTERVAL '3 hours'), (27, 77, NOW() - INTERVAL '4 hours'), +(16, 78, NOW() - INTERVAL '5 hours'), + +-- Nicki Minaj (79-81) +(14, 79, NOW() - INTERVAL '45 minutes'), (15, 79, NOW() - INTERVAL '1 hour'), (26, 80, NOW() - INTERVAL '1 hour'), +(13, 81, NOW() - INTERVAL '3 hours'), (27, 80, NOW() - INTERVAL '1 hour'); + +-- Insert Comment Likes +INSERT INTO comment_likes (user_id, comment_id, created_at) VALUES +-- Likes on comments +(1, 1, NOW() - INTERVAL '50 minutes'), (8, 1, NOW() - INTERVAL '40 minutes'), +(1, 2, NOW() - INTERVAL '35 minutes'), (2, 2, NOW() - INTERVAL '25 minutes'), +(1, 3, NOW() - INTERVAL '10 hours'), (2, 3, NOW() - INTERVAL '8 hours'), +(2, 4, NOW() - INTERVAL '1.5 days'), (9, 4, NOW() - INTERVAL '1 day'), +(2, 5, NOW() - INTERVAL '2.5 hours'), (3, 5, NOW() - INTERVAL '2 hours'), +(1, 6, NOW() - INTERVAL '22 hours'), (2, 6, NOW() - INTERVAL '20 hours'), +(1, 7, NOW() - INTERVAL '4.5 hours'), (8, 7, NOW() - INTERVAL '3 hours'), +(3, 8, NOW() - INTERVAL '18 hours'), (1, 8, NOW() - INTERVAL '16 hours'), +(4, 9, NOW() - INTERVAL '1.5 hours'), (5, 9, NOW() - INTERVAL '1 hour'), +(7, 10, NOW() - INTERVAL '45 minutes'), (8, 10, NOW() - INTERVAL '30 minutes'), +(13, 1, NOW() - INTERVAL '30 minutes'), (15, 1, NOW() - INTERVAL '20 minutes'), +(27, 2, NOW() - INTERVAL '25 minutes'), (14, 3, NOW() - INTERVAL '1 hour'), +(13, 4, NOW() - INTERVAL '2 hours'), (27, 5, NOW() - INTERVAL '1 hour'), +(1, 5, NOW() - INTERVAL '30 minutes'),(14, 6, NOW() - INTERVAL '1 hour'), (18, 6, NOW() - INTERVAL '45 minutes'), +(6, 7, NOW() - INTERVAL '1 hour'), (25, 8, NOW() - INTERVAL '2 hours'), +(13, 9, NOW() - INTERVAL '3 hours'), (18, 9, NOW() - INTERVAL '2 hours'), +(16, 10, NOW() - INTERVAL '1.5 hours'), +(19, 11, NOW() - INTERVAL '1 hour'), (25, 11, NOW() - INTERVAL '45 minutes'), +(19, 12, NOW() - INTERVAL '1.5 hours'), (18, 12, NOW() - INTERVAL '1 hour'), +(14, 13, NOW() - INTERVAL '3 hours'), (27, 13, NOW() - INTERVAL '2 hours'), +(13, 14, NOW() - INTERVAL '1 hour'), (15, 14, NOW() - INTERVAL '45 minutes'); + +-- Insert Notifications +INSERT INTO notifications (receiving_user_id, sending_user_id, type, post_id, comment_id, is_read, created_at) VALUES +-- Post like notifications +(1, 2, 'postLike', 1, NULL, true, NOW() - INTERVAL '1.5 hours'), +(1, 9, 'postLike', 1, NULL, false, NOW() - INTERVAL '1 hour'), +(1, 8, 'postLike', 2, NULL, false, NOW() - INTERVAL '15 hours'), +(2, 1, 'postLike', 4, NULL, true, NOW() - INTERVAL '3.5 hours'), +(2, 12, 'postLike', 5, NULL, false, NOW() - INTERVAL '1.5 days'), + +-- Comment notifications +(1, 2, 'comment', 1, 1, true, NOW() - INTERVAL '1 hour'), +(1, 9, 'comment', 1, 2, false, NOW() - INTERVAL '45 minutes'), +(1, 8, 'comment', 2, 3, false, NOW() - INTERVAL '12 hours'), +(2, 1, 'comment', 4, 4, true, NOW() - INTERVAL '3 hours'), +(3, 8, 'comment', 8, 8, false, NOW() - INTERVAL '20 hours'), + +-- Comment like notifications +(2, 1, 'commentLike', 1, 1, true, NOW() - INTERVAL '50 minutes'), +(9, 1, 'commentLike', 1, 2, false, NOW() - INTERVAL '35 minutes'), +(8, 1, 'commentLike', 2, 3, false, NOW() - INTERVAL '10 hours'); + +-- Friend request notifications +--(11, 4, 'friend_request', NULL, NULL, false, NOW() - INTERVAL '2 days'), +--(1, 10, 'friend_request', NULL, NULL, false, NOW() - INTERVAL '1 day'), +--(3, 7, 'friend_request', NULL, NULL, true, NOW() - INTERVAL '1 week'), +--(5, 11, 'friend_request', NULL, NULL, true, NOW() - INTERVAL '2 weeks'), +--(10, 6, 'friend_request', NULL, NULL, false, NOW() - INTERVAL '3 days'); + +-- Summary of seeded data +SELECT + 'Data Summary' as info, + (SELECT COUNT(*) FROM users) as total_users, + (SELECT COUNT(*) FROM posts) as total_posts, + (SELECT COUNT(*) FROM comments) as total_comments, + (SELECT COUNT(*) FROM friends) as total_friendships, + (SELECT COUNT(*) FROM friend_requests) as total_friend_requests, + (SELECT COUNT(*) FROM post_likes) as total_post_likes, + (SELECT COUNT(*) FROM comment_likes) as total_comment_likes, + (SELECT COUNT(*) FROM notifications) as total_notifications; \ No newline at end of file diff --git a/src/main/resources/static/Logo.png b/src/main/resources/static/Logo.png new file mode 100644 index 000000000..de71b20cf Binary files /dev/null and b/src/main/resources/static/Logo.png differ diff --git a/src/main/resources/static/error/404.html b/src/main/resources/static/error/404.html index f530a0825..2d729c0bc 100644 --- a/src/main/resources/static/error/404.html +++ b/src/main/resources/static/error/404.html @@ -1,12 +1,133 @@ - - - - Acebook - - - OOOOOOOOOPS - - You done got 404. - + + + + + Acebook + + + + + + + + + + + + + + + + + + +
+
+
+

+ OOOOOOOOOPS + + You done got 404. +

+
+
+
+ + +
+
+

+ + + + Copyright © Acebook-Noodle 2025. All rights reserved. Acebook.Noodle@hot-shot.com + + + +

+

Designed by Jordan Gill, Sasha Parkes, Avian Schmigiel, Shanni Williams, and Harry Mcconville

+
+
+ + + + diff --git a/src/main/resources/static/favicon.png b/src/main/resources/static/favicon.png new file mode 100644 index 000000000..ff6b31e2d Binary files /dev/null and b/src/main/resources/static/favicon.png differ diff --git a/src/main/resources/static/images/AcebookBackground.webp b/src/main/resources/static/images/AcebookBackground.webp new file mode 100644 index 000000000..805ee8cc2 Binary files /dev/null and b/src/main/resources/static/images/AcebookBackground.webp differ diff --git a/src/main/resources/static/images/AcebookFull.png b/src/main/resources/static/images/AcebookFull.png new file mode 100644 index 000000000..3133c9c0b Binary files /dev/null and b/src/main/resources/static/images/AcebookFull.png differ diff --git a/src/main/resources/static/images/AcebookSmall.png b/src/main/resources/static/images/AcebookSmall.png new file mode 100644 index 000000000..8ac554435 Binary files /dev/null and b/src/main/resources/static/images/AcebookSmall.png differ diff --git a/src/main/resources/static/images/other/noodlesFavicon.png b/src/main/resources/static/images/other/noodlesFavicon.png new file mode 100644 index 000000000..5e19d882f Binary files /dev/null and b/src/main/resources/static/images/other/noodlesFavicon.png differ diff --git a/src/main/resources/static/images/profile/default.jpg b/src/main/resources/static/images/profile/default.jpg new file mode 100644 index 000000000..50cbe9f19 Binary files /dev/null and b/src/main/resources/static/images/profile/default.jpg differ diff --git a/src/main/resources/static/main.css b/src/main/resources/static/main.css index d0260873c..4cc5ec631 100644 --- a/src/main/resources/static/main.css +++ b/src/main/resources/static/main.css @@ -1,3 +1,19 @@ +html, body { + background-color: #fbf4e6 !important; +} + +h1 { + color: #655e4e !important; +} + +h2 { + color: #655e4e !important; +} + +h3 { + color: #655e4e !important; +} + .posts-main { border-collapse: collapse; } @@ -8,3 +24,145 @@ text-align: left; margin-bottom: 0.5rem;; } + +.custom-image-1 { + max-width: 500px; + max-height: 400px; + width: auto; + height: auto; +} + +.custom-image-2 { + max-width: 1000px; + max-height: 800px; + width: auto; + height: auto; +} + +.red-text { + color: #cc3939 +} + +.red-gradient-text { + background: linear-gradient(256deg, #e06d6d 0%, #cc3939 100%); + background-clip: text; + color: transparent; +} + +.btn-primary { + background: linear-gradient(256deg, #e06d6d 0%, #cc3939 100%); + border-color: #cc3939 !important; +} + +.btn-primary:hover { + background: linear-gradient(256deg, #d15656 0%, #b43030 100%); + border-color: #b43030 !important; +} + +.btn-outline-primary { + color: #cc3939 !important; + border-color: #cc3939 !important; +} + +.btn-outline-primary:hover { + color: #ffffff !important; + background-color: #cc3939 !important; + border-color: #cc3939 !important; +} + +.navbar-text, +.nav-link { + colour: #655e4e +} + +.navbar .nav-link.active { + background: linear-gradient(256deg, #e06d6d 0%, #cc3939 100%) !important; + background-clip: text !important; + color: transparent !important; +} + +.greeting { + color: #655e4e!important; +} + +.greeting:hover { + color: #655e4e!important; +} + +.navbar { + height: 70px; + background-color: #e6d7b7; + position: relative; + z-index: 1; +} + +@media (max-width: 767.98px) { + .navbar-collapse { + position: absolute; + top: 100%; + left: 0; + width: 100%; + background-color: #e6d7b7; + z-index: 0; + } +} + +.navbar-brand img { + display: block; + height: 40px; +} + +.navbar-dropdown { + background-color: #fbf4e6 !important; + color: #655e4e; +} + +.navbar-dropdown .dropdown-item, +.navbar-dropdown .fw-bold, +.navbar-dropdown .text-truncate { + background-color: #fbf4e6; + color: #655e4e; +} + +.navbar-dropdown .dropdown-item:hover { + background-color: #fbf4e6; + color: #000000; +} + +/* Optional: Divider color */ +.custom-dropdown .dropdown-divider { + border-top-color: #555; +} + + +.login-button{ + background-color: #e6d7b7!important; + outline: none !important; + border: none !important; + box-shadow: none !important; + +} +.login-button:hover{ + background-color: #c7b57b!important; + color: white!important; + outline: none !important; + border: none !important; + box-shadow: none !important; + } +.login-button:active{ + background-color: #cc3939!important; + color: white!important; + outline: none !important; + border: none !important; + box-shadow: none !important; + +} + +.search-bar { + transition: width 0.3s ease-in-out; + width: 40px; +} + +.search-bar:focus { + width: 250px; +} \ No newline at end of file diff --git a/src/main/resources/templates/friends/friends.html b/src/main/resources/templates/friends/friends.html new file mode 100644 index 000000000..61d301c9c --- /dev/null +++ b/src/main/resources/templates/friends/friends.html @@ -0,0 +1,190 @@ + + + + + Friends + + + + + + + + + + + + + + + + + + +
+
+
+ + +

+ Friend Requests +

+

You have no pending friend requests!

+
+
+
+
+
+
+
+
+ +
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+

+ Your Friends +

+

You have no friends!

+
+
+
+
+
+

+

+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ + + + Copyright © Acebook-Noodle 2025. All rights reserved. Acebook.Noodle@hot-shot.com + + + +

Designed by Jordan Gill, Sasha Parkes, Avian Schmigiel, Shanni Williams, and Harry Mcconville

+
+
+ + + + + + diff --git a/src/main/resources/templates/friends/friendsSearch.html b/src/main/resources/templates/friends/friendsSearch.html new file mode 100644 index 000000000..59586bbb5 --- /dev/null +++ b/src/main/resources/templates/friends/friendsSearch.html @@ -0,0 +1,155 @@ + + + + + + Acebook + + + + + + + + + + + + + + + + + + +
+
+
+

+ Find Friends +

+
+
+
+
+
+ + +
+
+
+

+

No users found.

+
+
+ +
+
+
+
+
+
+
+ + + + + +
+
+ + + + Copyright © Acebook-Noodle 2025. All rights reserved. Acebook.Noodle@hot-shot.com + + + +

Designed by Jordan Gill, Sasha Parkes, Avian Schmigiel, Shanni Williams, and Harry Mcconville

+
+
+ + + + + diff --git a/src/main/resources/templates/friends/profile_friends.html b/src/main/resources/templates/friends/profile_friends.html new file mode 100644 index 000000000..8c1364753 --- /dev/null +++ b/src/main/resources/templates/friends/profile_friends.html @@ -0,0 +1,174 @@ + + + + + Friends + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

+

+ +
+
+
+
+
+

+

+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ + + + Copyright © Acebook-Noodle 2025. All rights reserved. Acebook.Noodle@hot-shot.com + + + +

Designed by Jordan Gill, Sasha Parkes, Avian Schmigiel, Shanni Williams, and Harry Mcconville

+
+
+ + + + + + + diff --git a/src/main/resources/templates/genericErrorPage.html b/src/main/resources/templates/genericErrorPage.html new file mode 100644 index 000000000..6bad41b74 --- /dev/null +++ b/src/main/resources/templates/genericErrorPage.html @@ -0,0 +1,131 @@ + + + + + + Acebook + + + + + + + + + + + + + + + + + + +
+
+
+

+ Nope, try again. +

+
+
+
+ + +
+
+

+ + + + Copyright © Acebook-Noodle 2025. All rights reserved. Acebook.Noodle@hot-shot.com + + + +

+

Designed by Jordan Gill, Sasha Parkes, Avian Schmigiel, Shanni Williams, and Harry Mcconville

+
+
+ + + + + diff --git a/src/main/resources/templates/landing.html b/src/main/resources/templates/landing.html new file mode 100644 index 000000000..84c412088 --- /dev/null +++ b/src/main/resources/templates/landing.html @@ -0,0 +1,39 @@ + + + + + Acebook + + + + + + + + + + + + +
+
+ Acebook Logo Full + +
+ + + + + diff --git a/src/main/resources/templates/notifications/index.html b/src/main/resources/templates/notifications/index.html new file mode 100644 index 000000000..c7426d661 --- /dev/null +++ b/src/main/resources/templates/notifications/index.html @@ -0,0 +1,182 @@ + + + + + + + Acebook + + + + + + + + + + + + + + + + +
+
+
+

+ Friend Requests +

+
+
+
+
+
+
+
+
+
+ + + +
+ +
+
+
+ + +
+
+
+
+
+
+
+
+
+ + + + +

+ Notifications +

+

You have no notifications!

+
+
+ + + +
+
+ + + + + + +
+
+ + +
+
+
+
+
+
+ + +
+
+ + + + Copyright © Acebook-Noodle 2025. All rights reserved. Acebook.Noodle@hot-shot.com + + + +

Designed by Jordan Gill, Sasha Parkes, Avian Schmigiel, Shanni Williams, and Harry Mcconville

+
+
+ + + + + diff --git a/src/main/resources/templates/posts/index.html b/src/main/resources/templates/posts/index.html index b5ef169f1..92016cfdf 100644 --- a/src/main/resources/templates/posts/index.html +++ b/src/main/resources/templates/posts/index.html @@ -1,27 +1,194 @@ - - - - Acebook - - - - -

Posts

- -
- Signed in as + + + + + Acebook + + + + + + + + + + + + + + -
-

Content:

-

-
+ +
+
+
+

+ Posts +

+ + + + +
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+
+
+
-
    -
  • -
+
+
+
+
+
+
+ + + +
+
+ + + +
+ Time Stamp +
+
+ Post image +
+ +
+
+
+
+ + +
+ + + + Comment +  0 + + + + View Post + +
+
+
+
+
+
+
+ + + +
+
+ + + + Copyright © Acebook-Noodle 2025. All rights reserved. Acebook.Noodle@hot-shot.com + + + +

Designed by Jordan Gill, Sasha Parkes, Avian Schmigiel, Shanni Williams, and Harry Mcconville

+
+
- + + + diff --git a/src/main/resources/templates/posts/post.html b/src/main/resources/templates/posts/post.html new file mode 100644 index 000000000..c43835374 --- /dev/null +++ b/src/main/resources/templates/posts/post.html @@ -0,0 +1,256 @@ + + + + + + + Acebook + Post Detail + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+ + Time Stamp +
+
+ Post image +
+ +
+
+ +
+
+ + +
+
+ + +
+ 0 +
+ Liked by: + User, + +
+
+
+ + +
+
+
+ +
+
+
+
+ + +
+
+
+

Comments

+
+
+

No comments yet. Be the first to comment!

+
+
+

1 comment so far.

+
+
+
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+
+
+
+
+
+ + +
+
+
+ + Time Stamp +
+
+
+
+ +
+
+
+ + +
+
+ + +
+ 0 + +
+ Liked by: + + User, + +
+ +
+ + User, + +
+
+
+
+ + +
+
+ +
+ +
+
+
+
+
+
+ + + + + + +
+
+ + + + Copyright © Acebook-Noodle 2025. All rights reserved. Acebook.Noodle@hot-shot.com + + + +

Designed by Jordan Gill, Sasha Parkes, Avian Schmigiel, Shanni Williams, and Harry Mcconville

+
+
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/users/profile.html b/src/main/resources/templates/users/profile.html new file mode 100644 index 000000000..840b6afb5 --- /dev/null +++ b/src/main/resources/templates/users/profile.html @@ -0,0 +1,241 @@ + + + + + + + Acebook + + + + + + + + + + + + + + + + + +
+
+
+
+ +

+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+

+
+ + +
+
+
+
+ +
+ +
+
+ +

+

+

Your Friends +

+ +

+ +
+
+
+
+
+

+

+
+
+
+ +
+
+
+
+ View all +
+
+ View all +
+
+
+
+
+
+
+
+ +

+

+

Your Posts +

+ +
+
+
+
+ + + Time Stamp +
+
+ Post image +
+ +
+
+ + +
+
+ + +
+ + + + Comment +  0 + + + + View Post + +
+
+
+
+
+ + +
+
+ + + + Copyright © Acebook-Noodle 2025. All rights reserved. Acebook.Noodle@hot-shot.com + + + +

Designed by Jordan Gill, Sasha Parkes, Avian Schmigiel, Shanni Williams, and Harry Mcconville

+
+
+ + + + + diff --git a/src/main/resources/templates/users/settings.html b/src/main/resources/templates/users/settings.html new file mode 100644 index 000000000..dc56110e4 --- /dev/null +++ b/src/main/resources/templates/users/settings.html @@ -0,0 +1,136 @@ + + + + + + Settings + + + + + + + + + + + + + + + +
+
+
+
+

+ Update your details +

+
+ +
+
+ Profile Picture:
+ +
+
+ + +
+ +
+ + +
+
+ + +
+
+
+
+
+ + +
+
+ + + + Copyright © Acebook-Noodle 2025. All rights reserved. Acebook.Noodle@hot-shot.com + + + +

Designed by Jordan Gill, Sasha Parkes, Avian Schmigiel, Shanni Williams, and Harry Mcconville

+
+
+ + + + + diff --git a/src/test/.DS_Store b/src/test/.DS_Store new file mode 100644 index 000000000..2b4ee20c6 Binary files /dev/null and b/src/test/.DS_Store differ diff --git a/src/test/java/.DS_Store b/src/test/java/.DS_Store new file mode 100644 index 000000000..de3a99e1e Binary files /dev/null and b/src/test/java/.DS_Store differ diff --git a/src/test/java/com/.DS_Store b/src/test/java/com/.DS_Store new file mode 100644 index 000000000..4ed73db6d Binary files /dev/null and b/src/test/java/com/.DS_Store differ diff --git a/src/test/java/com/makersacademy/.DS_Store b/src/test/java/com/makersacademy/.DS_Store new file mode 100644 index 000000000..6700e0985 Binary files /dev/null and b/src/test/java/com/makersacademy/.DS_Store differ diff --git a/src/test/java/com/makersacademy/acebook/.DS_Store b/src/test/java/com/makersacademy/acebook/.DS_Store new file mode 100644 index 000000000..cb520e21b Binary files /dev/null and b/src/test/java/com/makersacademy/acebook/.DS_Store differ diff --git a/src/test/java/com/makersacademy/acebook/feature/FriendsListTest.java b/src/test/java/com/makersacademy/acebook/feature/FriendsListTest.java new file mode 100644 index 000000000..b5631f1c4 --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/feature/FriendsListTest.java @@ -0,0 +1,46 @@ +package com.makersacademy.acebook.feature; + +import com.github.javafaker.Faker; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeDriver; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; + + +public class FriendsListTest { + WebDriver driver; + Faker faker; + + @Before + public void setup() { + System.setProperty("webdriver.chrome.driver", "/usr/local/bin/chromedriver"); + driver = new ChromeDriver(); + faker = new Faker(); + } + + @After + public void tearDown() { + driver.close(); + } + + @Test + public void FriendsControllerNavigatesToFriendsList() { + String email = faker.name().username() + "@email.com"; + + driver.get("http://localhost:8080/"); + driver.findElement(By.linkText("Sign up")).click(); + driver.findElement(By.name("email")).sendKeys(email); + driver.findElement(By.name("password")).sendKeys("P@55qw0rd"); + driver.findElement(By.name("action")).click(); + driver.findElement(By.name("action")).click(); + + driver.get("http://localhost:8080/friends"); + String actualTitle = driver.getTitle(); + assertThat(actualTitle, containsString("Friends")); + } +} diff --git a/src/test/java/com/makersacademy/acebook/feature/SettingsTest.java b/src/test/java/com/makersacademy/acebook/feature/SettingsTest.java new file mode 100644 index 000000000..f1719f6e0 --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/feature/SettingsTest.java @@ -0,0 +1,151 @@ +package com.makersacademy.acebook.feature; + +import com.github.javafaker.Faker; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.List; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SettingsTest { + + WebDriver driver; + Faker faker; + + @Before + public void setup() { + System.setProperty("webdriver.chrome.driver", "/usr/local/bin/chromedriver"); + driver = new ChromeDriver(); + faker = new Faker(); + } + + @After + public void tearDown() { + driver.close(); + } + + @Test + public void successfulGetRequestOnSettingsPage() { + String email = faker.name().username() + "@email.com"; + String givenName = faker.name().firstName(); + String familyName = faker.name().lastName(); + + driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5)); + driver.get("http://localhost:8080/"); + driver.findElement(By.linkText("Login / Sign Up")).click(); + driver.findElement(By.xpath("(//a[text()='Sign Up'])[1]")).click(); + driver.findElement(By.name("email")).sendKeys(email); + driver.findElement(By.name("given_name")).sendKeys(givenName); + driver.findElement(By.name("family_name")).sendKeys(familyName); + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); + WebElement passwordField = wait.until( + ExpectedConditions.elementToBeClickable(By.id("1-password")) + ); + passwordField.sendKeys("Password123"); + driver.findElement(By.className("auth0-label-submit")).click(); + driver.findElement(By.name("action")).click(); + WebElement settingsLink = wait.until( + ExpectedConditions.elementToBeClickable(By.linkText("| Settings |")) + ); + settingsLink.click(); + String actualTitle = driver.getTitle(); + assertThat(actualTitle, containsString("Settings")); + + } + + @Test + public void successfulImageUploadToSettingsPage() { + String email = faker.name().username() + "@email.com"; + String givenName = faker.name().firstName(); + String familyName = faker.name().lastName(); + + driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5)); + driver.get("http://localhost:8080/"); + driver.findElement(By.linkText("Login / Sign Up")).click(); + driver.findElement(By.xpath("(//a[text()='Sign Up'])[1]")).click(); + driver.findElement(By.name("email")).sendKeys(email); + driver.findElement(By.name("given_name")).sendKeys(givenName); + driver.findElement(By.name("family_name")).sendKeys(familyName); + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); + WebElement passwordField = wait.until( + ExpectedConditions.elementToBeClickable(By.id("1-password")) + ); + passwordField.sendKeys("Password123"); + driver.findElement(By.className("auth0-label-submit")).click(); + driver.findElement(By.name("action")).click(); + + driver.findElement(By.linkText("| Settings |")).click(); + + WebDriverWait waitForImageField = new WebDriverWait(driver, Duration.ofSeconds(10)); + waitForImageField.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//img[@alt='User Profile Image']"))); + + Path imagePath = Paths.get("src/test/resources/Test_Profile.png"); + WebElement fileInput = driver.findElement(By.name("file")); + fileInput.sendKeys(imagePath.toAbsolutePath().toString()); + + driver.findElement(By.id("submit")).click(); + + WebDriverWait waitForImage = new WebDriverWait(driver, Duration.ofSeconds(10)); + waitForImage.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//img[@alt='User Profile Image']"))); + + WebElement fileName = driver.findElement(By.xpath("//img[@alt='User Profile Image']")); + String src = fileName.getAttribute("src"); + + Assertions.assertFalse(src.contains("default.jpg")); + } + + @Test + public void successfulNameChangeToSettingsPage() { + String email = faker.name().username() + "@email.com"; + String givenName = faker.name().firstName(); + String familyName = faker.name().lastName(); + + driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5)); + driver.get("http://localhost:8080/"); + driver.findElement(By.linkText("Login / Sign Up")).click(); + driver.findElement(By.xpath("(//a[text()='Sign Up'])[1]")).click(); + driver.findElement(By.name("email")).sendKeys(email); + driver.findElement(By.name("given_name")).sendKeys(givenName); + driver.findElement(By.name("family_name")).sendKeys(familyName); + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); + WebElement passwordField = wait.until( + ExpectedConditions.elementToBeClickable(By.id("1-password")) + ); + passwordField.sendKeys("Password123"); + driver.findElement(By.className("auth0-label-submit")).click(); + driver.findElement(By.name("action")).click(); + + driver.findElement(By.linkText("| Settings |")).click(); + + WebDriverWait waitForImageField = new WebDriverWait(driver, Duration.ofSeconds(10)); + waitForImageField.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//img[@alt='User Profile Image']"))); + + driver.findElement(By.id("last_name")).clear(); + driver.findElement(By.id("firstName")).clear(); + driver.findElement(By.id("firstName")).sendKeys("Harry"); + driver.findElement(By.id("last_name")).sendKeys("Parkes"); + driver.findElement(By.xpath("//input[@type='submit']")).click(); + + waitForImageField.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//img[@alt='User Profile Image']"))); + List headers = driver.findElements(By.tagName("h2")); + assertEquals(headers.get(0).getText(), "First name: Harry"); + assertEquals(headers.get(1).getText(), "Surname: Parkes"); + + } + + } diff --git a/src/test/java/com/makersacademy/acebook/feature/SignUpTest.java b/src/test/java/com/makersacademy/acebook/feature/SignUpTest.java index dcb1416bb..16ea1f492 100644 --- a/src/test/java/com/makersacademy/acebook/feature/SignUpTest.java +++ b/src/test/java/com/makersacademy/acebook/feature/SignUpTest.java @@ -7,7 +7,15 @@ import org.junit.Test; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import java.time.Duration; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; public class SignUpTest { @@ -27,16 +35,27 @@ public void tearDown() { } @Test - public void successfulSignUpAlsoLogsInUser() { + public void userCanSignUp() { String email = faker.name().username() + "@email.com"; + String givenName = faker.name().firstName(); + String familyName = faker.name().lastName(); + driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5)); driver.get("http://localhost:8080/"); - driver.findElement(By.linkText("Sign up")).click(); + driver.findElement(By.linkText("Login / Sign Up")).click(); + driver.findElement(By.xpath("(//a[text()='Sign Up'])[1]")).click(); driver.findElement(By.name("email")).sendKeys(email); - driver.findElement(By.name("password")).sendKeys("P@55qw0rd"); + driver.findElement(By.name("given_name")).sendKeys(givenName); + driver.findElement(By.name("family_name")).sendKeys(familyName); + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); + WebElement passwordField = wait.until( + ExpectedConditions.elementToBeClickable(By.id("1-password")) + ); + passwordField.sendKeys("Password123"); + driver.findElement(By.className("auth0-label-submit")).click(); driver.findElement(By.name("action")).click(); - driver.findElement(By.name("action")).click(); - String greetingText = driver.findElement(By.id("greeting")).getText(); - Assert.assertEquals("Signed in as " + email, greetingText); + String actualTitle = driver.getTitle(); + assertThat(actualTitle, containsString("Acebook")); + } } diff --git a/src/test/java/com/makersacademy/acebook/model/FriendRequestTest.java b/src/test/java/com/makersacademy/acebook/model/FriendRequestTest.java new file mode 100644 index 000000000..0534f28bd --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/model/FriendRequestTest.java @@ -0,0 +1,26 @@ +package com.makersacademy.acebook.model; + +import static net.bytebuddy.matcher.ElementMatchers.is; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; + +import com.makersacademy.acebook.repository.FriendRequestRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.sql.Timestamp; + + +public class FriendRequestTest { + + @Autowired + FriendRequestRepository repository; + + @Test + public void friendRequestHasContent() { + FriendRequest request = new FriendRequest(null, 1L, 2L, "unapproved", new Timestamp(System.currentTimeMillis()), null); + repository.save(request); + assertThat(request.getStatus(), containsString("unapproved")); + } +} diff --git a/src/test/java/com/makersacademy/acebook/model/PostTest.java b/src/test/java/com/makersacademy/acebook/model/PostTest.java index 926d9b1bf..9f0e94e79 100644 --- a/src/test/java/com/makersacademy/acebook/model/PostTest.java +++ b/src/test/java/com/makersacademy/acebook/model/PostTest.java @@ -5,9 +5,15 @@ import org.junit.jupiter.api.Test; +import java.sql.Timestamp; +import java.time.Instant; + public class PostTest { - private Post post = new Post("hello"); + Instant instant = Instant.now(); + Timestamp now = Timestamp.from(instant); + + private Post post = new Post(null, "hello", 1L, now, null, null); @Test public void postHasContent() { diff --git a/src/test/java/com/makersacademy/acebook/model/UserTest.java b/src/test/java/com/makersacademy/acebook/model/UserTest.java new file mode 100644 index 000000000..e6d443189 --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/model/UserTest.java @@ -0,0 +1,27 @@ +package com.makersacademy.acebook.model; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class UserTest { + + private User user = new User("username", "Sasha", "Parkes", "static/images/profile/default.jpeg"); + + @Test + public void userHasContent() {assertThat(user.getUsername(), containsString("username"));} + + @Test + public void userIsEnabled() {assertEquals(true, user.isEnabled());} + + @Test + public void userHasFirstName() {assertThat(user.getFirstName(), containsString("Sasha"));} + + @Test + public void userHasLastName() {assertThat(user.getLastName(), containsString("Parkes"));} + + @Test + public void userHasProfilePic() {assertThat(user.getProfilePic(), containsString("images/profile/default.jpeg"));} +} diff --git a/src/test/resources/Test_Profile.png b/src/test/resources/Test_Profile.png new file mode 100644 index 000000000..a934b14ec Binary files /dev/null and b/src/test/resources/Test_Profile.png differ diff --git a/uploads/post_images/30.jpg b/uploads/post_images/30.jpg new file mode 100644 index 000000000..72576811c Binary files /dev/null and b/uploads/post_images/30.jpg differ diff --git a/uploads/post_images/31.jpg b/uploads/post_images/31.jpg new file mode 100644 index 000000000..686dacc50 Binary files /dev/null and b/uploads/post_images/31.jpg differ diff --git a/uploads/post_images/37.jpg b/uploads/post_images/37.jpg new file mode 100644 index 000000000..72576811c Binary files /dev/null and b/uploads/post_images/37.jpg differ diff --git a/uploads/post_images/39.jpg b/uploads/post_images/39.jpg new file mode 100644 index 000000000..ea6a165f9 Binary files /dev/null and b/uploads/post_images/39.jpg differ diff --git a/uploads/post_images/39.png b/uploads/post_images/39.png new file mode 100644 index 000000000..536612bfc Binary files /dev/null and b/uploads/post_images/39.png differ diff --git a/uploads/post_images/41.jpg b/uploads/post_images/41.jpg new file mode 100644 index 000000000..5517d911a Binary files /dev/null and b/uploads/post_images/41.jpg differ diff --git a/uploads/post_images/abel_post.jpg b/uploads/post_images/abel_post.jpg new file mode 100644 index 000000000..f74227fe8 Binary files /dev/null and b/uploads/post_images/abel_post.jpg differ diff --git a/uploads/post_images/adele_post.jpg b/uploads/post_images/adele_post.jpg new file mode 100644 index 000000000..f44543ec0 Binary files /dev/null and b/uploads/post_images/adele_post.jpg differ diff --git a/uploads/post_images/ariana_post.jpeg b/uploads/post_images/ariana_post.jpeg new file mode 100644 index 000000000..81ed03bcc Binary files /dev/null and b/uploads/post_images/ariana_post.jpeg differ diff --git a/uploads/post_images/billie_post.jpg b/uploads/post_images/billie_post.jpg new file mode 100644 index 000000000..b88eb327d Binary files /dev/null and b/uploads/post_images/billie_post.jpg differ diff --git a/uploads/post_images/bruno_post.png b/uploads/post_images/bruno_post.png new file mode 100644 index 000000000..11c0289fa Binary files /dev/null and b/uploads/post_images/bruno_post.png differ diff --git a/uploads/post_images/dua_post.jpg b/uploads/post_images/dua_post.jpg new file mode 100644 index 000000000..3a692e556 Binary files /dev/null and b/uploads/post_images/dua_post.jpg differ diff --git a/uploads/post_images/ed_post.jpg b/uploads/post_images/ed_post.jpg new file mode 100644 index 000000000..9c53d421d Binary files /dev/null and b/uploads/post_images/ed_post.jpg differ diff --git a/uploads/post_images/folder_for_post_images.txt b/uploads/post_images/folder_for_post_images.txt new file mode 100644 index 000000000..e69de29bb diff --git a/uploads/post_images/harry_post.jpg b/uploads/post_images/harry_post.jpg new file mode 100644 index 000000000..42fd1baeb Binary files /dev/null and b/uploads/post_images/harry_post.jpg differ diff --git a/uploads/post_images/justin_post.jpg b/uploads/post_images/justin_post.jpg new file mode 100644 index 000000000..91482ad81 Binary files /dev/null and b/uploads/post_images/justin_post.jpg differ diff --git a/uploads/post_images/olivia_post.jpg b/uploads/post_images/olivia_post.jpg new file mode 100644 index 000000000..bafe78bab Binary files /dev/null and b/uploads/post_images/olivia_post.jpg differ diff --git a/uploads/post_images/selena_post.png b/uploads/post_images/selena_post.png new file mode 100644 index 000000000..536612bfc Binary files /dev/null and b/uploads/post_images/selena_post.png differ diff --git a/uploads/post_images/taylor_post.jpg b/uploads/post_images/taylor_post.jpg new file mode 100644 index 000000000..f63b55945 Binary files /dev/null and b/uploads/post_images/taylor_post.jpg differ diff --git a/uploads/user_profile/13.jpg b/uploads/user_profile/13.jpg new file mode 100644 index 000000000..690a83bd4 Binary files /dev/null and b/uploads/user_profile/13.jpg differ diff --git a/uploads/user_profile/13.png b/uploads/user_profile/13.png new file mode 100644 index 000000000..536612bfc Binary files /dev/null and b/uploads/user_profile/13.png differ diff --git a/uploads/user_profile/abel.jpg b/uploads/user_profile/abel.jpg new file mode 100644 index 000000000..5517d911a Binary files /dev/null and b/uploads/user_profile/abel.jpg differ diff --git a/uploads/user_profile/adele.jpg b/uploads/user_profile/adele.jpg new file mode 100644 index 000000000..c7bfdfe20 Binary files /dev/null and b/uploads/user_profile/adele.jpg differ diff --git a/uploads/user_profile/ariana.jpg b/uploads/user_profile/ariana.jpg new file mode 100644 index 000000000..036593119 Binary files /dev/null and b/uploads/user_profile/ariana.jpg differ diff --git a/uploads/user_profile/beyonce.jpeg b/uploads/user_profile/beyonce.jpeg new file mode 100644 index 000000000..234b0fb8c Binary files /dev/null and b/uploads/user_profile/beyonce.jpeg differ diff --git a/uploads/user_profile/billie.jpg b/uploads/user_profile/billie.jpg new file mode 100644 index 000000000..7102018df Binary files /dev/null and b/uploads/user_profile/billie.jpg differ diff --git a/uploads/user_profile/bruno.jpg b/uploads/user_profile/bruno.jpg new file mode 100644 index 000000000..09ea1af38 Binary files /dev/null and b/uploads/user_profile/bruno.jpg differ diff --git a/uploads/user_profile/bts.jpeg b/uploads/user_profile/bts.jpeg new file mode 100644 index 000000000..2c0d30813 Binary files /dev/null and b/uploads/user_profile/bts.jpeg differ diff --git a/uploads/user_profile/camila.jpg b/uploads/user_profile/camila.jpg new file mode 100644 index 000000000..59929a5f3 Binary files /dev/null and b/uploads/user_profile/camila.jpg differ diff --git a/uploads/user_profile/charlie.jpg b/uploads/user_profile/charlie.jpg new file mode 100644 index 000000000..1e63c6aed Binary files /dev/null and b/uploads/user_profile/charlie.jpg differ diff --git a/uploads/user_profile/doja.jpg b/uploads/user_profile/doja.jpg new file mode 100644 index 000000000..fd76b4c04 Binary files /dev/null and b/uploads/user_profile/doja.jpg differ diff --git a/uploads/user_profile/drake.jpeg b/uploads/user_profile/drake.jpeg new file mode 100644 index 000000000..c81e7580c Binary files /dev/null and b/uploads/user_profile/drake.jpeg differ diff --git a/uploads/user_profile/dua.jpg b/uploads/user_profile/dua.jpg new file mode 100644 index 000000000..c15dfc96a Binary files /dev/null and b/uploads/user_profile/dua.jpg differ diff --git a/uploads/user_profile/ed.jpg b/uploads/user_profile/ed.jpg new file mode 100644 index 000000000..b2d0388c3 Binary files /dev/null and b/uploads/user_profile/ed.jpg differ diff --git a/uploads/user_profile/folder_for_profile_images.txt b/uploads/user_profile/folder_for_profile_images.txt new file mode 100644 index 000000000..897cd7f7a --- /dev/null +++ b/uploads/user_profile/folder_for_profile_images.txt @@ -0,0 +1 @@ +git m \ No newline at end of file diff --git a/uploads/user_profile/gaga.jpg b/uploads/user_profile/gaga.jpg new file mode 100644 index 000000000..8c6219a62 Binary files /dev/null and b/uploads/user_profile/gaga.jpg differ diff --git a/uploads/user_profile/halsey.jpg b/uploads/user_profile/halsey.jpg new file mode 100644 index 000000000..65f73dceb Binary files /dev/null and b/uploads/user_profile/halsey.jpg differ diff --git a/uploads/user_profile/harry.jpg b/uploads/user_profile/harry.jpg new file mode 100644 index 000000000..6efad45da Binary files /dev/null and b/uploads/user_profile/harry.jpg differ diff --git a/uploads/user_profile/justin.jpg b/uploads/user_profile/justin.jpg new file mode 100644 index 000000000..5ffbb5d6a Binary files /dev/null and b/uploads/user_profile/justin.jpg differ diff --git a/uploads/user_profile/khalid.jpeg b/uploads/user_profile/khalid.jpeg new file mode 100644 index 000000000..9bdbf39e7 Binary files /dev/null and b/uploads/user_profile/khalid.jpeg differ diff --git a/uploads/user_profile/lizzo.jpeg b/uploads/user_profile/lizzo.jpeg new file mode 100644 index 000000000..35844d0ad Binary files /dev/null and b/uploads/user_profile/lizzo.jpeg differ diff --git a/uploads/user_profile/nicki.jpg b/uploads/user_profile/nicki.jpg new file mode 100644 index 000000000..0702366fe Binary files /dev/null and b/uploads/user_profile/nicki.jpg differ diff --git a/uploads/user_profile/olivia.jpg b/uploads/user_profile/olivia.jpg new file mode 100644 index 000000000..5492f0ad3 Binary files /dev/null and b/uploads/user_profile/olivia.jpg differ diff --git a/uploads/user_profile/rihanna.jpg b/uploads/user_profile/rihanna.jpg new file mode 100644 index 000000000..d900a2abd Binary files /dev/null and b/uploads/user_profile/rihanna.jpg differ diff --git a/uploads/user_profile/selena.jpg b/uploads/user_profile/selena.jpg new file mode 100644 index 000000000..ebfa1e070 Binary files /dev/null and b/uploads/user_profile/selena.jpg differ diff --git a/uploads/user_profile/shawn.jpg b/uploads/user_profile/shawn.jpg new file mode 100644 index 000000000..f400241c2 Binary files /dev/null and b/uploads/user_profile/shawn.jpg differ diff --git a/uploads/user_profile/sza.jpg b/uploads/user_profile/sza.jpg new file mode 100644 index 000000000..3002feb2e Binary files /dev/null and b/uploads/user_profile/sza.jpg differ diff --git a/uploads/user_profile/taylor.jpg b/uploads/user_profile/taylor.jpg new file mode 100644 index 000000000..91ba659f7 Binary files /dev/null and b/uploads/user_profile/taylor.jpg differ diff --git a/uploads/user_profile/zayn.jpg b/uploads/user_profile/zayn.jpg new file mode 100644 index 000000000..24f439ecb Binary files /dev/null and b/uploads/user_profile/zayn.jpg differ