diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..d1447330a Binary files /dev/null and b/.DS_Store differ 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/.DS_Store b/src/.DS_Store new file mode 100644 index 000000000..0b3ee9cb1 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/main/.DS_Store b/src/main/.DS_Store new file mode 100644 index 000000000..cdaed1736 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..918f7b774 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/CommentController.java @@ -0,0 +1,58 @@ +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.PostRepository; +import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.CommentService; +import java.util.List; + +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; + +@Controller +@RequestMapping("/posts/comments") +public class CommentController { + + private final CommentService commentService; + private final PostRepository postRepository; + private final UserRepository userRepository; + + public CommentController(CommentService commentService, PostRepository postRepository, UserRepository userRepository) { + this.commentService = commentService; + this.postRepository = postRepository; + this.userRepository = userRepository; + } + + + @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); + return new RedirectView("/posts/" + post.getId()); + } + + +} \ No newline at end of file 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..5d8b6cace --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/FriendsController.java @@ -0,0 +1,188 @@ +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 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; + + @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()); + } + } + + modelAndView.addObject("friendUsers", friendUsers); + modelAndView.addObject("requesterUsers", requesterUsers); + + 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); + friendRequestRepository.save(request); + + return new RedirectView("/friends"); + } + +// @PostMapping("/friends") +// public RedirectView create(@ModelAttribute Friend friend, @AuthenticationPrincipal(expression = "attributes['email']") String email) { +// Optional user = userRepository.findUserByUsername(email); +// if (user.isPresent()) { +// Long id = user.get().getId(); +// } +// return new RedirectView("friends/friends"); +// } + } + 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..7ab848f8f --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/NotificationsController.java @@ -0,0 +1,65 @@ +package com.makersacademy.acebook.controller; + +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 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.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; + + + // Get notifications for current user + @GetMapping("/notifications") + public String index(Model model) { + DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder + .getContext() + .getAuthentication() + .getPrincipal(); + String username = (String) principal.getAttributes().get("email"); + User currentUser = userRepository.findUserByUsername(username).get(); + + Collection notifications = new ArrayList<>(); + notifications.addAll(notificationRepository.findByReceivingUserIdOrderByCreatedAtDesc(2L)); + Map senderNames = new HashMap<>(); + for (Notification notification : notifications) { + Optional sender = userRepository.findById(notification.getSendingUserId()); + sender.ifPresent(user -> senderNames.put(notification.getId(), user.getFirstName())); + } + model.addAttribute("notifications", notifications); + model.addAttribute("senderNames", senderNames); + 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); + Optional readNotification = notificationRepository.findById(notificationId); + if (readNotification.isPresent()) { + Notification activeNotification = readNotification.get(); + activeNotification.setRead(true); + notificationRepository.save(activeNotification); + String postId = Long.toString(activeNotification.getPostId()); + return new RedirectView("/posts/" + postId); + } + else { + return new RedirectView("genericErrorPage"); + } + } +} 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..72ed4308e --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/PostLikeController.java @@ -0,0 +1,34 @@ +package com.makersacademy.acebook.controller; + +import com.makersacademy.acebook.service.PostLikeService; +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") +public class PostLikeController { + private final PostLikeService postLikeService; + public PostLikeController(PostLikeService postLikeService) { + this.postLikeService = postLikeService; + } + + @Transactional + @PostMapping("/posts/{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("/posts/{postId}/unlike") + public RedirectView unlikePost(@PathVariable Long postId, + @AuthenticationPrincipal(expression = "attributes['email']") String email) { + postLikeService.unlikePost(postId, email); + return new RedirectView("/posts/" + postId); + } +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/controller/PostsController.java b/src/main/java/com/makersacademy/acebook/controller/PostsController.java index 57a7e5f4d..7db53439d 100644 --- a/src/main/java/com/makersacademy/acebook/controller/PostsController.java +++ b/src/main/java/com/makersacademy/acebook/controller/PostsController.java @@ -1,32 +1,193 @@ 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.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.transaction.annotation.Transactional; 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.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.List; +import java.util.Optional; +import lombok.*; + @Controller public class PostsController { @Autowired - PostRepository repository; + PostRepository postRepository; + @Autowired + PostLikeRepository postLikeRepository; + @Autowired + PostService postService; + @Autowired + PostLikeService postLikeService; + @Autowired + UserRepository userRepository; + @Autowired + ImageStorageService imageStorageService; + + // private final PostRepository postRepository; + // private final UserRepository userRepository; + 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(); + // Finds all posts and shows them in reverse chronological order + Iterable posts = postRepository.findByOrderByTimePostedDesc(); model.addAttribute("posts", posts); model.addAttribute("post", new Post()); + + // 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 and assign to username var + // Then utilizes the 'username' variable to search the database and return matching User object + // Adds the user object to the model (page) + String username = (String) principal.getAttributes().get("email"); + User user = userRepository.findUserByUsername(username).get(); + model.addAttribute("user", user); + return "posts/index"; } + + // Create new post @PostMapping("/posts") - public RedirectView create(@ModelAttribute Post post) { - repository.save(post); + public RedirectView create( + @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); + } + return new RedirectView("/posts"); } + + + //View specific post + @GetMapping("/posts/{id}") + public ModelAndView viewPost(@PathVariable("id") Long id) { + 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(); + + modelAndView.addObject("post", postDto); + modelAndView.addObject("comments", commentDtos); + modelAndView.addObject("newComment", new Comment()); + return modelAndView; + } + } + + @Transactional + 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); + } + } + + @Transactional + public void unlikePost(Long userId, Long postId) { + postLikeRepository.deleteByUserIdAndPostId(userId, postId); + } + + public long getLikesCount(Long postId) { + return postLikeRepository.countByPostId(postId); + } } + diff --git a/src/main/java/com/makersacademy/acebook/controller/UsersController.java b/src/main/java/com/makersacademy/acebook/controller/UsersController.java index a7c9db1d8..cfda6faa5 100644 --- a/src/main/java/com/makersacademy/acebook/controller/UsersController.java +++ b/src/main/java/com/makersacademy/acebook/controller/UsersController.java @@ -2,29 +2,167 @@ import com.makersacademy.acebook.model.User; import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.ImageStorageService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; 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.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Optional; + @RestController public class UsersController { @Autowired UserRepository userRepository; + @Autowired + private ImageStorageService imageStorageService; + + // 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, utilising 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()); + ModelAndView settings = new ModelAndView("/users/settings"); + 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"); + } } +// +// // Function to handle saving an uploaded image - takes a file and a userId string (for unique saving) as inputs) +// private String saveImage(MultipartFile file, String userId) throws IOException { +// +// // Creates new uploadPath item referencing the uploadDirectory object at top of the page +// // This is configured in application.properties +// // If it doesn't exist - create it +// Path uploadPath = Paths.get(uploadDir); +// if (!Files.exists(uploadPath)) { +// Files.createDirectories(uploadPath); +// } +// +// // Checks that the content type of the file is of type JPEG or PNG +// // else - Error handling TBC +// String contentType = file.getContentType(); +// if (!contentType.equals("image/jpeg") && !contentType.equals("image/png")) { +// throw new IllegalArgumentException("Only JPEG or PNG images are allowed"); +// } +// +// /* +// Compiles elements for the filename +// Uses original filename, and calls getFileExtension function below to extract .jpg / .png +// Then concatenates that with the userId - so when user #1 changes prof pic - it will always save as 1.png/.jpg +// This then links with the uploadDir variable above - so it creates a complete file path, within the correct +// directory, and with our chosen filename +// Bottom line replaces existing filepath - so overwrites user's existing prof_pic with a new one +// */ +// String fileName = file.getOriginalFilename(); +// String extension = getFileExtension(fileName); +// String newFileName = userId + extension; +// Path filePath = uploadPath.resolve(newFileName); +// Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING); +// +// return newFileName; +// } +// +// // Function to extract file extension e.g. .jpeg / .png - takes a filename as an input +// public static String getFileExtension(String fileName) { +// +// // Allows the item to be null +// // Filters the string for instances of '.' +// // Creates a substring from the last instance of '.' onwards - this isolates the extension +// // else returns nothing (?) +// return Optional.ofNullable(fileName) +// .filter(f -> f.contains(".")) +// .map(f -> f.substring(f.lastIndexOf("."))) +// .orElse(""); +// } +// +// +//} 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..34061a2cd --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/Comment.java @@ -0,0 +1,38 @@ +package com.makersacademy.acebook.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "comments") +@Getter +@Setter +@NoArgsConstructor +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @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..a7b5c7d99 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/Notification.java @@ -0,0 +1,34 @@ +package com.makersacademy.acebook.model; + + +import jakarta.persistence.*; +import lombok.*; +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="is_read") + private boolean isRead; + @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..e0a5ffcc0 100644 --- a/src/main/java/com/makersacademy/acebook/model/Post.java +++ b/src/main/java/com/makersacademy/acebook/model/Post.java @@ -1,22 +1,40 @@ package com.makersacademy.acebook.model; import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; -import lombok.Data; +import java.sql.Timestamp; +import java.util.List; @Data @Entity @Table(name = "POSTS") +@AllArgsConstructor +@NoArgsConstructor public class Post { @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; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", insertable = false, updatable = false) + private User user; + - public Post(String content) { - this.content = content; + public Post(Long id) { + this.id = id; } } 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..2dacc1800 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/PostLike.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 = "post_likes", uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "post_id"})) +@IdClass(PostLikeId.class) +@Getter +@Setter +@NoArgsConstructor +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..958205898 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/CommentRepository.java @@ -0,0 +1,16 @@ +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); +} \ No newline at end of file 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..d4fd07754 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/FriendRepository.java @@ -0,0 +1,15 @@ +package com.makersacademy.acebook.repository; + +import com.makersacademy.acebook.model.Friend; +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); +} 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..6327370bf --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/FriendRequestRepository.java @@ -0,0 +1,18 @@ +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); +} 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..543138be5 --- /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.domain.Sort; +import org.springframework.data.repository.CrudRepository; +import java.util.List; + +public interface NotificationRepository extends CrudRepository { + List findAllByReceivingUserId(Long receivingUserId); + List findByReceivingUserIdOrderByCreatedAtDesc(Long receivingUserId); +} 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..64a5943c1 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.Notification; import com.makersacademy.acebook.model.Post; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; -public interface PostRepository extends CrudRepository { +import java.util.List; +public interface PostRepository extends CrudRepository { + List findByUserIdOrderByTimePostedDesc(Long receivingUserId); + List findByOrderByTimePostedDesc(); } 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..cca6436c7 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/service/CommentLikeService.java @@ -0,0 +1,60 @@ +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; + + public CommentLikeService(CommentLikeRepository commentLikeRepository, UserRepository userRepository) { + this.commentLikeRepository = commentLikeRepository; + this.userRepository = userRepository; + } + + 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); + } + + } + + 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..54233a949 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/service/CommentService.java @@ -0,0 +1,61 @@ +package com.makersacademy.acebook.service; +import com.makersacademy.acebook.service.CommentService; + +import com.makersacademy.acebook.model.Comment; +import com.makersacademy.acebook.model.CommentLike; +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.CommentLikeRepository; +import com.makersacademy.acebook.repository.CommentRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +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); + } + + @Transactional + public void likeComment(Long userId, Long commentId) { + boolean alreadyLiked = commentLikeRepository + .findByUserIdAndCommentId(userId, commentId) + .isPresent(); + + if (!alreadyLiked) { + CommentLike like = new CommentLike(); + like.setUserId(userId); + like.setCommentId(commentId); + commentLikeRepository.save(like); + } + } + + @Transactional + public void unlikeComment(Long userId, Long commentId) { + commentLikeRepository.deleteByUserIdAndCommentId(userId, commentId); + } + + public long getLikesCount(Long commentId) { + return commentLikeRepository.countByCommentId(commentId); + } + + + +} \ No newline at end of file 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/PostLikeService.java b/src/main/java/com/makersacademy/acebook/service/PostLikeService.java new file mode 100644 index 000000000..4373f92a8 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/service/PostLikeService.java @@ -0,0 +1,60 @@ +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; + + public PostLikeService(PostLikeRepository postLikeRepository, UserRepository userRepository) { + this.postLikeRepository = postLikeRepository; + this.userRepository = userRepository; + } + + 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); + } + + } + + 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..51ebe6676 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/service/PostService.java @@ -0,0 +1,56 @@ +package com.makersacademy.acebook.service; +import com.makersacademy.acebook.service.PostService; + +import com.makersacademy.acebook.model.Post; +import com.makersacademy.acebook.model.PostLike; +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.PostLikeRepository; +import com.makersacademy.acebook.repository.PostRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class PostService { + + @Autowired + PostRepository postRepository; + @Autowired + PostLikeRepository postLikeRepository; + + public PostService(PostRepository postRepository, + PostLikeRepository postLikeRepository) { + this.postRepository = postRepository; + this.postLikeRepository = postLikeRepository; + } + + public Post newPost(Post post) { + return postRepository.save(post); + } + + @Transactional + 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); + } + } + + @Transactional + public void unlikePost(Long userId, Long postId) { + postLikeRepository.deleteByUserIdAndPostId(userId, postId); + } + + public long getLikesCount(Long postId) { + return postLikeRepository.countByPostId(postId); + } +} \ No newline at end of file diff --git a/src/main/resources/.DS_Store b/src/main/resources/.DS_Store new file mode 100644 index 000000000..2e6041a15 Binary files /dev/null and b/src/main/resources/.DS_Store differ 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..29f3cb71e --- /dev/null +++ b/src/main/resources/seeds/acebook_seeds.sql @@ -0,0 +1,235 @@ +-- 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'); + +-- 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', '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', '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', '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', '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', 'dua_post.gif'), +('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', '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', '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', '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', 'selena_post.jpg'), +('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', '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', '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', 'bruno_post.jpg'), +('Count on me to bring the groove to every situation', 12, NOW() - INTERVAL '6 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 + +-- 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'); + +-- 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'); + +-- 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'); + +-- 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, 'post_like', 1, NULL, true, NOW() - INTERVAL '1.5 hours'), +(1, 9, 'post_like', 1, NULL, false, NOW() - INTERVAL '1 hour'), +(1, 8, 'post_like', 2, NULL, false, NOW() - INTERVAL '15 hours'), +(2, 1, 'post_like', 4, NULL, true, NOW() - INTERVAL '3.5 hours'), +(2, 12, 'post_like', 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, 'comment_like', 1, 1, true, NOW() - INTERVAL '50 minutes'), +(9, 1, 'comment_like', 1, 2, false, NOW() - INTERVAL '35 minutes'), +(8, 1, 'comment_like', 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/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/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..0ed2099a8 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..ed118232b 100644 --- a/src/main/resources/static/main.css +++ b/src/main/resources/static/main.css @@ -1,3 +1,7 @@ +html, body { + background-color: #fbf4e6 !important; +} + .posts-main { border-collapse: collapse; } @@ -8,3 +12,89 @@ text-align: left; margin-bottom: 0.5rem;; } + +.navbar-text, +.nav-link { + colour: #655e4e +} + +.navbar .nav-link.active { + color: #cc3939 !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; + +} \ 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..a562ab406 --- /dev/null +++ b/src/main/resources/templates/friends/friends.html @@ -0,0 +1,82 @@ + + + + + Friends + + + + + + + + + + + + + + +

