(DB/FEAT): Began the implementation of the user engagement! #18

Merged
azpect merged 10 commits from feature/engagement into master 2025-07-15 21:53:17 -07:00
4 changed files with 137 additions and 47 deletions
Showing only changes of commit 7e355d5eda - Show all commits

View File

@ -106,7 +106,8 @@ func RecipePage(ctx *gin.Context) {
// Get signed in user, if they exist // Get signed in user, if they exist
var userId *int = nil var userId *int = nil
if domainServer.IsLoggedIn(ctx) { var loggedIn = domainServer.IsLoggedIn(ctx)
if loggedIn {
storeId := ctx.MustGet("userId").(int) storeId := ctx.MustGet("userId").(int)
userId = &storeId userId = &storeId
} }
@ -139,7 +140,7 @@ func RecipePage(ctx *gin.Context) {
// I also do not really like that this runs on refresh, might need some better handling // I also do not really like that this runs on refresh, might need some better handling
title := "Potion - View Recipe" title := "Potion - View Recipe"
page := pages.RecipePage(*recipe, *user) page := pages.RecipePage(*recipe, *user, loggedIn)
ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
} }

View File

@ -123,6 +123,10 @@ func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) {
// GetRecipe will get a recipe via its ID. Any errors will be bubbled to the caller. Furthermore, // GetRecipe will get a recipe via its ID. Any errors will be bubbled to the caller. Furthermore,
// if the recipe is nil, an error will be returned, so the caller does not need to check for a nil // if the recipe is nil, an error will be returned, so the caller does not need to check for a nil
// recipe (e.g., if the error is nil the recipe exists) // recipe (e.g., if the error is nil the recipe exists)
//
// A userId should be provided to allow the favorite status to be updated. Without a userId (nil),
// the favorite status will return false, not because its not a favorite, but because it cannot find
// out!
func (s *RecipeService) GetRecipe(id int, userId *int) (*domain.Recipe, error) { func (s *RecipeService) GetRecipe(id int, userId *int) (*domain.Recipe, error) {
recipe, err := s.recipeRepository.GetRecipe(id, userId) recipe, err := s.recipeRepository.GetRecipe(id, userId)

View File

@ -151,7 +151,7 @@ templ tagList(tags []domain.Tag, created time.Time, modified *time.Time) {
templ ingredientListItem(name, quantity string) { templ ingredientListItem(name, quantity string) {
<li <li
class="text-sm md:text-base p-2 hover:bg-gray-100 transition-all duration-300 rounded-sm flex items-center justify-start odd:bg-[#f8f8f8]" class="p-2 hover:bg-gray-100 transition-all duration-300 rounded-sm flex items-center justify-start odd:bg-[#f8f8f8]"
> >
<span class="mr-4"> <span class="mr-4">
<svg class="h-4 text-gray-400" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg class="h-4 text-gray-400" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -173,7 +173,7 @@ templ instructionListItem(num int, content string) {
<div class="size-8 md:size-10 bg-blue-50 rounded-full flex items-center justify-center flex-shrink-0"> <div class="size-8 md:size-10 bg-blue-50 rounded-full flex items-center justify-center flex-shrink-0">
<h3 class="text-base md:text-xl text-blue-600 font-semibold">{ num }</h3> <h3 class="text-base md:text-xl text-blue-600 font-semibold">{ num }</h3>
</div> </div>
<p class="text-sm md:text-base">{ content }</p> <p class="text-base">{ content }</p>
</li> </li>
} }
@ -183,13 +183,17 @@ templ tagListItem(content string) {
</li> </li>
} }
templ favoriteButton(favorited bool, id int) { templ favoriteButton(favorited bool, id int, loggedIn bool) {
if favorited { if favorited {
<button <button
hx-post={ fmt.Sprintf(domainServer.API_ENGAGEMENT_FAVORITE, id) } hx-post={ fmt.Sprintf(domainServer.API_ENGAGEMENT_FAVORITE, id) }
hx-trigger="click" hx-trigger="click"
hx-swap="none" hx-swap="none"
if loggedIn {
hx-on:click="favoriteButtonHandler();"
}
class="flex items-center justify-center gap-x-1 rounded-lg border border-blue-300 bg-blue-50 text-gray-800 px-6 py-3 flex-grow hover:bg-blue-100 hover:border-blue-500 duration-300" class="flex items-center justify-center gap-x-1 rounded-lg border border-blue-300 bg-blue-50 text-gray-800 px-6 py-3 flex-grow hover:bg-blue-100 hover:border-blue-500 duration-300"
id="favorite-button"
> >
<svg class="h-6 text-red-500" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg class="h-6 text-red-500" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path
@ -204,7 +208,11 @@ templ favoriteButton(favorited bool, id int) {
hx-post={ fmt.Sprintf(domainServer.API_ENGAGEMENT_FAVORITE, id) } hx-post={ fmt.Sprintf(domainServer.API_ENGAGEMENT_FAVORITE, id) }
hx-trigger="click" hx-trigger="click"
hx-swap="none" hx-swap="none"
if loggedIn {
hx-on:click="favoriteButtonHandler();"
}
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" 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"
id="favorite-button"
> >
<svg class="h-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg class="h-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path
@ -222,13 +230,15 @@ templ favoriteButton(favorited bool, id int) {
} }
} }
templ madeButton(id int) { templ madeButton(id int, loggedIn bool) {
<button <button
hx-post={ fmt.Sprintf(domainServer.API_ENGAGEMENT_MAKE, id) } hx-post={ fmt.Sprintf(domainServer.API_ENGAGEMENT_MAKE, id) }
hx-trigger="click" hx-trigger="click"
hx-swap="none" hx-swap="none"
id="make-button" id="make-button"
hx-on:click="makeButtonHandler();" if loggedIn {
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" 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 <svg
@ -270,15 +280,15 @@ templ shareButton() {
</button> </button>
} }
templ buttonSection(favorited bool, id int) { templ buttonSection(favorited bool, id int, loggedIn bool) {
<section class="w-full flex flex-col md:flex-row gap-x-4 gap-y-2 py-8 px-4 md:px-8"> <section class="w-full flex flex-col md:flex-row gap-x-4 gap-y-2 py-8 px-4 md:px-8">
@favoriteButton(favorited, id) @favoriteButton(favorited, id, loggedIn)
@madeButton(id) @madeButton(id, loggedIn)
@shareButton() @shareButton()
</section> </section>
} }
templ RecipePage(recipe domain.Recipe, user domainUser.User) { templ RecipePage(recipe domain.Recipe, user domainUser.User, loggedIn bool) {
@components.Navbar("") @components.Navbar("")
<div class="w-full flex justify-center"> <div class="w-full flex justify-center">
<div class="mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 h-full border-l border-r border-gray-300 bg-white"> <div class="mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 h-full border-l border-r border-gray-300 bg-white">
@ -289,7 +299,7 @@ templ RecipePage(recipe domain.Recipe, user domainUser.User) {
<p class="text-sm mb-2 text-gray-700">Category: { recipe.Category }</p> <p class="text-sm mb-2 text-gray-700">Category: { recipe.Category }</p>
</div> </div>
@metadataSection(recipe) @metadataSection(recipe)
@buttonSection(recipe.Favorite, recipe.Id) @buttonSection(recipe.Favorite, recipe.Id, loggedIn)
<div class="px-4 py-8 md:px-8"> <div class="px-4 py-8 md:px-8">
<h3 class="text-xl text-gray-800 font-semibold mb-2">About this recipe</h3> <h3 class="text-xl text-gray-800 font-semibold mb-2">About this recipe</h3>
<p class="text-gray-700">{ recipe.Description }</p> <p class="text-gray-700">{ recipe.Description }</p>
@ -377,9 +387,54 @@ templ scripts(id int) {
</svg> </svg>
Made This! Made This!
</button> </button>
`; `;
} }
function favoriteButtonHandler() {
const button = document.getElementById("favorite-button");
console.log(button.classList);
console.log(button.classList.contains("border-blue-300"));
const toggleClasses = [
"border-gray-300", "hover:bg-gray-50", "hover:border-blue-300",
"border-blue-300", "bg-blue-50", "hover:bg-blue-100", "hover:border-blue-500"
];
for (const cls of toggleClasses) {
console.log("toggling class " + cls);
button.classList.toggle(cls);
}
if (!button.classList.contains("border-blue-300")) {
button.innerHTML = `
<svg class="h-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 6.00019C10.2006 3.90317 7.19377 3.2551 4.93923 5.17534C2.68468 7.09558 2.36727 10.3061 4.13778 12.5772C5.60984 14.4654 10.0648 18.4479 11.5249 19.7369C11.6882 19.8811 11.7699 19.9532 11.8652 19.9815C11.9483 20.0062 12.0393 20.0062 12.1225 19.9815C12.2178 19.9532 12.2994 19.8811 12.4628 19.7369C13.9229 18.4479 18.3778 14.4654 19.8499 12.5772C21.6204 10.3061 21.3417 7.07538 19.0484 5.17534C16.7551 3.2753 13.7994 3.90317 12 6.00019Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
></path>
</svg>
Favorite
`;
} else {
button.innerHTML = `
<svg class="h-6 text-red-500" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M2 9.1371C2 14 6.01943 16.5914 8.96173 18.9109C10 19.7294 11 20.5 12 20.5C13 20.5 14 19.7294 15.0383 18.9109C17.9806 16.5914 22 14 22 9.1371C22 4.27416 16.4998 0.825464 12 5.50063C7.50016 0.825464 2 4.27416 2 9.1371Z"
fill="currentColor"
></path>
</svg>
Unfavorite
`;
}
}
</script> </script>
} }

File diff suppressed because one or more lines are too long