diff --git a/build.gradle b/build.gradle index 0eed974f7a02792eda7daacc68a5c30486d188c5..a85111aa20fda831624973f0582e0048cc5e7887 100644 --- a/build.gradle +++ b/build.gradle @@ -27,8 +27,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // Thymeleaf dependency - - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // Thymeleaf dependency + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.2.RELEASE' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-json' @@ -59,7 +58,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-jdbc' 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/admin/controllers/AdminApiController.java b/src/main/java/polish_community_group_11/polish_community/admin/controllers/AdminApiController.java new file mode 100644 index 0000000000000000000000000000000000000000..9e2b2728b76b02971f0ae41ca6932e4563b344bc --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/admin/controllers/AdminApiController.java @@ -0,0 +1,83 @@ +package polish_community_group_11.polish_community.admin.controllers; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; +import polish_community_group_11.polish_community.admin.services.AdminService; +import polish_community_group_11.polish_community.register.models.User; +import polish_community_group_11.polish_community.register.services.UserService; + +@RestController +public class AdminApiController { + private final AdminService adminService; + private final UserService userService; + private Authentication authentication; + + public AdminApiController(AdminService adminService, UserService userService) { + this.adminService = adminService; + this.userService = userService; + } + + @PutMapping("/admin/edit/{user_id}/role") + public ResponseEntity<Void> changeUserRole(@PathVariable("user_id") int user_id, + @RequestBody String role_name) { + authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication.getAuthorities().stream().anyMatch( + a -> a.getAuthority().equals("ROLE_ADMIN"))) { + if (role_name == null || role_name.trim().isEmpty()) { + return ResponseEntity.badRequest().build(); + } + try { + User user = userService.findById(user_id); + if (user == null) { + return ResponseEntity.notFound().build(); + } + adminService.updateUserRole(user, role_name); + return ResponseEntity.ok().build(); + } catch (Exception e) { + return ResponseEntity.internalServerError().build(); + } + } else { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + } + + @PutMapping("/admin/edit/{user_id}/enabled") + public ResponseEntity<Void> enableOrDisableUser( + @PathVariable("user_id") int user_id, @RequestParam Boolean enabled) { + authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication.getAuthorities().stream().anyMatch( + a -> a.getAuthority().equals("ROLE_ADMIN"))) { + try { + User user = userService.findById(user_id); + if (user == null) { + return ResponseEntity.notFound().build(); // User not found + } + adminService.enableOrDisableUser(user, enabled); + return ResponseEntity.ok().build(); + } catch (IllegalArgumentException e) { + // Invalid boolean or other illegal arguments + return ResponseEntity.badRequest().build(); + } catch (Exception e) { + return ResponseEntity.internalServerError().build(); + } + } else { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + } + + @GetMapping("/admin/get/{user_id}") + public ResponseEntity<User> getUser(@PathVariable("user_id") int user_id) { + try { + User user = userService.findById(user_id); + if (user == null) { + return ResponseEntity.notFound().build(); // User not found + } + return ResponseEntity.ok(user); + } catch (Exception e) { + return ResponseEntity.badRequest().build(); + } + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/admin/controllers/AdminController.java b/src/main/java/polish_community_group_11/polish_community/admin/controllers/AdminController.java new file mode 100644 index 0000000000000000000000000000000000000000..a892a06f9879d557dc957c944746ef8124be010a --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/admin/controllers/AdminController.java @@ -0,0 +1,52 @@ +package polish_community_group_11.polish_community.admin.controllers; + +import org.apache.coyote.BadRequestException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +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.admin.services.AdminService; +import polish_community_group_11.polish_community.register.services.RoleService; + +import java.sql.SQLException; + +@Controller +public class AdminController { + private final AdminService adminService; + private final RoleService roleService; + private Authentication authentication; + public AdminController(AdminService adminService, RoleService roleService) { + this.adminService = adminService; + this.roleService = roleService; + } + + @GetMapping("admin/userInfo") + public ModelAndView getFirstAdminUsers() throws SQLException, BadRequestException { + ModelAndView mav = new ModelAndView("admin/userManage"); + authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication.getAuthorities().stream().anyMatch( + a -> a.getAuthority().equals("ROLE_ADMIN"))) + { + mav.addObject("usersAdminInfo",adminService.getUserManagementInfo()); + mav.addObject("roles",roleService.findAllRoles()); + } + else{ + throw new BadRequestException("User is not admin"); + } + return mav; + } + @GetMapping("admin") + public ModelAndView getAdminDashboard() throws SQLException, BadRequestException { + ModelAndView mav = new ModelAndView("admin/adminBoard"); + authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication.getAuthorities().stream().anyMatch( + a -> a.getAuthority().equals("ROLE_ADMIN"))){ + mav.addObject("adminBoard",adminService.getBoardManagementInfo()); + } + else{ + throw new BadRequestException("User is not admin"); + } + return mav; + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/admin/dao/AdminRepository.java b/src/main/java/polish_community_group_11/polish_community/admin/dao/AdminRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..673b304145dacd2dae6b05a79cb5a6eb63290a62 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/admin/dao/AdminRepository.java @@ -0,0 +1,14 @@ +package polish_community_group_11.polish_community.admin.dao; + +import polish_community_group_11.polish_community.admin.models.AdminBoard; +import polish_community_group_11.polish_community.admin.models.ManageUser; +import polish_community_group_11.polish_community.register.models.User; + +import java.sql.SQLException; +import java.util.List; + +public interface AdminRepository { + List<ManageUser> getUserManagementInfo() throws SQLException; + AdminBoard getBoardManagementInfo() throws SQLException; + int updateUserRole(User user); +} diff --git a/src/main/java/polish_community_group_11/polish_community/admin/dao/AdminRepositoryImpl.java b/src/main/java/polish_community_group_11/polish_community/admin/dao/AdminRepositoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..c768512fc24a3cae024048f541092bbc3c84af98 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/admin/dao/AdminRepositoryImpl.java @@ -0,0 +1,116 @@ +package polish_community_group_11.polish_community.admin.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.RowMapper; +import org.springframework.stereotype.Repository; +import polish_community_group_11.polish_community.admin.models.AdminBoard; +import polish_community_group_11.polish_community.admin.models.ManageUser; +import polish_community_group_11.polish_community.register.models.User; + +import java.sql.SQLException; +import java.time.DateTimeException; +import java.util.List; + +@Repository +public class AdminRepositoryImpl implements AdminRepository { + private JdbcTemplate jdbc; // JdbcTemplate instance for performing database operations + private RowMapper<ManageUser> adminManageUserMapper; // RowMapper to map rows of the database result set to Manage User objects + private RowMapper<AdminBoard> adminBoardMapper; + public AdminRepositoryImpl(JdbcTemplate aJdbc){ + this.jdbc = aJdbc; + setAdminManageUserMapper(); + setAdminBoardMapper(); + } + + public void setAdminManageUserMapper(){ + adminManageUserMapper = (rs, i) ->{ + return new ManageUser( + rs.getInt("id"), + rs.getString("fullname"), + rs.getString("email"), + rs.getBoolean("enabled"), + rs.getInt("role_id"), + rs.getString("role_name") + ); + }; + } + public void setAdminBoardMapper(){ + adminBoardMapper = (rs, i) ->{ + return new AdminBoard( + rs.getInt("total_users"), + rs.getInt("total_posts"), + rs.getInt("upcoming_events"), + rs.getInt("new_comments") + ); + }; + } + + public List<ManageUser> getUserManagementInfo() throws SQLException { + String sql="select u.id, u.fullname, u.email, u.enabled,u.role_id, rol.role_name from users u" + + " left join user_roles u_rol" + + " on u.id = u_rol.user_id" + + " join roles rol" + + " on u.role_id = rol.id" + + " order by u.id asc"; +// + +// " limit 10"; + List<ManageUser> users=null; + try{ + users=jdbc.query(sql, adminManageUserMapper); + } + 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("Some unexpected database error occured."); + } + return users; + } + + public AdminBoard getBoardManagementInfo() throws SQLException { + String sql="SELECT" + + "(SELECT COUNT(id) FROM users) AS total_users," + + "(SELECT COUNT(post_id) FROM feed) AS total_posts," + + "(SELECT COUNT(event_id) FROM event WHERE event_date > CURRENT_DATE) AS upcoming_events," + + "(SELECT COUNT(id) FROM comment WHERE DATEDIFF(CURRENT_DATE, created_date) <= 3) AS new_comments"; + AdminBoard dashboard=null; + try{ + dashboard=jdbc.queryForObject(sql, adminBoardMapper); + } + 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("Some unexpected database error occured."); + } + return dashboard; + } + + public int updateUserRole(User user){ + // Validate input to ensure no null values are passed + if (user == null || user.getId() <= 0 || user.getRoleId() <= 0) { + throw new IllegalArgumentException("Invalid user or role details."); + } + // SQL query for updating the user's role and enabled status + String sql = "UPDATE users SET role_id = ?, enabled = ? WHERE id = ?"; + + try { + // Perform the update operation + int rowsUpdated = jdbc.update(sql, user.getRoleId(), user.getEnabled(), user.getId()); + return rowsUpdated; + } catch (DataAccessException e) { + throw new RuntimeException("Database error occurred while updating user role.", e); + } + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/admin/models/AdminBoard.java b/src/main/java/polish_community_group_11/polish_community/admin/models/AdminBoard.java new file mode 100644 index 0000000000000000000000000000000000000000..d26e64d4c47cbc996691ff7f7283dd8e2031831d --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/admin/models/AdminBoard.java @@ -0,0 +1,15 @@ +package polish_community_group_11.polish_community.admin.models; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AdminBoard { + private int totalNoOfUsers; + private int totalPosts; + private int upcomingEvents; + private int newComments; +} diff --git a/src/main/java/polish_community_group_11/polish_community/admin/models/ManageUser.java b/src/main/java/polish_community_group_11/polish_community/admin/models/ManageUser.java new file mode 100644 index 0000000000000000000000000000000000000000..154adb4de538e16958c18895c766c5ca242d1a63 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/admin/models/ManageUser.java @@ -0,0 +1,17 @@ +package polish_community_group_11.polish_community.admin.models; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ManageUser { + private int id; + private String fullName; + private String email; + private Boolean enabled; + private int role_id; + private String role; +} diff --git a/src/main/java/polish_community_group_11/polish_community/admin/services/AdminService.java b/src/main/java/polish_community_group_11/polish_community/admin/services/AdminService.java new file mode 100644 index 0000000000000000000000000000000000000000..f25075d9f267fcaa3ba1bc5a079ca33c79ee5a70 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/admin/services/AdminService.java @@ -0,0 +1,15 @@ +package polish_community_group_11.polish_community.admin.services; + +import polish_community_group_11.polish_community.admin.models.AdminBoard; +import polish_community_group_11.polish_community.admin.models.ManageUser; +import polish_community_group_11.polish_community.register.models.User; + +import java.sql.SQLException; +import java.util.List; + +public interface AdminService { + List<ManageUser> getUserManagementInfo() throws SQLException; + AdminBoard getBoardManagementInfo() throws SQLException; + void updateUserRole(User user, String roleName); + void enableOrDisableUser(User user, Boolean enabled); +} diff --git a/src/main/java/polish_community_group_11/polish_community/admin/services/AdminServiceImpl.java b/src/main/java/polish_community_group_11/polish_community/admin/services/AdminServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7952fb684302ba68bebf6218585342c512253b9 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/admin/services/AdminServiceImpl.java @@ -0,0 +1,42 @@ +package polish_community_group_11.polish_community.admin.services; + +import org.springframework.stereotype.Service; +import polish_community_group_11.polish_community.admin.dao.AdminRepository; +import polish_community_group_11.polish_community.admin.models.AdminBoard; +import polish_community_group_11.polish_community.admin.models.ManageUser; +import polish_community_group_11.polish_community.register.models.User; + +import java.sql.SQLException; +import java.util.List; + +@Service +public class AdminServiceImpl implements AdminService { + private final AdminRepository adminRepository; + + public AdminServiceImpl(AdminRepository adminRepository) { + this.adminRepository = adminRepository; + } + public List<ManageUser> getUserManagementInfo() throws SQLException { + return adminRepository.getUserManagementInfo(); + } + public AdminBoard getBoardManagementInfo() throws SQLException{ + return adminRepository.getBoardManagementInfo(); + } + + public void updateUserRole(User user, String roleName){ + int newRoleId=roleName.toLowerCase().contains("admin")?1:2; + boolean needsUpdate=user.getRoleId()==newRoleId?false:true; + if(needsUpdate){ + user.setRoleId(newRoleId); + adminRepository.updateUserRole(user); + } + } + + public void enableOrDisableUser(User user, Boolean enabled){ + boolean needsUpdate=(enabled==user.getEnabled())?false:true; + if(needsUpdate){ + user.setEnabled(enabled); + adminRepository.updateUserRole(user); + } + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/contact/Controllers/Contact.java b/src/main/java/polish_community_group_11/polish_community/contact/Controllers/Contact.java new file mode 100644 index 0000000000000000000000000000000000000000..baa02a3ac3dde79550028dc7ae734c236df789a7 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/contact/Controllers/Contact.java @@ -0,0 +1,14 @@ +package polish_community_group_11.polish_community.contact.Controllers; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.stereotype.Controller; + +@Controller +public class Contact { + @GetMapping("/contactus") + public ModelAndView home(){ + ModelAndView modelAndView = new ModelAndView("contact/contact"); + return modelAndView; + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/event/controllers/EventController.java b/src/main/java/polish_community_group_11/polish_community/event/controllers/EventController.java index e1bffd25295351330f5c86cb941aa87a86967ab2..92658e9952906751b11a4fdde6c54d944647fff2 100644 --- a/src/main/java/polish_community_group_11/polish_community/event/controllers/EventController.java +++ b/src/main/java/polish_community_group_11/polish_community/event/controllers/EventController.java @@ -1,20 +1,37 @@ package polish_community_group_11.polish_community.event.controllers; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.servlet.ModelAndView; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; -import polish_community_group_11.polish_community.event.services.EventRepository; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import polish_community_group_11.polish_community.event.models.viewmodel.EventForm; +import org.springframework.web.bind.annotation.*; +import polish_community_group_11.polish_community.event.models.Event; +import polish_community_group_11.polish_community.event.models.EventImpl; +import polish_community_group_11.polish_community.event.services.EventService; +import polish_community_group_11.polish_community.register.services.UserService; + +import java.security.Principal; +import java.sql.SQLException; @Controller public class EventController { - // Inject the EventRepository service to handle event-related data operations - private EventRepository eventService; + private EventService eventService; + private UserService userService; + @Autowired // Constructor-based dependency injection for EventRepository - public EventController(EventRepository eventService){ + public EventController(EventService eventService, UserService userService) { this.eventService = eventService; + this.userService = userService; } // Controller method to handle GET requests for the "/event" URL @@ -50,4 +67,65 @@ public class EventController { // Return the populated ModelAndView object, which will render the event details view return modelAndView; } + + //Controller Method to load create new event page + @GetMapping("event/add") + public ModelAndView addEvent() { + ModelAndView modelAndView = new ModelAndView("event/add-event"); + //Passing Form object for validation + modelAndView.addObject("event", new EventForm()); + + /* Form action string to check the type of action to perform and dynamically + use the same form for both create and update */ + modelAndView.addObject("formAction", "/event/add"); + return modelAndView; + } + + @PostMapping("event/add") + public ModelAndView addEvent(@Valid @ModelAttribute("event") EventForm event, + BindingResult bindingResult, Model model, + Principal principal, RedirectAttributes redirectAttributes) throws SQLException { + ModelAndView modelAndView = new ModelAndView("event/add-event"); + if(bindingResult.hasErrors()) { + modelAndView.addObject(model.asMap()); + + /* Form action string to check the type of action to perform and dynamically + use the same form for both create and update */ + modelAndView.addObject("formAction", "/event/add"); + } + else{ + if(event==null){ + throw new NullPointerException("New event is empty or null"); + } + eventService.addNewEvent(event.processEventForm(),principal.getName()); + redirectAttributes.addFlashAttribute("successMessage", "Event created successfully!"); + modelAndView.setViewName("redirect:/event"); + } + return modelAndView; + } + + //Update Events + @GetMapping("event/edit/{id}") + public ModelAndView editEvent(@PathVariable int id) { + EventForm event = eventService.getEventById(id).processEventToEventForm(); + ModelAndView modelAndView = new ModelAndView("event/add-event"); + modelAndView.addObject("event", event); + modelAndView.addObject("formAction", "/event/edit"); + return modelAndView; + } + + @PostMapping("/event/edit") + public ModelAndView editEvent(@Valid @ModelAttribute("event") EventForm event, + BindingResult bindingResult, Model model) throws SQLException { + ModelAndView modelAndView = new ModelAndView("event/add-event"); + if (bindingResult.hasErrors()) { + modelAndView.addObject(model.asMap()); + modelAndView.addObject("formAction", "/event/edit"); + } else { + eventService.getEditEvent(event.processEventForm()); + modelAndView.setViewName("redirect:/event"); + } + return modelAndView; + } + } diff --git a/src/main/java/polish_community_group_11/polish_community/event/dao/EventRepository.java b/src/main/java/polish_community_group_11/polish_community/event/dao/EventRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..2efd942099d1d78d12dd9f537ab8730323c124c0 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/event/dao/EventRepository.java @@ -0,0 +1,15 @@ +package polish_community_group_11.polish_community.event.dao; + +import polish_community_group_11.polish_community.event.models.Event; + +import java.sql.SQLException; +import java.util.List; + +public interface EventRepository { + + List<Event> getAllEvents(); + + Event getEventById(int id); + void addNewEvent(Event newEvent) throws SQLException; + Event editEvent(Event event)throws SQLException; +} diff --git a/src/main/java/polish_community_group_11/polish_community/event/dao/EventRepositoryImpl.java b/src/main/java/polish_community_group_11/polish_community/event/dao/EventRepositoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..df9bc2b134a5c744246f2b3a95e9459f9a0b118b --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/event/dao/EventRepositoryImpl.java @@ -0,0 +1,140 @@ +package polish_community_group_11.polish_community.event.dao; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import polish_community_group_11.polish_community.event.models.Event; +import polish_community_group_11.polish_community.event.models.EventImpl; +import java.sql.SQLException; + +import java.util.List; + +@Repository // Marks this class as a Spring Data repository for database interaction +public class EventRepositoryImpl implements EventRepository { + + // JdbcTemplate instance for executing SQL queries + private final JdbcTemplate jdbc; + + // RowMapper to map SQL result set rows to Event objects + private RowMapper<Event> eventItemMapper; + + // Constructor that accepts JdbcTemplate and initializes the RowMapper + public EventRepositoryImpl(JdbcTemplate aJdbc) { + this.jdbc = aJdbc; // Initialize JdbcTemplate for executing queries + setEventItemMapper(); // Set the RowMapper for mapping rows to Event objects + } + + /** + * Initializes the RowMapper for Event mapping. + * This will map a result set from the database to an Event object. + */ + public void setEventItemMapper() { + try { + // Creating a RowMapper to convert each row in the result set to an EventImpl instance + eventItemMapper = (rs, i) -> new EventImpl( + rs.getInt("event_id"), + rs.getString("event_title"), + rs.getString("event_description"), + rs.getDate("event_date").toLocalDate(), + rs.getTime("event_time").toLocalTime(), + rs.getString("location"), + rs.getInt("user_id"), + rs.getString("event_poster_url"), + rs.getString("whyJoin"), + rs.getString("benefits") + ); + } + catch (Exception ex) { + // Catch any exception while setting the RowMapper and print the error message + System.out.println(ex.getMessage()); + } + } + + + // Method to retrieve all events from the database + public List<Event> getAllEvents() { + + String sql = "select * from event order by event_id desc"; + return jdbc.query(sql, eventItemMapper); + } + + + /** + * Fetches an event from the database by its event_id. + * + * @param id the unique identifier of the event to fetch + * @return Event object containing the event data + */ + public Event getEventById(int id) { + // SQL query to fetch event details using the event_id + String sql = "select * from event where event_id = ?"; + + // Execute the query using JdbcTemplate and map the result to an Event object using the RowMapper + Event event = jdbc.queryForObject(sql, eventItemMapper, id); + + // Return the fetched event + return event; + } + + public void addNewEvent(Event newEvent) throws SQLException, NullPointerException{ + if(newEvent==null){ + throw new NullPointerException("Event cannot be null"); + } + String dbInsertSql = + "insert into event " + + "(event_title, event_description, location, " + + "event_date, event_time,user_id, event_poster_url,whyJoin,benefits)" + + " values (?,?,?,?,?,?,?,?,?)"; + try{ + jdbc.update(dbInsertSql, + newEvent.getEvent_title(), + newEvent.getDescription(), + newEvent.getDescription(), + newEvent.getEvent_date(), + newEvent.getEvent_time(), + newEvent.getUser_id(), + newEvent.getImageUrl(), + newEvent.getWhyJoin(), + newEvent.getBenefits() + ); + } + catch (DataAccessException e) { + throw new SQLException("Failed to insert new information record", e); + } + } + + // Updates selected record with new updated information + public Event editEvent(Event event)throws SQLException { + String updateSql = "UPDATE event " + + "SET event_title = ?, event_description = ?, " + + "event_date = ?, event_time = ?, " + + "location = ?, user_id = ?, event_poster_url = ?, " + + "whyJoin = ?, benefits = ? " + + "WHERE event_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(updateSql, + event.getEvent_title(), + event.getDescription(), + event.getEvent_date(), + event.getEvent_time(), + event.getLocation(), + event.getUser_id(), + event.getImageUrl(), + event.getWhyJoin(), + event.getBenefits(), + event.getEvent_id() + ); + + // error handling + if (rowsAffected == 0) { + throw new SQLException("No event item was updated. Check the ID provided."); + } + } catch (DataAccessException e) { + throw new SQLException("Error updating event with ID: " + event.getEvent_id(), e); + } + return event; + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/event/models/Event.java b/src/main/java/polish_community_group_11/polish_community/event/models/Event.java index e09329ed4bcb7cdcc99766913950c87a0afa0ac2..06bab520ed102dd498c48d7a4f92a1679b219a9d 100644 --- a/src/main/java/polish_community_group_11/polish_community/event/models/Event.java +++ b/src/main/java/polish_community_group_11/polish_community/event/models/Event.java @@ -1,5 +1,7 @@ package polish_community_group_11.polish_community.event.models; +import polish_community_group_11.polish_community.event.models.viewmodel.EventForm; + import java.time.LocalDate; import java.time.LocalTime; @@ -27,4 +29,5 @@ public interface Event { public String getBenefits(); public void setBenefits(String benefits); + EventForm processEventToEventForm(); } diff --git a/src/main/java/polish_community_group_11/polish_community/event/models/EventImpl.java b/src/main/java/polish_community_group_11/polish_community/event/models/EventImpl.java index d08a19da24131fc362cb05643a4d840d92dc5c30..61e14fdef04e64df50ff58b129f26755f639dc3f 100644 --- a/src/main/java/polish_community_group_11/polish_community/event/models/EventImpl.java +++ b/src/main/java/polish_community_group_11/polish_community/event/models/EventImpl.java @@ -1,7 +1,13 @@ package polish_community_group_11.polish_community.event.models; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Data; +import org.hibernate.validator.constraints.URL; +import polish_community_group_11.polish_community.event.models.viewmodel.EventForm; import java.time.LocalDate; import java.time.LocalTime; @@ -20,4 +26,12 @@ public class EventImpl implements Event{ private String whyJoin; private String benefits; + public EventForm processEventToEventForm(){ + + EventForm eventForm = new EventForm( + event_id, event_title, description, event_date, event_time, + location, user_id, imageUrl, whyJoin, benefits + ); + return eventForm; + } } diff --git a/src/main/java/polish_community_group_11/polish_community/event/models/viewmodel/EventForm.java b/src/main/java/polish_community_group_11/polish_community/event/models/viewmodel/EventForm.java new file mode 100644 index 0000000000000000000000000000000000000000..a85c34e182877a2a660fab7b8dadb22b467039f2 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/event/models/viewmodel/EventForm.java @@ -0,0 +1,62 @@ +package polish_community_group_11.polish_community.event.models.viewmodel; + +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.URL; +import polish_community_group_11.polish_community.event.models.Event; +import polish_community_group_11.polish_community.event.models.EventImpl; + +import java.time.LocalDate; +import java.time.LocalTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class EventForm { + private int event_id; + @NotEmpty(message = "Please enter the event title") + private String event_title; + @NotEmpty(message = "Please enter the event description") + private String description; + @FutureOrPresent(message = "The date cannot be in the past") + @NotNull(message = "Please enter the event date") + private LocalDate event_date; + @NotNull(message = "Please enter the time of the event") + private LocalTime event_time; + @NotEmpty(message = "Please provide the location of event") + private String location; + private int user_id; + @NotEmpty(message = "Please provide event image url") + @Pattern(regexp = ".*\\.(png|jpg|jpeg|gif).*", message = "Must be a valid image (png, jpg, jpeg, gif).") + @URL(message = "Not a valid image url") + private String imageUrl; + private String whyJoin; + private String benefits; + + public Event processEventForm(){ + + if (event_title == null || event_title.isEmpty()) { + throw new IllegalArgumentException("Event title is required"); + } + if (event_date == null) { + throw new IllegalArgumentException("Event date is required"); + } + if (event_time == null) { + throw new IllegalArgumentException("Event time is required"); + } + if (location == null || location.isEmpty()) { + throw new IllegalArgumentException("Location is required"); + } + + Event event = new EventImpl( + event_id, event_title, description, event_date, event_time, + location, user_id, imageUrl, whyJoin, benefits + ); + return event; + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/event/services/EventRepository.java b/src/main/java/polish_community_group_11/polish_community/event/services/EventRepository.java deleted file mode 100644 index d529da27847197c528aa76dce8076869ea35653e..0000000000000000000000000000000000000000 --- a/src/main/java/polish_community_group_11/polish_community/event/services/EventRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package polish_community_group_11.polish_community.event.services; - -import polish_community_group_11.polish_community.event.models.Event; -import java.util.List; - -public interface EventRepository { - - List<Event> getAllEvents(); - - Event getEventById(int id); -} diff --git a/src/main/java/polish_community_group_11/polish_community/event/services/EventRepositoryImpl.java b/src/main/java/polish_community_group_11/polish_community/event/services/EventRepositoryImpl.java deleted file mode 100644 index 80acfe225ce1fd0b245e04c60e59c4d947773676..0000000000000000000000000000000000000000 --- a/src/main/java/polish_community_group_11/polish_community/event/services/EventRepositoryImpl.java +++ /dev/null @@ -1,77 +0,0 @@ -package polish_community_group_11.polish_community.event.services; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.stereotype.Repository; -import polish_community_group_11.polish_community.event.models.Event; -import polish_community_group_11.polish_community.event.models.EventImpl; -import java.util.List; - -@Repository // Marks this class as a Spring Data repository for database interaction -public class EventRepositoryImpl implements EventRepository { - - // JdbcTemplate instance for executing SQL queries - private JdbcTemplate jdbc; - - // RowMapper to map SQL result set rows to Event objects - private RowMapper<Event> eventItemMapper; - - // Constructor that accepts JdbcTemplate and initializes the RowMapper - public EventRepositoryImpl(JdbcTemplate aJdbc) { - this.jdbc = aJdbc; // Initialize JdbcTemplate for executing queries - setEventItemMapper(); // Set the RowMapper for mapping rows to Event objects - } - - /** - * Initializes the RowMapper for Event mapping. - * This will map a result set from the database to an Event object. - */ - public void setEventItemMapper() { - try { - // Creating a RowMapper to convert each row in the result set to an EventImpl instance - eventItemMapper = (rs, i) -> new EventImpl( - rs.getInt("event_id"), - rs.getString("event_title"), - rs.getString("event_description"), - rs.getDate("event_date").toLocalDate(), - rs.getTime("event_time").toLocalTime(), - rs.getString("location"), - rs.getInt("user_id"), - rs.getString("event_poster_url"), - rs.getString("whyJoin"), - rs.getString("benefits") - ); - } - catch (Exception ex) { - // Catch any exception while setting the RowMapper and print the error message - System.out.println(ex.getMessage()); - } - } - - - // Method to retrieve all events from the database - public List<Event> getAllEvents() { - - String sql = "select * from event"; - return jdbc.query(sql, eventItemMapper); - } - - - /** - * Fetches an event from the database by its event_id. - * - * @param id the unique identifier of the event to fetch - * @return Event object containing the event data - */ - public Event getEventById(int id) { - // SQL query to fetch event details using the event_id - String sql = "select * from event where event_id = ?"; - - // Execute the query using JdbcTemplate and map the result to an Event object using the RowMapper - Event event = jdbc.queryForObject(sql, eventItemMapper, id); - - // Return the fetched event - return event; - } - -} diff --git a/src/main/java/polish_community_group_11/polish_community/event/services/EventService.java b/src/main/java/polish_community_group_11/polish_community/event/services/EventService.java new file mode 100644 index 0000000000000000000000000000000000000000..64b87f40b97d0d26dbab5db187fb67ae3e0ff471 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/event/services/EventService.java @@ -0,0 +1,13 @@ +package polish_community_group_11.polish_community.event.services; + +import polish_community_group_11.polish_community.event.models.Event; +import java.sql.SQLException; +import java.util.List; + +public interface EventService { + + List<Event> getAllEvents(); + Event getEventById(int id); + void addNewEvent(Event newEvent, String email) throws SQLException; + Event getEditEvent(Event event) throws SQLException; +} \ No newline at end of file diff --git a/src/main/java/polish_community_group_11/polish_community/event/services/EventServiceImpl.java b/src/main/java/polish_community_group_11/polish_community/event/services/EventServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..23e1de25a7040ca733f47a861a4131dd1dd1efa8 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/event/services/EventServiceImpl.java @@ -0,0 +1,38 @@ +package polish_community_group_11.polish_community.event.services; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import polish_community_group_11.polish_community.event.dao.EventRepository; +import polish_community_group_11.polish_community.event.models.Event; +import polish_community_group_11.polish_community.register.services.UserService; + +import java.sql.SQLException; +import java.util.List; + +@Service +public class EventServiceImpl implements EventService { + + private final EventRepository eventRepository; + private final UserService userService; + + @Autowired + public EventServiceImpl(EventRepository eventRepository, UserService userService) { + this.eventRepository = eventRepository; + this.userService=userService; + } + + public List<Event> getAllEvents(){ + return eventRepository.getAllEvents(); + } + public Event getEventById(int id){ + return eventRepository.getEventById(id); + } + public void addNewEvent(Event newEvent, String email) throws SQLException { + newEvent.setUser_id(userService.findUserIdByEmail(email)); + eventRepository.addNewEvent(newEvent); + } + + public Event getEditEvent(Event event) throws SQLException { + return eventRepository.editEvent(event); + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/feed/WebConfig.java b/src/main/java/polish_community_group_11/polish_community/feed/WebConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..20968c93dd696c75a7633cb585f897cc05be557e --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/feed/WebConfig.java @@ -0,0 +1,19 @@ +package polish_community_group_11.polish_community.feed; + + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + @Value("${file.upload-dir}") + private String uploadDir; + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/uploads/**") + .addResourceLocations("file:" + uploadDir + "/"); + } +} \ No newline at end of file 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 index 79bebcae3022b9bdd33964c505dc5a0d52e5283f..b79a08e51000813cfd02974461e75038938d7023 100644 --- 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 @@ -1,11 +1,20 @@ package polish_community_group_11.polish_community.feed.controllers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; 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 polish_community_group_11.polish_community.feed.services.FileStorageService; +import polish_community_group_11.polish_community.register.models.User; +import polish_community_group_11.polish_community.register.services.UserService; import java.util.List; @@ -14,18 +23,53 @@ import java.util.List; public class FeedApisController { private final FeedRepository feedRepository; + private final UserService userService; private final FeedService feedService; + private final FileStorageService fileStorageService; + private static final Logger log = LoggerFactory.getLogger(FeedApisController.class); - public FeedApisController(FeedRepository feedRepository, FeedService feedService) { + + public FeedApisController(FeedRepository feedRepository, FeedService feedService, UserService userService, FileStorageService fileStorageService) { this.feedService = feedService; this.feedRepository = feedRepository; + this.userService = userService; + this.fileStorageService = fileStorageService; } // getting all posts @GetMapping public List<FeedImpl> getAllPosts() { - return feedRepository.getAllPosts(); + log.info("Fetching all posts"); + // getting current user + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + User currentUser = null; + + if (auth != null && auth.isAuthenticated() && !auth.getPrincipal().equals("anonymousUser")) { + String email = auth.getName(); + log.info("User Email: " + email); + currentUser = userService.findByEmail(email); + log.info("Current User: " + currentUser); + } + + List<FeedImpl> posts = feedRepository.getAllPosts(); + + + if (currentUser != null){ + // set isDeletable flag and isEditable flag for each post depedant on whether they are an admin or own the post + for (FeedImpl post : posts) { + log.info("Role ID: " + currentUser.getRoleId()); + boolean isSuperAdmin = currentUser != null && currentUser.getRoleId() == 1; + log.info("IsSuperAdmin: " + isSuperAdmin); + boolean isOwner = currentUser != null && post.getUserId() == currentUser.getId(); + log.info("IsPostOwner: " + isOwner); + post.setIsDeletable(isSuperAdmin || isOwner); + post.setIsEditable( isOwner); + log.info("PostIsEditable: " + post.getIsDeletable()); + } + } + + return posts; } //getting them by id @@ -40,70 +84,127 @@ public class FeedApisController { } // creating a new post - @PostMapping("/add") - public ResponseEntity<?> addNewPost(@RequestBody FeedImpl feed) { + public ResponseEntity<?> addNewPost( + @RequestPart("post") FeedImpl feed, + @RequestPart(value = "image", required = false) MultipartFile image) { try { + if (image != null && !image.isEmpty()) { + String imageUrl = fileStorageService.storeFile(image); + feed.setPostImageUrl(imageUrl); + } 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()); + 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) { + @PatchMapping("/{postId}") + public ResponseEntity<?> updatePost( + @PathVariable int postId, + @RequestPart("post") FeedImpl feed, + @RequestPart(value = "image", required = false) MultipartFile image) { try { + + if (image != null && !image.isEmpty()) { + + String imageUrl = fileStorageService.storeFile(image); + feed.setPostImageUrl(imageUrl); + } else { + + FeedImpl existingPost = feedRepository.getPostById(postId); + if (existingPost != null) { + feed.setPostImageUrl(existingPost.getPostImageUrl()); + } + } + feedRepository.updatePost(postId, feed); - return ResponseEntity.ok().build(); + return ResponseEntity.ok().body("Post updated successfully"); } catch (Exception e) { - return ResponseEntity.notFound().build(); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error updating post: " + e.getMessage()); } } - // deleting a post + @DeleteMapping("/{postId}") public ResponseEntity<Void> deletePost(@PathVariable int postId) { try { + // get logged in user + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + + FeedImpl post = feedRepository.getPostById(postId); + if (post == null) { + return ResponseEntity.notFound().build(); + } + + // get current user + UserDetails userDetails = (UserDetails) auth.getPrincipal(); + User currentUser = userService.findByEmail(userDetails.getUsername()); + + // check if admin or post owner + boolean isSuperAdmin = currentUser.getRoleId() == 1; + boolean isPostOwner = post.getUserId() == currentUser.getId(); + + if (!isSuperAdmin && !isPostOwner) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + feedRepository.deletePost(postId); return ResponseEntity.ok().build(); } catch (Exception e) { - return ResponseEntity.notFound().build(); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } // liking a post @PostMapping("/{postId}/like") - public ResponseEntity<Void> likePost(@PathVariable int postId, @RequestParam int userId) { + public ResponseEntity<Void> likePost(@PathVariable int postId) { try { - feedRepository.likePost(postId, userId); + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + User currentUser = userService.findByEmail(auth.getName()); + feedRepository.likePost(postId, currentUser.getId()); return ResponseEntity.ok().build(); } catch (Exception e) { return ResponseEntity.badRequest().build(); } } - // removing a like from post + // remove like from post @DeleteMapping("/{postId}/like") - public ResponseEntity<Void> unlikePost(@PathVariable int postId, @RequestParam int userId) { + public ResponseEntity<Void> unlikePost(@PathVariable int postId) { try { - feedRepository.unlikePost(postId, userId); + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + User currentUser = userService.findByEmail(auth.getName()); + feedRepository.unlikePost(postId, currentUser.getId()); return ResponseEntity.ok().build(); } catch (Exception e) { return ResponseEntity.badRequest().build(); } } - // getting list of likes from a post + + // check if already liked @GetMapping("/{postId}/hasLiked") - public ResponseEntity<Boolean> hasUserLikedPost(@PathVariable int postId, @RequestParam int userId) { + public ResponseEntity<Boolean> hasUserLikedPost(@PathVariable int postId) { try { - boolean hasLiked = feedRepository.hasUserLikedPost(postId, userId); + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + User currentUser = userService.findByEmail(auth.getName()); + boolean hasLiked = feedRepository.hasUserLikedPost(postId, currentUser.getId()); 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/models/Feed.java b/src/main/java/polish_community_group_11/polish_community/feed/models/Feed.java index 19ff0e6faaf48bc60c68ea2dc1a0c9918c6c5f62..42ffd5d9ec762c28744c4cc8a901037a8fe49668 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 @@ -30,6 +30,15 @@ public interface Feed { int getLikesCount(); void setLikesCount(int likesCount); + + boolean getIsDeletable(); + void setIsDeletable(boolean isDeletable); + + boolean getIsEditable(); + void setIsEditable(boolean isEditable); + int getPostId(); + + } \ 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 6484ce5ba1e3e00a00dd6c6ff98a12e29b351f45..3a7f5762fa8f379f6da9d3703a84b8e90e072091 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 @@ -21,4 +21,27 @@ public class FeedImpl implements Feed { private String authorOrganization; private List<String> tags; private int likesCount; -} \ No newline at end of file + private boolean isDeletable; + private boolean isEditable; + + @Override + public boolean getIsDeletable() { + return isDeletable; + } + + @Override + public void setIsDeletable(boolean isDeletable) { + this.isDeletable = isDeletable; + } + + @Override + public boolean getIsEditable(){ + return isEditable; + } + + @Override + public void setIsEditable(boolean isEditable){ + this.isEditable = isEditable; + } +} + 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 index bb2037c7307c00dfab996624863794fe3253e25c..a895914078f0b2f692b25f715277a2e08ef36430 100644 --- 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 @@ -77,20 +77,20 @@ public class FeedRepositoryImpl implements FeedRepository { // 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 = ?"; + String sql = "UPDATE feed SET post_title = ?, post_description = ?, post_image_url = ? WHERE post_id = ?"; jdbcTemplate.update(sql, - feed.getPostImageUrl(), feed.getPostTitle(), feed.getPostDescription(), - java.sql.Date.valueOf(feed.getPostTime()), + feed.getPostImageUrl(), postId ); // update the tags jdbcTemplate.update("DELETE FROM feed_tags WHERE post_id = ?", postId); - insertTagsForPost(postId, feed.getTags()); + if (feed.getTags() != null && !feed.getTags().isEmpty()) { + insertTagsForPost(postId, feed.getTags()); + } } // deleting a post diff --git a/src/main/java/polish_community_group_11/polish_community/feed/services/FileStorageService.java b/src/main/java/polish_community_group_11/polish_community/feed/services/FileStorageService.java new file mode 100644 index 0000000000000000000000000000000000000000..bbbff28910cda3f5b671c4d459f331d76619d19f --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/feed/services/FileStorageService.java @@ -0,0 +1,48 @@ +package polish_community_group_11.polish_community.feed.services; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.UUID; + +@Service +public class FileStorageService { + + @Value("${file.upload-dir:uploads}") + private String uploadDir; + + private Path root; + + @PostConstruct + public void init() throws IOException { + this.root = Paths.get(uploadDir); + if (!Files.exists(root)) { + Files.createDirectories(root); + } + } + + + public String storeFile(MultipartFile file) throws IOException { + // making unique file name + String filename = UUID.randomUUID().toString() + "_" + file.getOriginalFilename(); + + // full path of file + Path filePath = this.root.resolve(filename); + + // save file + Files.copy(file.getInputStream(), filePath); + + // relative path usable in urls + return "/uploads/" + filename; + } + + public void deleteFile(String filename) throws IOException { + Path filePath = this.root.resolve(filename); + Files.deleteIfExists(filePath); + } +} \ No newline at end of file diff --git a/src/main/java/polish_community_group_11/polish_community/profile/controllers/ProfileController.java b/src/main/java/polish_community_group_11/polish_community/profile/controllers/ProfileController.java new file mode 100644 index 0000000000000000000000000000000000000000..fb82e1ed930c64a1369b15245aadde4468150270 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/profile/controllers/ProfileController.java @@ -0,0 +1,81 @@ +package polish_community_group_11.polish_community.profile.controllers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.ModelAndView; +import polish_community_group_11.polish_community.profile.models.Profile; +import polish_community_group_11.polish_community.profile.services.ProfileService; +import polish_community_group_11.polish_community.register.models.User; +import polish_community_group_11.polish_community.register.services.UserService; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandles; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +@Controller +public class ProfileController { + + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + @Autowired + private UserService userService; + @Autowired + private ProfileService profileService; + + @GetMapping("/profile") + public ModelAndView profile(Authentication authentication) { + + ModelAndView modelAndView = new ModelAndView("profile/profilePage"); + + Profile profile = getProfileForAuthenticatedUser(authentication); + modelAndView.addObject("profile", profile); + + return modelAndView; + } + + @PostMapping("/update") + public String updateProfile(@ModelAttribute Profile profile, Authentication authentication,@RequestParam("newPicture") MultipartFile newPicture) + throws IOException { + String username = authentication.getName(); + User user = userService.findByEmail(username); + profile.setUserId(user.getId()); + if (!newPicture.isEmpty()) { + String fileName = StringUtils.cleanPath(newPicture.getOriginalFilename()); + String uploadDir = "build/resources/main/static/assets/user-photos/" + user.getId(); + Path uploadPath = Paths.get(uploadDir); + + if (!Files.exists(uploadPath)) { + Files.createDirectories(uploadPath); + } + + try (InputStream inputStream = newPicture.getInputStream()) { + Path filePath = uploadPath.resolve(fileName); + Files.copy(inputStream, filePath, StandardCopyOption.REPLACE_EXISTING); + profile.setProfilePicture("/assets/user-photos/" + user.getId() + "/" + fileName); + } + } + profileService.updateProfile(profile); + return "redirect:/profile"; + } + private Profile getProfileForAuthenticatedUser(Authentication authentication) { + LOG.info("getting profile for {} - isAuthenticated: {}", authentication.getName(), authentication.isAuthenticated()); + String username = authentication.getName(); + + User user = userService.findByEmail(username); + Profile profile = profileService.getProfile(user.getId()); + return profile; + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/profile/controllers/ProfileRestController.java b/src/main/java/polish_community_group_11/polish_community/profile/controllers/ProfileRestController.java new file mode 100644 index 0000000000000000000000000000000000000000..67aef21422016f65845afd4d2a21e4200428f177 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/profile/controllers/ProfileRestController.java @@ -0,0 +1,30 @@ +package polish_community_group_11.polish_community.profile.controllers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import polish_community_group_11.polish_community.profile.models.Profile; +import polish_community_group_11.polish_community.profile.services.ProfileService; +import polish_community_group_11.polish_community.register.models.User; +import polish_community_group_11.polish_community.register.services.UserService; + +@RestController +public class ProfileRestController { + @Autowired + private UserService userService; + @Autowired + private ProfileService profileService; + + @GetMapping("/profile-json") + public Profile getProfile(Authentication authentication) { + return getProfileForAuthenticatedUser(authentication); + } + private Profile getProfileForAuthenticatedUser(Authentication authentication) { + String username = authentication.getName(); + + User user = userService.findByEmail(username); + Profile profile = profileService.getProfile(user.getId()); + return profile; + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/profile/models/Profile.java b/src/main/java/polish_community_group_11/polish_community/profile/models/Profile.java new file mode 100644 index 0000000000000000000000000000000000000000..ae0fa9b7366301cf254290dbd8b4324afffd3716 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/profile/models/Profile.java @@ -0,0 +1,23 @@ +package polish_community_group_11.polish_community.profile.models; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Profile { + private int userId; + private String fullName; + private String email; + private String profilePicture; + private LocalDate dob; + private String bio; + private String phoneNumber; + private String organisation; + private boolean showPhoneNumber; + private boolean showDob; +} diff --git a/src/main/java/polish_community_group_11/polish_community/profile/repositories/ProfileRepository.java b/src/main/java/polish_community_group_11/polish_community/profile/repositories/ProfileRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..3c1011095364f46e023dcf5cc2202f8c88f6221a --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/profile/repositories/ProfileRepository.java @@ -0,0 +1,13 @@ +package polish_community_group_11.polish_community.profile.repositories; + +import org.springframework.stereotype.Repository; +import polish_community_group_11.polish_community.profile.models.Profile; + +import java.util.List; + +@Repository +public interface ProfileRepository { + Profile getProfile(Integer id); + void addProfile(Profile profile); + void updateProfile(Profile profile); +} diff --git a/src/main/java/polish_community_group_11/polish_community/profile/repositories/ProfileRepositoryImpl.java b/src/main/java/polish_community_group_11/polish_community/profile/repositories/ProfileRepositoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..1c1a562776fb9e69a6d5fd7085101332ad673e2e --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/profile/repositories/ProfileRepositoryImpl.java @@ -0,0 +1,52 @@ +package polish_community_group_11.polish_community.profile.repositories; + +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.FeedImpl; +import polish_community_group_11.polish_community.profile.models.Profile; + +@Repository +public class ProfileRepositoryImpl implements ProfileRepository { + private final JdbcTemplate jdbcTemplate; + private final RowMapper<Profile> rowMapper; + + public ProfileRepositoryImpl(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + this.rowMapper = (rs, rowNum) -> new Profile( + rs.getInt("user_id"), + rs.getString("fullName"), + rs.getString("email"), + rs.getString("profile_picture"), + rs.getDate("dob").toLocalDate(), + rs.getString("bio"), + rs.getString("phone_number"), + rs.getString("organization"), + rs.getBoolean("show_phone_number"), + rs.getBoolean("show_dob") + ); + } + @Override + public void addProfile(Profile profile) { + String sql = "INSERT INTO user_profile (user_id) VALUES (?)"; + jdbcTemplate.update(sql, profile.getUserId()); + + } + @Override + public Profile getProfile(Integer id) { + String sql = "SELECT u.id as user_id, " + + "u.fullname, u.email, u.dob, u.organization, " + + "up.profile_picture, up.phone_number, up.bio, up.show_phone_number, up.show_dob " + + "FROM users u LEFT JOIN user_profile up ON " + + "u.id = up.user_id WHERE u.id = ?"; + return jdbcTemplate.queryForObject(sql, rowMapper, id); + } + + + @Override + public void updateProfile(Profile profile) { + String sql = "UPDATE user_profile SET profile_picture = ?," + + " bio = ?, phone_number = ?, show_phone_number = ?, show_dob = ? WHERE user_id = ?"; + jdbcTemplate.update(sql, profile.getProfilePicture(), profile.getBio(), profile.getPhoneNumber(),profile.isShowPhoneNumber(), profile.isShowDob(), profile.getUserId()); + } +}; diff --git a/src/main/java/polish_community_group_11/polish_community/profile/services/ProfileService.java b/src/main/java/polish_community_group_11/polish_community/profile/services/ProfileService.java new file mode 100644 index 0000000000000000000000000000000000000000..1e4de2551b8e8bdd81b768d1f705715846182750 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/profile/services/ProfileService.java @@ -0,0 +1,22 @@ +package polish_community_group_11.polish_community.profile.services; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import polish_community_group_11.polish_community.profile.models.Profile; +import polish_community_group_11.polish_community.profile.repositories.ProfileRepository; + +@Service +public class ProfileService { + private ProfileRepository profileRepository; + @Autowired + public ProfileService(ProfileRepository profileRepository) { + this.profileRepository = profileRepository; + } + public void addProfile(Profile profile) {profileRepository.addProfile(profile);} + public Profile getProfile(Integer userId) { + return profileRepository.getProfile(userId); + } + public void updateProfile(Profile profile) { + profileRepository.updateProfile(profile); + } +} 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 c6aba7401b29156bfa9fa9c9bdd8d64f2f35429f..b27e416ec76b4219449021e37a37562a14bed6d4 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 @@ -4,6 +4,8 @@ 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.profile.models.Profile; +import polish_community_group_11.polish_community.profile.services.ProfileService; 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; @@ -19,6 +21,9 @@ public class RegisterController { @Autowired private RoleService roleService; + + @Autowired + private ProfileService profileService; // displaying the registration form using get request and ModelAndView @GetMapping("/register") @@ -49,6 +54,10 @@ public class RegisterController { // save user to the database userService.saveUser(user); +// Profile newProfile = new Profile(); +// newProfile.setUserId(user.getId()); +// profileService.addProfile(newProfile); + // redirect to the login page return "redirect:/login"; } 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 index 945b72d91dfdb22bf599c5b4b185adf8513f87e6..2c6c3f1b02cc0cb6efd92df490bb8b66118689c9 100644 --- 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 @@ -8,9 +8,11 @@ public interface UserRepository { int saveUser(User user); // add user into the database User findByEmail(String email); // find user by email + int findUserIdByEmail(String email); User findById(int id); // find user by ID List<User> findAllUsers(); // get all the users + String findUserFullNameByEmail(String email); } \ 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 index 5d12c85607ddbfe25dda9c8281756e2d1a3c0dc4..d9beeee03923ffbf0e02c67ee77c085389db4245 100644 --- 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 @@ -85,6 +85,22 @@ public class UserRepositoryImpl implements UserRepository { } } + public int findUserIdByEmail(String email) { + if(email.trim().equals("")|| email == null){ + throw new IllegalArgumentException("Email is null or empty"); + } + User user = findByEmail(email); + if(user == null){ + throw new NullPointerException("User not found"); + } + return user.getId(); + } + + public String findUserFullNameByEmail(String email) { + User user = findByEmail(email); + return user.getFullname(); + } + // function for finding user by id public User findById(int id) { @@ -101,12 +117,13 @@ public class UserRepositoryImpl implements UserRepository { user.setDateOfBirth(rs.getObject("dob", LocalDate.class)); user.setRoleId(rs.getInt("role_id")); user.setOrganization(rs.getString("organization")); + user.setEnabled(rs.getBoolean("enabled")); 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/User.java b/src/main/java/polish_community_group_11/polish_community/register/models/User.java index 1c152fdb496ad6d984e1b9dada97dd024a1bf69d..ce12c01d1de239e752c6ef763c72b0d21c1f3e4f 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,8 +1,13 @@ package polish_community_group_11.polish_community.register.models; +import lombok.Getter; +import lombok.Setter; + import java.time.LocalDate; import java.util.List; +@Getter +@Setter public class User { private int id; private String email; @@ -11,7 +16,7 @@ public class User { private LocalDate dob; // use LocalDate for dob private int roleId; private String organization; - + private Boolean enabled; // getters and setters public int getId() { 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 63cf2d3a58c459ec7d14a667c5510223243a8fce..aa346e699b0dc7c54f3091995951fc8eeea7dc75 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 @@ -10,6 +10,8 @@ public interface UserService { User findById(int id); User findByEmail(String email); + int findUserIdByEmail(String email); List<User> findAllUsers(); + public String findUserFullNameByEmail(String email); } \ 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 index d2c0a3e498d415ace85964d8964c78108ed7afb5..233bdac350904a2da84dfbcefda455344ae0113b 100644 --- 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 @@ -37,4 +37,10 @@ public class UserServiceImpl implements UserService { // Calls findAll method of the UserRepository return userRepository.findAllUsers(); } + public int findUserIdByEmail(String email){ + return userRepository.findUserIdByEmail(email); + } + public String findUserFullNameByEmail(String email){ + return userRepository.findUserFullNameByEmail(email); + } } 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 962eda9faeada26f1522a5480142f7404c66682f..3119b99b7968d0bfe754d5a243c95d251d938388 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 @@ -1,5 +1,6 @@ package polish_community_group_11.polish_community.security; +import lombok.Value; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; @@ -14,6 +15,7 @@ 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 org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect; 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; @@ -35,7 +37,7 @@ public class WebSecurityConfig { private final String[] whiteListingPath = { // "/event", // "event/*" - "/api/feed/**" +// "/api/feed/**" , }; public WebSecurityConfig(UserService userService, RoleService roleService) { @@ -53,6 +55,8 @@ public class WebSecurityConfig { // require authentication for events only //update in future when more protected resources are available .authorizeHttpRequests((requests) -> requests +// .requestMatchers("/api/translations/**","/api/feed/**").permitAll() +// .requestMatchers().permitAll() .requestMatchers(whiteListingPath).authenticated() .anyRequest().permitAll() ) @@ -72,22 +76,10 @@ public class WebSecurityConfig { return http.build(); } - - //Use in memory userDetailsManager to store & access login details -// @Bean -// public UserDetailsService userDetailsService() { -// UserDetails user = -// User.withDefaultPasswordEncoder() -// .username("user@example.com") //use email as username -// .password("password") -// .roles("USER") -// .build(); -// -// //can extend create more users here when needed (admin, super-admin etc) -// -// return new InMemoryUserDetailsManager(user); -// } -//} + @Bean + public SpringSecurityDialect springSecurityDialect() { + return new SpringSecurityDialect(); + } @Bean public UserDetailsService userDetailsService() { @@ -115,4 +107,6 @@ public class WebSecurityConfig { }; } + + } diff --git a/src/main/java/polish_community_group_11/polish_community/translation/controller/TranslationApisController.java b/src/main/java/polish_community_group_11/polish_community/translation/controller/TranslationApisController.java new file mode 100644 index 0000000000000000000000000000000000000000..878db0d444f76e602a9c5a01d69460892ad718de --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/translation/controller/TranslationApisController.java @@ -0,0 +1,93 @@ +package polish_community_group_11.polish_community.translation.controller; + +import jakarta.annotation.PostConstruct; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import polish_community_group_11.polish_community.translation.model.Translation; +import polish_community_group_11.polish_community.translation.service.TranslationService; + +import java.util.List; + +@RestController +@RequestMapping("/api/translations") +public class TranslationApisController { + + private final TranslationService translationService; + + public TranslationApisController(TranslationService translationService) { + this.translationService = translationService; + } + + @GetMapping("/test") + public ResponseEntity<String> test() { + return ResponseEntity.ok("Translation API is working"); + } + + @PostConstruct + public void init() { + System.out.println("TranslationApisController initialized with mappings:"); + System.out.println("/api/translations"); + System.out.println("/api/translations/language/{language}"); + } + + // get all translations + @GetMapping + public ResponseEntity<List<Translation>> getAllTranslations() { + List<Translation> translations = translationService.getAllTranslations(); + return ResponseEntity.ok(translations); + } + + // for specific lang + @GetMapping("/language/{language}") + public ResponseEntity<List<Translation>> getTranslationsByLanguage(@PathVariable String language) { + List<Translation> translations = translationService.getTranslationsByLanguage(language); + return ResponseEntity.ok(translations); + } + + // for specific key and lang + @GetMapping("/{key}/language/{language}") + public ResponseEntity<Translation> getTranslationByKeyAndLanguage( + @PathVariable String key, + @PathVariable String language) { + Translation translation = translationService.getTranslationByKeyAndLanguage(key, language); + if (translation == null) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(translation); + } + + // adding new translation + @PostMapping + public ResponseEntity<String> addTranslation(@RequestBody Translation translation) { + translationService.addTranslation(translation); + return ResponseEntity.ok("Translation added successfully."); + } + + // update existing + @PutMapping("/{id}") + public ResponseEntity<String> updateTranslation(@PathVariable int id, @RequestBody Translation translation) { + translationService.updateTranslation(id, translation); + return ResponseEntity.ok("Translation updated successfully."); + } + + // deleting a translation + @DeleteMapping("/{id}") + public ResponseEntity<String> deleteTranslation(@PathVariable int id) { + translationService.deleteTranslation(id); + return ResponseEntity.ok("Translation deleted successfully."); + } + + // get all unique keys + @GetMapping("/keys") + public ResponseEntity<List<String>> getAllTranslationKeys() { + List<String> keys = translationService.getAllTranslationKeys(); + return ResponseEntity.ok(keys); + } + + // get all translations for a specific key across all langs + @GetMapping("/keys/{key}") + public ResponseEntity<List<Translation>> getTranslationsByKey(@PathVariable String key) { + List<Translation> translations = translationService.getTranslationsByKey(key); + return ResponseEntity.ok(translations); + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/translation/model/Translation.java b/src/main/java/polish_community_group_11/polish_community/translation/model/Translation.java new file mode 100644 index 0000000000000000000000000000000000000000..90b84c5c826b15fec1650fdcbc70369a6a92b359 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/translation/model/Translation.java @@ -0,0 +1,93 @@ +package polish_community_group_11.polish_community.translation.model; + +public class Translation { + private int id; + private String key; + private String language; + private String value; + + public Translation () { + + } + + public Translation(int id, String key, String language, String value){ + this.id = id; + this.key = key; + this.language = language; + this.value = value; + + } + + public Translation(String key, String language, String value){ + this.key = key; + this.language = language; + this.value = value; + } + + // Getters and Setters + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + // some utility methods for debugging + @Override + public String toString() { + return "Translation{" + + "id=" + id + + ", key='" + key + '\'' + + ", language='" + language + '\'' + + ", value='" + value + '\'' + + '}'; + } + + // cheking if two translations equal each other + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Translation that = (Translation) o; + + if (id != that.id) return false; + if (!key.equals(that.key)) return false; + if (!language.equals(that.language)) return false; + return value.equals(that.value); + } + + @Override + public int hashCode() { + int result = id; + result = 31 * result + key.hashCode(); + result = 31 * result + language.hashCode(); + result = 31 * result + value.hashCode(); + return result; + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/translation/repository/TranslationRepository.java b/src/main/java/polish_community_group_11/polish_community/translation/repository/TranslationRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..378e164bb66cc13256fb1e4687bf5ba26d3080eb --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/translation/repository/TranslationRepository.java @@ -0,0 +1,32 @@ +package polish_community_group_11.polish_community.translation.repository; + +import polish_community_group_11.polish_community.translation.model.Translation; + +import java.util.List; + +public interface TranslationRepository { + + + List<Translation> getAllTranslations(); + + + List<Translation> getTranslationsByLanguage(String language); + + + Translation getTranslationByKeyAndLanguage(String key, String language); + + + void addTranslation(Translation translation); + + + void updateTranslation(int id, Translation translation); + + + void deleteTranslation(int id); + + + List<String> getAllTranslationKeys(); + + + List<Translation> getTranslationsByKey(String key); +} diff --git a/src/main/java/polish_community_group_11/polish_community/translation/repository/TranslationRepositoryImpl.java b/src/main/java/polish_community_group_11/polish_community/translation/repository/TranslationRepositoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..62bc181d910e15069afc33400e560536fcf69e0b --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/translation/repository/TranslationRepositoryImpl.java @@ -0,0 +1,109 @@ +package polish_community_group_11.polish_community.translation.repository; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import polish_community_group_11.polish_community.translation.model.Translation; + +import java.util.List; + +@Repository +public class TranslationRepositoryImpl implements TranslationRepository { + private static final Logger logger = LoggerFactory.getLogger(TranslationRepositoryImpl.class); + + private final JdbcTemplate jdbcTemplate; + private final RowMapper<Translation> translationMapper; + + public TranslationRepositoryImpl(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + this.translationMapper = (rs, rowNum) -> { + Translation translation = new Translation(); + translation.setId(rs.getInt("id")); + translation.setKey(rs.getString("translation_key")); + translation.setLanguage(rs.getString("language")); + translation.setValue(rs.getString("value")); + return translation; + }; + // Test database connection on startup + try { + jdbcTemplate.queryForObject("SELECT 1", Integer.class); + logger.info("Database connection successful"); + } catch (Exception e) { + logger.error("Database connection failed", e); + } + } + + @Override + public List<Translation> getTranslationsByLanguage(String language) { + try { + logger.info("Attempting to fetch translations for language: {}", language); + String sql = "SELECT * FROM translations WHERE language = ?"; + List<Translation> results = jdbcTemplate.query(sql, translationMapper, language); + logger.info("Found {} translations for language {}", results.size(), language); + return results; + } catch (Exception e) { + logger.error("Error fetching translations for language {}: {}", language, e.getMessage()); + throw e; + } + } + + // getting all translaions + @Override + public List<Translation> getAllTranslations() { + String sql = "SELECT * FROM translations"; + return jdbcTemplate.query(sql, translationMapper); + } + + // get translation by key and language + @Override + public Translation getTranslationByKeyAndLanguage(String key, String language) { + String sql = "SELECT * FROM translations WHERE translation_key = ? AND language = ?"; + return jdbcTemplate.queryForObject(sql, translationMapper, key, language); + } + + // Add a new translation + @Override + public void addTranslation(Translation translation) { + String sql = "INSERT INTO translations (translation_key, language, translation_value) VALUES (?, ?, ?)"; + jdbcTemplate.update(sql, + translation.getKey(), + translation.getLanguage(), + translation.getValue() + ); + } + + // update existing translation + @Override + public void updateTranslation(int id, Translation translation) { + String sql = "UPDATE translations SET translation_key = ?, language = ?, translation_value = ? WHERE id = ?"; + jdbcTemplate.update(sql, + translation.getKey(), + translation.getLanguage(), + translation.getValue(), + id + ); + } + + // delete a translation + @Override + public void deleteTranslation(int id) { + String sql = "DELETE FROM translations WHERE id = ?"; + jdbcTemplate.update(sql, id); + } + + // getting all unique keys for word list management + @Override + public List<String> getAllTranslationKeys() { + String sql = "SELECT DISTINCT translation_key FROM translations"; + return jdbcTemplate.query(sql, (rs, rowNum) -> rs.getString("translation_key")); + } + + // get translations for something across all languages + @Override + public List<Translation> getTranslationsByKey(String key) { + String sql = "SELECT * FROM translations WHERE translation_key = ?"; + return jdbcTemplate.query(sql, translationMapper, key); + } +} diff --git a/src/main/java/polish_community_group_11/polish_community/translation/service/TranslationService.java b/src/main/java/polish_community_group_11/polish_community/translation/service/TranslationService.java new file mode 100644 index 0000000000000000000000000000000000000000..089f24dbdc3ebf37fe9bc8bfee9d0423b37fcad2 --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/translation/service/TranslationService.java @@ -0,0 +1,34 @@ +package polish_community_group_11.polish_community.translation.service; + + + +import polish_community_group_11.polish_community.translation.model.Translation; + +import java.util.List; + +public interface TranslationService { + + + List<Translation> getAllTranslations(); + + + List<Translation> getTranslationsByLanguage(String language); + + + Translation getTranslationByKeyAndLanguage(String key, String language); + + + void addTranslation(Translation translation); + + + void updateTranslation(int id, Translation translation); + + + void deleteTranslation(int id); + + + List<String> getAllTranslationKeys(); + + + List<Translation> getTranslationsByKey(String key); +} diff --git a/src/main/java/polish_community_group_11/polish_community/translation/service/TranslationServiceImpl.java b/src/main/java/polish_community_group_11/polish_community/translation/service/TranslationServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..82bba85308e0e4e116ff6c7f869b07d1f6f6394f --- /dev/null +++ b/src/main/java/polish_community_group_11/polish_community/translation/service/TranslationServiceImpl.java @@ -0,0 +1,57 @@ +package polish_community_group_11.polish_community.translation.service; + +import org.springframework.stereotype.Service; +import polish_community_group_11.polish_community.translation.model.Translation; +import polish_community_group_11.polish_community.translation.repository.TranslationRepository; + +import java.util.List; + +@Service +public class TranslationServiceImpl implements TranslationService { + + private final TranslationRepository translationRepository; + + public TranslationServiceImpl(TranslationRepository translationRepository) { + this.translationRepository = translationRepository; + } + + @Override + public List<Translation> getAllTranslations() { + return translationRepository.getAllTranslations(); + } + + @Override + public List<Translation> getTranslationsByLanguage(String language) { + return translationRepository.getTranslationsByLanguage(language); + } + + @Override + public Translation getTranslationByKeyAndLanguage(String key, String language) { + return translationRepository.getTranslationByKeyAndLanguage(key, language); + } + + @Override + public void addTranslation(Translation translation) { + translationRepository.addTranslation(translation); + } + + @Override + public void updateTranslation(int id, Translation translation) { + translationRepository.updateTranslation(id, translation); + } + + @Override + public void deleteTranslation(int id) { + translationRepository.deleteTranslation(id); + } + + @Override + public List<String> getAllTranslationKeys() { + return translationRepository.getAllTranslationKeys(); + } + + @Override + public List<Translation> getTranslationsByKey(String key) { + return translationRepository.getTranslationsByKey(key); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e12924f4d009b519539abb5995545d2526ee5dcd..b4135df2a5086e373049a590b80c2075d8fe6423 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -18,10 +18,13 @@ 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.sql.init.data-locations=classpath:/data.sql, classpath:/data_information_domains.sql, classpath:/translations.sql #spring.jpa.hibernate.ddl-auto=none spring.jpa.defer-datasource-initialization=true - +spring.servlet.multipart.enabled=true +spring.servlet.multipart.max-file-size=10MB +spring.servlet.multipart.max-request-size=10MB +file.upload-dir=uploads diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 50fdd5a36fa3d207c55898969b61bdf30c4624e5..439ca31f77ddffe56e637f61db79e9f2373ef918 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -15,26 +15,6 @@ VALUES delete from event; -insert into event (event_title, event_description, location, event_date, event_time,user_id, event_poster_url,whyJoin,benefits) -values ('Science Fair', 'Students explore through the science fair', 'Cardiff', current_date,current_time, 1, 'https://marketplace.canva.com/EAE53TNAVD8/1/0/1131w/canva-event-present-science-fair-promotion-poster-1abqT-GiCNQ.jpg','Participating in the Science Fair offers you a unique opportunity to dive deep into the world of science, innovation, and discovery. Whether you''re a student eager to showcase your scientific knowledge or an individual with a passion for learning, this event is the perfect platform to fuel your curiosity. Here’s why you should join: -Engage with Innovative Ideas: Explore cutting-edge scientific projects that challenge the status quo and inspire new ways of thinking. -Collaborate with Like-Minded Individuals: Meet fellow science enthusiasts, students, and professionals who share your interests and passion for discovery. -Boost Your Critical Thinking: Through the process of research, experimentation, and presentation, you’ll develop critical problem-solving skills that are invaluable in any field. -Expand Your Knowledge: Learn about new technologies, scientific theories, and groundbreaking advancements that will shape the future of science and innovation. -Be a Part of a Larger Community: Join a global community of science advocates and future scientists, making valuable connections that could open doors to future opportunities.', 'Hands-On Experience: Gain practical, hands-on experience in the scientific method, from research and hypothesis testing to data analysis and presentation. -Develop Presentation Skills: Sharpen your ability to communicate complex scientific concepts in an engaging and accessible way, an essential skill for any future career. -Exposure to Career Opportunities: Connect with professionals in science, education, and industry, opening up potential career pathways, internships, and scholarships. -Recognition and Prizes: Stand a chance to win awards and gain recognition for your hard work and creativity. Whether you’re awarded for your innovation, research, or presentation, your efforts will be acknowledged. -Confidence Building: Presenting your work to peers, teachers, and judges builds confidence in your abilities and boosts self-esteem, allowing you to grow as both a scientist and an individual. -Inspire Future Projects: Your participation could spark new ideas and motivate others to start their own scientific endeavors, contributing to a culture of curiosity and learning. -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'); -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'); - - -- mock data for news delete from news; INSERT INTO news (news_title, news_summary, news_source, news_link, news_image_url, user_id, news_upload_date) @@ -74,16 +54,6 @@ VALUES 5, current_date); --- mock data for events -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=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=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=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'); @@ -105,16 +75,18 @@ INSERT INTO users (email, password, fullname, dob, role_id, organization) VALUES INSERT INTO users (email, password, fullname, dob, role_id) -VALUES ('user@email.com', 'Abcdef1!', 'Jane Doe', '200-01-01', 1 ); +VALUES ('user@email.com', 'Abcdef1!', 'Jane Doe', '2000-01-01', 1 ); +UPDATE user_profile SET bio = 'Jane''s bio' WHERE user_id = (SELECT id from users where email = 'user@email.com'); + -- 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); + ('uploads/36b2c38d-c9d5-4e14-a433-895a565d3abf_steve-johnson-D7AuHpLxLPA-unsplash.jpg', 'Post 1', 'Description for post 1', '2024-12-07', 1), + ('uploads/5720f047-a3ca-4d4e-ab20-343aae7cc485_premium_photo-1733514691627-e62171fc052c.avif', 'Post 2', 'Description for post 2', '2024-12-07', 2), + ('uploads/d0753820-30b3-429c-92c4-d82c910ba083_nicolas-jehly-0UU9-_1EMvM-unsplash.jpg', 'Post 3', 'Description for post 3', '2024-12-07', 3); -- Insert tags INSERT INTO tags (tag_name) VALUES @@ -139,3 +111,29 @@ INSERT INTO post_likes (post_id, user_id) VALUES (2, 2), (2, 4); +insert into event (event_title, event_description, location, event_date, event_time,user_id, event_poster_url,whyJoin,benefits) +values ('Science Fair', 'Students explore through the science fair', 'Cardiff', current_date,current_time, 1, 'https://marketplace.canva.com/EAE53TNAVD8/1/0/1131w/canva-event-present-science-fair-promotion-poster-1abqT-GiCNQ.jpg','Participating in the Science Fair offers you a unique opportunity to dive deep into the world of science, innovation, and discovery. Whether you''re a student eager to showcase your scientific knowledge or an individual with a passion for learning, this event is the perfect platform to fuel your curiosity. Here’s why you should join: +Engage with Innovative Ideas: Explore cutting-edge scientific projects that challenge the status quo and inspire new ways of thinking. +Collaborate with Like-Minded Individuals: Meet fellow science enthusiasts, students, and professionals who share your interests and passion for discovery. +Boost Your Critical Thinking: Through the process of research, experimentation, and presentation, you’ll develop critical problem-solving skills that are invaluable in any field. +Expand Your Knowledge: Learn about new technologies, scientific theories, and groundbreaking advancements that will shape the future of science and innovation. +Be a Part of a Larger Community: Join a global community of science advocates and future scientists, making valuable connections that could open doors to future opportunities.', 'Hands-On Experience: Gain practical, hands-on experience in the scientific method, from research and hypothesis testing to data analysis and presentation. +Develop Presentation Skills: Sharpen your ability to communicate complex scientific concepts in an engaging and accessible way, an essential skill for any future career. +Exposure to Career Opportunities: Connect with professionals in science, education, and industry, opening up potential career pathways, internships, and scholarships. +Recognition and Prizes: Stand a chance to win awards and gain recognition for your hard work and creativity. Whether you’re awarded for your innovation, research, or presentation, your efforts will be acknowledged. +Confidence Building: Presenting your work to peers, teachers, and judges builds confidence in your abilities and boosts self-esteem, allowing you to grow as both a scientist and an individual. +Inspire Future Projects: Your participation could spark new ideas and motivate others to start their own scientific endeavors, contributing to a culture of curiosity and learning. +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'); +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'); +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=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=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=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='); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index c5da366ffb793f6af4587d797c6ec79b54c92a46..e54eea227e7ed0aadfd69be3ed6131ed0f894b78 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -9,9 +9,11 @@ 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_profile; DROP TABLE IF EXISTS user_roles; DROP TABLE IF EXISTS users; DROP TABLE IF EXISTS roles; +DROP TABLE IF EXISTS translations; create table if not exists categories ( category_id INT AUTO_INCREMENT PRIMARY KEY, @@ -39,22 +41,6 @@ create table if not exists information ) engine = InnoDB; --- create schema for event - -create table if not exists event( - event_id int primary key auto_increment, - event_title varchar(255), - event_description varchar(255), - location varchar(255), - event_date date, - event_time time, - user_id int, - event_poster_url varchar(255), - whyJoin text, - benefits text - )engine = InnoDB; - - -- schema for news create table if not exists news @@ -83,8 +69,9 @@ create table if not exists users ( password VARCHAR(255) NOT NULL, fullname VARCHAR(255) NOT NULL, dob DATE, - role_id INT NOT NULL, + role_id INT NOT NULL default 2, organization varchar(255), + enabled boolean default true, FOREIGN KEY (role_id) REFERENCES roles(id) ); @@ -137,7 +124,7 @@ create table if not exists post_likes ( CREATE TABLE IF NOT EXISTS comment ( - id INT AUTO_INCREMENT PRIMARY KEY, + id INT AUTO_INCREMENT PRIMARY KEY, comment_content TEXT NOT NULL, created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, user_id INT NOT NULL, @@ -145,3 +132,44 @@ CREATE TABLE IF NOT EXISTS comment FOREIGN KEY (post_id) REFERENCES feed(post_id), FOREIGN KEY (user_id) REFERENCES users (id) ) ENGINE = InnoDB; + +create table if not exists translations ( + id int auto_increment primary key, + translation_key varchar(255) not null, + language varchar(10) not null, + value text not null +) engine = InnoDB; + +ALTER TABLE translations MODIFY value TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- create schema for event + +create table if not exists event( + event_id int primary key auto_increment, + event_title varchar(255), + event_description varchar(255), + location varchar(255), + event_date date, + event_time time, + user_id int, + event_poster_url varchar(255), + whyJoin text, + benefits text, + foreign key event_user_id_fk (user_id) references users(id) +) engine = InnoDB; + +CREATE TABLE IF NOT EXISTS user_profile ( + user_id INT PRIMARY KEY NOT NULL, + profile_picture TEXT, + bio TEXT, + phone_number VARCHAR(20), + show_phone_number BOOLEAN DEFAULT FALSE, + show_dob BOOLEAN DEFAULT FALSE, + FOREIGN KEY (user_id) REFERENCES users(id) + ON DELETE CASCADE + ON UPDATE CASCADE +) engine = InnoDB; + +CREATE TRIGGER UMustHaveProfile AFTER INSERT ON users FOR EACH ROW INSERT INTO user_profile (user_id) VALUES (NEW.id); + + diff --git a/src/main/resources/static/assets/default-profile.jpg b/src/main/resources/static/assets/default-profile.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7a8a84327d080b877818d9fb2f528a5e4784c790 Binary files /dev/null and b/src/main/resources/static/assets/default-profile.jpg differ diff --git a/src/main/resources/static/css/admin/adminBoardStyles.css b/src/main/resources/static/css/admin/adminBoardStyles.css new file mode 100644 index 0000000000000000000000000000000000000000..b128e8d32476e221dc35f35fc9c7b65ca855298d --- /dev/null +++ b/src/main/resources/static/css/admin/adminBoardStyles.css @@ -0,0 +1,119 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +.header { + text-align: center; + margin-bottom: 20px; +} + +.header h1 { + font-size: 24px; + color: #333; +} + +.statistics { + display: flex; + justify-content: center; + gap: 1rem; + margin-bottom: 20px; +} + +.statistics .stat strong{ + font-size: 1.5rem; +} + +.statistics .stat aside{ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.statistics .stat { + display: flex; + justify-content: center; + gap:1rem; + align-items: center; + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 8px; + padding: 1rem 2rem; + height: 8rem; + width: 15.8rem; + text-align: center; + font-size: 1rem; + color: #333; +} + +i{ + font-size: 2rem; +} + +.dashboard { + display: flex; + flex-wrap: wrap; + gap: 1.2rem; + justify-content: center; +} + +.card { + background-color: #fff; + border: 1px solid #ddd; + border-radius: 8px; + padding: 2rem; + width: 21.3rem; + height: 12rem; + text-align: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: transform 0.2s; +} + +.card:hover { + transform: translateY(-5px); +} + +.card h3 { + font-size: 18px; + color: #333; + margin-bottom: 10px; +} + +.card p { + font-size: 14px; + color: #666; + margin-bottom: 15px; +} + +.card button { + background-color: #000; + color: #fff; + border: none; + border-radius: 4px; + padding: 10px 20px; + cursor: pointer; + font-size: 14px; +} + +.card button:hover { + background-color: #444; +} + +.footer { + text-align: center; + margin-top: 30px; + font-size: 14px; + color: #666; +} + +.footer a { + color: #000; + text-decoration: none; + margin: 0 10px; +} + +.footer a:hover { + text-decoration: underline; +} \ No newline at end of file diff --git a/src/main/resources/static/css/admin/userManageStyles.css b/src/main/resources/static/css/admin/userManageStyles.css new file mode 100644 index 0000000000000000000000000000000000000000..7c7e3b854ed23233e0c354e127f85670ae048b70 --- /dev/null +++ b/src/main/resources/static/css/admin/userManageStyles.css @@ -0,0 +1,43 @@ +table { + width: 100%; + border-collapse: collapse; + padding: 20px; +} +section{ + padding: 2rem; +} +.table-wrap{ + border-radius: 10px; +} + +th, td { + padding: 10px; + border: 1px solid #ccc; + text-align: center; +} +th { + background-color: #f4f4f4; +} +.btn { + padding: 10px 20px; + margin: 5px; + cursor: pointer; + border: none; + background-color: var(--primary-color); + color: var(--secondary-color); + border-radius: 5px; +} +.btn-disable { + background-color: #f44336; +} + +select { + width: 6rem; + padding: 10px; + border: 1px solid #ccc; + border-radius: 5px; +} + +option { + padding: 5px 10px; +} \ No newline at end of file diff --git a/src/main/resources/static/css/contact/contact.css b/src/main/resources/static/css/contact/contact.css new file mode 100644 index 0000000000000000000000000000000000000000..8ee14877241316282d20e2150c5065e9892c3bf2 --- /dev/null +++ b/src/main/resources/static/css/contact/contact.css @@ -0,0 +1,178 @@ +/* General Styles */ +body { + margin: 0; + padding: 0; + box-sizing: border-box; + background-color: #f4f4f9; + font-family: Arial, sans-serif; + padding-top: 80px; +} + +#title{ + + color: black; + text-align: center; + font-size: 35px; + margin-bottom: 30px; + font-weight: bold; + +} + +#contact-form { + margin: 20px auto; + max-width: 900px; + border-radius: 10px; + background-color: transparent; +} + +/* FAQ Section */ +#faq { + padding: 20px; +} + +.faq-question { + cursor: pointer; + font-weight: bold; + color: #333; +} + +.faq-question:hover { + color: #4CAF50; +} + +.faq-answer { + display: none; + color: #666; + padding-left: 20px; +} + +#map { + display: none; +} + +#contact-form { + display: flex; + justify-content: space-between; + align-items: flex-start; + text-align: left; + margin-top: 20px; + gap: 30px; +} + +#contact-form-container { + width: 50%; + padding: 30px; + background-color: transparent; + border: 1px solid #ddd; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +#contact-form-container h3 { + margin-bottom: 20px; + color: #333; +} + +#contact-info-container { + width: 50%; + padding: 30px; + background-color: transparent; + border: 1px solid #ddd; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +#contact-info-container h3 { + margin-bottom: 20px; + color: #333; +} + +#contactForm label { + display: block; + margin-bottom: 8px; + font-weight: bold; + color: #333; +} + +#contactForm input, #contactForm textarea { + width: 100%; + padding: 10px; + margin-bottom: 15px; + border: 1px solid #ccc; + border-radius: 5px; + box-sizing: border-box; + font-size: 16px; + color: #333; +} + +#contactForm input:focus, #contactForm textarea:focus { + border-color: #007BFF; + outline: none; + box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); +} + +#contactForm button { + display: inline-block; + padding: 10px 20px; + font-size: 16px; + color: #fff; + background-color: black; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; +} + +#contactForm button:hover { + background-color: #333; +} + +#formFeedback { + margin: 10px; + font-size: 14px; + color: grey; +} + +body, .faq-answer, #contactForm input, #contactForm textarea { + font-size: 14px; + color: #666; +} + +h2, h3, .faq-question { + color: #333; +} + +/* Media Queries for Mobile Optimization */ +@media (max-width: 768px) { + /* Stack Contact Form and Contact Info Vertically on Smaller Screens */ + #contact-form { + flex-direction: column; + } + + #contact-form-container, #contact-info-container { + width: 100%; + margin-bottom: 20px; + } +} + +@media (max-width: 480px) { + #faq h2 { + font-size: 20px; + } + + #contact-form h2 { + font-size: 18px; + } + + .faq-question { + font-size: 16px; + } + + #contact-form input, #contact-form textarea { + font-size: 14px; + } + + #contact-form button { + font-size: 14px; + } +} diff --git a/src/main/resources/static/css/events/add-event.css b/src/main/resources/static/css/events/add-event.css new file mode 100644 index 0000000000000000000000000000000000000000..8c504a663f9d796ad5d9bcb75d1091e09598e0bd --- /dev/null +++ b/src/main/resources/static/css/events/add-event.css @@ -0,0 +1,122 @@ +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="date"], input[type="time"]{ + margin-bottom: 10px; + width: 80%; + padding: 10px; + box-sizing: border-box; + border-radius: 4px; + border: 1px solid #ccc; +} + +input[type="date"]:focus, input[type="time"]:focus { + /*border-color: 1px solid var(--primary-color);*/ + box-shadow: 0 0 5px var(--border-color); + outline: solid var(--border-color); +} + +fieldset > input[type="text"], textarea { + width: 95%; + padding: 10px; + margin-bottom: 10px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 16px; +} + +input[type="text"]:focus, textarea:focus{ + outline: solid var(--border-color); +} + +.event-date-and-location-card{ + display: flex; + justify-content: space-between; + width: 95%; +} +.event-date-card, .event-time-card{ + flex:1; +} +.event-location-card{ + flex: 2; +} +.event-location-input{ + width: 100%; + padding: 10px; + margin-bottom: 10px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 16px; +} + +textarea { + height: 100px; +} +.buttons { + display: flex; + justify-content: flex-end; + width: 97%; +} +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(--alternate-text); +} +button:hover { + opacity: 0.7; +} + +a.button { + display: inline-block; + padding: 10px 20px; + margin-left: 10px; + font-size: 16px; + text-align: center; + text-decoration: none; + cursor: pointer; +} + +.backButton{ + color: black; + text-decoration: none; +} + + +a.button:hover { + opacity: 0.7; +} + +.alert{ + color: red; + display: block; + height: auto; + margin: 2px 0 10px; +} \ No newline at end of file diff --git a/src/main/resources/static/css/events/editEvent.css b/src/main/resources/static/css/events/editEvent.css new file mode 100644 index 0000000000000000000000000000000000000000..ba9e567176848d63a69f14c48529c1c5718e69a6 --- /dev/null +++ b/src/main/resources/static/css/events/editEvent.css @@ -0,0 +1,128 @@ +/* General Styles */ +/* Center the form on the screen */ +section { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + /*height: 100vh;*/ + padding: 0 20px; +} + +/* Form styling */ +form { + background-color: var(--secondary-color); + border-radius: 10px; + padding: 30px; + width: 100%; + max-width: 600px; + box-sizing: border-box; + margin: auto; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Add some shadow to give the form a floating effect */ +} + +/* Header */ +h1 { + text-align: left; + margin-bottom: 20px; + font-size: 24px; + color: #333; +} + +/* Label styling */ +label { + display: block; + font-weight: bold; + margin-bottom: 10px; + color: #333; + font-size: 16px; +} + +/* Input and Textarea Styling */ +input[type="text"], input[type="date"], input[type="time"], input[type="url"], textarea { + width: 100%; + padding: 12px; + margin-bottom: 20px; + border: 1px solid var(--border-color); + border-radius: 5px; + font-size: 16px; + color: #333; + background-color: var(--background-color); /* Light background for inputs */ +} + +textarea { + resize: vertical; + min-height: 120px; +} + +/* Alert message styling for errors */ +.alert { + color: var(--secondary-color); + background-color: var(--border-color); + padding: 5px; + border-radius: 5px; + font-size: 14px; +} + +.alert-warning { + color: #8a6d3b; + background-color: #fcf8e3; +} + +/* Button styling */ +.buttons { + display: flex; + justify-content: space-between; + gap: 15px; +} + +/* Submit and Cancel button styling */ +button.submit { + background-color: #5bc0de; + color: white; + border: none; + padding: 12px 20px; + font-size: 16px; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; +} + +button.submit:hover { + background-color: #31b0d5; +} + +#cancelButton { + background-color: #f44336; + color: white; + border: none; + padding: 12px 20px; + font-size: 16px; + border-radius: 5px; + cursor: pointer; + text-decoration: none; /* Remove underline */ + display: inline-block; + text-align: center; + transition: background-color 0.3s; +} + +#cancelButton:hover { + background-color: #e53935; +} + +/* Responsive Styles */ +@media (max-width: 768px) { + section { + padding: 20px; + } + + form { + width: 100%; + max-width: 90%; + padding: 20px; + } + + h1 { + font-size: 20px; + } +} diff --git a/src/main/resources/static/css/events/event-detail.css b/src/main/resources/static/css/events/event-detail.css index 142d7f9a263a06a7ec9c37d33dc24c4a30c08bbc..266d18d6a1c5fb7563668be4af1409e667b8388b 100644 --- a/src/main/resources/static/css/events/event-detail.css +++ b/src/main/resources/static/css/events/event-detail.css @@ -1,30 +1,12 @@ -/* Basic reset to ensure consistent styling across browsers */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: Arial, sans-serif; - background-color: #E3E2DF; - display: flex; - justify-content: center; - align-items: flex-start; - height: 100vh; /* Full height of the viewport */ - margin: 0; - padding-top: 10px; -} - #event-detail-container { - width: 80%; - max-width: 90%; - background-color: #fff; + width: 100%; + background-color: var(--secondary-color); padding: 30px; - border-radius: 10px; - box-shadow: 0 0 15px 2px #E3AFBC; - margin-top: 2rem; - background-image: linear-gradient(to right, #e3e2df,white); +} + +.backButton{ + color: var(--text-color); + text-decoration: none; } .event-detail-card { @@ -34,7 +16,7 @@ body { .event-detail-card h1 { font-size: 2rem; - color: #5D001E; + color: var(--text-color); margin-bottom: 20px; } @@ -48,14 +30,14 @@ body { .event-image { width: 80%; /* Resizes the image to 80% of the container */ height: auto; - border-radius: 8px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + border-radius: 10px; + box-shadow: 0 4px 6px var(--border-color); } /* Style for event description, date, and location */ p { font-size: 1rem; - color: #555; + color: var(--secondary-text-color); line-height: 1.6; margin-bottom: 15px; } @@ -72,15 +54,15 @@ strong { .event-benefits { margin: 20px 0; /* Adds vertical spacing between sections */ padding: 15px; /* Adds padding inside the sections */ - background-color: #E3E2DF; /* White background for each section */ - border: 1px solid #E3AFBC; /* Light border to separate sections */ + background-color: var(--border-color); /* White background for each section */ + border: 1px solid var(--primary-color); /* Light border to separate sections */ border-radius: 8px; /* Rounded corners for the section */ } /* Styling for the headings inside the event-benefits sections */ .event-benefits h2 { font-size: 24px; /* Sets a larger font size for the headings */ - color: #EE4C7C; /* Blue color for the headings */ + color: var(--text-color); /* Blue color for the headings */ margin-bottom: 10px; /* Adds space below the heading */ } @@ -106,8 +88,8 @@ strong { .register-button { - background-color: #EE4C7C; - color: white; + background-color: var(--primary-color); + color: var(--secondary-color); border: none; padding: 10px 20px; font-size: 1rem; @@ -117,5 +99,6 @@ strong { } .register-button:hover { - background-color: #9A1750; + background-color: var(--border-color); + color: var(--text-color); } diff --git a/src/main/resources/static/css/events/event.css b/src/main/resources/static/css/events/event.css index 5127df5ebfbfc3c6342d61c938679865adfd118a..33f983dd2c3a4d5c64d7db19d583893a87581f75 100644 --- a/src/main/resources/static/css/events/event.css +++ b/src/main/resources/static/css/events/event.css @@ -1,17 +1,46 @@ /* General Styles */ -body { + +section { font-family: Arial, sans-serif; - margin: 0; - padding: 0; - background-color: #E3E2DF; + /*padding: 20px;*/ +} + +.general-headings-layout h1{ + font-weight: bolder; + font-size:xx-large; +} + +.general-headings-layout button{ + padding: 10px 15px; + border: none; + font-size: 14px; + cursor: pointer; + border-radius: 5px; + background-color: var(--primary-color); + color: var(--alternate-text); +} + +.general-headings-layout button:hover { + background-color: var(--border-color); + color: var(--text-color); + border: 2px solid var(--primary-color); } /* Main container for event details */ #event-details { - padding: 20px; - background-color: #E3E2DF; + /*padding: 20px;*/ + margin: 0 5% 2% 5%; + background-color: var(--background-color); } +.general-headings-layout{ + display: flex; + align-items: center; + justify-content: space-between; + margin: 0 3%; +} + + /* Event grid container for dynamically generated cards */ .event-grid { display: grid; @@ -23,26 +52,24 @@ body { /* Styling each event card */ .event-card { - background: #fff; - border: 1px solid #ddd; + background: var(--secondary-color); + border: 2px solid var(--border-color); border-radius: 10px; - overflow: hidden; + /*overflow: hidden;*/ transition: transform 0.3s ease, box-shadow 0.3s ease; - width: 100%; - max-width: 300px; /* Ensures cards do not get too wide */ - box-shadow: 0 0 15px 2px #E3AFBC; + min-width: 60%; + max-width: 80%; /* Ensures cards do not get too wide */ + box-shadow: 0 0 15px 2px var(--primary-color); } /* Hover effect for the card */ .event-card:hover { transform: translateY(-10px); - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); + box-shadow: 0 8px 16px var(--border-color); } /* Event poster styling */ .event-poster { - position: relative; - overflow: hidden; height: 200px; justify-content: center; align-items: center; @@ -51,14 +78,15 @@ body { .event-poster img { width: 100%; height: 100%; - object-fit: cover; /* Ensures the image covers the container */ + object-fit: fill; + /* Ensures the image covers the container */ } /* Title styling */ .event-card h2, .event-card h3 { font-size: 1.5em; - color: #5D001E; + color: var(--primary-color); margin: 10px 0; text-align: center; } @@ -70,41 +98,52 @@ body { margin: 10px; } -/* Remove underline from links with the class 'event-link' */ -.event-link { - text-decoration: none; /* Removes the underline */ +/* Event buttons container */ +.event-card-button { + display: flex; + /*flex-direction: column;*/ + align-items: center; /* Center buttons horizontally */ + /*gap: 10px; !* Adds space between buttons *!*/ } -/* Register button styling */ -.register-button { - display: block; - width: 50%; /* Ensures the button spans the full width of its container */ - max-width: 300px; /* You can set a max-width to control the button size */ + +/* Common button styling for both buttons */ +.event-card-button button { + max-width: 250px; /* Controls the button size */ padding: 10px; margin: 10px auto; /* Horizontally centers the button and adds vertical spacing */ - background-color: #EE4C7C; - color: #fff; + background-color: var(--primary-color); + color: var(--secondary-color); border: none; border-radius: 5px; cursor: pointer; font-size: 1em; + transition: background-color 0.3s ease, transform 0.3s ease; /* Add transition effects */ } .register-button:hover { - background-color: #9A1750; + background-color: var(--border-color); + color: var(--primary-color); } -/* Responsive adjustments */ +/* Hover effect for the buttons */ +.event-card-button button:hover { + background-color: var(--border-color); + transform: scale(1.05); /* Slight scaling effect on hover */ +} -/* On medium screens, switch to a 2-column layout */ -@media (max-width: 768px) { - .event-grid { - grid-template-columns: repeat(2, 1fr); - } +/* Remove underline from links with the class 'event-link' */ +.event-link { + text-decoration: none; /* Removes the underline */ + margin:auto; } -/* On smaller screens, switch to a single column layout */ -@media (max-width: 480px) { - .event-grid { - grid-template-columns: 1fr; /* Single column for very small screens */ - } +.event-link button { + max-width: 250px; /* Controls the button size */ + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 1em; +} +.event-link button:hover{ + color: var(--text-color); } diff --git a/src/main/resources/static/css/feed/feed.css b/src/main/resources/static/css/feed/feed.css index a32014b87edfb7c3226bad46b1482207eaa7fa89..df8ce1011508b92a93e215b566685363e0caaab5 100644 --- a/src/main/resources/static/css/feed/feed.css +++ b/src/main/resources/static/css/feed/feed.css @@ -34,12 +34,35 @@ gap: 20px; } +.post-header { + display: flex; + align-items: center; + justify-content: space-between; +} + .author-details{ display: flex; gap: 10px; align-items: center; } +.post-manipulation > button { + border: none; + background-color: transparent; + cursor: pointer; +} + +.post-manipulation { + display: flex; + gap: 15px; +} +.delete-post { + color: var(--tertiary-color); +} +.edit-post { + color: var(--secondary-text-color); +} + .profile-picture{ width: 46px; height: 46px; @@ -155,12 +178,12 @@ using the same styles used for the create new category modal, since we did not u we just copy paste in order not to affect other form styles. */ -.create-new-modal { +.create-new-modal, .share-modal { + position: fixed; 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; @@ -169,22 +192,52 @@ we just copy paste in order not to affect other form styles. overflow: auto; } -.modal-content { +.modal-content, .share-box { background: var(--background-color); padding: 24px; border-radius: 10px; } -.modal-header { +.modal-header , .share-controls{ display: flex; margin-bottom: 20px; } + +.share-options { + display: flex; + gap: 15px; + align-items: center; + font-size: 36px; +} + +.share-options > button { + border: none; + background: none; + cursor: pointer; + font-size: 32px; +} +.share-options > a { + text-decoration: none !important; + color: var(--primary-color); +} + +.share-controls { + width: 100%; + +} + +.share-controls > button { + border: none; + background: none; + cursor: pointer; + font-size: 16px; +} .modal-title-desc{ flex: 1; } .modal-title-desc> h3 { - font-size: 16px; + font-size: 1rem; } .modal-title-desc > p{ @@ -231,4 +284,9 @@ form > label > input,textarea { #closeModalBtn { cursor: pointer; -} \ No newline at end of file +} + +.like-button.liked { + color: #007bff; + font-weight: bold; +} diff --git a/src/main/resources/static/css/headings/headings.css b/src/main/resources/static/css/headings/headings.css index 7c529b08c7cdd973682acac509c4cd925cd2cc11..547d068352bfa01d97e645c8d7e79d2b32e5ab71 100644 --- a/src/main/resources/static/css/headings/headings.css +++ b/src/main/resources/static/css/headings/headings.css @@ -6,7 +6,7 @@ } button { - padding: 10px 15px; + padding: 1rem 1rem; border: none; font-size: 14px; cursor: pointer; @@ -86,6 +86,5 @@ input[type="checkbox" i] { #delete-button{ visibility: hidden; float: right; - margin: 5px 0; - + /*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 6f44f6bdcbce25eec369bb2574edb44a21ea10f7..1694ebbdf2ebaf8a8729a7c1315737c4e3322c58 100644 --- a/src/main/resources/static/css/home/home.css +++ b/src/main/resources/static/css/home/home.css @@ -1,18 +1,9 @@ -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; + display: flex; + flex-direction: column; + align-items: center; } @@ -139,7 +130,7 @@ button:hover { } #eventsTile { - background: linear-gradient(135deg, #FF7EB3, #FF758C); + background: linear-gradient(135deg, yellowgreen, #006400); color: white; } diff --git a/src/main/resources/static/css/layout/layout.css b/src/main/resources/static/css/layout/layout.css index 921144a67e6f3796047d70e4689c0ab806517c5a..d5e14947d5224ce34fc41ecd7de8620af43363bb 100644 --- a/src/main/resources/static/css/layout/layout.css +++ b/src/main/resources/static/css/layout/layout.css @@ -23,6 +23,7 @@ body { min-height: 100vh; /* Ensures the body covers the viewport height */ display: flex; flex-direction: column; + margin: 0; } .main-content { @@ -131,8 +132,7 @@ nav.sidebar { } .footer { - position: relative; - width: 100%; + position: relative; display: grid; grid-template-columns: 1fr 1fr 1fr; /* grid layout */ grid-template-rows: auto auto; @@ -216,4 +216,39 @@ nav.sidebar { .navText { font-size: 14px; font-weight: 500; -} \ No newline at end of file +} + + +/* ACCOUNT OPTIONS STYLING */ +/* Dropdown styles */ +.dropdown { + position: relative; + display: inline-block; +} + +.dropdown-content { + display: none; + position: absolute; + background-color: #f9f9f9; + min-width: 160px; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 1; +} + +.dropdown-content a { + color: black; + padding: 12px 16px; + text-decoration: none; + display: block; +} + +.dropdown-content a:hover { + background-color: #f1f1f1; +} + +.dropdown:hover .dropdown-content { + display: block; +} + + +/* ACCOUNT OPTIONS STYLING END */ \ No newline at end of file diff --git a/src/main/resources/static/css/login/login.css b/src/main/resources/static/css/login/login.css index b45f963121391a552ad0d7a3cf649ed3c897f8a6..96e7ddcb729e044d795540de7359bd4fa9b29761 100644 --- a/src/main/resources/static/css/login/login.css +++ b/src/main/resources/static/css/login/login.css @@ -1,21 +1,36 @@ * { - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif + font-family: "inter", sans-serif; } form { - justify-items: end; - padding: 10px; + justify-items: baseline; + padding: 30px; max-width: max-content; + width: 300px; box-sizing: border-box; - margin-top: 15px; - background-color: #e3afbc; - border: 1px solid #ccc; + margin: 50px auto; + /*margin-top: 15px;*/ + background-color: var(--secondary-color); + border: 1px solid var(--border-color); border-radius: 5px; } -input { - background-color: #e3e2df; - border: 1px solid #ccc; +input[type="email"], input[type="password"]{ + background-color: var(--background-color); + border: 1px solid var(--border-color); border-radius: 5px; + padding: 10px 15px; + width: 80%; + margin-bottom: 10px; +} +input[type="submit"] { + background-color: var(--primary-color); + color: var(--alternate-text); + cursor: pointer; + transition: background-color 0.3s ease; +} + +input[type="submit"]:hover { + background-color: #555; } \ No newline at end of file diff --git a/src/main/resources/static/css/news/editNewsStyle.css b/src/main/resources/static/css/news/editNewsStyle.css index 789ca2f6a311df6145159d66a7af1319505ae0ed..0b575d42276f870de0cd3951de929305e89ca890 100644 --- a/src/main/resources/static/css/news/editNewsStyle.css +++ b/src/main/resources/static/css/news/editNewsStyle.css @@ -1,9 +1,3 @@ -body{ - font-family: Arial, sans-serif; - margin: 0; - padding: 0; -} - .form-container { display: flex; justify-content: center; diff --git a/src/main/resources/static/css/news/newsStyles.css b/src/main/resources/static/css/news/newsStyles.css index 97d67379050ab7b6e04020c1c14411bc1de7df95..d9abc1ac9ae2764b2f1b0800e9f9278761406bc3 100644 --- a/src/main/resources/static/css/news/newsStyles.css +++ b/src/main/resources/static/css/news/newsStyles.css @@ -1,19 +1,14 @@ -body { +.content{ font-family: Arial, sans-serif; - margin: 0; - padding: 0; - background-color: white; - color: var(--text-color); + padding: 20px; } - .general-headings-layout{ display: flex; /*margin-top: 5%;*/ align-items: center; justify-content: space-between; - /*margin-bottom: 20px;*/ - margin: 5% 8% 0 8%; + margin: 0 8% 0 8%; } h1 { diff --git a/src/main/resources/static/css/profile/profile.css b/src/main/resources/static/css/profile/profile.css new file mode 100644 index 0000000000000000000000000000000000000000..fed39bcd6e966147b770fc5ad97cf89c71312965 --- /dev/null +++ b/src/main/resources/static/css/profile/profile.css @@ -0,0 +1,105 @@ +.profile-container { + display: flex; + justify-content: center; + align-items: center; + min-height: calc(100vh - 64px - 100px); /* Adjust for header and footer */ + padding: 20px; +} + +.profile-card { + background-color: var(--secondary-color); + border-radius: 15px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + padding: 30px; + width: 100%; + max-width: 400px; + text-align: center; +} +.profile-picture { + width: 150px; + height: 150px; + border-radius: 50%; + object-fit: cover; + margin-bottom: 20px; + border: 3px solid var(--border-color); +} + +.full-name { + font-size: 24px; + margin-bottom: 10px; + color: var(--text-color); +} +.email, #edit-email { + font-size: 14px; + color: var(--text-color); + margin-bottom: 15px; +} +.organisation { + font-size: 16px; + color: var(--secondary-text-color); + margin-bottom: 15px; +} +/*#edit-organisation {*/ +/* color: var(--text-color);*/ +/*}*/ +.optional-field, #edit-bio, #edit-phone-number, #edit-phone-number { + font-size: 14px; + color: var(--text-color); + margin-bottom: 10px; +} + +.edit-profile-btn { + background-color: var(--secondary-text-color); + color: var(--text-color); + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +#edit-profile-form input[type="text"], +#edit-profile-form input[type="tel"], +#edit-profile-form textarea { + width: 100%; + padding: 8px; + margin-bottom: 10px; + border: 1px solid var(--secondary-text-color); + border-radius: 5px; + background-color: var(--background-color); + color: var(--text-color); +} + +.checkbox-container { + display: flex; + justify-content: space-between; + margin-bottom: 10px; +} +.not-editable { + cursor: not-allowed; + background-color: var(--secondary-color) !important; +} + +@media (max-width: 768px) { + .profile-card { + padding: 20px; + } + + .profile-picture { + width: 120px; + height: 120px; + } + + .full-name{ + font-size: 20px; + } + + .email, #edit-email { + font-size: 14px; + } + + .optional-field, #edit-bio, #edit-phone-number{ + font-size: 12px; + } +} + diff --git a/src/main/resources/static/images/logo.png b/src/main/resources/static/images/logo.png deleted file mode 100644 index b7aa35736858285f7f535c77181c595e2018765a..0000000000000000000000000000000000000000 Binary files a/src/main/resources/static/images/logo.png and /dev/null differ diff --git a/src/main/resources/static/js/admin/admin.js b/src/main/resources/static/js/admin/admin.js new file mode 100644 index 0000000000000000000000000000000000000000..19d73c0a19f7034fb081e1e09345af925f98a0b1 --- /dev/null +++ b/src/main/resources/static/js/admin/admin.js @@ -0,0 +1,66 @@ +// Function to enable or disable a user +function toggleUserEnabled(userId, enable) { + //check current status and do the alternate + enable = enable !== true; + const url = `/admin/edit/${userId}/enabled?enabled=${enable}`; + + // Use Fetch API to send the PUT request + fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + credentials:"include" + }) + .then(response => { + if (response.ok) { + // Reload the page to reflect the changes + location.reload(); + } else { + alert('Failed to update user status.'); + } + }) + .catch(error => { + console.error('Error:', error); + alert('Error occurred while updating user status.'); + }); +} + +function updateUserRole(userId){ + const roleSelect = document.getElementById('roleDdl'); + const selectedValue = roleSelect.value; + const url = `/admin/edit/${userId}/role`; + + // Use Fetch API to send the PUT request + fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + credentials:"include", + body:JSON.stringify(selectedValue) + }) + .then(response => { + if (response.ok) { + // Reload the page to reflect the changes + location.reload(); + } else { + alert('Failed to update user role.'); + } + }) + .catch(error => { + console.error('Error:', error); + alert('Error occurred while updating user role.'); + }); +} + +function disableColorChange(){ + var actionBtn = document.querySelectorAll("#btn-enable-disable"); + actionBtn.forEach(e =>{ + if(e.innerText.toLowerCase().includes('disable')){ + e.style.backgroundColor='#E5E4E6'; + e.style.color='#09090B'; + } + }); +} +disableColorChange(); diff --git a/src/main/resources/static/js/contact/contact.js b/src/main/resources/static/js/contact/contact.js new file mode 100644 index 0000000000000000000000000000000000000000..5294bbc947e215731ca73758fbb6f95016ef2e69 --- /dev/null +++ b/src/main/resources/static/js/contact/contact.js @@ -0,0 +1,174 @@ +// initalise the chart when the page loads, could also initialize map but didn't work like the chart did +// also sort out the animations of the contact form +document.addEventListener('DOMContentLoaded', () => { + + // call the chart initialization method + initUserChart(); + + // input focus and blur animation for the text + document.querySelectorAll('input, textarea').forEach(field => { + field.addEventListener('focus', () => { + anime({ + targets: field, + scale: 1.05, // enlarge teh box slightly on focus + duration: 300, + easing: 'easeInOutQuad' + }); + }); + + field.addEventListener('blur', () => { + anime({ + targets: field, + scale: 1, // return the box to the original size + duration: 300, + easing: 'easeInOutQuad' + }); + }); + }); + + // submit button hover bounce effect + const submitButton = document.getElementById('submitButton'); + submitButton.addEventListener('mouseover', () => { + anime({ + targets: submitButton, + translateY: [-10, 0], + duration: 500, + easing: 'easeOutBounce' + }); + }); + + // FAQ functionality that includes the map initialization + document.querySelectorAll('.faq-question').forEach(question => { + question.addEventListener('click', function () { + // answer of the questions + const answer = this.nextElementSibling; + // the map element + const mapContainer = this.nextElementSibling.nextElementSibling; + + // toggle visibility of the answer (block = visible, none = hidden) + const isHidden = answer.style.display === 'none' || answer.style.display === ''; + answer.style.display = isHidden ? 'block' : 'none'; + + // if the map container is hidden, initialize the map + if (mapContainer.style.display === 'none' || mapContainer.style.display === '') { + // show the map + mapContainer.style.display = 'block'; + // call the map initialization method + initMap(mapContainer); + } else { + // hide the map + mapContainer.style.display = 'none'; + } + + }); + }); + + // form submission that displays a feedback message + document.getElementById('contactForm').addEventListener('submit', function (event) { + event.preventDefault(); + + // get all the inputs from the fields + const name = document.getElementById('name').value.trim(); + const email = document.getElementById('email').value.trim(); + const message = document.getElementById('message').value.trim(); + + // get the feedback element and clear the previous feedback + const feedback = document.getElementById('formFeedback'); + feedback.textContent = ''; + feedback.style.color = ''; + + let isValid = true; + + // check for empty fields + if (!name || !email || !message) { + feedback.textContent = 'Please fill out all fields.'; + feedback.style.color = 'red'; + isValid = false; + } + + // check for valid email + if (email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + feedback.textContent = 'Please enter a valid email address.'; + feedback.style.color = 'red'; + isValid = false; + } + } + + // if everything is valid + if (isValid) { + feedback.textContent = 'Thank you for your message! We will get back to you within 2-3 days.'; + feedback.style.color = 'green'; + + // reset the form fields + this.reset(); + } + }); + +}); + +// funtion for initialising the map +function initMap(container) { + // using Cardiff location coordinates + const location = { lat: 51.4778, lng: -3.1776 }; + + // create the map that centers at Cardiff + const map = new google.maps.Map(container, { + zoom: 12, + center: location + }); + + // add the marker that is placed at Cardiff + const marker = new google.maps.Marker({ + position: location, + map: map, + title: "Our Location", + }); +} + +// funtion for initialising the chart +function initUserChart() { + // get the html chart + const ctx = document.getElementById('userChart').getContext('2d'); + + // mock data for years and the corresponding number of users + // in future versions, this would collect data from the users database + const years = [2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024]; + const numberOfUsers = [500, 700, 900, 1200, 1500, 2000, 2500, 3000, 4000, 5000, 6000]; + + // set up the chart + new Chart("userChart", { + type: "line", // line chart + data: { + labels: years, + datasets: [{ + label: "Number of Users", + backgroundColor: "rgba(0,0,255,0.2)", + borderColor: "rgba(0,0,255,1.0)", + data: numberOfUsers, + fill: true, // fill the area below the line + }] + }, + // styling options for the chart + options: { + scales: { + x: { + title: { + display: true, + text: 'Number of Users' + }, + beginAtZero: true, + }, + y: { + title: { + display: true, + text: 'Year' + } + } + }, + responsive: true, + + } + }); +} diff --git a/src/main/resources/static/js/feed/feed.js b/src/main/resources/static/js/feed/feed.js index b21bcb70cc8b8a088dcb89980c15f894e572b7ff..cf791a2697db1aa659090b16928ccb5ff1cf5610 100644 --- a/src/main/resources/static/js/feed/feed.js +++ b/src/main/resources/static/js/feed/feed.js @@ -1,40 +1,52 @@ 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'); +const postForm = document.getElementById('post-form'); - -addNewPost.addEventListener('click', () => { - modal.style.display = 'flex' -}) - -closeModalBtn.addEventListener('click', () => { - modal.style.display = 'none' + - '' -}) +let isEditing = false; +let editPostId = null; // maintaining state let posts = []; +// had issues with form so func to reset it +function resetForm() { + postForm.reset(); + isEditing = false; + editPostId = null; + + const preview = document.getElementById('imagePreview'); + if (preview) { + preview.innerHTML = ''; // Remove any existing preview content + } +} + +addNewPost.addEventListener('click', () => { + resetForm(); + modal.style.display = 'flex'; +}); + +closeModalBtn.addEventListener('click', () => { + modal.style.display = 'none'; + resetForm(); +}); + // 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 @@ -45,6 +57,10 @@ function renderPosts() { async function renderPost(post) { const postElement = postTemplate.content.cloneNode(true); + const deleteButton = postElement.querySelector('.delete-post'); + const editButton = postElement.querySelector('.edit-post'); + deleteButton.style.display = post.isDeletable ? 'block' : 'none'; + editButton.style.display = post.isEditable ? 'block' : 'none'; postElement.querySelector('.author').textContent = post.authorName; postElement.querySelector('.author-title').textContent = post.authorOrganization; postElement.querySelector('.post-title').textContent = post.postTitle; @@ -68,105 +84,234 @@ async function renderPost(post) { } // set the count of the likes + const likeButton = postElement.querySelector('.like-button'); const likeCount = postElement.querySelector('.like-count'); likeCount.textContent = post.likesCount || 0; + likeButton.addEventListener('click', () => handleLike(post.postId, likeCount, likeButton)); + + // 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)); + // share button func + const shareButton = postElement.querySelector('.share-post'); + shareButton.addEventListener('click', () => sharePost(post.postId)); // Set data attributes for reference const postDiv = postElement.querySelector('.post'); postDiv.dataset.postId = post.postId; + //deleting post + if (post.isDeletable) { + deleteButton.addEventListener('click', async () => { + if (confirm('Are you sure you want to delete this post?')) { + try { + const response = await fetch(`${API_BASE_URL}/${post.postId}`, { + method: 'DELETE', + credentials: 'include' + }); + + if (!response.ok) { + throw new Error('Failed to delete post'); + } + + const postDiv = deleteButton.closest('.post'); + postDiv.remove(); + posts = posts.filter(p => p.postId !== post.postId); + } catch (error) { + console.error('Error deleting post:', error); + alert('Error deleting post. Please try again.'); + } + } + }); + } + postFeed.appendChild(postElement); } // add a like if user had not already liked and remove a like if already liked -async function handleLike(postId, likeCountElement) { +async function handleLike(postId, likeCountElement, likeButton) { 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 response = await fetch(`${API_BASE_URL}/${postId}/hasLiked`, { + method: 'GET', + credentials: 'include' + }); + if (!response.ok) throw new Error('Failed to check like status'); + const hasLiked = await response.json(); const method = hasLiked ? 'DELETE' : 'POST'; - const response = await fetch(`${API_BASE_URL}/${postId}/like?userId=${MOCK_USER_ID}`, { - method: method + const likeResponse = await fetch(`${API_BASE_URL}/${postId}/like`, { + method: method, + credentials: 'include' }); + if (!likeResponse.ok) throw new Error('Failed to update like'); - 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); - + likeButton.classList.toggle('liked', !hasLiked); } catch (error) { console.error('Error updating like:', error); + alert('Error updating like. Please try again.'); } } -//* ************************* Submitting the form *********************** *// -const postForm = document.getElementById('post-form'); +// share funtionality +function sharePost(postId) { + const postUrl = encodeURIComponent(`${window.location.origin}/post/${postId}`); + const facebookUrl = `https://www.facebook.com/sharer/sharer.php?u=${postUrl}`; + const twitterUrl = `https://twitter.com/intent/tweet?url=${postUrl}`; + const linkedinUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${postUrl}`; + + const shareModal = document.createElement('div'); + shareModal.innerHTML = ` + <div class="share-box"> + <div class="share-controls"><button class="close-share"><i class="bi bi-x-lg"></button></i></div> + <div class="share-options"> + <a href="${facebookUrl}" target="_blank" style="color:#4267B2"><i class="bi bi-facebook"></i></a> + <a href="${twitterUrl}" target="_blank" style="color:#1DA1F2"><i class="bi bi-twitter"></i></a> + <a href="${linkedinUrl}" target="_blank" style="color:#0A66C2"><i class="bi bi-linkedin"></i></a> + <button id="copyLink"><i class="bi bi-link-45deg"></i></button> + </div> + </div> + `; + + document.body.appendChild(shareModal); + shareModal.classList.add('share-modal'); + shareModal.style.display = 'flex'; + + shareModal.querySelector('.close-share').addEventListener('click', () => { + shareModal.remove(); + }) + + document.getElementById('copyLink').addEventListener('click', () => { + navigator.clipboard.writeText(decodeURIComponent(postUrl)).then(() => { + alert('Post link copied to clipboard!'); + }).catch(err => { + console.error('Failed to copy: ', err); + }); + }); +} -postForm.addEventListener('submit', (event) => { + +// handling form submission whether update or post +postForm.addEventListener('submit', async (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 + + const formData = new FormData(); + + // getting post data + const postData = { + postTitle: document.getElementById('postTitle').value, + postDescription: document.getElementById('postDescription').value, + tags: document.getElementById('postTags').value + .split(',') + .map(tag => tag.trim()) + .filter(tag => tag.length > 0) }; - console.log('Submitting post:', data); + formData.append('post', new Blob([JSON.stringify(postData)], { + type: 'application/json' + })); - // 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.'); + // handle the case of an image selected + const imageFile = document.getElementById('postImage').files[0]; + if (imageFile) { + formData.append('image', imageFile); + } + + try { + let url = `${API_BASE_URL}/add`; + let method = 'POST'; + + if (isEditing && editPostId) { + url = `${API_BASE_URL}/${editPostId}`; + method = 'PATCH'; + } + + const response = await fetch(url, { + method: method, + credentials: 'include', + body: formData }); + + if (!response.ok) { + throw new Error('Network response was not ok'); + } + + alert(isEditing ? 'Post updated successfully' : 'Post added successfully'); + modal.style.display = 'none'; + resetForm(); + fetchPosts(); + } catch (error) { + console.error('Error:', error); + alert(isEditing ? 'Error updating post' : 'Error adding post'); + } +}); + +//previewing the image selected +document.getElementById('postImage').addEventListener('change', function(e) { + const file = e.target.files[0]; + const preview = document.getElementById('imagePreview'); + + if (file) { + const reader = new FileReader(); + reader.onload = function(e) { + preview.innerHTML = `<img src="${e.target.result}" style="max-width: 200px; max-height: 200px;">`; + } + reader.readAsDataURL(file); + } else { + preview.innerHTML = ''; + } }); + +// onclick handling for edit +document.addEventListener('click', async (event) => { + const editButton = event.target.closest('.edit-post'); + if (!editButton) return; + + const postDiv = editButton.closest('.post'); + const postId = postDiv.dataset.postId; + + try { + const response = await fetch(`${API_BASE_URL}/${postId}`); + if (!response.ok) throw new Error('Failed to fetch post data'); + + const postData = await response.json(); + + // set edit state ti true + isEditing = true; + editPostId = postId; + + + document.getElementById('postTitle').value = postData.postTitle; + document.getElementById('postDescription').value = postData.postDescription; + document.getElementById('postTags').value = postData.tags.join(', '); + + // Show existing image in preview if available + const preview = document.getElementById('imagePreview'); + if (postData.postImageUrl) { + preview.innerHTML = `<img src="${postData.postImageUrl}" style="max-width: 200px; max-height: 200px;">`; + } else { + preview.innerHTML = ''; // Clear preview if no image exists + } + + + modal.style.display = 'flex'; + } catch (error) { + console.error('Error fetching post data:', error); + alert('Error fetching post data. Please try again.'); + } +}); + +// initialize on page load posts on page load 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 +// refresh posts periodically +// setInterval(fetchPosts, 60000); \ No newline at end of file diff --git a/src/main/resources/static/js/layout/accountDropdown.js b/src/main/resources/static/js/layout/accountDropdown.js new file mode 100644 index 0000000000000000000000000000000000000000..f7c5077f80078f568727d3bff16a42c80d3a7722 --- /dev/null +++ b/src/main/resources/static/js/layout/accountDropdown.js @@ -0,0 +1,18 @@ +document.addEventListener('DOMContentLoaded', function() { + const accountOptions = document.getElementById('account-options'); + const dropdownContent = document.getElementsByClassName('dropdown-content'); + + accountOptions.addEventListener('click', function (e) { + [...dropdownContent].forEach(dropdown => { + dropdown.style.display = dropdown.style.display === + 'block' ? 'none' : 'block' + }); + }); + + //hide dropdown when user clicks outside of element + document.addEventListener('click', function(e) { + if (!accountOptions.contains(e.target)) { + [...dropdownContent].style.display = 'none'; + } + }); +}); \ No newline at end of file diff --git a/src/main/resources/static/js/layout/layout.js b/src/main/resources/static/js/layout/layout.js new file mode 100644 index 0000000000000000000000000000000000000000..f4647f6766bd47d40c78a18f1af972f900fcf8f8 --- /dev/null +++ b/src/main/resources/static/js/layout/layout.js @@ -0,0 +1,54 @@ +let translations = []; +let currentLanguage = "en"; + +/** + * Fetch translations for the given language. + * @param {string} language - Language code to fetch translations for. + */ +async function fetchTranslations(language) { + try { + const response = await fetch(`/api/translations/language/${language}`); + if (!response.ok) { + throw new Error(`Failed to fetch translations: ${response.statusText}`); + } + translations = await response.json(); // This is now an array of translation objects + currentLanguage = language; + console.log("Translations updated:", translations); + updateUIWithTranslations(); + document.dispatchEvent(new Event("translations-updated")); + } catch (error) { + console.error("Error fetching translations:", error); + } +} + +function updateUIWithTranslations() { + const elementsToTranslate = document.querySelectorAll("[data-translate-key]"); + + elementsToTranslate.forEach(element => { + const translationKey = element.getAttribute("data-translate-key"); + // Find the translation object with matching key + const translation = translations.find(t => t.key === translationKey); + if (translation) { + element.textContent = translation.value; + } else { + console.warn(`No translation found for key: ${translationKey}`); + } + }); +} + +// fetch translations on page load +document.addEventListener("DOMContentLoaded", () => { + fetchTranslations(currentLanguage); + console.log("I have run"); + + // handle language selection + const languageSelector = document.querySelector(".languageSelector select"); + if (languageSelector) { + languageSelector.addEventListener("change", async (event) => { + const selectedLanguage = event.target.value; + if (selectedLanguage !== currentLanguage) { + await fetchTranslations(selectedLanguage); + } + }); + } +}); \ No newline at end of file diff --git a/src/main/resources/static/js/profile/profile.js b/src/main/resources/static/js/profile/profile.js new file mode 100644 index 0000000000000000000000000000000000000000..7a8ccc5f9d18336d9543fd06bba0168ede8a0d40 --- /dev/null +++ b/src/main/resources/static/js/profile/profile.js @@ -0,0 +1,83 @@ +document.addEventListener('DOMContentLoaded', function () { + const profileCard = document.getElementById('profile-card'); + const editForm = document.getElementById('edit-profile-form'); + const editBtn = document.getElementById('edit-profile-btn'); + const cancelBtn = document.getElementById('cancel-edit'); + const profilePictureInput = document.getElementById('profile-picture-input'); + const changeProfilePictureBtn = document.getElementById('change-profile-picture-btn'); + const editProfilePicture = document.getElementById('edit-profile-picture'); + fetch('/profile-json') + .then(response => response.json()) + .then(profile => { + document.getElementById('profile-picture').src = profile.profilePicture || '/assets/default-profile.jpg'; + /*document.getElementById('profile-heading').innerHTML = `${profile.fullName}'s bio`;*/ + document.getElementById('full-name').innerHTML = profile.fullName; + document.getElementById('email').innerHTML = profile.email; + if (profile.organisation) { + document.getElementById('organisation').innerHTML = profile.organisation; + } + if (profile.bio) { + document.getElementById('bio').innerHTML = profile.bio; + } + if (profile.showDob && profile.dob) { + document.getElementById('date-of-birth').innerHTML = profile.dob; + } + if (profile.showPhoneNumber && profile.phoneNumber) { + document.getElementById('phone-number').innerHTML = `Phone: ${profile.phoneNumber}`; + } + populateEditForm(profile); + updateProfileCard(profile); + + }) + .catch(error => { + console.error('Error:', error); + console.error('Error stack:', error.stack); + document.getElementById('profile-container').innerHTML = 'Error loading profile.'; + }); + editBtn.addEventListener('click', () => { + profileCard.style.display = 'none'; + editForm.style.display = 'block'; + }); + + cancelBtn.addEventListener('click', () => { + editForm.style.display = 'none'; + profileCard.style.display = 'block'; + }); + changeProfilePictureBtn.addEventListener('click', () => { + profilePictureInput.click(); + }); + + profilePictureInput.addEventListener('change', (event) => { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + editProfilePicture.src = e.target.result; + }; + reader.readAsDataURL(file); + } + }); + + function updateProfileCard(profile) { + document.getElementById('profile-picture').src = profile.profilePicture || '/assets/default-profile.jpg'; + document.getElementById('full-name').textContent = profile.fullName; + document.getElementById('email').textContent = profile.email; + document.getElementById('organisation').textContent = profile.organisation || ''; + document.getElementById('bio').textContent = profile.bio || ''; + document.getElementById('date-of-birth').textContent = profile.showDob && profile.dob ? profile.dob : ''; + document.getElementById('phone-number').textContent = profile.showPhoneNumber && profile.phoneNumber ? `Phone: ${profile.phoneNumber}` : ''; + } + + function populateEditForm(profile) { + document.getElementById('edit-profile-picture').src = profile.profilePicture || '/assets/default-profile.jpg'; + document.getElementById('original-profile-picture').value = profile.profilePicture; + document.getElementById('edit-full-name').value = profile.fullName; + document.getElementById('edit-email').value = profile.email; + document.getElementById('edit-organisation').value = profile.organisation || ''; + document.getElementById('edit-bio').value = profile.bio || ''; + document.getElementById('edit-date-of-birth').value = profile.dob || ''; + document.getElementById('edit-phone-number').value = profile.phoneNumber || ''; + document.getElementById('show-dob').checked = profile.showDob; + document.getElementById('show-number').checked = profile.showPhoneNumber; + } +}); \ No newline at end of file diff --git a/src/main/resources/templates/admin/adminBoard.html b/src/main/resources/templates/admin/adminBoard.html new file mode 100644 index 0000000000000000000000000000000000000000..630fee8b0b835c1e1e3a302cfb631922ab9705b3 --- /dev/null +++ b/src/main/resources/templates/admin/adminBoard.html @@ -0,0 +1,83 @@ +<!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"> + <title>Title</title> + <link rel="stylesheet" href="/css/admin/adminBoardStyles.css"> +</head> +<section layout:fragment="content"> + <div class="header"> + <h1>Admin Dashboard</h1> + </div> + + <div class="statistics"> + <div class="stat"><i class="bi bi-people"></i> + <aside><p> Total Users</p><strong><span th:text="*{adminBoard.getTotalNoOfUsers()}"></span></strong></p></aside> + </div> + <div class="stat"><p><i class="bi bi-file-earmark-text"></i> + <aside><p> Total Posts</p><strong><span th:text="*{adminBoard.getTotalPosts()}"></span></strong></p></aside> + </div> + <div class="stat"><p><i class="bi bi-calendar-week"></i> + <aside><p> Upcoming Events</p><strong><span th:text="*{adminBoard.getUpcomingEvents()}"></span></strong></p> + </aside> + </div> + <div class="stat"><p><i class="bi bi-chat-left-dots"></i> + <aside><p> New Comments</p><strong><span th:text="*{adminBoard.getNewComments()}"></span></strong></p> + </aside> + </div> + </div> + + <div class="dashboard"> + <div class="card"> + <h3>Manage Users</h3> + <p>View and manage user accounts</p> + <a th:href="@{/admin/userInfo}"> + <button>Manage</button> + </a> + </div> + + <div class="card"> + <h3>Feeds Management</h3> + <p>Review and manage community feeds</p> + <a th:href="@{/feed}"> + <button>Manage</button> + </a> + </div> + + <div class="card"> + <h3>Event Management</h3> + <p>Create and manage community events</p> + <a th:href="@{/event}"> + <button>Manage</button> + </a> + </div> + + <div class="card"> + <h3>Database Management</h3> + <p>Manage community information database</p> + <a th:href="@{/categories}"> + <button>Manage</button> + </a> + </div> + + <div class="card"> + <h3>Comments Moderation</h3> + <p>Review and moderate user comments</p> + <a th:href="@{/feed}"> + <button>Manage</button> + </a> + </div> + + <div class="card"> + <h3>News Management</h3> + <p>Create and manage community news</p> + <a th:href="@{/news}"> + <button>Manage</button> + </a> + </div> + </div> + +</section> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/admin/userManage.html b/src/main/resources/templates/admin/userManage.html new file mode 100644 index 0000000000000000000000000000000000000000..23483a1ccbfb0d15e2b34ffed3e9590eb2cfc1a1 --- /dev/null +++ b/src/main/resources/templates/admin/userManage.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<html xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{layout/layout}"> + +<head> + <title>Manage Users</title> + <!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">--> + <link rel="stylesheet" href="/css/admin/userManageStyles.css"> + <script src="/js/admin/admin.js" defer></script> +</head> +<section layout:fragment="content"> + <h2>User Management</h2> + <div class="table-wrap"> + <table> + <thead> + <tr> + <th>ID</th> + <th>Full Name</th> + <th>Email</th> + <th>Role</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + <tr th:each="user : ${usersAdminInfo}"> + <td th:text="${user.id}"></td> + <td th:text="${user.fullName}"></td> + <td th:text="${user.email}"></td> + <td th:field="${user.role}"> + <select id="roleDdl" th:onchange="|updateUserRole(${user.id})|"> + <option th:selected="${role.name==user.role}" th:each="role : ${roles}" + th:text="${role.name}"></option> + </select> + </td> + <td> + <button id="btn-enable-disable" + class="btn" + th:onclick="|toggleUserEnabled(${user.id}, ${user.enabled})|"> + <span th:text="${user.enabled ? 'Disable' : 'Enable'}"></span> + </button> + </td> + </tr> + </tbody> + </table> + </div> +</section> +</html> diff --git a/src/main/resources/templates/contact/contact.html b/src/main/resources/templates/contact/contact.html new file mode 100644 index 0000000000000000000000000000000000000000..f8d51304093cd2b96a1305686dc2b91d346497a0 --- /dev/null +++ b/src/main/resources/templates/contact/contact.html @@ -0,0 +1,96 @@ +<!DOCTYPE html> +<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>Contact Us</title> + <link rel="stylesheet" href="/css/contact/contact.css"> + + <script src="https://cdn.jsdelivr.net/npm/animejs@3.2.1/lib/anime.min.js"></script> + <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyAcnCewJfgcEPXtBPkokQwJXcOwUy-9iwI&callback=initMap" async defer></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.js"></script> + +</head> +<body layout:fragment="content"> + + <h1 id="title">Contact Us</h1> + <!-- Contact Form Section --> + <section id="contact-form"> + + <!-- Contact Form Container --> + <div id="contact-form-container"> + <h2>Get in Touch</h2> + <form id="contactForm" action="/submit" method="POST" novalidate> + <label for="name">Name:</label> + <input type="text" id="name" name="name"> + + <label for="email">Email:</label> + <input type="email" id="email" name="email"> + + <label for="message">Message:</label> + <textarea id="message" name="message"></textarea> + + <div id="formFeedback"></div> + + <button type="submit" id="submitButton">Submit</button> + </form> + </div> + + <!-- Contact Information Container --> + <div id="contact-info-container"> + <h3>Contact Information</h3> + <p><strong>Phone:</strong> +123456789</p> + <p><strong>Email:</strong> contact@domain.com</p> + <p><strong>Address:</strong> 123 Street, City, Country</p> + + <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> + + </section> + + + + <!-- FAQ Section --> + <section id="faq"> + + <h2>Frequently Asked Questions</h2> + + <div class="faq-item"> + <h3 class="faq-question">What is the community?</h3> + <p class="faq-answer">Our community is a group of people who support and connect with each other.</p> + </div> + + <div class="faq-item"> + <h3 class="faq-question">How can I join the community?</h3> + <p class="faq-answer">You can join by signing up through the main page or by contacting us directly.</p> + </div> + + <div class="faq-item"> + <h3 class="faq-question">How do I contact support?</h3> + <p class="faq-answer">You can reach out to us through the contact form above.</p> + </div> + + <div class="faq-item"> + <h3 class="faq-question">How many users are currently on the platform?</h3> + <div class="faq-answer"> + <canvas id="userChart" width="400" height="200"></canvas> <!-- The canvas for the chart --> + </div> + </div> + + <div class="faq-item"> + <h3 class="faq-question">Where are you located?</h3> + <p class="faq-answer">We are located in the heart of the city. You can find us on the map below!</p> + + <!-- Interactive Map --> + <div id="map" style="height: 400px;"></div> <!-- Map container --> + </div> + </section> + + <script src="/js/contact/contact.js"></script> +</body> +</html> + diff --git a/src/main/resources/templates/event/add-event.html b/src/main/resources/templates/event/add-event.html new file mode 100644 index 0000000000000000000000000000000000000000..cfed5e8d9a6f6043520df66dc7709e302a791b22 --- /dev/null +++ b/src/main/resources/templates/event/add-event.html @@ -0,0 +1,77 @@ +<!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 Event</title> + <link rel="stylesheet" href="/css/events/add-event.css"> +</head> +<section layout:fragment="content"> + <a th:href="@{/event}" id="backToHeadings" class="backButton"> + <p><i class="bi bi-chevron-left"></i> Back to Events</p> + </a> + + <h1 th:text="${formAction.contains('edit')} ? 'Edit Event' : 'Add New Event'">Add New Event</h1> + + <form th:action="@{${formAction}}" th:method="post" th:object="${event}" id="addEventForm"> + <fieldset> + <!-- Hidden Event ID --> + <input th:field="*{event_id}" type="hidden" /> + <input th:field="*{user_id}" type="hidden" /> + + <!-- Event Title --> + <label th:for="*{event_title}">Event Title</label> + <input type="text" th:field="*{event_title}" placeholder="Enter the title for the event..."> + <div class="alert" th:errors="*{event_title}" th:if="${#fields.hasErrors('event_title')}"></div> + + <!-- Event Description --> + <label th:for="*{description}">Event Description</label> + <textarea th:field="*{description}" placeholder="Enter the event description..."></textarea> + <div class="alert" th:errors="*{description}" th:if="${#fields.hasErrors('description')}">Description Error</div> + + <div class="event-date-and-location-card"> + <!-- Event Date --> + <div class="event-date-card"> + <label th:for="*{event_date}">Event Date</label> + <input class="event-date" type="date" th:field="*{event_date}"> + <div class="alert" th:errors="*{event_date}" th:if="${#fields.hasErrors('event_date')}">Date Error</div> + </div> + <!-- Event Time --> + <div class="event-time-card"> + <label th:for="*{event_time}">Event Time</label> + <input type="time" th:field="*{event_time}"> + <div class="alert" th:errors="*{event_time}" th:if="${#fields.hasErrors('event_time')}">Time Error</div> + </div> + <!-- Location --> + <div class="event-location-card"> + <label th:for="*{location}">Location</label> + <input class="event-location-input" type="text" th:field="*{location}" autocomplete="off" placeholder="Enter the event location..."> + <div class="alert" th:errors="*{location}" th:if="${#fields.hasErrors('location')}">Location Error</div> + </div> + </div> + + <!-- Image URL --> + <label th:for="*{imageUrl}">Image URL</label> + <input type="text" th:field="*{imageUrl}" placeholder="Enter the image URL..."> + <div class="alert" th:errors="*{imageUrl}" th:if="${#fields.hasErrors('imageUrl')}">Image URL Error</div> + + <!-- Why Join --> + <label th:for="*{whyJoin}">Why Join</label> + <textarea th:field="*{whyJoin}" placeholder="Explain why someone should join..."></textarea> + <div class="alert" th:errors="*{whyJoin}" th:if="${#fields.hasErrors('whyJoin')}">Why Join Error</div> + + <!-- Benefits --> + <label th:for="*{benefits}">Benefits</label> + <textarea th:field="*{benefits}" placeholder="List the benefits..."></textarea> + <div class="alert" th:errors="*{benefits}" th:if="${#fields.hasErrors('benefits')}">Benefits Error</div> + </fieldset> + + <!-- Buttons --> + <div class="buttons"> + <a th:href="@{/event}" id="cancelButton" class="button cancel">Cancel</a> + <button th:text="${formAction.contains('edit')}?'Update Event':'Add Event'" type="submit" class="submit">Add Event</button> + </div> + </form> +</section> +</html> diff --git a/src/main/resources/templates/event/editEvent.html b/src/main/resources/templates/event/editEvent.html new file mode 100644 index 0000000000000000000000000000000000000000..48b921ca87ec76198db8873e577342ae25465554 --- /dev/null +++ b/src/main/resources/templates/event/editEvent.html @@ -0,0 +1,63 @@ +<!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"> + <title>Edit Event</title> + <link rel="stylesheet" href="/css/events/editEvent.css"></link> +</head> +<section layout:fragment="content"> + <h1>Add New Information</h1> + <form th:action="@{/event/editEvent}" th:method="post" th:object="${editEvent}" id="addEventForm"> + <!-- Hidden Event ID --> + <input th:field="*{event_id}" type="hidden" /> + + <!-- Event Title --> + <label th:for="*{event_title}">Event Title</label> + <input type="text" th:field="*{event_title}" placeholder="Enter the title for the event..."> + <div class="alert alert-warning" th:errors="*{event_title}" th:if="${#fields.hasErrors('event_title')}">Title Error</div> + + <!-- Event Description --> + <label th:for="*{description}">Event Description</label> + <textarea th:field="*{description}" placeholder="Enter the event description..."></textarea> + <div class="alert alert-warning" th:errors="*{description}" th:if="${#fields.hasErrors('description')}">Description Error</div> + + <!-- Event Date --> + <label th:for="*{event_date}">Event Date</label> + <input type="date" th:field="*{event_date}"> + <div class="alert alert-warning" th:errors="*{event_date}" th:if="${#fields.hasErrors('event_date')}">Date Error</div> + + <!-- Event Time --> + <label th:for="*{event_time}">Event Time</label> + <input type="time" th:field="*{event_time}"> + <div class="alert alert-warning" th:errors="*{event_time}" th:if="${#fields.hasErrors('event_time')}">Time Error</div> + + <!-- Location --> + <label th:for="*{location}">Location</label> + <input type="text" th:field="*{location}" placeholder="Enter the event location..."> + <div class="alert alert-warning" th:errors="*{location}" th:if="${#fields.hasErrors('location')}">Location Error</div> + + <!-- Image URL --> + <label th:for="*{imageUrl}">Image URL</label> + <input type="text" th:field="*{imageUrl}" placeholder="Enter the image URL..."> + <div class="alert alert-warning" th:errors="*{imageUrl}" th:if="${#fields.hasErrors('imageUrl')}">Image URL Error</div> + + <!-- Why Join --> + <label th:for="*{whyJoin}">Why Join</label> + <textarea th:field="*{whyJoin}" placeholder="Explain why someone should join..."></textarea> + <div class="alert alert-warning" th:errors="*{whyJoin}" th:if="${#fields.hasErrors('whyJoin')}">Why Join Error</div> + + <!-- Benefits --> + <label th:for="*{benefits}">Benefits</label> + <textarea th:field="*{benefits}" placeholder="List the benefits..."></textarea> + <div class="alert alert-warning" th:errors="*{benefits}" th:if="${#fields.hasErrors('benefits')}">Benefits Error</div> + + <!-- Buttons --> + <div class="buttons"> + <a href="#" id="cancelButton" class="button cancel">Cancel</a> + <button type="submit" class="submit">Add Event</button> + </div> + </form> +</section> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/event/event-detail.html b/src/main/resources/templates/event/event-detail.html index fc6eb0d7fd8ecdfa2757dbe55b4d289deea90390..a2dbb8f04bb7dc590782b2af91606539ce116b82 100644 --- a/src/main/resources/templates/event/event-detail.html +++ b/src/main/resources/templates/event/event-detail.html @@ -9,45 +9,47 @@ <title>Event Details</title> <link rel="stylesheet" href="/css/events/event-detail.css"> </head> - -<body> -<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"> - </article> - - <header> - <h1 th:text="${event.getEvent_title()}">Event Name</h1> - </header> - - <section class="text-details"> - <p><strong>Description:</strong> <span th:text="${event.getDescription()}">Event description</span></p> - <p><strong>Date and Time:</strong> <span th:text="${event.getEvent_date()}">Event date</span>, <span - th:text="${event.getEvent_time()}">Event time</span></p> - <p><strong>Location:</strong> <span th:text="${event.getLocation()}">Event location</span></p> - </section> - - <!-- Additional Event Information --> - <section class="event-benefits"> - <h2>Why Should You Join?</h2> - <p th:text="${event.getWhyJoin()}">Reasons to join the event</p> - </section> - - <section class="event-benefits"> - <h2>Benefits of Joining</h2> - <ul> - <li th:each="benefit : ${event.getBenefits()}"> - <span th:text="${benefit}">Benefit 1</span> - </li> - </ul> +<section layout:fragment="content"> + <main id="event-detail-container"> + <a th:href="@{/event}" id="backToHeadings" class="backButton"> + <p><i class="bi bi-chevron-left"></i> <strong>Back to Events</strong></p> + </a> + <section class="event-detail-card"> + <article class="image-container"> + <img th:src="${event.getImageUrl()}" alt="Event Image" class="event-image"> + </article> + + <header> + <h1 th:text="${event.getEvent_title()}">Event Name</h1> + </header> + + <section class="text-details"> + <p><strong>Description:</strong> <span th:text="${event.getDescription()}">Event description</span></p> + <p><strong>Date and Time:</strong> <span th:text="${event.getEvent_date()}">Event date</span>, <span + th:text="${event.getEvent_time()}">Event time</span></p> + <p><strong>Location:</strong> <span th:text="${event.getLocation()}">Event location</span></p> + </section> + + <!-- Additional Event Information --> + <section class="event-benefits"> + <h2>Why Should You Join?</h2> + <p th:text="${event.getWhyJoin()}">Reasons to join the event</p> + </section> + + <section class="event-benefits"> + <h2>Benefits of Joining</h2> + <ul> + <li th:each="benefit : ${event.getBenefits()}"> + <span th:text="${benefit}">Benefit 1</span> + </li> + </ul> + </section> + + <footer> + <button class="register-button">Register Now</button> + </footer> </section> - - <footer> - <button class="register-button">Register Now</button> - </footer> - </section> -</main> -</body> + </main> +</section> </html> diff --git a/src/main/resources/templates/event/event.html b/src/main/resources/templates/event/event.html index 6d75e5ae5be7669e90c89de2419a49d163fdb2ae..c4e3731ac6202bb777f5f59c9b499864661a5657 100644 --- a/src/main/resources/templates/event/event.html +++ b/src/main/resources/templates/event/event.html @@ -8,16 +8,19 @@ <title>Event Details</title> <link rel="stylesheet" href="/css/events/event.css"> </head> - - <body layout:fragment="content"> + <section layout:fragment="content"> <div id="event-details" class="event-details"> - <div class="event-grid"> + <div class="general-headings-layout"> + <h1>Community Events</h1> + <a th:href="@{event/add}"><button class="add-event-button">Add Event</button></a> + </div> + <div class="event-grid"> <!-- Loop through each event and create a clickable link --> <div class="event-card" th:each="event : ${events}"> <div class="event-card-content"> <a th:href="@{/event/{id}(id=${event.getEvent_id()})}" class="event-link"> <div class="event-poster"> - <img th:src="${event.getImageUrl()} " alt="Image description" id="myImage"> + <img th:src="${event.getImageUrl()} " class=".event-poster-img" alt="Image description" id="myImage"> </div> <h2 th:text="${event.getEvent_title()}">Sample Event</h2> <p><strong>Description:</strong> <span th:text="${event.getDescription()}">Event description</span></p> @@ -25,10 +28,16 @@ <p><strong>Location:</strong> <span th:text="${event.getLocation()}">Event location</span></p> </a> </div> - <button class="register-button">Register Now</button> + <div class ="event-card-button"> + <button class="register-button">Register Now</button> + <a th:href="@{/event/edit/{id}(id=${event.getEvent_id()})}" class="event-link"><button>Edit Event</button></a> + </div> </div> </div> </div> <script src="/js/event/event.js"></script> - </body> + <script th:if="${successMessage}"> + alert("Event created successfully"); + </script> + </section> </html> diff --git a/src/main/resources/templates/feed/feed.html b/src/main/resources/templates/feed/feed.html index 73c225c99675f992fa552684add08e0f4267b346..058d86c2839162c3036e38cca1d1b74d172ae8cb 100644 --- a/src/main/resources/templates/feed/feed.html +++ b/src/main/resources/templates/feed/feed.html @@ -21,14 +21,25 @@ <!-- 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 class="post-header"> + <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="text-details"> - <h5 class="author"></h5> - <p class="author-title"></p> + <div class="post-manipulation"> + <button title="Edit post" class="edit-post" id="edit-post"> + <i class="bi bi-pencil"></i> + </button> + <button title="Delete post" class="delete-post" id="delete-post"> + <i class="bi bi-trash3"></i> + </button> </div> + </div> <div class="post-details"> <h3 class="post-title"></h3> @@ -42,15 +53,15 @@ </div> <div class="post-meta"> <div class="post-actions"> - <button class="like-button"> + <button title="Like post" class="like-button"> <i class="bi bi-hand-thumbs-up"></i> <span class="like-count">0</span> </button> - <button> + <button title="Commend on post"> <i class="bi bi-chat-left"></i> <span>35</span> </button> - <button> + <button title="Share post" id="share-post" class="share-post"> <i class="bi bi-share"></i> </button> </div> @@ -89,6 +100,15 @@ required></textarea> </label> + <label for="postImage">Image + <input type="file" + id="postImage" + name="postImage" + accept="image/*" + class="form-control"> + <div id="imagePreview" class="mt-2"></div> + </label> + <label for="postTags">Tags <input type="text" id="postTags" name="postTags" placeholder="Enter tags separated by commas (e.g., news, community, event)" diff --git a/src/main/resources/templates/headings/headings.html b/src/main/resources/templates/headings/headings.html index 590b35c4d8271c8ad49bc2983a9cda80a194dc4e..80cf562f5e032c351038c644a6f272581870b88c 100644 --- a/src/main/resources/templates/headings/headings.html +++ b/src/main/resources/templates/headings/headings.html @@ -12,7 +12,9 @@ <div th:replace="~{infoHeadingFragment/infoHeadingFragment :: breadcrumb}"></div> <div class="title-section"> <h1 id="category-name"></h1> + <div class="buttons"> + <button id="delete-button" disabled>Delete</button> <button id="edit-mode-button">Edit</button> <a id="addInfo"> <button id="add-new-information"> @@ -22,9 +24,9 @@ </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 4eb8664b934ec3f5e0ec921ea97cf731182383de..7d96e8cf728d88db502d5f68870ece0d5bf41e51 100644 --- a/src/main/resources/templates/home/home.html +++ b/src/main/resources/templates/home/home.html @@ -8,64 +8,64 @@ <title>HOME</title> <link rel="stylesheet" href="/css/home/home.css"> </head> -<body> + <section layout:fragment="content"> - <h1>Welcome to our Community</h1> - <p>Connect, Share, and Grow Together</p> + <h1 data-translate-key="home.header">Welcome to our Community</h1> + <p data-translate-key="home.smallheader">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> + <img src="/assets/navbarImages/feed.png" class="tile-logo"><span class="tile-title" data-translate-key="navbar.feed">Feed</span> </div> - <p class="tile-description">Stay updated with the latest posts</p> + <p class="tile-description" data-translate-key="home.latest_posts">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> + <img src="/assets/navbarImages/news.png" class="tile-logo"><span class="tile-title" data-translate-key="navbar.news">News</span> </div> - <p class="tile-description">Discover the latest community news</p> + <p class="tile-description" data-translate-key="home.news">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> + <img src="/assets/navbarImages/events.png" class="tile-logo"><span class="tile-title" data-translate-key="navbar.events">Events</span> </div> - <p class="tile-description">Join our upcoming community events</p> + <p class="tile-description" data-translate-key="home.events">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> + <img src="/assets/navbarImages/info.png" class="tile-logo"><span class="tile-title" data-translate-key="navbar.db_info">Info Database</span> </div> - <p class="tile-description">Access our community information database</p> + <p class="tile-description" data-translate-key="home.db_info">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> + <img src="/assets/navbarImages/about.png" class="tile-logo"><span class="tile-title" data-translate-key="navbar.about_us">About Us</span> </div> - <p class="tile-description">Learn about our mission and values</p> + <p class="tile-description" data-translate-key="home.about_us">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> + <h1 data-translate-key="home.join_community">Join Our Thriving Community Today!</h1> + <p data-translate-key="home.tagline">Connect with like-minded individuals, share your ideas, and be part of something amazing</p> + <a th:href="@{/register}"><button data-translate-key="home.get_started">Get Started</button></a> </section> -</body> + </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 03d4db4d0ccfbe479a9ec72f65eb40cd510b0942..829aeca235d83698170723c179cdbc3c662c0401 100644 --- a/src/main/resources/templates/layout/layout.html +++ b/src/main/resources/templates/layout/layout.html @@ -1,111 +1,136 @@ <!DOCTYPE html> <html lang="en"> <html xmlns:th="http://www.thymeleaf.org" - xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6"> + <head> <meta name="viewport" CHARSET="UTF-8" content="width=device-width, initial-scale=1"> <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 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" href="/css/comments/comments.css"> <link rel="stylesheet" href="/css/layout/layout.css"> </head> <body> - <header class="header"> - <section class="sidebar"> - <nav class="sidebar"> - <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> +<header class="header"> + <section class="sidebar"> + <nav class="sidebar"> + <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" + data-translate-key="navbar.home">Home</span> + </a> + <a th:href="@{/feed}" class="navLink"> + <img src="/assets/navbarImages/feed.png" class="navIcons"><span class="navText" + data-translate-key="navbar.feed">Feed</span> + </a> + <a th:href="@{/news}" class="navLink"> + <img src="/assets/navbarImages/news.png" class="navIcons"><span class="navText" + data-translate-key="navbar.news">News</span> + </a> + <a th:href="@{/event}" class="navLink"> + <img src="/assets/navbarImages/events.png" class="navIcons"><span class="navText" + data-translate-key="navbar.events">Events</span> + </a> + <a th:href="@{/categories}" class="navLink"> + <img src="/assets/navbarImages/info.png" class="navIcons"><span class="navText" + data-translate-key="navbar.db_info">Info Database</span> + </a> + <a th:href="@{/aboutUs}" class="navLink"> + <img src="/assets/navbarImages/about.png" class="navIcons"><span class="navText" + data-translate-key="navbar.about_us">About Us</span> + </a> + <a th:href="@{/contactus}" class="navLink"> + <img src="/assets/navbarImages/contact.png" class="navIcons"><span class="navText" + data-translate-key="navbar.contact_us">Contact Us</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> + <!-- 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="en">English</option> + <option value="pl">Polish</option> + </select> </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> + <!-- Profile --> + <div id="account-options"> + <div sec:authorize="isAnonymous()"> + <img src="/assets/navbarImages/profile.png" class="navIcons anonymous"> + <span class="navText">Sign in</span> + <div class="dropdown-content"> + <a class="dropdown-nav-link" th:href="@{/login}">Login</a> + <a class="dropdown-nav-link" th:href="@{/register}">Sign up</a> + </div> + </div> + <div sec:authorize="isAuthenticated()"> + <img src="/assets/navbarImages/profile.png" class="navIcons authenticated"> + <span class="navText"data-translate-key="navbar.profile">Profile</span> + <div class="dropdown-content"> + <a class="navLink" th:href="@{/profile}">View profile</a> + <a class="navLink" th:href="@{/logout}">Logout</a> + </div> </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"> +<div class="mainBody"> + <main layout:fragment="content" class="content"></main> +</div> - - <main layout:fragment="content" class="content"></main> +<footer class="footer"> + <div class="footer-section about"> + <h3 class="footerTitle">Ludek Polonia Wajiska</h3> + <p class="footerText" data-translate-key="footer.tagline">Connecting people, sharing ideas, and building a + better future together.</p> </div> + <div class="footer-section links"> + <h3 class="footerTitle" data-translate-key="footer.quick_links">Quick Links</h3> + <ul class="footerLinks"> + <li><a th:href="@{/aboutus}" class="footerLink" data-translate-key="navbar.about_us">About Us</a></li> + <li><a th:href="@{/contact}" class="footerLink" data-translate-key="navbar.contact">Contact</a></li> + <li><a th:href="@{/}" class="footerLink" data-translate-key="footer.privacy_policy">Privacy Policy</a></li> + <li><a th:href="@{/}" class="footerLink" data-translate-key="footer.terms_of_service">Terms of Service</a> + </li> + </ul> + </div> - - <footer class="footer"> - <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 class="footer-section connect"> + <h3 class="footerTitle" data-translate-key="footer.connect_with_us">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> - </footer> - <script th:replace="~{comments/commentFragment::commentScript}"></script> + </div> + <div class="footer-section copyright"> + <p class="footerCompanyName">© LUDEK PCG ltd. <span data-translate-key="footer.all_rights_reserved" >All rights reserved.</span></p> + </div> +</footer> +<script src="/js/layout/layout.js" defer></script> +<script th:replace="~{comments/commentFragment::commentScript}"></script> + <script src="/js/layout/accountDropdown.js"></script> </body> </html> \ No newline at end of file diff --git a/src/main/resources/templates/news/newsList.html b/src/main/resources/templates/news/newsList.html index 835352cd6f0341e4a4d2a5016e760558707b870a..2e2a97ce2b01f99b9bfcedce8662a2fcab204ff6 100644 --- a/src/main/resources/templates/news/newsList.html +++ b/src/main/resources/templates/news/newsList.html @@ -10,14 +10,10 @@ <script src="/js/news/addNews.js" defer></script> <script src="/js/news/newsScripts.js" defer></script> </head> - <section layout:fragment="content" > + <section layout:fragment="content" class="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 --> diff --git a/src/main/resources/templates/profile/profilePage.html b/src/main/resources/templates/profile/profilePage.html new file mode 100644 index 0000000000000000000000000000000000000000..bba95b8f6c911dd2e9f482ed246f1ed13c3d9fe8 --- /dev/null +++ b/src/main/resources/templates/profile/profilePage.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<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>Profile</title> + <link rel="stylesheet" href="/css/profile/profile.css"> + <script src="/js/profile/profile.js" defer></script> + +</head> +<section class="profile-container" id="profile-container" layout:fragment="content"> + <div class="profile-card" id="profile-card"> + <!--<h3 id="profile-heading">Profile Page</h3>--> + <img id="profile-picture" src="/assets/default-profile.jpg" alt="Profile Picture" class="profile-picture"> + <div class="profile-info"> + <h2 class="full-name" id="full-name">Name</h2> + <p id="organisation" class="organisation"></p> + <p class="email" id="email"></p> + <p id="bio" class="optional-field"></p> + <p id="date-of-birth" class="optional-field"></p> + <p id="phone-number" class="optional-field"></p> + </div> + <button class="edit-profile-btn" id="edit-profile-btn">Edit Profile</button> + </div> + <form id="edit-profile-form" class="profile-card" style="display: none" th:action="@{/update}" method="post" enctype="multipart/form-data"> + <img id="edit-profile-picture" src="/assets/default-profile.jpg" alt="Profile Picture" class="profile-picture"> + <input type="file" id="profile-picture-input" name="newPicture" accept="image/*" style="display: none;"> + <br> + <button type="button" id="change-profile-picture-btn">Change Profile Picture</button> + <input type="hidden" id="original-profile-picture" name="profilePicture" readonly> + <input type="text" class="not-editable" id="edit-full-name" readonly> + <input type="text" class="not-editable" id="edit-email" readonly> + <input type="text" class="not-editable" id="edit-organisation" readonly placeholder="Organisation"> + <textarea id="edit-bio" name="bio" placeholder="Bio" class="optional-field"></textarea> + <input type="text" class="not-editable" id="edit-date-of-birth" readonly> + <input type="tel" id="edit-phone-number" name="phoneNumber" placeholder="Phone Number" class="optional-field"> + <div class="checkbox-container"> + <label><input type="checkbox" id="show-dob" name="showDob"> Show Date of Birth</label> + <label><input type="checkbox" id="show-number" name="showPhoneNumber"> Show Phone Number</label> + </div> + <button type="submit" class="edit-profile-btn">Save Changes</button> + <button type="button" id="cancel-edit" class="edit-profile-btn">Cancel</button> + </form> +</section> +</html> \ No newline at end of file diff --git a/src/main/resources/translations.sql b/src/main/resources/translations.sql new file mode 100644 index 0000000000000000000000000000000000000000..1ef42bbaf33963cb21d6fb99877771d42a6a5346 --- /dev/null +++ b/src/main/resources/translations.sql @@ -0,0 +1,54 @@ +INSERT INTO translations (translation_key, language, value) VALUES + -- English translations + ('navbar.home', 'en', 'Home'), + ('navbar.news', 'en', 'News'), + ('navbar.events', 'en', 'Events'), + ('navbar.db_info', 'en', 'Info Database'), + ('navbar.contact_us', 'en', 'Contact Us'), + ('navbar.feed', 'en', 'Feed'), + ('navbar.about_us', 'en', 'About Us'), + ('navbar.profile', 'en', 'Profile'), + ('home.header', 'en', 'Welcome to our Community'), + ('home.smallheader', 'en', 'Connect, Share, and Grow Together'), + ('home.latest_posts', 'en', 'Stay updated with the latest posts'), + ('home.news', 'en', 'Discover the latest community news'), + ('home.events', 'en', 'Join our upcoming community events'), + ('home.db_info', 'en', 'Access our community information database'), + ('home.about_us', 'en', 'Learn about our mission and values'), + ('home.join_community', 'en', 'Join Our Thriving Community Today!'), + ('home.tagline', 'en', 'Connect with like-minded individuals, share your ideas, and be part of something amazing'), + ('home.get_started', 'en', 'Get Started'), + ('footer.tagline', 'en', 'Connecting people, sharing ideas, and building a better future together.'), + ('footer.quick_links', 'en', 'Quick Links'), + ('footer.connect_with_us', 'en', 'Connect with us'), + ('footer.privacy_policy', 'en', 'Privacy Policy'), + ('footer.terms_of_service', 'en', 'Terms of Service'), + ('footer.all_rights_reserved', 'en', 'All rights reserved.'), + + -- Polish translations + ('navbar.home', 'pl', 'Strona główna'), + ('navbar.news', 'pl', 'AktualnoÅ›ci'), + ('navbar.events', 'pl', 'Wydarzenia'), + ('navbar.db_info', 'pl', 'Informacje o bazie danych'), + ('navbar.contact_us', 'pl', 'Skontaktuj siÄ™ z nami'), + ('navbar.feed', 'pl', 'KanaÅ‚'), + ('navbar.about_us', 'pl', 'O nas'), + ('navbar.profile', 'pl', 'Profil'), + ('home.header', 'pl', 'Witamy w naszej spoÅ‚ecznoÅ›ci'), + ('home.smallheader', 'pl', 'ÅÄ…cz siÄ™, dziel i rozwijaj razem'), + ('home.latest_posts', 'pl', 'BÄ…dź na bieżąco z najnowszymi postami'), + ('home.news', 'pl', 'Odkryj najnowsze wiadomoÅ›ci spoÅ‚ecznoÅ›ciowe'), + ('home.events', 'pl', 'Dołącz do nadchodzÄ…cych wydarzeÅ„ spoÅ‚ecznoÅ›ciowych'), + ('home.db_info', 'pl', 'Uzyskaj dostÄ™p do naszej bazy informacji spoÅ‚ecznoÅ›ciowych'), + ('home.about_us', 'pl', 'Dowiedz siÄ™ o naszej misji i wartoÅ›ciach'), + ('home.join_community', 'pl', 'Dołącz do naszej rozwijajÄ…cej siÄ™ spoÅ‚ecznoÅ›ci już dziÅ›!'), + ('home.tagline', 'pl', 'ÅÄ…cz siÄ™ z podobnie myÅ›lÄ…cymi ludźmi, dziel siÄ™ pomysÅ‚ami i bÄ…dź częściÄ… czegoÅ› niesamowitego'), + ('home.get_started', 'pl', 'Zacznij teraz'), + ('footer.tagline', 'pl', 'ÅÄ…czÄ…c ludzi, dzielÄ…c siÄ™ pomysÅ‚ami i budujÄ…c lepszÄ… przyszÅ‚ość razem.'), + ('footer.quick_links', 'pl', 'Szybkie linki'), + ('footer.connect_with_us', 'pl', 'Połącz siÄ™ z nami'), + ('footer.privacy_policy', 'pl', 'Polityka prywatnoÅ›ci'), + ('footer.terms_of_service', 'pl', 'Warunki korzystania z usÅ‚ugi'), + ('footer.all_rights_reserved', 'pl', 'Wszelkie prawa zastrzeżone.'); + + diff --git a/src/test/java/polish_community_group_11/polish_community/event/TestEventPage.java b/src/test/java/polish_community_group_11/polish_community/event/TestEventPage.java index 91b12c44f0d9e64f0b256bd70ee9bad20fcefc3c..3587f3c9311467df7cf2d9a683fcf5565443e129 100644 --- a/src/test/java/polish_community_group_11/polish_community/event/TestEventPage.java +++ b/src/test/java/polish_community_group_11/polish_community/event/TestEventPage.java @@ -7,7 +7,7 @@ //import org.springframework.boot.test.context.SpringBootTest; //import org.springframework.boot.test.mock.mockito.MockBean; //import org.springframework.test.web.servlet.MockMvc; -//import polish_community_group_11.polish_community.event.services.EventRepository; +//import polish_community_group_11.polish_community.event.dao.EventRepository; // //import static org.junit.jupiter.api.Assertions.assertEquals; // diff --git a/src/test/java/polish_community_group_11/polish_community/profile/controllers/ProfileControllerTest.java b/src/test/java/polish_community_group_11/polish_community/profile/controllers/ProfileControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..571bc90b2183b9c621cfbd440420d3ea9340b969 --- /dev/null +++ b/src/test/java/polish_community_group_11/polish_community/profile/controllers/ProfileControllerTest.java @@ -0,0 +1,36 @@ +package polish_community_group_11.polish_community.profile.controllers; + +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.register.models.User; +import polish_community_group_11.polish_community.register.services.UserService; + +import static org.junit.jupiter.api.Assertions.*; +@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment. RANDOM_PORT) +@AutoConfigureMockMvc +class ProfileControllerTest { + @Autowired + MockMvc mockMvc; + @Test + @WithUserDetails("user@email.com") + void whenUserCallsProfileEndpointThenTheirProfileIsReturned() throws Exception { + + mockMvc.perform(MockMvcRequestBuilders.get("/profile-json") + .accept(MediaType.APPLICATION_JSON) + ) + .andExpect(MockMvcResultMatchers.status().is2xxSuccessful()) + .andExpect(MockMvcResultMatchers.content().json("{\"email\": \"user@email.com\"}")) + .andExpect(MockMvcResultMatchers.content().json("{\"bio\": \"Jane's bio\"}")); + } +} + + + + 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 index 135cad41d80e0c79de7d4b9dfabdd7f80f3a2158..f2079deb2e615797b8a18652b15866592f48ead7 100644 --- 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 @@ -1,59 +1,130 @@ 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.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import static org.junit.jupiter.api.Assertions.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; -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; +import java.util.List; +import java.util.Arrays; -@ExtendWith(MockitoExtension.class) + +@SpringBootTest +@Transactional public class RegisterTest { - @Mock - private UserRepository userRepository; // Mocking the UserRepository + @Autowired + private UserRepository userRepository; + + @BeforeEach + public void setUp() { + // Create and save 3 users to the database before each test + User user1 = new User(); + user1.setEmail("user1@example.com"); + user1.setPassword("password1"); + user1.setFullname("User One"); + user1.setDateOfBirth(LocalDate.of(2003, 1, 1)); + user1.setRoleId(1); + userRepository.saveUser(user1); - @InjectMocks - private UserServiceImpl userService; // Injecting mock into the service + User user2 = new User(); + user2.setEmail("user2@example.com"); + user2.setPassword("password2"); + user2.setFullname("User Two"); + user2.setDateOfBirth(LocalDate.of(2003, 2, 2)); + user2.setRoleId(2); + userRepository.saveUser(user2); + + User user3 = new User(); + user3.setEmail("user3@example.com"); + user3.setPassword("password3"); + user3.setFullname("User Three"); + user3.setDateOfBirth(LocalDate.of(2003, 3, 3)); + user3.setRoleId(1); + userRepository.saveUser(user3); + + System.out.println("3 users have been saved to the database."); + } @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); + public void testSaveUser() { + // create a new user + User newUser = new User(); + newUser.setEmail("newuser@example.com"); + newUser.setFullname("New User"); + newUser.setPassword("password4"); + newUser.setDateOfBirth(LocalDate.of(2000, 1, 1)); + newUser.setRoleId(1); + + // call the saveUser method from the repository + int userId = userRepository.saveUser(newUser); + + // print out the saved user's ID + System.out.println("Saved user ID: " + userId); + + // verify that the returned user ID is greater than 0, meaning the user was saved + assertTrue(userId > 0, "User ID is greater than 0 therefore it was a successful insert"); - 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 + } + + + @Test + public void testFindAllUsers() { + // call the repository method to retrieve all users + List<User> users = userRepository.findAllUsers(); + + // print out all users + System.out.println("Users list:"); + for (User user : users) { + System.out.println(user.getFullname() + " - " + user.getEmail()); + } + + // verify that the result is not null and contains the expected number of users assertNotNull(users); - assertFalse(users.isEmpty()); - assertEquals(1, users.size()); // Expecting 1 user + assertEquals(4, users.size()); + + // verify that the users have the correct details + for (User user : users) { + assertNotNull(user.getEmail()); + assertNotNull(user.getFullname()); + } + + System.out.println("Test completed: Found " + users.size() + " users."); + } + + + @Test + public void testFindByEmail() { + + // intialiaze the testing emails + List<String> emails = Arrays.asList("user1@example.com", "user2@example.com", "user3@example.com"); + + // loop through each email + for (String email : emails) { + // call the repository method to retrieve user by email + User userByEmail = userRepository.findByEmail(email); + + if (userByEmail != null) { + System.out.println("User found by email: " + userByEmail.getFullname() + " - " + userByEmail.getEmail()); + } else { + System.out.println("No user found with the email " + email); + } + + // verify that the user is returned + assertNotNull(userByEmail); + assertEquals(email, userByEmail.getEmail()); + } + + System.out.println("Test completed: Found 3 users"); + + User wrongUser = userRepository.findByEmail("nonexistent@example.com"); - System.out.println("Test completed successfully. Found " + users.size() + " users."); + // ensure that no user is found when + assertNull(wrongUser); } } diff --git a/src/test/java/polish_community_group_11/polish_community/translation/controller/TranslationControllerTest.java b/src/test/java/polish_community_group_11/polish_community/translation/controller/TranslationControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..187f7c4925e51412887b4649ca95b2ca67b2e911 --- /dev/null +++ b/src/test/java/polish_community_group_11/polish_community/translation/controller/TranslationControllerTest.java @@ -0,0 +1,85 @@ +package polish_community_group_11.polish_community.translation.controller; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import polish_community_group_11.polish_community.translation.model.Translation; +import polish_community_group_11.polish_community.translation.service.TranslationService; + +import java.util.Arrays; +import java.util.List; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(TranslationApisController.class) +class TranslationControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private TranslationService translationService; + + private List<Translation> mockTranslations; + + @BeforeEach + void setUp() { + mockTranslations = Arrays.asList( + new Translation(1, "greeting", "en", "Hello"), + new Translation(2, "greeting", "pl", "Cześć") + ); + } + + @Test + void getAllTranslations_shouldReturnAllTranslations() throws Exception { + + when(translationService.getAllTranslations()).thenReturn(mockTranslations); + mockMvc.perform(get("/api/translations") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].key", is("greeting"))) + .andExpect(jsonPath("$[0].value", is("Hello"))); + } + + @Test + void getTranslationsByLanguage_shouldReturnTranslationsForSpecificLanguage() throws Exception { + List<Translation> englishTranslations = List.of( + new Translation(1, "greeting", "en", "Hello") + ); + + when(translationService.getTranslationsByLanguage("en")).thenReturn(englishTranslations); + + mockMvc.perform(get("/api/translations/language/en") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].value", is("Hello"))); + } + + @Test + void addTranslation_shouldAddNewTranslation() throws Exception { + Translation newTranslation = new Translation(0, "farewell", "en", "Goodbye"); + + + Mockito.doNothing().when(translationService).addTranslation(Mockito.any(Translation.class)); + + mockMvc.perform(post("/api/translations") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"key\":\"farewell\",\"language\":\"en\",\"value\":\"Goodbye\"}")) + .andExpect(status().isOk()) + .andExpect(content().string("Translation added successfully.")); + } + + +} diff --git a/src/test/java/polish_community_group_11/polish_community/translation/service/TranslationServiceImplTest.java b/src/test/java/polish_community_group_11/polish_community/translation/service/TranslationServiceImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..18dad83d2eba0a5f25f7398e565f48fbcd1a23c1 --- /dev/null +++ b/src/test/java/polish_community_group_11/polish_community/translation/service/TranslationServiceImplTest.java @@ -0,0 +1,42 @@ +package polish_community_group_11.polish_community.translation.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import polish_community_group_11.polish_community.translation.model.Translation; +import polish_community_group_11.polish_community.translation.repository.TranslationRepository; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class TranslationServiceImplTest { + + @Mock + private TranslationRepository translationRepository; + + @InjectMocks + private TranslationServiceImpl translationService; + + private Translation mockTranslation; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + mockTranslation = new Translation(1, "greeting", "en", "Hello"); + } + + @Test + void getAllTranslations_shouldReturnAllTranslations() { + when(translationRepository.getAllTranslations()).thenReturn(List.of(mockTranslation)); + + List<Translation> result = translationService.getAllTranslations(); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("Hello", result.get(0).getValue()); + } +} diff --git a/uploads/36b2c38d-c9d5-4e14-a433-895a565d3abf_steve-johnson-D7AuHpLxLPA-unsplash.jpg b/uploads/36b2c38d-c9d5-4e14-a433-895a565d3abf_steve-johnson-D7AuHpLxLPA-unsplash.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82f2f1fa5fefccb98af0ffb041279b1a87d3fae4 Binary files /dev/null and b/uploads/36b2c38d-c9d5-4e14-a433-895a565d3abf_steve-johnson-D7AuHpLxLPA-unsplash.jpg differ diff --git a/uploads/4c5aed17-7ad1-490d-bb5c-a98d7a1d6406_nicolas-jehly-0UU9-_1EMvM-unsplash.jpg b/uploads/4c5aed17-7ad1-490d-bb5c-a98d7a1d6406_nicolas-jehly-0UU9-_1EMvM-unsplash.jpg new file mode 100644 index 0000000000000000000000000000000000000000..648e6c93860d6984c454a484b2ef392bd8fbc642 Binary files /dev/null and b/uploads/4c5aed17-7ad1-490d-bb5c-a98d7a1d6406_nicolas-jehly-0UU9-_1EMvM-unsplash.jpg differ diff --git a/uploads/5720f047-a3ca-4d4e-ab20-343aae7cc485_premium_photo-1733514691627-e62171fc052c.avif b/uploads/5720f047-a3ca-4d4e-ab20-343aae7cc485_premium_photo-1733514691627-e62171fc052c.avif new file mode 100644 index 0000000000000000000000000000000000000000..9631a6ae6f56719d409cad7d80ff047a2b6c78c4 Binary files /dev/null and b/uploads/5720f047-a3ca-4d4e-ab20-343aae7cc485_premium_photo-1733514691627-e62171fc052c.avif differ diff --git a/uploads/d0753820-30b3-429c-92c4-d82c910ba083_nicolas-jehly-0UU9-_1EMvM-unsplash.jpg b/uploads/d0753820-30b3-429c-92c4-d82c910ba083_nicolas-jehly-0UU9-_1EMvM-unsplash.jpg new file mode 100644 index 0000000000000000000000000000000000000000..648e6c93860d6984c454a484b2ef392bd8fbc642 Binary files /dev/null and b/uploads/d0753820-30b3-429c-92c4-d82c910ba083_nicolas-jehly-0UU9-_1EMvM-unsplash.jpg differ