(FEAT): Wired backend to frontend for recipe creation. #7
@ -135,50 +135,50 @@ creation process will take place here
|
||||
|
||||
##### UI Requirements
|
||||
|
||||
- [ ] Create Recipe Heading Banner
|
||||
- [ ] Large, simple text banner that displays a header
|
||||
- [ ] Smaller text section that contains some useful information for using the wizard
|
||||
- [x] Create Recipe Heading Banner
|
||||
- [x] Large, simple text banner that displays a header
|
||||
- [x] Smaller text section that contains some useful information for using the wizard
|
||||
|
||||
- [ ] Recipe Creation Wizard
|
||||
- [ ] **Recipe Details:** required
|
||||
- [ ] **Recipe title:** small text input (1 to 128)
|
||||
- [ ] **Recipe description:** large text input
|
||||
- [ ] **Meal category:** pill radio buttons or drop down*
|
||||
- [ ] **Service size:** numeric input (1 to 16)
|
||||
- [ ] **Difficulty rating:** star buttons that can be selected (up to 5)
|
||||
- [ ] **Duration:** prep and cook time, individual numeric inputs
|
||||
- [ ] **Ingredients:** required
|
||||
- [ ] **Dynamic ingredient list:** List of each ingredient
|
||||
- [ ] **Content:** name and quantity (both text inputs)
|
||||
- [x] Recipe Creation Wizard
|
||||
- [x] **Recipe Details:** required
|
||||
- [x] **Recipe title:** small text input (1 to 128)
|
||||
- [x] **Recipe description:** large text input
|
||||
- [x] **Meal category:** pill radio buttons or drop down*
|
||||
- [x] **Service size:** numeric input (1 to 16)
|
||||
- [x] **Difficulty rating:** star buttons that can be selected (up to 5)
|
||||
- [x] **Duration:** prep and cook time, individual numeric inputs
|
||||
- [x] **Ingredients:** required
|
||||
- [x] **Dynamic ingredient list:** List of each ingredient
|
||||
- [x] **Content:** name and quantity (both text inputs)
|
||||
- [ ] **Actions:** delete button and **reorder element***
|
||||
- [ ] **Add ingredient button:** Simple button to add a blank ingredient row
|
||||
- [ ] **Instructions:** required
|
||||
- [ ] **Dynamic instructions list:** Numbered list of each instruction
|
||||
- [ ] **Content:** number and large text instruction
|
||||
- [x] **Add ingredient button:** Simple button to add a blank ingredient row
|
||||
- [x] **Instructions:** required
|
||||
- [x] **Dynamic instructions list:** Numbered list of each instruction
|
||||
- [x] **Content:** number and large text instruction
|
||||
- [ ] **Rich text editor?***
|
||||
- [ ] **Actions:** delete button and **reorder element***
|
||||
- [ ] **Add step button:** Simple button to add a blank instruction element
|
||||
- [ ] **Media & Tags:** optional
|
||||
- [ ] **Image Upload**
|
||||
- [ ] Single image selector for the thumbnail image
|
||||
- [ ] Small image display once one has been upload
|
||||
- [ ] Remove button to remove the image
|
||||
- [ ] Replace button to replace the image
|
||||
- [ ] **Tags**
|
||||
- [ ] Text input to add tags
|
||||
- [x] **Add step button:** Simple button to add a blank instruction element
|
||||
- [x] **Media & Tags:** optional
|
||||
- [x] **Image Upload**
|
||||
- [x] Single image selector for the thumbnail image
|
||||
- [x] Small image display once one has been upload
|
||||
- [x] Remove button to remove the image
|
||||
- [x] Replace button to replace the image
|
||||
- [x] **Tags**
|
||||
- [x] Text input to add tags
|
||||
- [ ] Using list of existing tags, use a prefill while typing
|
||||
- [ ] Tags that don't exist will be added
|
||||
- [ ] Display a small list of added tags
|
||||
- [ ] Clicking a tag will remove it from the list
|
||||
- [ ] **Footer & Submit**
|
||||
- [ ] Save recipe button to complete the form
|
||||
- [x] Tags that don't exist will be added
|
||||
- [x] Display a small list of added tags
|
||||
- [x] Clicking a tag will remove it from the list
|
||||
- [x] **Footer & Submit**
|
||||
- [x] Save recipe button to complete the form
|
||||
- [ ] Button is disabled until the minimum required fields are complete*
|
||||
|
||||
- [ ] Input Validation
|
||||
- [ ] Required elements should have a **required indicator**
|
||||
- [ ] Required elements will be validated input
|
||||
- [x] Input Validation
|
||||
- [x] Required elements should have a **required indicator**
|
||||
- [x] Required elements will be validated input
|
||||
- [ ] **Valid:** Green outline or indicator* maybe a small, green check mark
|
||||
- [ ] **Invalid:** Red outline or indicator* maybe a red border with error text
|
||||
- [x] **Invalid:** Red outline or indicator* maybe a red border with error text
|
||||
|
||||
|
||||
'*': Not sure yet, still under consideration
|
||||
@ -187,13 +187,13 @@ creation process will take place here
|
||||
##### API Requirements
|
||||
|
||||
|
||||
- [ ] Middleware
|
||||
- [ ] **Authentication:** User must be logged in to access this page
|
||||
- [ ] **Authentication:** User must be logged in to submit the creation of a recipe (fallback)
|
||||
- [x] Middleware
|
||||
- [x] **Authentication:** User must be logged in to access this page
|
||||
- [x] **Authentication:** User must be logged in to submit the creation of a recipe (fallback)
|
||||
|
||||
- [ ] Recipe Creation Wizard
|
||||
- [ ] Create a new recipe object in the database
|
||||
- [ ] Recipe should be attached to a user (logged in)
|
||||
- [x] Recipe Creation Wizard
|
||||
- [x] Create a new recipe object in the database
|
||||
- [x] Recipe should be attached to a user (logged in)
|
||||
- [ ] User should be directed to the view recipe page on a successful creation
|
||||
|
||||
|
||||
|
||||
@ -31,6 +31,12 @@ func FavoritesPage(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
func CreatePage(ctx *gin.Context) {
|
||||
// If not logged in, direct to the login page
|
||||
if !domain.IsLoggedIn(ctx) {
|
||||
ctx.Redirect(http.StatusSeeOther, domain.WEB_LOGIN)
|
||||
return
|
||||
}
|
||||
|
||||
title := "Potion - Create"
|
||||
page := pages.CreatePage()
|
||||
|
||||
|
||||
@ -4,46 +4,27 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
// domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||
)
|
||||
|
||||
const CREATE_SUCCESS_HTML = `
|
||||
<p id="response" class="text-sm text-green-600 px-4 py-1 bg-green-100 rounded-full w-fit">
|
||||
Success! Your new masterpiece was created!
|
||||
</p>
|
||||
`
|
||||
|
||||
const CREATE_ERROR_HTML = `
|
||||
<p id="response" class="text-sm text-red-500 px-4 py-1 bg-red-100 rounded-full w-fit">
|
||||
Uh oh! Something went wrong when creating your recipe. Please try again. %s
|
||||
</p>
|
||||
`
|
||||
|
||||
func CreateRecipe(ctx *gin.Context) {
|
||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||
|
||||
title := ctx.PostForm("title")
|
||||
description := ctx.PostForm("description")
|
||||
preparation := ctx.PostForm("preparation-time")
|
||||
cook := ctx.PostForm("cook-time")
|
||||
serving := ctx.PostForm("serving-size")
|
||||
category := ctx.PostForm("category")
|
||||
difficulty := ctx.PostForm("difficulty")
|
||||
ingredients := ctx.PostFormArray("ingredients")
|
||||
quantity := ctx.PostFormArray("quantity")
|
||||
instructions := ctx.PostFormArray("instructions")
|
||||
tags := ctx.PostForm("tags") // this is a list of strings split with a comma (,)
|
||||
|
||||
// Have to get the image differently
|
||||
image, err := ctx.FormFile("image")
|
||||
deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||
_, err := deps.RecipeService.CreateRecipe(ctx)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusOK, gin.H{"error": err.Error()})
|
||||
ctx.String(http.StatusOK, CREATE_ERROR_HTML, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"title": title,
|
||||
"description": description,
|
||||
"cook time": cook,
|
||||
"preparation time": preparation,
|
||||
"serving size": serving,
|
||||
"category": category,
|
||||
"difficulty": difficulty,
|
||||
"ingredients": ingredients,
|
||||
"quantity": quantity,
|
||||
"instructions": instructions,
|
||||
"tags": tags,
|
||||
"image": image.Filename,
|
||||
})
|
||||
|
||||
// deps.RecipeService.CreateRecipe(ctx)
|
||||
// ctx.JSON(http.StatusCreated, gin.H{"recipe": recipe})
|
||||
ctx.String(http.StatusOK, CREATE_SUCCESS_HTML)
|
||||
}
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
domain "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
||||
domainServer "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||
)
|
||||
|
||||
// RecipeService implements the domain.RecipeService defined in the domain module.
|
||||
@ -22,45 +27,85 @@ func NewRecipeService(recipeRepository domain.RecipeRepository) domain.RecipeSer
|
||||
return &RecipeService{recipeRepository: recipeRepository}
|
||||
}
|
||||
|
||||
func (s *RecipeService) CreateRecipe(ctx *gin.Context) domain.Recipe {
|
||||
// TODO: Implement
|
||||
func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) {
|
||||
// Ensure user is logged in
|
||||
if !domainServer.IsLoggedIn(ctx) {
|
||||
return nil, fmt.Errorf("User is not logged in.")
|
||||
}
|
||||
|
||||
title := ctx.PostForm("title")
|
||||
description := ctx.PostForm("description")
|
||||
preparation := ctx.PostForm("preparation-time")
|
||||
cook := ctx.PostForm("cook-time")
|
||||
serving := ctx.PostForm("serving-size")
|
||||
category := ctx.PostForm("category")
|
||||
difficulty := ctx.PostForm("difficulty")
|
||||
ingredients := ctx.PostFormArray("ingredients")
|
||||
quantity := ctx.PostFormArray("quantity")
|
||||
instructions := ctx.PostFormArray("instructions")
|
||||
tags := strings.Split(ctx.PostForm("tags"), ",")
|
||||
userId := ctx.MustGet("userId").(int)
|
||||
|
||||
// Have to get the image differently
|
||||
image, err := ctx.FormFile("image")
|
||||
if err != nil && !errors.Is(err, http.ErrMissingFile) {
|
||||
// Error getting image
|
||||
}
|
||||
|
||||
// Convert to proper values
|
||||
servingInt, _ := strconv.Atoi(serving)
|
||||
difficultyInt, _ := strconv.Atoi(difficulty)
|
||||
prepInt, _ := strconv.Atoi(preparation)
|
||||
cookInt, _ := strconv.Atoi(cook)
|
||||
|
||||
var ingredientSlice []domain.RecipeIngredient
|
||||
for i := range len(ingredients) {
|
||||
if strings.TrimSpace(ingredients[i]) != "" {
|
||||
ins := domain.RecipeIngredient{
|
||||
Name: ingredients[i],
|
||||
Quantity: quantity[i],
|
||||
}
|
||||
|
||||
ingredientSlice = append(ingredientSlice, ins)
|
||||
}
|
||||
}
|
||||
|
||||
var instructionSlice []string
|
||||
for _, ins := range instructions {
|
||||
if ins != "" {
|
||||
instructionSlice = append(instructionSlice, ins)
|
||||
}
|
||||
}
|
||||
|
||||
// Create the recipe
|
||||
recipe := domain.Recipe{
|
||||
Title: "Delicious Go Curry",
|
||||
Description: "A savory and easy-to-make curry, perfect for weeknights.",
|
||||
Instructions: []string{
|
||||
"Chop all vegetables.",
|
||||
"Sauté onions and garlic until fragrant.",
|
||||
"Add curry paste and stir for 1 minute.",
|
||||
"Add coconut milk and vegetables, simmer until cooked.",
|
||||
"Serve with rice.",
|
||||
},
|
||||
Serves: 4,
|
||||
Difficulty: 3,
|
||||
Title: title,
|
||||
Description: description,
|
||||
Instructions: instructionSlice,
|
||||
Serves: servingInt,
|
||||
Difficulty: difficultyInt,
|
||||
Duration: domain.RecipeDuration{
|
||||
Total: 45,
|
||||
Prep: 15,
|
||||
Cook: 30,
|
||||
Total: prepInt + cookInt,
|
||||
Prep: prepInt,
|
||||
Cook: cookInt,
|
||||
},
|
||||
Category: domain.MealDinner, // Using our EMeal type. Ensure this matches an updated enum value.
|
||||
Ingredients: []domain.RecipeIngredient{
|
||||
{Name: "Onion", Quantity: "1 large"},
|
||||
{Name: "Garlic", Quantity: "3 cloves"},
|
||||
{Name: "Curry Paste", Quantity: "2 tbsp"},
|
||||
{Name: "Coconut Milk", Quantity: "400ml can"},
|
||||
{Name: "Broccoli", Quantity: "1 head"},
|
||||
{Name: "Bell Pepper", Quantity: "1 red"},
|
||||
{Name: "Rice", Quantity: "As needed"},
|
||||
},
|
||||
UserId: 3,
|
||||
Created: time.Now(),
|
||||
Category: domain.RecipeMeal(category),
|
||||
Ingredients: ingredientSlice,
|
||||
UserId: userId,
|
||||
Created: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.recipeRepository.CreateRecipe(&recipe); err != nil {
|
||||
ctx.JSON(http.StatusOK, gin.H{"err": err.Error()})
|
||||
return domain.Recipe{}
|
||||
return &recipe, err
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, gin.H{"recipe": recipe})
|
||||
return recipe
|
||||
// TODO: Upload the image
|
||||
if image != nil {
|
||||
}
|
||||
|
||||
// TODO: Create the tags in the database
|
||||
if len(tags) > 0 {
|
||||
}
|
||||
|
||||
return &recipe, nil
|
||||
}
|
||||
|
||||
@ -3,6 +3,5 @@ package domain
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
type RecipeService interface {
|
||||
CreateRecipe(ctx *gin.Context) Recipe
|
||||
CreateRecipe(ctx *gin.Context) (*Recipe, error)
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@ templ CreatePage() {
|
||||
|
||||
templ Page() {
|
||||
@components.BannerText("Create Your Masterpiece")
|
||||
<p id="output"></p>
|
||||
<div class="mx-4 md:mx-16 my-8">
|
||||
<p class="mb-8">
|
||||
Welcome to the Recipe Creation Wizard! Simply fill in the details about your culinary creation,
|
||||
@ -24,7 +23,13 @@ templ Page() {
|
||||
button to
|
||||
share your masterpiece!
|
||||
</p>
|
||||
<form>
|
||||
<form
|
||||
hx-post="/v1/api/recipe"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="#response"
|
||||
hx-trigger="submit"
|
||||
hx-encoding="multipart/form-data"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<label for="title" class="text-sm mb-2">
|
||||
Recipe Title
|
||||
@ -32,12 +37,17 @@ templ Page() {
|
||||
</label>
|
||||
<input
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
class="peer border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500
|
||||
focus:ring-2 duration-200 ease-in-out transition-all shadow-sm invalid:border-red-500"
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
required
|
||||
maxlength="128"
|
||||
minlength="1"
|
||||
placeholder="e.g., Classic Chicken Curry"
|
||||
/>
|
||||
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">Please enter a title. Between 1-128 characters.</p>
|
||||
</div>
|
||||
<div class="flex flex-col my-4">
|
||||
<label for="description" class="text-sm mb-2">
|
||||
@ -45,12 +55,19 @@ templ Page() {
|
||||
<span class="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all resize-none shadow-sm"
|
||||
class="peer border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500
|
||||
focus:ring-2 duration-200 ease-in-out transition-all resize-none shadow-sm invalid:border-red-500"
|
||||
id="description"
|
||||
name="description"
|
||||
rows="4"
|
||||
required
|
||||
maxlength="1024"
|
||||
minlength="1"
|
||||
placeholder="A brief description of your delicious recipe..."
|
||||
></textarea>
|
||||
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">
|
||||
Please enter a description. Between 1-1000 characters.
|
||||
</p>
|
||||
</div>
|
||||
<div class="my-4 flex flex-col gap-x-2">
|
||||
<div class="flex flex-col flex-grow">
|
||||
@ -84,12 +101,19 @@ templ Page() {
|
||||
</label>
|
||||
<input
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
class="peer border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500
|
||||
focus:ring-2 duration-200 ease-in-out transition-all shadow-sm invalid:border-red-500"
|
||||
type="number"
|
||||
id="preparation-time"
|
||||
name="preparation-time"
|
||||
required
|
||||
min="0"
|
||||
max="120"
|
||||
placeholder="e.g., 20"
|
||||
/>
|
||||
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">
|
||||
Please enter a time (minutes).
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col flex-grow w-1/3">
|
||||
<label for="cook-time" class="text-sm mb-2">
|
||||
@ -98,12 +122,19 @@ templ Page() {
|
||||
</label>
|
||||
<input
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
class="peer border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500
|
||||
focus:ring-2 duration-200 ease-in-out transition-all shadow-sm invalid:border-red-500"
|
||||
type="number"
|
||||
id="cook-time"
|
||||
name="cook-time"
|
||||
required
|
||||
min="0"
|
||||
max="120"
|
||||
placeholder="e.g., 45"
|
||||
/>
|
||||
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">
|
||||
Please enter a time (minutes).
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col flex-grow w-1/3">
|
||||
<label for="serving-size" class="text-sm mb-2">
|
||||
@ -112,14 +143,19 @@ templ Page() {
|
||||
</label>
|
||||
<input
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
class="peer border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500
|
||||
focus:ring-2 duration-200 ease-in-out transition-all shadow-sm invalid:border-red-500"
|
||||
type="number"
|
||||
max="16"
|
||||
min="1"
|
||||
required
|
||||
id="serving-size"
|
||||
name="serving-size"
|
||||
placeholder="e.g., 4"
|
||||
/>
|
||||
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">
|
||||
Please enter a serving size.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-4 flex gap-x-2">
|
||||
@ -131,7 +167,10 @@ templ Page() {
|
||||
<select
|
||||
id="category"
|
||||
name="category"
|
||||
class="border border-gray-300 bg-gray-200 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
required
|
||||
class="peer border border-gray-300 bg-gray-200 px-4 py-2 rounded-lg focus:outline-none
|
||||
focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm
|
||||
invalid:border-red-500"
|
||||
>
|
||||
<option value="">Select a category</option>
|
||||
<option value="breakfast">Breakfast</option>
|
||||
@ -142,6 +181,9 @@ templ Page() {
|
||||
<option value="side">Side</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">
|
||||
Please select a category.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col flex-grow w-1/3">
|
||||
<label for="difficulty" class="text-sm mb-2">
|
||||
@ -151,7 +193,10 @@ templ Page() {
|
||||
<select
|
||||
id="difficulty"
|
||||
name="difficulty"
|
||||
class="border border-gray-300 bg-gray-200 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
required
|
||||
class="peer border border-gray-300 bg-gray-200 px-4 py-2 rounded-lg focus:outline-none
|
||||
focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm
|
||||
invalid:border-red-500"
|
||||
>
|
||||
<option value="">Select a difficulty</option>
|
||||
<option value="1">Beginner</option>
|
||||
@ -160,6 +205,9 @@ templ Page() {
|
||||
<option value="4">Challenging</option>
|
||||
<option value="5">Extreme</option>
|
||||
</select>
|
||||
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">
|
||||
Please select a difficulty.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col my-4">
|
||||
@ -169,22 +217,40 @@ templ Page() {
|
||||
</label>
|
||||
<div id="ingredient-list">
|
||||
<div class="w-full flex gap-x-2 py-2">
|
||||
<input
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="flex-grow border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
type="text"
|
||||
id="ingredients"
|
||||
name="ingredients"
|
||||
placeholder="Ingredient name (e.g., Chicken Breast)"
|
||||
/>
|
||||
<input
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="w-1/3 border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||
type="text"
|
||||
id="quantity"
|
||||
name="quantity"
|
||||
placeholder="Quantity (e.g., 1lb)"
|
||||
/>
|
||||
<div class="flex-grow">
|
||||
<input
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="peer w-full border border-gray-300 px-4 py-2 rounded-lg focus:outline-none
|
||||
focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm
|
||||
invalid:border-red-500"
|
||||
type="text"
|
||||
id="ingredients"
|
||||
name="ingredients"
|
||||
required
|
||||
minlength="1"
|
||||
placeholder="Ingredient name (e.g., Chicken Breast)"
|
||||
/>
|
||||
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">
|
||||
Please enter at least one ingredient.
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-1/3">
|
||||
<input
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="peer w-full border border-gray-300 px-4 py-2 rounded-lg focus:outline-none
|
||||
focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm
|
||||
invalid:border-red-500"
|
||||
type="text"
|
||||
id="quantity"
|
||||
name="quantity"
|
||||
required
|
||||
minlength="1"
|
||||
placeholder="Quantity (e.g., 1lb)"
|
||||
/>
|
||||
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">
|
||||
Please provide a quantity.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@ -202,12 +268,19 @@ templ Page() {
|
||||
</label>
|
||||
<div id="instruction-list" class="flex flex-col">
|
||||
<textarea
|
||||
class="border border-gray-300 my-2 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all resize-none shadow-sm"
|
||||
class="peer border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500
|
||||
focus:ring-2 duration-200 ease-in-out transition-all resize-none shadow-sm invalid:border-red-500
|
||||
valid:my-2 invalid:mt-2"
|
||||
id="instructions"
|
||||
name="instructions"
|
||||
rows="3"
|
||||
required
|
||||
minlength="1"
|
||||
placeholder="Step 1: Describe this step..."
|
||||
></textarea>
|
||||
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">
|
||||
Please enter at least one step.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@ -229,14 +302,9 @@ templ Page() {
|
||||
class="my-2 block w-full text-sm text-placeholder file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:bg-blue-100 file:text-blue-700 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<!-- <form hx-post="/v1/api/recipe" hx-swap="innerHTML" hx-target="#output" hx-trigger="submit"> -->
|
||||
<p id="response" class="hidden"></p>
|
||||
<button
|
||||
type="submit"
|
||||
hx-post="/v1/api/recipe"
|
||||
hx-swap="innerHTML"
|
||||
hx-target="#output"
|
||||
hx-trigger="click"
|
||||
hx-encoding="multipart/form-data"
|
||||
class="w-full mt-8 bg-gradient-to-r from-blue-200 to-purple-200 py-2 rounded-lg text-lg cursor-pointer shadow-md"
|
||||
>
|
||||
Create Recipe
|
||||
@ -271,7 +339,7 @@ templ Page() {
|
||||
|
||||
function addInstruction() {
|
||||
const list = document.getElementById("instruction-list");
|
||||
const itemNum = list.children.length + 1;
|
||||
const itemNum = list.querySelectorAll("textarea").length + 1;
|
||||
const item = document.createElement("textarea");
|
||||
item.id = "instructions";
|
||||
item.name = "instructions";
|
||||
|
||||
File diff suppressed because one or more lines are too long
7
internal/templates/pages/recipe.templ
Normal file
7
internal/templates/pages/recipe.templ
Normal file
@ -0,0 +1,7 @@
|
||||
package templates
|
||||
|
||||
templ RecipePage() {
|
||||
<p>
|
||||
Viewing page
|
||||
</p>
|
||||
}
|
||||
40
internal/templates/pages/recipe_templ.go
Normal file
40
internal/templates/pages/recipe_templ.go
Normal file
@ -0,0 +1,40 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.865
|
||||
package templates
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func RecipePage() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<p>Viewing page</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@ -9,6 +9,10 @@
|
||||
monospace;
|
||||
--color-red-100: oklch(93.6% 0.032 17.717);
|
||||
--color-red-500: oklch(63.7% 0.237 25.331);
|
||||
--color-green-100: oklch(96.2% 0.044 156.743);
|
||||
--color-green-500: oklch(72.3% 0.219 149.579);
|
||||
--color-green-600: oklch(62.7% 0.194 149.214);
|
||||
--color-green-700: oklch(52.7% 0.154 150.069);
|
||||
--color-blue-100: oklch(93.2% 0.032 255.585);
|
||||
--color-blue-200: oklch(88.2% 0.059 254.128);
|
||||
--color-blue-300: oklch(80.9% 0.105 251.813);
|
||||
@ -215,6 +219,9 @@
|
||||
.static {
|
||||
position: static;
|
||||
}
|
||||
.top-1 {
|
||||
top: calc(var(--spacing) * 1);
|
||||
}
|
||||
.top-1\/2 {
|
||||
top: calc(1/2 * 100%);
|
||||
}
|
||||
@ -224,6 +231,9 @@
|
||||
.left-0 {
|
||||
left: calc(var(--spacing) * 0);
|
||||
}
|
||||
.left-1 {
|
||||
left: calc(var(--spacing) * 1);
|
||||
}
|
||||
.left-1\/2 {
|
||||
left: calc(1/2 * 100%);
|
||||
}
|
||||
@ -242,6 +252,9 @@
|
||||
.mx-4 {
|
||||
margin-inline: calc(var(--spacing) * 4);
|
||||
}
|
||||
.my-1 {
|
||||
margin-block: calc(var(--spacing) * 1);
|
||||
}
|
||||
.my-2 {
|
||||
margin-block: calc(var(--spacing) * 2);
|
||||
}
|
||||
@ -338,9 +351,15 @@
|
||||
.h-screen {
|
||||
height: 100vh;
|
||||
}
|
||||
.w-1 {
|
||||
width: calc(var(--spacing) * 1);
|
||||
}
|
||||
.w-1\/3 {
|
||||
width: calc(1/3 * 100%);
|
||||
}
|
||||
.w-3 {
|
||||
width: calc(var(--spacing) * 3);
|
||||
}
|
||||
.w-3\/4 {
|
||||
width: calc(3/4 * 100%);
|
||||
}
|
||||
@ -353,6 +372,9 @@
|
||||
.w-5 {
|
||||
width: calc(var(--spacing) * 5);
|
||||
}
|
||||
.w-9 {
|
||||
width: calc(var(--spacing) * 9);
|
||||
}
|
||||
.w-9\/10 {
|
||||
width: calc(9/10 * 100%);
|
||||
}
|
||||
@ -374,16 +396,30 @@
|
||||
.max-w-xl {
|
||||
max-width: var(--container-xl);
|
||||
}
|
||||
.flex-shrink {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.flex-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.border-collapse {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.-translate-x-1 {
|
||||
--tw-translate-x: calc(var(--spacing) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-x-1\/2 {
|
||||
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1 {
|
||||
--tw-translate-y: calc(var(--spacing) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1\/2 {
|
||||
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
@ -391,6 +427,9 @@
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.resize {
|
||||
resize: both;
|
||||
}
|
||||
.resize-none {
|
||||
resize: none;
|
||||
}
|
||||
@ -509,6 +548,12 @@
|
||||
.bg-gray-200 {
|
||||
background-color: var(--color-gray-200);
|
||||
}
|
||||
.bg-green-100 {
|
||||
background-color: var(--color-green-100);
|
||||
}
|
||||
.bg-red-100 {
|
||||
background-color: var(--color-red-100);
|
||||
}
|
||||
.bg-white {
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
@ -671,6 +716,15 @@
|
||||
.text-gray-800 {
|
||||
color: var(--color-gray-800);
|
||||
}
|
||||
.text-green-500 {
|
||||
color: var(--color-green-500);
|
||||
}
|
||||
.text-green-600 {
|
||||
color: var(--color-green-600);
|
||||
}
|
||||
.text-green-700 {
|
||||
color: var(--color-green-700);
|
||||
}
|
||||
.text-red-500 {
|
||||
color: var(--color-red-500);
|
||||
}
|
||||
@ -680,6 +734,9 @@
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.shadow {
|
||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
@ -758,6 +815,11 @@
|
||||
.\[-webkit-line-clamp\:4\] {
|
||||
-webkit-line-clamp: 4;
|
||||
}
|
||||
.peer-invalid\:block {
|
||||
&:is(:where(.peer):invalid ~ *) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.file\:mr-4 {
|
||||
&::file-selector-button {
|
||||
margin-right: calc(var(--spacing) * 4);
|
||||
@ -800,6 +862,26 @@
|
||||
color: var(--color-blue-700);
|
||||
}
|
||||
}
|
||||
.valid\:my-2 {
|
||||
&:valid {
|
||||
margin-block: calc(var(--spacing) * 2);
|
||||
}
|
||||
}
|
||||
.valid\:border-gray-300 {
|
||||
&:valid {
|
||||
border-color: var(--color-gray-300);
|
||||
}
|
||||
}
|
||||
.invalid\:mt-2 {
|
||||
&:invalid {
|
||||
margin-top: calc(var(--spacing) * 2);
|
||||
}
|
||||
}
|
||||
.invalid\:border-red-500 {
|
||||
&:invalid {
|
||||
border-color: var(--color-red-500);
|
||||
}
|
||||
}
|
||||
.hover\:cursor-pointer {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
@ -1054,6 +1136,16 @@
|
||||
width: calc(2/7 * 100%);
|
||||
}
|
||||
}
|
||||
.\[\&\:not\(\:placeholder-shown\)\:invalid\]\:border-red-500 {
|
||||
&:not(:placeholder-shown):invalid {
|
||||
border-color: var(--color-red-500);
|
||||
}
|
||||
}
|
||||
.\[\&\:not\(\:placeholder-shown\)\:valid\]\:border-green-500 {
|
||||
&:not(:placeholder-shown):valid {
|
||||
border-color: var(--color-green-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@property --tw-translate-x {
|
||||
syntax: "*";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user