Remove saved recipe functionality

This commit is contained in:
Madeleine Stamp
2026-04-29 09:44:24 -06:00
parent 57e3115634
commit 55b61d761a
4 changed files with 98 additions and 60 deletions
@@ -71,8 +71,16 @@ public class SiteController {
@GetMapping("/recipes/{id}") @GetMapping("/recipes/{id}")
public String viewRecipe(@PathVariable Integer id, Model model, Principal principal) { public String viewRecipe(@PathVariable Integer id, Model model, Principal principal) {
RecipeDto recipe = recipeService.getRecipeById(id); RecipeDto recipe = recipeService.getRecipeById(id);
boolean saved = false;
if (principal != null) {
saved = userService.isFavorite(principal.getName(), id);
}
model.addAttribute("recipe", recipe); model.addAttribute("recipe", recipe);
model.addAttribute("guest", principal == null); model.addAttribute("guest", principal == null);
model.addAttribute("saved", saved);
return "view-recipe"; return "view-recipe";
} }
@@ -17,6 +17,9 @@ import com.example.demo.repository.RecipeRepo;
import com.example.demo.repository.UserRepo; import com.example.demo.repository.UserRepo;
import com.example.demo.service.RecipeService; import com.example.demo.service.RecipeService;
import com.example.demo.service.UserService; import com.example.demo.service.UserService;
import com.example.demo.entity.Favorite;
import com.example.demo.entity.FavoriteId;
import com.example.demo.repository.FavoriteRepo;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
@@ -27,14 +30,16 @@ public class UserServiceImpl implements UserService {
private RecipeRepo recipeRepository; private RecipeRepo recipeRepository;
private PasswordEncoder passwordEncoder; private PasswordEncoder passwordEncoder;
private RecipeService recipeService; private RecipeService recipeService;
private FavoriteRepo favoriteRepository;
public UserServiceImpl(UserRepo userRepository, RecipeRepo recipeRepository, public UserServiceImpl(UserRepo userRepository, RecipeRepo recipeRepository,
PasswordEncoder passwordEncoder, RecipeService recipeService) { PasswordEncoder passwordEncoder, RecipeService recipeService, FavoriteRepo favoriteRepository) {
super(); super();
this.userRepository = userRepository; this.userRepository = userRepository;
this.recipeRepository = recipeRepository; this.recipeRepository = recipeRepository;
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
this.recipeService = recipeService; this.recipeService = recipeService;
this.favoriteRepository = favoriteRepository;
} }
@Override @Override
@@ -78,13 +83,16 @@ public class UserServiceImpl implements UserService {
@Transactional @Transactional
public UserDto saveFavorite(Integer userId, Integer recipeId) { public UserDto saveFavorite(Integer userId, Integer recipeId) {
User existingUser = userRepository.findById(userId) User existingUser = userRepository.findById(userId)
.orElseThrow(() -> new NotFoundException("User", "id", userId)); .orElseThrow(() -> new NotFoundException("User", "id", userId));
Recipe existingRecipe = recipeRepository.findById(recipeId) recipeRepository.findById(recipeId)
.orElseThrow(() -> new NotFoundException("Recipe", "id", recipeId)); .orElseThrow(() -> new NotFoundException("Recipe", "id", recipeId));
existingUser.getFavRecipes().add(existingRecipe); FavoriteId favoriteId = new FavoriteId(userId, recipeId);
userRepository.save(existingUser);
if (!favoriteRepository.existsById(favoriteId)) {
favoriteRepository.save(new Favorite(userId, recipeId));
}
return convertToDto(existingUser); return convertToDto(existingUser);
} }
@@ -112,14 +120,17 @@ public class UserServiceImpl implements UserService {
@Override @Override
@Transactional @Transactional
public void deleteFavorite(Integer userId, Integer recipeId) { public void deleteFavorite(Integer userId, Integer recipeId) {
User existingUser = userRepository.findById(userId) userRepository.findById(userId)
.orElseThrow(() -> new NotFoundException("User", "id", userId)); .orElseThrow(() -> new NotFoundException("User", "id", userId));
Recipe existingRecipe = recipeRepository.findById(recipeId) recipeRepository.findById(recipeId)
.orElseThrow(() -> new NotFoundException("Recipe", "id", recipeId)); .orElseThrow(() -> new NotFoundException("Recipe", "id", recipeId));
existingUser.getFavRecipes().remove(existingRecipe); FavoriteId favoriteId = new FavoriteId(userId, recipeId);
userRepository.save(existingUser);
if (favoriteRepository.existsById(favoriteId)) {
favoriteRepository.deleteById(favoriteId);
}
} }
@Override @Override
@@ -235,4 +246,13 @@ public class UserServiceImpl implements UserService {
return favoriteDtos; return favoriteDtos;
} }
@Override
@Transactional
public boolean isFavorite(String username, Integer recipeId) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new NotFoundException("User", "username", username));
return favoriteRepository.existsById(new FavoriteId(user.getId(), recipeId));
}
} }
@@ -42,4 +42,6 @@ public interface UserService {
ProfileDto updateProfile(String username, UpdateProfileDto dto); ProfileDto updateProfile(String username, UpdateProfileDto dto);
List<RecipeDto> getFavoriteRecipesByUsername(String username); List<RecipeDto> getFavoriteRecipesByUsername(String username);
boolean isFavorite(String username, Integer recipeId);
} }
+51 -43
View File
@@ -119,7 +119,13 @@
</div> </div>
</section> </section>
<button type="button" class="save-btn" th:data-recipe-id="${recipe.id}">Save Recipe</button> <button type="button"
class="save-btn"
th:data-recipe-id="${recipe.id}"
th:data-saved="${saved}"
th:text="${saved} ? 'Remove Saved Recipe' : 'Save Recipe'">
Save Recipe
</button>
</div> </div>
<div th:unless="${recipe != null}" class="recipe-not-found"> <div th:unless="${recipe != null}" class="recipe-not-found">
@@ -131,55 +137,57 @@
</div> </div>
<script> <script>
async function getCurrentUser() { async function getCurrentUser() {
const response = await fetch('/api/users/me', { const response = await fetch('/api/users/me', {
credentials: 'same-origin' credentials: 'same-origin'
}); });
if (!response.ok) { if (!response.ok) {
return null; return null;
}
const user = await response.json();
return user || null;
}
async function saveRecipe(recipeId, button) {
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.content;
const csrfToken = document.querySelector('meta[name="_csrf"]')?.content;
const user = await getCurrentUser();
if (!user || !user.id) {
window.location.href = '/login';
return;
}
const response = await fetch(`/api/users/${user.id}/favorites/${recipeId}`, {
method: 'POST',
credentials: 'same-origin',
headers: {
...(csrfHeader && csrfToken ? { [csrfHeader]: csrfToken } : {})
} }
});
if (!response.ok) { const user = await response.json();
const text = await response.text(); return user || null;
alert(text || 'Could not save recipe.');
return;
} }
button.textContent = 'Saved'; async function toggleSaveRecipe(recipeId, button) {
button.disabled = true; const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.content;
} const csrfToken = document.querySelector('meta[name="_csrf"]')?.content;
document.querySelectorAll('.save-btn').forEach(button => { const user = await getCurrentUser();
button.addEventListener('click', async () => {
const recipeId = button.dataset.recipeId; if (!user || !user.id) {
await saveRecipe(recipeId, button); window.location.href = '/login';
return;
}
const isSaved = button.dataset.saved === 'true';
const response = await fetch(`/api/users/${user.id}/favorites/${recipeId}`, {
method: isSaved ? 'DELETE' : 'POST',
credentials: 'same-origin',
headers: {
...(csrfHeader && csrfToken ? { [csrfHeader]: csrfToken } : {})
}
});
if (!response.ok) {
const text = await response.text();
alert(text || 'Could not update saved recipe.');
return;
}
button.dataset.saved = isSaved ? 'false' : 'true';
button.textContent = isSaved ? 'Save Recipe' : 'Un-save Recipe';
}
document.querySelectorAll('.save-btn').forEach(button => {
button.addEventListener('click', async () => {
const recipeId = button.dataset.recipeId;
await toggleSaveRecipe(recipeId, button);
});
}); });
}); </script>
</script>
</body> </body>
</html> </html>