Friend Requests

+
    +
  • + +
    + + +
    +
  • +
+ +

Your Friends

+
    +
  • + +
    + +
    +
  • +
+ + + + + + + + + diff --git a/src/main/resources/templates/genericErrorPage.html b/src/main/resources/templates/genericErrorPage.html new file mode 100644 index 000000000..592a51214 --- /dev/null +++ b/src/main/resources/templates/genericErrorPage.html @@ -0,0 +1,68 @@ + + + + + + Acebook + + + + + + + + + + + + + +
+

Nope, that didn't work. +

+
+ + + + + + + + + + diff --git a/src/main/resources/templates/landing.html b/src/main/resources/templates/landing.html new file mode 100644 index 000000000..23db9518f --- /dev/null +++ b/src/main/resources/templates/landing.html @@ -0,0 +1,24 @@ + + + + + Acebook + + + + + + + + + + + + + + + + diff --git a/src/main/resources/templates/notifications/index.html b/src/main/resources/templates/notifications/index.html new file mode 100644 index 000000000..c15d4c46c --- /dev/null +++ b/src/main/resources/templates/notifications/index.html @@ -0,0 +1,82 @@ + + + + + + + + + Acebook + + + + + + + + + + + + + +

Notifications

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



Posts

-
-

Content:

-

