diff --git a/src/main/java/uk/ac/cf/spring/demo/sports/config/WebConfig.java b/src/main/java/uk/ac/cf/spring/demo/sports/config/WebConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..4bd1f4302b749e201d9d8f90819e33f9ae8043ec --- /dev/null +++ b/src/main/java/uk/ac/cf/spring/demo/sports/config/WebConfig.java @@ -0,0 +1,14 @@ +package uk.ac.cf.spring.demo.sports.config; + +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 { + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/uploads/**") + .addResourceLocations("file:./uploads/"); // æŒ‡å®šé™æ€èµ„æºçš„实际路径 + } +} diff --git a/src/main/java/uk/ac/cf/spring/demo/sports/images/Image.java b/src/main/java/uk/ac/cf/spring/demo/sports/images/Image.java new file mode 100644 index 0000000000000000000000000000000000000000..b51b609b91fcce93d91ffd7c3d89372f01548955 --- /dev/null +++ b/src/main/java/uk/ac/cf/spring/demo/sports/images/Image.java @@ -0,0 +1,16 @@ +package uk.ac.cf.spring.demo.sports.images; + +import java.sql.Timestamp; +import lombok.Data; +import lombok.NoArgsConstructor; +@Data +@NoArgsConstructor + +public class Image { + private Integer id; + private String url; + private Timestamp uploadedAt; + + +} + diff --git a/src/main/java/uk/ac/cf/spring/demo/sports/images/ImageController.java b/src/main/java/uk/ac/cf/spring/demo/sports/images/ImageController.java new file mode 100644 index 0000000000000000000000000000000000000000..c137de20530d933d88f72b4ff075e905d877e7ff --- /dev/null +++ b/src/main/java/uk/ac/cf/spring/demo/sports/images/ImageController.java @@ -0,0 +1,39 @@ +package uk.ac.cf.spring.demo.sports.images; +import java.io.IOException; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping("/api/images") +public class ImageController { + private final ImageService imageService; + + @Autowired + public ImageController(ImageService imageService) { + this.imageService = imageService; + } + + @GetMapping + public List<Image> getImages() { + List<Image> images = imageService.getAllImages(); + System.out.println("Returned JSON data: " + images); + return imageService.getAllImages(); + } + + @PostMapping + public ResponseEntity<Void> uploadImage(@RequestParam("image") MultipartFile file) { + try { + imageService.saveImage(file); + List<Image> images = imageService.getAllImages(); + System.out.println("Returned JSON data: " + images); + return ResponseEntity.ok().build(); + } catch (IOException e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } +} + diff --git a/src/main/java/uk/ac/cf/spring/demo/sports/images/ImageRepository.java b/src/main/java/uk/ac/cf/spring/demo/sports/images/ImageRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..3feedb21d13c98ddd40abc5592b4e2436dc5e50c --- /dev/null +++ b/src/main/java/uk/ac/cf/spring/demo/sports/images/ImageRepository.java @@ -0,0 +1,42 @@ +package uk.ac.cf.spring.demo.sports.images; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +@Repository +public class ImageRepository { + private final JdbcTemplate jdbcTemplate; + + @Autowired + public ImageRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + + public int save(String url) { + String sql = "INSERT INTO images (url) VALUES (?)"; + return jdbcTemplate.update(sql, url); + } + // èŽ·å–æ‰€æœ‰å›¾ç‰‡è®°å½• + public List<Image> findAll() { + String sql = "SELECT * FROM images"; + return jdbcTemplate.query(sql, new ImageRowMapper()); + } + //æ˜ å°„æ–¹æ³• + private static class ImageRowMapper implements RowMapper<Image> { + @Override + public Image mapRow(ResultSet rs, int rowNum) throws SQLException { + Image image = new Image(); + image.setId(rs.getInt("id")); + image.setUrl(rs.getString("url")); + image.setUploadedAt(rs.getTimestamp("uploaded_at")); + return image; + } + } + +} diff --git a/src/main/java/uk/ac/cf/spring/demo/sports/images/ImageService.java b/src/main/java/uk/ac/cf/spring/demo/sports/images/ImageService.java new file mode 100644 index 0000000000000000000000000000000000000000..7ea853716315f773b269a6b96fb220e54dbc5f19 --- /dev/null +++ b/src/main/java/uk/ac/cf/spring/demo/sports/images/ImageService.java @@ -0,0 +1,11 @@ +package uk.ac.cf.spring.demo.sports.images; + +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; + +public interface ImageService { + List<Image> getAllImages(); + void saveImage(MultipartFile file) throws IOException; +} diff --git a/src/main/java/uk/ac/cf/spring/demo/sports/images/ImageServiceImpl.java b/src/main/java/uk/ac/cf/spring/demo/sports/images/ImageServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..bac30bb083bd67189a56cfbd86f3fd4bb8373572 --- /dev/null +++ b/src/main/java/uk/ac/cf/spring/demo/sports/images/ImageServiceImpl.java @@ -0,0 +1,50 @@ +package uk.ac.cf.spring.demo.sports.images; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.beans.factory.annotation.Value; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.UUID; + +@Service +public class ImageServiceImpl implements ImageService { + + private final ImageRepository imageRepository; + + @Value("${image.upload.dir}") + private String uploadDir; + + @Autowired + public ImageServiceImpl(ImageRepository imageRepository) { + this.imageRepository = imageRepository; + } + + @Override + public List<Image> getAllImages() { + return imageRepository.findAll(); + } + + @Override + public void saveImage(MultipartFile file) throws IOException { + // 生æˆå”¯ä¸€æ–‡ä»¶å + String fileName = UUID.randomUUID().toString() + "_" + file.getOriginalFilename(); + Path filePath = Paths.get(uploadDir, fileName); + + // ç¡®ä¿ç›®å½•å˜åœ¨ + Files.createDirectories(filePath.getParent()); + + // å°†æ–‡ä»¶å†™å…¥ç›®æ ‡ç›®å½• + Files.write(filePath, file.getBytes()); + + // ä¿å˜å›¾ç‰‡è·¯å¾„到数æ®åº“ + String url = "/uploads/" + fileName; + imageRepository.save(url); + } + +} diff --git a/src/main/java/uk/ac/cf/spring/demo/sports/security/SecurityConfig.java b/src/main/java/uk/ac/cf/spring/demo/sports/security/SecurityConfig.java index 10a8e80dedb69bb13ea24da5ff55cbf1fa1189ff..e401b0ff6a5f35c377ab513f4f56e65afceddf82 100644 --- a/src/main/java/uk/ac/cf/spring/demo/sports/security/SecurityConfig.java +++ b/src/main/java/uk/ac/cf/spring/demo/sports/security/SecurityConfig.java @@ -11,72 +11,52 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import static org.springframework.security.config.Customizer.withDefaults; - @Configuration @EnableWebSecurity public class SecurityConfig { public static final String[] ENDPOINTS_WHITELIST = { "/pics/**", - "/css/**", // .CSS Style Files - "/js/**", // .JS Files + "/css/**", // Style Files + "/js/**", // JS Files "/images/**", // images "/", // root path "/match", // match data path "/match/**", - "/users", // user data path - "/users/**", "/api/**", // API "/html/**", // static HTML - "/html/register.html", // registration path + "/html/register.html", // register "/html/login.html", // login "/html/table-tennisrules.html", + "/html/login.html" , // login "/rankings", //ranking data path "/rankings/**", - "/html/table-tennisrules.html" - }; + "/uploads", //images + "/uploads/**", - public static final String[] ADMIN_WHITELIST = { - "/admin/**", }; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - // Configure access rights for paths .authorizeHttpRequests(request -> request - .requestMatchers(ENDPOINTS_WHITELIST).permitAll() // Allow whitelisted pages without authentication - .requestMatchers("/users/current").permitAll() - .requestMatchers("/html/backGroundMatch.html").hasRole("ADMIN") // Only users with the ADMIN role are allowed to access - .anyRequest().authenticated() // Other paths require authentication + .requestMatchers(ENDPOINTS_WHITELIST).permitAll() // å…许白åå•页é¢ä¸éœ€è¦è®¤è¯ + .anyRequest().authenticated() // 其他页é¢éœ€è¦è®¤è¯ ) - - // Disable CSRF (can be enabled on demand) .csrf().disable() - - // Forms Login Configuration +// .formLogin(form -> form +// .loginPage("/login").permitAll() // å®šä¹‰è‡ªå®šä¹‰ç™»å½•é¡µé¢ +// ) .formLogin(form -> form - .loginPage("/html/login.html").permitAll() // Using a customized login page - .defaultSuccessUrl("/html/matchSchedule.html", true) // After successful login, you will be redirected to this page - .failureUrl("/html/login.html?error=true") // You will be redirected to this page after a failed login + .loginPage("/html/login.html").permitAll() // ä½¿ç”¨è‡ªå®šä¹‰é¡µé¢ + .defaultSuccessUrl("/html/matchSchedule.html", true) // 登录æˆåŠŸåŽè·³è½¬ + .failureUrl("/html/login.html?error=true") // 登录失败åŽè·³è½¬ ) - - // Basic authentication configuration (for API access) - .httpBasic(withDefaults()) // Basic Auth is supported for API calls - - // Logout Configuration - .logout(logout -> logout - .logoutUrl("/logout") // Set the logout path - .logoutSuccessUrl("/html/login.html") // The path to which you are redirected after the logout is successful - .invalidateHttpSession(true) // Invalidate the session - .clearAuthentication(true) // Clear authentication information - ); + .logout().logoutUrl("/logout").logoutSuccessUrl("/"); // 登出é…ç½® return http.build(); } - @Bean public UserDetailsService userDetailsService() { UserDetails user = @@ -90,7 +70,7 @@ public class SecurityConfig { @Bean public BCryptPasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); // Use BCrypt for password encryption + return new BCryptPasswordEncoder(); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a025cc6041e1be9ce644d6cdb23ec401ff2e70e6..59586e3f5bac34c5411ad3b7cfa4e13e2aebf1e4 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,4 +9,5 @@ spring.datasource.driver-class-name=org.mariadb.jdbc.Driver #spring.mvc.view.suffix=.html # ???? spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html -spring.thymeleaf.mode=HTML \ No newline at end of file +spring.thymeleaf.mode=HTML +image.upload.dir=./uploads \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 82dc0657f8670edb6b2ff9196240444d93ac8912..121020aa60db2d055e5cb47010ab7dc5b6cc35bb 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -46,7 +46,6 @@ CREATE TABLE IF NOT EXISTS ranking ( ); CREATE TABLE images ( id INT AUTO_INCREMENT PRIMARY KEY, - filename VARCHAR(255) NOT NULL, -- Image file name or path - uploader VARCHAR(100) NOT NULL, -- Uploader Name - upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- Upload time + url VARCHAR(255) NOT NULL, -- å›¾ç‰‡æ–‡ä»¶åæˆ–路径 + uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP-- ä¸Šä¼ æ—¶é—´ ); diff --git a/src/main/resources/static/css/gallery.css b/src/main/resources/static/css/gallery.css new file mode 100644 index 0000000000000000000000000000000000000000..0404d3e5d594bcdb5f44fe1ba4397b4f29cf5a9e --- /dev/null +++ b/src/main/resources/static/css/gallery.css @@ -0,0 +1,89 @@ +/* General Styles for Gallery */ +.gallery-container { + max-width: 1200px; + margin: 20px auto; + padding: 20px; + text-align: center; + font-family: Arial, sans-serif; + background-color: #f9f9f9; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.gallery-container h1 { + margin-bottom: 20px; + font-size: 2em; + color: #333; +} + +/* Carousel Styles */ +.carousel { + position: relative; + overflow: hidden; + width: 80%; + margin: 0 auto; + max-height: 400px; +} + +#carousel-images { + display: flex; + transition: transform 0.5s ease-in-out; +} + +#carousel-images img { + max-width: 100%; + max-height: 400px; + object-fit: cover; + border-radius: 8px; +} + +.carousel-button { + position: absolute; + top: 50%; + transform: translateY(-50%); + background-color: rgba(0, 0, 0, 0.5); + color: white; + border: none; + padding: 10px; + cursor: pointer; + border-radius: 50%; + font-size: 1.5em; +} + +.carousel-button.prev { + left: 10px; +} + +.carousel-button.next { + right: 10px; +} + +.carousel-button:hover { + background-color: rgba(0, 0, 0, 0.8); +} + +/* Upload Section */ +.upload-section { + margin-top: 20px; +} + +.upload-btn { + background-color: #007bff; + color: white; + border: none; + padding: 10px 20px; + font-size: 1em; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.upload-btn:hover { + background-color: #0056b3; +} +.carousel img { + display: block; /* Ensure the image displays as a block-level element */ + margin: 0 auto; /* Horizontally centers the image */ + max-width: 100%; /* Keep image responsive */ + height: auto; +} \ No newline at end of file diff --git a/src/main/resources/static/html/gallery.html b/src/main/resources/static/html/gallery.html new file mode 100644 index 0000000000000000000000000000000000000000..71a86f2db25eec323ba3cd86bcc15170d7d08dbd --- /dev/null +++ b/src/main/resources/static/html/gallery.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>image gallery</title> + <link rel="stylesheet" href="/css/navBar.css"> + <link rel="stylesheet" href="/css/footer.css"> + <link rel="stylesheet" href="/css/gallery.css"> +</head> +<body> +<div id="navbar-container"></div> + +<!-- Main Content --> +<main class="gallery-container"> + <h1>Image Gallery</h1> + + <!-- Carousel --> + <div class="carousel"> + <div id="carousel-images"> + <!-- Images will be dynamically inserted here --> + </div> + <button class="carousel-button prev" onclick="prevImage()">❮</button> + <button class="carousel-button next" onclick="nextImage()">❯</button> + </div> + + <!-- Upload Section --> + <div class="upload-section"> + <button class="upload-btn" onclick="document.getElementById('file-input').click()">Upload Image</button> + <input type="file" id="file-input" accept="image/*" style="display: none;" onchange="uploadImage(event)"> + </div> +</main> + +<!-- Footer Section --> +<div id="footer-container"></div> + +<!-- JavaScript --> +<script src="/js/navBar.js"></script> +<script src="/js/footer.js"></script> +<script src="/js/gallery.js"></script> +<script> + // Load header and footer + loadNavbar('navbar-container'); + loadFooter('footer-container'); +</script> +</body> +</html> \ No newline at end of file diff --git a/src/main/resources/static/html/navBar.html b/src/main/resources/static/html/navBar.html index 4d9895c801b6ca59aa56f193fb168e9806501275..c1e68cafbb857d9ddc335d6ed4f92106a7a89f1c 100644 --- a/src/main/resources/static/html/navBar.html +++ b/src/main/resources/static/html/navBar.html @@ -5,7 +5,7 @@ <!-- put nav links here --> <ul class="nav-links"> <li><a href="#">News</a></li> - <li><a href="#">Videos</a></li> + <li><a href="http://localhost:8080/html/gallery.html">Gallery</a></li> <li><a href="http://localhost:8080/html/rules.html">Rules</a></li> <li><a href="http://localhost:8080/html/UserCenter.html">Players</a></li> <li><a href="http://localhost:8080/html/matchSchedule.html">Matches</a></li> diff --git a/src/main/resources/static/html/ranking.html b/src/main/resources/static/html/ranking.html index 2be8d2275c1f3958708e60d74d19678f7d0b0ffb..691ed42b53246422fa502a51bbbd800363ca12f8 100644 --- a/src/main/resources/static/html/ranking.html +++ b/src/main/resources/static/html/ranking.html @@ -52,6 +52,5 @@ loadFooter('footer-container'); </script> <script src="/js/rankingTable.js"></script> -<!--<script src="/js/matchSchedule.js"></script>--> </body> </html> \ No newline at end of file diff --git a/src/main/resources/static/js/gallery.js b/src/main/resources/static/js/gallery.js new file mode 100644 index 0000000000000000000000000000000000000000..68949a4ed82cabdaf0cfb421cc27e5db464190d6 --- /dev/null +++ b/src/main/resources/static/js/gallery.js @@ -0,0 +1,70 @@ +let currentIndex = 0; +let images = []; // This will hold the URLs of the images + +// Load images into the carousel +function loadImages() { + const carouselImages = document.getElementById("carousel-images"); + carouselImages.innerHTML = ""; // Clear current images + + images.forEach((imageUrl, index) => { + const img = document.createElement("img"); + img.src = imageUrl; // Correctly use the URL for the image src + console.log(img.src) + img.alt = `Image ${index + 1}`; + img.style.display = index === currentIndex ? "block" : "none"; + carouselImages.appendChild(img); + }); +} + +// Show previous image +function prevImage() { + currentIndex = (currentIndex - 1 + images.length) % images.length; + loadImages(); +} + +// Show next image +function nextImage() { + currentIndex = (currentIndex + 1) % images.length; + loadImages(); +} + +// Upload image +function uploadImage(event) { + const file = event.target.files[0]; + if (!file) return; + + const formData = new FormData(); + formData.append("image", file); + + fetch("/api/images", { + method: "POST", + body: formData, + }) + .then(response => { + if (!response.ok) { + throw new Error("Failed to upload image"); + } + return response.json(); + }) + .then(data => { + images.push(data.url); // Assuming the server returns the image URL + loadImages(); + }) + .catch(error => { + console.error("Error uploading image:", error); + }); +} + +// Initialize the gallery on page load +window.onload = () => { + fetch("/api/images") + .then(response => response.json()) + .then(data => { + images = data.map(image => image.url); // Extract URLs from the server response + loadImages(); + }) + .catch(error => { + console.error("Error loading images:", error); + }); +}; + diff --git a/uploads/2acbe313-8dea-40df-b480-b209a15e4263_dart-1418245.webp b/uploads/2acbe313-8dea-40df-b480-b209a15e4263_dart-1418245.webp new file mode 100644 index 0000000000000000000000000000000000000000..7da8666fdc10a75cc88eb5541adedaf3967893d0 Binary files /dev/null and b/uploads/2acbe313-8dea-40df-b480-b209a15e4263_dart-1418245.webp differ diff --git a/uploads/9c7253d7-9608-432e-ac38-f23a3521b080_pair-ping-pong-paddles-ball-260nw-2472167701.webp b/uploads/9c7253d7-9608-432e-ac38-f23a3521b080_pair-ping-pong-paddles-ball-260nw-2472167701.webp new file mode 100644 index 0000000000000000000000000000000000000000..090a736346d3d720a6315a369dca22aaafba0ca3 Binary files /dev/null and b/uploads/9c7253d7-9608-432e-ac38-f23a3521b080_pair-ping-pong-paddles-ball-260nw-2472167701.webp differ