Files
meal-prep-vibecoded/handlers/ingredients.go

516 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handlers
import (
"html/template"
"mealprep/auth"
"mealprep/database"
"net/http"
"strconv"
"strings"
)
// IngredientsHandler handles the ingredients page
func IngredientsHandler(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r)
// Check if there's a search query
searchTags := r.URL.Query().Get("tags")
var ingredients []interface{}
var err error
if searchTags != "" {
tagNames := strings.Split(searchTags, ",")
for i := range tagNames {
tagNames[i] = strings.TrimSpace(tagNames[i])
}
ings, err := database.SearchIngredientsByTags(userID, tagNames)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, ing := range ings {
ingredients = append(ingredients, ing)
}
} else {
ings, err := database.GetIngredientsWithTags(userID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, ing := range ings {
ingredients = append(ingredients, ing)
}
}
// Get all available tags for the filter UI (only tags that are in use)
allTags, err := database.GetUsedTags(userID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl := `
<div id="ingredients-content">
<h2>Ingredients</h2>
<form hx-post="/ingredients" hx-target="#ingredients-list" hx-swap="beforeend" class="add-form">
<input type="text" name="name" placeholder="Ingredient name" required />
<input type="text" name="unit" placeholder="Unit (e.g., grams, cups)" required />
<input type="text" name="tags" placeholder="Tags (comma-separated)" />
<button type="submit">Add Ingredient</button>
</form>
<div class="search-section">
<h3>Filter by Tags</h3>
<div class="tag-filter">
{{range .AllTags}}
<button class="tag-filter-btn"
onclick="toggleTagFilter('{{.Name}}')">
{{.Name}}
</button>
{{end}}
</div>
<div id="active-filters" class="active-filters"></div>
<button onclick="clearFilters()" class="clear-filters-btn">Clear Filters</button>
</div>
<div id="ingredients-list" class="items-list">
{{range .Ingredients}}
<div class="item" id="ingredient-{{.ID}}">
<div class="item-content">
<span class="item-name">{{.Name}}</span>
<span class="item-unit">({{.Unit}})</span>
<div class="item-tags">
{{range .Tags}}
<span class="tag">{{.Name}}</span>
{{end}}
</div>
</div>
<div class="item-actions">
<button
hx-get="/ingredients/{{.ID}}/tags"
hx-target="#tag-modal-content"
hx-swap="innerHTML"
onclick="document.getElementById('tag-modal').style.display='block'"
class="edit-tags-btn">
Edit Tags
</button>
<button
hx-delete="/ingredients/{{.ID}}"
hx-target="#ingredient-{{.ID}}"
hx-swap="outerHTML"
hx-confirm="Are you sure you want to delete this ingredient?"
class="delete-btn">
Delete
</button>
</div>
</div>
{{end}}
</div>
</div>
<!-- Tag Edit Modal -->
<div id="tag-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="document.getElementById('tag-modal').style.display='none'">&times;</span>
<div id="tag-modal-content">
<!-- Content loaded via HTMX -->
</div>
</div>
</div>
<script>
let activeFilters = new Set();
// Listen for tag changes and refresh ingredients
document.body.addEventListener('refreshIngredients', function() {
// Close the modal first
document.getElementById('tag-modal').style.display='none';
// Then refresh the page
htmx.ajax('GET', '/ingredients', {target: '#ingredients-content', swap: 'innerHTML'});
});
function toggleTagFilter(tagName) {
if (activeFilters.has(tagName)) {
activeFilters.delete(tagName);
} else {
activeFilters.add(tagName);
}
updateFilters();
}
function clearFilters() {
activeFilters.clear();
updateFilters();
}
function updateFilters() {
// Update active filters display
const activeFiltersDiv = document.getElementById('active-filters');
if (activeFilters.size > 0) {
activeFiltersDiv.innerHTML = 'Active filters: ' + Array.from(activeFilters).join(', ');
} else {
activeFiltersDiv.innerHTML = '';
}
// Update filter button states
document.querySelectorAll('.tag-filter-btn').forEach(btn => {
if (activeFilters.has(btn.textContent.trim())) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
// Reload ingredients with filters
const url = activeFilters.size > 0
? '/ingredients?tags=' + Array.from(activeFilters).join(',')
: '/ingredients';
htmx.ajax('GET', url, {target: '#ingredients-content', swap: 'innerHTML'});
}
</script>
`
data := struct {
Ingredients []interface{}
AllTags []interface{}
}{
Ingredients: ingredients,
AllTags: []interface{}{},
}
for _, tag := range allTags {
data.AllTags = append(data.AllTags, tag)
}
t := template.Must(template.New("ingredients").Parse(tmpl))
t.Execute(w, data)
}
// AddIngredientHandler handles adding a new ingredient
func AddIngredientHandler(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"))
unit := strings.TrimSpace(r.FormValue("unit"))
tagsStr := strings.TrimSpace(r.FormValue("tags"))
if name == "" || unit == "" {
http.Error(w, "Name and unit are required", http.StatusBadRequest)
return
}
id, err := database.AddIngredient(userID, name, unit)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Process tags if provided
var tags []string
if tagsStr != "" {
tagNames := strings.Split(tagsStr, ",")
for _, tagName := range tagNames {
tagName = strings.TrimSpace(tagName)
if tagName != "" {
// Get or create tag
tagID, err := database.GetOrCreateTag(userID, tagName)
if err != nil {
continue // Skip this tag on error
}
// Link tag to ingredient
database.AddIngredientTag(userID, int(id), int(tagID))
tags = append(tags, tagName)
}
}
}
tmpl := `
<div class="item" id="ingredient-{{.ID}}">
<div class="item-content">
<span class="item-name">{{.Name}}</span>
<span class="item-unit">({{.Unit}})</span>
<div class="item-tags">
{{range .Tags}}
<span class="tag">{{.}}</span>
{{end}}
</div>
</div>
<div class="item-actions">
<button
hx-get="/ingredients/{{.ID}}/tags"
hx-target="#tag-modal-content"
hx-swap="innerHTML"
onclick="document.getElementById('tag-modal').style.display='block'"
class="edit-tags-btn">
Edit Tags
</button>
<button
hx-delete="/ingredients/{{.ID}}"
hx-target="#ingredient-{{.ID}}"
hx-swap="outerHTML"
hx-confirm="Are you sure you want to delete this ingredient?"
class="delete-btn">
Delete
</button>
</div>
</div>
`
data := struct {
ID int64
Name string
Unit string
Tags []string
}{id, name, unit, tags}
t := template.Must(template.New("ingredient").Parse(tmpl))
t.Execute(w, data)
}
// DeleteIngredientHandler handles deleting an ingredient
func DeleteIngredientHandler(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r)
idStr := strings.TrimPrefix(r.URL.Path, "/ingredients/")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
if err := database.DeleteIngredient(userID, id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
// GetIngredientTagsHandler returns the tag editing UI for an ingredient
func GetIngredientTagsHandler(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r)
// Extract ingredient ID from path /ingredients/{id}/tags
pathParts := strings.Split(strings.TrimPrefix(r.URL.Path, "/ingredients/"), "/")
if len(pathParts) < 1 {
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}
ingredientID, err := strconv.Atoi(pathParts[0])
if err != nil {
http.Error(w, "Invalid ingredient ID", http.StatusBadRequest)
return
}
// Get current tags for ingredient
currentTags, err := database.GetIngredientTags(userID, ingredientID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Get all available tags
allTags, err := database.GetAllTags(userID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Build a set of current tag IDs for easy lookup
currentTagIDs := make(map[int]bool)
for _, tag := range currentTags {
currentTagIDs[tag.ID] = true
}
tmpl := `
<h3>Edit Tags</h3>
<div class="tag-edit-section">
<h4>Current Tags</h4>
<div id="current-tags-{{.IngredientID}}">
{{range .CurrentTags}}
<span class="tag">
{{.Name}}
<button
hx-delete="/ingredients/{{$.IngredientID}}/tags/{{.ID}}"
hx-target="#current-tags-{{$.IngredientID}}"
hx-swap="innerHTML"
class="tag-remove">
×
</button>
</span>
{{end}}
</div>
<h4>Add Tags</h4>
<form hx-post="/ingredients/{{.IngredientID}}/tags"
hx-target="#current-tags-{{.IngredientID}}"
hx-swap="innerHTML"
class="add-tag-form">
<input type="text" name="tag_name" placeholder="New tag name" list="existing-tags" required />
<datalist id="existing-tags">
{{range .AllTags}}
{{if not (index $.CurrentTagIDs .ID)}}
<option value="{{.Name}}">
{{end}}
{{end}}
</datalist>
<button type="submit">Add Tag</button>
</form>
</div>
`
data := struct {
IngredientID int
CurrentTags []interface{}
AllTags []interface{}
CurrentTagIDs map[int]bool
}{
IngredientID: ingredientID,
CurrentTags: []interface{}{},
AllTags: []interface{}{},
CurrentTagIDs: currentTagIDs,
}
for _, tag := range currentTags {
data.CurrentTags = append(data.CurrentTags, tag)
}
for _, tag := range allTags {
data.AllTags = append(data.AllTags, tag)
}
t := template.Must(template.New("ingredient-tags").Parse(tmpl))
t.Execute(w, data)
}
// AddIngredientTagHandler adds a tag to an ingredient
func AddIngredientTagHandler(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r)
// Extract ingredient ID from path
pathParts := strings.Split(strings.TrimPrefix(r.URL.Path, "/ingredients/"), "/")
if len(pathParts) < 1 {
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}
ingredientID, err := strconv.Atoi(pathParts[0])
if err != nil {
http.Error(w, "Invalid ingredient ID", http.StatusBadRequest)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
tagName := strings.TrimSpace(r.FormValue("tag_name"))
if tagName == "" {
http.Error(w, "Tag name is required", http.StatusBadRequest)
return
}
// Get or create the tag
tagID, err := database.GetOrCreateTag(userID, tagName)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Add tag to ingredient
if err := database.AddIngredientTag(userID, ingredientID, int(tagID)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Return updated tag list with trigger to refresh page
w.Header().Set("HX-Trigger", "refreshIngredients")
renderCurrentTags(w, userID, ingredientID)
}
// RemoveIngredientTagHandler removes a tag from an ingredient
func RemoveIngredientTagHandler(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r)
// Extract ingredient ID and tag ID from path /ingredients/{ingredient_id}/tags/{tag_id}
pathParts := strings.Split(strings.TrimPrefix(r.URL.Path, "/ingredients/"), "/")
if len(pathParts) < 3 {
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}
ingredientID, err := strconv.Atoi(pathParts[0])
if err != nil {
http.Error(w, "Invalid ingredient ID", http.StatusBadRequest)
return
}
tagID, err := strconv.Atoi(pathParts[2])
if err != nil {
http.Error(w, "Invalid tag ID", http.StatusBadRequest)
return
}
// Remove tag from ingredient
if err := database.RemoveIngredientTag(userID, ingredientID, tagID); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Return updated tag list with trigger to refresh page
w.Header().Set("HX-Trigger", "refreshIngredients")
renderCurrentTags(w, userID, ingredientID)
}
// Helper function to render current tags
func renderCurrentTags(w http.ResponseWriter, userID, ingredientID int) {
currentTags, err := database.GetIngredientTags(userID, ingredientID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl := `
{{range .Tags}}
<span class="tag">
{{.Name}}
<button
hx-delete="/ingredients/{{$.IngredientID}}/tags/{{.ID}}"
hx-target="#current-tags-{{$.IngredientID}}"
hx-swap="innerHTML"
class="tag-remove">
×
</button>
</span>
{{end}}
`
type TagWithID struct {
ID int
Name string
}
data := struct {
Tags []TagWithID
IngredientID int
}{
Tags: []TagWithID{},
IngredientID: ingredientID,
}
for _, tag := range currentTags {
data.Tags = append(data.Tags, TagWithID{ID: tag.ID, Name: tag.Name})
}
t := template.Must(template.New("current-tags").Parse(tmpl))
t.Execute(w, data)
}