-
+
+
+ +

Say Something:

+ + + +
+
-
    -
  • -
+
+
+
+
+
+ + + posted: + + Time Stamp? +
+
+ + Post image +
+ +
+
+
+
+
+
+ + diff --git a/src/main/resources/templates/posts/post.html b/src/main/resources/templates/posts/post.html new file mode 100644 index 000000000..2f36b5598 --- /dev/null +++ b/src/main/resources/templates/posts/post.html @@ -0,0 +1,151 @@ + + + + + + + Acebook + Post Detail + + + + + + + + + + + + +
+ + User Profile Image +

+ Post image + + +
+
+ + +
+
+ + +
+
+ +

Likes: 0

+ +

+ Liked by: + + User, + +

+
+ +
+ +
+

Comments

+
+

No comments yet. Be the first to comment!

+
+ +
    +
  • +

    +

    + + User Profile Image + + Name said: +

    +

    +

    Date

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

    Likes: 0

    + + +

    + Liked by: + + User, + +

    +
  • +
+
+ +
+ +
+
Add a Comment
+
+ + +
+ +
+
+ + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/users/settings.html b/src/main/resources/templates/users/settings.html new file mode 100644 index 000000000..52d77a358 --- /dev/null +++ b/src/main/resources/templates/users/settings.html @@ -0,0 +1,74 @@ + + + + + + Settings + + + + + + + + + + + +

+

+User Profile Image + +
+ +
+ +
+ +
+ + +
+ + + + + + + + + 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/38.png b/uploads/post_images/38.png new file mode 100644 index 000000000..13120893e Binary files /dev/null and b/uploads/post_images/38.png 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/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/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..033ab7de6 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/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/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/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/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/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/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