Skip to content

Commit

Permalink
Implement clone recipe (#445)
Browse files Browse the repository at this point in the history
  • Loading branch information
reaper47 authored Sep 29, 2024
1 parent 6ddce8f commit 0b23b83
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 11 deletions.
44 changes: 44 additions & 0 deletions internal/server/handlers_recipes.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ func (s *Server) recipeAddManualHandler() http.HandlerFunc {
View: &templates.ViewRecipeData{
Categories: categories,
Keywords: keywords,
Recipe: &models.Recipe{},
},
}).Render(r.Context(), w)
}
Expand Down Expand Up @@ -1073,6 +1074,49 @@ func (s *Server) recipeShareAddHandler() http.HandlerFunc {
}
}

func (s *Server) recipeDuplicateHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var (
userID = getUserID(r)
userIDAttr = slog.Int64("userID", userID)
)

recipeID, err := parsePathPositiveID(r.PathValue("id"))
if err != nil {
slog.Error("Failed to parse recipe ID", userIDAttr, "error", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

recipe, err := s.Repository.Recipe(recipeID, getUserID(r))
if err != nil {
slog.Error("Failed to fetch recipe", userIDAttr, "error", err)
notFoundHandler(w, r)
return
}
recipe.Name = recipe.Name + " (copy)"

categories, err := s.Repository.Categories(userID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

keywords, err := s.Repository.Keywords()
if err != nil {
slog.Error("Failed to fetch keywords", "error", err)
}

_ = components.AddRecipeManual(templates.Data{
About: templates.NewAboutData(),
IsAdmin: userID == 1,
IsAuthenticated: true,
IsHxRequest: r.Header.Get("Hx-Request") == "true",
View: templates.NewViewRecipeData(recipeID, recipe, categories, keywords, true, false),
}).Render(r.Context(), w)
}
}

func (s *Server) recipesCategoriesDeleteHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var (
Expand Down
80 changes: 76 additions & 4 deletions internal/server/handlers_recipes_test.go

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ func (s *Server) mountHandlers() {
mux.Handle("GET /recipes/{id}/scale", s.mustBeLoggedInMiddleware(s.recipeScaleHandler()))
mux.Handle("POST /recipes/{id}/share", withLog(s.recipeSharePostHandler()))
mux.Handle("GET /recipes/{id}/share/add", withLog(s.recipeShareAddHandler()))
mux.Handle("GET /recipes/{id}/duplicate", withLog(s.recipeDuplicateHandler()))
mux.Handle("GET /recipes/{id}/edit", s.mustBeLoggedInMiddleware(s.recipesEditHandler()))
mux.Handle("PUT /recipes/{id}/edit", withLog(s.recipesEditPutHandler()))
mux.Handle("GET /recipes/add", s.mustBeLoggedInMiddleware(recipesAddHandler()))
Expand Down
6 changes: 6 additions & 0 deletions web/components/icons.templ
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,12 @@ templ iconDeleteSmall() {
</svg>
}

templ iconDocumentDuplicate() {
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 hover:text-red-600">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"></path>
</svg>
}

templ iconDotsVertical() {
<svg
xmlns="http://www.w3.org/2000/svg"
Expand Down
144 changes: 137 additions & 7 deletions web/components/recipes.templ
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,9 @@ templ addRecipeManual(data *templates.ViewRecipeData) {
placeholder="Title of the recipe*"
autocomplete="off"
class="input w-full btn-ghost text-center"
if data.Recipe.Name != "" {
value={ data.Recipe.Name }
}
/>
</label>
</h2>
Expand Down Expand Up @@ -446,6 +449,7 @@ templ addRecipeManual(data *templates.ViewRecipeData) {
class="input input-bordered input-sm w-48 md:w-36 lg:w-48"
placeholder="Breakfast"
autocomplete="off"
value={ data.Recipe.Category }
/>
<datalist id="categories">
for _, c := range data.Categories {
Expand All @@ -463,7 +467,11 @@ templ addRecipeManual(data *templates.ViewRecipeData) {
type="number"
min="1"
name="yield"
value="1"
if data.Recipe.Yield == 0 {
value="1"
} else {
value={ strconv.FormatInt(int64(data.Recipe.Yield), 10) }
}
class="input input-bordered input-sm w-24 md:w-20 lg:w-24"
/>
</label>
Expand All @@ -479,6 +487,7 @@ templ addRecipeManual(data *templates.ViewRecipeData) {
placeholder="Source"
name="source"
class="input input-bordered input-sm md:w-28 lg:w-40 xl:w-44"
value={ data.Recipe.URL }
/>
</label>
<button
Expand All @@ -493,6 +502,13 @@ templ addRecipeManual(data *templates.ViewRecipeData) {
</div>
<div class="border-gray-700 border-y col-span-6 md:grid-cols-3">
<div class="p-4 flex gap-2 flex-wrap">
for _, kw := range data.Recipe.Keywords {
<div class="badge badge-sm badge-neutral p-3 pr-0">
<input type="hidden" name="keywords" value={ kw }/>
<span class="select-none">{ kw }</span>
<button type="button" class="btn btn-xs btn-ghost" _="on click remove closest <div/>">X</button>
</div>
}
@recipeKeywordEmpty(data.Keywords)
</div>
</div>
Expand All @@ -503,7 +519,11 @@ templ addRecipeManual(data *templates.ViewRecipeData) {
<input
type="text"
name="time-preparation"
value="00:15:00"
if data.FormattedTimes.PrepEdit != "" {
value={ data.FormattedTimes.PrepEdit }
} else {
value="00:15:00"
}
class="input input-bordered input-xs max-w-24 html-duration-picker"
/>
</label>
Expand All @@ -514,7 +534,11 @@ templ addRecipeManual(data *templates.ViewRecipeData) {
<input
type="text"
name="time-cooking"
value="00:30:00"
if data.FormattedTimes.CookEdit != "" {
value={ data.FormattedTimes.CookEdit }
} else {
value="00:30:00"
}
class="input input-bordered input-xs max-w-24 html-duration-picker"
/>
</label>
Expand Down Expand Up @@ -692,7 +716,7 @@ templ addRecipeManual(data *templates.ViewRecipeData) {
name="description"
placeholder="This Thai curry chicken will make you drool."
class="textarea w-full h-full resize-none"
></textarea>
>{ data.Recipe.Description }</textarea>
</label>
</div>
</div>
Expand All @@ -707,15 +731,27 @@ templ addRecipeManual(data *templates.ViewRecipeData) {
<span class="underline">Tools</span>
</h2>
<ol id="tools-list" class="pl-4 list-decimal">
@AddTool(models.HowToItem{})
if len(data.Recipe.Instructions) > 0 {
for _, tool := range data.Recipe.Tools {
@AddTool(tool)
}
} else {
@AddTool(models.HowToItem{})
}
</ol>
<div class="divider"></div>
<h2 class="font-semibold text-center pb-2">
<span class="underline">Ingredients</span>
<sup class="text-red-600">*</sup>
</h2>
<ol id="ingredients-list" class="pl-4 list-decimal">
@AddIngredient("")
if len(data.Recipe.Ingredients) > 0 {
for _, ing := range data.Recipe.Ingredients {
@AddIngredient(ing)
}
} else {
@AddIngredient("")
}
</ol>
</div>
<div class="col-span-6 px-6 py-2 border-gray-700 md:rounded-bl-none md:col-span-4">
Expand All @@ -724,7 +760,13 @@ templ addRecipeManual(data *templates.ViewRecipeData) {
<sup class="text-red-600">*</sup>
</h2>
<ol id="instructions-list" class="grid list-decimal">
@AddInstruction("")
if len(data.Recipe.Instructions) > 0 {
for _, ins := range data.Recipe.Instructions {
@AddInstruction(ins)
}
} else {
@AddInstruction("")
}
</ol>
</div>
</div>
Expand Down Expand Up @@ -1750,6 +1792,80 @@ templ editRecipe(data *templates.ViewRecipeData) {
</script>
}

templ fuck(data *templates.ViewRecipeData) {
for i, image := range data.Recipe.Images {
<label id={ "media-" + strconv.Itoa(i+1) } class={ templ.KV("hidden", i > 0) }>
<img
alt=""
class="object-cover mb-2 w-full max-h-[39rem]"
if data.IsImagesExist[i] {
src={ "/data/images/" + image.String() + ".webp" }
} else {
src=""
}
/>
<span class="grid gap-1 max-w-sm" style="margin: auto auto 0.25rem;">
<div class="mr-1 hidden">
<input
type="file"
accept="image/*,video/*"
name="images"
class="file-input file-input-sm file-input-bordered w-full max-w-sm"
value={ "/data/images/" + image.String() + ".webp" }
_="on dragover or dragenter halt the event then set the target's style.background to 'lightgray'
on dragleave or drop set the target's style.background to ''
on drop or change
make an FileReader called reader then
if event.dataTransfer
get event.dataTransfer.files[0]
else
get event.target.files[0]
end then
if it.type.startsWith('video')
put `<video controls class='object-cover mb-2 w-full max-h-[39rem]' src='${window.URL.createObjectURL(it)}'></video>` after previous <img/> then
add .hidden to previous <img/>
else
set {src: window.URL.createObjectURL(it)} on previous <img/>
end then
remove .hidden from me.parentElement.parentElement.querySelectorAll('button') then
add .hidden to the parentElement of me"
/>
<div class="divider">OR</div>
<span class="hidden input-error"></span>
<div class="flex">
<input type="url" placeholder="Enter the URL of an image" class="input input-bordered input-sm w-full max-w-sm mr-1"/>
<button
type="button"
class="btn btn-sm"
hx-get="/fetch"
hx-vals="js:{url: event.target.previousElementSibling.value}"
hx-swap="none"
_="on htmx:afterRequest
if event.detail.successful then
set a to first in event.target.parentElement.parentElement.children then
call updateMediaFromFetch(a, event.detail.xhr.responseURL)
end"
>
Fetch
</button>
</div>
<div _="on load if not navigator.clipboard hide me">
<div class="divider">OR</div>
<button type="button" class="btn btn-sm" onclick="pasteImage(event)">Paste copied image</button>
</div>
</div>
<button
type="button"
class={ "btn btn-sm btn-error btn-outline", templ.KV("hidden", !data.IsImagesExist[i]) }
onclick="deleteMedia(event)"
>
Delete
</button>
</span>
</label>
}
}

templ RecipesIndex(data templates.Data) {
if data.IsHxRequest {
<title hx-swap-oob="true">Recipes | Recipya</title>
Expand Down Expand Up @@ -2016,6 +2132,17 @@ templ viewRecipe(data *templates.ViewRecipeData, isAuthenticated bool) {
</a>
</li>
}
<li>
<a
title="Duplicate recipe"
hx-push-url="/recipes/add/manual"
hx-get={ fmt.Sprintf("/recipes/%d/duplicate", data.ID) }
hx-target="#content"
>
@iconDocumentDuplicate()
Duplicate
</a>
</li>
<li title="Print recipe" _="on click print()">
<a>
@iconPrint()
Expand Down Expand Up @@ -2070,6 +2197,9 @@ templ viewRecipe(data *templates.ViewRecipeData, isAuthenticated bool) {
@iconShare()
</button>
}
<button class="mr-2 hidden sm:block" title="Duplicate recipe" hx-push-url="/recipes/add/manual" hx-get={ fmt.Sprintf("/recipes/%d/duplicate", data.ID) } hx-target="#content">
@iconDocumentDuplicate()
</button>
<button class="mr-2 hidden sm:block" title="Print recipe" _="on click print()">
@iconPrint()
</button>
Expand Down

0 comments on commit 0b23b83

Please sign in to comment.