diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go index 71368e0..99a2bca 100644 --- a/internal/app/handlers/page_handler.go +++ b/internal/app/handlers/page_handler.go @@ -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() diff --git a/internal/app/handlers/recipe_handler.go b/internal/app/handlers/recipe_handler.go index ee12ece..fa736dc 100644 --- a/internal/app/handlers/recipe_handler.go +++ b/internal/app/handlers/recipe_handler.go @@ -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 = ` +

+ Success! Your new masterpiece was created! +

+` + +const CREATE_ERROR_HTML = ` +

+ Uh oh! Something went wrong when creating your recipe. Please try again. %s +

+` + 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) } diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go index dcfa557..b0d302b 100644 --- a/internal/app/service/recipe_service.go +++ b/internal/app/service/recipe_service.go @@ -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 } diff --git a/internal/domain/recipe/service.go b/internal/domain/recipe/service.go index f11282e..c05649d 100644 --- a/internal/domain/recipe/service.go +++ b/internal/domain/recipe/service.go @@ -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) } - diff --git a/internal/templates/pages/create.templ b/internal/templates/pages/create.templ index 73edb4b..25e874c 100644 --- a/internal/templates/pages/create.templ +++ b/internal/templates/pages/create.templ @@ -13,7 +13,6 @@ templ CreatePage() { templ Page() { @components.BannerText("Create Your Masterpiece") -

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!

-
+
+
+
@@ -84,12 +101,19 @@ templ Page() { +
+
+
@@ -131,7 +167,10 @@ templ Page() { +
@@ -169,22 +217,40 @@ templ Page() {
- - +
+ + +
+
+ + +
- +
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

Welcome to the Recipe Creation Wizard! Simply fill in the details about your culinary creation, including the recipe's name, a description, and other specifics like its category, duration, and difficulty. Don't forget to dynamically add all your ingredients and instructions using the dedicated buttons, and feel free to upload an appealing image. All required fields are marked with an *. Once everything looks perfect, just hit the \"Create Recipe\" button to share your masterpiece!

Please enter a title. Between 1-128 characters.

Please enter a description. Between 1-1000 characters.

    Please enter a time (minutes).

    Please enter a time (minutes).

    Please enter a serving size.

    Please select a category.

    Please select a difficulty.

    Please enter at least one ingredient.

    Please provide a quantity.

    Please enter at least one step.

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/web/static/css/tailwind.css b/web/static/css/tailwind.css index 8c1bf62..f2a784d 100644 --- a/web/static/css/tailwind.css +++ b/web/static/css/tailwind.css @@ -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: "*";