(FEAT): Engagement for recipes is translated.
This commit is contained in:
parent
2f5dd0dbc4
commit
c68cb72ffb
@ -14,12 +14,17 @@ type AuthenticatedFunc func(ctx *gin.Context, user *domain.User)
|
||||
// the function will return an error with a 401 status.
|
||||
//
|
||||
// BUG: This is probably not very effecient, since we hit the DB on every single protected request.
|
||||
// If this ends up being a bottle neck we could simply hit the context for the userId, since
|
||||
// that is usually all we need...Or maybe have two methods, for those that need the whole user
|
||||
// and those that just need the ID.
|
||||
//
|
||||
// If this ends up being a bottle neck we could simply hit the context for the userId, since
|
||||
// that is usually all we need...Or maybe have two methods, for those that need the whole user
|
||||
// and those that just need the ID.
|
||||
func (s *Server) withAuthenticatedUser(ctx *gin.Context, handler AuthenticatedFunc) {
|
||||
user := s.deps.UserService.GetAuthenicatedUser(ctx)
|
||||
if user == nil {
|
||||
// User is stale, ensure they are logged out so they can be prompted to log back in
|
||||
s.SetCookie(ctx, "jwt_token", "", -1)
|
||||
// s.SetCookie(ctx, "search-filters", "", -1) // TODO: Might need this again
|
||||
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{
|
||||
"status": http.StatusUnauthorized,
|
||||
"message": "[UNAUTHORIZED] Could not fetch authenticated user.",
|
||||
@ -29,17 +34,16 @@ func (s *Server) withAuthenticatedUser(ctx *gin.Context, handler AuthenticatedFu
|
||||
handler(ctx, user)
|
||||
}
|
||||
|
||||
|
||||
// getUserId retrieves the userId from the context and returns a pointer to it. A nil
|
||||
// pointer can be returned and will if the userId does not exist.
|
||||
func getUserId(ctx *gin.Context) *int {
|
||||
userIdAny, exists := ctx.Get("userId")
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
userIdInt, ok := userIdAny.(int)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &userIdInt
|
||||
userIdAny, exists := ctx.Get("userId")
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
userIdInt, ok := userIdAny.(int)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &userIdInt
|
||||
}
|
||||
|
||||
143
internal/app/server/engagement_handler_v2.go
Normal file
143
internal/app/server/engagement_handler_v2.go
Normal file
@ -0,0 +1,143 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||
)
|
||||
|
||||
func (s *Server) EngagementViewRecipeHandlerV2(ctx *gin.Context) {
|
||||
recipeId, err := strconv.Atoi(ctx.Param("id"))
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": http.StatusBadRequest,
|
||||
"message": fmt.Sprintf("[ERROR] Failed to parse ID parameter. %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userId := getUserId(ctx)
|
||||
if userId == nil {
|
||||
if engagement, err := s.deps.EngagementService.ViewRecipe(recipeId); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": http.StatusBadRequest,
|
||||
"message": fmt.Sprintf("[ERROR] Failed to create engagement. %s", err.Error()),
|
||||
})
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"status": http.StatusOK,
|
||||
"message": "[OK] Successfully created engagement.",
|
||||
"engagement": engagement,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
if engagement, err := s.deps.EngagementService.UserViewRecipe(*userId, recipeId); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": http.StatusBadRequest,
|
||||
"message": fmt.Sprintf("[ERROR] Failed to create engagement. %s", err.Error()),
|
||||
})
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"status": http.StatusOK,
|
||||
"message": "[OK] Successfully created engagement.",
|
||||
"engagement": engagement,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) EngagementShareRecipeHandlerV2(ctx *gin.Context) {
|
||||
recipeId, err := strconv.Atoi(ctx.Param("id"))
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": http.StatusBadRequest,
|
||||
"message": fmt.Sprintf("[ERROR] Failed to parse ID parameter. %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userId := getUserId(ctx)
|
||||
if userId == nil {
|
||||
if engagement, err := s.deps.EngagementService.ShareRecipe(recipeId); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": http.StatusBadRequest,
|
||||
"message": fmt.Sprintf("[ERROR] Failed to create engagement. %s", err.Error()),
|
||||
})
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"status": http.StatusOK,
|
||||
"message": "[OK] Successfully created engagement.",
|
||||
"engagement": engagement,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if engagement, err := s.deps.EngagementService.UserShareRecipe(*userId, recipeId); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": http.StatusBadRequest,
|
||||
"message": fmt.Sprintf("[ERROR] Failed to create engagement. %s", err.Error()),
|
||||
})
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"status": http.StatusOK,
|
||||
"message": "[OK] Successfully created engagement.",
|
||||
"engagement": engagement,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) EngagementFavoriteRecipeHandlerV2(ctx *gin.Context) {
|
||||
s.withAuthenticatedUser(ctx, func(ctx *gin.Context, user *domain.User) {
|
||||
recipeId, err := strconv.Atoi(ctx.Param("id"))
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": http.StatusBadRequest,
|
||||
"message": fmt.Sprintf("[ERROR] Failed to parse ID parameter. %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if engagement, err := s.deps.EngagementService.UserFavoriteRecipe(user.Id, recipeId); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": http.StatusBadRequest,
|
||||
"message": fmt.Sprintf("[ERROR] Failed to create engagement. %s", err.Error()),
|
||||
})
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"status": http.StatusOK,
|
||||
"message": "[OK] Successfully created engagement.",
|
||||
"engagement": engagement,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) EngagementMakeRecipeHandlerV2(ctx *gin.Context) {
|
||||
s.withAuthenticatedUser(ctx, func(ctx *gin.Context, user *domain.User) {
|
||||
recipeId, err := strconv.Atoi(ctx.Param("id"))
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": http.StatusBadRequest,
|
||||
"message": fmt.Sprintf("[ERROR] Failed to parse ID parameter. %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if engagement, err := s.deps.EngagementService.UserMakeRecipe(user.Id, recipeId); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": http.StatusBadRequest,
|
||||
"message": fmt.Sprintf("[ERROR] Failed to create engagement. %s", err.Error()),
|
||||
})
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"status": http.StatusOK,
|
||||
"message": "[OK] Successfully created engagement.",
|
||||
"engagement": engagement,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -220,5 +220,10 @@ func (s *Server) Setup() *Server {
|
||||
ctx.JSON(http.StatusOK, gin.H{"msg": "YAY"})
|
||||
})
|
||||
|
||||
router_api_v2.POST("/engagement/view/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementViewRecipeHandlerV2)
|
||||
router_api_v2.POST("/engagement/share/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementShareRecipeHandlerV2)
|
||||
router_api_v2.POST("/engagement/favorite/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementFavoriteRecipeHandlerV2)
|
||||
router_api_v2.POST("/engagement/make/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementMakeRecipeHandlerV2)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ func (s *Server) GetAuthenicatedUserRecipesV2(ctx *gin.Context) {
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": http.StatusBadRequest,
|
||||
"message": fmt.Sprintf("[FAILED] Could not fetch authenticated users's recipes. %s", err.Error()),
|
||||
"message": fmt.Sprintf("[ERROR] Could not fetch authenticated users's recipes. %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -43,7 +43,7 @@ func (s *Server) GetAuthenicatedUserFavoritesV2(ctx *gin.Context) {
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": http.StatusBadRequest,
|
||||
"message": fmt.Sprintf("[FAILED] Could not fetch authenticated users's favorites. %s", err.Error()),
|
||||
"message": fmt.Sprintf("[ERROR] Could not fetch authenticated users's favorites. %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -62,7 +62,7 @@ func (s *Server) GetAuthenicatedUserEngagementV2(ctx *gin.Context) {
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": http.StatusBadRequest,
|
||||
"message": fmt.Sprintf("[FAILED] Failed to get authenticated user engagement. %s", err.Error()),
|
||||
"message": fmt.Sprintf("[ERROR] Failed to get authenticated user engagement. %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -81,7 +81,7 @@ func (s *Server) GetAuthenicatedUserMadeRecipesV2(ctx *gin.Context) {
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": http.StatusBadRequest,
|
||||
"message": fmt.Sprintf("[FAILED] Failed to get authenticated user's made recipes. %s", err.Error()),
|
||||
"message": fmt.Sprintf("[ERROR] Failed to get authenticated user's made recipes. %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -100,7 +100,7 @@ func (s *Server) GetAuthenicatedUserViewedRecipesV2(ctx *gin.Context) {
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": http.StatusBadRequest,
|
||||
"message": fmt.Sprintf("[FAILED] Failed to get authenticated user's viewed recipes. %s", err.Error()),
|
||||
"message": fmt.Sprintf("[ERROR] Failed to get authenticated user's viewed recipes. %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ export default function Spinner({ content }: SpinnerProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="w-8 h-8 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin" />
|
||||
<h2 className="text-xl text-gray-700"> { content }</h2>
|
||||
<h2 className="text-xl text-gray-700"> {content}</h2>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { use, useEffect, useState } from "react";
|
||||
import { AuthContext } from "../../context/AuthContext";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { EngagementFavoriteRecipe } from "../../services/EngagementService";
|
||||
import { isApiError } from "../../types/api/error";
|
||||
|
||||
|
||||
interface FavoriteButtonProps {
|
||||
@ -7,11 +11,29 @@ interface FavoriteButtonProps {
|
||||
}
|
||||
|
||||
export default function FavoriteButton({ favorite, id }: FavoriteButtonProps) {
|
||||
// CONTEXT
|
||||
const { isLoggedIn } = use(AuthContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [_favorite, setFavorite] = useState<boolean>();
|
||||
|
||||
const clickHandler = () => {
|
||||
// TODO: Implement action here!
|
||||
const clickHandler = async () => {
|
||||
// This button cannot be used if not logged in
|
||||
if (!isLoggedIn) {
|
||||
await navigate("/v2/web/login");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!id) return;
|
||||
|
||||
// Toggle button first, to feel fast
|
||||
setFavorite(!_favorite);
|
||||
|
||||
const result = await EngagementFavoriteRecipe(id);
|
||||
if (isApiError(result)) {
|
||||
console.error(result.message);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@ -23,7 +45,7 @@ export default function FavoriteButton({ favorite, id }: FavoriteButtonProps) {
|
||||
return _favorite ? (
|
||||
<button
|
||||
className="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 cursor-pointer"
|
||||
onClick={clickHandler}
|
||||
onClick={() => void clickHandler()}
|
||||
>
|
||||
<svg className="h-6 text-red-500" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
@ -37,7 +59,7 @@ export default function FavoriteButton({ favorite, id }: FavoriteButtonProps) {
|
||||
) : (
|
||||
<button
|
||||
className="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 cursor-pointer"
|
||||
onClick={clickHandler}
|
||||
onClick={() => void clickHandler()}
|
||||
>
|
||||
<svg className="h-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
|
||||
@ -1,25 +1,42 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { use, useState } from "react";
|
||||
import { AuthContext } from "../../context/AuthContext";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { EngagementMakeRecipe } from "../../services/EngagementService";
|
||||
import { isApiError } from "../../types/api/error";
|
||||
|
||||
interface MadeButtonProps {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export default function MadeButton({ id }: MadeButtonProps) {
|
||||
// CONTEXT
|
||||
const { isLoggedIn } = use(AuthContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [clicked, setClicked] = useState<boolean>(false);
|
||||
|
||||
const clickHandler = () => {
|
||||
if (clicked) return;
|
||||
const clickHandler = async () => {
|
||||
// This button cannot be used if not logged in
|
||||
if (!isLoggedIn) {
|
||||
await navigate("/v2/web/login");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Implement actions
|
||||
if (!id || clicked) return;
|
||||
|
||||
// Toggle button first, to feel fast
|
||||
setClicked(true);
|
||||
|
||||
const result = await EngagementMakeRecipe(id);
|
||||
if (isApiError(result)) {
|
||||
console.error(result.message);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`flex items-center justify-center gap-x-1 rounded-lg border ${clicked ? "border-blue-500 text-blue-500" : "border-gray-300 text-gray-800"} px-6 py-3 flex-grow hover:bg-gray-50 hover:border-blue-300 duration-300 cursor-pointer`}
|
||||
onClick={clickHandler}
|
||||
onClick={() => void clickHandler()}
|
||||
>
|
||||
<svg
|
||||
className="h-6"
|
||||
|
||||
@ -1,17 +1,28 @@
|
||||
import { useState } from "react";
|
||||
import { EngagementShareRecipe } from "../../services/EngagementService";
|
||||
import { isApiError } from "../../types/api/error";
|
||||
|
||||
|
||||
interface ShareButtonProps {
|
||||
id: number;
|
||||
}
|
||||
|
||||
// TODO: Abstract this somehow, this needs to be loaded from the env
|
||||
const DOMAIN = "http://localhost:5173/";
|
||||
|
||||
export default function ShareButton({ id }: ShareButtonProps) {
|
||||
const [clicked, setClicked] = useState<boolean>(false);
|
||||
|
||||
const clickHandler = () => {
|
||||
const clickHandler = async () => {
|
||||
if (clicked) return;
|
||||
|
||||
// TODO: Implement action here
|
||||
// Copy first, so it feels fast
|
||||
await navigator.clipboard.writeText(`${DOMAIN}/v2/web/recipe/${id}`)
|
||||
|
||||
const result = await EngagementShareRecipe(id);
|
||||
if (isApiError(result)) {
|
||||
console.error(result.message);
|
||||
}
|
||||
|
||||
setClicked(true);
|
||||
};
|
||||
@ -30,7 +41,7 @@ export default function ShareButton({ id }: ShareButtonProps) {
|
||||
) : (
|
||||
<button
|
||||
className="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 cursor-pointer"
|
||||
onClick={clickHandler}
|
||||
onClick={() => void clickHandler()}
|
||||
>
|
||||
<svg className="h-7" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
|
||||
@ -1,16 +1,29 @@
|
||||
import type { Recipe } from "../../types/recipe";
|
||||
import RecipePlaceholder from "../../assets/images/recipe_placeholder.png"
|
||||
import LikeButton from "../buttons/LikeButton";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { EngagementViewRecipe } from "../../services/EngagementService";
|
||||
import { isApiError } from "../../types/api/error";
|
||||
|
||||
interface RecipeCardLargeProps {
|
||||
recipe: Recipe | null;
|
||||
}
|
||||
|
||||
export default function RecipeCardLarge({ recipe }: RecipeCardLargeProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// HANDLERS
|
||||
const makeButtonHandler = () => console.log("makeButtonHandler()");
|
||||
const makeButtonHandler = async () => {
|
||||
if (!recipe) return;
|
||||
|
||||
// Navigate first, so it feels faster
|
||||
await navigate(`/v2/web/recipe/${recipe.Id}`);
|
||||
|
||||
const result = await EngagementViewRecipe(recipe.Id);
|
||||
if (isApiError(result)) {
|
||||
console.error(result.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (recipe == null) {
|
||||
return <h2 className="text-2xl md:text-3xl text-gray-400">Coming soon!</h2>
|
||||
@ -36,7 +49,7 @@ export default function RecipeCardLarge({ recipe }: RecipeCardLargeProps) {
|
||||
{recipe.Favorite && <LikeButton />}
|
||||
</div>
|
||||
<button
|
||||
onClick={makeButtonHandler}
|
||||
onClick={() => void makeButtonHandler()}
|
||||
className="w-full rounded-lg py-2 bg-gradient-to-r from-blue-400 to-blue-600 text-white mt-2 hover:ring-blue-700 hover:shadow shadow-blue-300 duration-200 cursor-pointer"
|
||||
>
|
||||
Make Now!
|
||||
|
||||
@ -1,15 +1,27 @@
|
||||
import type { Recipe } from "../../types/recipe";
|
||||
import RecipePlaceholder from "../../assets/images/recipe_placeholder.png"
|
||||
import LikeButton from "../buttons/LikeButton";
|
||||
import { EngagementViewRecipe } from "../../services/EngagementService";
|
||||
import { isApiError } from "../../types/api/error";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
interface RecipeCardSmallProps {
|
||||
recipe: Recipe;
|
||||
}
|
||||
|
||||
export default function RecipeCardSmall({ recipe }: RecipeCardSmallProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// HANDLERS
|
||||
const makeButtonHandler = () => console.log("makeButtonHandler()");
|
||||
const makeButtonHandler = async () => {
|
||||
// Navigate first, so it feels faster
|
||||
await navigate(`/v2/web/recipe/${recipe.Id}`);
|
||||
|
||||
const result = await EngagementViewRecipe(recipe.Id);
|
||||
if (isApiError(result)) {
|
||||
console.error(result.message);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-between rounded-lg border-gray-300 border shadow-md p-4 flex-shrink-0">
|
||||
@ -28,7 +40,7 @@ export default function RecipeCardSmall({ recipe }: RecipeCardSmallProps) {
|
||||
{recipe.Favorite && <LikeButton />}
|
||||
</div>
|
||||
<button
|
||||
onClick={makeButtonHandler}
|
||||
onClick={() => void makeButtonHandler()}
|
||||
className="w-full rounded-lg py-2 bg-gradient-to-r from-blue-400 to-blue-600 text-white mt-2 hover:ring-blue-700 hover:shadow shadow-blue-300 duration-200 cursor-pointer"
|
||||
>
|
||||
Make Now!
|
||||
|
||||
@ -3,22 +3,22 @@ interface InstructionListProps {
|
||||
}
|
||||
export default function InstructionList({ instructions }: InstructionListProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="px-4 py-8 md:px-8">
|
||||
<h2 className="text-2xl text-gray-800 font-semibold mb-2">Instructions</h2>
|
||||
<hr className="text-gray-300"/>
|
||||
<ul className="text-lg my-4 text-gray-700">
|
||||
<>
|
||||
<div className="px-4 py-8 md:px-8">
|
||||
<h2 className="text-2xl text-gray-800 font-semibold mb-2">Instructions</h2>
|
||||
<hr className="text-gray-300" />
|
||||
<ul className="text-lg my-4 text-gray-700">
|
||||
{instructions?.map((instruction, i) => (
|
||||
<li key={instruction} className="p-4 flex items-start gap-x-4 odd:bg-[#f8f8f8]">
|
||||
<div className="size-8 md:size-10 bg-blue-50 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<h3 className="text-base md:text-xl text-blue-600 font-semibold">{ i + 1}</h3>
|
||||
<h3 className="text-base md:text-xl text-blue-600 font-semibold">{i + 1}</h3>
|
||||
</div>
|
||||
<p className="text-base">{ instruction }</p>
|
||||
<p className="text-base">{instruction}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -63,7 +63,6 @@ export default function RecipePage() {
|
||||
<TagList tags={recipe.Tags} created={recipe.Created} modified={recipe.Modified} />
|
||||
</>
|
||||
) : (
|
||||
/* TODO: Implement a loading page! */
|
||||
<div className="min-h-[90vh] flex items-center justify-center gap-x-4">
|
||||
<Spinner content={"Recipe is loading..."} />
|
||||
</div>
|
||||
|
||||
60
web/src/services/EngagementService.ts
Normal file
60
web/src/services/EngagementService.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import axios from "axios";
|
||||
import type { ApiError } from "../types/api/error";
|
||||
import type { Engagement } from "../types/engagement";
|
||||
import type { EngagementFavoriteRecipeResponse, EngagementMakeRecipeResponse, EngagementShareRecipeResponse, EngagementViewRecipeResponse } from "../types/api/engagement";
|
||||
|
||||
export async function EngagementViewRecipe(recipeId: number): Promise<Engagement | ApiError> {
|
||||
const response = await axios.post<EngagementViewRecipeResponse>(`http://localhost:3000/v2/api/engagement/view/${recipeId}`);
|
||||
|
||||
if (response.status !== 200 || response.data.engagement === undefined) {
|
||||
const err: ApiError = {
|
||||
status: response.data.status,
|
||||
message: response.data.message
|
||||
};
|
||||
return err;
|
||||
}
|
||||
|
||||
return response.data.engagement;
|
||||
}
|
||||
|
||||
export async function EngagementShareRecipe(recipeId: number): Promise<Engagement | ApiError> {
|
||||
const response = await axios.post<EngagementShareRecipeResponse>(`http://localhost:3000/v2/api/engagement/share/${recipeId}`);
|
||||
|
||||
if (response.status !== 200 || response.data.engagement === undefined) {
|
||||
const err: ApiError = {
|
||||
status: response.data.status,
|
||||
message: response.data.message
|
||||
};
|
||||
return err;
|
||||
}
|
||||
|
||||
return response.data.engagement;
|
||||
}
|
||||
|
||||
export async function EngagementFavoriteRecipe(recipeId: number): Promise<Engagement | ApiError> {
|
||||
const response = await axios.post<EngagementFavoriteRecipeResponse>(`http://localhost:3000/v2/api/engagement/favorite/${recipeId}`);
|
||||
|
||||
if (response.status !== 200 || response.data.engagement === undefined) {
|
||||
const err: ApiError = {
|
||||
status: response.data.status,
|
||||
message: response.data.message
|
||||
};
|
||||
return err;
|
||||
}
|
||||
|
||||
return response.data.engagement;
|
||||
}
|
||||
|
||||
export async function EngagementMakeRecipe(recipeId: number): Promise<Engagement | ApiError> {
|
||||
const response = await axios.post<EngagementMakeRecipeResponse>(`http://localhost:3000/v2/api/engagement/make/${recipeId}`);
|
||||
|
||||
if (response.status !== 200 || response.data.engagement === undefined) {
|
||||
const err: ApiError = {
|
||||
status: response.data.status,
|
||||
message: response.data.message
|
||||
};
|
||||
return err;
|
||||
}
|
||||
|
||||
return response.data.engagement;
|
||||
}
|
||||
26
web/src/types/api/engagement.ts
Normal file
26
web/src/types/api/engagement.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import type { Engagement } from "../engagement";
|
||||
|
||||
|
||||
export interface EngagementViewRecipeResponse {
|
||||
status: number;
|
||||
message: string;
|
||||
engagement?: Engagement;
|
||||
}
|
||||
|
||||
export interface EngagementShareRecipeResponse {
|
||||
status: number;
|
||||
message: string;
|
||||
engagement?: Engagement;
|
||||
}
|
||||
|
||||
export interface EngagementFavoriteRecipeResponse {
|
||||
status: number;
|
||||
message: string;
|
||||
engagement?: Engagement;
|
||||
}
|
||||
|
||||
export interface EngagementMakeRecipeResponse {
|
||||
status: number;
|
||||
message: string;
|
||||
engagement?: Engagement;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user