mirror of
https://gitlab.com/etc404/software-engineering-project.git
synced 2026-05-10 20:52:58 +00:00
Update 16 files
- /src/main/resources/templates/create-account.html - /src/main/resources/templates/my-profile.html - /src/main/resources/templates/home.html - /src/main/resources/templates/update-recipe.html - /src/main/resources/templates/explore.html - /src/main/resources/templates/public-profile.html - /src/main/java/com/example/demo/controller/SiteController.java - /src/main/java/com/example/demo/controller/ProfileController.java - /src/main/java/com/example/demo/dto/UserDto.java - /src/main/java/com/example/demo/dto/UpdateProfileDto.java - /src/main/java/com/example/demo/dto/ProfileDto.java - /src/main/java/com/example/demo/service/Impl/RecipeServiceImpl.java - /src/main/java/com/example/demo/service/Impl/UserServiceImpl.java - /src/main/java/com/example/demo/service/UserService.java - /src/main/java/com/example/demo/config/SecurityConfig.java - /src/main/java/com/example/demo/entity/User.java
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
package com.example.demo.config;
|
||||
ppackage com.example.demo.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -6,7 +6,6 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.csrf.CsrfAuthenticationStrategy;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
@@ -22,6 +21,7 @@ public class SecurityConfig {
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/login", "/register", "/css/**", "/images/**").permitAll()
|
||||
.requestMatchers("/api/users").permitAll()
|
||||
.requestMatchers("/users/**").permitAll()
|
||||
.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
||||
import com.example.demo.dto.ProfileDto;
|
||||
import com.example.demo.dto.UpdateProfileDto;
|
||||
import com.example.demo.service.UserService;
|
||||
|
||||
@Controller
|
||||
public class ProfileController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
public ProfileController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping("/users/{id}")
|
||||
public String viewPublicProfile(@PathVariable Integer id, Model model) {
|
||||
ProfileDto profile = userService.getProfileByUserId(id);
|
||||
model.addAttribute("profile", profile);
|
||||
return "public-profile";
|
||||
}
|
||||
|
||||
@GetMapping("/my-profile")
|
||||
public String viewMyProfile(Principal principal, Model model) {
|
||||
String username = principal.getName();
|
||||
|
||||
ProfileDto profile = userService.getCurrentUserProfile(username);
|
||||
|
||||
UpdateProfileDto updateProfileDto = new UpdateProfileDto();
|
||||
updateProfileDto.setDisplayName(profile.getDisplayName());
|
||||
updateProfileDto.setBio(profile.getBio());
|
||||
|
||||
model.addAttribute("profile", profile);
|
||||
model.addAttribute("updateProfileDto", updateProfileDto);
|
||||
|
||||
return "my-profile";
|
||||
}
|
||||
|
||||
@PostMapping("/my-profile/update")
|
||||
public String updateMyProfile(@ModelAttribute UpdateProfileDto dto, Principal principal) {
|
||||
String username = principal.getName();
|
||||
userService.updateProfile(username, dto);
|
||||
return "redirect:/my-profile";
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,8 @@ package com.example.demo.controller;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import com.example.demo.service.RecipeService;
|
||||
import com.example.demo.dto.RecipeDto;
|
||||
import com.example.demo.entity.Recipe;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -20,10 +18,10 @@ public class SiteController {
|
||||
public SiteController(RecipeService recipeService) {
|
||||
this.recipeService = recipeService;
|
||||
}
|
||||
|
||||
@GetMapping("/")
|
||||
public String viewHomePage(Model model) {
|
||||
//model.addAttribute("allemplist", employeeServiceImpl.getAllEmployee());
|
||||
List<RecipeDto> recipes = recipeService.getAllRecipes();
|
||||
List<RecipeDto> recipes = recipeService.getAllRecipes();
|
||||
model.addAttribute("recipes", recipes);
|
||||
return "home";
|
||||
}
|
||||
@@ -50,13 +48,12 @@ public class SiteController {
|
||||
return "view-recipe";
|
||||
}
|
||||
|
||||
// @GetMapping("/explore")
|
||||
// public String viewExplorePage(Model model) {
|
||||
// //model.addAttribute("allemplist", employeeServiceImpl.getAllEmployee());
|
||||
// List<RecipeDto> recipes = recipeService.getAllRecipes();
|
||||
// model.addAttribute("recipes", recipes);
|
||||
// return "explore";
|
||||
// }
|
||||
@GetMapping("/recipes/{id}/edit")
|
||||
public String viewEditRecipePage(@PathVariable Integer id, Model model) {
|
||||
RecipeDto recipe = recipeService.getRecipeById(id);
|
||||
model.addAttribute("recipe", recipe);
|
||||
return "update-recipe";
|
||||
}
|
||||
|
||||
@GetMapping("/explore")
|
||||
public String explore(
|
||||
@@ -69,13 +66,8 @@ public class SiteController {
|
||||
) {
|
||||
List<RecipeDto> recipes = recipeService.getRecipes(q, tags, prices, cookTime, prepTime);
|
||||
model.addAttribute("recipes", recipes);
|
||||
String displayQuery = q;
|
||||
|
||||
|
||||
model.addAttribute("q", q);
|
||||
model.addAttribute("tags", tags);
|
||||
return "explore";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.example.demo.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ProfileDto {
|
||||
private Integer id;
|
||||
private String username;
|
||||
private String displayName;
|
||||
private String bio;
|
||||
private List<RecipeDto> recipes;
|
||||
|
||||
public ProfileDto() {
|
||||
}
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public void setDisplayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getBio() {
|
||||
return bio;
|
||||
}
|
||||
|
||||
public void setBio(String bio) {
|
||||
this.bio = bio;
|
||||
}
|
||||
|
||||
public List<RecipeDto> getRecipes() {
|
||||
return recipes;
|
||||
}
|
||||
|
||||
public void setRecipes(List<RecipeDto> recipes) {
|
||||
this.recipes = recipes;
|
||||
}
|
||||
|
||||
public String getEffectiveDisplayName() {
|
||||
if (displayName != null && !displayName.isBlank()) {
|
||||
return displayName;
|
||||
}
|
||||
return username;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.example.demo.dto;
|
||||
|
||||
public class UpdateProfileDto {
|
||||
private String displayName;
|
||||
private String bio;
|
||||
|
||||
public UpdateProfileDto() {
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public void setDisplayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getBio() {
|
||||
return bio;
|
||||
}
|
||||
|
||||
public void setBio(String bio) {
|
||||
this.bio = bio;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ public class UserDto {
|
||||
private Integer id;
|
||||
private String username;
|
||||
private String email;
|
||||
private String displayName;
|
||||
private String bio;
|
||||
|
||||
public UserDto() {
|
||||
}
|
||||
@@ -14,6 +16,14 @@ public class UserDto {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public UserDto(Integer id, String username, String email, String displayName, String bio) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.email = email;
|
||||
this.displayName = displayName;
|
||||
this.bio = bio;
|
||||
}
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -38,4 +48,26 @@ public class UserDto {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public void setDisplayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getBio() {
|
||||
return bio;
|
||||
}
|
||||
|
||||
public void setBio(String bio) {
|
||||
this.bio = bio;
|
||||
}
|
||||
|
||||
public String getEffectiveDisplayName() {
|
||||
if (displayName != null && !displayName.isBlank()) {
|
||||
return displayName;
|
||||
}
|
||||
return username;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,12 @@ public class User implements UserDetails {
|
||||
@Column(nullable = false, unique = true)
|
||||
private String username;
|
||||
|
||||
@Column(length = 100)
|
||||
private String displayName;
|
||||
|
||||
@Column(length = 1000)
|
||||
private String bio;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String role;
|
||||
|
||||
@@ -118,6 +124,22 @@ public class User implements UserDetails {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public void setDisplayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getBio() {
|
||||
return bio;
|
||||
}
|
||||
|
||||
public void setBio(String bio) {
|
||||
this.bio = bio;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
@@ -173,4 +195,11 @@ public class User implements UserDetails {
|
||||
public void setFavRecipes(Set<Recipe> favRecipes) {
|
||||
this.FavRecipes = favRecipes;
|
||||
}
|
||||
|
||||
public String getEffectiveDisplayName() {
|
||||
if (displayName != null && !displayName.isBlank()) {
|
||||
return displayName;
|
||||
}
|
||||
return username;
|
||||
}
|
||||
}
|
||||
@@ -71,8 +71,13 @@ public class RecipeServiceImpl implements RecipeService {
|
||||
|
||||
List<TagDto> tagDtos = recipe.getTags().stream().map(ri -> new TagDto(ri.getName())).toList();
|
||||
|
||||
UserDto userDto = new UserDto(recipe.getUser().getId(), recipe.getUser().getUsername(),
|
||||
recipe.getUser().getEmail());
|
||||
UserDto userDto = new UserDto(
|
||||
recipe.getUser().getId(),
|
||||
recipe.getUser().getUsername(),
|
||||
recipe.getUser().getEmail(),
|
||||
recipe.getUser().getDisplayName(),
|
||||
recipe.getUser().getBio()
|
||||
);
|
||||
|
||||
RecipeDto dto = new RecipeDto(recipe.getTitle(), recipe.getDescription(), recipe.getPrepTimeMinutes(),
|
||||
recipe.getCookTimeMinutes(), recipe.getServings(), userDto, recipe.getStatus(), ingredientDtos,
|
||||
@@ -129,7 +134,6 @@ public class RecipeServiceImpl implements RecipeService {
|
||||
ensureUserNotBanned(currentUser);
|
||||
enforceUploadLimit(currentUser);
|
||||
|
||||
|
||||
Recipe recipe = new Recipe(dto.getTitle(), dto.getDescription(), dto.getPrepTimeMinutes(),
|
||||
dto.getCookTimeMinutes(), dto.getServings(), currentUser, dto.getStatus(), dto.getCost());
|
||||
|
||||
@@ -428,7 +432,6 @@ public class RecipeServiceImpl implements RecipeService {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
List<RecipeDto> recipeList = new ArrayList<>();
|
||||
|
||||
for (Recipe recipe : recipes) {
|
||||
|
||||
@@ -2,24 +2,20 @@ package com.example.demo.service.Impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.example.demo.dto.ImageDto;
|
||||
import com.example.demo.dto.RecipeDto;
|
||||
import com.example.demo.dto.RecipeIngredientDto;
|
||||
import com.example.demo.dto.StepDto;
|
||||
import com.example.demo.dto.TagDto;
|
||||
import com.example.demo.dto.ProfileDto;
|
||||
import com.example.demo.dto.UpdateProfileDto;
|
||||
import com.example.demo.dto.UserDto;
|
||||
import com.example.demo.dto.RecipeDto;
|
||||
import com.example.demo.entity.Recipe;
|
||||
import com.example.demo.entity.User;
|
||||
import com.example.demo.exception.NotFoundException;
|
||||
import com.example.demo.repository.RecipeRepo;
|
||||
import com.example.demo.repository.UserRepo;
|
||||
import com.example.demo.service.RecipeService;
|
||||
import com.example.demo.service.UserService;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
@@ -30,16 +26,26 @@ public class UserServiceImpl implements UserService {
|
||||
private UserRepo userRepository;
|
||||
private RecipeRepo recipeRepository;
|
||||
private PasswordEncoder passwordEncoder;
|
||||
private RecipeService recipeService;
|
||||
|
||||
public UserServiceImpl(UserRepo userRepository, RecipeRepo recipeRepository, PasswordEncoder passwordEncoder) {
|
||||
public UserServiceImpl(UserRepo userRepository, RecipeRepo recipeRepository,
|
||||
PasswordEncoder passwordEncoder, RecipeService recipeService) {
|
||||
super();
|
||||
this.userRepository = userRepository;
|
||||
this.recipeRepository = recipeRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.recipeService = recipeService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDto convertToDto(User user) {
|
||||
return new UserDto(user.getId(), user.getUsername(), user.getEmail());
|
||||
return new UserDto(
|
||||
user.getId(),
|
||||
user.getUsername(),
|
||||
user.getEmail(),
|
||||
user.getDisplayName(),
|
||||
user.getBio()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -49,27 +55,23 @@ public class UserServiceImpl implements UserService {
|
||||
}
|
||||
user.setBanned(false);
|
||||
user.setHashedpassword(passwordEncoder.encode(user.getPassword()));
|
||||
|
||||
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserDto> getAllUsers() {
|
||||
|
||||
List<UserDto> list = new ArrayList<>();
|
||||
for (User user : userRepository.findAll()) {
|
||||
UserDto userDto = convertToDto(user);
|
||||
list.add(userDto);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDto getUserById(Integer Id) {
|
||||
|
||||
return convertToDto(userRepository.findById(Id).orElseThrow(() -> new NotFoundException("User", "id", Id)));
|
||||
public UserDto getUserById(Integer id) {
|
||||
return convertToDto(userRepository.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("User", "id", id)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,9 +90,9 @@ public class UserServiceImpl implements UserService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDto updateUser(User user, Integer Id) {
|
||||
|
||||
User existingUser = userRepository.findById(Id).orElseThrow(() -> new NotFoundException("User", "id", Id));
|
||||
public UserDto updateUser(User user, Integer id) {
|
||||
User existingUser = userRepository.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("User", "id", id));
|
||||
|
||||
existingUser.setUsername(user.getUsername());
|
||||
existingUser.setEmail(user.getEmail());
|
||||
@@ -101,9 +103,10 @@ public class UserServiceImpl implements UserService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteUser(Integer Id) {
|
||||
userRepository.findById(Id).orElseThrow(() -> new NotFoundException("User", "id", Id));
|
||||
userRepository.deleteById(Id);
|
||||
public void deleteUser(Integer id) {
|
||||
userRepository.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("User", "id", id));
|
||||
userRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -114,27 +117,26 @@ public class UserServiceImpl implements UserService {
|
||||
|
||||
Recipe existingRecipe = recipeRepository.findById(recipeId)
|
||||
.orElseThrow(() -> new NotFoundException("Recipe", "id", recipeId));
|
||||
userRepository.save(existingUser);
|
||||
|
||||
existingUser.getFavRecipes().remove(existingRecipe);
|
||||
|
||||
userRepository.save(existingUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserDto> getUsersByName(String name) {
|
||||
List<User> users = userRepository.findByUsernameContainingIgnoreCase(name);
|
||||
|
||||
if (users.isEmpty()) {
|
||||
throw new NotFoundException("User", "username containing", name);
|
||||
}
|
||||
if (users.isEmpty()) {
|
||||
throw new NotFoundException("User", "username containing", name);
|
||||
}
|
||||
|
||||
List<UserDto> userList = new ArrayList<>();
|
||||
List<UserDto> userList = new ArrayList<>();
|
||||
|
||||
for (User user : users) {
|
||||
UserDto dto = convertToDto(user);
|
||||
userList.add(dto);
|
||||
}
|
||||
return userList;
|
||||
for (User user : users) {
|
||||
UserDto dto = convertToDto(user);
|
||||
userList.add(dto);
|
||||
}
|
||||
return userList;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -172,4 +174,50 @@ public class UserServiceImpl implements UserService {
|
||||
userRepository.save(user);
|
||||
return convertToDto(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ProfileDto getProfileByUserId(Integer userId) {
|
||||
User user = userRepository.findById(userId)
|
||||
.orElseThrow(() -> new NotFoundException("User", "id", userId));
|
||||
|
||||
List<Recipe> recipes = recipeRepository.findByUserId(userId);
|
||||
List<RecipeDto> recipeDtos = new ArrayList<>();
|
||||
|
||||
for (Recipe recipe : recipes) {
|
||||
recipeDtos.add(recipeService.convertToDto(recipe));
|
||||
}
|
||||
|
||||
ProfileDto profile = new ProfileDto();
|
||||
profile.setId(user.getId());
|
||||
profile.setUsername(user.getUsername());
|
||||
profile.setDisplayName(user.getDisplayName());
|
||||
profile.setBio(user.getBio());
|
||||
profile.setRecipes(recipeDtos);
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ProfileDto getCurrentUserProfile(String username) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new NotFoundException("User", "username", username));
|
||||
|
||||
return getProfileByUserId(user.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ProfileDto updateProfile(String username, UpdateProfileDto dto) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new NotFoundException("User", "username", username));
|
||||
|
||||
user.setDisplayName(dto.getDisplayName());
|
||||
user.setBio(dto.getBio());
|
||||
|
||||
userRepository.save(user);
|
||||
|
||||
return getProfileByUserId(user.getId());
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package com.example.demo.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.example.demo.dto.ProfileDto;
|
||||
import com.example.demo.dto.UpdateProfileDto;
|
||||
import com.example.demo.dto.UserDto;
|
||||
import com.example.demo.entity.User;
|
||||
|
||||
@@ -31,4 +33,10 @@ public interface UserService {
|
||||
UserDto makeAdmin(Integer id);
|
||||
|
||||
UserDto makeUser(Integer id);
|
||||
|
||||
ProfileDto getProfileByUserId(Integer userId);
|
||||
|
||||
ProfileDto getCurrentUserProfile(String username);
|
||||
|
||||
ProfileDto updateProfile(String username, UpdateProfileDto dto);
|
||||
}
|
||||
|
||||
@@ -51,34 +51,39 @@
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
const form = document.getElementById("createUserForm");
|
||||
const passwordField = document.getElementById("password");
|
||||
const confirmPasswordField = document.getElementById("confirmPassword");
|
||||
const passwordError = document.getElementById("passwordError");
|
||||
|
||||
function checkPasswords() {
|
||||
|
||||
if (confirmPasswordField.value === "") {
|
||||
confirmPasswordField.classList.remove("invalid");
|
||||
passwordError.textContent = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordField.value !== confirmPasswordField.value) {
|
||||
confirmPasswordField.classList.add("invalid");
|
||||
passwordError.textContent = "Passwords do not match.";
|
||||
} else {
|
||||
confirmPasswordField.classList.remove("invalid");
|
||||
passwordError.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
passwordField.addEventListener("input", checkPasswords);
|
||||
confirmPasswordField.addEventListener("input", checkPasswords);
|
||||
|
||||
document.getElementById("createUserForm").addEventListener("submit", function(e) {
|
||||
form.addEventListener("submit", async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const password = passwordField.value;
|
||||
const confirmPassword = confirmPasswordField.value;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
e.preventDefault();
|
||||
confirmPasswordField.classList.add("invalid");
|
||||
passwordError.textContent = "Passwords do not match.";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -86,23 +91,39 @@
|
||||
username: document.getElementById("username").value,
|
||||
email: document.getElementById("email").value,
|
||||
hashedpassword: password,
|
||||
role: "USER"
|
||||
role: "ROLE_USER"
|
||||
};
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="_csrf"]').getAttribute('content');
|
||||
const csrfHeader = document.querySelector('meta[name="_csrf_header"]').getAttribute('content');
|
||||
const csrfHeader = document.querySelector('meta[name="_csrf_header"]').getAttribute('content');
|
||||
|
||||
console.log("JSON to submit:", JSON.stringify(userData, null, 2));
|
||||
try {
|
||||
const response = await fetch("/api/users", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[csrfHeader]: csrfToken,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(userData)
|
||||
});
|
||||
|
||||
fetch("http://localhost:8080/api/users", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[csrfHeader]: csrfToken,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(userData)
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
if (response.ok) {
|
||||
passwordError.style.color = "green";
|
||||
passwordError.textContent = "Account created successfully. Redirecting to login...";
|
||||
setTimeout(function () {
|
||||
window.location.href = "/login";
|
||||
}, 1500);
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
passwordError.style.color = "red";
|
||||
passwordError.textContent = "Account creation failed. Please try a different username or email.";
|
||||
console.error("Create account failed:", errorText);
|
||||
}
|
||||
} catch (error) {
|
||||
passwordError.style.color = "red";
|
||||
passwordError.textContent = "Could not connect to the server.";
|
||||
console.error("Request error:", error);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,235 +1,195 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Thyme Crunch Home</title>
|
||||
<link rel="stylesheet" th:href="@{css/explore.css}">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Delius+Swash+Caps&family=Mali:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
|
||||
<title>Explore Recipes</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Explore Recipes</h1>
|
||||
|
||||
<header class="top-header">
|
||||
<img th:src="@{images/header_left.png}" alt="Violin f-hole shape to the left of header." class="swirl">
|
||||
<h1 class="site-name">Thyme Crunch</h1>
|
||||
<img th:src="@{images/header_right.png}" alt="Violin f-hole shape to the right of header." class="swirl">
|
||||
</header>
|
||||
<nav>
|
||||
<a th:href="@{/}">Home</a> |
|
||||
<a th:href="@{/explore}">Explore</a> |
|
||||
<a th:href="@{/my-profile}">Profile</a>
|
||||
</nav>
|
||||
|
||||
<div class="body">
|
||||
<!--Navigation Bar -->
|
||||
<div class="body-left">
|
||||
<nav class="sidebar-left">
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="#">Explore</a></li>
|
||||
<li><a href="#">Profile</a></li>
|
||||
<li><a href="#">Saved</a></li>
|
||||
<li>
|
||||
<form th:action="@{/logout}" method="post">
|
||||
<input type="image" th:src="@{images/logout_icon.png}" alt="Logout button" class="nav_icon"/>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<a th:href="@{/create}" target="_blank" class="create_icon">
|
||||
<img th:src="@{images/create_icon.png}" alt="Create New Recipe Icon (Red mixing bowl with a spoon and yellow addition symbol.">
|
||||
</a>
|
||||
<form id="search-form" th:action="@{/explore}" method="get">
|
||||
<input type="text" id="site-search" name="q" th:value="${q}" placeholder="Search recipes">
|
||||
<button type="submit">Search</button>
|
||||
|
||||
<h3>Prep Time</h3>
|
||||
<label>
|
||||
<input type="checkbox" name="prepTime" value="15"> <15 min
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="prepTime" value="30"> 15~30 min
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="prepTime" value="60"> 30~60 min
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="prepTime" value="240"> 1~4 hours
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="prepTime" value="241"> 4+ hours
|
||||
</label>
|
||||
|
||||
<h3>Cook Time</h3>
|
||||
<label>
|
||||
<input type="checkbox" name="cookTime" value="15"> <15 min
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="cookTime" value="30"> 15~30 min
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="cookTime" value="60"> 30~60 min
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="cookTime" value="120"> 1~2 hours
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="cookTime" value="121"> 2+ hours
|
||||
</label>
|
||||
|
||||
<h3>Price</h3>
|
||||
<label>
|
||||
<input type="checkbox" name="price" value="1"> $
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="price" value="2"> $$
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="price" value="3"> $$$
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="price" value="4"> $$$$
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<div th:if="${#lists.isEmpty(recipes)}">
|
||||
<p>No recipes found.</p>
|
||||
</div>
|
||||
|
||||
<div th:unless="${#lists.isEmpty(recipes)}">
|
||||
<div th:each="recipe : ${recipes}" style="margin-bottom: 20px; border: 1px solid #ccc; padding: 10px;">
|
||||
<h3>
|
||||
<a th:href="@{/recipes/{id}(id=${recipe.id})}" th:text="${recipe.title}">Recipe Title</a>
|
||||
</h3>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<div class="search-bar">
|
||||
<form action="/explore" method="get" id="search-form">
|
||||
<label for="site-search">Search:</label>
|
||||
<div class="search-btn">
|
||||
<div id="tag-input-wrapper">
|
||||
<!-- <div id="chips-container"></div> -->
|
||||
<input type="text" id="site-search" name="q" th:value="${q}" placeholder="Search for recipes...">
|
||||
</div>
|
||||
<button type="submit"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="recipe-card">
|
||||
<p>
|
||||
<strong>Author:</strong>
|
||||
<a th:href="@{/users/{id}(id=${recipe.userDto.id})}"
|
||||
th:text="${recipe.userDto.displayName != null and !#strings.isEmpty(recipe.userDto.displayName) ? recipe.userDto.displayName : recipe.userDto.username}">
|
||||
Author Name
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<a th:href="@{/recipes/{id}(id=${recipe.id})}" class="card" th:each="recipe : ${recipes}">
|
||||
<div class="card-text">
|
||||
<h2 th:text="${recipe.title}"></h2>
|
||||
<p th:text="${recipe.description}"></p>
|
||||
</div>
|
||||
<div class="card-right">
|
||||
<div th:each="img : ${recipe.images}">
|
||||
<img th:src="${img.imageUrl}" alt="Recipe Image"/>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<p th:text="${recipe.description}">Recipe description</p>
|
||||
|
||||
<!--Filter -->
|
||||
<div class="body-right">
|
||||
<div class="sidebar-right">
|
||||
<h1> FILTER </h1>
|
||||
<form id="filter-form">
|
||||
<h3>Prep Time</h3>
|
||||
<p>
|
||||
<strong>Prep:</strong> <span th:text="${recipe.prepTimeMinutes}">0</span> min |
|
||||
<strong>Cook:</strong> <span th:text="${recipe.cookTimeMinutes}">0</span> min |
|
||||
<strong>Servings:</strong> <span th:text="${recipe.servings}">0</span> |
|
||||
<strong>Cost:</strong> <span th:text="${recipe.cost}">0</span>
|
||||
</p>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="prepTime" value="15"><15 min
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="prepTime" value="30">15~30 min
|
||||
</label><br>
|
||||
<div th:if="${recipe.images != null and !#lists.isEmpty(recipe.images)}">
|
||||
<img th:src="${recipe.images[0].imageUrl}" alt="Recipe Image" style="max-width: 200px;">
|
||||
</div>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="prepTime" value="60">30~60 min
|
||||
</label><br>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="prepTime" value="240">1~4 hours
|
||||
</label><br>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="prepTime" value="241">4+ hours
|
||||
</label>
|
||||
<h3>Cook Time</h3>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="cookTime" value="15"><15 min
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="cookTime" value="30">15~30 min
|
||||
</label><br>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="cookTime" value="60">30~60 min
|
||||
</label><br>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="cookTime" value="120">1~2 hours
|
||||
</label><br>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="cookTime" value="121">2+ hours
|
||||
</label>
|
||||
<h3>Price</h3>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="price" value="1"> $
|
||||
</label><br>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="price" value="2"> $$
|
||||
</label><br>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="price" value="3"> $$$
|
||||
</label><br>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="price" value="4"> $$$$
|
||||
</label>
|
||||
</form>
|
||||
<p><a th:href="@{/recipes/{id}(id=${recipe.id})}">View Recipe</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const input = document.getElementById('site-search');
|
||||
const tags = [];
|
||||
|
||||
// Restore price checkboxes from URL
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.getAll('prices').forEach(p => {
|
||||
const cb = document.querySelector(`input[name="price"][value="${p}"]`);
|
||||
if (cb) cb.checked = true;
|
||||
});
|
||||
<script>
|
||||
(function () {
|
||||
const input = document.getElementById('site-search');
|
||||
const tags = [];
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
// Restore cookTime checkboxes from URL
|
||||
params.getAll('cookTime').forEach(p => {
|
||||
const cb = document.querySelector(`input[name="cookTime"][value="${p}"]`);
|
||||
if (cb) cb.checked = true;
|
||||
});
|
||||
params.getAll('prices').forEach(p => {
|
||||
const cb = document.querySelector(`input[name="price"][value="${p}"]`);
|
||||
if (cb) cb.checked = true;
|
||||
});
|
||||
|
||||
// Restore prepTime checkboxes from URL
|
||||
params.getAll('prepTime').forEach(p => {
|
||||
const cb = document.querySelector(`input[name="prepTime"][value="${p}"]`);
|
||||
if (cb) cb.checked = true;
|
||||
});
|
||||
params.getAll('cookTime').forEach(p => {
|
||||
const cb = document.querySelector(`input[name="cookTime"][value="${p}"]`);
|
||||
if (cb) cb.checked = true;
|
||||
});
|
||||
|
||||
// Restore tag chips from URL
|
||||
params.getAll('tags').forEach(addChip);
|
||||
params.getAll('prepTime').forEach(p => {
|
||||
const cb = document.querySelector(`input[name="prepTime"][value="${p}"]`);
|
||||
if (cb) cb.checked = true;
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if ((e.key === ' ' || e.key === 'Enter') && input.value.includes('#')) {
|
||||
const val = input.value + ' ';
|
||||
const match = val.match(/(^|\s)(#\w+)(\s)/); // NASTY regex ):
|
||||
if (match) {
|
||||
e.preventDefault();
|
||||
addChip(match[2].substring(1));
|
||||
input.value = val.replace(match[0], '');
|
||||
input.value += ' ';
|
||||
params.getAll('tags').forEach(addChip);
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if ((e.key === ' ' || e.key === 'Enter') && input.value.includes('#')) {
|
||||
const val = input.value + ' ';
|
||||
const match = val.match(/(^|\s)(#\w+)(\s)/);
|
||||
if (match) {
|
||||
e.preventDefault();
|
||||
addChip(match[2].substring(1));
|
||||
input.value = val.replace(match[0], '');
|
||||
input.value += ' ';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('input[name="price"]').forEach(cb => {
|
||||
cb.addEventListener('change', submitSearch);
|
||||
});
|
||||
document.querySelectorAll('input[name="price"]').forEach(cb => {
|
||||
cb.addEventListener('change', submitSearch);
|
||||
});
|
||||
|
||||
document.querySelectorAll('input[name="cookTime"]').forEach(cb => {
|
||||
cb.addEventListener('change', submitSearch);
|
||||
});
|
||||
document.querySelectorAll('input[name="cookTime"]').forEach(cb => {
|
||||
cb.addEventListener('change', submitSearch);
|
||||
});
|
||||
|
||||
document.querySelectorAll('input[name="prepTime"]').forEach(cb => {
|
||||
cb.addEventListener('change', submitSearch);
|
||||
});
|
||||
document.querySelectorAll('input[name="prepTime"]').forEach(cb => {
|
||||
cb.addEventListener('change', submitSearch);
|
||||
});
|
||||
|
||||
document.getElementById('search-form').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
document.getElementById('search-form').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
submitSearch();
|
||||
});
|
||||
|
||||
submitSearch();
|
||||
});
|
||||
function addChip(tag) {
|
||||
if (tags.includes(tag)) return;
|
||||
tags.push(tag);
|
||||
|
||||
function addChip(tag) {
|
||||
if (tags.includes(tag)) return;
|
||||
tags.push(tag);
|
||||
const chip = document.createElement('span');
|
||||
chip.className = 'tag-chip';
|
||||
chip.innerHTML = `#${tag} <button type="button" aria-label="Remove ${tag}">×</button>`;
|
||||
chip.querySelector('button').addEventListener('click', () => {
|
||||
chip.remove();
|
||||
tags.splice(tags.indexOf(tag), 1);
|
||||
});
|
||||
const lastChip = input.parentElement.querySelector('.tag-chip:last-of-type');
|
||||
if (lastChip) {
|
||||
lastChip.insertAdjacentElement('afterend', chip);
|
||||
} else {
|
||||
input.insertAdjacentElement('afterend', chip);
|
||||
}
|
||||
}
|
||||
const chip = document.createElement('span');
|
||||
chip.className = 'tag-chip';
|
||||
chip.innerHTML = `#${tag} <button type="button" aria-label="Remove ${tag}">×</button>`;
|
||||
|
||||
function submitSearch() {
|
||||
// Only use chips for tags, not strings!
|
||||
const cleanedQuery = input.value.replace(/#\w+/g, '').trim();
|
||||
chip.querySelector('button').addEventListener('click', () => {
|
||||
chip.remove();
|
||||
tags.splice(tags.indexOf(tag), 1);
|
||||
submitSearch();
|
||||
});
|
||||
|
||||
input.insertAdjacentElement('afterend', chip);
|
||||
}
|
||||
|
||||
const out = new URLSearchParams();
|
||||
if (cleanedQuery) out.set('q', cleanedQuery);
|
||||
tags.forEach(t => out.append('tags', t));
|
||||
document.querySelectorAll('input[name="price"]:checked')
|
||||
.forEach(cb => out.append('prices', cb.value));
|
||||
function submitSearch() {
|
||||
const cleanedQuery = input.value.replace(/#\w+/g, '').trim();
|
||||
const out = new URLSearchParams();
|
||||
|
||||
document.querySelectorAll('input[name="cookTime"]:checked')
|
||||
.forEach(cb => out.append('cookTime', cb.value));
|
||||
if (cleanedQuery) out.set('q', cleanedQuery);
|
||||
tags.forEach(t => out.append('tags', t));
|
||||
|
||||
document.querySelectorAll('input[name="prepTime"]:checked')
|
||||
.forEach(cb => out.append('prepTime', cb.value));
|
||||
document.querySelectorAll('input[name="price"]:checked')
|
||||
.forEach(cb => out.append('prices', cb.value));
|
||||
|
||||
window.location.href = '/explore?' + out.toString();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
document.querySelectorAll('input[name="cookTime"]:checked')
|
||||
.forEach(cb => out.append('cookTime', cb.value));
|
||||
|
||||
document.querySelectorAll('input[name="prepTime"]:checked')
|
||||
.forEach(cb => out.append('prepTime', cb.value));
|
||||
|
||||
window.location.href = '/explore?' + out.toString();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -21,7 +21,7 @@
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a th:href="@{/explore}">Explore</a></li>
|
||||
<li><a href="#">Profile</a></li>
|
||||
<li><a th:href="@{/my-profile}">Profile</a></li>
|
||||
<li><a href="#">Saved</a></li>
|
||||
<li>
|
||||
<form th:action="@{/logout}" method="post">
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>My Profile</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>My Profile</h1>
|
||||
|
||||
<p><strong>Username:</strong> <span th:text="${profile.username}">username</span></p>
|
||||
<p>
|
||||
<strong>Public Profile:</strong>
|
||||
<a th:href="@{/users/{id}(id=${profile.id})}">View my public profile</a>
|
||||
</p>
|
||||
|
||||
<h2>Edit Profile</h2>
|
||||
<form th:action="@{/my-profile/update}" method="post" th:object="${updateProfileDto}">
|
||||
<div>
|
||||
<label for="displayName">Screen Name</label><br>
|
||||
<input type="text" id="displayName" th:field="*{displayName}" maxlength="100">
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div>
|
||||
<label for="bio">Bio</label><br>
|
||||
<textarea id="bio" th:field="*{bio}" rows="6" cols="50" maxlength="1000"></textarea>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<button type="submit">Save Profile</button>
|
||||
</form>
|
||||
|
||||
<h2>My Recipes</h2>
|
||||
|
||||
<div th:if="${#lists.isEmpty(profile.recipes)}">
|
||||
<p>You have not created any recipes yet.</p>
|
||||
</div>
|
||||
|
||||
<div th:unless="${#lists.isEmpty(profile.recipes)}">
|
||||
<div th:each="recipe : ${profile.recipes}" style="margin-bottom: 20px; border: 1px solid #ccc; padding: 10px;">
|
||||
<h3 th:text="${recipe.title}">Recipe Title</h3>
|
||||
|
||||
<p>
|
||||
<strong>Author:</strong>
|
||||
<a th:href="@{/users/{id}(id=${recipe.userDto.id})}"
|
||||
th:text="${recipe.userDto.effectiveDisplayName}">
|
||||
Author Name
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p th:text="${recipe.description}">Recipe description</p>
|
||||
|
||||
<a th:href="@{/recipes/{id}(id=${recipe.id})}">View Recipe</a>
|
||||
<span> | </span>
|
||||
<a th:href="@{/recipes/{id}/edit(id=${recipe.id})}">Edit Recipe</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,195 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Explore Recipes</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Explore Recipes</h1>
|
||||
|
||||
<nav>
|
||||
<a th:href="@{/}">Home</a> |
|
||||
<a th:href="@{/explore}">Explore</a> |
|
||||
<a th:href="@{/my-profile}">Profile</a>
|
||||
</nav>
|
||||
|
||||
<form id="search-form" th:action="@{/explore}" method="get">
|
||||
<input type="text" id="site-search" name="q" th:value="${q}" placeholder="Search recipes">
|
||||
<button type="submit">Search</button>
|
||||
|
||||
<h3>Prep Time</h3>
|
||||
<label>
|
||||
<input type="checkbox" name="prepTime" value="15"> <15 min
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="prepTime" value="30"> 15~30 min
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="prepTime" value="60"> 30~60 min
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="prepTime" value="240"> 1~4 hours
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="prepTime" value="241"> 4+ hours
|
||||
</label>
|
||||
|
||||
<h3>Cook Time</h3>
|
||||
<label>
|
||||
<input type="checkbox" name="cookTime" value="15"> <15 min
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="cookTime" value="30"> 15~30 min
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="cookTime" value="60"> 30~60 min
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="cookTime" value="120"> 1~2 hours
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="cookTime" value="121"> 2+ hours
|
||||
</label>
|
||||
|
||||
<h3>Price</h3>
|
||||
<label>
|
||||
<input type="checkbox" name="price" value="1"> $
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="price" value="2"> $$
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="price" value="3"> $$$
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="price" value="4"> $$$$
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<div th:if="${#lists.isEmpty(recipes)}">
|
||||
<p>No recipes found.</p>
|
||||
</div>
|
||||
|
||||
<div th:unless="${#lists.isEmpty(recipes)}">
|
||||
<div th:each="recipe : ${recipes}" style="margin-bottom: 20px; border: 1px solid #ccc; padding: 10px;">
|
||||
<h3>
|
||||
<a th:href="@{/recipes/{id}(id=${recipe.id})}" th:text="${recipe.title}">Recipe Title</a>
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
<strong>Author:</strong>
|
||||
<a th:href="@{/users/{id}(id=${recipe.userDto.id})}"
|
||||
th:text="${recipe.userDto.displayName != null and !#strings.isEmpty(recipe.userDto.displayName) ? recipe.userDto.displayName : recipe.userDto.username}">
|
||||
Author Name
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p th:text="${recipe.description}">Recipe description</p>
|
||||
|
||||
<p>
|
||||
<strong>Prep:</strong> <span th:text="${recipe.prepTimeMinutes}">0</span> min |
|
||||
<strong>Cook:</strong> <span th:text="${recipe.cookTimeMinutes}">0</span> min |
|
||||
<strong>Servings:</strong> <span th:text="${recipe.servings}">0</span> |
|
||||
<strong>Cost:</strong> <span th:text="${recipe.cost}">0</span>
|
||||
</p>
|
||||
|
||||
<div th:if="${recipe.images != null and !#lists.isEmpty(recipe.images)}">
|
||||
<img th:src="${recipe.images[0].imageUrl}" alt="Recipe Image" style="max-width: 200px;">
|
||||
</div>
|
||||
|
||||
<p><a th:href="@{/recipes/{id}(id=${recipe.id})}">View Recipe</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const input = document.getElementById('site-search');
|
||||
const tags = [];
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
params.getAll('prices').forEach(p => {
|
||||
const cb = document.querySelector(`input[name="price"][value="${p}"]`);
|
||||
if (cb) cb.checked = true;
|
||||
});
|
||||
|
||||
params.getAll('cookTime').forEach(p => {
|
||||
const cb = document.querySelector(`input[name="cookTime"][value="${p}"]`);
|
||||
if (cb) cb.checked = true;
|
||||
});
|
||||
|
||||
params.getAll('prepTime').forEach(p => {
|
||||
const cb = document.querySelector(`input[name="prepTime"][value="${p}"]`);
|
||||
if (cb) cb.checked = true;
|
||||
});
|
||||
|
||||
params.getAll('tags').forEach(addChip);
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if ((e.key === ' ' || e.key === 'Enter') && input.value.includes('#')) {
|
||||
const val = input.value + ' ';
|
||||
const match = val.match(/(^|\s)(#\w+)(\s)/);
|
||||
if (match) {
|
||||
e.preventDefault();
|
||||
addChip(match[2].substring(1));
|
||||
input.value = val.replace(match[0], '');
|
||||
input.value += ' ';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('input[name="price"]').forEach(cb => {
|
||||
cb.addEventListener('change', submitSearch);
|
||||
});
|
||||
|
||||
document.querySelectorAll('input[name="cookTime"]').forEach(cb => {
|
||||
cb.addEventListener('change', submitSearch);
|
||||
});
|
||||
|
||||
document.querySelectorAll('input[name="prepTime"]').forEach(cb => {
|
||||
cb.addEventListener('change', submitSearch);
|
||||
});
|
||||
|
||||
document.getElementById('search-form').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
submitSearch();
|
||||
});
|
||||
|
||||
function addChip(tag) {
|
||||
if (tags.includes(tag)) return;
|
||||
tags.push(tag);
|
||||
|
||||
const chip = document.createElement('span');
|
||||
chip.className = 'tag-chip';
|
||||
chip.innerHTML = `#${tag} <button type="button" aria-label="Remove ${tag}">×</button>`;
|
||||
|
||||
chip.querySelector('button').addEventListener('click', () => {
|
||||
chip.remove();
|
||||
tags.splice(tags.indexOf(tag), 1);
|
||||
submitSearch();
|
||||
});
|
||||
|
||||
input.insertAdjacentElement('afterend', chip);
|
||||
}
|
||||
|
||||
function submitSearch() {
|
||||
const cleanedQuery = input.value.replace(/#\w+/g, '').trim();
|
||||
const out = new URLSearchParams();
|
||||
|
||||
if (cleanedQuery) out.set('q', cleanedQuery);
|
||||
tags.forEach(t => out.append('tags', t));
|
||||
|
||||
document.querySelectorAll('input[name="price"]:checked')
|
||||
.forEach(cb => out.append('prices', cb.value));
|
||||
|
||||
document.querySelectorAll('input[name="cookTime"]:checked')
|
||||
.forEach(cb => out.append('cookTime', cb.value));
|
||||
|
||||
document.querySelectorAll('input[name="prepTime"]:checked')
|
||||
.forEach(cb => out.append('prepTime', cb.value));
|
||||
|
||||
window.location.href = '/explore?' + out.toString();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user