summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api')
-rw-r--r--internal/api/interfaces.go45
-rw-r--r--internal/api/obsidian.go216
-rw-r--r--internal/api/plantoeat.go138
-rw-r--r--internal/api/todoist.go171
-rw-r--r--internal/api/trello.go219
5 files changed, 789 insertions, 0 deletions
diff --git a/internal/api/interfaces.go b/internal/api/interfaces.go
new file mode 100644
index 0000000..95cc0e7
--- /dev/null
+++ b/internal/api/interfaces.go
@@ -0,0 +1,45 @@
+package api
+
+import (
+ "context"
+ "time"
+
+ "task-dashboard/internal/models"
+)
+
+// TodoistAPI defines the interface for Todoist operations
+type TodoistAPI interface {
+ GetTasks(ctx context.Context) ([]models.Task, error)
+ GetProjects(ctx context.Context) (map[string]string, error)
+ CreateTask(ctx context.Context, content, projectID string, dueDate *time.Time, priority int) (*models.Task, error)
+ CompleteTask(ctx context.Context, taskID string) error
+}
+
+// TrelloAPI defines the interface for Trello operations
+type TrelloAPI interface {
+ GetBoards(ctx context.Context) ([]models.Board, error)
+ GetCards(ctx context.Context, boardID string) ([]models.Card, error)
+ GetBoardsWithCards(ctx context.Context) ([]models.Board, error)
+ CreateCard(ctx context.Context, listID, name, description string, dueDate *time.Time) (*models.Card, error)
+ UpdateCard(ctx context.Context, cardID string, updates map[string]interface{}) error
+}
+
+// ObsidianAPI defines the interface for Obsidian operations
+type ObsidianAPI interface {
+ GetNotes(ctx context.Context, limit int) ([]models.Note, error)
+}
+
+// PlanToEatAPI defines the interface for PlanToEat operations
+type PlanToEatAPI interface {
+ GetUpcomingMeals(ctx context.Context, days int) ([]models.Meal, error)
+ GetRecipes(ctx context.Context) error
+ AddMealToPlanner(ctx context.Context, recipeID string, date time.Time, mealType string) error
+}
+
+// Ensure concrete types implement interfaces
+var (
+ _ TodoistAPI = (*TodoistClient)(nil)
+ _ TrelloAPI = (*TrelloClient)(nil)
+ _ ObsidianAPI = (*ObsidianClient)(nil)
+ _ PlanToEatAPI = (*PlanToEatClient)(nil)
+)
diff --git a/internal/api/obsidian.go b/internal/api/obsidian.go
new file mode 100644
index 0000000..a8ba80d
--- /dev/null
+++ b/internal/api/obsidian.go
@@ -0,0 +1,216 @@
+package api
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strings"
+ "time"
+
+ "task-dashboard/internal/models"
+)
+
+// ObsidianClient handles reading notes from an Obsidian vault
+type ObsidianClient struct {
+ vaultPath string
+}
+
+// NewObsidianClient creates a new Obsidian vault reader
+func NewObsidianClient(vaultPath string) *ObsidianClient {
+ return &ObsidianClient{
+ vaultPath: vaultPath,
+ }
+}
+
+// fileInfo holds file metadata for sorting
+type fileInfo struct {
+ path string
+ modTime time.Time
+}
+
+// GetNotes reads and returns the most recently modified notes from the vault
+func (c *ObsidianClient) GetNotes(ctx context.Context, limit int) ([]models.Note, error) {
+ if c.vaultPath == "" {
+ return nil, fmt.Errorf("obsidian vault path not configured")
+ }
+
+ // Check if vault path exists
+ if _, err := os.Stat(c.vaultPath); os.IsNotExist(err) {
+ return nil, fmt.Errorf("vault path does not exist: %s", c.vaultPath)
+ }
+
+ // Collect all markdown files with their modification times
+ var files []fileInfo
+
+ err := filepath.Walk(c.vaultPath, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return nil // Skip files we can't access
+ }
+
+ // Skip directories and non-markdown files
+ if info.IsDir() || !strings.HasSuffix(info.Name(), ".md") {
+ return nil
+ }
+
+ // Skip hidden files and directories
+ if strings.HasPrefix(info.Name(), ".") {
+ return nil
+ }
+
+ files = append(files, fileInfo{
+ path: path,
+ modTime: info.ModTime(),
+ })
+
+ return nil
+ })
+
+ if err != nil {
+ return nil, fmt.Errorf("failed to walk vault directory: %w", err)
+ }
+
+ // Sort by modification time (most recent first)
+ sort.Slice(files, func(i, j int) bool {
+ return files[i].modTime.After(files[j].modTime)
+ })
+
+ // Limit the number of files to process
+ if limit > 0 && len(files) > limit {
+ files = files[:limit]
+ }
+
+ // Parse each file
+ notes := make([]models.Note, 0, len(files))
+ for _, file := range files {
+ note, err := c.parseMarkdownFile(file.path, file.modTime)
+ if err != nil {
+ // Skip files that fail to parse
+ continue
+ }
+ notes = append(notes, *note)
+ }
+
+ return notes, nil
+}
+
+// parseMarkdownFile reads and parses a markdown file
+func (c *ObsidianClient) parseMarkdownFile(path string, modTime time.Time) (*models.Note, error) {
+ file, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+
+ scanner := bufio.NewScanner(file)
+
+ var content strings.Builder
+ var tags []string
+ inFrontmatter := false
+ lineCount := 0
+
+ // Parse file
+ for scanner.Scan() {
+ line := scanner.Text()
+ lineCount++
+
+ // Check for YAML frontmatter
+ if lineCount == 1 && line == "---" {
+ inFrontmatter = true
+ continue
+ }
+
+ if inFrontmatter {
+ if line == "---" {
+ inFrontmatter = false
+ continue
+ }
+ // Extract tags from frontmatter
+ if strings.HasPrefix(line, "tags:") {
+ tagsStr := strings.TrimPrefix(line, "tags:")
+ tagsStr = strings.Trim(tagsStr, " []")
+ if tagsStr != "" {
+ tags = strings.Split(tagsStr, ",")
+ for i, tag := range tags {
+ tags[i] = strings.TrimSpace(tag)
+ }
+ }
+ }
+ continue
+ }
+
+ // Add to content (limit to preview)
+ if content.Len() < 500 { // Limit to ~500 chars
+ content.WriteString(line)
+ content.WriteString("\n")
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ return nil, err
+ }
+
+ // Extract inline tags (e.g., #tag)
+ inlineTags := extractInlineTags(content.String())
+ tags = append(tags, inlineTags...)
+ tags = uniqueStrings(tags)
+
+ // Get filename and title
+ filename := filepath.Base(path)
+ title := strings.TrimSuffix(filename, ".md")
+
+ // Try to extract title from first H1 heading
+ contentStr := content.String()
+ h1Regex := regexp.MustCompile(`^#\s+(.+)$`)
+ lines := strings.Split(contentStr, "\n")
+ for _, line := range lines {
+ if matches := h1Regex.FindStringSubmatch(line); len(matches) > 1 {
+ title = matches[1]
+ break
+ }
+ }
+
+ note := &models.Note{
+ Filename: filename,
+ Title: title,
+ Content: strings.TrimSpace(contentStr),
+ Modified: modTime,
+ Path: path,
+ Tags: tags,
+ }
+
+ return note, nil
+}
+
+// extractInlineTags finds all #tags in the content
+func extractInlineTags(content string) []string {
+ tagRegex := regexp.MustCompile(`#([a-zA-Z0-9_-]+)`)
+ matches := tagRegex.FindAllStringSubmatch(content, -1)
+
+ tags := make([]string, 0, len(matches))
+ for _, match := range matches {
+ if len(match) > 1 {
+ tags = append(tags, match[1])
+ }
+ }
+
+ return tags
+}
+
+// uniqueStrings returns a slice with duplicate strings removed
+func uniqueStrings(slice []string) []string {
+ seen := make(map[string]bool)
+ result := make([]string, 0, len(slice))
+
+ for _, item := range slice {
+ if !seen[item] && item != "" {
+ seen[item] = true
+ result = append(result, item)
+ }
+ }
+
+ return result
+}
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")
+}
diff --git a/internal/api/todoist.go b/internal/api/todoist.go
new file mode 100644
index 0000000..be59e73
--- /dev/null
+++ b/internal/api/todoist.go
@@ -0,0 +1,171 @@
+package api
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "time"
+
+ "task-dashboard/internal/models"
+)
+
+const (
+ todoistBaseURL = "https://api.todoist.com/rest/v2"
+)
+
+// TodoistClient handles interactions with the Todoist API
+type TodoistClient struct {
+ apiKey string
+ httpClient *http.Client
+}
+
+// NewTodoistClient creates a new Todoist API client
+func NewTodoistClient(apiKey string) *TodoistClient {
+ return &TodoistClient{
+ apiKey: apiKey,
+ httpClient: &http.Client{
+ Timeout: 30 * time.Second,
+ },
+ }
+}
+
+// todoistTaskResponse represents the API response structure
+type todoistTaskResponse struct {
+ ID string `json:"id"`
+ Content string `json:"content"`
+ Description string `json:"description"`
+ ProjectID string `json:"project_id"`
+ Priority int `json:"priority"`
+ Labels []string `json:"labels"`
+ Due *struct {
+ Date string `json:"date"`
+ Datetime string `json:"datetime"`
+ } `json:"due"`
+ URL string `json:"url"`
+ CreatedAt string `json:"created_at"`
+}
+
+// todoistProjectResponse represents the project API response
+type todoistProjectResponse struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+}
+
+// GetTasks fetches all active tasks from Todoist
+func (c *TodoistClient) GetTasks(ctx context.Context) ([]models.Task, error) {
+ req, err := http.NewRequestWithContext(ctx, "GET", todoistBaseURL+"/tasks", nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ req.Header.Set("Authorization", "Bearer "+c.apiKey)
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to fetch tasks: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("todoist API error (status %d): %s", resp.StatusCode, string(body))
+ }
+
+ var apiTasks []todoistTaskResponse
+ if err := json.NewDecoder(resp.Body).Decode(&apiTasks); err != nil {
+ return nil, fmt.Errorf("failed to decode response: %w", err)
+ }
+
+ // Fetch projects to get project names
+ projects, err := c.GetProjects(ctx)
+ if err != nil {
+ // If we can't get projects, continue with empty project names
+ projects = make(map[string]string)
+ }
+
+ // Convert to our model
+ tasks := make([]models.Task, 0, len(apiTasks))
+ for _, apiTask := range apiTasks {
+ task := models.Task{
+ ID: apiTask.ID,
+ Content: apiTask.Content,
+ Description: apiTask.Description,
+ ProjectID: apiTask.ProjectID,
+ ProjectName: projects[apiTask.ProjectID],
+ Priority: apiTask.Priority,
+ Completed: false,
+ Labels: apiTask.Labels,
+ URL: apiTask.URL,
+ }
+
+ // Parse created_at
+ if createdAt, err := time.Parse(time.RFC3339, apiTask.CreatedAt); err == nil {
+ task.CreatedAt = createdAt
+ }
+
+ // Parse due date
+ if apiTask.Due != nil {
+ var dueDate time.Time
+ if apiTask.Due.Datetime != "" {
+ dueDate, err = time.Parse(time.RFC3339, apiTask.Due.Datetime)
+ } else if apiTask.Due.Date != "" {
+ dueDate, err = time.Parse("2006-01-02", apiTask.Due.Date)
+ }
+ if err == nil {
+ task.DueDate = &dueDate
+ }
+ }
+
+ tasks = append(tasks, task)
+ }
+
+ return tasks, nil
+}
+
+// GetProjects fetches all projects and returns a map of project ID to name
+func (c *TodoistClient) GetProjects(ctx context.Context) (map[string]string, error) {
+ req, err := http.NewRequestWithContext(ctx, "GET", todoistBaseURL+"/projects", nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ req.Header.Set("Authorization", "Bearer "+c.apiKey)
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to fetch projects: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("todoist API error (status %d): %s", resp.StatusCode, string(body))
+ }
+
+ var apiProjects []todoistProjectResponse
+ if err := json.NewDecoder(resp.Body).Decode(&apiProjects); err != nil {
+ return nil, fmt.Errorf("failed to decode response: %w", err)
+ }
+
+ // Convert to map
+ projects := make(map[string]string, len(apiProjects))
+ for _, project := range apiProjects {
+ projects[project.ID] = project.Name
+ }
+
+ return projects, nil
+}
+
+// CreateTask creates a new task in Todoist
+func (c *TodoistClient) CreateTask(ctx context.Context, content, projectID string, dueDate *time.Time, priority int) (*models.Task, error) {
+ // This will be implemented in Phase 2
+ return nil, fmt.Errorf("not implemented yet")
+}
+
+// CompleteTask marks a task as complete in Todoist
+func (c *TodoistClient) CompleteTask(ctx context.Context, taskID string) error {
+ // This will be implemented in Phase 2
+ return fmt.Errorf("not implemented yet")
+}
diff --git a/internal/api/trello.go b/internal/api/trello.go
new file mode 100644
index 0000000..899f6df
--- /dev/null
+++ b/internal/api/trello.go
@@ -0,0 +1,219 @@
+package api
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "time"
+
+ "task-dashboard/internal/models"
+)
+
+const (
+ trelloBaseURL = "https://api.trello.com/1"
+)
+
+// TrelloClient handles interactions with the Trello API
+type TrelloClient struct {
+ apiKey string
+ token string
+ httpClient *http.Client
+}
+
+// NewTrelloClient creates a new Trello API client
+func NewTrelloClient(apiKey, token string) *TrelloClient {
+ return &TrelloClient{
+ apiKey: apiKey,
+ token: token,
+ httpClient: &http.Client{
+ Timeout: 30 * time.Second,
+ },
+ }
+}
+
+// trelloBoardResponse represents a board from the API
+type trelloBoardResponse struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+}
+
+// trelloCardResponse represents a card from the API
+type trelloCardResponse struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ IDList string `json:"idList"`
+ Due *string `json:"due"`
+ URL string `json:"url"`
+ Desc string `json:"desc"`
+ IDBoard string `json:"idBoard"`
+}
+
+// trelloListResponse represents a list from the API
+type trelloListResponse struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+}
+
+// GetBoards fetches all boards for the authenticated user
+func (c *TrelloClient) GetBoards(ctx context.Context) ([]models.Board, error) {
+ url := fmt.Sprintf("%s/members/me/boards?key=%s&token=%s", trelloBaseURL, c.apiKey, c.token)
+
+ req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to fetch boards: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("trello API error (status %d): %s", resp.StatusCode, string(body))
+ }
+
+ var apiBoards []trelloBoardResponse
+ if err := json.NewDecoder(resp.Body).Decode(&apiBoards); err != nil {
+ return nil, fmt.Errorf("failed to decode response: %w", err)
+ }
+
+ // Convert to our model
+ boards := make([]models.Board, 0, len(apiBoards))
+ for _, apiBoard := range apiBoards {
+ board := models.Board{
+ ID: apiBoard.ID,
+ Name: apiBoard.Name,
+ Cards: []models.Card{}, // Will be populated by GetCards
+ }
+ boards = append(boards, board)
+ }
+
+ return boards, nil
+}
+
+// GetCards fetches all cards for a specific board
+func (c *TrelloClient) GetCards(ctx context.Context, boardID string) ([]models.Card, error) {
+ url := fmt.Sprintf("%s/boards/%s/cards?key=%s&token=%s", trelloBaseURL, boardID, c.apiKey, c.token)
+
+ req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to fetch cards: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("trello API error (status %d): %s", resp.StatusCode, string(body))
+ }
+
+ var apiCards []trelloCardResponse
+ if err := json.NewDecoder(resp.Body).Decode(&apiCards); err != nil {
+ return nil, fmt.Errorf("failed to decode response: %w", err)
+ }
+
+ // Fetch lists to get list names
+ lists, err := c.getLists(ctx, boardID)
+ if err != nil {
+ // If we can't get lists, continue with empty list names
+ lists = make(map[string]string)
+ }
+
+ // Convert to our model
+ cards := make([]models.Card, 0, len(apiCards))
+ for _, apiCard := range apiCards {
+ card := models.Card{
+ ID: apiCard.ID,
+ Name: apiCard.Name,
+ ListID: apiCard.IDList,
+ ListName: lists[apiCard.IDList],
+ URL: apiCard.URL,
+ }
+
+ // Parse due date if present
+ if apiCard.Due != nil && *apiCard.Due != "" {
+ dueDate, err := time.Parse(time.RFC3339, *apiCard.Due)
+ if err == nil {
+ card.DueDate = &dueDate
+ }
+ }
+
+ cards = append(cards, card)
+ }
+
+ return cards, nil
+}
+
+// getLists fetches lists for a board and returns a map of list ID to name
+func (c *TrelloClient) getLists(ctx context.Context, boardID string) (map[string]string, error) {
+ url := fmt.Sprintf("%s/boards/%s/lists?key=%s&token=%s", trelloBaseURL, boardID, c.apiKey, c.token)
+
+ req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to fetch lists: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("trello API error (status %d): %s", resp.StatusCode, string(body))
+ }
+
+ var apiLists []trelloListResponse
+ if err := json.NewDecoder(resp.Body).Decode(&apiLists); err != nil {
+ return nil, fmt.Errorf("failed to decode response: %w", err)
+ }
+
+ // Convert to map
+ lists := make(map[string]string, len(apiLists))
+ for _, list := range apiLists {
+ lists[list.ID] = list.Name
+ }
+
+ return lists, nil
+}
+
+// GetBoardsWithCards fetches all boards and their cards in one call
+func (c *TrelloClient) GetBoardsWithCards(ctx context.Context) ([]models.Board, error) {
+ boards, err := c.GetBoards(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ // Fetch cards for each board
+ for i := range boards {
+ cards, err := c.GetCards(ctx, boards[i].ID)
+ if err != nil {
+ // Log error but continue with other boards
+ continue
+ }
+ boards[i].Cards = cards
+ }
+
+ return boards, nil
+}
+
+// CreateCard creates a new card (for Phase 2)
+func (c *TrelloClient) CreateCard(ctx context.Context, listID, name, description string, dueDate *time.Time) (*models.Card, error) {
+ // This will be implemented in Phase 2
+ return nil, fmt.Errorf("not implemented yet")
+}
+
+// UpdateCard updates a card (for Phase 2)
+func (c *TrelloClient) UpdateCard(ctx context.Context, cardID string, updates map[string]interface{}) error {
+ // This will be implemented in Phase 2
+ return fmt.Errorf("not implemented yet")
+}