Files
meal-prep-vibecoded/handlers/meals.go
2025-10-25 16:06:42 +02:00

389 lines
10 KiB
Go

package handlers
import (
"html/template"
"mealprep/auth"
"mealprep/database"
"net/http"
"strconv"
"strings"
)
// MealsHandler handles the meals page
func MealsHandler(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r)
meals, err := database.GetAllMeals(userID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl := `
<div id="meals-content">
<h2>Meals</h2>
<form hx-post="/meals" hx-target="#meals-list" hx-swap="beforeend" class="add-form">
<input type="text" name="name" placeholder="Meal name" required />
<input type="text" name="description" placeholder="Description" />
<select name="meal_type" required>
<option value="">Select type...</option>
<option value="breakfast">Breakfast</option>
<option value="lunch">Lunch</option>
<option value="snack">Snack</option>
</select>
<input type="number" name="prep_time" placeholder="Prep time (minutes)" min="0" />
<input type="url" name="image_url" placeholder="Image URL (optional)" />
<textarea name="instructions" placeholder="Instructions (optional)" rows="3"></textarea>
<button type="submit">Add Meal</button>
</form>
<div id="meals-list" class="items-list">
{{range .}}
<div class="meal-item" id="meal-{{.ID}}">
<div class="meal-header">
{{if .ImageURL}}
<img src="{{.ImageURL}}" alt="{{.Name}}" class="meal-image" />
{{end}}
<div class="meal-info-section">
<div>
<span class="item-name">{{.Name}}</span>
<span class="meal-type-tag tag-{{.MealType}}">{{.MealType}}</span>
{{if gt .PrepTime 0}}
<span class="prep-time">⏱️ {{.PrepTime}} min</span>
{{end}}
</div>
<span class="item-description">{{.Description}}</span>
{{if .Instructions}}
<details class="instructions-preview">
<summary>Instructions</summary>
<p class="instructions-text">{{.Instructions}}</p>
</details>
{{end}}
</div>
<div>
<button
hx-get="/meals/{{.ID}}/ingredients"
hx-target="#meal-{{.ID}}-ingredients"
hx-swap="innerHTML"
class="view-btn">
View Ingredients
</button>
<button
hx-delete="/meals/{{.ID}}"
hx-target="#meal-{{.ID}}"
hx-swap="outerHTML"
hx-confirm="Are you sure you want to delete this meal?"
class="delete-btn">
Delete
</button>
</div>
</div>
<div id="meal-{{.ID}}-ingredients" class="meal-ingredients"></div>
</div>
{{end}}
</div>
</div>
`
t := template.Must(template.New("meals").Parse(tmpl))
t.Execute(w, meals)
}
// AddMealHandler handles adding a new meal
func AddMealHandler(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r)
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
name := strings.TrimSpace(r.FormValue("name"))
description := strings.TrimSpace(r.FormValue("description"))
mealType := r.FormValue("meal_type")
instructions := strings.TrimSpace(r.FormValue("instructions"))
imageURL := strings.TrimSpace(r.FormValue("image_url"))
prepTime := 0
if prepTimeStr := r.FormValue("prep_time"); prepTimeStr != "" {
prepTime, _ = strconv.Atoi(prepTimeStr)
}
if name == "" || mealType == "" {
http.Error(w, "Name and meal type are required", http.StatusBadRequest)
return
}
// Validate meal type
if mealType != "breakfast" && mealType != "lunch" && mealType != "snack" {
http.Error(w, "Invalid meal type", http.StatusBadRequest)
return
}
id, err := database.AddMeal(userID, name, description, mealType, instructions, imageURL, prepTime)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl := `
<div class="meal-item" id="meal-{{.ID}}">
<div class="meal-header">
{{if .ImageURL}}
<img src="{{.ImageURL}}" alt="{{.Name}}" class="meal-image" />
{{end}}
<div class="meal-info-section">
<div>
<span class="item-name">{{.Name}}</span>
<span class="meal-type-tag tag-{{.MealType}}">{{.MealType}}</span>
{{if gt .PrepTime 0}}
<span class="prep-time">⏱️ {{.PrepTime}} min</span>
{{end}}
</div>
<span class="item-description">{{.Description}}</span>
{{if .Instructions}}
<details class="instructions-preview">
<summary>Instructions</summary>
<p class="instructions-text">{{.Instructions}}</p>
</details>
{{end}}
</div>
<div>
<button
hx-get="/meals/{{.ID}}/ingredients"
hx-target="#meal-{{.ID}}-ingredients"
hx-swap="innerHTML"
class="view-btn">
View Ingredients
</button>
<button
hx-delete="/meals/{{.ID}}"
hx-target="#meal-{{.ID}}"
hx-swap="outerHTML"
hx-confirm="Are you sure you want to delete this meal?"
class="delete-btn">
Delete
</button>
</div>
</div>
<div id="meal-{{.ID}}-ingredients" class="meal-ingredients"></div>
</div>
`
data := struct {
ID int64
Name string
Description string
MealType string
Instructions string
PrepTime int
ImageURL string
}{id, name, description, mealType, instructions, prepTime, imageURL}
t := template.Must(template.New("meal").Parse(tmpl))
t.Execute(w, data)
}
// DeleteMealHandler handles deleting a meal
func DeleteMealHandler(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r)
idStr := strings.TrimPrefix(r.URL.Path, "/meals/")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
if err := database.DeleteMeal(userID, id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
// GetMealIngredientsHandler shows ingredients for a specific meal
func GetMealIngredientsHandler(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r)
parts := strings.Split(r.URL.Path, "/")
if len(parts) < 3 {
http.Error(w, "Invalid URL", http.StatusBadRequest)
return
}
mealID, err := strconv.Atoi(parts[2])
if err != nil {
http.Error(w, "Invalid meal ID", http.StatusBadRequest)
return
}
mealIngredients, err := database.GetMealIngredients(userID, mealID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
allIngredients, err := database.GetAllIngredients(userID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl := `
<div class="ingredients-section">
<h4>Ingredients:</h4>
<form hx-post="/meals/{{.MealID}}/ingredients"
hx-target="#meal-{{.MealID}}-ingredients-list"
hx-swap="beforeend"
class="add-ingredient-form">
<select name="ingredient_id" required>
<option value="">Select ingredient</option>
{{range .AllIngredients}}
<option value="{{.ID}}">{{.Name}} ({{.Unit}})</option>
{{end}}
</select>
<input type="number" name="quantity" placeholder="Quantity" step="0.01" min="0" required />
<button type="submit">Add</button>
</form>
<div id="meal-{{.MealID}}-ingredients-list" class="sub-items-list">
{{range .MealIngredients}}
<div class="sub-item" id="meal-{{.MealID}}-ingredient-{{.IngredientID}}">
<span>{{.IngredientName}}: {{.Quantity}} {{.Unit}}</span>
<button
hx-delete="/meals/{{.MealID}}/ingredients/{{.IngredientID}}"
hx-target="#meal-{{.MealID}}-ingredient-{{.IngredientID}}"
hx-swap="outerHTML"
class="delete-btn small">
Remove
</button>
</div>
{{end}}
</div>
</div>
`
data := struct {
MealID int
MealIngredients interface{}
AllIngredients interface{}
}{
MealID: mealID,
MealIngredients: mealIngredients,
AllIngredients: allIngredients,
}
t := template.Must(template.New("mealIngredients").Parse(tmpl))
t.Execute(w, data)
}
// AddMealIngredientHandler adds an ingredient to a meal
func AddMealIngredientHandler(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r)
parts := strings.Split(r.URL.Path, "/")
if len(parts) < 3 {
http.Error(w, "Invalid URL", http.StatusBadRequest)
return
}
mealID, err := strconv.Atoi(parts[2])
if err != nil {
http.Error(w, "Invalid meal ID", http.StatusBadRequest)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ingredientID, err := strconv.Atoi(r.FormValue("ingredient_id"))
if err != nil {
http.Error(w, "Invalid ingredient ID", http.StatusBadRequest)
return
}
quantity, err := strconv.ParseFloat(r.FormValue("quantity"), 64)
if err != nil {
http.Error(w, "Invalid quantity", http.StatusBadRequest)
return
}
if err := database.AddMealIngredient(userID, mealID, ingredientID, quantity); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Get the ingredient details to display
ingredients, err := database.GetMealIngredients(userID, mealID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Find the ingredient we just added
var addedIngredient *interface{}
for _, ing := range ingredients {
if ing.IngredientID == ingredientID {
var temp interface{} = ing
addedIngredient = &temp
break
}
}
if addedIngredient == nil {
http.Error(w, "Could not find added ingredient", http.StatusInternalServerError)
return
}
tmpl := `
<div class="sub-item" id="meal-{{.MealID}}-ingredient-{{.IngredientID}}">
<span>{{.IngredientName}}: {{.Quantity}} {{.Unit}}</span>
<button
hx-delete="/meals/{{.MealID}}/ingredients/{{.IngredientID}}"
hx-target="#meal-{{.MealID}}-ingredient-{{.IngredientID}}"
hx-swap="outerHTML"
class="delete-btn small">
Remove
</button>
</div>
`
t := template.Must(template.New("mealIngredient").Parse(tmpl))
t.Execute(w, *addedIngredient)
}
// DeleteMealIngredientHandler removes an ingredient from a meal
func DeleteMealIngredientHandler(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r)
parts := strings.Split(r.URL.Path, "/")
if len(parts) < 5 {
http.Error(w, "Invalid URL", http.StatusBadRequest)
return
}
mealID, err := strconv.Atoi(parts[2])
if err != nil {
http.Error(w, "Invalid meal ID", http.StatusBadRequest)
return
}
ingredientID, err := strconv.Atoi(parts[4])
if err != nil {
http.Error(w, "Invalid ingredient ID", http.StatusBadRequest)
return
}
if err := database.DeleteMealIngredient(userID, mealID, ingredientID); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}