(DB/FEAT): Implemented toggle favorite in the backend.
The frontend is half wired up, just need to update the button. I also want to update the recipe methods to return the favorite status. This will follow very similar to the way I updated the tags. Another method which can be called to attach the favorite state.
This commit is contained in:
parent
d29426290d
commit
bebeb25492
@ -299,11 +299,11 @@ found in **OTHER** section.
|
||||
- [x] UserId (FK: User.Id) Serial, optional for not logged in users
|
||||
- [x] Created (Required) date/time stamp
|
||||
|
||||
- [ ] Likes: **Many-to-many** table to represent a list of recipes liked by a user.
|
||||
- [ ] ID (PK) *Composite key***
|
||||
- [ ] UserId (FK: User.Id, Required) Serial
|
||||
- [ ] RecipeId (FK: Recipe.Id, Required) Serial
|
||||
- [ ] Created (Required) date/time stamp
|
||||
- [x] Favorites: **Many-to-many** table to represent a list of recipes favorites by a user.
|
||||
- [x] ID (PK) *Composite key***
|
||||
- [x] UserId (FK: User.Id, Required) Serial
|
||||
- [x] RecipeId (FK: Recipe.Id, Required) Serial
|
||||
- [x] Created (Required) date/time stamp
|
||||
|
||||
- [x] Tags: Represents a single tag that can be had by many recipes.
|
||||
- [x] ID (PK) Serial
|
||||
|
||||
@ -33,7 +33,7 @@ func EngagementViewRecipe(ctx *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func EngagementLikeRecipe(ctx *gin.Context) {
|
||||
func EngagementFavoriteRecipe(ctx *gin.Context) {
|
||||
deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||
|
||||
if !domain.IsLoggedIn(ctx) {
|
||||
@ -46,7 +46,7 @@ func EngagementLikeRecipe(ctx *gin.Context) {
|
||||
recipeId, _ := strconv.Atoi(id)
|
||||
userId := ctx.MustGet("userId").(int)
|
||||
|
||||
if _, err := deps.EngagementService.UserLikeRecipe(userId, recipeId); err != nil {
|
||||
if _, err := deps.EngagementService.UserFavoriteRecipe(userId, recipeId); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": http.StatusInternalServerError,
|
||||
"message": err.Error(),
|
||||
|
||||
@ -188,7 +188,7 @@ func (s *Server) Setup() *Server {
|
||||
|
||||
// Engagement endpoints
|
||||
router_api.POST("/engagement/view/:id", handlers.EngagementViewRecipe)
|
||||
router_api.POST("/engagement/like/:id", handlers.EngagementLikeRecipe)
|
||||
router_api.POST("/engagement/favorite/:id", handlers.EngagementFavoriteRecipe)
|
||||
router_api.POST("/engagement/make/:id", handlers.EngagementMakeRecipe)
|
||||
|
||||
// Catch un-routed URLS
|
||||
|
||||
@ -39,22 +39,35 @@ func (s *EngagementService) UserViewRecipe(userId, recipeId int) (domain.Engagem
|
||||
return s.engagementRepository.AddUserEntityEngagement(userId, recipeId, message, domain.EngagementViewed)
|
||||
}
|
||||
|
||||
// UserLikeRecipe requires a user ID and a recipe ID to create an engagement record in the database.
|
||||
// UserFavoriteRecipe requires a user ID and a recipe ID to create an engagement record in the database.
|
||||
// A message will be generated using the recipe data and then used to add a like engagement to the
|
||||
// database.
|
||||
func (s *EngagementService) UserLikeRecipe(userId, recipeId int) (domain.Engagement, error) {
|
||||
func (s *EngagementService) UserFavoriteRecipe(userId, recipeId int) (domain.Engagement, error) {
|
||||
recipe, err := s.recipeRepository.GetRecipe(recipeId)
|
||||
if err != nil {
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("Liked \"%s\"", recipe.Title)
|
||||
// Update the favorites DB
|
||||
liked, err := s.engagementRepository.UserFavoriteRecipeToggle(userId, recipeId)
|
||||
if err != nil {
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
// Determine if this like is a saving or unsaving
|
||||
var message string
|
||||
if liked {
|
||||
message = fmt.Sprintf("Saved \"%s\"", recipe.Title)
|
||||
} else {
|
||||
message = fmt.Sprintf("Unsaved \"%s\"", recipe.Title)
|
||||
}
|
||||
|
||||
return s.engagementRepository.AddUserEntityEngagement(userId, recipeId, message, domain.EngagementLiked)
|
||||
|
||||
}
|
||||
|
||||
// UserLikeRecipe requires a user ID and a recipe ID to create an engagement record in the database.
|
||||
// A message will be generated using the recipe data and then used to add a like engagement to the
|
||||
// UserMakeRecipe requires a user ID and a recipe ID to create an engagement record in the database.
|
||||
// A message will be generated using the recipe data and then used to add a make engagement to the
|
||||
// database.
|
||||
func (s *EngagementService) UserMakeRecipe(userId, recipeId int) (domain.Engagement, error) {
|
||||
recipe, err := s.recipeRepository.GetRecipe(recipeId)
|
||||
|
||||
@ -4,4 +4,5 @@ type EngagementRepository interface {
|
||||
AddUserEngagement(userId int, message string, engagementType EngagementType) (Engagement, error)
|
||||
AddUserEntityEngagement(userId, entityId int, message string, engagementType EngagementType) (Engagement, error)
|
||||
GetUserEngagement(userId, limit int) ([]Engagement, error)
|
||||
UserFavoriteRecipeToggle(userId, recipeId int) (bool, error)
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ package domain
|
||||
|
||||
type EngagementService interface {
|
||||
UserViewRecipe(userId, recipeId int) (Engagement, error)
|
||||
UserLikeRecipe(userId, recipeId int) (Engagement, error)
|
||||
UserFavoriteRecipe(userId, recipeId int) (Engagement, error)
|
||||
UserMakeRecipe(userId, recipeId int) (Engagement, error)
|
||||
GetUserEngagement(userId, limit int) ([]Engagement, error)
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ const API_CREATE_RECIPE = VERSION + API + "/recipe"
|
||||
const API_SEARCH_RECIPES = VERSION + API + "/recipe/search"
|
||||
|
||||
const API_ENGAGEMENT_VIEW = VERSION + API + "/engagement/view/%d"
|
||||
const API_ENGAGEMENT_LIKE = VERSION + API + "/engagement/like/%d"
|
||||
const API_ENGAGEMENT_FAVORITE = VERSION + API + "/engagement/favorite/%d"
|
||||
const API_ENGAGEMENT_MAKE = VERSION + API + "/engagement/make/%d"
|
||||
|
||||
// State prefixed routes
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
-- Author: Hayden Hargreaves (hhargreaves2006@gmail.com)
|
||||
-- Desc: Create the favorites table.
|
||||
-- Date: 07/14/2025
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS Favorites (
|
||||
Id SERIAL PRIMARY KEY NOT NULL,
|
||||
UserId INTEGER NOT NULL REFERENCES users(id),
|
||||
RecipeId INTEGER NOT NULL REFERENCES recipes(id),
|
||||
Created TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@ -153,3 +154,62 @@ func (r *EngagementRepository) GetUserEngagement(userId, limit int) ([]domain.En
|
||||
|
||||
return engagements, err
|
||||
}
|
||||
|
||||
// UserFavoriteRecipeToggle toggles the status of a users favorite of a recipe. If the user has already
|
||||
// favorited the provided recipe, the database entry will be delete, hence removing the favorite. Otherwise,
|
||||
// an entry will be created. The NEW status of the users favorite will be returned as the boolean. Any
|
||||
// errors will be bubbled to the caller.
|
||||
func (r *EngagementRepository) UserFavoriteRecipeToggle(userId, recipeId int) (bool, error) {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT id
|
||||
FROM favorites
|
||||
WHERE userid = $1 AND recipeid = $2
|
||||
`
|
||||
|
||||
var id int
|
||||
|
||||
err = tx.QueryRow(query, userId, recipeId).Scan(&id)
|
||||
if err != nil {
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
tx.Rollback()
|
||||
return false, fmt.Errorf("Failed to get recipe favorite. %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Means we should create
|
||||
var success bool
|
||||
if id == 0 {
|
||||
createQuery := "INSERT INTO favorites (userid, recipeid, created) VALUES ($1, $2, $3);"
|
||||
|
||||
if result, err := tx.Exec(createQuery, userId, recipeId, time.Now()); err != nil {
|
||||
tx.Rollback()
|
||||
return false, fmt.Errorf("Failed to create recipe favorite. %s", err.Error())
|
||||
} else {
|
||||
rows, _ := result.RowsAffected()
|
||||
success = rows == 1
|
||||
}
|
||||
} else {
|
||||
deleteQuery := "DELETE FROM favorites WHERE id = $1;"
|
||||
|
||||
if result, err := tx.Exec(deleteQuery, id); err != nil {
|
||||
tx.Rollback()
|
||||
return false, fmt.Errorf("Failed to remove recipe favorite. %s", err.Error())
|
||||
} else {
|
||||
rows, _ := result.RowsAffected()
|
||||
success = rows == 1
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
return success, nil
|
||||
}
|
||||
|
||||
@ -228,7 +228,7 @@ templ madeButton(id int) {
|
||||
hx-trigger="click"
|
||||
hx-swap="none"
|
||||
id="make-button"
|
||||
hx-on:click="makeButtonHandler();"
|
||||
hx-on:click="makeButtonHandler();"
|
||||
class="flex items-center justify-center gap-x-1 rounded-lg border border-gray-300 text-gray-800 px-6 py-3 flex-grow hover:bg-gray-50 hover:border-blue-300 duration-300"
|
||||
>
|
||||
<svg
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.865
|
||||
package templates
|
||||
@ -584,7 +584,7 @@ func favoriteButton(favorited bool, id int) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(domainServer.API_ENGAGEMENT_LIKE, id))
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(domainServer.API_ENGAGEMENT_FAVORITE, id))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 189, Col: 62}
|
||||
}
|
||||
@ -602,7 +602,7 @@ func favoriteButton(favorited bool, id int) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(domainServer.API_ENGAGEMENT_LIKE, id))
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(domainServer.API_ENGAGEMENT_FAVORITE, id))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 204, Col: 62}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user