617 lines
17 KiB
Go
617 lines
17 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}}/edit"
|
|
hx-target="#edit-modal"
|
|
hx-swap="innerHTML"
|
|
class="edit-btn">
|
|
Edit
|
|
</button>
|
|
<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>
|
|
|
|
<!-- Edit Modal -->
|
|
<div id="edit-modal"></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}}/edit"
|
|
hx-target="#edit-modal"
|
|
hx-swap="innerHTML"
|
|
class="edit-btn">
|
|
Edit
|
|
</button>
|
|
<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)
|
|
}
|
|
|
|
// GetEditMealHandler shows the edit modal for a meal
|
|
func GetEditMealHandler(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
|
|
}
|
|
|
|
// Get meal with security check
|
|
meal, err := database.GetMealByID(userID, mealID)
|
|
if err != nil {
|
|
http.Error(w, "Meal not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
tmpl := `
|
|
<div class="modal-overlay" id="modal-overlay">
|
|
<div class="modal-content">
|
|
<h3>Edit Meal</h3>
|
|
<form hx-post="/meals/{{.ID}}/update"
|
|
hx-target="#meal-{{.ID}}"
|
|
hx-swap="outerHTML"
|
|
class="edit-form">
|
|
<div class="form-group">
|
|
<label>Name:</label>
|
|
<input type="text" name="name" value="{{.Name}}" required />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Description:</label>
|
|
<input type="text" name="description" value="{{.Description}}" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Type:</label>
|
|
<select name="meal_type" required>
|
|
<option value="breakfast" {{if eq .MealType "breakfast"}}selected{{end}}>Breakfast</option>
|
|
<option value="lunch" {{if eq .MealType "lunch"}}selected{{end}}>Lunch</option>
|
|
<option value="snack" {{if eq .MealType "snack"}}selected{{end}}>Snack</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Prep Time (minutes):</label>
|
|
<input type="number" name="prep_time" value="{{.PrepTime}}" min="0" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Image URL:</label>
|
|
<input type="url" name="image_url" value="{{.ImageURL}}" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Instructions:</label>
|
|
<textarea name="instructions" rows="5">{{.Instructions}}</textarea>
|
|
</div>
|
|
<div class="modal-buttons">
|
|
<button type="submit" class="btn-primary">Save</button>
|
|
<button type="button" onclick="closeModal()" class="btn-secondary">Cancel</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
function closeModal() {
|
|
document.getElementById('edit-modal').innerHTML = '';
|
|
}
|
|
document.getElementById('modal-overlay').addEventListener('click', function(e) {
|
|
if (e.target.id === 'modal-overlay') {
|
|
closeModal();
|
|
}
|
|
});
|
|
</script>
|
|
`
|
|
|
|
t := template.Must(template.New("editMeal").Parse(tmpl))
|
|
t.Execute(w, meal)
|
|
}
|
|
|
|
// UpdateMealHandler updates a meal
|
|
func UpdateMealHandler(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
|
|
}
|
|
|
|
// Verify ownership
|
|
_, err = database.GetMealByID(userID, mealID)
|
|
if err != nil {
|
|
http.Error(w, "Meal not found or access denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Update meal
|
|
_, err = database.DB.Exec(
|
|
"UPDATE meals SET name = ?, description = ?, meal_type = ?, instructions = ?, prep_time = ?, image_url = ? WHERE id = ? AND user_id = ?",
|
|
name, description, mealType, instructions, prepTime, imageURL, mealID, userID,
|
|
)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Return updated meal card
|
|
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}}/edit"
|
|
hx-target="#edit-modal"
|
|
hx-swap="innerHTML"
|
|
class="edit-btn">
|
|
Edit
|
|
</button>
|
|
<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>
|
|
<script>
|
|
document.getElementById('edit-modal').innerHTML = '';
|
|
</script>
|
|
`
|
|
|
|
data := struct {
|
|
ID int
|
|
Name string
|
|
Description string
|
|
MealType string
|
|
Instructions string
|
|
PrepTime int
|
|
ImageURL string
|
|
}{mealID, name, description, mealType, instructions, prepTime, imageURL}
|
|
|
|
t := template.Must(template.New("updatedMeal").Parse(tmpl))
|
|
t.Execute(w, data)
|
|
}
|