package handlers import ( "context" "encoding/json" "net/http" "time" "lookbook/internal/data/item" "lookbook/internal/data/media" "lookbook/internal/data/tag" ) type itemResponse struct { ID string `json:"id"` Title *string `json:"title,omitempty"` Description *string `json:"description,omitempty"` LinkURL *string `json:"linkUrl,omitempty"` ItemType string `json:"itemType"` EmbedHTML *string `json:"embedHtml,omitempty"` Tags []string `json:"tags"` CreatedAt string `json:"createdAt"` MediaID *int64 `json:"mediaId,omitempty"` ThumbnailID *int64 `json:"thumbnailId,omitempty"` ThumbnailSourceURL *string `json:"thumbnailSourceUrl,omitempty"` } type createItemRequest struct { Title *string `json:"title"` Description *string `json:"description"` LinkURL *string `json:"linkUrl"` ItemType string `json:"itemType"` // 'image', 'video', 'quote', 'embed' EmbedProvider *string `json:"embedProvider"` EmbedVideoID *string `json:"embedVideoId"` EmbedHTML *string `json:"embedHtml"` Tags []string `json:"tags"` } type updateItemRequest struct { Title *string `json:"title"` Description *string `json:"description"` LinkURL *string `json:"linkUrl"` Tags []string `json:"tags"` } // HandleListItems handles GET /api/items func HandleListItems(rc *RequestContext, w http.ResponseWriter, r *http.Request) error { ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() tagFilter := r.URL.Query().Get("tag") var items []item.Row var err error if tagFilter != "" { items, err = item.QListByTag(ctx, rc.DB, tagFilter) } else { items, err = item.QList(ctx, rc.DB) } if err != nil { return err } response := make([]itemResponse, 0, len(items)) for _, it := range items { resp, err := buildItemResponse(ctx, rc, it) if err != nil { return err } response = append(response, resp) } return writeJSON(w, http.StatusOK, response) } // HandleGetItem handles GET /api/items/{id} func HandleGetItem(rc *RequestContext, w http.ResponseWriter, r *http.Request) error { pubID := r.PathValue("id") if pubID == "" { return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing id"}) } ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() it, err := item.QFindByPubID(ctx, rc.DB, pubID) if err != nil { return err } if it == nil { return writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) } resp, err := buildItemResponse(ctx, rc, *it) if err != nil { return err } return writeJSON(w, http.StatusOK, resp) } // HandleCreateItem handles POST /api/items func HandleCreateItem(rc *RequestContext, w http.ResponseWriter, r *http.Request) error { if !rc.RequireAdmin(w) { return nil } var req createItemRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"}) } if req.ItemType == "" { return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "itemType required"}) } ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() it, err := item.QCreate(ctx, rc.DB, item.CreateParams{ Title: req.Title, Description: req.Description, LinkURL: req.LinkURL, ItemType: req.ItemType, EmbedProvider: req.EmbedProvider, EmbedVideoID: req.EmbedVideoID, EmbedHTML: req.EmbedHTML, }) if err != nil { return err } // Set tags if len(req.Tags) > 0 { if err := tag.QSetTagsForItem(ctx, rc.DB, it.ID, req.Tags); err != nil { return err } } resp, err := buildItemResponse(ctx, rc, it) if err != nil { return err } return writeJSON(w, http.StatusCreated, resp) } // HandleUpdateItem handles PUT /api/items/{id} func HandleUpdateItem(rc *RequestContext, w http.ResponseWriter, r *http.Request) error { if !rc.RequireAdmin(w) { return nil } pubID := r.PathValue("id") if pubID == "" { return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing id"}) } var req updateItemRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"}) } ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() it, err := item.QFindByPubID(ctx, rc.DB, pubID) if err != nil { return err } if it == nil { return writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) } if err := item.QUpdate(ctx, rc.DB, it.ID, item.UpdateParams{ Title: req.Title, Description: req.Description, LinkURL: req.LinkURL, }); err != nil { return err } // Update tags if err := tag.QSetTagsForItem(ctx, rc.DB, it.ID, req.Tags); err != nil { return err } // Refetch to return updated item it, err = item.QFindByID(ctx, rc.DB, it.ID) if err != nil { return err } resp, err := buildItemResponse(ctx, rc, *it) if err != nil { return err } return writeJSON(w, http.StatusOK, resp) } // HandleDeleteItem handles DELETE /api/items/{id} func HandleDeleteItem(rc *RequestContext, w http.ResponseWriter, r *http.Request) error { if !rc.RequireAdmin(w) { return nil } pubID := r.PathValue("id") if pubID == "" { return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing id"}) } ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() it, err := item.QFindByPubID(ctx, rc.DB, pubID) if err != nil { return err } if it == nil { return writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) } if err := item.QSoftDelete(ctx, rc.DB, it.ID); err != nil { return err } return writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) } func buildItemResponse(ctx context.Context, rc *RequestContext, it item.Row) (itemResponse, error) { tags, err := tag.QTagsForItem(ctx, rc.DB, it.ID) if err != nil { return itemResponse{}, err } tagNames := make([]string, len(tags)) for i, t := range tags { tagNames[i] = t.Name } resp := itemResponse{ ID: it.PubID, Title: it.Title, Description: it.Description, LinkURL: it.LinkURL, ItemType: it.ItemType, EmbedHTML: it.EmbedHTML, Tags: tagNames, CreatedAt: it.CreatedAt.Format(time.RFC3339), } // Get media IDs mediaList, err := media.QFindByItemID(ctx, rc.DB, it.ID) if err != nil { return itemResponse{}, err } for _, m := range mediaList { if m.MediaType == "original" { resp.MediaID = &m.ID } else if m.MediaType == "thumbnail" { resp.ThumbnailID = &m.ID resp.ThumbnailSourceURL = m.SourceURL } } return resp, nil }