diff --git a/build.gradle b/build.gradle index 2df78a215f1cf6f7acd505e995ef8add9e50a1f4..0eed974f7a02792eda7daacc68a5c30486d188c5 100644 --- a/build.gradle +++ b/build.gradle @@ -60,7 +60,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.security:spring-security-test' - + testImplementation 'org.mockito:mockito-core:4.0.0' // Mocking library + testImplementation 'org.mockito:mockito-junit-jupiter:4.0.0' // JUnit 5 support for Mockito + } diff --git a/src/main/java/polish_community_group_11/polish_community/Categories/Categories.java b/src/main/java/polish_community_group_11/polish_community/Categories/Categories.java index 972d329514fc237cf017adbb61ae257737f7c5e0..e4f0f3b186f5f74fd74f84b280ed878aa27e9faa 100644 --- a/src/main/java/polish_community_group_11/polish_community/Categories/Categories.java +++ b/src/main/java/polish_community_group_11/polish_community/Categories/Categories.java @@ -16,7 +16,7 @@ public class Categories { @Column(nullable = false, length = 255) private String categoryDescription; - @Column(nullable = false) + @Column(nullable = true) private int userId; // Getters and Setters diff --git a/src/main/java/polish_community_group_11/polish_community/Categories/Controller/CategoriesController.java b/src/main/java/polish_community_group_11/polish_community/Categories/Controller/CategoriesController.java index a34a539f2ec89f10fcca11e83f29ccb54587900b..6bb3459785a52295cc2201ee70ac5b1721de3e7a 100644 --- a/src/main/java/polish_community_group_11/polish_community/Categories/Controller/CategoriesController.java +++ b/src/main/java/polish_community_group_11/polish_community/Categories/Controller/CategoriesController.java @@ -31,7 +31,7 @@ public class CategoriesController { } // create a new category - @PostMapping + @PostMapping("/add") public Categories createCategory(@RequestBody Categories category) { return categoriesService.saveCategory(category); } diff --git a/src/main/java/polish_community_group_11/polish_community/Categories/Pages/PagesController.java b/src/main/java/polish_community_group_11/polish_community/Categories/Pages/PagesController.java new file mode 100644 index 0000000000000000000000000000000000000000..4c1e997d00dd1d8110ea9c4e088e154c52129812 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/Categories/Pages/PagesController.java @@ -0,0 +1,21 @@ +package polish_community_group_11.polish_community.Categories.Pages; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.servlet.ModelAndView; + +@Controller("categoriesPagesController") +public class PagesController { + @GetMapping("/categories") + public ModelAndView categories() { + return new ModelAndView("categories/categories"); + } + + @GetMapping("/categories/{id}") + public ModelAndView headings(@PathVariable("id") int id) { + ModelAndView modelAndView = new ModelAndView("headings/headings"); + modelAndView.addObject("categoryId", id); + return modelAndView; + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/Error/CustomErrorController.java b/src/main/java/polish_community_group_11/polish_community/Error/CustomErrorController.java new file mode 100644 index 0000000000000000000000000000000000000000..5eaee4faedcabcd5dfb909b97c805e705b9cde2a --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/Error/CustomErrorController.java @@ -0,0 +1,21 @@ +package polish_community_group_11.polish_community.Error; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +public class CustomErrorController implements ErrorController { + + @RequestMapping("/error") + public String handleError(HttpServletRequest request, HttpServletResponse response, Model model) { + Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code"); + response.setStatus(statusCode); + // Pass the error code to the model + model.addAttribute("statusCode", statusCode); + return "error/error"; + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/Error/GlobalExceptionHandler.java b/src/main/java/polish_community_group_11/polish_community/Error/GlobalExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..b868b2d58c1076e9ab4b479c5aa8d5fd178b4f3d --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/Error/GlobalExceptionHandler.java @@ -0,0 +1,17 @@ +package polish_community_group_11.polish_community.Error; +import org.springframework.http.HttpStatus; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ResponseStatus(HttpStatus.NOT_FOUND) + @ExceptionHandler(Exception.class) + public String handleNotFoundException(Exception e, Model model) { + model.addAttribute("statusCode", 404); + return "error/error"; // Redirect to the error page + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/Pages/PagesController.java b/src/main/java/polish_community_group_11/polish_community/Pages/PagesController.java deleted file mode 100644 index 9967ea954f8c7401802d38a12c2ab69a3678f9fd..0000000000000000000000000000000000000000 --- a/src/main/java/polish_community_group_11/polish_community/Pages/PagesController.java +++ /dev/null @@ -1,13 +0,0 @@ -package polish_community_group_11.polish_community.Pages; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.servlet.ModelAndView; - -@Controller -public class PagesController { - @GetMapping("/dbinfo") - public ModelAndView categories() { - return new ModelAndView("categories/categories"); - } -} diff --git a/src/main/java/polish_community_group_11/polish_community/comments/controllers/CommentController.java b/src/main/java/polish_community_group_11/polish_community/comments/controllers/CommentController.java new file mode 100644 index 0000000000000000000000000000000000000000..c8b635ed9a338d95bb1475ab4002f79050bdfc98 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/comments/controllers/CommentController.java @@ -0,0 +1,48 @@ +package polish_community_group_11.polish_community.comments.controllers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestParam; +import polish_community_group_11.polish_community.comments.models.Comment; +import polish_community_group_11.polish_community.comments.models.NewComment; +import polish_community_group_11.polish_community.comments.services.CommentService; +import polish_community_group_11.polish_community.feed.models.Feed; +import polish_community_group_11.polish_community.feed.services.FeedService; + +import java.lang.invoke.MethodHandles; + +@RestController +@RequestMapping("/feed/comments") +//@RequiredArgsConstructor +public class CommentController { + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private final CommentService commentService; + private final FeedService feedService; + + @Autowired + public CommentController(CommentService commentService, FeedService feedService) { + this.commentService = commentService; + this.feedService = feedService; + } + + @PostMapping("/comments/publish") + public ResponseEntity<Comment> publishComment(@RequestBody NewComment newComment) { + + LOG.info("Received new comment {}", newComment); + Feed feed = feedService.getFeedById(newComment.getPostId()); + Comment comment = commentService.publishComment(feed, newComment.getContent()); + return ResponseEntity.ok(comment); + } + + @DeleteMapping("/{id}") + public ResponseEntity<Void> deleteComment(@PathVariable Integer id) { + commentService.deleteComment(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/comments/controllers/FeedWithCommentsController.java b/src/main/java/polish_community_group_11/polish_community/comments/controllers/FeedWithCommentsController.java new file mode 100644 index 0000000000000000000000000000000000000000..d13bf8ed748735bd16312116bd83585a7d52f234 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/comments/controllers/FeedWithCommentsController.java @@ -0,0 +1,30 @@ +package polish_community_group_11.polish_community.comments.controllers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.servlet.ModelAndView; +import polish_community_group_11.polish_community.comments.services.CommentService; +import polish_community_group_11.polish_community.feed.repository.FeedRepositoryImpl; + +@Controller +public class FeedWithCommentsController { + private FeedRepositoryImpl feedRepository; + private CommentService commentService; + + @Autowired + public FeedWithCommentsController(FeedRepositoryImpl feedRepository, CommentService commentService){ + this.feedRepository = feedRepository; + this.commentService = commentService; + } + + @GetMapping("/feed-with-comments") + public ModelAndView getFeed(){ + ModelAndView modelAndView = new ModelAndView("feed/feedWithComments"); + modelAndView.addObject("posts" , feedRepository.getAllPosts()); + // Fetch comments for each post and add to model + modelAndView.addObject("commentService", commentService); + return modelAndView; + } + +} diff --git a/src/main/java/polish_community_group_11/polish_community/comments/dao/CommentJdbcRepository.java b/src/main/java/polish_community_group_11/polish_community/comments/dao/CommentJdbcRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..c7b248ef58603fcdf6af1361d84b1da5db217e05 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/comments/dao/CommentJdbcRepository.java @@ -0,0 +1,68 @@ +package polish_community_group_11.polish_community.comments.dao; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import polish_community_group_11.polish_community.comments.models.Comment; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +@Repository +public class CommentJdbcRepository implements CommentRepository { + + private final JdbcTemplate jdbcTemplate; + + @Autowired + public CommentJdbcRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public Comment save(Comment comment) { + String sql = "INSERT INTO comment (comment_content, user_id, post_id, created_date) VALUES (?, ?, ?, ?)"; + jdbcTemplate.update(sql, + comment.getContent(), + comment.getUserId(), + comment.getPostId(), + comment.getCreatedDate() + ); + + // Retrieve the last inserted ID + Integer commentId = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Integer.class); + comment.setId(commentId); + return comment; + } + + @Override + public List<Comment> findByPostId(int postId) { + String sql = "SELECT c.id, c.comment_content, c.user_id, c.post_id, c.created_date, " + + "u.fullname as username, u.email as user_email " + + "FROM comment c " + + "JOIN users u ON c.user_id = u.id " + + "WHERE c.post_id = ?"; + return jdbcTemplate.query(sql, new CommentRowMapper(), postId); + } + + private static class CommentRowMapper implements RowMapper<Comment> { + @Override + public Comment mapRow(ResultSet rs, int rowNum) throws SQLException { + Comment comment = new Comment(); + comment.setId(rs.getInt("id")); + comment.setContent(rs.getString("comment_content")); + comment.setUserId(rs.getInt("user_id")); + comment.setPostId(rs.getInt("post_id")); + comment.setCreatedDate(rs.getTimestamp("created_date").toLocalDateTime()); + comment.setUsername(rs.getString("username")); + comment.setUserEmail(rs.getString("user_email")); + return comment; + } + } + @Override + public void deleteById(Integer id) { + String sql = "DELETE FROM comment WHERE id = ?"; + jdbcTemplate.update(sql, id); + } +} \ No newline at end of file diff --git a/src/main/java/polish_community_group_11/polish_community/comments/dao/CommentRepository.java b/src/main/java/polish_community_group_11/polish_community/comments/dao/CommentRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..98a7a5b5fd3d2540abe55abc1eaecf692033b73a --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/comments/dao/CommentRepository.java @@ -0,0 +1,11 @@ +package polish_community_group_11.polish_community.comments.dao; + +import polish_community_group_11.polish_community.comments.models.Comment; +import java.util.List; + +public interface CommentRepository { + Comment save(Comment comment); + List<Comment> findByPostId(int postId); + void deleteById(Integer id); + +} diff --git a/src/main/java/polish_community_group_11/polish_community/comments/models/Comment.java b/src/main/java/polish_community_group_11/polish_community/comments/models/Comment.java new file mode 100644 index 0000000000000000000000000000000000000000..43ebc885ea96f444436f261f4da1390af09d882c --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/comments/models/Comment.java @@ -0,0 +1,32 @@ +package polish_community_group_11.polish_community.comments.models; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Comment { + + private Integer id; + private String content; + private int userId; + private int postId; + private String userEmail; + private String username; + private LocalDateTime createdDate; + + // Constructor without id for creating new comments + public Comment(String content, int userId, int postId, String userEmail, String username, LocalDateTime createdDate) { + this.content = content; + this.userId = userId; + this.postId = postId; + this.userEmail = userEmail; + this.username = username; + this.createdDate = createdDate; + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/comments/models/CommentResponse.java b/src/main/java/polish_community_group_11/polish_community/comments/models/CommentResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..21ec38d6f93ced3e4a1c36443ba95299979bce2e --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/comments/models/CommentResponse.java @@ -0,0 +1,13 @@ +package polish_community_group_11.polish_community.comments.models; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class CommentResponse { + private String content; + private String postTitle; + private String createdDate; + private String username; +} \ No newline at end of file diff --git a/src/main/java/polish_community_group_11/polish_community/comments/models/NewComment.java b/src/main/java/polish_community_group_11/polish_community/comments/models/NewComment.java new file mode 100644 index 0000000000000000000000000000000000000000..bcb0f1656c0f0521cedabc16e8adf3a78d761608 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/comments/models/NewComment.java @@ -0,0 +1,16 @@ +package polish_community_group_11.polish_community.comments.models; + +import jakarta.persistence.Column; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class NewComment { + + private String content; + private int postId; + +} diff --git a/src/main/java/polish_community_group_11/polish_community/comments/services/CommentService.java b/src/main/java/polish_community_group_11/polish_community/comments/services/CommentService.java new file mode 100644 index 0000000000000000000000000000000000000000..fde2ecadb7d6363fb441ab86bfad49052cb88631 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/comments/services/CommentService.java @@ -0,0 +1,53 @@ +package polish_community_group_11.polish_community.comments.services; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import polish_community_group_11.polish_community.comments.dao.CommentRepository; +import polish_community_group_11.polish_community.comments.models.Comment; +import polish_community_group_11.polish_community.feed.models.Feed; +import polish_community_group_11.polish_community.register.models.User; +import polish_community_group_11.polish_community.register.dao.UserRepository; + +import java.lang.invoke.MethodHandles; +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class CommentService { + + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private CommentRepository commentRepository; + + private UserRepository userRepository; + + @Autowired + public CommentService(CommentRepository commentRepository, UserRepository userRepository) { + this.commentRepository = commentRepository; + this.userRepository = userRepository; + } + + public List<Comment> findAllCommentsByPostId(int postId) { + LOG.info("Finding comments for: {}", postId); + return commentRepository.findByPostId(postId); + } + public Comment publishComment(Feed feed, String content) { + String userEmail = SecurityContextHolder.getContext().getAuthentication().getName(); + User user = userRepository.findByEmail(userEmail); + Comment comment = new Comment( + content, + user.getId(), + feed.getPostId(), + userEmail, + user.getFullname(), + LocalDateTime.now() + ); + return commentRepository.save(comment); + } + public void deleteComment(Integer id) { + commentRepository.deleteById(id); + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/exception/ApiExceptionHandler.java b/src/main/java/polish_community_group_11/polish_community/exception/ApiExceptionHandler.java index 0d1d79cb2ee027939355691082588e8ecb8e54e6..27192f134cfee89eee24b17d41450bbf5519dc32 100644 --- a/src/main/java/polish_community_group_11/polish_community/exception/ApiExceptionHandler.java +++ b/src/main/java/polish_community_group_11/polish_community/exception/ApiExceptionHandler.java @@ -1,17 +1,22 @@ package polish_community_group_11.polish_community.exception; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import java.lang.invoke.MethodHandles; import java.time.ZoneId; import java.time.ZonedDateTime; @ControllerAdvice public class ApiExceptionHandler { + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + @ExceptionHandler(value = {EmptyResultDataAccessException.class}) public ResponseEntity<Object> handleEmptyResultDataAccessException(EmptyResultDataAccessException e) { return buildResponse(e, HttpStatus.NOT_FOUND); @@ -45,6 +50,7 @@ public class ApiExceptionHandler { // Generic method to build the response entity private ResponseEntity<Object> buildResponse(Exception e, HttpStatus status) { + LOG.error("Returning {} status for error {}", status, e.getMessage(), e); ApiException apiException = new ApiException( e.getMessage(), status, diff --git a/src/main/java/polish_community_group_11/polish_community/feed/controllers/FeedApisController.java b/src/main/java/polish_community_group_11/polish_community/feed/controllers/FeedApisController.java new file mode 100644 index 0000000000000000000000000000000000000000..79bebcae3022b9bdd33964c505dc5a0d52e5283f --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/feed/controllers/FeedApisController.java @@ -0,0 +1,109 @@ +package polish_community_group_11.polish_community.feed.controllers; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import polish_community_group_11.polish_community.feed.models.FeedImpl; +import polish_community_group_11.polish_community.feed.repository.FeedRepository; +import polish_community_group_11.polish_community.feed.services.FeedService; + +import java.util.List; + +@RestController +@RequestMapping("/api/feed") +public class FeedApisController { + private final FeedRepository feedRepository; + + private final FeedService feedService; + + + public FeedApisController(FeedRepository feedRepository, FeedService feedService) { + this.feedService = feedService; + this.feedRepository = feedRepository; + } + + // getting all posts + @GetMapping + public List<FeedImpl> getAllPosts() { + return feedRepository.getAllPosts(); + } + + //getting them by id + @GetMapping("/{postId}") + public ResponseEntity<FeedImpl> getPostById(@PathVariable int postId) { + try { + FeedImpl post = feedRepository.getPostById(postId); + return ResponseEntity.ok(post); + } catch (Exception e) { + return ResponseEntity.notFound().build(); + } + } + + // creating a new post + + @PostMapping("/add") + public ResponseEntity<?> addNewPost(@RequestBody FeedImpl feed) { + try { + feedService.addNewFeedPost(feed); + return ResponseEntity.ok().body("Post created successfully"); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("User not authenticated"); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error creating post: " + e.getMessage()); + } + } + // updating the post + @PutMapping("/{postId}") + public ResponseEntity<Void> updatePost(@PathVariable int postId, @RequestBody FeedImpl feed) { + try { + feedRepository.updatePost(postId, feed); + return ResponseEntity.ok().build(); + } catch (Exception e) { + return ResponseEntity.notFound().build(); + } + } + + // deleting a post + @DeleteMapping("/{postId}") + public ResponseEntity<Void> deletePost(@PathVariable int postId) { + try { + feedRepository.deletePost(postId); + return ResponseEntity.ok().build(); + } catch (Exception e) { + return ResponseEntity.notFound().build(); + } + } + + // liking a post + @PostMapping("/{postId}/like") + public ResponseEntity<Void> likePost(@PathVariable int postId, @RequestParam int userId) { + try { + feedRepository.likePost(postId, userId); + return ResponseEntity.ok().build(); + } catch (Exception e) { + return ResponseEntity.badRequest().build(); + } + } + + // removing a like from post + @DeleteMapping("/{postId}/like") + public ResponseEntity<Void> unlikePost(@PathVariable int postId, @RequestParam int userId) { + try { + feedRepository.unlikePost(postId, userId); + return ResponseEntity.ok().build(); + } catch (Exception e) { + return ResponseEntity.badRequest().build(); + } + } + + // getting list of likes from a post + @GetMapping("/{postId}/hasLiked") + public ResponseEntity<Boolean> hasUserLikedPost(@PathVariable int postId, @RequestParam int userId) { + try { + boolean hasLiked = feedRepository.hasUserLikedPost(postId, userId); + return ResponseEntity.ok(hasLiked); + } catch (Exception e) { + return ResponseEntity.badRequest().build(); + } + } +} \ No newline at end of file diff --git a/src/main/java/polish_community_group_11/polish_community/feed/controllers/FeedController.java b/src/main/java/polish_community_group_11/polish_community/feed/controllers/FeedController.java index 4f889c1892f7841b71d93a9088cc6bc84a81d310..78a3a8cff5ec610241469ce8f9d8a5059673f856 100644 --- a/src/main/java/polish_community_group_11/polish_community/feed/controllers/FeedController.java +++ b/src/main/java/polish_community_group_11/polish_community/feed/controllers/FeedController.java @@ -1,24 +1,15 @@ package polish_community_group_11.polish_community.feed.controllers; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.servlet.ModelAndView; -import polish_community_group_11.polish_community.feed.services.FeedRepositoryImpl; @Controller public class FeedController { - private FeedRepositoryImpl feedRepository; - - public FeedController(FeedRepositoryImpl feedRepository){ - this.feedRepository = feedRepository; - } @GetMapping("/feed") - public ModelAndView getFeed(){ + public ModelAndView getFeedPage() { ModelAndView modelAndView = new ModelAndView("feed/feed"); - modelAndView.addObject("posts" , feedRepository.getAllPosts()); return modelAndView; } - } diff --git a/src/main/java/polish_community_group_11/polish_community/feed/models/Feed.java b/src/main/java/polish_community_group_11/polish_community/feed/models/Feed.java index 3be1b906a6aa39df9c19dd15613b36432e20fed7..19ff0e6faaf48bc60c68ea2dc1a0c9918c6c5f62 100644 --- a/src/main/java/polish_community_group_11/polish_community/feed/models/Feed.java +++ b/src/main/java/polish_community_group_11/polish_community/feed/models/Feed.java @@ -1,27 +1,35 @@ package polish_community_group_11.polish_community.feed.models; import java.time.LocalDate; -import java.util.Date; +import java.util.List; public interface Feed { - public String getPostImageUrl(); + String getPostImageUrl(); + void setPostImageUrl(String postImageUrl); - public void setPostImageUrl(String postImageUrl); + String getPostTitle(); + void setPostTitle(String postTitle); - public String getPost_title(); + String getPostDescription(); + void setPostDescription(String postDescription); - public void setPost_title(String post_title); + LocalDate getPostTime(); + void setPostTime(LocalDate postTime); - public String getPost_description(); + int getUserId(); + void setUserId(int userId); - public void setPost_description(String post_description); + String getAuthorName(); + void setAuthorName(String authorName); - public String getPost_author(); + String getAuthorOrganization(); + void setAuthorOrganization(String organization); - public void setPost_author(String post_author); + List<String> getTags(); + void setTags(List<String> tags); - public LocalDate getPost_time(); + int getLikesCount(); + void setLikesCount(int likesCount); + int getPostId(); - public void setPost_time(LocalDate post_time); - -} +} \ No newline at end of file diff --git a/src/main/java/polish_community_group_11/polish_community/feed/models/FeedImpl.java b/src/main/java/polish_community_group_11/polish_community/feed/models/FeedImpl.java index f7c2a0c52f4a02460660361091d374ffa18e046d..6484ce5ba1e3e00a00dd6c6ff98a12e29b351f45 100644 --- a/src/main/java/polish_community_group_11/polish_community/feed/models/FeedImpl.java +++ b/src/main/java/polish_community_group_11/polish_community/feed/models/FeedImpl.java @@ -2,21 +2,23 @@ package polish_community_group_11.polish_community.feed.models; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import java.time.LocalDate; -import java.util.Date; +import java.util.List; @Data @AllArgsConstructor -public class FeedImpl implements Feed{ - +@NoArgsConstructor +public class FeedImpl implements Feed { + private int postId; private String postImageUrl; - private String post_title; - private String post_description; - private String post_author; - private LocalDate post_time; - private int user_id; - - - -} + private String postTitle; + private String postDescription; + private LocalDate postTime; + private int userId; + private String authorName; + private String authorOrganization; + private List<String> tags; + private int likesCount; +} \ No newline at end of file diff --git a/src/main/java/polish_community_group_11/polish_community/feed/repository/FeedRepository.java b/src/main/java/polish_community_group_11/polish_community/feed/repository/FeedRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..4c41b0a3e827a3a50d305a1ad6b23cc874d60259 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/feed/repository/FeedRepository.java @@ -0,0 +1,24 @@ +package polish_community_group_11.polish_community.feed.repository; + +import polish_community_group_11.polish_community.feed.models.Feed; +import polish_community_group_11.polish_community.feed.models.FeedImpl; + +import java.util.List; + +public interface FeedRepository { + List<FeedImpl> getAllPosts(); + + FeedImpl getPostById(int postId); + + void addNewFeedPost(FeedImpl feed); + + void updatePost(int postId, FeedImpl feed); + + void deletePost(int postId); + + void likePost(int postId, int userId); + + void unlikePost(int postId, int userId); + + boolean hasUserLikedPost(int postId, int userId); +} diff --git a/src/main/java/polish_community_group_11/polish_community/feed/repository/FeedRepositoryImpl.java b/src/main/java/polish_community_group_11/polish_community/feed/repository/FeedRepositoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..bb2037c7307c00dfab996624863794fe3253e25c --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/feed/repository/FeedRepositoryImpl.java @@ -0,0 +1,189 @@ +package polish_community_group_11.polish_community.feed.repository; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import polish_community_group_11.polish_community.feed.models.Feed; +import polish_community_group_11.polish_community.feed.models.FeedImpl; + +import java.util.List; + +@Repository +public class FeedRepositoryImpl implements FeedRepository { + private final JdbcTemplate jdbcTemplate; + private final RowMapper<FeedImpl> feedMapper; + + public FeedRepositoryImpl(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + this.feedMapper = (rs, rowNum) -> { + FeedImpl feed = new FeedImpl(); + feed.setPostId(rs.getInt("post_id")); + feed.setPostImageUrl(rs.getString("post_image_url")); + feed.setPostTitle(rs.getString("post_title")); + feed.setPostDescription(rs.getString("post_description")); + feed.setPostTime(rs.getDate("post_time").toLocalDate()); + feed.setUserId(rs.getInt("user_id")); + feed.setAuthorName(rs.getString("fullname")); + feed.setAuthorOrganization(rs.getString("organization")); + feed.setTags(getTagsForPost(rs.getInt("post_id"))); + feed.setLikesCount(getLikesCount(rs.getInt("post_id"))); + return feed; + }; + } + + // getting all posts + @Override + public List<FeedImpl> getAllPosts() { + String sql = "SELECT f.*, u.fullname, u.organization " + + "FROM feed f " + + "LEFT JOIN users u ON f.user_id = u.id " + + "ORDER BY f.post_time DESC"; + return jdbcTemplate.query(sql, feedMapper); + } + + // getting a post by id + @Override + public FeedImpl getPostById(int postId) { + String sql = "SELECT f.*, u.fullname, u.organization " + + "FROM feed f " + + "LEFT JOIN users u ON f.user_id = u.id " + + "WHERE f.post_id = ?"; + return jdbcTemplate.queryForObject(sql, feedMapper, postId); + } + + // adding a new post + @Override + public void addNewFeedPost(FeedImpl feed) { + String sql = "INSERT INTO feed (post_image_url, post_title, post_description, post_time, user_id) " + + "VALUES (?, ?, ?, ?, ?)"; + + jdbcTemplate.update(sql, + feed.getPostImageUrl() != null ? feed.getPostImageUrl() : "", + feed.getPostTitle(), + feed.getPostDescription(), + java.sql.Date.valueOf(feed.getPostTime()), + feed.getUserId() + ); + + int postId = getGeneratedPostId(); + + List<String> tags = feed.getTags(); + if (tags != null && !tags.isEmpty()) { + insertTagsForPost(postId, tags); + } + } + + + // update a post that is editing + @Override + public void updatePost(int postId, FeedImpl feed) { + String sql = "UPDATE feed SET post_image_url = ?, post_title = ?, post_description = ?, " + + "post_author = ?, post_time = ?, author_title = ? WHERE post_id = ?"; + + jdbcTemplate.update(sql, + feed.getPostImageUrl(), + feed.getPostTitle(), + feed.getPostDescription(), + java.sql.Date.valueOf(feed.getPostTime()), + postId + ); + + // update the tags + jdbcTemplate.update("DELETE FROM feed_tags WHERE post_id = ?", postId); + insertTagsForPost(postId, feed.getTags()); + } + + // deleting a post + @Override + public void deletePost(int postId) { + // delete associated likes, tags then the post + jdbcTemplate.update("DELETE FROM post_likes WHERE post_id = ?", postId); + + jdbcTemplate.update("DELETE FROM feed_tags WHERE post_id = ?", postId); + + jdbcTemplate.update("DELETE FROM feed WHERE post_id = ?", postId); + } + + // liking a post + @Override + public void likePost(int postId, int userId) { + String sql = "INSERT INTO post_likes (post_id, user_id) VALUES (?, ?)"; + jdbcTemplate.update(sql, postId, userId); + } + + // removing like + @Override + public void unlikePost(int postId, int userId) { + String sql = "DELETE FROM post_likes WHERE post_id = ? AND user_id = ?"; + jdbcTemplate.update(sql, postId, userId); + } + + // checking if user had liked or not + @Override + public boolean hasUserLikedPost(int postId, int userId) { + String sql = "SELECT COUNT(*) FROM post_likes WHERE post_id = ? AND user_id = ?"; + int count = jdbcTemplate.queryForObject(sql, Integer.class, postId, userId); + return count > 0; + } + + // getting number of likes on post + private int getLikesCount(int postId) { + String sql = "SELECT COUNT(*) FROM post_likes WHERE post_id = ?"; + return jdbcTemplate.queryForObject(sql, Integer.class, postId); + } + + + // getting tags associated with post + private List<String> getTagsForPost(int postId) { + String sql = "SELECT t.tag_name FROM tags t " + + "INNER JOIN feed_tags ft ON t.tag_id = ft.tag_id " + + "WHERE ft.post_id = ?"; + return jdbcTemplate.query(sql, (rs, rowNum) -> rs.getString("tag_name"), postId); + } + + // getting id of the recently created post + private int getGeneratedPostId() { + return jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Integer.class); + } + + + private void insertTagsForPost(int postId, List<String> tags) { + for (String tag : tags) { + // check for empty tags + if (tag == null || tag.trim().isEmpty()) { + continue; + } + + String cleanTag = tag.trim().toLowerCase(); + int tagId = insertTagAndGetId(cleanTag); + // link to post + try { + jdbcTemplate.update( + "INSERT INTO feed_tags (post_id, tag_id) VALUES (?, ?)", + postId, tagId + ); + } catch (Exception e) { + // handle duplicate tag associations silently + if (!e.getMessage().contains("duplicate")) { + throw e; + } + } + } + } + + + private int insertTagAndGetId(String tag) { + try { + String checkSql = "SELECT tag_id FROM tags WHERE tag_name = ?"; + Integer tagId = jdbcTemplate.queryForObject(checkSql, new Object[]{tag}, Integer.class); + return tagId != null ? tagId : createNewTag(tag); + } catch (Exception e) { + return createNewTag(tag); + } + } + + private int createNewTag(String tag) { + jdbcTemplate.update("INSERT INTO tags (tag_name) VALUES (?)", tag); + return jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Integer.class); + } +} \ No newline at end of file diff --git a/src/main/java/polish_community_group_11/polish_community/feed/services/FeedRepository.java b/src/main/java/polish_community_group_11/polish_community/feed/services/FeedRepository.java deleted file mode 100644 index 19ebd742d5647671741b2b356ac82c96e0deef69..0000000000000000000000000000000000000000 --- a/src/main/java/polish_community_group_11/polish_community/feed/services/FeedRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package polish_community_group_11.polish_community.feed.services; - -import polish_community_group_11.polish_community.feed.models.Feed; - -import java.util.List; - -public interface FeedRepository { - List<Feed> getAllPosts(); -} diff --git a/src/main/java/polish_community_group_11/polish_community/feed/services/FeedRepositoryImpl.java b/src/main/java/polish_community_group_11/polish_community/feed/services/FeedRepositoryImpl.java deleted file mode 100644 index 743ee609c6dae0e7bcf2ce405fd188cd30334441..0000000000000000000000000000000000000000 --- a/src/main/java/polish_community_group_11/polish_community/feed/services/FeedRepositoryImpl.java +++ /dev/null @@ -1,39 +0,0 @@ -package polish_community_group_11.polish_community.feed.services; - -import org.springframework.stereotype.Service; -import polish_community_group_11.polish_community.feed.models.Feed; -import polish_community_group_11.polish_community.feed.models.FeedImpl; - -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.Arrays; -import java.util.Date; -import java.util.List; - -@Service -public class FeedRepositoryImpl implements FeedRepository { - - public List<Feed> getAllPosts() { - return Arrays.asList( - new FeedImpl("assets/post-image.jpg", - "Pentagon unlikely to use all of the billions Congress authorized it to spend on Ukraine weapons before Biden leaves office, officials say", - "The Pentagon is unlikely to use all of the billions of dollars authorized by Congress to arm Ukraine before President Joe Biden leaves office, according to two US officials and three defense officials", - "John Doe", - LocalDate.now(), - 1), - new FeedImpl("assets/post-image2.webp", - "Business owners, churches take legal action against Harvey mayor, alleging improper fees", - "Some business owners and churches in south suburban Harvey are taking legal action against the city, alleging that they're being unfairly targeted with improper fees to help raise revenue.", - "Jane Smith", - LocalDate.now(), - 2), - new FeedImpl("Customize Toolbar...", - "Post Title 3 ", - "A quick look at some interesting insights and updates.", - "Mark Lee", - LocalDate.now(), - 3) - ); - } - -} diff --git a/src/main/java/polish_community_group_11/polish_community/feed/services/FeedService.java b/src/main/java/polish_community_group_11/polish_community/feed/services/FeedService.java new file mode 100644 index 0000000000000000000000000000000000000000..7f8b833c75055e190580c887fa92968f6c058841 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/feed/services/FeedService.java @@ -0,0 +1,60 @@ +package polish_community_group_11.polish_community.feed.services; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import polish_community_group_11.polish_community.feed.models.Feed; +import polish_community_group_11.polish_community.feed.models.FeedImpl; +import polish_community_group_11.polish_community.feed.repository.FeedRepository; +import polish_community_group_11.polish_community.register.models.User; +import polish_community_group_11.polish_community.register.services.UserService; + +import java.time.LocalDate; +import java.util.List; + +@Service +public class FeedService { + private final FeedRepository feedRepository; + private final UserService userService; + private static final Logger log = LoggerFactory.getLogger(FeedService.class); + + public FeedService(FeedRepository feedRepository, UserService userService) { + this.feedRepository = feedRepository; + this.userService = userService; + } + + + public void addNewFeedPost(FeedImpl feed) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + log.error("No authentication found"); + throw new SecurityException("No authentication found"); + } + + String userEmail = authentication.getName(); + log.info("Adding new post for user: {}", userEmail); + + User user = userService.findByEmail(userEmail); + if (user == null) { + log.error("No user found for email: {}", userEmail); + throw new SecurityException("No authenticated user found"); + } + + feed.setUserId(user.getId()); + feed.setPostTime(LocalDate.now()); + + try { + feedRepository.addNewFeedPost(feed); + } catch (Exception e) { + log.error("Error adding new post: ", e); + throw e; + } + } + + public Feed getFeedById(int postId) { + return feedRepository.getPostById(postId); + } +} \ No newline at end of file diff --git a/src/main/java/polish_community_group_11/polish_community/information/common/Utility.java b/src/main/java/polish_community_group_11/polish_community/information/common/Utility.java index e0a6f75741b52320f1fa8bd4a7591f8d03638f17..07805e9517222ea4c4ff0c617dd644e476449081 100644 --- a/src/main/java/polish_community_group_11/polish_community/information/common/Utility.java +++ b/src/main/java/polish_community_group_11/polish_community/information/common/Utility.java @@ -11,20 +11,15 @@ Utility class to perform common operations public class Utility { //This method converts Markdown text to html formatted output text - public static String markdownToHtml(String markdown){ - try{ - if (markdown == null || markdown.trim().isEmpty()) { - throw new IllegalArgumentException("Input markdown text cannot be null or empty."); - } - else{ - Parser parser = Parser.builder().build(); - HtmlRenderer renderer = HtmlRenderer.builder().build(); - Node document = parser.parse(markdown); - return renderer.render(document); - } + public static String markdownToHtml(String markdown) throws IllegalArgumentException{ + if (markdown == null || markdown.trim().isEmpty()) { + throw new IllegalArgumentException("Input markdown text cannot be null or empty."); } - catch (Exception ex){ - return "<h2>Error getting the information from the markdown.</h2>"; + else{ + Parser parser = Parser.builder().build(); + HtmlRenderer renderer = HtmlRenderer.builder().build(); + Node document = parser.parse(markdown); + return renderer.render(document); } } } diff --git a/src/main/java/polish_community_group_11/polish_community/information/controllers/InformationApiController.java b/src/main/java/polish_community_group_11/polish_community/information/controllers/InformationApiController.java new file mode 100644 index 0000000000000000000000000000000000000000..a524e16241f23af5b3e42c1113396a8917ddb519 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/information/controllers/InformationApiController.java @@ -0,0 +1,29 @@ +package polish_community_group_11.polish_community.information.controllers; + +import org.springframework.web.bind.annotation.*; +import polish_community_group_11.polish_community.information.models.DBInfo; +import polish_community_group_11.polish_community.information.services.InformationService; + +import java.sql.SQLException; +import java.util.List; + + +@RestController +public class InformationApiController { + private final InformationService infoService; + + // Constructor for dependency injection of the InformationService + public InformationApiController(InformationService infoService){ + this.infoService = infoService; + } + + @GetMapping("getInfoByCategoryId/{category_id}") + public List<DBInfo> getAllInformation(@PathVariable int category_id){ + return infoService.getAllItemsByCategoryId(category_id); + } + + @DeleteMapping("api/info/delete") + public int deleteInformation(@RequestParam List<Integer> deleteIdList) throws SQLException { + return infoService.deleteInfoByList(deleteIdList); + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/information/controllers/InformationController.java b/src/main/java/polish_community_group_11/polish_community/information/controllers/InformationController.java index 2312778cc40628920b1a3aa09bcdf89401b695ee..355d619bcdd7f55376a653250f3c051956f9717a 100644 --- a/src/main/java/polish_community_group_11/polish_community/information/controllers/InformationController.java +++ b/src/main/java/polish_community_group_11/polish_community/information/controllers/InformationController.java @@ -1,77 +1,105 @@ package polish_community_group_11.polish_community.information.controllers; +import jakarta.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; import polish_community_group_11.polish_community.information.common.Utility; import polish_community_group_11.polish_community.information.models.DBInfo; +import polish_community_group_11.polish_community.information.models.ViewModel.DBInfoForm; import polish_community_group_11.polish_community.information.services.InformationService; import java.lang.invoke.MethodHandles; +import java.sql.SQLException; @Controller public class InformationController { - private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private final InformationService infoService; // Constructor for dependency injection of the InformationService + @Autowired public InformationController(InformationService infoService){ this.infoService = infoService; } -// @GetMapping("getInfo") -// public ModelAndView getAllInformation(){ -// try{ -// ModelAndView modelAndView=new ModelAndView(""); -// modelAndView.addObject(infoService.getAllItems()); -// return modelAndView; -// } -// -// } - // Method to handle GET requests for retrieving a specific information entry by ID - @GetMapping("getInfo/{id}") - public ModelAndView getInformationById(@PathVariable int id){ + @GetMapping("info/{id}") + public ModelAndView getInformationById(@PathVariable int id) + throws NullPointerException, EmptyResultDataAccessException, IncorrectResultSizeDataAccessException, SQLException + { ModelAndView modelAndView = new ModelAndView("information/information"); - try{ - DBInfo dbInfo = infoService.getItemById(id); - if(dbInfo!=null){ - // Convert the information description from Markdown to HTML - String html = Utility.markdownToHtml(dbInfo.getInfoDescription()); - modelAndView.addObject("dbInfo", dbInfo); - modelAndView.addObject("html",html); - } - else{ - throw new NullPointerException(); - } + DBInfo selectedDbInfo = infoService.getInfomrationItemById(id); + if(selectedDbInfo!=null){ + // Convert the information description from Markdown to HTML + String html = Utility.markdownToHtml(selectedDbInfo.getInfoDescription()); + modelAndView.addObject("selectedInfo", selectedDbInfo); + modelAndView.addObject("html",html); } - - // Handle the case where no matching record is found (null result) - catch(NullPointerException ex){ - modelAndView.setViewName("error/error"); - modelAndView.addObject("errorMessage", - String.format("Oops! Did not find any database of information for this id: %d",id)); + else{ + throw new NullPointerException(); } + return modelAndView; + } - // Handle cases where a query returns no results - catch(EmptyResultDataAccessException ex){ - modelAndView.setViewName("error/error"); - modelAndView.addObject("errorMessage","Sorry! Did not find any database of information for this record."); - } + @GetMapping("info/add/{categoryId}") + public ModelAndView addInformation(@PathVariable int categoryId){ + ModelAndView modelAndView=new ModelAndView("information/addInformation"); + DBInfoForm newInfo = new DBInfoForm(); + newInfo.setCategoryId(categoryId); + modelAndView.addObject("newInfo",newInfo); + modelAndView.addObject("formAction", "/info/add"); + return modelAndView; + } - // Handle all other unexpected exceptions - catch(Exception ex){ - modelAndView.setViewName("error/error"); - modelAndView.addObject("errorMessage", - "Ohh! An unexpected error occurred"); + @PostMapping("/info/add") + public ModelAndView addInformation(@Valid @ModelAttribute("newInfo") DBInfoForm newInfo, + BindingResult bindingResult, Model model) throws SQLException { + ModelAndView modelAndView = new ModelAndView("information/addInformation"); + if(bindingResult.hasErrors()){ + modelAndView.addObject(model.asMap()); + modelAndView.addObject("formAction", "/info/add"); } + else{ + DBInfo addInfo = infoService.processDbInfoForm(newInfo); + infoService.addNewInfo(addInfo); + modelAndView = new ModelAndView( + String.format("redirect:../categories/%d", addInfo.getCategoryId())); + } + return modelAndView; + } + + @GetMapping("/info/edit/{info_id}") + public ModelAndView editInformation(@PathVariable int info_id) throws SQLException { + ModelAndView modelAndView = new ModelAndView("information/addInformation"); + DBInfo selectedInfo = infoService.getInfomrationItemById(info_id); + DBInfoForm newInfo = infoService.processDbInfoFormForEdit(selectedInfo); + modelAndView.addObject("newInfo", newInfo); + modelAndView.addObject("formAction", "/info/edit"); return modelAndView; } + @PostMapping("/info/edit") + public ModelAndView editInformation(@Valid @ModelAttribute("newInfo") DBInfoForm newInfo, + BindingResult bindingResult, Model model) throws SQLException { + ModelAndView modelAndView = new ModelAndView("information/addInformation"); + if(bindingResult.hasErrors()){ + modelAndView.addObject(model.asMap()); + modelAndView.addObject("formAction", "/info/edit"); + } + else{ + DBInfo updatedInfo = infoService.processDbInfoForm(newInfo); + infoService.editInfo(updatedInfo); + modelAndView = new ModelAndView( + String.format("redirect:../categories/%d", updatedInfo.getCategoryId())); + } + return modelAndView; + } } diff --git a/src/main/java/polish_community_group_11/polish_community/information/dao/InformationRepository.java b/src/main/java/polish_community_group_11/polish_community/information/dao/InformationRepository.java index 076723577446ad5fa02c8d36e722978ed46cdfb0..a088642e943bc898e461c23381e606a8fc6c6976 100644 --- a/src/main/java/polish_community_group_11/polish_community/information/dao/InformationRepository.java +++ b/src/main/java/polish_community_group_11/polish_community/information/dao/InformationRepository.java @@ -1,17 +1,27 @@ package polish_community_group_11.polish_community.information.dao; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import polish_community_group_11.polish_community.information.models.DBInfo; -import polish_community_group_11.polish_community.information.models.DBInfoImpl; +import java.sql.SQLException; import java.util.List; /** + * This Information Repository interface acts as a layer to hide the main implementations of the + information repository methods and make only the methods available. + It defines methods to retrieve specific data -items and lists of database information from the data source.*/ +items and lists of database information from the data source. +**/ public interface InformationRepository { - public DBInfo getItemById(int id); - public List<DBInfo> getDbInfoList(); + public DBInfo getInfomrationById(int id) + throws EmptyResultDataAccessException, IncorrectResultSizeDataAccessException, SQLException; + public List<DBInfo> getDbInfoListByCategory(int category_id); List<DBInfo> findDbInfo(String userInput); + void addNewInfo(DBInfo newInfo) throws SQLException; + int deleteInfoList(List<Integer> deleteIdList) throws SQLException; + int editInfo(DBInfo updatedInfo) throws SQLException; } diff --git a/src/main/java/polish_community_group_11/polish_community/information/dao/InformationRepositoryImpl.java b/src/main/java/polish_community_group_11/polish_community/information/dao/InformationRepositoryImpl.java index 0ace7dd010edb5b0cf170369ec568b6d71228cbe..3f6e19eae53bbb426af0eba351f62e618e9f888f 100644 --- a/src/main/java/polish_community_group_11/polish_community/information/dao/InformationRepositoryImpl.java +++ b/src/main/java/polish_community_group_11/polish_community/information/dao/InformationRepositoryImpl.java @@ -1,15 +1,19 @@ package polish_community_group_11.polish_community.information.dao; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.PreparedStatementSetter; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; import polish_community_group_11.polish_community.information.models.DBInfo; import polish_community_group_11.polish_community.information.models.DBInfoImpl; -import java.sql.PreparedStatement; import java.sql.SQLException; +import java.time.DateTimeException; +import java.time.LocalDate; import java.util.List; +import java.util.Objects; /** Implementation of the InformationRepository interface. @@ -29,29 +33,62 @@ public class InformationRepositoryImpl implements InformationRepository { Sets mapping rows from the database to instances of the DBInfo. */ public void setInfoItemMapper(){ - infoItemMapper = (rs, i) -> new DBInfoImpl( - rs.getInt("category_id"), - rs.getInt("tag_id"), - rs.getInt("info_id"), - rs.getString("info_title"), - rs.getString("created_by"), - rs.getDate("created_date").toLocalDate(), - rs.getDate("updated_date").toLocalDate(), - rs.getString("info_article") - ); + infoItemMapper = (rs, i) ->{ + try { + // Check if any required fields are missing or null + if (rs.getInt("category_id") == 0 + || rs.getInt("info_id") == 0 + || rs.getString("info_title") == null) { + throw new NullPointerException("Required News fields cannot be empty"); + } + return new DBInfoImpl( + rs.getInt("category_id"), + rs.getInt("tag_id"), + rs.getInt("info_id"), + rs.getString("info_title"), + rs.getString("created_by"), + rs.getDate("created_date").toLocalDate(), + rs.getDate("updated_date").toLocalDate(), + rs.getString("short_info"), + rs.getString("info_article") + ); + } + catch (NullPointerException ex) { + // Handle NullPointerException if any required fields are missing or null + throw new NullPointerException("Error in mapping News data: " + ex.getMessage()); + } catch (DateTimeException ex) { + // Handle DateTimeException if the date format is invalid + throw new RuntimeException("Invalid date format for News Upload Date"); + } catch (Exception ex) { + // Handle any other unexpected exceptions + throw new RuntimeException("Unexpected error during mapping: " + ex.getMessage(), ex); + } + }; } // Retrieves a single DBInfo item from the database based on the provided info id. - public DBInfo getItemById(int id) { - String sql = "select * from information where info_id=?"; - return jdbc.queryForObject(sql, infoItemMapper, id); + public DBInfo getInfomrationById(int id) throws EmptyResultDataAccessException, IncorrectResultSizeDataAccessException, SQLException { + try{ + String sql = "select * from information where info_id=?"; + return jdbc.queryForObject(sql, infoItemMapper, id); + } + catch(EmptyResultDataAccessException ex){ + // Handle case where no records were found + throw new EmptyResultDataAccessException("Did not find any records with selected id", 0); + } catch (IncorrectResultSizeDataAccessException ex) { + // Handle case where multiple results were found + throw new IncorrectResultSizeDataAccessException("Multiple records generated, only one record expected",1); + } catch (DataAccessException e) { + // Handle other database-related exceptions + throw new SQLException("Database error occurred", e); + } } - // Retrieves a list of all DBInfo items from the database. - public List<DBInfo> getDbInfoList(){ - String sql = "select * from information (nolock)"; - return jdbc.query(sql,infoItemMapper); + public List<DBInfo> getDbInfoListByCategory(int category_id){ + String sql = "select * from information where category_id=?"; +// return jdbc.query(sql,(rs, rowNum) -> rs.getString("info_title"),category_id); + return jdbc.query(sql,infoItemMapper,category_id); } public List<DBInfo> findDbInfo(String userInput) { @@ -59,4 +96,60 @@ public class InformationRepositoryImpl implements InformationRepository { // from https://www.geeksforgeeks.org/spring-prepared-statement-jdbc-template/ return jdbc.query(sql, ps -> ps.setString(1, "%" + userInput.toLowerCase() + "%"), infoItemMapper); } + + // Inserts new database of information record to the information table + public void addNewInfo(DBInfo newInfo) throws SQLException { + String dbInsertSql = + "insert into information " + + "(category_id, info_title, created_by, created_date, " + + "updated_date, tag_id, short_info, info_article)" + + " values (?,?,?,?,?,?,?,?)"; + try { + jdbc.update(dbInsertSql, + newInfo.getCategoryId(), + newInfo.getInfoTitle(), + "Nitish Marnuri", //Mocking the user for now + LocalDate.now(), + LocalDate.now(), + 1, // Mocking the tag id for now + newInfo.getShortDescription(), + newInfo.getInfoDescription() + ); + } catch (DataAccessException e) { + throw new SQLException("Failed to insert new information record", e); + } + } + + // Deletes a list of records from information table with list of Information Ids + public int deleteInfoList(List<Integer> deleteIdList) throws SQLException { + // Prepare query for deletion + String deleteSql = "delete from information where info_id in (" + + String.join(",", deleteIdList.stream().map(info_id -> "?").toArray(String[]::new)) + ")"; + + Object[] deleteParams = deleteIdList.toArray(); + try { + return jdbc.update(deleteSql, deleteParams); + } catch (DataAccessException e) { + throw new SQLException("Failed to delete information records", e); + } + } + + // Updates selected record with new updated information + public int editInfo(DBInfo updatedInfo) throws SQLException { + String updateSql = "update information " + + "set info_title=?, short_info=?, info_article=? " + + "where info_id=?"; + + try { + return jdbc.update(updateSql, + updatedInfo.getInfoTitle(), + updatedInfo.getShortDescription(), + updatedInfo.getInfoDescription(), + updatedInfo.getInformationId() + ); + } catch (DataAccessException e) { + throw new SQLException("Failed to update information record", e); + } + + } } diff --git a/src/main/java/polish_community_group_11/polish_community/information/models/DBInfo.java b/src/main/java/polish_community_group_11/polish_community/information/models/DBInfo.java index 480fa5d6179654f80055388d5ae4dbc661eaf3d5..02243c8b7f534abbed13e0f42cfeffbccd9f98b4 100644 --- a/src/main/java/polish_community_group_11/polish_community/information/models/DBInfo.java +++ b/src/main/java/polish_community_group_11/polish_community/information/models/DBInfo.java @@ -11,4 +11,6 @@ public interface DBInfo extends Information { public void setCategoryId(int categoryId); public int getTagId(); public void setTagId(int tagId); + public String getShortDescription(); + public void setShortDescription(String shortDescription); } diff --git a/src/main/java/polish_community_group_11/polish_community/information/models/DBInfoImpl.java b/src/main/java/polish_community_group_11/polish_community/information/models/DBInfoImpl.java index f71d86c73366aaa66280560a3b381e746eb310fe..5880994482e4ce00aaae7493dc2c57864cea6961 100644 --- a/src/main/java/polish_community_group_11/polish_community/information/models/DBInfoImpl.java +++ b/src/main/java/polish_community_group_11/polish_community/information/models/DBInfoImpl.java @@ -1,5 +1,11 @@ package polish_community_group_11.polish_community.information.models; +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + import java.time.LocalDate; /** @@ -7,31 +13,21 @@ import java.time.LocalDate; Represents an information entity with additional attributes for category ID and tag ID. Extends the InformationImpl class to reuse common information-related attributes and behavior. */ - +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor public class DBInfoImpl extends InformationImpl implements DBInfo { private int categoryId; // Id to map the information with Category table as a foreign key. private int tagId; // Id to map the information with tag table as a foreign key. + private String shortDescription; public DBInfoImpl(int categoryId, int tagId, int informationId, String infoTitle, - String createdBy, LocalDate createdDate, LocalDate updatedDate, String infoDescription){ + String createdBy, LocalDate createdDate, LocalDate updatedDate,String shortDescription, String infoDescription){ super(informationId, infoTitle,createdBy, createdDate, updatedDate, infoDescription); this.categoryId=categoryId; this.tagId=tagId; + this.shortDescription=shortDescription; } - public int getCategoryId() { - return categoryId; - } - - public void setCategoryId(int categoryId) { - this.categoryId = categoryId; - } - - public int getTagId() { - return tagId; - } - - public void setTagId(int tagId) { - this.tagId = tagId; - } } diff --git a/src/main/java/polish_community_group_11/polish_community/information/models/InformationImpl.java b/src/main/java/polish_community_group_11/polish_community/information/models/InformationImpl.java index 26082c15b315547befd21905373c39a1aca3228a..2e7b1999ab09c778978220a1c7f5381d68311a36 100644 --- a/src/main/java/polish_community_group_11/polish_community/information/models/InformationImpl.java +++ b/src/main/java/polish_community_group_11/polish_community/information/models/InformationImpl.java @@ -1,7 +1,9 @@ package polish_community_group_11.polish_community.information.models; +import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import java.time.LocalDate; @@ -12,6 +14,7 @@ import java.time.LocalDate; @Data @AllArgsConstructor +@NoArgsConstructor public abstract class InformationImpl implements Information { private int informationId; private String infoTitle; diff --git a/src/main/java/polish_community_group_11/polish_community/information/models/ViewModel/DBInfoForm.java b/src/main/java/polish_community_group_11/polish_community/information/models/ViewModel/DBInfoForm.java new file mode 100644 index 0000000000000000000000000000000000000000..37f7a0d27dd624cee57dcb00fd7beaea40976e23 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/information/models/ViewModel/DBInfoForm.java @@ -0,0 +1,33 @@ +package polish_community_group_11.polish_community.information.models.ViewModel; + +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +/* + This is a view model class DB Info form to take form data and perform validations. +*/ + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DBInfoForm { + private int informationId; + private int categoryId; + private int tagId; + + // Add validations for not empty fields + @NotEmpty(message = "Please enter a title for the new information heading") + private String infoTitle; + @NotEmpty(message = "Please enter a short description") + private String shortDescription; + @NotEmpty(message = "Please add the detailed content information for the new information.") + private String infoDescription; + + private String createdBy; + private LocalDate createdDate; + private LocalDate updatedDate; +} diff --git a/src/main/java/polish_community_group_11/polish_community/information/services/InformationService.java b/src/main/java/polish_community_group_11/polish_community/information/services/InformationService.java index b8e6676b2a76bf131adab06cc5f52c098a658b10..c221c73f70bbc63cba2ff323eb327818f81e52d8 100644 --- a/src/main/java/polish_community_group_11/polish_community/information/services/InformationService.java +++ b/src/main/java/polish_community_group_11/polish_community/information/services/InformationService.java @@ -1,8 +1,12 @@ package polish_community_group_11.polish_community.information.services; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import polish_community_group_11.polish_community.information.models.DBInfo; import polish_community_group_11.polish_community.information.models.SearchResult; +import polish_community_group_11.polish_community.information.models.ViewModel.DBInfoForm; +import java.sql.SQLException; import java.util.List; /** @@ -13,8 +17,14 @@ import java.util.List; */ public interface InformationService { - public DBInfo getItemById(int id); - public List<DBInfo> getAllItems(); + DBInfo getInfomrationItemById(int id) + throws EmptyResultDataAccessException, IncorrectResultSizeDataAccessException, SQLException; + List<DBInfo> getAllItemsByCategoryId(int category_id); List<SearchResult> searchInfo(String userInput); + DBInfo processDbInfoForm(DBInfoForm dbInfoForm); + void addNewInfo(DBInfo newInfo) throws SQLException; + int deleteInfoByList(List<Integer> deleteIdList) throws SQLException; + int editInfo(DBInfo updatedInfo) throws SQLException; + DBInfoForm processDbInfoFormForEdit(DBInfo selectedInfo); } diff --git a/src/main/java/polish_community_group_11/polish_community/information/services/InformationServiceImpl.java b/src/main/java/polish_community_group_11/polish_community/information/services/InformationServiceImpl.java index 8cf49c189874c70ec16b227910bf0a28cff49ee2..227b82d6291278743b5011a39dba760b79c651ca 100644 --- a/src/main/java/polish_community_group_11/polish_community/information/services/InformationServiceImpl.java +++ b/src/main/java/polish_community_group_11/polish_community/information/services/InformationServiceImpl.java @@ -1,10 +1,16 @@ package polish_community_group_11.polish_community.information.services; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.stereotype.Service; import polish_community_group_11.polish_community.information.dao.InformationRepository; import polish_community_group_11.polish_community.information.models.DBInfo; +import polish_community_group_11.polish_community.information.models.DBInfoImpl; import polish_community_group_11.polish_community.information.models.SearchResult; +import polish_community_group_11.polish_community.information.models.ViewModel.DBInfoForm; +import java.sql.SQLException; +import java.time.LocalDate; import java.util.List; /** @@ -17,19 +23,19 @@ import java.util.List; public class InformationServiceImpl implements InformationService{ // The InformationRepository is injected by Spring using constructor injection - private InformationRepository informationRepository; + private final InformationRepository informationRepository; // Spring automatically injects an instance of InformationRepository into this service at runtime. public InformationServiceImpl(InformationRepository informationRepository){ this.informationRepository = informationRepository; } - public DBInfo getItemById(int id) { - return informationRepository.getItemById(id); + public DBInfo getInfomrationItemById(int id) throws EmptyResultDataAccessException, IncorrectResultSizeDataAccessException, SQLException { + return informationRepository.getInfomrationById(id); } - public List<DBInfo> getAllItems() { - return informationRepository.getDbInfoList(); + public List<DBInfo> getAllItemsByCategoryId(int category_id) { + return informationRepository.getDbInfoListByCategory(category_id); } public List<SearchResult> searchInfo(String userInput) { return informationRepository.findDbInfo(userInput) @@ -41,4 +47,48 @@ public class InformationServiceImpl implements InformationService{ .build()) .toList(); } + + public void addNewInfo(DBInfo newInfo) throws SQLException { + informationRepository.addNewInfo(newInfo); + } + + public int deleteInfoByList(List<Integer> deleteIdList) throws SQLException { + return informationRepository.deleteInfoList(deleteIdList); + } + + public int editInfo(DBInfo updatedInfo) throws SQLException { + return informationRepository.editInfo(updatedInfo); + } + + public DBInfo processDbInfoForm(DBInfoForm dbInfoForm) { + if (dbInfoForm == null) { + throw new IllegalArgumentException("dbInfoForm cannot be null"); + } + DBInfo newInfo = new DBInfoImpl(); + newInfo.setCategoryId(dbInfoForm.getCategoryId()); + newInfo.setInfoTitle(dbInfoForm.getInfoTitle()); + newInfo.setShortDescription(dbInfoForm.getShortDescription()); + newInfo.setInfoDescription(dbInfoForm.getInfoDescription()); + newInfo.setInformationId(dbInfoForm.getInformationId()); + newInfo.setCreatedBy(dbInfoForm.getCreatedBy()); + newInfo.setCreatedDate(dbInfoForm.getCreatedDate()); + newInfo.setTagId(dbInfoForm.getTagId()); + return newInfo; + } + + public DBInfoForm processDbInfoFormForEdit(DBInfo selectedInfo) { + if (selectedInfo == null) { + throw new IllegalArgumentException("dbInfoForm cannot be null"); + } + DBInfoForm selectedInfoForm = new DBInfoForm(); + selectedInfoForm.setCategoryId(selectedInfo.getCategoryId()); + selectedInfoForm.setInfoTitle(selectedInfo.getInfoTitle()); + selectedInfoForm.setInformationId(selectedInfo.getInformationId()); + selectedInfoForm.setShortDescription(selectedInfo.getShortDescription()); + selectedInfoForm.setInfoDescription(selectedInfo.getInfoDescription()); + selectedInfoForm.setCreatedBy(selectedInfo.getCreatedBy()); + selectedInfoForm.setCreatedDate(selectedInfo.getCreatedDate()); + selectedInfoForm.setTagId(selectedInfo.getTagId()); + return selectedInfoForm; + } } diff --git a/src/main/java/polish_community_group_11/polish_community/news/controllers/EditNewsController.java b/src/main/java/polish_community_group_11/polish_community/news/controllers/EditNewsController.java new file mode 100644 index 0000000000000000000000000000000000000000..0a5b3b08ffeedb7657d6d132325a942b31ec06c1 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/news/controllers/EditNewsController.java @@ -0,0 +1,93 @@ +package polish_community_group_11.polish_community.news.controllers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; + +import polish_community_group_11.polish_community.news.services.NewsService; +import polish_community_group_11.polish_community.news.models.News; +import polish_community_group_11.polish_community.news.models.NewsImpl; + +import java.sql.SQLException; +import java.time.LocalDate; + +// controller for the edit news page +// this file is used for handling the displaying the page and for handling the buttons requests +@Controller +public class EditNewsController { + + private final NewsService newsService; + + @Autowired + public EditNewsController(NewsService newsService) { + this.newsService = newsService; + } + + // mapping for the edit page with the particular news id the user is editing + @GetMapping("/editNews/{id}") + public ModelAndView showEditNewsForm(@PathVariable("id") int id) throws SQLException { + // fetch the news by the ID from the news service class + News news = newsService.getNewsById(id); + + // error checking + // check to see if the news class is empty + if (news == null) { + throw new IllegalArgumentException("Invalid news ID: " + id); + } + + // check to see if the date class is empty and if so then set a default date + if (news.getNews_upload_date() == null) { + news.setNews_upload_date(LocalDate.now()); + } + + // logs to check if mapping is working correctly + System.out.println("News ID: " + news.getNews_id()); + + ModelAndView modelAndView = new ModelAndView("news/editNews"); + // add the news object to the model + modelAndView.addObject("news", news); + return modelAndView; + } + + // post mapping for when the user submits the form + // used @RequestParam due to @ModelAttribute not working + // @RequestParam selects the fields from the form individually + + @PostMapping("/editNews/{id}") + public String editOrDeleteNews(@RequestParam("news_id") int news_id, + @RequestParam("news_title") String news_title, + @RequestParam("news_summary") String news_summary, + @RequestParam("news_source") String news_source, + @RequestParam("news_link") String news_link, + @RequestParam("news_image_url") String news_image_url, + @RequestParam("user_id") int user_id, + @RequestParam("news_upload_date") String news_upload_date, + @RequestParam("action") String action) throws SQLException { + + // if the user clicks the edit button + if (action.equals("edit")) { + + // create new instance of the news class with the data in the fields + News news = new NewsImpl(news_id, news_title, news_summary, news_source, news_link, + news_image_url, user_id, LocalDate.parse(news_upload_date)); + // update the news using the news service class + newsService.updateNews(news); + // redirect to the news list + return "redirect:/news"; + } + + // if the user clicks the delete button + else if (action.equals("delete")) { + // delete the news using the news service class + newsService.deleteNews(news_id); + return "redirect:/news"; + } + // error if the event is not recognised + throw new IllegalArgumentException("Invalid action: " + action); + } +} \ No newline at end of file diff --git a/src/main/java/polish_community_group_11/polish_community/news/controllers/NewsController.java b/src/main/java/polish_community_group_11/polish_community/news/controllers/NewsController.java index 659f55075138bd9758efbbf46d1ff08c923f8a37..5f7d5ec3351c8ff9810351c1aebdb99274c036e9 100644 --- a/src/main/java/polish_community_group_11/polish_community/news/controllers/NewsController.java +++ b/src/main/java/polish_community_group_11/polish_community/news/controllers/NewsController.java @@ -1,9 +1,16 @@ package polish_community_group_11.polish_community.news.controllers; +import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; +import polish_community_group_11.polish_community.information.models.DBInfo; +import polish_community_group_11.polish_community.information.models.DBInfoImpl; +import polish_community_group_11.polish_community.news.models.News; +import polish_community_group_11.polish_community.news.models.NewsImpl; import polish_community_group_11.polish_community.news.services.NewsService; import java.sql.SQLException; @@ -21,6 +28,22 @@ public class NewsController { public ModelAndView getNews() throws SQLException { ModelAndView modelAndView = new ModelAndView("news/newsList"); modelAndView.addObject("newsList",newsService.getAllNews()); + modelAndView.addObject("news" , new NewsImpl()); return modelAndView; } -} + + @PostMapping("/news/add") + public ModelAndView addInformation(@Valid @ModelAttribute("news") NewsImpl news, + BindingResult bindingResult, Model model) throws SQLException { + ModelAndView modelAndView = new ModelAndView("news/addNews"); + if(bindingResult.hasErrors()){ + modelAndView.addObject(model.asMap()); + } + else{ + News newsToAdd = news; + newsService.addNews(newsToAdd); + modelAndView = new ModelAndView("redirect:/news"); + } + return modelAndView; + } + } diff --git a/src/main/java/polish_community_group_11/polish_community/news/dao/NewsRepository.java b/src/main/java/polish_community_group_11/polish_community/news/dao/NewsRepository.java index 941f89beb276a3ba4494635ad430fe6e733ea13d..94a31a6e6a915298053c813be3e16ec5c5f8a1cf 100644 --- a/src/main/java/polish_community_group_11/polish_community/news/dao/NewsRepository.java +++ b/src/main/java/polish_community_group_11/polish_community/news/dao/NewsRepository.java @@ -1,5 +1,6 @@ package polish_community_group_11.polish_community.news.dao; +import polish_community_group_11.polish_community.information.models.DBInfo; import polish_community_group_11.polish_community.news.models.News; import java.sql.SQLException; @@ -7,4 +8,12 @@ import java.util.List; public interface NewsRepository { public List<News> getAllNews() throws SQLException; + + + void addNews(News news) throws SQLException; + + News getNewsById(int id) throws SQLException; + void updateNews(News news) throws SQLException; + void deleteNews(int id) throws SQLException; } + diff --git a/src/main/java/polish_community_group_11/polish_community/news/dao/NewsRepositoryImpl.java b/src/main/java/polish_community_group_11/polish_community/news/dao/NewsRepositoryImpl.java index da3b1a3923400959a2bf181095bdee4ec24a2429..435159e4e241b605d29dcf738a9b8aa2ab72a683 100644 --- a/src/main/java/polish_community_group_11/polish_community/news/dao/NewsRepositoryImpl.java +++ b/src/main/java/polish_community_group_11/polish_community/news/dao/NewsRepositoryImpl.java @@ -6,11 +6,13 @@ import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; +import polish_community_group_11.polish_community.information.models.DBInfo; import polish_community_group_11.polish_community.news.models.News; import polish_community_group_11.polish_community.news.models.NewsImpl; import java.sql.SQLException; import java.time.DateTimeException; +import java.time.LocalDate; import java.util.List; @Repository // Marks the class as a repository in the Spring context, responsible for data access logic @@ -80,4 +82,91 @@ public class NewsRepositoryImpl implements NewsRepository { throw new SQLException("There is an error in your SQL syntax"); } } + + + public void addNews(News news) throws SQLException{ + String dbInsertSql = + "insert into news " + + "(news_title, news_summary, news_source, " + + "news_link, news_image_url, user_id, news_upload_date)" + + " values (?,?,?,?,?,?,?)"; + try { + int x = jdbc.update(dbInsertSql, + news.getNews_title(), + news.getNews_summary(), + news.getNews_source(), + news.getNews_link(), + news.getNews_image_url(), + 1, + LocalDate.now() + ); + }catch (DataAccessException e) { + throw new SQLException("Failed to insert new information record", e); + } + } + + + + + + @Override + public News getNewsById(int id) throws SQLException { + String sql = "SELECT * FROM news WHERE news_id = ?"; + try { + return jdbc.queryForObject(sql, newsMapper, id); + } catch (EmptyResultDataAccessException e) { + throw new SQLException("No news item found with ID: " + id); + } catch (DataAccessException e) { + throw new SQLException("Error retrieving news with ID: " + id); + } + } + + // method for updating news data from the database + @Override + public void updateNews(News news) throws SQLException { + + // sql query that says to update the fields where it match the news id + String sql = "UPDATE news SET news_title = ?, news_summary = ?, news_source = ?, news_link = ?, " + + "news_image_url = ?, user_id = ?, news_upload_date = ? WHERE news_id = ?"; + try { + // jdbc.update() is a method that will execute the sql query + // replaces the ? with the actual values from the news object + int rowsAffected = jdbc.update(sql, + news.getNews_title(), + news.getNews_summary(), + news.getNews_source(), + news.getNews_link(), + news.getNews_image_url(), + news.getUser_id(), + news.getNews_upload_date(), + news.getNews_id() + ); + + // error handling + if (rowsAffected == 0) { + throw new SQLException("No news item was updated. Check the ID provided."); + } + } catch (DataAccessException e) { + throw new SQLException("Error updating news with ID: " + news.getNews_id(), e); + } + } + + // method for deleting news data from the database + @Override + public void deleteNews(int id) throws SQLException { + + // sql query that says to delete the row where the id matches + String sql = "DELETE FROM news WHERE news_id = ?"; + try { + // executing the query with the news id + int rowsAffected = jdbc.update(sql, id); + + // error handling + if (rowsAffected == 0) { + throw new SQLException("No news item was deleted. Check the ID provided."); + } + } catch (DataAccessException e) { + throw new SQLException("Error deleting news with ID: " + id, e); + } + } } diff --git a/src/main/java/polish_community_group_11/polish_community/news/models/NewsImpl.java b/src/main/java/polish_community_group_11/polish_community/news/models/NewsImpl.java index 2446dd30b4f2b64ac9bcb08e4128be6097e275c3..9ba56a32a2678183a7734eadbb2585616e1bb486 100644 --- a/src/main/java/polish_community_group_11/polish_community/news/models/NewsImpl.java +++ b/src/main/java/polish_community_group_11/polish_community/news/models/NewsImpl.java @@ -1,15 +1,20 @@ package polish_community_group_11.polish_community.news.models; +import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import java.time.LocalDate; @Data +@NoArgsConstructor @AllArgsConstructor public class NewsImpl implements News{ private int news_id; + @NotEmpty(message = "News Title should not be empty!") private String news_title; + @NotEmpty(message = "News summary should not be empty!") private String news_summary; private String news_source; private String news_link; diff --git a/src/main/java/polish_community_group_11/polish_community/news/services/NewsService.java b/src/main/java/polish_community_group_11/polish_community/news/services/NewsService.java index ec61e2f18ae56de77327ebf97747ea95ad259b1f..d417544d8ef701a309f0a5eb28d9fad526fba486 100644 --- a/src/main/java/polish_community_group_11/polish_community/news/services/NewsService.java +++ b/src/main/java/polish_community_group_11/polish_community/news/services/NewsService.java @@ -1,5 +1,6 @@ package polish_community_group_11.polish_community.news.services; +import polish_community_group_11.polish_community.information.models.DBInfo; import polish_community_group_11.polish_community.news.models.News; import java.sql.SQLException; @@ -7,4 +8,13 @@ import java.util.List; public interface NewsService { public List<News> getAllNews() throws SQLException; + + // Save new news article + void addNews(News news) throws SQLException; + + + News getNewsById(int id) throws SQLException; + void updateNews(News news) throws SQLException; + void deleteNews(int id) throws SQLException; + } diff --git a/src/main/java/polish_community_group_11/polish_community/news/services/NewsServiceImpl.java b/src/main/java/polish_community_group_11/polish_community/news/services/NewsServiceImpl.java index 2edf15c2130c5a9339f1722d022fc5d05812bd90..eba45933e36a90aa53d5f67be7ae7b7699362c89 100644 --- a/src/main/java/polish_community_group_11/polish_community/news/services/NewsServiceImpl.java +++ b/src/main/java/polish_community_group_11/polish_community/news/services/NewsServiceImpl.java @@ -20,4 +20,27 @@ public class NewsServiceImpl implements NewsService { public List<News> getAllNews() throws SQLException, EmptyResultDataAccessException { return newsRepository.getAllNews(); } + + @Override + + public void addNews(News news)throws SQLException { + newsRepository.addNews(news); + } + + + public News getNewsById(int id) throws SQLException { + return newsRepository.getNewsById(id); + } + + @Override + public void updateNews(News news) throws SQLException { + // link to the repository that directly interactes with the database + newsRepository.updateNews(news); + } + + @Override + public void deleteNews(int id) throws SQLException { + newsRepository.deleteNews(id); + } + } diff --git a/src/main/java/polish_community_group_11/polish_community/register/controllers/RegisterController.java b/src/main/java/polish_community_group_11/polish_community/register/controllers/RegisterController.java index fab07ffd15cc1fb8a668c3cc1a1ad2bed71ce7ec..c6aba7401b29156bfa9fa9c9bdd8d64f2f35429f 100644 --- a/src/main/java/polish_community_group_11/polish_community/register/controllers/RegisterController.java +++ b/src/main/java/polish_community_group_11/polish_community/register/controllers/RegisterController.java @@ -1,50 +1,56 @@ package polish_community_group_11.polish_community.register.controllers; -/* -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.servlet.ModelAndView; - -import java.util.List; -import java.util.ArrayList;*/ -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; -import polish_community_group_11.polish_community.register.services.UserRepository; import polish_community_group_11.polish_community.register.models.User; +import polish_community_group_11.polish_community.register.models.Role; import polish_community_group_11.polish_community.register.services.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; -import org.springframework.stereotype.Controller; +import polish_community_group_11.polish_community.register.services.RoleService; + +import java.util.List; @Controller public class RegisterController { @Autowired private UserService userService; - + @Autowired + private RoleService roleService; + + // displaying the registration form using get request and ModelAndView @GetMapping("/register") public ModelAndView showRegistrationForm() { + // create a ModelAndView object using the register.html file ModelAndView modelAndView = new ModelAndView("register/register"); + + // add an empty User object to the model modelAndView.addObject("user", new User()); + + // add all the roles currently in the roles table so the so the admin can create a account with a speicif role + modelAndView.addObject("roles", roleService.findAllRoles()); return modelAndView; } + // for handling form submission @PostMapping("/register") - public String registerUser(@ModelAttribute User user) { + public String registerUser(@ModelAttribute User user, @RequestParam int roleId) { - if (user.getRole() == null || user.getRole().isEmpty()) { - user.setRole("USER"); // set default role to user - } + // Set the role ID (instead of using the role enum) + user.setRoleId(roleId); + // check to see if the users email already exists if (userService.findByEmail(user.getEmail()) != null) { return "redirect:/register?error=email_taken"; } // save user to the database userService.saveUser(user); + + // redirect to the login page return "redirect:/login"; } -} +} \ No newline at end of file diff --git a/src/main/java/polish_community_group_11/polish_community/register/controllers/UserApiController.java b/src/main/java/polish_community_group_11/polish_community/register/controllers/UserApiController.java new file mode 100644 index 0000000000000000000000000000000000000000..56915242a4b6471718d3846aa54f86dd0e164cb9 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/register/controllers/UserApiController.java @@ -0,0 +1,29 @@ +package polish_community_group_11.polish_community.register.controllers; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import polish_community_group_11.polish_community.register.dao.UserRepository; +import polish_community_group_11.polish_community.register.models.User; + +@RestController +@RequestMapping("/api/users") +public class UserApiController { + private final UserRepository userRepository; + + public UserApiController(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @GetMapping("/{userId}") + public ResponseEntity<User> getUserById(@PathVariable int userId) { + try { + User user = userRepository.findById(userId); + return ResponseEntity.ok(user); + } catch (Exception e) { + return ResponseEntity.notFound().build(); + } + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/register/dao/RoleRepository.java b/src/main/java/polish_community_group_11/polish_community/register/dao/RoleRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..1fe64109142fc647c96f0b6758f9f36b9ec575a7 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/register/dao/RoleRepository.java @@ -0,0 +1,9 @@ +package polish_community_group_11.polish_community.register.dao; + +import polish_community_group_11.polish_community.register.models.Role; +import java.util.List; + +public interface RoleRepository { + Role findRoleById(int roleId); + List<Role> findAllRoles(); +} \ No newline at end of file diff --git a/src/main/java/polish_community_group_11/polish_community/register/dao/RoleRepositoryImpl.java b/src/main/java/polish_community_group_11/polish_community/register/dao/RoleRepositoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5692dead187d0a6e3596df272734e3bdca66a787 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/register/dao/RoleRepositoryImpl.java @@ -0,0 +1,37 @@ +package polish_community_group_11.polish_community.register.dao; + +import polish_community_group_11.polish_community.register.models.Role; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.stereotype.Repository; + +@Repository +public class RoleRepositoryImpl implements RoleRepository { + + @Autowired + private JdbcTemplate jdbcTemplate; + + // Fetch a role by its ID + public Role findRoleById(int roleId) { + String sql = "SELECT id, role_name FROM roles WHERE id = ?"; + return jdbcTemplate.queryForObject(sql, new Object[]{roleId}, (rs, rowNum) -> { + Role role = new Role(); + role.setId(rs.getInt("id")); + role.setName(rs.getString("role_name")); + return role; + }); + } + + // Fetch all roles (for displaying in registration form) + public List<Role> findAllRoles() { + String sql = "SELECT id, role_name FROM roles"; + return jdbcTemplate.query(sql, (rs, rowNum) -> { + Role role = new Role(); + role.setId(rs.getInt("id")); + role.setName(rs.getString("role_name")); + return role; + }); + } +} \ No newline at end of file diff --git a/src/main/java/polish_community_group_11/polish_community/register/dao/UserRepository.java b/src/main/java/polish_community_group_11/polish_community/register/dao/UserRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..945b72d91dfdb22bf599c5b4b185adf8513f87e6 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/register/dao/UserRepository.java @@ -0,0 +1,16 @@ +package polish_community_group_11.polish_community.register.dao; + +import java.util.List; + +import polish_community_group_11.polish_community.register.models.User; + +public interface UserRepository { + int saveUser(User user); // add user into the database + + User findByEmail(String email); // find user by email + + User findById(int id); // find user by ID + + List<User> findAllUsers(); // get all the users + +} \ No newline at end of file diff --git a/src/main/java/polish_community_group_11/polish_community/register/dao/UserRepositoryImpl.java b/src/main/java/polish_community_group_11/polish_community/register/dao/UserRepositoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5d12c85607ddbfe25dda9c8281756e2d1a3c0dc4 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/register/dao/UserRepositoryImpl.java @@ -0,0 +1,112 @@ +package polish_community_group_11.polish_community.register.dao; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import polish_community_group_11.polish_community.register.models.User; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.List; + +@Repository +public class UserRepositoryImpl implements UserRepository { + + @Autowired + private JdbcTemplate jdbcTemplate; // JdbcTemplate is used to interact with the database + + // function for saving user + public int saveUser(User user) { + // sql query for inserting into users table + String sql = "INSERT INTO users (email, password, fullname, dob, role_id) VALUES (?, ?, ?, ?, ?)"; + int rowsAffected = jdbcTemplate.update(sql, + user.getEmail(), + user.getPassword(), + user.getFullname(), + user.getDateOfBirth(), + user.getRoleId()); + user.getOrganization(); + + if (rowsAffected > 0) { + // if user is successfully inserted, get the user ID + String getUserIdSql = "SELECT LAST_INSERT_ID()"; + int userId = jdbcTemplate.queryForObject(getUserIdSql, Integer.class); + + // link the user to the role by inserting into the user_roles table + String userRolesSql = "INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)"; + jdbcTemplate.update(userRolesSql, userId, user.getRoleId()); + + return userId; // or return something else if needed + } + + return -1; + } + + // function for fetching all users + public List<User> findAllUsers() { + // sql query for selecting all users from users table + String sql = "SELECT * FROM users"; + return jdbcTemplate.query(sql, (rs, rowNum) -> { + User user = new User(); + user.setId(rs.getInt("id")); + user.setEmail(rs.getString("email")); + user.setPassword(rs.getString("password")); + user.setFullname(rs.getString("fullname")); + user.setDateOfBirth(rs.getObject("dob", LocalDate.class)); // attempt to get object by using localdate + user.setRoleId(rs.getInt("role_id")); + user.setOrganization(rs.getString("organization")); + return user; + }); + } + + // function for finding user by email + public User findByEmail(String email) { + // SQL query to find user by email + String sql = "SELECT * FROM users WHERE email = ?"; + + try { + // Using queryForObject to directly return the result as a single User object + return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> { + User user = new User(); + user.setId(rs.getInt("id")); + user.setEmail(rs.getString("email")); + user.setPassword(rs.getString("password")); + user.setFullname(rs.getString("fullname")); + user.setDateOfBirth(rs.getObject("dob", LocalDate.class)); + user.setRoleId(rs.getInt("role_id")); + user.setOrganization(rs.getString("organization")); + return user; + }, email); + } catch (EmptyResultDataAccessException e) { + // return null if no user is found with the email + return null; + } + } + + + // function for finding user by id + public User findById(int id) { + // sql query for finding a user by id + String sql = "SELECT * FROM users WHERE id = ?"; + try { + // Using queryForObject to directly return the result as a single User object + return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> { + User user = new User(); + user.setId(rs.getInt("id")); + user.setEmail(rs.getString("email")); + user.setPassword(rs.getString("password")); + user.setFullname(rs.getString("fullname")); + user.setDateOfBirth(rs.getObject("dob", LocalDate.class)); + user.setRoleId(rs.getInt("role_id")); + user.setOrganization(rs.getString("organization")); + return user; + }, id); + } catch (EmptyResultDataAccessException e) { + // return null if no user is found with the id + return null; + } + } + +} diff --git a/src/main/java/polish_community_group_11/polish_community/register/models/Role.java b/src/main/java/polish_community_group_11/polish_community/register/models/Role.java new file mode 100644 index 0000000000000000000000000000000000000000..a4006478f5faca31f5a01ce7fa5ba0e7e427b602 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/register/models/Role.java @@ -0,0 +1,23 @@ +package polish_community_group_11.polish_community.register.models; + +public class Role { + private int id; + private String name; // Role name + + // Getters and setters + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/register/models/User.java b/src/main/java/polish_community_group_11/polish_community/register/models/User.java index d9a24488718b24dd628093092302fc874d408a2c..1c152fdb496ad6d984e1b9dada97dd024a1bf69d 100644 --- a/src/main/java/polish_community_group_11/polish_community/register/models/User.java +++ b/src/main/java/polish_community_group_11/polish_community/register/models/User.java @@ -1,28 +1,19 @@ package polish_community_group_11.polish_community.register.models; -import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.List; -@Entity -@Table(name = "users") public class User { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; - - @Column(nullable = false) private String email; - - @Column(nullable = false) private String password; - - @Column(nullable = false) - private String role; - - @Column(nullable = false) private String fullname; + private LocalDate dob; // use LocalDate for dob + private int roleId; + private String organization; + - // Getters and setters + // getters and setters public int getId() { return id; } @@ -55,11 +46,23 @@ public class User { this.email = email; } - public String getRole() { - return role; + public LocalDate getDateOfBirth() { + return dob; } - public void setRole(String role) { - this.role = role; + public void setDateOfBirth(LocalDate dob) { + this.dob = dob; } -} + + public int getRoleId() { + return roleId; + } + + public void setRoleId(int roleId) { + this.roleId = roleId; + } + + public String getOrganization() { return organization; } + + public void setOrganization(String organization){ this.organization = organization;} +} \ No newline at end of file diff --git a/src/main/java/polish_community_group_11/polish_community/register/services/RoleService.java b/src/main/java/polish_community_group_11/polish_community/register/services/RoleService.java new file mode 100644 index 0000000000000000000000000000000000000000..c2062a2478641f3829c3dc964e00e88f62b31f43 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/register/services/RoleService.java @@ -0,0 +1,22 @@ +package polish_community_group_11.polish_community.register.services; + +import polish_community_group_11.polish_community.register.dao.RoleRepository; +import polish_community_group_11.polish_community.register.models.Role; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import java.util.List; + +@Service +public class RoleService { + + @Autowired + private RoleRepository roleRepository; + + public List<Role> findAllRoles() { + return roleRepository.findAllRoles(); + } + + public Role findRoleById(int roleId) { + return roleRepository.findRoleById(roleId); + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/register/services/UserRepository.java b/src/main/java/polish_community_group_11/polish_community/register/services/UserRepository.java deleted file mode 100644 index 2102840d4ca40c742749853621269b57887be525..0000000000000000000000000000000000000000 --- a/src/main/java/polish_community_group_11/polish_community/register/services/UserRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package polish_community_group_11.polish_community.register.services; - -import polish_community_group_11.polish_community.register.models.User; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface UserRepository extends JpaRepository<User, Long> { - User findByEmail(String email); -} \ No newline at end of file diff --git a/src/main/java/polish_community_group_11/polish_community/register/services/UserService.java b/src/main/java/polish_community_group_11/polish_community/register/services/UserService.java index 7259c33967bf873282ad751a59f6ba90f2c057ad..63cf2d3a58c459ec7d14a667c5510223243a8fce 100644 --- a/src/main/java/polish_community_group_11/polish_community/register/services/UserService.java +++ b/src/main/java/polish_community_group_11/polish_community/register/services/UserService.java @@ -1,26 +1,15 @@ package polish_community_group_11.polish_community.register.services; -import polish_community_group_11.polish_community.register.models.User; -import polish_community_group_11.polish_community.register.services.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; +import java.util.List; -@Service -public class UserService { +import polish_community_group_11.polish_community.register.models.User; - @Autowired - private UserRepository userRepository; +public interface UserService { + void saveUser(User user); - public User saveUser(User user) { - // uses the Jpa repository built in save methods - return userRepository.save(user); - } + User findById(int id); - public User findById(Long id) { - return userRepository.findById(id).orElse(null); - } - - public User findByEmail(String email) { - return userRepository.findByEmail(email); - } -} + User findByEmail(String email); + + List<User> findAllUsers(); +} \ No newline at end of file diff --git a/src/main/java/polish_community_group_11/polish_community/register/services/UserServiceImpl.java b/src/main/java/polish_community_group_11/polish_community/register/services/UserServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..d2c0a3e498d415ace85964d8964c78108ed7afb5 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/register/services/UserServiceImpl.java @@ -0,0 +1,40 @@ +package polish_community_group_11.polish_community.register.services; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import polish_community_group_11.polish_community.register.models.User; +import polish_community_group_11.polish_community.register.dao.UserRepository; +import java.util.List; + +@Service +public class UserServiceImpl implements UserService { + + // @Autowired has spring automatically create an instance of UserRepository + @Autowired + private UserRepository userRepository; + + // @Override marks this method as an implementation of the saveUser method from UserService + @Override + public void saveUser(User user) { + // calls saveUser method + userRepository.saveUser(user); + } + + @Override + public User findById(int id) { + // calls findById method + return userRepository.findById(id); + } + + @Override + public User findByEmail(String email) { + // calls findByEmail method + return userRepository.findByEmail(email); + } + + @Override + public List<User> findAllUsers() { + // Calls findAll method of the UserRepository + return userRepository.findAllUsers(); + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/security/WebSecurityConfig.java b/src/main/java/polish_community_group_11/polish_community/security/WebSecurityConfig.java index cb62031327e515e114915933b67ff3c5af8147d9..962eda9faeada26f1522a5480142f7404c66682f 100644 --- a/src/main/java/polish_community_group_11/polish_community/security/WebSecurityConfig.java +++ b/src/main/java/polish_community_group_11/polish_community/security/WebSecurityConfig.java @@ -13,10 +13,14 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; import polish_community_group_11.polish_community.register.models.User; import polish_community_group_11.polish_community.register.services.UserService; +import polish_community_group_11.polish_community.register.models.Role; +import polish_community_group_11.polish_community.register.services.RoleService; import java.lang.invoke.MethodHandles; +import java.util.Arrays; import java.util.Set; @Configuration @@ -26,9 +30,17 @@ public class WebSecurityConfig { private final UserService userService; + private final RoleService roleService; - public WebSecurityConfig(UserService userService) { + private final String[] whiteListingPath = { +// "/event", +// "event/*" + "/api/feed/**" + }; + + public WebSecurityConfig(UserService userService, RoleService roleService) { this.userService = userService; + this.roleService = roleService; } @@ -37,10 +49,11 @@ public class WebSecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http + .csrf(csrf -> csrf.disable()) // require authentication for events only //update in future when more protected resources are available .authorizeHttpRequests((requests) -> requests - .requestMatchers("/event", "event/*").authenticated() + .requestMatchers(whiteListingPath).authenticated() .anyRequest().permitAll() ) .formLogin((form) -> form @@ -87,9 +100,13 @@ public class WebSecurityConfig { LOG.error("Couldn't find user with this name: {}", username); throw new UsernameNotFoundException(username); } + + Role role = roleService.findRoleById(user.getRoleId()); // Assuming getRoleId() returns the roleId + String roleName = "ROLE_" + role.getName().toUpperCase(); // Format the role to ROLE_NAME + //prefix all passwords with {noop} to allow login to run without adding encryption return new org.springframework.security.core.userdetails.User(user.getEmail(), "{noop}"+user.getPassword(), true, true, - true, true, Set.of(new SimpleGrantedAuthority("ROLE_" + user.getRole()))); + true, true, Set.of(new SimpleGrantedAuthority(roleName))); } catch (DataAccessException e) { LOG.error("Error when finding user by email: {}", username, e); throw new UsernameNotFoundException(username); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3a8fb1ae5acf97d0ab820cee646d1bbe6546fae0..e12924f4d009b519539abb5995545d2526ee5dcd 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -10,11 +10,18 @@ spring.thymeleaf.layout-dialect.enabled=true spring.datasource.url=jdbc:mariadb://localhost:3306/polish_community spring.datasource.username=root spring.datasource.password=comsc -spring.jpa.hibernate.ddl-auto=update + spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true spring.sql.init.mode=always +server.error.whitelabel.enabled=false +server.error.path=/error +spring.jpa.hibernate.ddl-auto=none +spring.sql.init.schema-locations=classpath:/schema.sql +spring.sql.init.data-locations=classpath:/data.sql, classpath:/data_information_domains.sql +#spring.jpa.hibernate.ddl-auto=none +spring.jpa.defer-datasource-initialization=true + -spring.jpa.hibernate.ddl-auto=create diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index bb2c4c511209b70a45fad27cdd4409c00f166f8a..50fdd5a36fa3d207c55898969b61bdf30c4624e5 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,87 +1,15 @@ --- mock data for information - +-- mock data for category delete from information; - -insert into information (category_id, info_title, created_by, created_date, updated_date, tag_id, info_article) -values (1, 'Education Information for Polish Nationals in the UK', 'Krzysztof Nowak', CURRENT_DATE, CURRENT_DATE, 3, '# **Education System in the UK for Polish Nationals** - -This guide is designed to help Polish nationals in the UK navigate the education system, from primary school to higher education. It offers information on enrollment procedures, qualifications, and financial support available for Polish students. - ---- - -## **Enrollment Procedures** -- Children of Polish nationals are entitled to free primary and secondary education in the UK. -- For higher education, Polish students can apply for UK universities, often requiring proof of previous educational qualifications. - ---- - -## **Key Education Policies** -- Polish students in the UK can apply for student loans or financial assistance, depending on residency status and eligibility. -- Many universities offer support services for international students, including language assistance and career advice. - -> **Tip:** It''s important to check with the university for specific entry requirements related to your previous education in Poland. - ---- - -## **Post-Graduation Opportunities** -- Polish students graduating from UK universities are eligible for the Graduate Route visa, allowing them to stay and work in the UK for two years. - ---- - -## **Useful Links & Resources** -- [UK Government Education Portal](https://www.gov.uk/education) -- [Polish Community in the UK Education Support](https://www.polandinuk.co.uk)'); -insert into information (category_id, info_title, created_by, created_date, updated_date, tag_id, info_article) -values (2, 'Healthcare Access for Polish Nationals in the UK', 'Marek Jablonski', CURRENT_DATE, CURRENT_DATE, 4, '# **Navigating Healthcare in the UK for Polish Nationals** -As a Polish national residing in the UK, you have access to healthcare services through the NHS (National Health Service). This guide will explain how to register with a GP (General Practitioner), access emergency services, and apply for healthcare-related assistance. - ---- - -## **Registering with a GP** -- To receive NHS healthcare, you must first register with a GP. -- Bring proof of your address and identification when registering. - ---- - -## **Emergency Healthcare** -- In emergencies, dial 999 for an ambulance or visit your nearest Accident and Emergency (A&E) department. -- Polish citizens are eligible for free emergency treatment. - ---- - -## **Health Insurance and Costs** -- Polish nationals who are employed in the UK contribute to the National Insurance system, which covers many healthcare costs. -- For non-residents or those without work, certain services may require payment. - ---- - -## **Important Notes** -- Ensure to carry your NHS number when visiting health services to avoid delays in treatment. -- If you’re planning to travel outside the UK, consider travel health insurance. - ---- - -## **Useful Links & Resources** -- [NHS Services for Overseas Nationals](https://www.nhs.uk/using-the-nhs/healthcare-when-abroad) -- [Polish Healthcare Support in the UK](https://www.polishcommunity.org.uk)'); - -INSERT INTO information (category_id, info_title, created_by, created_date, updated_date, tag_id, info_article) -VALUES (3, 'Find legal advice and information', 'Jane Doe', CURRENT_DATE, CURRENT_DATE, 4, 'Overview -You can get legal advice and information to solve common problems, for example about: - -debts -decisions about benefits -discrimination at work -immigration statuses -disagreements with neighbours -making arrangements for children, money or property if you divorce or separate -Get advice and information as early as you can. It might stop a problem from getting worse. - -You may need to find a legal adviser with specialist training in the area of your problem, for example a solicitor. They could help you solve it, or give you advice about what to do next. - -You might be able to solve the problem without going to court, for example by working with a mediator. - -If you need to go to court to solve the problem, you can find out how to prepare for a court hearing.'); +delete from categories; +INSERT INTO categories (category_title, category_description, user_id) +VALUES + ('Work', 'Information about employment opportunities, workplace rights, and support available to migrants in Wales', 1), + ('Housing', 'Guidance on finding accommodation, understanding tenancy agreements, and accessing housing support services', 2), + ('Health and Social care', 'Information on accessing healthcare services, registering with a GP, and support for social care needs', 3), + ('Social connections', 'Opportunities and programs to foster integration and build friendships between migrants and local communities', 4), + ('Education and Skills', 'Details about schools, colleges, training programs, and opportunities for skill development in Wales', 5), + ('Safety and Stability', 'Important information on maintaining personal safety, reporting hate crimes, and accessing support for victims of crime', 6), + ('Rights and Responsibilities', 'Guidance on legal rights, civic responsibilities, and how migrants can actively participate in Welsh society', 7); -- mock data for event @@ -102,9 +30,9 @@ Inspire Future Projects: Your participation could spark new ideas and motivate o Stay Ahead of the Curve: By participating, you gain knowledge about the latest trends in science and technology, giving you an edge in academic and professional fields. By joining this science fair, you are not only enriching your own learning experience but also contributing to a vibrant community of innovators and explorers.'); insert into event (event_title, event_description, location, event_date, event_time,user_id, event_poster_url,whyJoin,benefits) -values ('Games Fair', 'Gamers explore through the game fair', 'Bristol', current_date,current_time, 1, 'https://d1csarkz8obe9u.cloudfront.net/posterpreviews/game-event-poster-template-c54aaeed440befaacf79e6dd35deb8f5_screen.jpg?ts=1486132851',"Abc", "Def"); +values ('Games Fair', 'Gamers explore through the game fair', 'Bristol', current_date,current_time, 1, 'https://d1csarkz8obe9u.cloudfront.net/posterpreviews/game-event-poster-template-c54aaeed440befaacf79e6dd35deb8f5_screen.jpg?ts=1486132851','Abc', 'Def'); insert into event (event_title, event_description, location, event_date, event_time,user_id, event_poster_url,whyJoin,benefits) -values ('Bikes Fair', 'Riders explore through the Ride fair', 'Newport', current_date,current_time, 1, 'https://d1csarkz8obe9u.cloudfront.net/posterpreviews/bike-fest-poster-design-template-fb1cc1ab4b2aee783f8ee75476c4c92d_screen.jpg?ts=1637012682',"Abc", "Def"); +values ('Bikes Fair', 'Riders explore through the Ride fair', 'Newport', current_date,current_time, 1, 'https://d1csarkz8obe9u.cloudfront.net/posterpreviews/bike-fest-poster-design-template-fb1cc1ab4b2aee783f8ee75476c4c92d_screen.jpg?ts=1637012682','Abc', 'Def'); -- mock data for news @@ -155,3 +83,59 @@ insert into event (event_title, event_description, location, event_date, event_t values ('Bikes Fair', 'Riders explore through the Ride fair', 'Newport', current_date,current_time, 1, 'https://d1csarkz8obe9u.cloudfront.net/posterpreviews/bike-fest-poster-design-template-fb1cc1ab4b2aee783f8ee75476c4c92d_screen.jpg?ts=1637012682'); insert into event (event_title, event_description, location, event_date, event_time,user_id, event_poster_url) values ('Bikes Fair', 'Riders explore through the Ride fair', 'Newport', current_date,current_time, 1, 'https://d1csarkz8obe9u.cloudfront.net/posterpreviews/bike-fest-poster-design-template-fb1cc1ab4b2aee783f8ee75476c4c92d_screen.jpg?ts='); + + +-- insert standard roles into roles table +INSERT INTO roles (role_name) VALUES ('ADMIN'); +INSERT INTO roles (role_name) VALUES ('USER'); + +-- insert a admin user into the table +INSERT INTO users (email, password, fullname, dob, role_id, organization) VALUES + ('admin@gmail.com', 'Password!', 'Harri Daives', '2003-07-22', 1, 'Ludek Polonia Wajiska'), + ('user1@gmail.com', 'Password123', 'John Smith', '1998-04-15', 2, 'Tech Solutions Ltd'), + ('user2@gmail.com', 'SecurePass1!', 'Jane Doe', '1995-11-20', 2, 'Global Innovations'), + ('user3@gmail.com', 'Pa$$w0rd', 'Emily Johnson', '1990-02-10', 2, 'Quick Logistics'), + ('user4@gmail.com', 'MySecureP@ss', 'Michael Brown', '1988-06-30', 2, 'Creative Works Inc.'), + ('user5@gmail.com', 'Welcome123!', 'Jessica Davis', '2001-08-12', 2, 'EduTech Solutions'), + ('user6@gmail.com', 'P@ssword789', 'Daniel Garcia', '1993-03-05', 2, 'HealthCare Inc.'), + ('user7@gmail.com', 'SuperSecure@', 'Sophia Wilson', '1999-09-25', 2, 'Food Supply Co.'), + ('user8@gmail.com', 'Passw0rd!', 'James Martinez', '2000-01-18', 2, 'Tech Support Hub'), + ('user9@gmail.com', 'UserPass!234', 'Isabella Taylor', '1996-05-22', 2, 'Dynamic Solutions'), + ('user10@gmail.com', 'ChangeMe#1', 'David Anderson', '1992-12-13', 2, 'EcoGreen Enterprises'); + + +INSERT INTO users (email, password, fullname, dob, role_id) +VALUES ('user@email.com', 'Abcdef1!', 'Jane Doe', '200-01-01', 1 ); +-- insert the user and role id's of the created roles and user +INSERT INTO user_roles (user_id, role_id) +VALUES (1, 1); + +-- Insert posts +INSERT INTO feed (post_image_url, post_title, post_description, post_time, user_id) VALUES + ('https://example.com/image1.jpg', 'Post 1', 'Description for post 1', '2024-12-07', 1), + ('https://example.com/image2.jpg', 'Post 2', 'Description for post 2', '2024-12-07', 2), + ('https://example.com/image3.jpg', 'Post 3', 'Description for post 3', '2024-12-07', 3); + +-- Insert tags +INSERT INTO tags (tag_name) VALUES + ('Technology'), + ('Lifestyle'), + ('Health'), + ('Sports'), + ('News'); + +-- Insert post-tag relationships +INSERT INTO feed_tags (post_id, tag_id) VALUES + (1, 1), + (1, 2), + (2, 3), + (2, 4); + +-- Insert likes +INSERT INTO post_likes (post_id, user_id) VALUES + (1, 1), + (1, 2), + (1, 3), + (2, 2), + (2, 4); + diff --git a/src/main/resources/data_information_domains.sql b/src/main/resources/data_information_domains.sql new file mode 100644 index 0000000000000000000000000000000000000000..b6872450e41082ff2885a868bcad8543a19d294d --- /dev/null +++ b/src/main/resources/data_information_domains.sql @@ -0,0 +1,728 @@ +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (1, 'Domain 1: work - Introduction', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Domain 1: work - Introduction', '**Domain 1: work** +------------------ + +Being in employment (particularly in job roles which match migrants\' skills and qualifications) can be a critical factor in promoting integration and independence. Employment provides social status, social connections and a sense of purpose. Supporting entrepreneurship and self-employment opportunities can also provide additional avenues for economic integration. + +Work provides opportunities for migrants and members of the host community to meet each other, as well as informally learn the language and workplace culture and customs in the UK. Work also helps individuals to build confidence, social connections, and financial well-being. Gaining employment is usually the number one priority for many migrants coming to Wales. + +### **Key Indicators of Integration** + +Although there are many ways to measure the integration of migrants in the context of work, we have selected a few key indicators which we will use for the purposes of this Framework.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (1, 'Indicator 1: percentage employed', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 1: percentage employed at a level appropriate to skills, qualifications, and experience', '#### **Indicator 1: percentage employed at a level appropriate to skills, qualifications, and experience** + +1. Does data exist for UK-born individuals in Wales? Yes: **[Labour market statistics by UK country and country of birth, year ending September 2023](https://www.gov.wales/ad-hoc-statistical-requests-26-february-8-march-2024)**. +2. Does data exist for migrants in Wales? Yes: **[Labour market statistics by UK country and country of birth, year ending September 2023](https://www.gov.wales/ad-hoc-statistical-requests-26-february-8-march-2024)**. + +Question asked: + +Which of the following statements describes your skills in your own work: + +1. I lack some skills required in my current duties +2. My present skills correspond well with my duties +3. I have the skills to cope with more demanding duties.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (1, 'Indicator 2: percentage of people in employment', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 2: percentage of people in employment, who are on permanent contracts (or on temporary contracts, and not seeking permanent employment) and who earn at least the real Living Wage', '#### **Indicator 2: percentage of people in employment, who are on permanent contracts (or on temporary contracts, and not seeking permanent employment) and who earn at least the real Living Wage** + +(Links to **[Wellbeing of Wales National Indicator 16](https://www.gov.wales/wellbeing-wales-national-indicators)**). + +1. Does data exist for UK-born individuals in Wales? Yes: **[Labour market statistics by UK country and country of birth, year ending September 2023](https://www.gov.wales/ad-hoc-statistical-requests-26-february-8-march-2024)**. +2. Does data exist for migrants in Wales? Yes: **[Labour market statistics by UK country and country of birth, year ending September 2023](https://www.gov.wales/ad-hoc-statistical-requests-26-february-8-march-2024)**. + +Questions asked: + +Are you working as an employee, self-employed or not working? + +1. Employee +2. Self-employed +3. Government scheme +4. Unpaid family worker +5. Not working + +In your main job are you working: + +1. Full time +2. Part time + +Some people have special working hours arrangements that vary from the usual full-time pattern. In your (main) job is your agreed working pattern any of the following: + +1. Flexitime (flexible working hours) +2. An annualised hours contract +3. Term-time working +4. Job sharing +5. Condensed/compressed hours +6. Zero hours contract +7. On-call working +8. None of these'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (1, 'Indicator 3: percentage of people living in households in income poverty', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 3: percentage of people living in households in income poverty relative to the UK median: measured for children, working age and those of pension age', '#### **Indicator 3: percentage of people living in households in income poverty relative to the UK median: measured for children, working age and those of pension age** + +(Links to **[Wellbeing of Wales National Indicator 18](https://www.gov.wales/wellbeing-wales-national-indicators)**). + +1. Does data exist for UK-born individuals in Wales? Yes: **[Population in relative income poverty, by the head of household\'s location of birth and devolved nation, FYE 2020 to FYE 2023](https://www.gov.wales/ad-hoc-statistical-requests-3-14-june-2024)**. +2. Does data exist for migrants in Wales? Yes: **[Population in relative income poverty, by the head of household\'s location of birth and devolved nation, FYE 2020 to FYE 2023](https://www.gov.wales/ad-hoc-statistical-requests-3-14-june-2024)**.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (1, 'Indicator 4: percentage reporting satisfaction with current employment', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 4: percentage reporting satisfaction with current employment', '#### **Indicator 4: percentage reporting satisfaction with current employment** + +1. Does data exist for UK-born individuals in Wales? Yes: **[National Survey for Wales](https://www.gov.wales/national-survey-wales-results-viewer)**. +2. Does data exist for migrants in Wales? No + +Question asked: + +On a scale of nought to 10, where nought is ‘not at all’ and 10 is ‘completely’, overall, how satisfied are you with your present job? (Asked of people in work).'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (1, 'Indicator 5: percentage reporting financial insecurity', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 5: percentage reporting financial insecurity', '#### **Indicator 5: percentage reporting financial insecurity** + +1. Does data exist for UK-born individuals in Wales? Yes: **[National Survey for Wales](https://www.gov.wales/national-survey-wales-results-viewer)**. +2. Does data exist for migrants in Wales? No. + +Question asked: + +Which one of these statements best describes how well you are keeping up with your bills and credit commitments at the moment: + +1. Keeping up with all bills and credit commitments without any difficulties +2. Keeping up with all bills and credit commitments but it is a struggle from time to time +3. Keeping up with all bills and credit commitments but it is a constant struggle +4. Falling behind with some bills or credit commitments +5. Having real financial problems and have fallen behind with many bills or credit commitments +6. Have no bills + +If you or your organisation are working with migrants in Wales and could ask some of the questions above, we would like to discuss this with you. Please contact us via **[migrationpolicy@gov.wales](mailto:migrationpolicy@gov.wales)** for us to arrange a conversation. + +Where existing data collections do not currently collect migrant data for Wales, dedicated surveys can help us to better understand outcomes and inequalities. This will help us to make reforms where needed and possible.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (1, 'Approaches', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Approaches', '### **Approaches** + +We know that certain groups of people face greater disadvantage in the labour market, and migrants are among those groups. We also know that there is a business case for diversity. The following approaches should guide actions to promote the integration of migrants into employment: + +- Addressing disadvantage in the labour market. +- Addressing barriers to inclusion, such as a lack of access to English for Speakers of Other Languages (ESOL) classes. +- Recognising the particular challenges faced by certain groups, such as refugees and women, in accessing work opportunities. +- Supporting the recognition of overseas qualifications and experience, and skills matching. +- Supporting entrepreneurship, such as access to micro-finance and legal advice, as a way of fostering economic activity. +- Encouraging inclusive practices within the workplace, such as unconscious bias training, workforce mentoring schemes, and employee networks. + +These approaches will help in promoting economic inclusion for migrants in Wales.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (2, 'Domain 2: housing - Introduction', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Domain 2: housing - Introduction', '**Domain 2: housing** +--------------------- + +The availability of adequate, affordable and stable housing is an essential component of migrant integration, as it can provide individuals with stability, security, and a sense of belonging and well-being. Providing access to good quality housing which meets the needs of individuals and families can therefore be an important factor in promoting integration. + +While considering housing needs it is important to take into account not only the quality, size, affordability, and suitability of available housing, but also the associated social and cultural aspects. Those who migrate to Wales may find that renting a house or room is difficult as they may not have credit history, references, a guarantor, or money to pay a bond. They may also find it difficult to understand the difference between the various housing options available to them. Precarious financial situations and a lack of social connections may also contribute towards less stable housing arrangements, requiring frequent moves which make individuals feel less secure. + + + +### **Key Indicators of Integration** + +For the purposes of this Framework, we have identified several key indicators which can be used to measure the integration of migrants in relation to housing.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (2, 'Indicator 1: percentage living in overcrowded housing', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 1: percentage living in overcrowded housing', '#### **Indicator 1: percentage living in overcrowded housing** + +1. Does data exist for UK-born individuals in Wales? Yes: **[2021 Census](https://www.ons.gov.uk/datasets/create)**. +2. Does data exist for migrants in Wales? Yes: **[2021 Census](https://www.ons.gov.uk/datasets/create)**. + +Questions asked: + +How many people usually live in your household? + +How many rooms are available for use only by this household?'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (2, 'Indicator 2: percentage living in owner-occupier/secure or assured tenancy conditions', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 2: percentage living in owner-occupier/secure or assured tenancy conditions', '#### **Indicator 2: percentage living in owner-occupier/secure or assured tenancy conditions** + +1. Does data exist for UK-born individuals in Wales? Yes: **[2021 Census](https://www.ons.gov.uk/datasets/create)**. +2. Does data exist for migrants in Wales? Yes: **[2021 Census](https://www.ons.gov.uk/datasets/create)**. + +Questions asked: + +Does your household own or rent this accommodation? + +1. Owns outright +2. Owns with a mortgage or loan +3. Part-owns and part-rents (shared ownership) +4. Rents (with or without housing benefit) +5. Lives here rent free + +If not homeowner: + +Who is your landlord? + +1. Housing association, housing co-operative, charitable trust, registered social landlord +2. Council or local authority +3. Private landlord or lettings agency +4. Employer of a household member +5. Relative or friend of a household member +6. Other'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (2, 'Indicator 3: percentage homeless (number of households successfully prevented from becoming homeless per 10,000 households)', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 3: percentage homeless (number of households successfully prevented from becoming homeless per 10,000 households)', '#### **Indicator 3: percentage homeless (number of households successfully prevented from becoming homeless per 10,000 households)** + +(Links to **[Wellbeing of Wales National Indicator 34](https://www.gov.wales/wellbeing-wales-national-indicators)**) + +1. Does data exist for UK-born individuals in Wales? Yes: **[Homelessness](https://www.gov.wales/homelessness-statistics)**. +2. Does data exist for migrants in Wales? No. + +Questions asked: + +Are you behind on rent, has your landlord given you an eviction notice or have you been threatened with homelessness in any other way? + +1. Yes +2. No + +If yes: + +Have you contacted your local authority for help and was this successful? + +1. Yes +2. No'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (2, 'Indicator 4: percentage reporting being very or fairly satisfied with their accommodation', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 4: percentage reporting being very or fairly satisfied with their accommodation', '#### **Indicator 4: percentage reporting being very or fairly satisfied with their accommodation** + +1. Does data exist for UK-born individuals in Wales? Yes: **[National Survey for Wales](https://www.gov.wales/national-survey-wales-results-viewer)**. +2. Does data exist for migrants in Wales? No. + +Question asked: + +How satisfied are you with this accommodation? + +1. Very satisfied +2. Fairly satisfied +3. Neither satisfied nor dissatisfied +4. Fairly dissatisfied +5. Very dissatisfied + +Is your home kept in a good state of repair? + +1. Yes +2. No + +If you or your organisation are working with migrants in Wales and could ask some of the questions above, we would like to discuss this with you. Please contact us via **[migrationpolicy@gov.wales](mailto:migrationpolicy@gov.wales)** for us to arrange a conversation.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (2, 'Approaches', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Approaches', '### **Approaches** + +We know that certain approaches can improve the integration of migrants with regards to housing. We encourage relevant organisations to embed the approaches below to support better outcomes. + +1. Engagement of migrant and refugee community organisations with local authorities and Housing Organisations to promote change to make their services more migrant friendly. +2. Developing the information and training for the organisation supporting migrants so the information is available in their national language. + +> It has been highlighted that with support and stable accommodation many EEA Nationals could return to or gain employment and manage a private rented tenancy, thus giving the opportunity for a lasting exit from homelessness. Ty Cyfle aims to provide secure, safe and good quality accommodation to EEA Nationals rough sleeping or accessing emergency homeless accommodation who are unable to access public funds, as well as intensive support to remove any barriers to employment and long-term housing. + +Read more about this successful approach in our **[accompanying case studies document](https://gov.wales/migrant-integration-framework-case-studies)**.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (3, 'Domain 3: health and social care - Introduction', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Domain 3: health and social care - Introduction', '**Domain 3: health and social care** +------------------------------------ + +The availability of high quality and timely health and social care services is essential for promoting the health and well-being of individuals and communities and is therefore an important factor in enabling the integration of migrants. Ensuring migrants have access to these services and that they are tailored to meet migrants’ diverse needs can help to promote their well-being and inclusion. + +Migrants will have different cultural backgrounds and experiences of health and social care, both from their country of origin and in the UK. It is important to make sure migrants are given an opportunity to learn and understand what help is available in a culturally competent way and support with how to access it. Consequently, it is important that service providers adapt by ensuring culturally competent approaches are mainstreamed in their practice. + +Some migrants may travel back to their country of origin (if permitted and able) to undergo medical treatments or procedures, partly due to language barriers and partly due to a lack of understanding or satisfaction with the UK healthcare system. For the same reasons, migrants may also access private unregulated medical services, including online pharmacies, which could lead to negative health impacts. + +Some migrants are particularly vulnerable to mental ill-health as a result of their previous experiences. They may have experienced significant trauma causing their displacement, trauma on journeys to the UK, or trauma whilst residing in isolation within the UK. Those who have experienced gender-based violence will also have particular need for sensitive and inclusive healthcare services. + +### **Key Indicators of Integration** + +There are many ways to measure the integration of migrants in health and social care. We have selected a few key indicators which we will use for the purposes of this Framework.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (3, 'Indicator 1: percentage expressing good self-rated health and well-being', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 1: percentage expressing good self-rated health and well-being', '#### **Indicator 1: percentage expressing good self-rated health and well-being** + +1. Does data exist for UK-born individuals in Wales? Yes: **[2021 Census](https://www.ons.gov.uk/datasets/create)**. +2. Does data exist for migrants in Wales? Yes: **[2021 Census](https://www.ons.gov.uk/datasets/create)**. + +Questions asked: + +How is your health in general? + +1. Very good +2. Good +3. Fair +4. Bad +5. Very bad'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (3, 'Indicator 2: percentage registered with a GP', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 2: percentage registered with a GP', '#### **Indicator 2: percentage registered with a GP** + +1. Does data exist for UK-born individuals in Wales? Yes: **[Welsh Demographic Service Dataset](https://saildatabank.com/data/apply-to-work-with-the-data/)**. Data available in SAIL (need to request access). +2. Does data exist for migrants in Wales? No + +Questions asked: + +Are you registered with a GP in Wales? + +1. Yes +2. No +3. Don’t know'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (3, 'Indicator 3: percentage of adults with two or more healthy lifestyle behaviours', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 3: percentage of adults with two or more healthy lifestyle behaviours', '#### **Indicator 3: percentage of adults with two or more healthy lifestyle behaviours** + +(links to **[Wellbeing of Wales National Indicator 3](https://www.gov.wales/wellbeing-wales-national-indicators)**). + +1. Does data exist for UK-born individuals in Wales? Yes: **[National Survey for Wales](https://www.gov.wales/national-survey-wales-results-viewer)**. +2. Does data exist for migrants in Wales? No. + +Questions asked: + +Number of healthy lifestyle behaviours: + +1. not smoking +2. healthy weight +3. eat 5 fruit or veg +4. not drinking above guidelines +5. active'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (3, 'Indicator 4: mean mental well-being score for people', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 4: mean mental well-being score for people', '#### **Indicator 4: mean mental well-being score for people** + +(Links to **[Wellbeing of Wales National Indicator 29](https://www.gov.wales/wellbeing-wales-national-indicators)**). + +1. Does data exist for UK-born individuals in Wales? Yes: **[National Survey for Wales: results viewer](https://www.gov.wales/national-survey-wales-results-viewer)**. +2. Does data exist for migrants in Wales? No. + +Questions asked: + +You will see some statements about wellbeing. For each one, please pick the answer that best describes your experience over the last 2 weeks from: + +1. None of the time +2. Rarely +3. Some of the time +4. Often +5. All of the time +6. Don’t know +7. Prefer not to say + +The statements are: + +1. I’ve been feeling optimistic about the future +2. I’ve been feeling useful +3. I’ve been feeling relaxed +4. I’ve been feeling interested in other people +5. I’ve had energy to spare +6. I’ve been dealing with problems well +7. I’ve been thinking clearly +8. I’ve been feeling good about myself +9. I’ve been feeling close to other people +10. I’ve been feeling confident +11. I’ve been able to make up my own mind about things +12. I’ve been feeling loved +13. I’ve been interested in new things +14. I’ve been feeling cheerful'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (3, 'Indicator 5: percentage service users who say social care services have made them feel safe and secure', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 5: percentage service users who say social care services have made them feel safe and secure', '#### **Indicator 5: percentage service users who say social care services have made them feel safe and secure** + +1. Does data exist for UK-born individuals in Wales? Yes: **[National Survey for Wales](https://www.gov.wales/national-survey-wales-results-viewer)**. +2. Does data exist for migrants in Wales? No. + +Questions asked: + +Consider asking social care users/carers: + +To what extent do you agree or disagree...I feel safe: + +1. Strongly agree +2. Agree +3. Neither agree nor disagree +4. Disagree +5. Strongly disagree + +If you or your organisation are working with migrants in Wales and could ask some of the questions above, we would like to discuss this with you. Please contact us via **[migrationpolicy@gov.wales](mailto:migrationpolicy@gov.wales)** for us to arrange a conversation.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (3, 'Approaches', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Approaches', '### **Approaches** + +1. Preparation of toolkits with the community and for the community in community languages. +2. Specialist service provision available where high concentration of local need (example TB screening where prevalence is much higher in country of origin). +3. Support to access health and social care services (e.g., availability of appropriate interpreting services +4. Availability of accessible local health promotion, antenatal/postnatal and disability support initiatives +5. Using communication channels that are used by targeted community to improve health literacy +6. Use creative approaches (film, visual arts etc.) to bear witness to the disorientation and loss which cannot be expressed easily in language. This helps to develop migrant self-representation and will benefit themselves and wider understanding in the community. + +> The **[Trauma-Informed Wales Framework](https://traumaframeworkcymru.com/)** sets out the approach to developing and implementing trauma-informed practice across Wales, providing the best possible support to those who need it most. The Framework establishes how individuals, families/other support networks, communities, organisations and systems take account of adversity and trauma, recognising and supporting the strengths of an individual to overcome this experience in their lives. It also sets out the support they can expect to receive from the organisations, sectors and systems that they may turn to for help. It is inclusive of people of all ages, from babies, children and young people right through to older adults. + +Read more about this successful approach in our **[accompanying case studies document](https://gov.wales/migrant-integration-framework-case-studies)**.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (4, 'Domain 4: social connections (bonds, bridges and links) - Introduction', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Domain 4: social connections (bonds, bridges and links) - Introduction', '**Domain 4: social connections (bonds, bridges and links)** +----------------------------------------------------------- + +Building social connections and fostering a sense of belonging is an important component of integration. Ensuring migrants and their new neighbours can meet and share ideas will promote social cohesion and reduce social isolation. + +This domain involves three different types of social connection. + +Firstly, it is about connections that allow migrants to feel like they belong. This involves the family and others who migrants feel are ‘like them’, such as compatriots from their country of origin or those with a similar migration status. We call the connection between these individuals social bonds. Social bonds can also include those who can authentically represent the ‘voice’ of migrant communities and may advocate on their behalf or provide advice to those in similar situations. + +Secondly, social connections can be bridges. Social bridges relate to connections between people who are considered to be from different social groups. For example, a migrant may form a social bridge with a new neighbour or work colleague born in Wales. These connections can be built through mixing within communities, including via schools, workplaces, social clubs, religious settings, sport, or political activities. Cultural events are particularly powerful opportunities to build social bridges, particularly where opportunities for two-way sharing exist. Volunteering opportunities also help to build connections. + +Social links refer to connections that are made between individuals and service providers, like police, NHS or local government. For example, links may be made via migrant community outreach services or more inclusive practices in mainstream approaches. + +All three types of social connection are important to ensure effective integration of migrants within host communities. Social bonds, bridges, and links help to build understanding of the new society among migrants and create support networks and opportunities allowing them to thrive. They also build greater awareness of the skills and culture brought to Wales by new migrants, which can be harnessed to support the community overall. Integration is a two-way process, with both the host community and new migrants benefitting greatly from these opportunities. Wales, as a whole, benefits from these connections being made. + +### **Key Indicators of Integration** + +Although there are many ways to measure the integration of migrants in terms of social connections, we have selected a few key indicators which we will use for the purposes of this Framework.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (4, 'Indicator 1: percentage reporting sense of ‘belonging’ to neighbourhood and local area', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 1: percentage reporting sense of ‘belonging’ to neighbourhood and local area', '#### **Indicator 1: percentage reporting sense of ‘belonging’ to neighbourhood and local area** + +(Links to **[Wellbeing of Wales National Indicator 27](https://www.gov.wales/wellbeing-wales-national-indicators)**). + +1. Does data exist for UK-born individuals in Wales? Yes: **[National Survey for Wales](https://www.gov.wales/national-survey-wales-results-viewer)**. +2. Does data exist for migrants in Wales? No. + +Questions asked: + +To what extent would you agree or disagree that you belong to your local area? + +1. Strongly agree +2. Tend to agree +3. Neither agree nor disagree +4. Tend to disagree +5. Strongly disagree'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (4, 'Indicator 2: percentage reporting that people of different backgrounds get on well in their area', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 2: percentage reporting that people of different backgrounds get on well in their area', '#### **Indicator 2: percentage reporting that people of different backgrounds get on well in their area** + +(Links to **[Wellbeing of Wales National Indicator 27](https://www.gov.wales/wellbeing-wales-national-indicators)**). + +1. Does data exist for UK-born individuals in Wales? Yes: **[National Survey for Wales](https://www.gov.wales/national-survey-wales-results-viewer)**. +2. Does data exist for migrants in Wales? No. + +Questions asked: + +To what extent do you agree or disagree that this local area is a place where people from different backgrounds get on well together? + +1. Strongly agree +2. Tend to agree +3. Neither agree nor disagree +4. Tend to disagree +5. Strongly disagree'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (4, 'Indicator 3: percentage of people attending or participating in arts, culture or heritage activities at least 3 times a year', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 3: percentage of people attending or participating in arts, culture or heritage activities at least 3 times a year', '#### **Indicator 3: percentage of people attending or participating in arts, culture or heritage activities at least 3 times a year** + +(Links to **[Wellbeing of Wales National Indicator 35](https://www.gov.wales/wellbeing-wales-national-indicators)**). + +1. Does data exist for UK-born individuals in Wales Yes: **[National Survey for Wales](https://www.gov.wales/national-survey-wales-results-viewer)**. +2. Does data exist for migrants in Wales? No. + +Questions asked: + +People attending or participating in arts, culture or heritage activities at least 3 times a year + +1. Yes +2. No + +What do i want? + +1. Success +2. Intelligence'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (4, 'Indicator 4: percentage of people who volunteer', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 4: percentage of people who volunteer', '#### **Indicator 4: percentage of people who volunteer** + +(Links to **[Wellbeing of Wales National Indicator 28](https://www.gov.wales/wellbeing-wales-national-indicators)**) + +1. Does data exist for UK-born individuals in Wales? Yes: **[National Survey for Wales](https://www.gov.wales/national-survey-wales-results-viewer)**. +2. Does data exist for migrants in Wales? No. + +Questions asked: + +Which of these clubs or organisations, if any, are you currently giving your time to for free: + +1. Charitable organisation +2. School or young persons’ group +3. Tenants/residents group or neighbourhood watch +4. Religious group +5. Pensioners group/organisation +6. Sports club +7. Arts group (e.g. drama, music, art or crafts) +8. Environmental group +9. Museum/heritage site +10. Other club or organisation +11. None of these'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (4, 'Indicator 5: percentage reporting having friends with different backgrounds', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 5: percentage reporting having friends with different backgrounds', '#### **Indicator 5: percentage reporting having friends with different backgrounds** + +1. Does data exist for UK-born individuals in Wales? No. +2. Does data exist for migrants in Wales? No. + +Questions asked: + +Do you have friends who have a different nationality to you? + +1. All the same as me +2. More than a half +3. About a half +4. Less than a half + +If you or your organisation are working with migrants in Wales and could ask some of the questions above, we would like to discuss this with you. Please contact us via **[migrationpolicy@gov.wales](mailto:migrationpolicy@gov.wales)** for us to arrange a conversation.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (4, 'Approaches', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Approaches', '### **Approaches** + +We know that certain approaches can improve the integration of migrants in terms of social connections. We encourage relevant organisations to embed the approaches below to support better outcomes. + +1. Providing support and mentoring to community organisations and leaders +2. Training and outreach programmes to encourage and support involvement in public and civic life for migrants. +3. Accessible funding for cultural activities +4. Provision of activities aimed at encouraging participation of diverse groups. + +> The mainly voluntary team at Oasis organise and deliver a wide variety of services to promote integration, ranging from food clubs to trips, sports events, gardening and language tuition (ESOL). This informal provision provides crucial linguistic, psychological, and emotional scaffolding for the newly arrived sanctuary seekers, enabling them to begin language learning, form friendships and access support as soon as they arrive. + +Read more about this successful approach in our **[accompanying case studies document](https://gov.wales/migrant-integration-framework-case-studies)**.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (5, 'Domain 5: education and skills - Introduction', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Domain 5: education and skills (including language, communication and digital) - Introduction', '**Domain 5: education and skills (including language, communication and digital)** +---------------------------------------------------------------------------------- + +Education and training can provide individuals with the skills and knowledge they need to succeed in the labour market and participate fully in society. Access to quality and timely education and training opportunities (or opportunities to certify evidence of skills brought to the UK) can therefore be an important factor in promoting integration. Language proficiency, specifically, is vital in advancing social cohesion and a sense of belonging. The ability to speak, read, and write in English or Welsh is essential for accessing education, employment, and other services. It can also help migrants to communicate effectively with their communities and build social connections. Therefore, measuring the English language proficiency of migrants in Wales could provide insights into their integration and social inclusion. + +English/Welsh for Speakers of Other Languages (E/WSOL) courses (and informal English/Welsh language opportunities) are especially crucial at the beginning of a migrant’s integration with their host community. However, there is growing evidence that migrant communities would additionally benefit from higher level provisions, to make sure their language abilities enable the use of the other skills individuals bring to the Welsh economy. All migrants bring with them language skills and these can also be of use to Welsh communities and the Welsh economy. The ability to speak English or Welsh may not be the most important element of an individual’s ability to work productively. Foreign language skills can support international trade, programming languages can be near-universal, and developing multilingualism can increase the capacity for creative thinking, to provide just a few examples of this. + +Digital skills are increasingly vital to be able to engage in society, whether it is for employment, socialising, managing, and monitoring health conditions, education, and further learning. Digital inclusion, a key social justice and equalities issue, the internet and wider digital technology can be an enabler but for non-users or limited users of digital technology they are at risk of missing these benefits and are potentially unable to take advantage of the improvements to public services as more of these undergo digital transformation. + +Access to quality and timely education and training can aid migrant integration in a number of ways, some of which are not confined solely to learning. By attending training, migrants will be mixing with the host population and creating social connections. It can also help individuals to understand UK systems and processes. + +A lack of information about the education system and what is expected of children and parents, can be a huge barrier to child development. This can lead to poor choices about which subjects to study which subsequently leads to more limited employment and education options. + +### **Key Indicators of Integration** + +For the purposes of this Framework we have identified several key indicators that can be used to measure integration in the context of education.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (5, 'Indicator 1: percentage people who do not have English/Welsh as a first language reporting ability to hold simple conversation with local language speaker (e.g., a neighbour)', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 1: percentage people who do not have English/Welsh as a first language reporting ability to hold simple conversation with local language speaker (e.g., a neighbour)', '#### **Indicator 1: percentage people who do not have English/Welsh as a first language reporting ability to hold simple conversation with local language speaker (e.g., a neighbour)** + +1. Does data exist for UK-born individuals in Wales? Yes: **[2021 Census](https://www.ons.gov.uk/datasets/create)**. +2. Does data exist for migrants in Wales? Yes: **[2021 Census](https://www.ons.gov.uk/datasets/create)**. + +Questions asked: + +What is your main language? + +1. English or Welsh +2. Other, write in (including British Sign Language) + +How well can you speak English? + +1. Very well +2. Well +3. Not well +4. Not at all + +Can you understand, speak, read or write Welsh? + +1. Understand spoken Welsh +2. Speak Welsh +3. Read Welsh +4. Write Welsh +5. None of the above'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (5, 'Indicator 2: percentage achieving 5 or more GCSEs at A* to C grade', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 2: percentage achieving 5 or more GCSEs at A* to C grade', '#### **Indicator 2: percentage achieving 5 or more GCSEs at A\\* to C grade** + +(Links to **[Wellbeing of Wales National Indicator 7](https://www.gov.wales/wellbeing-wales-national-indicators)**). + +1. Does data exist for UK-born individuals in Wales? Yes: **[2021 Census](https://www.ons.gov.uk/datasets/create)**. +2. Does data exist for migrants in Wales? Yes **[2021 Census](https://www.ons.gov.uk/datasets/create)**. + +Questions asked: + +Have you achieved any other qualifications? + +GCSEs or equivalent: + +1. 5 or more GCSEs (A\\* to C, 9 to 4), O levels (passes), CSEs (grade 1) or Intermediate Welsh Baccalaureate +2. Any other GCSEs, O levels or CSEs (any grades), Basic Skills course or Foundation Welsh Baccalaureate'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (5, 'Indicator 3: percentage which ‘personally use the internet’?', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 3: percentage which ‘personally use the internet’?', '#### **Indicator 3: percentage which ‘personally use the internet’?** + +(Links to **[Wellbeing of Wales National Indicator 50](https://www.gov.wales/wellbeing-wales-national-indicators)**). + +1. Data available for UK-born: Yes: **[National Survey for Wales](https://www.gov.wales/national-survey-wales-results-viewer)**. +2. Does data exist for migrants in Wales? No. + +Questions asked: + +Do you personally use the internet at home, work or elsewhere (including smart tv and handheld devices) + +1. Yes (on my own) +2. Yes (with help) +3. No +4. Don’t know'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (5, 'Indicator 4: percentage young people and adults achieving admission to tertiary education', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 4: percentage young people and adults achieving admission to tertiary education', '#### **Indicator 4: percentage young people and adults achieving admission to tertiary education** + +1. Does data exist for UK-born individuals in Wales? Yes: **[Lifelong Learning Wales Record; Higher education student record, Higher Education Statistics Agency](https://statswales.gov.wales/Catalogue/Education-and-Skills/Post-16-Education-and-Training)**. +2. Does data exist for migrants in Wales? Yes: both the Lifelong Learning Wales Record and the Higher education student record, Higher Education Statistics Agency contain data on nationality, plus data is available for Wales, can be requested from **[Post16ed.stats@gov.wales](mailto:Post16ed.stats@gov.wales)**.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (5, 'Indicator 5: what is the highest level of your education?', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 5: what is the highest level of your education?', '#### **Indicator 5: what is the highest level of your education?** + +(Links to **[Wellbeing of Wales National Indicator 8](https://www.gov.wales/wellbeing-wales-national-indicators)**). + +1. Does data exist for UK-born individuals in Wales? Yes: **[Levels of highest qualification held by working age adults (aged 18 to 64), by country of residence and country of birth, 2022](https://www.gov.wales/ad-hoc-statistical-requests-1-12-january-2024)**. +2. Does data exist for migrants in Wales? Yes: **[Levels of highest qualification held by working age adults (aged 18 to 64), by country of residence and country of birth, 2022](https://www.gov.wales/ad-hoc-statistical-requests-1-12-january-2024)**. + +Questions asked: + +Was your highest qualification gained in the UK, or outside of the UK? + +1. In the UK +2. Outside the UK +3. Don’t know + +What type of qualification is it? + +1. Postgraduate degree +2. Undergraduate degree +3. Higher qualification below degree level +4. A-level/Vocational A-level or equivalent +5. AS-level/Vocational AS-level or equivalent +6. International Baccalaureate +7. O-levels or equivalent +8. GCSE/Vocational GCSE or equivalent +9. Other work-related or professional qualification +10. School Leavers Certificate +11. Don\'t know + +If you or your organisation are working with migrants in Wales and could ask some of the questions above, we would like to discuss this with you. Please contact us via **[migrationpolicy@gov.wales](mailto:migrationpolicy@gov.wales)** for us to arrange a conversation.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (5, 'Approaches', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Approaches', '### **Approaches** + +We know that certain approaches can improve the integration of migrants in terms of education. We encourage relevant organisations to embed the approaches below to support better outcomes. + +1. Provide language tuition which most closely meets the attainment level and aspirations of the learner, rather than on the basis of home language, nationality or immigration status. This means learners of many different nationalities learn alongside each other but at a similar learning level. +2. Provide opportunities for the Welsh language to be taught to migrants, in particular in Welsh-speaking heartlands. Such an approach can evidence the positive effect which migration can have in safeguarding Welsh cultural identity and provides alternative integration opportunities. +3. Bursary schemes can support access to tertiary education for migrants with socio-economic disadvantages. +4. Seek to develop multi-agency approaches to support integration. Consider the role that family, community and other partners can take to develop a whole-system approach. Each element is connected and can contribute towards a more holistic and sustainable approach. + +> As part of its mission to welcome people from all backgrounds to learn and enjoy Welsh, the National Centre for Learning Welsh has a ‘Croeso i Bawb’ project to teach the Welsh language to people who do not speak English as a first language, including refugees and asylum speakers. + +Read more about this successful approach in our **[accompanying case studies document.](https://gov.wales/migrant-integration-framework-case-studies)**'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (6, 'Domain 6: safety and stability', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Domain 6: safety and stability', '**Domain 6: safety and stability** +---------------------------------- + +Helping individuals feel safe can support more effective integration with local communities. Discrimination and prejudice can create significant barriers to integration by limiting access to employment, housing, and services. They also foster social isolation and exclusion. A lack of purpose, insecure immigration status or unstable life circumstances can also undermine integration. + +A key feature of this domain is how people feel. Being safe and feeling safe are not always the same thing. A house can be safe with locks and an alarm but it may not feel safe because the neighbourhood contains people who are prejudiced against migrants. + +Stability can be viewed as individuals feeling comfortable that they can control things that happen in their lives. There may be continuity of services. People may have created a support network and financial or mental resilience. + +While social connections are an important part of the development of safety and stability, this is much more about personal well-being and confidence. + +Experiences of hate crime or discrimination arising from an individual’s national background or intersectional characteristics (including sexual orientation, sex, disability, gender identity or religion) can be particularly impactful as individuals have been targeted because of something which is intrinsic to their identity. Experiences of trafficking, abuse, and crime, can also undermine the feeling of safety. Poverty and insecure immigration status can undermine individuals’ ability to feel their life is on a stable footing. + +Without achieving a feeling of safety and security, individuals are unlikely to fully contribute to local communities and achieve their full potential. Communities must be welcoming and inclusive to ensure the benefits of migration are shared by all. + +### **Key Indicators of Integration** + +With regards to the integration of migrants in terms of safety and security, we have selected a few key indicators which we will use for the purposes of this Framework.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (6, 'Indicator 1: percentage reporting confidence that the Criminal Justice System is fair', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 1: percentage reporting confidence that the Criminal Justice System is fair', '#### **Indicator 1: percentage reporting confidence that the Criminal Justice System is fair** + +(Links to **[Wellbeing of Wales National Indicator 47](https://www.gov.wales/wellbeing-wales-national-indicators)**). + +1. Does data exist for UK-born individuals in Wales? Yes: **[Crime survey for England and Wales](https://beta.ukdataservice.ac.uk/datacatalogue/series/series?id=200009)**. +2. Does data exist for migrants in Wales? Yes: **[Crime survey for England and Wales](https://beta.ukdataservice.ac.uk/datacatalogue/series/series?id=200009)**. + +Questions asked: + +How confident are you that the Criminal Justice System as a whole is fair? + +1. Very confident +2. Fairly confident +3. Not very confident +4. Not at all confident +5. Don’t know'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (6, 'Indicator 2: percentage reporting feeling safe in local community', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 2: percentage reporting feeling safe in local community', '#### **Indicator 2: percentage reporting feeling safe in local community** + +(Links to **[Wellbeing of Wales National Indicator 25](https://www.gov.wales/wellbeing-wales-national-indicators)**). + +1. Does data exist for UK-born individuals in Wales? Yes: **[Crime survey for England and Wales](https://beta.ukdataservice.ac.uk/datacatalogue/series/series?id=200009)**. +2. Does data exist for migrants in Wales? Yes: **[Crime survey for England and Wales](https://beta.ukdataservice.ac.uk/datacatalogue/series/series?id=200009)**. + +Questions asked: + +How safe do/would you feel walking alone in this area after dark? By this area I mean within 15 minutes walk from here: + +1. Very safe +2. Fairly safe +3. A bit unsafe +4. Very unsafe + +Indicator 3: percentage reporting to be a target of a hate crime or incident. + +1. Does data exist for UK-born individuals in Wales? Yes: **[Crime survey for England and Wales](https://beta.ukdataservice.ac.uk/datacatalogue/series/series?id=200009)**. +2. Does data exist for migrants in Wales? Yes: **[Crime survey for England and Wales](https://beta.ukdataservice.ac.uk/datacatalogue/series/series?id=200009)**. + +Questions asked: + +Do you think the incident was motivated by the offender’s attitude towards any of these factors? + +1. Your race +2. Your religion or religious beliefs +3. Your sexuality or sexual orientation +4. Your age +5. Your sex +6. Any disability you have +7. Your gender identity (transgender) +8. Don’t Know +9. None of these + +Was there anything about the incident that made you think it might have been motivated by any of these factors? + +1. Your race +2. Your religion or religious beliefs +3. Your sexuality or sexual orientation +4. Your age +5. Your sex +6. Any disability you have +7. Your gender identity (transgender) +8. None of these'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (6, 'Indicator 4: percentage reporting satisfaction with local area', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 4: percentage reporting satisfaction with local area', '#### **Indicator 4: percentage reporting satisfaction with local area** + +(Links to **[Wellbeing of Wales National Indicator 26](https://www.gov.wales/wellbeing-wales-national-indicators)**). + +1. Does data exist for UK-born individuals in Wales? Yes: **[National Survey for Wales](https://www.gov.wales/national-survey-wales-results-viewer)**. +2. Does data exist for migrants in Wales?: No. + +Questions asked: + +Overall, how satisfied or dissatisfied are you with your local area as a place to live? + +1. Very satisfied +2. Fairly satisfied +3. Neither satisfied nor dissatisfied +4. Fairly dissatisfied +5. Very dissatisfied + +If you or your organisation are working with migrants in Wales and could ask some of the questions above, we would like to discuss this with you. Please contact us via **[migrationpolicy@gov.wales](mailto:migrationpolicy@gov.wales)** for us to arrange a conversation.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (6, 'Approaches', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Approaches', '### **Approaches** + +We know that certain approaches can improve the integration of migrants in terms of education. We encourage relevant organisations to embed the approaches below to support better outcomes. + +1. Undertake outreach with migrant support groups in the local area to ensure concerns are understood and awareness can be raised about local opportunities. Communities are rarely ‘hard-to-reach’ but are ‘seldom heard’. +2. Provide tailored support to those feeling targeted or discriminated against because of their national origins. +3. Help those forcibly displaced or those experiencing destitution through informal local support networks. Support can be financial or through goods but even just navigating local systems, providing cultural orientation and friendship can have very positive impacts. +4. Using creative approaches, develop new ways of challenging stereotypes of migrants and helping to identify motivations, improving social competencies and offering supportive ways of learning for migrants. + +> I think the concept of ‘cwtch’ applies here. I think a big part of the Welsh heritage is you cwtch people in, you nourish and support them… I didn’t realise it at the time, but they’ve been the light I needed. Having them here has really changed my life. I feel so grateful. They’ve done as much for me as I could do for them. I feel like my life has been enhanced, as does my partner and my children. + +Read more about this successful approach in our **[accompanying case studies document](https://gov.wales/migrant-integration-framework-case-studies)**.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (7, 'Domain 7: rights and responsibilities - Introduction', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Domain 7: rights and responsibilities - Introduction', '**Domain 7: rights and responsibilities** +----------------------------------------- + +In seeking to become a Nation of Sanctuary, as well as through implementing the Anti Racist Wales Action Plan, the Welsh Government is seeking to eliminate inequalities and support integration. For people to fully integrate with Welsh communities, they need to understand their rights and responsibilities. Host community members equally have responsibilities to follow the law, engage with democratic processes and participate in local communities. Individuals also need to be able to exercise their rights to ensure they have the safety net which is sometimes required. This is why advice services and awareness-raising activities can play a critical role in supporting integration. + +Awareness of rights and responsibilities will support new migrants to build social connections and awareness of Welsh systems more quickly. Rights and responsibilities establish a common framework for interactions between all individuals in a community. It is therefore important that new migrants are supported to understand these as soon as possible. + +### **Key Indicators of Integration** + +Although there are many ways to measure the integration of migrants in terms of rights and responsibilities, we have selected a few key indicators which we will use for the purposes of this Framework.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (7, 'Indicator 1: percent registering to vote', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 1: percent registering to vote', '#### **Indicator 1: percent registering to vote** + +1. Does data exist for UK-born individuals in Wales? Yes: **[Electoral statistics for the UK](https://www.ons.gov.uk/peoplepopulationandcommunity/elections/electoralregistration/datasets/electoralstatisticsforuk)**. +2. Does data exist for migrants in Wales? Yes: **[Overseas, anonymous, opted-out, EU citizens and Parliamentary electors by local authority](https://www.ons.gov.uk/peoplepopulationandcommunity/elections/electoralregistration/datasets/overseasanonymouselectorsoptedoutandparliamentarybylocalauthority)**.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (7, 'Indicator 2: percentage utilising advice services', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 2: percentage utilising advice services', '#### **Indicator 2: percentage utilising advice services** + +1. Does data exist for UK-born individuals in Wales? Yes: **[National Survey for Wales](https://www.gov.wales/national-survey-wales-results-viewer)**. +2. Does data exist for migrants in Wales? No. + +Questions asked: + +In the last 12 months, have you had advice or support from any organisations in these areas of life? + +1. Debt +2. Financial matters other than debt +3. Welfare benefits +4. Housing +5. Employment +6. Discrimination +7. Divorce or problems relating to relationship breakdown +8. Social care +9. Goods and services you have bought +10. None of these +11. Other'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (7, 'Indicator 3: percentage reporting knowledge of rights', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Indicator 3: percentage reporting knowledge of rights', '#### **Indicator 3: percentage reporting knowledge of rights** + +1. Does data exist for UK-born individuals in Wales? No. +2. Does data exist for migrants in Wales? No. + +Questions to ask: + +Which of the following best describes your knowledge of ...? + +1. Human Rights Act +2. Equality Act +3. Social Services and Well-being Act + +Response options for each: + +1. I know nothing at all +2. I know a little +3. I know a fair amount +4. I know a great deal + +If you or your organisation are working with migrants in Wales and could ask some of the questions above, we would like to discuss this with you. Please contact us via **[migrationpolicy@gov.wales](mailto:migrationpolicy@gov.wales)** for us to arrange a conversation.'); +INSERT INTO information(category_id, info_title, created_by, created_date, updated_date, tag_id, short_info, info_article) VALUES (7, 'Approaches', 'Nitish Marnuri', '2024-12-07', '2024-12-07', 1, 'Approaches', '### **Approaches** + +We know that certain approaches can improve the integration of migrants in terms of education. We encourage relevant organisations to embed the approaches below to support better outcomes. + +1. Provide information about living in Wales (or local areas) which is tailored to your migrant audience and communicate it via community support organisations and community communication channels (e.g. Telegram, Whatsapp, Facebook etc). +2. Ensure you monitor the uptake of advice services and receipt of information by migrant communities to ensure services are accessible. Take active steps to improve uptake where needed. +3. Actively consider how to involve migrant communities in registration drives and political participation initiatives. + +> Swansea produced a Step-by-Step Guide on how to register to vote in multiple languages which was housed on the local authority’s website. The guide was provided in 10 languages and helped removed a crucial barrier to accessing information. The approach proved successful, and the number of registered qualifying foreign nationals almost doubled from January to April 2022. + +Read more about this successful approach in our **[accompanying case studies document](https://gov.wales/migrant-integration-framework-case-studies)**.'); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 8b96cb72d9fe2e47e55f7c0c7cee604c2396ad8d..c5da366ffb793f6af4587d797c6ec79b54c92a46 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,5 +1,28 @@ + +-- drop tables at the start of a session +DROP TABLE IF EXISTS information; +DROP TABLE IF EXISTS categories; +DROP TABLE IF EXISTS event; +DROP TABLE IF EXISTS news; +DROP TABLE IF EXISTS post_likes; +DROP TABLE IF EXISTS feed_tags; +DROP TABLE IF EXISTS comment; +DROP TABLE IF EXISTS feed; +DROP TABLE IF EXISTS tags; +DROP TABLE IF EXISTS user_roles; +DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS roles; + +create table if not exists categories ( + category_id INT AUTO_INCREMENT PRIMARY KEY, + category_title VARCHAR(100) NOT NULL, + category_description VARCHAR(255) NOT NULL, + user_id INT NOT NULL +); + + -- create schema for information -drop table if exists information; + create table if not exists information ( @@ -10,12 +33,13 @@ create table if not exists information created_date date, updated_date date, tag_id int, - info_article text + short_info text, + info_article text, + foreign key category_foreign_key (category_id) references categories(category_id) ) engine = InnoDB; -- create schema for event -drop table if exists event; create table if not exists event( event_id int primary key auto_increment, @@ -32,7 +56,6 @@ create table if not exists event( -- schema for news -drop table if exists news; create table if not exists news ( @@ -44,4 +67,81 @@ create table if not exists news news_image_url varchar(255), user_id int, news_upload_date date -) engine = InnoDB; \ No newline at end of file +) engine = InnoDB; + + +-- schema for roles +CREATE TABLE IF NOT EXISTS roles ( + id INT AUTO_INCREMENT PRIMARY KEY, + role_name VARCHAR(50) NOT NULL UNIQUE +); + +-- schema for users +create table if not exists users ( + id INT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + fullname VARCHAR(255) NOT NULL, + dob DATE, + role_id INT NOT NULL, + organization varchar(255), + FOREIGN KEY (role_id) REFERENCES roles(id) +); + +-- schema for user roles +CREATE TABLE IF NOT EXISTS user_roles ( + user_id INT, + role_id INT, + PRIMARY KEY (user_id, role_id), + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (role_id) REFERENCES roles(id) + ); + + + +create table if not exists feed ( + post_id int primary key auto_increment, + post_image_url varchar(255), + post_title varchar(255), + post_description TEXT, + post_time date, + user_id int, + foreign key (user_id) references users(id) + ) engine = InnoDB; + + + +create table if not exists tags ( + tag_id int primary key auto_increment, + tag_name varchar(100) + ) engine = InnoDB; + +CREATE TABLE IF NOT EXISTS feed_tags ( + post_id INT, + tag_id INT, + FOREIGN KEY (post_id) REFERENCES feed(post_id), + FOREIGN KEY (tag_id) REFERENCES tags(tag_id), + PRIMARY KEY (post_id, tag_id) + ) ENGINE = InnoDB; + + +create table if not exists post_likes ( + like_id int primary key auto_increment, + post_id int, + user_id int, + created_at timestamp default current_timestamp, + foreign key (post_id) references feed(post_id), + foreign key (user_id) references users(id), + constraint unique_post_user unique (post_id, user_id) + ) engine = InnoDB; + +CREATE TABLE IF NOT EXISTS comment +( + id INT AUTO_INCREMENT PRIMARY KEY, + comment_content TEXT NOT NULL, + created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + user_id INT NOT NULL, + post_id INT NOT NULL, + FOREIGN KEY (post_id) REFERENCES feed(post_id), + FOREIGN KEY (user_id) REFERENCES users (id) +) ENGINE = InnoDB; diff --git a/src/main/resources/static/assets/navbarImages/about.png b/src/main/resources/static/assets/navbarImages/about.png new file mode 100644 index 0000000000000000000000000000000000000000..7a010a0457d7b8b634b8c5998683b7d3bc644d84 Binary files /dev/null and b/src/main/resources/static/assets/navbarImages/about.png differ diff --git a/src/main/resources/static/assets/navbarImages/contact.png b/src/main/resources/static/assets/navbarImages/contact.png new file mode 100644 index 0000000000000000000000000000000000000000..db217b810f9fa2e0358e57bae84a9ea8d2a0b68a Binary files /dev/null and b/src/main/resources/static/assets/navbarImages/contact.png differ diff --git a/src/main/resources/static/assets/navbarImages/events.png b/src/main/resources/static/assets/navbarImages/events.png new file mode 100644 index 0000000000000000000000000000000000000000..83670b03f3e41afac8016bd6a62d583a413cc293 Binary files /dev/null and b/src/main/resources/static/assets/navbarImages/events.png differ diff --git a/src/main/resources/static/assets/navbarImages/facebook.png b/src/main/resources/static/assets/navbarImages/facebook.png new file mode 100644 index 0000000000000000000000000000000000000000..fe54d3a35f61cc37b1f35d8b6c5fdb6a0d846ce0 Binary files /dev/null and b/src/main/resources/static/assets/navbarImages/facebook.png differ diff --git a/src/main/resources/static/assets/navbarImages/feed.png b/src/main/resources/static/assets/navbarImages/feed.png new file mode 100644 index 0000000000000000000000000000000000000000..329a6ff3f4adbf3a89dc16b590bd0bf44b6e49f0 Binary files /dev/null and b/src/main/resources/static/assets/navbarImages/feed.png differ diff --git a/src/main/resources/static/assets/navbarImages/globe.png b/src/main/resources/static/assets/navbarImages/globe.png new file mode 100644 index 0000000000000000000000000000000000000000..6c60ea6481bdbde40c99060128dc2490c6b436fa Binary files /dev/null and b/src/main/resources/static/assets/navbarImages/globe.png differ diff --git a/src/main/resources/static/assets/navbarImages/home.png b/src/main/resources/static/assets/navbarImages/home.png new file mode 100644 index 0000000000000000000000000000000000000000..3943c2ae3817821bf88f26995db352be6ce54268 Binary files /dev/null and b/src/main/resources/static/assets/navbarImages/home.png differ diff --git a/src/main/resources/static/assets/navbarImages/info.png b/src/main/resources/static/assets/navbarImages/info.png new file mode 100644 index 0000000000000000000000000000000000000000..a1f9c97d8f3d304e89cb69e3c84a9d818a130a4b Binary files /dev/null and b/src/main/resources/static/assets/navbarImages/info.png differ diff --git a/src/main/resources/static/assets/navbarImages/instagram.png b/src/main/resources/static/assets/navbarImages/instagram.png new file mode 100644 index 0000000000000000000000000000000000000000..89f9c52ffca9ed8a1c305e9325d6c764a87fb2ad Binary files /dev/null and b/src/main/resources/static/assets/navbarImages/instagram.png differ diff --git a/src/main/resources/static/assets/navbarImages/logo.png b/src/main/resources/static/assets/navbarImages/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8522032d6546282bfb74242e65387d88419bf4a0 Binary files /dev/null and b/src/main/resources/static/assets/navbarImages/logo.png differ diff --git a/src/main/resources/static/assets/navbarImages/news.png b/src/main/resources/static/assets/navbarImages/news.png new file mode 100644 index 0000000000000000000000000000000000000000..4cbf993cd5a5dabbe458150c480b7bf091945858 Binary files /dev/null and b/src/main/resources/static/assets/navbarImages/news.png differ diff --git a/src/main/resources/static/assets/navbarImages/profile.png b/src/main/resources/static/assets/navbarImages/profile.png new file mode 100644 index 0000000000000000000000000000000000000000..45685e99c3995101e7d8e81d4caadcb5017bd864 Binary files /dev/null and b/src/main/resources/static/assets/navbarImages/profile.png differ diff --git a/src/main/resources/static/assets/navbarImages/twitter.png b/src/main/resources/static/assets/navbarImages/twitter.png new file mode 100644 index 0000000000000000000000000000000000000000..1e1101c732b9616d0a86cbf84bdff8bde8edd33a Binary files /dev/null and b/src/main/resources/static/assets/navbarImages/twitter.png differ diff --git a/src/main/resources/static/css/Categories/categories.css b/src/main/resources/static/css/Categories/categories.css index 6cee61e1efea93efc41e09bc085c32154145b398..7008983d634c449354f0d120f2883d45ddb41f22 100644 --- a/src/main/resources/static/css/Categories/categories.css +++ b/src/main/resources/static/css/Categories/categories.css @@ -1,47 +1,140 @@ -.card-container { +*{ + margin: 0; + box-sizing: border-box; + padding: 0; +} +.info-database { + font-family: "Inter", sans-serif; + padding: 20px; +} + +.title-section { display: flex; - flex-wrap: wrap; - gap: 20px; - justify-content: center; - margin: 20px; + align-items: center; + justify-content: space-between; + margin-bottom: 30px; } -.card { - background-color: #f9f9f9; - border-radius: 15px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - padding: 20px; - width: 250px; - text-align: center; - transition: transform 0.2s; +.title-section > h1 { + font-size: 32px; } -.card:hover { - transform: scale(1.05); +button { + padding: 10px 15px; + border: none; + font-size: 14px; + cursor: pointer; + border-radius: 5px; + background-color: var(--primary-color); + color: var(--alternate-text); } -.add-card { - background-color: #e0e0e0; +.title-section > button { display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - font-size: 2rem; - color: #555; - border: 2px dashed #ccc; + gap: 15px; + +} + +.card-container { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 2rem; +} + +.card { + border: 1px solid var(--border-color); + border-radius: 10px; + padding: 15px 20px; + transition: transform 0.2s; + text-decoration: none; } -.add-card:hover { - background-color: #d0d0d0; +.card > div { + display: flex; + flex-direction: column; + gap: 10px; +} + +.card:hover { + transform: scale(1.005); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } -.card h3 { - margin: 10px 0; + +.card > div > h2 { font-size: 1.5rem; - color: #333; + font-weight: 500; + color: var(--primary-color); } .card p { color: #777; font-size: 1rem; + line-height: 1.4; +} + +.create-new-modal { + display: none; /* initially hidden */ + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; + justify-content: center; + align-items: center; + overflow: auto; +} + +.modal-content { + background: var(--background-color); + padding: 30px; + border-radius: 10px; +} + +.modal-header { + display: flex; + margin-bottom: 20px; +} +.modal-title-desc{ + flex: 1; +} + +.modal-title-desc > p{ + color: #757575; + margin-top: 8px; } + +form { + display: flex; + flex-direction: column; + gap: 10px +} + +form > label { + display: flex; + flex-direction: column; + gap: 7px; +} + +form > label > input,textarea { + padding: 10px; + border-radius: 10px; + border: 1px solid var(--border-color); +} + +.form-buttons { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.cancel-button { + background-color: var(--background-color); + color: var(--text-color); + border: 1px solid var(--border-color); +} +#closeModalBtn { + cursor: pointer; +} \ No newline at end of file diff --git a/src/main/resources/static/css/comments/comments.css b/src/main/resources/static/css/comments/comments.css new file mode 100644 index 0000000000000000000000000000000000000000..57cfd1abc4fd8960035c067041499e066a78c251 --- /dev/null +++ b/src/main/resources/static/css/comments/comments.css @@ -0,0 +1,94 @@ + +.comments-section { + width: 100%; + max-width: 800px; + margin: 0 auto; + padding: 20px; + background-color: var(--background-color); + border-radius: 4px; + h4 { + margin: 0; + display: flex; + font-size: 0.9em; + color: var(--primary-color); + opacity: 0.8; + } +} +.comment { + max-width: fit-content; + display: flex; + flex-direction: column; + border: 1px solid var(--border-color); + border-radius: 8px; + margin-bottom: 15px; + padding: 15px; + background-color: var(--secondary-color); + transition: box-shadow 0.3s ease; +} +.comment:hover { + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); +} +.comment p { + margin: 0 0 10px 0; + color: var(--text-color); + line-height: 1.5; +} +.comment small { + padding: 2px; + color: var(--primary-color); + opacity: 0.7; + font-size: 0.85em; +} +@media screen and (max-width: 600px) { + .comments-section { + padding: 10px; + } + + .comment { + padding: 10px; + } + + .comment p { + font-size: 14px; + } + + .comment small { + font-size: 12px; + } + .comment-submission textarea { + font-size: 14px; + } + + .comment-submission button { + width: 100%; + } +} +.comment:not(:last-child) { + border-bottom: 1px solid var(--border-color); +} + +/*Comment form styling*/ +.comment-submission { + margin-top: 20px; +} + +.comment-submission textarea { + max-width: 90%; + padding: 10px; + margin-bottom: 10px; + border: 1px solid var(--border-color); + border-radius: 4px; +} + +.comment-submission button { + background-color: var(--border-color); + color: var(--alternate-text); + border: none; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; +} + +.comment-submission button:hover { + opacity: 0.9; +} \ No newline at end of file diff --git a/src/main/resources/static/css/feed/feed.css b/src/main/resources/static/css/feed/feed.css index fe4fe7cb8e68d1588d292853f657e625df06aea7..a32014b87edfb7c3226bad46b1482207eaa7fa89 100644 --- a/src/main/resources/static/css/feed/feed.css +++ b/src/main/resources/static/css/feed/feed.css @@ -1,173 +1,234 @@ -body { - font-family: Arial, sans-serif; +*{ margin: 0; + box-sizing: border-box; padding: 0; - background-color: #E3E3DF; } -/* Main Container */ -.container { - width: 100%; - max-width: 100vw; - margin: 0 auto; - padding: 20px; - box-sizing: border-box; + +.feed-container { + font-family: "Inter", sans-serif; + padding: 40px 16px; } -.hero-text{ - color:#5D001E; - font-weight: bolder; +.feed-hero > h1 { text-align: center; + font-size: 36px; + margin-bottom: 32px; } -/* Sort Container */ -.sort-container { - margin-bottom: 20px; - font-size: 16px; + +.post-feed { display: flex; - justify-content: space-between; + flex-direction: column; align-items: center; - color: #5D001E; /* Deep red text for labels */ + gap: 30px; } -.sort-container label { - font-weight: bold; +.post { + padding: 24px; + border: 1px solid var(--border-color); + border-radius: 10px; + flex: 1; + width: 80%; + display: flex; + flex-direction: column; + gap: 20px; } -.sort-container select { - padding: 5px; - font-size: 14px; - border: 1px solid #9A1750; - background-color: #E3AFBC; - color: #5D001E; - border-radius: 5px; - transition: border-color 0.3s ease; +.author-details{ + display: flex; + gap: 10px; + align-items: center; } -.sort-container select:hover { - border-color: #EE4C7C; +.profile-picture{ + width: 46px; + height: 46px; + background: var(--secondary-color); + border-radius: 50%; } -/* Post Feed */ -.post-feed { - display: flex; - flex-direction: column; - gap: 20px; - max-height: 40%; - overflow-y: auto; - padding-right: 10px; - scroll-behavior: smooth; - scroll-snap-type: y mandatory; +.text-details > h5 { + font-size: 16px; + margin-bottom: 2px; + font-weight: 450; } -/* Individual Post */ -.post { +.text-details > p { + color: var(--secondary-text-color); + font-size: 14px; +} + +.post-details { display: flex; flex-direction: column; - width: 75vw; - background-color: #FFFFFF; - border-radius: 8px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - overflow: hidden; - scroll-snap-align: start; - transition: transform 0.3s ease-in-out; - border: 2px solid #E3AFBC; + gap: 8px; +} + +.post-details > h3 { + font-size: 24px; } -.post:hover { - transform: translateY(-10px); - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); +.post-details > p { + color: var(--secondary-text-color); } -/* Post Image */ + .post-image { - height: 70%; - overflow: hidden; width: 100%; + height: 300px; + border-radius: 15px; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--secondary-color); + } .post-image img { - object-fit: cover; width: 100%; height: 100%; + object-fit: cover; + border-radius: 15px; } -/* Post Content */ -.post-content { +.post-tags { display: flex; - flex-direction: column; - justify-content: space-between; - padding: 15px; - background-color: #E3E3DF; - flex-grow: 1; + gap: 10px; + margin-top: 10px; } -/* Post Title */ -.post-title { - font-size: 24px; - font-weight: bold; - color: #9A1750; - margin-bottom: 10px; - line-height: 1.3; - overflow: hidden; - white-space: normal; - word-wrap: break-word; +.post-tags > span { + display: flex; + gap: 5px; + align-items: center; + font-size: 12px; + padding: 2px 10px; + border-radius: 10px; + font-weight: 600; + cursor: pointer; + background: var(--secondary-color); } -/* Post Description */ -.post-description { - font-size: 16px; - color: #5D001E; - margin-top: 10px; - flex-grow: 1; - line-height: 1.5; - overflow: hidden; - white-space: normal; - word-wrap: break-word; +.post-meta{ + display: flex; + font-size: 14px; + color: var(--secondary-text-color) !important; } -.post-meta { +.post-actions{ display: flex; + flex: 5; justify-content: space-between; - font-size: 14px; - color: #9A1750; - margin-top: 10px; - padding-top: 5px; - border-top: 1px solid #E3AFBC; } -/* Author Styling */ -.post-meta .author { - font-style: italic; +.post-actions > button { + border: none; + background: none; + cursor: pointer; + display: flex; + gap: 10px; + align-items: center; +} + +.timestamp { + flex: 2; + text-align: right; +} + +.add-post { + height: 56px; + width: 56px; + border-radius: 50%; + font-size: 24px; + position: fixed; + cursor: pointer; + right: 40px; + bottom: 30px; + background-color: var(--primary-color); + color: var(--alternate-text); + z-index: 100; +} + +/* +using the same styles used for the create new category modal, since we did not use classnames +we just copy paste in order not to affect other form styles. +*/ + +.create-new-modal { + display: none; /* initially hidden */ + top: 0; + left: 0; + width: 100%; + position: fixed; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; + justify-content: center; + align-items: center; + overflow: auto; } -/* Timestamp Styling */ -.post-meta .timestamp { - font-weight: bold; - color: #EE4C7C; +.modal-content { + background: var(--background-color); + padding: 24px; + border-radius: 10px; } -/* Scrollbar Customization */ -::-webkit-scrollbar { - width: 8px; +.modal-header { + display: flex; + margin-bottom: 20px; +} +.modal-title-desc{ + flex: 1; } -::-webkit-scrollbar-thumb { - background: #888; - border-radius: 4px; - transition: background 0.3s ease; +.modal-title-desc> h3 { + font-size: 16px; } -::-webkit-scrollbar-thumb:hover { - background: #5D001E; +.modal-title-desc > p{ + color: #757575; + margin-top: 8px; + font-size: 14px; } -::-webkit-scrollbar-track { - background: #E3E3DF; - border-radius: 4px; +form { + display: flex; + flex-direction: column; + gap: 10px } +form > label { + display: flex; + flex-direction: column; + gap: 7px; + font-size: 14px; +} +form > label > input,textarea { + padding: 10px; + border-radius: 10px; + border: 1px solid var(--border-color); +} +.submit-button { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + justify-content: center; + padding: 10px 15px; + border: none; + font-size: 14px; + cursor: pointer; + border-radius: 5px; + background-color: var(--primary-color); + color: var(--alternate-text); +} +#closeModalBtn { + cursor: pointer; +} \ No newline at end of file diff --git a/src/main/resources/static/css/headings/headings.css b/src/main/resources/static/css/headings/headings.css new file mode 100644 index 0000000000000000000000000000000000000000..7c529b08c7cdd973682acac509c4cd925cd2cc11 --- /dev/null +++ b/src/main/resources/static/css/headings/headings.css @@ -0,0 +1,91 @@ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +button { + padding: 10px 15px; + border: none; + font-size: 14px; + cursor: pointer; + border-radius: 5px; + background-color: var(--primary-color); + color: var(--alternate-text); +} + +#add-new-information { + display: flex; + gap: 15px; +} + +.general-headings-layout{ + font-family: "Roboto", serif; + padding: 20px; +} + +.title-section { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 30px; +} + +.title-section > h1 { + font-size: 32px; +} + +.buttons{ + display: flex; + gap:5px; +} + +.headings-container { + display: flex; + flex-direction: column; + gap: 15px; +} + +.list-item{ + background-color: var(--secondary-color); + padding: 20px 10px; + border-left: 4px solid var(--primary-color); + display: flex; + flex-direction: column; + gap: 10px; + border-radius: 5px; + align-items: start; +} + +.list-item h2 { + font-weight: 450; +} + +.list-item:hover { + border-left: 4px solid var(--tertiary-color); +} + +.list-item button { + background-color: var(--background-color); + border: 1px solid var(--border-color); + color: var(--text-color); + display: flex; + gap: 15px; +} + +a { + text-decoration: none; + width: fit-content; +} + +input[type="checkbox" i] { + appearance: menulist-button; +} + +#delete-button{ + visibility: hidden; + float: right; + margin: 5px 0; + +} \ No newline at end of file diff --git a/src/main/resources/static/css/home/home.css b/src/main/resources/static/css/home/home.css index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6f44f6bdcbce25eec369bb2574edb44a21ea10f7 100644 --- a/src/main/resources/static/css/home/home.css +++ b/src/main/resources/static/css/home/home.css @@ -0,0 +1,154 @@ +body { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + margin: 0; + padding: 0; + font-family: Arial, sans-serif; +} + +section { + max-width: 1200px; + width: 100%; + padding: 20px; +} + + +h1 { + font-size: 2.5rem; + margin-bottom: 10px; +} + +p { + font-size: 1.2rem; + margin-bottom: 20px; +} + +button { + padding: 10px 20px; + font-size: 1rem; + background-color: #007bff; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + margin-top: 20px; +} + +button:hover { + background-color: #0056b3; +} + +/* Tiles Section */ +.tiles { + display: grid; + grid-template-columns: repeat(3, 1fr); /* 3 equal columns */ + gap: 20px; + margin: 20px 0; + justify-content: center; + align-items: center; +} + +/* Styling for individual tiles */ +.tile { + background-color: #f9f9f9; + padding: 10px; + border: 1px solid #ddd; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + text-align: left; /* Align text to the left */ + width: 250px; /* Fixed width for tiles */ + height: 150px; /* Adjusted height for tiles */ + display: flex; + flex-direction: column; + justify-content: space-between; /* Space out the content inside the tile */ +} + +/* Logo and title container */ +.tile-logo-title { + display: flex; + align-items: center; /* Align logo and title vertically */ + gap: 10px; /* Space between logo and title */ + margin-bottom: 10px; /* Space between logo/title and description */ +} + +/* Logo inside tile */ +.tile-logo { + width: 30px; /* Adjusted logo size for better presentation */ + height: 30px; + object-fit: cover; +} + +/* Title inside tile */ +.tile-title { + font-size: 1.2rem; /* Smaller title font */ + color: white; + margin-bottom: 0; /* Remove bottom margin */ +} + +/* Description inside tile */ +.tile-description { + font-size: 0.9rem; /* Smaller description text */ + color: white; /* Lighter color for description */ + line-height: 1.4; + margin-top: 15px; /* Add space between title/logo and description */ +} + +/* Styling the links */ +.tileLink { + text-decoration: none; /* Remove default underline */ + color: inherit; /* Inherit color from the parent (tile) */ + display: flex; + flex-direction: column; + align-items: flex-start; /* Align content to the left */ + height: 100%; /* Ensures the link takes full height of the tile */ +} + +.tileLink:hover { + background-color: #f0f0f0; /* Light background on hover */ + border-radius: 10px; + transition: background-color 0.3s ease; +} + +.tileLink:active { + transform: scale(0.98); /* Slightly shrink on click */ +} + +/* Adding spacing between the tiles */ +.tile:not(:last-child) { + margin-right: 20px; +} + +.topTile { + display: flex; + justify-content: center; + align-items: center; + flex-direction: row; +} + +#feedTile { + background: linear-gradient(135deg, #FF8008, #FFC837); /* Pinkish gradient */ + color: white; +} + +#newsTile { + background: linear-gradient(135deg, #42E695, #3BB2B8); + color: white; +} + +#eventsTile { + background: linear-gradient(135deg, #FF7EB3, #FF758C); + color: white; +} + +#infoTile { + background: linear-gradient(135deg, #6A11CB, #2575FC); + color: white; +} + +#aboutTile { + background: linear-gradient(135deg, #9D50BB, #6E48AA); + color: white; +} \ No newline at end of file diff --git a/src/main/resources/static/css/information/AddInfoStyles.css b/src/main/resources/static/css/information/AddInfoStyles.css new file mode 100644 index 0000000000000000000000000000000000000000..6da416e8612df4dfd0f51051eb6226e813bd357e --- /dev/null +++ b/src/main/resources/static/css/information/AddInfoStyles.css @@ -0,0 +1,93 @@ +section { + font-family: Arial, sans-serif; + padding: 20px; +} +h1 { + font-size: 24px; + margin-bottom: 20px; +} +fieldset { + border: none; + padding: 0; +} +label { + display: block; + margin-bottom: 8px; + margin-top: 10px; + font-weight: bold; +} + +input[type="text"], textarea { + width: 97%; + padding: 10px; + margin-bottom: 10px; + border: 0.1px solid darkgray; + border-radius: 4px; + font-size: 16px; +} + +textarea { + height: 100px; +} +.buttons { + display: flex; + justify-content: flex-end; +} +button { + padding: 10px 20px; + margin-left: 10px; + border: none; + border-radius: 4px; + font-size: 16px; + cursor: pointer; +} + +.cancel { + background-color: var(--border-color); + color: var(--primary-color); +} +.submit { + background-color: var(--primary-color); + color: var(--secondary-color); +} +button:hover { + opacity: 0.9; +} + +a.button { + display: inline-block; + padding: 10px 20px; + margin-left: 10px; + border-radius: 4px; + font-size: 16px; + text-align: center; + text-decoration: none; + cursor: pointer; +} + +.backButton{ + color: black; + text-decoration: none; +} + +a.cancel{ + background-color: var(--border-color); + color: var(--primary-color); +} + +a.button:hover { + opacity: 0.9; +} + +.alert{ + color: var(--warning-color); + display: block; + height: 20px; + margin: 0 0 10px; +} + +#editor{ + height: 200px; + width: auto; + margin-bottom: 15px; +} \ No newline at end of file diff --git a/src/main/resources/static/css/information/infoStyles.css b/src/main/resources/static/css/information/infoStyles.css index 2de6c1579d10120cf475b423b4ed6d910c272957..90202542e152c378d66fd96c922ca57d0c942ea6 100644 --- a/src/main/resources/static/css/information/infoStyles.css +++ b/src/main/resources/static/css/information/infoStyles.css @@ -1,17 +1,18 @@ *{ box-sizing: border-box; - font-family:'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif + font-family: "Roboto", serif; + /*font-family:'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif*/ } .container{ text-align: center; max-width: 90%; - background-color: #9a1750; - background-image: linear-gradient(to right, #9a1750 20%,#e3afbc); - color: #e3e2df; + background-color: var(--primary-color); + /*background-image: linear-gradient(to right, var(--primary-color) 20%,var(--border-color));*/ + color: var(--alternate-text); margin: 2% auto; - box-shadow: 0px 0px 5px 2px #e3afbc; + box-shadow: 0px 0px 10px 1px var(--primary-color); border-radius: 5px; } @@ -21,8 +22,35 @@ align-items: center; gap: 10px; padding: 10px; + justify-content: space-between; } +.sub-head-main-container{ + display: flex; + align-items: center; + gap: 10px; + padding: 10px; +} + +#editButtonLink button{ + padding: 12px 20px; + border: none; + font-size: 14px; + cursor: pointer; + border-radius: 5px; + margin-right: 30px; + box-shadow: 0px 0px 10px 1px var(--primary-color); + background-color: var(--secondary-color); + color: var(--primary-color); +} + +#editButtonLink button:hover{ + background-color: var(--border-color); + /*color: var(--secondary-color);*/ + box-shadow: 0px 0px 1px 2px var(--text-color); +} + + #markupcontent{ text-align: center; padding: 0 8% 1% 8%; @@ -42,7 +70,7 @@ max-width: 100%; /* Ensures the image does not overflow the parent container */ height: auto; /* Maintains aspect ratio */ border-radius: 8px; /* Adds a slight rounding to the corners for better aesthetics */ - box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); + box-shadow: 0px 4px 6px var(--primary-color); } #markupcontent p{ @@ -68,12 +96,12 @@ /*background-color: #9a1750;*/ } -main{ +.main-content{ max-width: 100%; - background-color: #e3e2df; - color: #5d001e; + /*background-color: #e3e2df;*/ + color: var(--primary-color); margin: auto; - background-image: linear-gradient(to right, #e3e2df,white); + background-image: linear-gradient(to right, var(--secondary-color),var(--background-color)); /* box-shadow: 0 0 10px 0px 2px #5d001e; */ height: auto; border-radius: 0 0 2px 2px; diff --git a/src/main/resources/static/css/layout/layout.css b/src/main/resources/static/css/layout/layout.css index 362d68cd953f733d140fb6c67086c21bc60ed97e..921144a67e6f3796047d70e4689c0ab806517c5a 100644 --- a/src/main/resources/static/css/layout/layout.css +++ b/src/main/resources/static/css/layout/layout.css @@ -1,67 +1,219 @@ + * { - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif + font-family: "Inter", sans-serif; } -.header { - max-width: 100%; + +:root { + --primary-color: #000000; + --secondary-color: #F4F4F5; + --border-color: #E5E4E6; + --text-color: #09090B; + --alternate-text: #ffffff; + --background-color: #ffffff; + --tertiary-color: #DC143C; + --secondary-text-color: #71717A; + --warning-color: red; +} + + + +body { + padding-top: 10vh; + min-height: 100vh; /* Ensures the body covers the viewport height */ display: flex; - flex-direction: row; - align-items: center; - justify-content: space-evenly; - overflow: hidden; - top: 0px; - position: absolute; - max-height: 10vh; - background-color: #e3afbc; - color: #5d001e; + flex-direction: column; +} + +.main-content { + flex: 1; /* Allows the main content to grow and push the footer down */ + +} + +.sidebar { + width: 100%; + top: 0; left: 0; - right: 0; + background-color: var(--primary-color); + padding: 0; + position: fixed; + height: 64px; } -.mainBody { - padding-top: 10vh; + + +/* Navbar container */ +nav.sidebar { + + display: flex; + justify-content: space-between; + align-items: center; + flex-direction: row; /* Horizontal layout */ + width: 100%; /* Full width of the screen */ + position: relative; /* Ensure positioning for the right section */ + padding: 10px 20px; /* Padding on left and right */ +} + +.navMiddle { + display: flex; + justify-content: center; /* Center the navMiddle horizontally */ + align-items: center; +} + +.navRight { + display: flex; + align-items: center; + gap: 15px; /* Space between the language selector and profile */ +} + +.navLeft { + display: flex; + justify-content: flex-end; +} + +.logoImage { + width: 55px; + height: 55px; +} + +.navLink { + display: flex; + align-items: center; + text-decoration: none; + color: #fff; + padding: 6px 8px; + font-size: 12px; + font-family: Arial, sans-serif; + font-weight: 300; + transition: background-color 0.3s ease; +} + +.navLink:hover { + background-color: #555; + border-radius: 5px; +} + +.logoLink { + display: flex; + gap: 10px; + align-items: center; + text-decoration: none; + color: #fff; + font-size: 12px; + font-family: Arial, sans-serif; + font-weight: 300; + transition: background-color 0.3s ease; /* hoover effect transition*/ +} + +.logoLink:hover { + background-color: #555; } -.logo { - max-height: 50px; - max-width: 25%; - padding: 2px; - object-fit: cover; +.navIcons { + width: 15px; + height: 15px; + margin-right: 5px; } -.siteName { - max-width: 50vw; + +.languageSelector { + display: flex; + align-items: center; /* Vertically center the icon and select box */ + gap: 5px; } -.icon { - transform: scale(2); - padding-right: 20px; + +.languageSelector select { + background-color: #333; + color: white; + border: none; + padding: 5px; + font-size: 12px; + font-family: Arial, sans-serif; + cursor: pointer; } + .footer { - overflow: hidden; - text-decoration: none; - bottom:0; - text-align: center; - border: 1px solid; + position: relative; width: 100%; - background-color: #4F4F51; - position: absolute; - height: 75px; - background-color: #e3afbc; - color: #5d001e; - left: 0; - right: 0; - p { - margin-bottom: 0; - margin-top: 5px; - } - dl { - margin: 0; - } - a { - text-decoration: none; - } -} -.footerContacts { + display: grid; + grid-template-columns: 1fr 1fr 1fr; /* grid layout */ + grid-template-rows: auto auto; + gap: 20px; + background-color: gray; + color: #fff; + padding: 20px; + border-top: 1px solid #D3D3D3; + margin-top: auto; /* Push footer below content */ +} + + +.about { + grid-column: 1 / 2;/* first column */ +} + +.links { + grid-column: 2 / 3; /* second column */ +} + +.connect { + grid-column: 3 / 4; /* third column */ +} + +.copyright { + grid-column: 1 / 4; /* all columns */ + text-align: center; + font-size: 12px; + color: #bbb; + margin-top: 20px; +} + +.footerTitle { + font-size: 18px; + margin-bottom: 10px; + color: #fff; +} + +.footerText { + font-size: 14px; + color: #ddd; + line-height: 1.5; +} + +.footerLinks { + list-style-type: none; + padding: 0; + margin: 0; +} + +.footerLinks li { + margin-bottom: 5px; +} + +.footerLink { + text-decoration: none; + color: #00bfff; + transition: color 0.3s ease; +} + +.footerLink:hover { + color: #1e90ff; +} + +.social-icons { display: flex; - flex-direction: row; - justify-content: space-evenly; + gap: 10px; + margin-top: 10px; +} + +.social-icon img { + width: 24px; + height: 24px; + transition: transform 0.3s ease; +} + +.social-icon img:hover { + transform: scale(1.2); } +.navText { + font-size: 14px; + font-weight: 500; +} \ No newline at end of file diff --git a/src/main/resources/static/css/news/addNews.css b/src/main/resources/static/css/news/addNews.css new file mode 100644 index 0000000000000000000000000000000000000000..5161adcc615429bfdc196b2bbf38469415314a2b --- /dev/null +++ b/src/main/resources/static/css/news/addNews.css @@ -0,0 +1,176 @@ +/* Basic reset */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* Modal Styling */ +.modal { + display: none; /* Hidden by default */ + align-items: center; + justify-content: center; + position: fixed; + z-index: 9999; /* Sit on top */ + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: hidden; /* Prevent scrolling of the modal itself */ + background-color: rgba(0, 0, 0, 0.4); /* Black with opacity */ +} + +/* Overlay */ +.overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); /* Dark background */ + z-index: 1; /* Behind the modal content */ +} + +/* Modal Content */ +.modal-content { + position: fixed; /* Ensure content stays above the overlay */ + background-color: #fff; + margin: auto; /* Centered horizontally and vertically */ + padding: 20px; + border-radius: 8px; + width: 80%; + max-width: 600px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + z-index: 2; /* Make sure modal content stays above overlay */ + max-height: 80vh; /* Prevents scrolling by limiting height */ + overflow: auto; /* Allow internal scrolling if necessary but prevents overall scrolling */ +} + +/* Close button */ +.close-btn { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + cursor: pointer; +} + +.close-btn:hover, +.close-btn:focus { + color: black; + text-decoration: none; + cursor: pointer; +} + +/* Button to open modal */ +.openModalBtn { + padding: 10px 15px; + background-color: var(--primary-color); + color: var(--secondary-color); + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 16px; +} + +.openModalBtn:hover { + background-color: var(--border-color); + color:var(--text-color); +} + +/* Form Styling */ +h2 { + text-align: center; + margin-bottom: 20px; +} + +form { + background-color: #fff; + padding: 20px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 600px; + margin: 0 auto; +} + +.form-group { + margin-bottom: 15px; +} + +label { + display: block; + font-size: 14px; + color: #555; + margin-bottom: 5px; +} + +input[type="text"], textarea { + width: 100%; + padding: 10px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + outline: none; + transition: border 0.3s ease; +} + +input[type="text"]:focus, textarea:focus { + border-color: #007bff; +} + +textarea { + resize: vertical; +} + +.form-buttons { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 20px; +} + +button { + padding: 10px 15px; + border: none; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.cancelButton { + background-color: black; + color: #fff; +} + +.submitButton { + background-color: black; + color: #fff; +} + +button:hover { + opacity: 0.9; +} + +/* Responsive Design */ +@media (max-width: 600px) { + form { + width: 90%; + padding: 15px; + } + + .form-buttons { + flex-direction: column; + gap: 10px; + } + + .submitButton, .cancelButton { + width: 100%; + text-align: center; + } + + .modal-content { + width: 90%; /* Ensure modal is responsive */ + } +} diff --git a/src/main/resources/static/css/news/editNewsStyle.css b/src/main/resources/static/css/news/editNewsStyle.css new file mode 100644 index 0000000000000000000000000000000000000000..789ca2f6a311df6145159d66a7af1319505ae0ed --- /dev/null +++ b/src/main/resources/static/css/news/editNewsStyle.css @@ -0,0 +1,100 @@ +body{ + font-family: Arial, sans-serif; + margin: 0; + padding: 0; +} + +.form-container { + display: flex; + justify-content: center; + align-items: center; + /* full screen height */ + height: 100vh; +} + +/* form style */ +form { + background-color: white; + border-radius: 10px; + padding: 30px; + width: 500px; + max-width: 100%; + box-sizing: border-box; + margin: auto; +} + +h1 { + text-align: left; + margin-bottom: 20px; + padding: 10px; +} + +label { + display: block; + font-weight: bold; + margin-bottom: 10px; + color: #333; +} + +/* edit all the input and textarea fields */ +input[type="text"], input[type="url"], input[type="date"], textarea { + width: 100%; + padding: 10px; + margin-bottom: 20px; + border: 1px solid #ccc; + border-radius: 5px; + font-size: 15px; + color: #777; +} + +textarea { + resize: vertical; + min-height: 100px; +} + +.button-group { + display: flex; + /* align buttons */ + justify-content: space-between; + gap: 10px; +} + +/* for the buttons to the right (save, delete) */ +.buttonsRight { + display: flex; + justify-content: flex-end; + gap: 10px; + +} + +button { + margin-top: 10px; + padding: 10px 20px; + font-size: 15px; + border-radius: 5px; + cursor: pointer; +} + +.btnSave { + background-color: #4CAF50; + color: white; + border: none; +} + +.btnDelete { + background-color: #f44336; + color: white; + border: none; +} + +.btnCancel { + background-color: white; + border-width: 1px solid #777; + display: flex; + justify-content: flex-start; +} + +.linkCancel { + text-decoration: none; +} + diff --git a/src/main/resources/static/css/news/newsStyles.css b/src/main/resources/static/css/news/newsStyles.css index 0e1105a09c48c71ad7054e53734fc4b7fad38e03..97d67379050ab7b6e04020c1c14411bc1de7df95 100644 --- a/src/main/resources/static/css/news/newsStyles.css +++ b/src/main/resources/static/css/news/newsStyles.css @@ -3,14 +3,17 @@ body { margin: 0; padding: 0; background-color: white; - color: #5D001E; + color: var(--text-color); } -.heading { - background-image: linear-gradient(to right, #9a1750 20%, #e3afbc); - color: #e3e2df; - text-align: center; - padding: 1% 0; + +.general-headings-layout{ + display: flex; + /*margin-top: 5%;*/ + align-items: center; + justify-content: space-between; + /*margin-bottom: 20px;*/ + margin: 5% 8% 0 8%; } h1 { @@ -27,8 +30,8 @@ h1 { } .news-card { - background-image: linear-gradient(to right, #e3e2df, white); - box-shadow: 0px 0px 15px 2px #e3afbc; + background-image: linear-gradient(to right, var(--border-color), var(--secondary-color)); + box-shadow: 0px 0px 15px 2px var(--border-color); padding: 1.5rem; width: 100%; max-width: 90%; @@ -71,7 +74,7 @@ h1 { justify-content: space-between; align-items: center; font-size: 0.9rem; - color: #777; + color: var(--primary-color); } .small-cards { @@ -85,7 +88,7 @@ h1 { flex: 1 1 calc(45% - 1rem); max-width: calc(45% - 1rem); padding: 1rem; - background-image: linear-gradient(to right, #e3e2df, white); + background-image: linear-gradient(to right, var(--border-color), var(--secondary-color)); display: flex; flex-direction: column; justify-content: space-between; @@ -113,3 +116,63 @@ h1 { .source a:hover { text-decoration: underline; } + +/*create news*/ +/* Modal background */ +.create-new-modal { + display: none; /* Initially hidden */ + position: fixed; /* Stay in place */ + z-index: 1; /* Sit on top */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + background-color: rgba(0, 0, 0, 0.4); /* Semi-transparent background */ +} + +/* Modal content */ +.modal-content { + background-color: #fff; + margin: 15% auto; /* Center the modal */ + padding: 20px; + border: 1px solid #888; + width: 80%; /* Set a width */ + max-width: 600px; +} + +/* Close button */ +.close-modal { + color: #aaa; + font-size: 28px; + font-weight: bold; + cursor: pointer; +} + +/* Change color on hover */ +.close-modal:hover, +.close-modal:focus { + color: black; + text-decoration: none; + cursor: pointer; +} + +.form-buttons { + display: flex; + justify-content: space-between; + padding-top: 15px; +} +.modify-btn { + display: inline-block; + margin-top: 5px; + padding: 5px 10px; + background-color: var(--primary-color); + color:var(--secondary-color); + text-decoration: none; + border-radius: 5px; + font-size: 14px; +} + +.modify-btn:hover { + background-color: var(--border-color); + color:var(--primary-color) +} diff --git a/src/main/resources/static/css/register/register.css b/src/main/resources/static/css/register/register.css index d162a6cc331416d3166406ea0433477fa6a4218f..fce9b8317fa68725866157c3baa1d42ed818a9b4 100644 --- a/src/main/resources/static/css/register/register.css +++ b/src/main/resources/static/css/register/register.css @@ -1,61 +1,166 @@ - -* { - box-sizing: border-box; -} - +/* General Styles */ body { font-family: Arial, sans-serif; - background-color: #E2AFBC; + background-color: #f9f9f9; + margin: 0; + padding: 0; display: flex; - justify-content: center; + flex-direction: column; align-items: center; - flex-direction: column; + justify-content: center; + height: 100vh; } -h1{ - padding: 30px; +/* Title */ +.registerTitle { + font-size: 2rem; + margin-bottom: 20px; + color: #333; } +/* Container */ .registerContainer { - background-color: #E3E2DF; - padding: 50px; + background-color: #ffffff; + padding: 30px; border-radius: 10px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 400px; + box-sizing: border-box; +} + +/* Form */ +.registerForm { + display: flex; + flex-direction: column; + gap: 20px; } .registerField { - margin-bottom: 20px; + display: flex; + flex-direction: column; } .registerField label { - font-size: 14px; - color: #333; - margin-bottom: 10px; + font-weight: bold; + margin-bottom: 5px; + color: #555; } -.registerField input { - width: 100%; +.registerField input, +.registerField select { padding: 10px; - border: 1px solid #ccc; + font-size: 1rem; + border: 1px solid #ddd; border-radius: 5px; - font-size: 15px; - background-color: #f9f9f9; + outline: none; + transition: border-color 0.3s; } -button { - width: 100%; - padding: 10px; - background-color: #9A1750; +.registerField input:focus, +.registerField select:focus { + border-color: #5b9bd5; +} + +/* Button */ +.registerButtonContainer { + text-align: center; +} + +.registerButton { + background-color: #5b9bd5; color: white; + padding: 10px 20px; + font-size: 1rem; border: none; border-radius: 5px; - font-size: 15px; cursor: pointer; + transition: background-color 0.3s; } -button:hover { - background-color: #5d001e; +.registerButton:hover { + background-color: #4a8abc; } -form { +/* Already Have an Account Section */ +.alreadyAccount { + text-align: center; margin-top: 20px; -} \ No newline at end of file + font-size: 0.9rem; + color: #555; +} + +.alreadyAccount a.loginLink { + color: #5b9bd5; + text-decoration: none; + font-weight: bold; + transition: color 0.3s; +} + +.alreadyAccount a.loginLink:hover { + color: #4a8abc; + text-decoration: underline; +} + +/* Agree to Terms Field */ +.termsField { + margin-top: 15px; + font-size: 0.9rem; + color: #555; + display: flex; + align-items: center; + gap: 10px; +} + +.termsField input[type="checkbox"] { + transform: scale(1.2); + margin-right: 8px; +} + +.termsField .termsLink { + color: #5b9bd5; + text-decoration: none; + font-weight: bold; + transition: color 0.3s; +} + +.termsField .termsLink:hover { + color: #4a8abc; + text-decoration: underline; +} + +input.invalid { + border: 2px solid red; +} + +span.error { + color: red; + font-size: 0.9rem; + margin-top: 5px; + display: block; +} + +/* Ensure strength container and error message are well spaced */ +/* Ensure strength container and error message are well spaced */ +.strength-container { + display: flex; + align-items: center; + gap: 10px; + margin-top: 5px; /* Add space between input and strength container */ +} + +#strengthBar { + width: 100%; + height: 10px; + border: 1px solid #ccc; + border-radius: 5px; +} + +.error-message { + color: red; + font-size: 0.9em; + margin-top: 5px; /* Add space between strength bar and error message */ + display: block; +} + + diff --git a/src/main/resources/static/error/error.css b/src/main/resources/static/error/error.css new file mode 100644 index 0000000000000000000000000000000000000000..f21dcb8481f58cb74300501ce05d1ecd468a91fa --- /dev/null +++ b/src/main/resources/static/error/error.css @@ -0,0 +1,23 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background-color: #f8f9fa; + margin: 0; +} + +#error-container { + text-align: center; +} + +#error-code { + font-size: 72px; + color: #dc3545; +} + +#error-message { + font-size: 24px; + color: #6c757d; +} diff --git a/src/main/resources/static/js/Categories/categories.js b/src/main/resources/static/js/Categories/categories.js index 8575093b742015afec6ede24cfb8e33f733ad334..da596b978540d1a818b76a8e76e9c7522f4ba6ee 100644 --- a/src/main/resources/static/js/Categories/categories.js +++ b/src/main/resources/static/js/Categories/categories.js @@ -18,33 +18,90 @@ function renderCategories(categories) { container.innerHTML = ''; categories.forEach(category => { - const card = document.createElement('div'); - card.classList.add('card'); - // Create a clickable link for the category title - const categoryLink = document.createElement('a'); - categoryLink.href = `/categories/${category.categoryId}?categoryName=${encodeURIComponent(category.categoryTitle)}`; - categoryLink.innerHTML = `<h3>${category.categoryTitle}</h3>`; - - const description = document.createElement('p'); - description.textContent = category.categoryDescription; - - card.appendChild(categoryLink); - card.appendChild(description); - container.appendChild(card); + const cardLink = document.createElement('a'); + cardLink.href = `/categories/${category.categoryId}?categoryName=${encodeURIComponent(category.categoryTitle)}`; + cardLink.classList.add('card'); + const cardContent = document.createElement('div'); + cardContent.innerHTML = ` + <h2>${category.categoryTitle}</h2> + <p>${category.categoryDescription}</p> + `; + + // Append content to the clickable link + cardLink.appendChild(cardContent); + + // Add the clickable card to the container + container.appendChild(cardLink); }); - // add category button - const addCard = document.createElement('div'); - addCard.classList.add('card', 'add-card'); - addCard.innerHTML = '<span>+</span>'; - addCard.onclick = openAddCategoryModal; - container.appendChild(addCard); } - function openAddCategoryModal() { alert("Prompt user to add a new category maybe in a modal"); } // Fetch and render categories on page load fetchCategories(); + + +/* ********* Modal opening and closing and adding of new categories ************************ */ + +const modal = document.getElementById('create-new-modal'); +const openModalBtn = document.getElementById('add-new-category'); +const closeModalButtons = document.querySelectorAll('.close-modal'); + +// open modal +openModalBtn.addEventListener('click', () => { + modal.style.display = 'flex'; +}); + +// close the modal, iterate over each button and add onclick +closeModalButtons.forEach(button => { + button.addEventListener('click', () => { + modal.style.display = 'none' + }) +}) + +// close the modal when one clicks outside of it +window.addEventListener('click', (event) => { + if (event.target === modal) { + modal.style.display = 'none'; + } +}); + + +//* ************************* Submitting the form *********************** *// +const form = document.getElementById('category-form'); + +form.addEventListener('submit', (event) => { + event.preventDefault() // prevent page reloading and default behaviour of form + + // get the data + const categoryTitle = document.getElementById('categoryTitle').value; + const categoryDescription = document.getElementById('categoryDescription').value; + + const data = { + categoryTitle, categoryDescription + } + console.log(data) + + // send post request + fetch('/api/categories/add', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(result => { + alert('Category added successfully') + modal.style.display = 'none' + fetchCategories() + }) + .catch(error => { + console.error('Error', error) + }) + + +}) \ No newline at end of file diff --git a/src/main/resources/static/js/Error/error.js b/src/main/resources/static/js/Error/error.js new file mode 100644 index 0000000000000000000000000000000000000000..94a737f0d06ad3d70f95a381eebac287dc2e2aa9 --- /dev/null +++ b/src/main/resources/static/js/Error/error.js @@ -0,0 +1,18 @@ +// check if error code is passed from the backend via the model. +let errorCode = window.errorCode || + new URLSearchParams(window.location.search).get('code') || // Check query parameter + 500; // Default to 500 if neither is available + +// Mapping error code to message +const errorMessages = { + 404: "The page you are looking for was not found.", + 403: "You don't have permission to access this resource.", + 500: "An internal server error occurred. Please try again later." +}; + +const errorCodeElement = document.getElementById("error-code"); +const errorMessageElement = document.getElementById("error-message"); + + +errorCodeElement.textContent = `Error ${errorCode}`; +errorMessageElement.textContent = errorMessages[errorCode] || "An unexpected error occurred."; diff --git a/src/main/resources/static/js/comments/comments.js b/src/main/resources/static/js/comments/comments.js new file mode 100644 index 0000000000000000000000000000000000000000..3f069f9d489d554219fff19b7dc7831ddd832c3f --- /dev/null +++ b/src/main/resources/static/js/comments/comments.js @@ -0,0 +1,76 @@ + +const commentForms = document.getElementsByClassName('comment-submission'); +for (let i = 0; i < commentForms.length; i++) { + let form = commentForms[i]; + form.addEventListener("submit", submitComment); + form.addEventListener() +} +function submitComment(event) { + event.preventDefault(); + + const form = event.target; + const postId = form.querySelector('input[name="postId"]').value; + const content = form.querySelector('textarea[name="content"]').value; + + // Create a new XMLHttpRequest object + const xhr = new XMLHttpRequest(); + + + xhr.open('POST', '/feed/comments/comments/publish', true); + xhr.setRequestHeader('Content-Type', 'application/json'); + + // Prepare data to send + const data = JSON.stringify({ + postId: postId, + content: content + }); + + // Setup a function to handle the response + xhr.onload = function () { + if (xhr.status >= 200 && xhr.status < 300) { + // Parse JSON response + const comment = JSON.parse(xhr.responseText); + + // Append new comment to comments section + const commentsSection = document.getElementById('comments-' + postId); + const newCommentHtml = ` + <div class="comment"> + <p>${comment.content}</p> + <small>By ${comment.username} on ${new Date(comment.createdDate).toLocaleString()}</small> + </div>`; + commentsSection.insertAdjacentHTML('beforeend', newCommentHtml); + + // Clear textarea + form.querySelector('textarea[name="content"]').value = ''; + } else { + console.error('Error posting comment: %o', xhr); + } + }; + + xhr.onerror = function () { + console.error('Request failed'); + }; + + // Send the request over the network + xhr.send(data); + +} +function deleteComment(commentId, postId) { + const xhr = new XMLHttpRequest(); + xhr.open('DELETE', `/feed/comments/${commentId}`, true); + xhr.onload = function() { + if (xhr.status === 204) { + // Remove the comment from the DOM + const commentElement = document.getElementById(`comment-${commentId}`); + if (commentElement) { + commentElement.remove(); + } + } else { + console.error('Error deleting comment'); + } + }; + xhr.onerror = function() { + console.error('Request failed'); + }; + xhr.send(); +} diff --git a/src/main/resources/static/js/feed/feed.js b/src/main/resources/static/js/feed/feed.js new file mode 100644 index 0000000000000000000000000000000000000000..b21bcb70cc8b8a088dcb89980c15f894e572b7ff --- /dev/null +++ b/src/main/resources/static/js/feed/feed.js @@ -0,0 +1,172 @@ +const API_BASE_URL = '/api/feed'; + + +const postFeed = document.getElementById('postFeed'); +const postTemplate = document.getElementById('post-template'); +const addNewPost = document.getElementById('add-post'); +const closeModalBtn = document.getElementById('closeModalBtn'); +const modal = document.getElementById('create-new-modal'); + + +addNewPost.addEventListener('click', () => { + modal.style.display = 'flex' +}) + +closeModalBtn.addEventListener('click', () => { + modal.style.display = 'none' + + '' +}) + +// maintaining state +let posts = []; + +// getting all posts using api +async function fetchPosts() { + try { + const response = await fetch(API_BASE_URL); + if (!response.ok) throw new Error('Failed to fetch posts'); + posts = await response.json(); + posts.reverse(); + renderPosts(); + } catch (error) { + console.error('Error fetching posts:', error); + } +} + + + +// render all posts after getting them +function renderPosts() { + postFeed.innerHTML = ''; // clear any posts + posts.forEach(post => renderPost(post)); // go over each item in post array and call the function renderPost on it +} + +// a single post +async function renderPost(post) { + const postElement = postTemplate.content.cloneNode(true); + + postElement.querySelector('.author').textContent = post.authorName; + postElement.querySelector('.author-title').textContent = post.authorOrganization; + postElement.querySelector('.post-title').textContent = post.postTitle; + postElement.querySelector('.post-description').textContent = post.postDescription; + + const postImage = postElement.querySelector('.post-image img'); + if (post.postImageUrl) { + postImage.src = post.postImageUrl; + } else { + postImage.style.display = 'none'; + } + + // render the tags + const tagsContainer = postElement.querySelector('.post-tags'); + if (post.tags && post.tags.length > 0) { + post.tags.forEach(tag => { + const tagSpan = document.createElement('span'); + tagSpan.innerHTML = `<i class="bi bi-tag"></i> ${tag}`; + tagsContainer.appendChild(tagSpan); + }); + } + + // set the count of the likes + const likeCount = postElement.querySelector('.like-count'); + likeCount.textContent = post.likesCount || 0; + + // timestamp + const timestamp = postElement.querySelector('.timestamp'); + timestamp.textContent = new Date(post.postTime).toLocaleDateString(); + + // add a like when presses + const likeButton = postElement.querySelector('.like-button'); + likeButton.addEventListener('click', () => handleLike(post.postId, likeCount)); + + // Set data attributes for reference + const postDiv = postElement.querySelector('.post'); + postDiv.dataset.postId = post.postId; + + postFeed.appendChild(postElement); +} + +// add a like if user had not already liked and remove a like if already liked +async function handleLike(postId, likeCountElement) { + try { + // check if user had already liked + const checkResponse = await fetch(`${API_BASE_URL}/${postId}/hasLiked?userId=${MOCK_USER_ID}`); + const hasLiked = await checkResponse.json(); + + const method = hasLiked ? 'DELETE' : 'POST'; + const response = await fetch(`${API_BASE_URL}/${postId}/like?userId=${MOCK_USER_ID}`, { + method: method + }); + + if (!response.ok) throw new Error('Failed to update like'); + + // update like count appropriately + const currentCount = parseInt(likeCountElement.textContent); + likeCountElement.textContent = hasLiked ? currentCount - 1 : currentCount + 1; + + // change color to show they have liked already + const button = likeCountElement.closest('.like-button'); + button.classList.toggle('liked', !hasLiked); + + } catch (error) { + console.error('Error updating like:', error); + } +} + +//* ************************* Submitting the form *********************** *// +const postForm = document.getElementById('post-form'); + + +postForm.addEventListener('submit', (event) => { + event.preventDefault(); + + // getting form data + const postTitle = document.getElementById('postTitle').value; + const postDescription = document.getElementById('postDescription').value; + const postTagsInput = document.getElementById('postTags').value; + + // convert comma-separated tags to array and trim whitespace + const tags = postTagsInput + .split(',') + .map(tag => tag.trim()) + .filter(tag => tag.length > 0); // removing empty tags + + const data = { + postTitle, + postDescription, + tags // sending as array + }; + + console.log('Submitting post:', data); + + // Send post request + fetch(`${API_BASE_URL}/add`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify(data) + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.text(); + }) + .then(result => { + alert('Post added successfully'); + modal.style.display = 'none'; + fetchPosts(); + }) + .catch(error => { + console.error('Error adding post:', error); + alert('Error adding post. Please try again.'); + }); +}); +document.addEventListener('DOMContentLoaded', () => { + fetchPosts(); +}); + +// refresh posts each minute , though cannot show at moment since not online +setInterval(fetchPosts, 60000); \ No newline at end of file diff --git a/src/main/resources/static/js/headings/headings.js b/src/main/resources/static/js/headings/headings.js new file mode 100644 index 0000000000000000000000000000000000000000..4c439b16623d49f1a59e8d4c4784567060899159 --- /dev/null +++ b/src/main/resources/static/js/headings/headings.js @@ -0,0 +1,190 @@ +// get category from url params +const urlParams = new URLSearchParams(window.location.search); +const categoryId = window.location.pathname.split('/')[2]; // Assuming /categories/{id} +const categoryName = urlParams.get('categoryName'); + +// fetch category headings from api +const apiUrl = '/getInfoByCategoryId'; + +async function fetchCategoryHeadings() { + try { + const response = await fetch(`${apiUrl}/${categoryId}`); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + + const headings = await response.json(); + renderHeadings(headings); + } catch (error) { + console.error("Error fetching category headings:", error); + } +} + +// list headings as clickable links +function renderHeadings(headings) { + const container = document.getElementById('headings-container'); + container.innerHTML = ''; + + headings.forEach(heading => { + const listItem = document.createElement('div'); + listItem.classList.add('list-item') + + const input = document.createElement('input'); + input.type='checkbox'; + + const title = document.createElement('h2') + title.innerHTML = `<i class="bi bi-file-earmark-text"></i> ${heading.infoTitle} ` + + // const description = document.createElement('p'); + // description.textContent = heading.description + + const link = document.createElement('a') + link.href = `/info/${heading.informationId}`; + link.classList.add('heading-link'); + + const readMore = document.createElement('button') + readMore.innerHTML = 'Read More <i class="bi bi-chevron-right"></i>' + readMore.classList.add('read-more-btn') + + link.appendChild(readMore); + + listItem.appendChild(title) + // listItem.appendChild(description) + listItem.appendChild(link) + container.appendChild(listItem) + }); +} + +// change the title of the page bases on category +document.getElementById('category-name').textContent = categoryName ? `${categoryName}` : 'Category Name Not Found'; + +// Fetch and render the category headings when the page loads +fetchCategoryHeadings(); + + +// Get the category id from the url +function getCategoryIdFromUrl() { + const path = window.location.pathname; + const parts = path.split("/"); + return parts[2]; +} + +// Function to dynamically add href url to add information button +function generateAddInfoPageUrl() { + // Extract categoryId from the URL + const categoryId = getCategoryIdFromUrl(); + + // Create url from category id + const url = `/info/add/${categoryId}`; + const link = document.getElementById("addInfo"); + link.setAttribute("href", url); +} + +generateAddInfoPageUrl(); + + +// Function to toggle edit mode to enable multi-select for deletion +function toggleEditMode() { + const container = document.getElementById('headings-container'); + const editButton = document.getElementById('edit-mode-button'); + const deleteButton = document.getElementById('delete-button'); + const isEditMode = container.classList.toggle('edit-mode'); + + const listItems = container.querySelectorAll('.list-item'); + + if (isEditMode) { + // Add checkboxes to each heading list item + listItems.forEach(item => { + if (!item.querySelector('input[type="checkbox"]')) { + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.classList.add('multi-select-checkbox'); + checkbox.addEventListener('change', toggleDeleteButtonState); + + // Inserts the checkbox before the first child of the list item + item.insertBefore(checkbox, item.firstChild); + } + }); + editButton.textContent = 'Done'; + // Enable the delete button + deleteButton.disabled = false; + deleteButton.style.visibility="visible" + } else { + // Remove checkboxes from each list item + listItems.forEach(item => { + const checkbox = item.querySelector('.multi-select-checkbox'); + if (checkbox) { + item.removeChild(checkbox); + } + }); + editButton.textContent = 'Edit'; + // Disables and hides the delete button + deleteButton.disabled = true; + deleteButton.style.visibility="hidden" + } +} + +// Function to toggle the delete button state based on checkbox selection +function toggleDeleteButtonState() { + const selectedItems = document.querySelectorAll('.multi-select-checkbox:checked'); + const deleteButton = document.getElementById('delete-button'); + deleteButton.disabled = selectedItems.length === 0; +} + +// Function to delete selected items +async function deleteSelectedItems() { + const selectedItems = document.querySelectorAll('.multi-select-checkbox:checked'); + + // Get the list of heading ids to be deleted + const idsToDelete = Array.from(selectedItems).map(checkbox => { + const listItem = checkbox.closest('.list-item'); + const anchor = listItem.querySelector('a.heading-link'); + if (anchor) { + const href = anchor.getAttribute('href'); + const idMatch = href.match(/info\/(\d+)/); // Extract the ID using regex + return idMatch ? idMatch[1] : null; // Return the ID if matched + } + return null; // Skip if no anchor element or invalid format + }).filter(id => id !== null); + + if (idsToDelete.length === 0) { + alert("No headings selected for deletion."); + return; + } + + // Check confirmation before deletion + const confirmed = confirm("Are you sure you want to delete the selected list of headings?"); + if (!confirmed) return; + + // Convert array of heading IDs to a comma-separated string to be passed as a parameter + const idList = idsToDelete.join(','); + + // Fetch api call to delete the list of headings at server side + try { + const response = await fetch('/api/info/delete', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: `deleteIdList=${encodeURIComponent(idList)}` + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + // Remove deleted items from the DOM + selectedItems.forEach(checkbox => { + const listItem = checkbox.closest('.list-item'); + listItem.remove(); + }); + + alert("Items deleted successfully."); + + } catch (error) { + console.error("Error deleting items:", error); + alert("Failed to delete items. Please try again."); + } +} + +// Attach event listeners to toggle edit button and perform deletion +document.getElementById('edit-mode-button').addEventListener('click', toggleEditMode); +document.getElementById('delete-button').addEventListener('click', deleteSelectedItems); \ No newline at end of file diff --git a/src/main/resources/static/js/infoHeadingFragment/infoHeadingFragment.js b/src/main/resources/static/js/infoHeadingFragment/infoHeadingFragment.js index 970f1d38d51506c6dbe31137b738f936edb8108a..20338a29a9009a767de78cc2e8e5d41b2a498cde 100644 --- a/src/main/resources/static/js/infoHeadingFragment/infoHeadingFragment.js +++ b/src/main/resources/static/js/infoHeadingFragment/infoHeadingFragment.js @@ -19,9 +19,9 @@ document.addEventListener("DOMContentLoaded", function() { sessionStorage.setItem('categoryUrl', categoryUrl); breadcrumbContainer.innerHTML = ` - <li><a href="/">Home</a></li> - <li><a href="/categories">Categories</a></li> - <li><a href="${categoryUrl}">${categoryName}</a></li> + <li><a href="/">Home</a> > </li> + <li><a href="/categories">Categories</a> > </li> + <li>${categoryName}</li> `; } else { console.warn("Category name is missing in the URL parameters."); @@ -29,7 +29,7 @@ document.addEventListener("DOMContentLoaded", function() { } // If we are on the heading page, set the breadcrumb as Home > Categories > Category Name > Heading Name - else if (window.location.pathname.includes('/getInfo/')) { + else if (window.location.pathname.includes('/info/')) { const headingName = document.querySelector('h1').textContent || 'Heading'; // Get the heading name from the page const categoryId = sessionStorage.getItem('categoryId'); diff --git a/src/main/resources/static/js/information/addInfoScript.js b/src/main/resources/static/js/information/addInfoScript.js new file mode 100644 index 0000000000000000000000000000000000000000..79d9d87e848d7c720c1c220861570bdf94b0cd17 --- /dev/null +++ b/src/main/resources/static/js/information/addInfoScript.js @@ -0,0 +1,31 @@ +const quill = new Quill('#editor', { + theme: 'snow' +}); + + +// Add event listener to save content +document.getElementById("addInfoForm").addEventListener("submit", + function (event) { + + const turndownService = new TurndownService(); + // const delta = quill.getContents(); // Get Quill Delta + const html = quill.root.innerHTML; // Get HTML content + const markdown = turndownService.turndown(html); // Convert HTML to Markdown + var elem = document.getElementById("infoDescription"); + elem.value = markdown; +}); + +function markdownToHtml(markdown){ + return marked.parse(markdown); +} + +function addContentToRichText(){ + debugger + const descriptionElem = document.getElementById("infoDescription"); + const convertedHtml = markdownToHtml(descriptionElem.defaultValue); + quill.root.innerHTML = convertedHtml; +} + +addContentToRichText(); + + diff --git a/src/main/resources/static/js/information/infoScript.js b/src/main/resources/static/js/information/infoScript.js index e88ee5a4e1fe3cb892232912ddf3911051bb7dc7..c891fd93613d9835e3f655b28d25616290059e9b 100644 --- a/src/main/resources/static/js/information/infoScript.js +++ b/src/main/resources/static/js/information/infoScript.js @@ -16,4 +16,31 @@ // } // // // document.addEventListener('onload',htmlToMarkdown()); -// document.addEventListener('onload',markdownToHtml()); \ No newline at end of file +// document.addEventListener('onload',markdownToHtml()); + +document.addEventListener("DOMContentLoaded", () => { + function getHeadingsUrl() { + const categoryId = sessionStorage.getItem('categoryId'); + const categoryName = sessionStorage.getItem('categoryName'); + + // Ensure categoryId and categoryName exist + if (!categoryId || !categoryName) { + console.error('Category id or name is not available in the session'); + } + + const url = `/categories/${categoryId}?categoryName=${categoryName}`; + return url + } + + function addUrlToElement(elementId){ + const link = document.getElementById(elementId); + if (link!=null) { + link.setAttribute("href", getHeadingsUrl()); + } else { + console.error('Anchor element is null'); + } + } + + addUrlToElement('backToHeadings'); + addUrlToElement('cancelButton'); +}); \ No newline at end of file diff --git a/src/main/resources/static/js/news/addNews.js b/src/main/resources/static/js/news/addNews.js new file mode 100644 index 0000000000000000000000000000000000000000..1e5f7a6d3ea9dff109d86d199c445f7f51d3750e --- /dev/null +++ b/src/main/resources/static/js/news/addNews.js @@ -0,0 +1,28 @@ + +// Get the modal and button +const modal = document.getElementById("modal"); +const closeBtn = document.getElementsByClassName("close-btn")[0]; +const cancelButton = document.getElementById("cancelButton"); +const overlay = document.getElementById("overlay"); + +// Close the modal +closeBtn.onclick = function() { + modal.style.display = "none"; +} + +// Close the modal if the overlay is clicked +window.onclick = function(event) { + if (event.target == modal) { + modal.style.display = "none"; + } +} + +// Close the modal when the user clicks the "Cancel" button +cancelButton.addEventListener("click", function() { + modal.style.display = "none"; +}); + +// Close the modal when the user clicks outside of the modal content +overlay.addEventListener("click", function() { + modal.style.display = "none"; +}); \ No newline at end of file diff --git a/src/main/resources/static/js/news/editNewsScript.js b/src/main/resources/static/js/news/editNewsScript.js new file mode 100644 index 0000000000000000000000000000000000000000..582ed9625f0dc759c4d4ced88695a377c6b24d30 --- /dev/null +++ b/src/main/resources/static/js/news/editNewsScript.js @@ -0,0 +1,38 @@ +// function for basic validation of the form, checks to see if any field is empty +function validateForm(event) { + + // there are different funtions depending on what button is pressed by the user + // :focus is used to to having multiple submit buttons on the form + const action = event.target.querySelector('[type="submit"]:focus').value; + + // if the user clicked the save changes button + if (action === 'edit') { + // get all input fields + const inputs = document.querySelectorAll('input[type="text"], input[type="url"], input[type="date"], textarea'); + + // loop through each input field to check if it's empty + for (let i = 0; i < inputs.length; i++) { + // check if the field is empty + // .trim() is used to avoid accepting cases where there is spaces in the fields + if (inputs[i].value.trim() === "") { + alert("Please fill out all fields."); + // prevent form submission + event.preventDefault(); + return false; + } + } + return true; + } +} + +// function for handling the delete button, +// this does not need the validation above as it does not matter if the fields are left empty +function confirmDelete(event) { + // confirmation message to the user + const confirmation = confirm('Are you sure you want to delete this news article?'); + if (!confirmation) { + event.preventDefault(); + return false; + } + return true; +} \ No newline at end of file diff --git a/src/main/resources/static/js/news/newsScripts.js b/src/main/resources/static/js/news/newsScripts.js index 53ef3b339a4a2484d8518a88b229ae2e58740e97..d07fb04169b7c869106a948c9322016b5b454325 100644 --- a/src/main/resources/static/js/news/newsScripts.js +++ b/src/main/resources/static/js/news/newsScripts.js @@ -33,3 +33,20 @@ document.addEventListener("DOMContentLoaded", () => { }); } }); + + +// // This will handle the modal opening when the Add News tile is clicked +function openNewsForm() { + const openModalBtn = document.getElementById("openModalBtn"); + // Open the modal + openModalBtn.onclick = function() { + modal.style.display = "flex"; + } + + const newsFormSection = document.getElementById('newsForm'); + if(newsFormSection.style.display == 'none'){ + newsFormSection.style.display = 'block'; + }else + newsFormSection.style.display = 'none'; +} + diff --git a/src/main/resources/static/js/register/register.js b/src/main/resources/static/js/register/register.js index 3bb7aa7ab929281bd930cff52e0fa23c0bae4180..1d13aef00dd90c7a4f575741139f9022332c77e8 100644 --- a/src/main/resources/static/js/register/register.js +++ b/src/main/resources/static/js/register/register.js @@ -1,27 +1,92 @@ -function registerValidate(){ +// Wait until the DOM is fully loaded before attaching event listeners +document.addEventListener("DOMContentLoaded", () => { + // fetch the input fields + const fullnameField = document.getElementById("fullname"); + const emailField = document.getElementById("email"); + const passwordField = document.getElementById("password"); + const confirmPasswordField = document.getElementById("confirmPassword"); + + // Add event listeners to input fields to validate as the user types + fullnameField.addEventListener("input", () => + // validate fullname with a regex to allow only letters and spaces + validateField(fullnameField, /^[A-Za-z\s]+$/, "Full name must only contain letters.")); + + emailField.addEventListener("input", () => + // validate email with a regex pattern + validateField(emailField, /^[^\s@]+@[^\s@]+\.[^\s@]+$/, "Enter a valid email address.")); - var pass = true; - var fullname = document.getElementById("fullname").value; - var password = document.getElementById("password").value; - var confirmPassword = document.getElementById("confirmPassword").value; - - if (password != confirmPassword){ - pass = false; - alert("Passwords must match") + passwordField.addEventListener("input", () => { + // validate password with a regex for minimum length, at least 1 number and special character + validateField(passwordField, /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, + "Password must be at least 8 characters long, include 1 number, and 1 special character."); + // update the strength bar based on the current password + updateStrengthBar(passwordField); + }); + confirmPasswordField.addEventListener("input", () => + // validate that the passwords match + validatePasswords(passwordField, confirmPasswordField)); +}); + +// function that updates the strength bar of the password +function updateStrengthBar(passwordField) { + const password = passwordField.value; + const strengthMessage = document.getElementById("strengthMessage"); + const strengthBar = document.getElementById("strengthBar"); + + let strength = 0; + + // add values based on what the password contains + if (password.length >= 8) strength += 20; + if (/[A-Za-z]/.test(password)) strength += 20; + if (/\d/.test(password)) strength += 20; + if (/[@$!%*?&]/.test(password)) strength += 20; + if (password.length >= 12) strength += 20; + + // update the bar + strengthBar.value = strength; + + // change the colour of the bar based on how strong the password is + if (strength < 40) { + strengthMessage.textContent = "Weak"; + strengthMessage.style.color = "red"; + } else if (strength < 80) { + strengthMessage.textContent = "Moderate"; + strengthMessage.style.color = "orange"; + } else { + strengthMessage.textContent = "Strong"; + strengthMessage.style.color = "green"; } +} - var nameRegex = /^[A-Za-z\s]+$/; - var passwordStrengthRegex = /^(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; +// function for checking a fields against the regex inputs +function validateField(field, regex, errorMessage) { + // find the error message <span> + const errorSpan = field.parentElement.querySelector(".error"); - if (!nameRegex.test(fullname)) { - pass = false; - alert("Full name must only contain letters and spaces."); + // show the error message if the regex test fails + if (!regex.test(field.value)) { + errorSpan.textContent = errorMessage; + // add styling + field.classList.add("invalid"); + } else { + // remove the error if the regex test is successful + errorSpan.textContent = ""; + // remove styling + field.classList.remove("invalid"); } - - if (!passwordStrengthRegex.test(password)) { - pass = false; - alert("Password must be at least 8 characters long, contain at least 1 uppercase letter, 1 number, and 1 special character."); +} + +// function that checks if the password and confirm password are the same +function validatePasswords(passwordField, confirmPasswordField) { + const errorSpan = confirmPasswordField.parentElement.querySelector(".error"); + + // show error if the password and confirm password don't match + if (passwordField.value !== confirmPasswordField.value) { + errorSpan.textContent = "Passwords do not match."; + confirmPasswordField.classList.add("invalid"); + } else { + errorSpan.textContent = ""; + confirmPasswordField.classList.remove("invalid"); } - - return pass; -} \ No newline at end of file +} + diff --git a/src/main/resources/templates/aboutUs/aboutUs.html b/src/main/resources/templates/aboutUs/aboutUs.html index c16209a3d66deb82d4e83bc378a9d6306070c881..aa74252adfd77052da4a18188a34948000d6124d 100644 --- a/src/main/resources/templates/aboutUs/aboutUs.html +++ b/src/main/resources/templates/aboutUs/aboutUs.html @@ -1,5 +1,7 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> +<html xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{layout/layout}"> <head> <meta charset="UTF-8"> <title>Title</title> @@ -7,7 +9,7 @@ <script src = "/js/aboutUs/aboutUs.js"></script> </head> - <body> + <body layout:fragment="content"> <!-- Hero Section --> <section class="hero"> </section> diff --git a/src/main/resources/templates/categories/categories.html b/src/main/resources/templates/categories/categories.html index 278854b238472782c5f945af8209af643ab863b7..8a4e5917e918dd1dd2650944b8dcde296f475094 100644 --- a/src/main/resources/templates/categories/categories.html +++ b/src/main/resources/templates/categories/categories.html @@ -1,17 +1,51 @@ <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{layout/layout}"> > <head> <title>Categories</title> <link rel="stylesheet" href="/css/Categories/categories.css"> <script defer src="/js/Categories/categories.js"></script> </head> -<body> -<section > - <h2>Categories</h2> +<section class="info-database" layout:fragment="content" > + <div class="title-section"> + <h1>Community Information Database</h1> + <button id="add-new-category"> + <i class="bi bi-folder-plus"></i> + New Category + </button> + </div> <div class="card-container" id="card-container"> <!-- cards will be added here with javascript --> </div> + <div id="create-new-modal" class="create-new-modal"> + <div class="modal-content"> + <div class="modal-header"> + <div class="modal-title-desc"> + <h2>Create New Category</h2> + <p>Add a new category to organize Community information</p> + </div> + <span class="close-modal" id="closeModalBtn"><i class="bi bi-x-lg"></i></span> + </div> + <form id="category-form"> + <label for="categoryTitle">Category Name + <input type="text" id="categoryTitle" name="categoryTitle" + placeholder="eg, Health & Wellness" + required> + </label> + <label for="categoryDescription">Description + <textarea id="categoryDescription" name="categoryDescription" + placeholder="Briefly describe this category..." + rows="4" + required></textarea> + </label> + <div class="form-buttons"> + <button type="button" class="cancel-button close-modal">Cancel</button> + <button class="submit-button" type="submit">Create Category</button> + </div> + </form> + </div> + </div> </section> -</body> </html> diff --git a/src/main/resources/templates/comments/commentFragment.html b/src/main/resources/templates/comments/commentFragment.html new file mode 100644 index 0000000000000000000000000000000000000000..857d3c3eb29488b981da450a21e1d53543f3fca6 --- /dev/null +++ b/src/main/resources/templates/comments/commentFragment.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html xmlns:th="http://www.thymeleaf.org"> +<head> + <title>Comment Section</title> + <link rel="stylesheet" th:href="@{/static/css/comments/comments.css}"> +</head> +<body> + +<!-- Define a Thymeleaf fragment for comments --> +<div th:fragment="commentsFragment(postId, comments)"> + <div class="comments-section"> + <h4>Comments:</h4> + <div th:id="'comments-' + ${postId}"> + <!-- Loop through comments --> + <div th:each="comment : ${comments}"> + <div class="comment"> + <p th:text="${comment.content}"></p> + <small>By + <span th:text="${comment.username}"></span> on + <span th:text="${#temporals.format(comment.createdDate, 'dd/MM/yyyy HH:mm')}"></span> + </small> + <button th:attr="onclick='deleteComment(' + ${comment.id} + ', \'' + ${postTitle} + '\')'">Delete</button> + </div> + </div> + </div> + + <!-- Comment Form --> + <form method="post" class="comment-submission" > + <input type="hidden" name="postId" th:value="${postId}" /> + <label for="commentInput" hidden="hidden">Write your comment here</label><textarea id="commentInput" name="content" placeholder="Write a comment..." required></textarea> + <button id="commentSubmit" type="submit">Post Comment</button> + </form> + </div> +</div> +<script th:fragment="commentScript" src="/js/comments/comments.js"></script> +</body> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/error/error.html b/src/main/resources/templates/error/error.html index 0b799cd34ddc81185850e740eade7a65e00b87ce..437a32183733a97b408879864e158c49c7a1e234 100644 --- a/src/main/resources/templates/error/error.html +++ b/src/main/resources/templates/error/error.html @@ -1,11 +1,22 @@ <!DOCTYPE html> -<html lang="en"> +<html lang="en" xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{layout/layout}"> <head> <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Error Page</title> + <link rel="stylesheet" href="/error/error.css"> </head> -<body> - <h1>Error</h1> - <p th:text="${errorMessage}"></p> +<body layout:fragment="content"> +<div id="error-container"> + <h1 id="error-code">Error</h1> + <p id="error-message"></p> +</div> +// passing the error code to javascript +<script th:inline="javascript"> + window.errorCode = [[${statusCode}]] +</script> +<script src="/js/Error/error.js"></script> </body> -</html> \ No newline at end of file +</html> diff --git a/src/main/resources/templates/event/event-detail.html b/src/main/resources/templates/event/event-detail.html index bf03aceb0b58fe1780989b73a017fa60862d6962..fc6eb0d7fd8ecdfa2757dbe55b4d289deea90390 100644 --- a/src/main/resources/templates/event/event-detail.html +++ b/src/main/resources/templates/event/event-detail.html @@ -1,5 +1,7 @@ <!DOCTYPE html> -<html lang="en"> +<html xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{layout/layout}"> <head> <meta charset="UTF-8"> @@ -9,7 +11,7 @@ </head> <body> -<main id="event-detail-container"> +<main id="event-detail-container" layout:fragment="content"> <section class="event-detail-card"> <article class="image-container"> <img th:src="${event.getImageUrl()}" alt="Event Image" class="event-image"> diff --git a/src/main/resources/templates/event/event.html b/src/main/resources/templates/event/event.html index e3a8fcf25d39ac16ebb852278c75513f7fef2627..6d75e5ae5be7669e90c89de2419a49d163fdb2ae 100644 --- a/src/main/resources/templates/event/event.html +++ b/src/main/resources/templates/event/event.html @@ -1,5 +1,7 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> +<html xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{layout/layout}"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> @@ -7,7 +9,7 @@ <link rel="stylesheet" href="/css/events/event.css"> </head> - <body> + <body layout:fragment="content"> <div id="event-details" class="event-details"> <div class="event-grid"> <!-- Loop through each event and create a clickable link --> diff --git a/src/main/resources/templates/feed/feed.html b/src/main/resources/templates/feed/feed.html index d10dbbf3d62031b9cb9a94e50970c11a60b2ab6e..73c225c99675f992fa552684add08e0f4267b346 100644 --- a/src/main/resources/templates/feed/feed.html +++ b/src/main/resources/templates/feed/feed.html @@ -1,43 +1,109 @@ <!DOCTYPE html> -<html lang="en" > - <head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>Post Feed</title> - <link rel="stylesheet" href="/css/feed/feed.css"> - </head> - <body> - <div class="container"> - <div class="hero-text"> - <h1 > Welcome to the Feed Page</h1> - </div> - <!-- Sort By Filter --> - <div class="sort-container"> - <label for="sortBy">Sort By:</label> - <select id="sortBy"> - <option value="date">Date</option> - <option value="chronology">Chronology</option> - </select> - </div> +<html lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{layout/layout}"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Post Feed</title> + <link rel="stylesheet" href="/css/feed/feed.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <script defer src="/js/feed/feed.js"></script> +</head> +<section class="feed-container" layout:fragment="content"> + <div class="feed-hero"> + <h1>Community Posts Feed</h1> + </div> - <!-- Posts Container (Scrollable) --> - <div id="postFeed" class="post-feed"> - <!-- Loop through the posts list --> - <div th:each="post : ${posts}" class="post"> + <!-- posts will appear here --> + <div id="postFeed" class="post-feed"> + <!-- using template since I want to use it in javascript --> + <template id="post-template"> + <div class="post"> + <div class="author-details"> + <div class="profile-picture"> + <!--add the profile picture later --> + </div> + <div class="text-details"> + <h5 class="author"></h5> + <p class="author-title"></p> + </div> + </div> + <div class="post-details"> + <h3 class="post-title"></h3> + <p class="post-description"></p> <div class="post-image"> - <img th:src="${post.getPostImageUrl()}" alt="Post Image"> + <img src="" alt="Post Image"> + </div> + <div class="post-tags"> + <!-- Tags will be dynamically inserted --> </div> - <div class="post-content"> - <h3 class="post-title" th:text="${post.getPost_title()}"></h3> - <p class="post-description" th:text="${post.getPost_description()}"></p> - <div class="post-meta"> - <span class="author" th:text="'By ' + ${post.getPost_author()}"></span> - <span class="timestamp" th:text="${post.getPost_time()}"></span> - </div> + </div> + <div class="post-meta"> + <div class="post-actions"> + <button class="like-button"> + <i class="bi bi-hand-thumbs-up"></i> + <span class="like-count">0</span> + </button> + <button> + <i class="bi bi-chat-left"></i> + <span>35</span> + </button> + <button> + <i class="bi bi-share"></i> + </button> </div> + <span class="timestamp"></span> + </div> + </div> + </template> + </div> + + <button class="add-post" id="add-post"> + <i class="bi bi-plus"></i> + </button> + <div id="create-new-modal" class="create-new-modal"> + <div class="modal-content"> + <div class="modal-header"> + <div class="modal-title-desc"> + <h3>Create a New Post</h3> + <p>Share your thoughts, ideas, or news with the community.</p> </div> + <span class="close-modal" id="closeModalBtn"><i class="bi bi-x-lg"></i></span> </div> + <form id="post-form"> + <label for="postTitle">Post Title + <input type="text" id="postTitle" name="postTitle" + placeholder="Post Title" + minlength="3" + maxlength="100" + required> + </label> + <label for="postDescription">Description + <textarea id="postDescription" name="postDescription" + placeholder="What's on your mind?" + rows="6" + minlength="10" + maxlength="1000" + required></textarea> + </label> + + <label for="postTags">Tags + <input type="text" id="postTags" name="postTags" + placeholder="Enter tags separated by commas (e.g., news, community, event)" + pattern="^[a-zA-Z0-9\s,.-]+$" + title="Tags should be comma-separated and contain only letters, numbers, spaces, dots, and hyphens" + required> + </label> + <div class="form-buttons"> + <button class="submit-button" type="submit"> + <i class="bi bi-send"></i> + <p>Post</p> + </button> + </div> + </form> </div> - <script src="/js/scripts.js"></script> - </body> -</html> + </div> +</section> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/feed/feedWithComments.html b/src/main/resources/templates/feed/feedWithComments.html new file mode 100644 index 0000000000000000000000000000000000000000..900093ca277ba90ba6e4a326a0dba4e2cadac807 --- /dev/null +++ b/src/main/resources/templates/feed/feedWithComments.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<html xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{layout/layout}"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Post Feed</title> + <link rel="stylesheet" href="/css/feed/feed.css"> +</head> +<body > +<div class="container" layout:fragment="content"> + <div class="hero-text"> + <h1 > Welcome to the Feed Page</h1> + </div> + <!-- Sort By Filter --> + <div class="sort-container"> + <label for="sortBy">Sort By:</label> + <select id="sortBy"> + <option value="date">Date</option> + <option value="chronology">Chronology</option> + </select> + </div> + + <!-- Posts Container (Scrollable) --> + <div id="postFeed" class="post-feed"> + <!-- Loop through the posts list --> + <div th:each="post : ${posts}" class="post"> + <div class="post-image"> + <img th:src="${post.getPostImageUrl()}" alt="Post Image"> + </div> + <div class="post-content"> + <h3 class="post-title" th:text="${post.getPostTitle()}"></h3> + <p class="post-description" th:text="${post.getPostDescription()}"></p> + <div class="post-meta"> + <span class="author" th:text="'By ' + ${post.getAuthorName()}"></span> + <span class="timestamp" th:text="${post.getPostTime()}"></span> + </div> + <!-- Include Comments Fragment --> + <!-- Fetch comments for this post --> + <div th:with="comments=${@commentService.findAllCommentsByPostId(post.getPostId())}"> + <th:block th:replace="comments/commentFragment :: commentsFragment(postId=${post.getPostId()}, comments=${comments})"></th:block> + </div> + </div> + </div> + </div> +</div> +<script src="/js/scripts.js"></script> +</body> +</html> diff --git a/src/main/resources/templates/headings/headings.html b/src/main/resources/templates/headings/headings.html new file mode 100644 index 0000000000000000000000000000000000000000..590b35c4d8271c8ad49bc2983a9cda80a194dc4e --- /dev/null +++ b/src/main/resources/templates/headings/headings.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{layout/layout}"> +<head> + <title>Category Headings</title> + <link rel="stylesheet" href="/css/headings/headings.css"> + <script defer src="/js/headings/headings.js"></script> + <link rel="stylesheet" href="/css/infoHeadingFragment/infoHeadingFragment.css"> + <script src="/js/infoHeadingFragment/infoHeadingFragment.js" defer></script> +</head> +<section class="general-headings-layout" layout:fragment="content"> + <div th:replace="~{infoHeadingFragment/infoHeadingFragment :: breadcrumb}"></div> + <div class="title-section"> + <h1 id="category-name"></h1> + <div class="buttons"> + <button id="edit-mode-button">Edit</button> + <a id="addInfo"> + <button id="add-new-information"> + <i class="bi bi-folder-plus"></i> + New Information + </button> + </a> + </div> + </div> + <div id="headings-container" class="headings-container"> + <!-- i will render headings dynamically added here from js --> + </div> + <button id="delete-button" disabled>Delete</button> +</section> +</html> diff --git a/src/main/resources/templates/home/home.html b/src/main/resources/templates/home/home.html index 3c50ef256d081d0b810cf1b701cd881fdb3336a6..4eb8664b934ec3f5e0ec921ea97cf731182383de 100644 --- a/src/main/resources/templates/home/home.html +++ b/src/main/resources/templates/home/home.html @@ -6,11 +6,66 @@ <head> <meta charset="UTF-8"> <title>HOME</title> + <link rel="stylesheet" href="/css/home/home.css"> </head> <body> <section layout:fragment="content"> - <h1>POLISH COMMUNITY GROUP</h1> + <h1>Welcome to our Community</h1> + <p>Connect, Share, and Grow Together</p> + + <section class="tiles"> + <div class="tile" id="feedTile"> + <a th:href="@{/feed}" class="tileLink"> + <div class="topTile"> + <img src="/assets/navbarImages/feed.png" class="tile-logo"><span class="tile-title">Feed</span> + </div> + <p class="tile-description">Stay updated with the latest posts</p> + </a> + </div> + + <div class="tile" id="newsTile"> + <a th:href="@{/news}" class="tileLink"> + <div class="topTile"> + <img src="/assets/navbarImages/news.png" class="tile-logo"><span class="tile-title">News</span> + </div> + <p class="tile-description">Discover the latest community news</p> + </a> + </div> + + <div class="tile" id="eventsTile"> + <a th:href="@{/event}" class="tileLink"> + <div class="topTile"> + <img src="/assets/navbarImages/events.png" class="tile-logo"><span class="tile-title">Events</span> + </div> + <p class="tile-description">Join our upcoming community events</p> + </a> + </div> + + <div class="tile" id="infoTile"> + <a th:href="@{/categories}" class="tileLink"> + <div class="topTile"> + <img src="/assets/navbarImages/info.png" class="tile-logo"><span class="tile-title">Info Database</span> + </div> + <p class="tile-description">Access our community information database</p> + </a> + </div> + + <div class="tile" id="aboutTile"> + <a th:href="@{/aboutUs}" class="tileLink"> + <div class="topTile"> + <img src="/assets/navbarImages/about.png" class="tile-logo"><span class="tile-title">About Us</span> + </div> + <p class="tile-description">Learn about our mission and values</p> + </a> + </div> + + </section> + + <h1>Join Our Thriving Community Today!</h1> + <p>Connect with like-minded individuals, share your ideas, and be part of something amazing</p> + <button>Get Started</button> </section> + </body> </html> \ No newline at end of file diff --git a/src/main/resources/templates/infoHeadingFragment/infoHeadingFragment.html b/src/main/resources/templates/infoHeadingFragment/infoHeadingFragment.html index 8b803bb198bb35ef23d1977551e825bfb44490ca..bdbd4ad650ee10243ef43455c003c1d5e96ed887 100644 --- a/src/main/resources/templates/infoHeadingFragment/infoHeadingFragment.html +++ b/src/main/resources/templates/infoHeadingFragment/infoHeadingFragment.html @@ -3,7 +3,7 @@ <head> <meta charset="UTF-8"> </head> -<body> +<body layout:fragment="content"> <div id="breadcrumbContainer"> <div th:fragment="breadcrumb"> diff --git a/src/main/resources/templates/information/addInformation.html b/src/main/resources/templates/information/addInformation.html new file mode 100644 index 0000000000000000000000000000000000000000..148e4613c0b1c6443f1607647816b066a7e530b0 --- /dev/null +++ b/src/main/resources/templates/information/addInformation.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{layout/layout}"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Add Information</title> + <!-- Stylesheet for rich text editor--> + <link href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css" rel="stylesheet" /> + <script defer src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script> + + <!-- Scripts for converting markdown to html and vice-versa.--> + <script defer src="https://cdn.jsdelivr.net/npm/turndown@7.0.0/dist/turndown.min.js"></script> + <script src="https://cdn.jsdelivr.net/npm/marked@15.0.2/lib/marked.umd.min.js"></script> + + <!-- Custom scripts--> + <link rel="stylesheet" href="/css/information/AddInfoStyles.css"> + <script defer src="/js/information/addInfoScript.js"></script> + <script defer src="/js/information/infoScript.js"></script> +</head> + +<!--Removed <body> tags as we are creating each feature as a layout component--> +<section layout:fragment="content"> + <a href="#" id="backToHeadings" class="backButton"> + <strong><p><i class="bi bi-chevron-left"></i> + <span id="backArrowTitle"> Back to headings</span></p></strong> + </a> + + <!-- Using same page for both edit and add information feature--> + <h1 th:text="${formAction.contains('edit')} ? 'Edit Information' : 'Add New Information'"></h1> + + <!-- Get action method from the controller based on the operation for add/edit--> + <form th:action="@{${formAction}}" th:method="post" th:object="${newInfo}" id="addInfoForm"> + <fieldset> + <input th:field="*{informationId}" type="hidden"/> + <input th:field="*{categoryId}" type="hidden"/> + <input th:field="*{createdBy}" type="hidden"/> + <input th:field="*{createdDate}" type="hidden"/> + <input th:field="*{tagId}" type="hidden"/> + <input type="hidden" th:field="*{infoDescription}"/> + <label th:for="*{infoTitle}">Title</label> + <input type="text" th:field="*{infoTitle}"> + <div class="alert" th:errors="*{infoTitle}" th:if="${#fields.hasErrors('infoTitle')}">Title Error</div> + + <label th:for="*{shortDescription}">Short Description</label> + <textarea th:field="*{shortDescription}"></textarea> + <div class="alert" th:errors="*{shortDescription}" th:if="${#fields.hasErrors('shortDescription')}">Short description Error</div> + + <!--Rich text editor for storing formatted text --> + <label for="editor">Content</label> + <div id="editor"></div> + + <div class="alert" th:errors="*{infoDescription}" th:if="${#fields.hasErrors('infoDescription')}">Description Error</div> + </fieldset> + <div class="buttons"> + <!--Add redirection dynamically through js--> + <a href="#" id="cancelButton" class="button cancel">Cancel</a> + + <!--Change button based on the operation--> + <button id="submitButton" th:text="${formAction.contains('edit')}?'Update Information':'Add Information'" type="submit" class="submit"></button> + </div> + </form> +</section> + +</html> diff --git a/src/main/resources/templates/information/information.html b/src/main/resources/templates/information/information.html index 39b17b841ff7356284d772c66abd3c76497f55af..37c2ee57f7161670c274485e27b0052b9d992e55 100644 --- a/src/main/resources/templates/information/information.html +++ b/src/main/resources/templates/information/information.html @@ -1,31 +1,39 @@ <!DOCTYPE html> -<html lang="en"> +<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{layout/layout}"> <head> <meta charset="UTF-8"> + <!--Custom styles--> <link rel="stylesheet" href="/css/information/infoStyles.css"> - <script src="https://cdn.jsdelivr.net/npm/turndown@7.0.0/dist/turndown.min.js"></script> - <script src="https://cdn.jsdelivr.net/npm/marked@15.0.2/lib/marked.umd.min.js"></script> - <link rel="stylesheet" href="/css/infoHeadingFragment/infoHeadingFragment.css"> + + <!--Custom scripts --> <script src="/js/infoHeadingFragment/infoHeadingFragment.js" defer></script> - - <title>DB Info</title> + <script src="/js/information/infoScript.js" defer></script> + + <title th:text="${selectedInfo.getInfoTitle()}">DB Info</title> </head> - <body> - - <div th:replace="~{infoHeadingFragment/infoHeadingFragment :: breadcrumb}"></div> - <div class="container"> - <div class="head-container"> - <a href="#"><img src="/assets/back-arrow.png" alt="back-arrow"></a> - <h1 th:text="${dbInfo.getInfoTitle()}">Information Title</h1> - </div> - <main> + <!--Removed <body> tag as it is added as a component to the main layout--> + <section layout:fragment="content"> + <div th:replace="~{infoHeadingFragment/infoHeadingFragment :: breadcrumb}"></div> + <main class="container"> + <section class="head-container"> + <section class="sub-head-main-container"> + <a id="backToHeadings" href="#"><img src="/assets/back-arrow.png" alt="back-arrow"></a> + <h1 th:text="${selectedInfo.getInfoTitle()}">Information Title</h1> + </section> + <aside class="sub-head-edit-container"> + <a id="editButtonLink" th:href="@{/info/edit/{id} (id=${selectedInfo.getInformationId()})}"> + <button><strong>Edit</strong></button></a> + </aside> + </section> + <section class="main-content"> <!-- Main content section displaying the information markup --> - <section id="markupcontent" th:utext="${html}"> + <article id="markupcontent" th:utext="${html}"> <!-- Dynamic content will be inserted here --> - </section> - </main> - </div> - </body> + </article> + </section> + </main> + </section> </html> \ No newline at end of file diff --git a/src/main/resources/templates/layout/layout.html b/src/main/resources/templates/layout/layout.html index 16d6f9523fc6737a53ffa416233299cb4d2a246f..03d4db4d0ccfbe479a9ec72f65eb40cd510b0942 100644 --- a/src/main/resources/templates/layout/layout.html +++ b/src/main/resources/templates/layout/layout.html @@ -4,74 +4,108 @@ xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> <head> <meta name="viewport" CHARSET="UTF-8" content="width=device-width, initial-scale=1"> - <title>POLISH COMMUNITY GROUP</title> + + <title>Ludek Polonia Wajiska</title> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap" rel="stylesheet"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> - <link rel="stylesheet" th:fragment="layoutstyle" href="/css/layout/layout.css"> - <link rel="stylesheet" href="/css/sidebar/sidebar.css"> + <link rel="stylesheet" href="/css/comments/comments.css"> + <link rel="stylesheet" href="/css/layout/layout.css"> + </head> <body> - <header class="header" > - - <img class="logo" src="/images/logo.png"> - <h1 class="siteName">LUDEK POLONIA</h1> - <i class="bi icon bi-person-circle"></i> - - </header> - <!-- <div> - <aside layout:fragment="sidebar"></aside> - </div> --> - <div class="mainBody"> + <header class="header"> <section class="sidebar"> <nav class="sidebar"> - <a th:href="@{/profile}" class="navLink"> - <img src="/assets/userProfile.png" class = "navIcons"><span class="navText">Profile</span> - </a> - - <a th:href="@{/}" class="navLink"> - <img src="/assets/home.png" class="navIcons"><span class="navText">Home</span> - </a> - - <a th:href="@{/event}" class="navLink"> - <img src="/assets/events.png" class="navIcons"><span class="navText">Events</span> - </a> - - <a th:href="@{/catagories}" class="navLink"> - <img src="/assets/info.png" class="navIcons"><span class="navText">Catagories</span> - </a> - - <a th:href="@{/news}" class="navLink"> - <img src="/assets/news.png" class="navIcons"><span class="navText">News</span> - </a> - - <a th:href="@{/blogs}" class="navLink"> - <img src="/assets/blogs.png" class="navIcons"><span class="navText">Blogs</span> - </a> - - <a th:href="@{/aboutus}" class="navLink"> - <img src="/assets/aboutUs.png" class="navIcons"><span class="navText">About Us</span> - </a> - + <div class="navLeft"> + <a th:href="@{/}" class="logoLink"> + <img src="/assets/navbarImages/logo.png" class="logoImage"><span class="navText">Ludek Polonia Wajiska</span> + </a> + </div> + + <div class="navMiddle"> + <a th:href="@{/}" class="navLink"> + <img src="/assets/navbarImages/home.png" class="navIcons"><span class="navText">Home</span> + </a> + <a th:href="@{/feed}" class="navLink"> + <img src="/assets/navbarImages/feed.png" class="navIcons"><span class="navText">Feed</span> + </a> + <a th:href="@{/news}" class="navLink"> + <img src="/assets/navbarImages/news.png" class="navIcons"><span class="navText">News</span> + </a> + <a th:href="@{/event}" class="navLink"> + <img src="/assets/navbarImages/events.png" class="navIcons"><span class="navText">Events</span> + </a> + <a th:href="@{/categories}" class="navLink"> + <img src="/assets/navbarImages/info.png" class="navIcons"><span class="navText">Info Database</span> + </a> + <a th:href="@{/aboutUs}" class="navLink"> + <img src="/assets/navbarImages/about.png" class="navIcons"><span class="navText">About Us</span> + </a> + <a th:href="@{/contactus}" class="navLink"> + <img src="/assets/navbarImages/contact.png" class="navIcons"><span class="navText">Contact Us</span> + </a> + </div> + + <!-- Right Section: Language Selector and Profile --> + <div class="navRight"> + <!-- Language Selector --> + <div class="languageSelector"> + <img src="/assets/navbarImages/globe.png" class="navIcons"> + <select name="language"> + <option value="english">English</option> + <option value="polish">Polish</option> + </select> + </div> + + <!-- Profile --> + <a th:href="@{/profile}" class="navLink"> + <img src="/assets/navbarImages/profile.png" class="navIcons"><span class="navText">Profile</span> + </a> + </div> </nav> </section> + </header> + + <div class="mainBody"> + <main layout:fragment="content" class="content"></main> </div> + + + <footer class="footer"> - <p class="footerCompanyName">© LUDEK PCG ltd. All rights reserved.</p> - <div class="footerContacts"> - <dl class="contactInfo"> - <dt>Address:</dt> - <dd>Address, city, CF1 0PX</dd> - </dl> - <dl class="contactInfo"> - <dt>email:</dt> - <dd><a href="mailto:#">example@yahoo.com</a></dd> - </dl> - <dl class="contactInfo"> - <dt>phone:</dt> - <dd>+44 1234 876548</dd> - </dl> + <div class="footer-section about"> + <h3 class="footerTitle">Polish Community Website</h3> + <p class="footerText">Connecting people, sharing ideas, and building a better future together.</p> + </div> + + <div class="footer-section links"> + <h3 class="footerTitle">Quick Links</h3> + <ul class="footerLinks"> + <li><a th:href="@{/aboutus}" class="footerLink">About Us</a></li> + <li><a th:href="@{/contact}" class="footerLink">Contact</a></li> + <li><a th:href="@{/}" class="footerLink">Privacy Policy</a></li> + <li><a th:href="@{/}" class="footerLink">Terms of Service</a></li> + </ul> + </div> + + <div class="footer-section connect"> + <h3 class="footerTitle">Connect with us</h3> + <div class="social-icons"> + <a href="https://www.facebook.com/LudekPCG" class="social-icon"><img src="/assets/navbarImages/facebook.png" alt="Facebook"></a> + <a href="#" class="social-icon"><img src="/assets/navbarImages/twitter.png" alt="Twitter"></a> + <a href="#" class="social-icon"><img src="/assets/navbarImages/instagram.png" alt="Instagram"></a> + </div> + </div> + + <div class="footer-section copyright"> + <p class="footerCompanyName">© LUDEK PCG ltd. All rights reserved.</p> </div> </footer> + <script th:replace="~{comments/commentFragment::commentScript}"></script> + </body> </html> \ No newline at end of file diff --git a/src/main/resources/templates/login/login.html b/src/main/resources/templates/login/login.html index 7d27a3d39b1df853eabdd602fe8b2efea5d2849e..e1a65281fda0503ba3b4eea6f5459daebaf5dd6a 100644 --- a/src/main/resources/templates/login/login.html +++ b/src/main/resources/templates/login/login.html @@ -1,10 +1,12 @@ <!DOCTYPE html> -<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"> +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" +xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" +layout:decorate="~{layout/layout}">> <head> <title>Login Form </title> <link rel="stylesheet" th:fragment="loginstyle" href="/css/login/login.css"> </head> -<body> +<body layout:fragment="content"> <form th:action="@{/login}" method="post"> <!-- option to login via user name --> diff --git a/src/main/resources/templates/news/addNews.html b/src/main/resources/templates/news/addNews.html new file mode 100644 index 0000000000000000000000000000000000000000..785e2cd0ac7c1a8db4beacd88203045f49939197 --- /dev/null +++ b/src/main/resources/templates/news/addNews.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/layout}"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Add News</title> + <link rel="stylesheet" href="/css/news/addNews.css"> + <script src="/js/news/addNews.js" defer></script> +</head> + <section th:fragment="addNewsForm"> + + <!-- Modal structure --> + <div id="modal" class="modal"> + <div class="overlay" id="overlay"></div> + <div class="modal-content" > + <span class="close-btn">×</span> + <form id="newsForm" th:action="@{/news/add}" th:method="post" th:object="${news}"> + <h2>Create News</h2> + + <div class="form-group"> + <label th:for="newsTitle">News Title</label> + <input type="text" id="newsTitle" name="newsTitle" th:field="*{news_title}" placeholder="Enter news title" required> + </div> + + <div class="form-group"> + <label th:for="newsDescription">Description</label> + <textarea id="newsDescription" name="newsDescription" th:field="*{news_summary}" placeholder="Briefly describe this news article..." rows="4" required></textarea> + </div> + + <div class="form-group"> + <label th:for="newsImageUrl">Image URL (optional)</label> + <input type="text" id="newsImageUrl" name="newsImageUrl" th:field="*{news_image_url}" placeholder="Enter image URL"> + </div> + + <div class="form-group"> + <label th:for="newsSource">Source</label> + <input type="text" id="newsSource" name="newsSource" th:field="*{news_source}" placeholder="Enter source" required> + </div> + + <div class="form-group"> + <label th:for="newsLink">News Link</label> + <input type="text" name="newsLink" th:field="*{news_link}" placeholder="Enter Link" required> + </div> + + <div class="form-buttons"> + <button type="button" class="cancelButton" id="cancelButton">Cancel</button> + <button class="submitButton" type="submit">Create News</button> + </div> + </form> + </div> + </div> + + </section> +</html> diff --git a/src/main/resources/templates/news/editNews.html b/src/main/resources/templates/news/editNews.html new file mode 100644 index 0000000000000000000000000000000000000000..9719d75d7d30f2fa5af6fcc0ca25f692b93cf017 --- /dev/null +++ b/src/main/resources/templates/news/editNews.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + + <link rel="stylesheet" href="/css/news/editNewsStyle.css"> + <script src="/js/news/editNewsScript.js"></script> + +</head> +<body> + <h1>Edit or Delete News</h1> + <form th:action="@{/editNews/{id}(id=${news.news_id})}" th:object="${news}" method="post" onsubmit="return validateForm(event)"> + <!-- Hidden field to hold the news ID --> + <input type="hidden" th:field="*{news_id}" /> + <input type="hidden" th:field="*{user_id}" /> + + <!-- Title --> + <div> + <label for="news_title">Title:</label> + <input type="text" id="news_title" th:field="*{news_title}" /> + </div> + + <!-- Summary --> + <div> + <label for="news_summary">Summary:</label> + <textarea id="news_summary" th:field="*{news_summary}" rows="4"></textarea> + </div> + + <!-- Source --> + <div> + <label for="news_source">Source:</label> + <input type="text" id="news_source" th:field="*{news_source}" /> + </div> + + <!-- Link --> + <div> + <label for="news_link">Link:</label> + <input type="url" id="news_link" th:field="*{news_link}" /> + </div> + + <!-- Image URL --> + <div> + <label for="news_image_url">Image URL:</label> + <input type="text" id="news_image_url" th:field="*{news_image_url}" /> + </div> + + + <!-- Upload Date --> + <div> + <label for="news_upload_date">Upload Date:</label> + <input type="date" id="news_upload_date" th:field="*{news_upload_date}" /> + </div> + + <!-- Button Group --> + <div class="button-group"> + <a th:href="@{/news}" class="linkCancel"><button type="button" class="btnCancel">Cancel</button></a> + + <div class="buttonsRight"> + <!-- Edit Button --> + <button type="submit" name="action" value="edit" class="btnSave">Save Changes</button> + + <!-- Delete Button --> + <button type="submit" name="action" value="delete" class="btnDelete" onclick="return confirmDelete(event)">Delete</button> + </div> + + </div> + </form> +</body> +</html> + diff --git a/src/main/resources/templates/news/newsList.html b/src/main/resources/templates/news/newsList.html index 3cb277a1b19d4e3c891b9d1f4683529924a8a08a..835352cd6f0341e4a4d2a5016e760558707b870a 100644 --- a/src/main/resources/templates/news/newsList.html +++ b/src/main/resources/templates/news/newsList.html @@ -1,45 +1,64 @@ <!DOCTYPE html> -<html lang="en"> -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>News Page</title> - <link rel="stylesheet" href="/css/news/newsStyles.css"> -</head> -<body class="content"> - <header class="heading"> - <h1>Latest News</h1> - </header> - <main class="news-container"> - <!-- Main news card --> - <div class="news-card main-card"> - <div class="main-card-container"> - <div class="text-container"> - <a th:href="${newsList[0].getNews_link()}"><h2 th:text="${newsList[0].getNews_title()}"></h2></a> - <p class="summary" th:text="${newsList[0].getNews_summary()}"></p> +<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{layout/layout}"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>News Page</title> + <link rel="stylesheet" href="/css/news/newsStyles.css"> + <link rel="stylesheet" href="/css/news/addNews.css"> + <script src="/js/news/addNews.js" defer></script> + <script src="/js/news/newsScripts.js" defer></script> + </head> + <section layout:fragment="content" > + <div class="general-headings-layout"> + + <h1>Community News</h1> + + + <button onclick="openNewsForm()" id="openModalBtn" class="openModalBtn">Add News</button> + + </div> + <main class="news-container"> + <!-- Main news card --> + <div class="news-card main-card"> + <div class="main-card-container"> + <div class="text-container"> + <a th:href="${newsList[0].getNews_link()}"><h2 th:text="${newsList[0].getNews_title()}"></h2></a> + <p class="summary" th:text="${newsList[0].getNews_summary()}"></p> + </div> + <div class="image-container"> + <img th:src="${newsList[0].getNews_image_url()}" alt="News Image"> + </div> </div> - <div class="image-container"> - <img th:src="${newsList[0].getNews_image_url()}" alt="News Image"> + <div class="card-footer"> + <p class="source">Source: <span th:text="${newsList[0].getNews_source()}"></span></p> + <a th:href="@{/editNews/{id}(id=${newsList[0].getNews_id()})}" class="modify-btn modify-link">Modify</a> + <p class="date" th:text="${newsList[0].getNews_upload_date()}"></p> </div> </div> - <div class="card-footer"> - <p class="source">Source: <span th:text="${newsList[0].getNews_source()}"></span></p> - <p class="date" th:text="${newsList[0].getNews_upload_date()}"></p> - </div> - </div> - <!-- Smaller news cards --> - <div class="small-cards"> - <div class="news-card small-card" th:each="news : ${newsList}" th:if="${newsStat.index != 0}"> - <a th:href="${news.getNews_link()}"><h3 th:text="${news.getNews_title()}"></h3></a> - <p class="summary" th:text="${news.getNews_summary()}"></p> - <div class="card-footer"> - <p class="source">Source: <span th:text="${news.getNews_source()}"></span></p> - <p class="date" th:text="${news.getNews_upload_date()}"></p> + <!-- Smaller news cards --> + <div class="small-cards"> + <div class="news-card small-card" th:each="news,newsStat : ${newsList}" th:if="${newsStat.index != 0}"> + <a th:href="${news.getNews_link()}"><h3 th:text="${news.getNews_title()}"></h3></a> + <p class="summary" th:text="${news.getNews_summary()}"></p> + <div class="card-footer"> + <p class="source">Source: <span th:text="${news.getNews_source()}"></span></p> + <p class="date" th:text="${news.getNews_upload_date()}"></p> + <a th:href="@{/editNews/{id}(id=${news.getNews_id()})}" class="modify-btn modify-link">Modify</a> + </div> </div> </div> - </div> - </main> - <script src="/js/news/newsScripts.js"></script> -</body> -</html> \ No newline at end of file + <!-- Add News Button --> + <div> + + <!-- Empty section to be replaced by the fragment --> + <section id="newsForm" style="display: none;" > + <!-- Fragment will be loaded here dynamically --> + <div class="news-card small-card" th:replace="news/addNews :: addNewsForm"></div> + </section> + </div> + </main> + </section> +</html> diff --git a/src/main/resources/templates/register/register.html b/src/main/resources/templates/register/register.html index 6d5b11d323493fe4c6c15676e883665a4e0a0254..87bd84f6a24a084bbec0c49992132737da7a2924 100644 --- a/src/main/resources/templates/register/register.html +++ b/src/main/resources/templates/register/register.html @@ -1,5 +1,7 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> +<html xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{layout/layout}"> <head> <title>Register</title> @@ -10,33 +12,64 @@ <body> <h1>Create an Account</h1> - <div class="registerContainer"> + <div class="registerContainer" layout:fragment="content"> <!-- The form is bound to the 'user' object --> - <form th:action="@{/register}" th:method="post" th:object="${user}" class="registerForm" onsubmit="return registerValidate()"> + <form th:action="@{/register}" th:method="post" th:object="${user}" class="registerForm"> <div class="registerField"> <label for="fullname">Full Name</label> <input type="text" id="fullname" th:field="*{fullname}" required placeholder="Full Name"> + <span class="error"></span> </div> <div class="registerField"> <label for="email">Email</label> <input type="email" id="email" th:field="*{email}" required placeholder="Email"> + <span class="error"></span> </div> <div class="registerField"> <label for="password">Password</label> <input type="password" id="password" th:field="*{password}" required placeholder="Password"> - </div> + <div class="strength-container"> + <progress id="strengthBar" value="0" max="100"></progress> + <span id="strengthMessage"></span> + </div> + <span class="error"></span> + </div> <div class="registerField"> <label for="confirmPassword">Confirm Password</label> <input type="password" id="confirmPassword" required placeholder="Confirm Password"> + <span class="error"></span> + </div> + + <div class="registerField"> + <label for="dob">Date of Birth:</label> + <input type="date" id="dob" name="dob" required> + </div> + + <div class="registerField"> + <label for="roleId">Role</label> + <select name="roleId"> + <option th:each="role : ${roles}" th:value="${role.id}" th:text="${role.name}"></option> + </select> + </div> + + <div class="termsField"> + <input type="checkbox" id="agreeTerms" required> + <label for="agreeTerms"> + I agree to the <a href="/terms" class="termsLink" target="_blank">Terms and Conditions</a> + </label> + </div> + + <div class="registerButtonContainer"> + <button type="submit" class="registerButton">Sign Up</button> </div> - <div> - <button type="submit">Sign Up</button> + <div class="alreadyAccount"> + <p>Already have an account? <a th:href="@{/login}" class="loginLink">Log in here</a>.</p> </div> </form> diff --git a/src/main/resources/users.sql b/src/main/resources/users.sql deleted file mode 100644 index d0fa7c822adc804f67769a03d40487b7fb55de22..0000000000000000000000000000000000000000 --- a/src/main/resources/users.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE users ( - id INT AUTO_INCREMENT PRIMARY KEY, - email VARCHAR(255) NOT NULL UNIQUE, - password VARCHAR(255) NOT NULL, - role VARCHAR(50), - fullname VARCHAR(255) NOT NULL -); \ No newline at end of file diff --git a/src/test/java/polish_community_group_11/polish_community/SearchResultTest.java b/src/test/java/polish_community_group_11/polish_community/SearchResultTest.java index 851bcc61ca13f97fa367635c05ea6fab6b7a5ee3..8b137891791fe96927ad78e64b0aad7bded08bdc 100644 --- a/src/test/java/polish_community_group_11/polish_community/SearchResultTest.java +++ b/src/test/java/polish_community_group_11/polish_community/SearchResultTest.java @@ -1,43 +1 @@ -package polish_community_group_11.polish_community; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import polish_community_group_11.polish_community.information.models.DBInfo; -import polish_community_group_11.polish_community.information.models.SearchResult; - -import static org.assertj.core.api.Assertions.assertThat; - -@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment. RANDOM_PORT) -public class SearchResultTest { - @Autowired - TestRestTemplate restTemplate; - - @Test - void whenSearchingWithUnknownTermThenReturnsNoResults() { - - // When we ask for all articles - ResponseEntity<SearchResult[]> response = restTemplate.getForEntity("/searchresults?q=unknown", SearchResult[].class); - - // Then we get a success response - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - // and no articles - assertThat(response.getBody()).isEmpty(); - } - - @Test - void whenSearchingWithKnownTermThenReturnSomeResults() { - - // When we ask for all articles - ResponseEntity<SearchResult[]> response = restTemplate.getForEntity("/searchresults?q=finance", SearchResult[].class); - - // Then we get a success response - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - // and some articles - assertThat(response.getBody()).hasSizeGreaterThan(0); - } - -} diff --git a/src/test/java/polish_community_group_11/polish_community/comments/CommentControllerTest.java b/src/test/java/polish_community_group_11/polish_community/comments/CommentControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..902ba2b4fa98dafd3551d123597f41bc41316275 --- /dev/null +++ b/src/test/java/polish_community_group_11/polish_community/comments/CommentControllerTest.java @@ -0,0 +1,42 @@ +package polish_community_group_11.polish_community.comments; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import polish_community_group_11.polish_community.comments.models.Comment; +import polish_community_group_11.polish_community.comments.services.CommentService; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment. RANDOM_PORT) +@AutoConfigureMockMvc +class CommentControllerTest { + @Autowired + MockMvc mockMvc; + @Autowired + CommentService commentService; + @Test + @WithUserDetails("user@email.com") + void whenUserPublishesACommentThenTheCommentIsSaved() throws Exception { + // given a user is logged in + + // when they add a comment to a post + mockMvc.perform(MockMvcRequestBuilders.post("/feed/comments/comments/publish") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"content\":\"This is really great and meaningful\", \"postId\":1}") ) + .andExpect(MockMvcResultMatchers.status().is2xxSuccessful()); + + // then the comment exists in the database + List<Comment> comments = commentService.findAllCommentsByPostId(1); + assertThat(comments).hasSizeGreaterThan(0) + .anySatisfy(comment -> assertThat(comment.getContent()).isEqualTo("This is really great and meaningful")); + } +} \ No newline at end of file diff --git a/src/test/java/polish_community_group_11/polish_community/information/TestInformation.java b/src/test/java/polish_community_group_11/polish_community/information/TestInformation.java index cc4756b203fccdefbdc841638d7a67eabfd31067..8b137891791fe96927ad78e64b0aad7bded08bdc 100644 --- a/src/test/java/polish_community_group_11/polish_community/information/TestInformation.java +++ b/src/test/java/polish_community_group_11/polish_community/information/TestInformation.java @@ -1,16 +1 @@ -package polish_community_group_11.polish_community.information; -import org.junit.jupiter.api.BeforeAll; -import polish_community_group_11.polish_community.information.dao.InformationRepositoryImpl; -import polish_community_group_11.polish_community.information.models.DBInfo; -import polish_community_group_11.polish_community.information.services.InformationService; -import polish_community_group_11.polish_community.information.services.InformationServiceImpl; - - -import java.util.List; - -import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; - -public class TestInformation { - -} diff --git a/src/test/java/polish_community_group_11/polish_community/news/AddNewsTest.java b/src/test/java/polish_community_group_11/polish_community/news/AddNewsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..202c31693b4d3335d7b8b943c944107169c9b2fc --- /dev/null +++ b/src/test/java/polish_community_group_11/polish_community/news/AddNewsTest.java @@ -0,0 +1,68 @@ +package polish_community_group_11.polish_community.news; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import polish_community_group_11.polish_community.news.models.NewsImpl; +import polish_community_group_11.polish_community.news.services.NewsService; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; + +@SpringBootTest +@AutoConfigureMockMvc +public class AddNewsTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private NewsService newsService; + + private NewsImpl validNews; + private NewsImpl invalidNews; + + @BeforeEach + public void setup() { + validNews = new NewsImpl(); + validNews.setNews_title("Valid News Title"); + validNews.setNews_summary("Summary of valid news"); + validNews.setNews_link("https://www.msn.com/en-in/news/India/currency-notes-in-rajya-sabha-diversion-tactics-alleges-congress-bjp-seeks-probe/ar-AA1vnPFP?ocid=BingNewsSerp"); + } + + @Test + public void testAddNewsValid() throws Exception { + // Mock the NewsService to simulate saving news + doNothing().when(newsService).addNews(any(NewsImpl.class)); + + // Perform the POST request for adding valid news + mockMvc.perform(post("/news/add") + .param("news_title", validNews.getNews_title()) + .param("news_summary", validNews.getNews_summary()) + .param("news_link", validNews.getNews_link())) + .andExpect(status().is3xxRedirection()) // Expect redirect (successful) + .andExpect(view().name("redirect:/news")) // Should redirect to /news + .andExpect(model().attributeDoesNotExist("news")); // No validation errors on "news" + + // Verify that the service method was called once + verify(newsService, times(1)).addNews(any(NewsImpl.class)); + } +} diff --git a/src/test/java/polish_community_group_11/polish_community/news/NewsTest.java b/src/test/java/polish_community_group_11/polish_community/news/NewsTest.java index 68b6d088a8d50349549e6526ae769b82bb9a6878..8b137891791fe96927ad78e64b0aad7bded08bdc 100644 --- a/src/test/java/polish_community_group_11/polish_community/news/NewsTest.java +++ b/src/test/java/polish_community_group_11/polish_community/news/NewsTest.java @@ -1,64 +1 @@ -package polish_community_group_11.polish_community.news; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import com.fasterxml.jackson.core.type.TypeReference; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.test.web.servlet.setup.SharedHttpSessionConfigurer; -import org.springframework.web.context.WebApplicationContext; -import polish_community_group_11.polish_community.news.models.News; - -import java.util.Arrays; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@SpringBootTest -@AutoConfigureMockMvc -public class NewsTest { - @Autowired - private MockMvc mockMvc; - @Autowired - private WebApplicationContext webApplicationContext; - @Autowired - private ObjectMapper objectMapper; - private List<News> newsList; - - @BeforeEach() - public void setup(){ - mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext) - .apply(SharedHttpSessionConfigurer.sharedHttpSession()) - .build(); - } - - @Test - public void checkSuccessResponseForNews() throws Exception { - MvcResult mvcResult = mockMvc.perform(get("/news")) - .andExpect(status().isOk()) - .andReturn(); - String content = mvcResult.getResponse().getContentAsString(); - News[] response = objectMapper.readValue(content, News[].class); - newsList = Arrays.asList(response); - - // To check the api returns a successful response or HTTP 200 Ok - assertTrue(content.contains("\"isStatus\":true")); - } - - @Test - public void checkIfNewsListIsNotEmptyOrNull() throws Exception { - // Check to ensure news list does not return a null value - assertNotNull(newsList); - - // To check the news list shouldn't be empty - assertTrue(!newsList.isEmpty()); - } -} diff --git a/src/test/java/polish_community_group_11/polish_community/register/RegisterTest.java b/src/test/java/polish_community_group_11/polish_community/register/RegisterTest.java new file mode 100644 index 0000000000000000000000000000000000000000..135cad41d80e0c79de7d4b9dfabdd7f80f3a2158 --- /dev/null +++ b/src/test/java/polish_community_group_11/polish_community/register/RegisterTest.java @@ -0,0 +1,59 @@ +package polish_community_group_11.polish_community.register; + +import polish_community_group_11.polish_community.register.dao.UserRepository; +import polish_community_group_11.polish_community.register.services.UserServiceImpl; // Assuming UserServiceImpl is the correct implementation +import polish_community_group_11.polish_community.register.models.User; + +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.time.LocalDate; +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class RegisterTest { + + @Mock + private UserRepository userRepository; // Mocking the UserRepository + + @InjectMocks + private UserServiceImpl userService; // Injecting mock into the service + + @Test + public void testFindAllUsers() { + System.out.println("Starting testFindAllUsers..."); + // Arrange: Create a mock user using setters + List<User> mockUsers = new ArrayList<>(); + User user = new User(); + user.setId(1); + user.setEmail("test@example.com"); + user.setPassword("password"); + user.setFullname("John Doe"); + user.setDateOfBirth(LocalDate.of(1990, 1, 1)); + user.setRoleId(1); + + mockUsers.add(user); + + // Mock the findAllUsers method to return the mock users + when(userRepository.findAllUsers()).thenReturn(mockUsers); + + // Act: Call the service method + List<User> users = userService.findAllUsers(); + System.out.println("Returned users: " + users); + + // Assert: Verify that the result is not null, contains the expected users, and is not empty + assertNotNull(users); + assertFalse(users.isEmpty()); + assertEquals(1, users.size()); // Expecting 1 user + + System.out.println("Test completed successfully. Found " + users.size() + " users."); + } +}