summaryrefslogtreecommitdiff
path: root/internal/api/plantoeat.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api/plantoeat.go')
-rw-r--r--internal/api/plantoeat.go138
1 files changed, 138 insertions, 0 deletions
diff --git a/internal/api/plantoeat.go b/internal/api/plantoeat.go
new file mode 100644
index 0000000..6fe640d
--- /dev/null
+++ b/internal/api/plantoeat.go
@@ -0,0 +1,138 @@
+package api
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "time"
+
+ "task-dashboard/internal/models"
+)
+
+const (
+ planToEatBaseURL = "https://www.plantoeat.com/api/v2"
+)
+
+// PlanToEatClient handles interactions with the PlanToEat API
+type PlanToEatClient struct {
+ apiKey string
+ httpClient *http.Client
+}
+
+// NewPlanToEatClient creates a new PlanToEat API client
+func NewPlanToEatClient(apiKey string) *PlanToEatClient {
+ return &PlanToEatClient{
+ apiKey: apiKey,
+ httpClient: &http.Client{
+ Timeout: 30 * time.Second,
+ },
+ }
+}
+
+// planToEatPlannerItem represents a planner item from the API
+type planToEatPlannerItem struct {
+ ID int `json:"id"`
+ Date string `json:"date"`
+ MealType string `json:"meal_type"`
+ Recipe struct {
+ ID int `json:"id"`
+ Title string `json:"title"`
+ URL string `json:"url"`
+ } `json:"recipe"`
+}
+
+// planToEatResponse wraps the API response
+type planToEatResponse struct {
+ Items []planToEatPlannerItem `json:"items"`
+}
+
+// GetUpcomingMeals fetches meals for the next N days
+func (c *PlanToEatClient) GetUpcomingMeals(ctx context.Context, days int) ([]models.Meal, error) {
+ if days <= 0 {
+ days = 7 // Default to 7 days
+ }
+
+ startDate := time.Now()
+ endDate := startDate.AddDate(0, 0, days)
+
+ req, err := http.NewRequestWithContext(ctx, "GET", planToEatBaseURL+"/planner_items", nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ // Add query parameters
+ q := req.URL.Query()
+ q.Add("start_date", startDate.Format("2006-01-02"))
+ q.Add("end_date", endDate.Format("2006-01-02"))
+ req.URL.RawQuery = q.Encode()
+
+ // Add API key (check docs for correct header name)
+ req.Header.Set("Authorization", "Bearer "+c.apiKey)
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to fetch meals: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("plantoeat API error (status %d): %s", resp.StatusCode, string(body))
+ }
+
+ var apiResponse planToEatResponse
+ if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
+ return nil, fmt.Errorf("failed to decode response: %w", err)
+ }
+
+ // Convert to our model
+ meals := make([]models.Meal, 0, len(apiResponse.Items))
+ for _, item := range apiResponse.Items {
+ mealDate, err := time.Parse("2006-01-02", item.Date)
+ if err != nil {
+ continue // Skip invalid dates
+ }
+
+ meal := models.Meal{
+ ID: fmt.Sprintf("%d", item.ID),
+ RecipeName: item.Recipe.Title,
+ Date: mealDate,
+ MealType: normalizeMealType(item.MealType),
+ RecipeURL: item.Recipe.URL,
+ }
+
+ meals = append(meals, meal)
+ }
+
+ return meals, nil
+}
+
+// normalizeMealType ensures meal type matches our expected values
+func normalizeMealType(mealType string) string {
+ switch mealType {
+ case "breakfast", "Breakfast":
+ return "breakfast"
+ case "lunch", "Lunch":
+ return "lunch"
+ case "dinner", "Dinner":
+ return "dinner"
+ case "snack", "Snack":
+ return "snack"
+ default:
+ return "dinner" // Default to dinner
+ }
+}
+
+// GetRecipes fetches recipes (for Phase 2)
+func (c *PlanToEatClient) GetRecipes(ctx context.Context) error {
+ // This will be implemented in Phase 2
+ return fmt.Errorf("not implemented yet")
+}
+
+// AddMealToPlanner adds a meal to the planner (for Phase 2)
+func (c *PlanToEatClient) AddMealToPlanner(ctx context.Context, recipeID string, date time.Time, mealType string) error {
+ // This will be implemented in Phase 2
+ return fmt.Errorf("not implemented yet")
+}