From bb685be5d57f06bf2e62bf7d8228fb1cfe836f1d Mon Sep 17 00:00:00 2001 From: Darius klein Date: Sat, 23 Aug 2025 21:51:33 +0200 Subject: [PATCH] todo v1 beta --- client/todo/add.go | 55 +++++++++ client/todo/httpClient/httpClient.go | 4 +- client/todo/login.go | 2 +- client/todo/main.go | 28 +---- client/todo/sync.go | 43 ++++--- client/todo/todo.go | 173 +++++++++++++++++++++++++++ common/bolt.go | 15 ++- common/const.go | 11 +- common/todo.go | 2 +- server/handler/syncHandler.go | 2 +- 10 files changed, 285 insertions(+), 50 deletions(-) create mode 100644 client/todo/add.go create mode 100644 client/todo/todo.go diff --git a/client/todo/add.go b/client/todo/add.go new file mode 100644 index 0000000..9b4464a --- /dev/null +++ b/client/todo/add.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "fmt" + + "gitea.kleinsense.nl/DariusKlein/kleinTodo/common" + "github.com/urfave/cli/v3" +) + +// Add Command +func Add() *cli.Command { + return &cli.Command{ + Name: "add", + Usage: "add todo item (s)", + Action: addAction, + } +} + +// addAction logic for Template +func addAction(context context.Context, c *cli.Command) error { + store, err := common.GetTodoDataStore() + if err != nil { + return err + } + var newTodos []common.Todo + var adding = true + for adding { + newTodos = append(newTodos, createNewTodo()) + if !common.AskUserBool("Want to add more?") { + adding = false + } + } + + for _, t := range newTodos { + err := t.Store(store, cfg.Server.Credentials.Username) + if err != nil { + return err + } + } + + return nil +} + +func createNewTodo() common.Todo { + return common.Todo{ + Name: common.AskUserString("Name:\n"), + Description: common.AskUserString("Description:\n"), + Status: common.AskUserString(fmt.Sprintf("Status(%s, %s, %s, %s, %s, %s):\n", + common.NotStarted, common.Done, common.WIP, common.Pending, common.Blocked, common.Failed, + ), + ), + Owner: cfg.Server.Credentials.Username, + } +} diff --git a/client/todo/httpClient/httpClient.go b/client/todo/httpClient/httpClient.go index d05daa7..9fbf5ea 100644 --- a/client/todo/httpClient/httpClient.go +++ b/client/todo/httpClient/httpClient.go @@ -3,6 +3,8 @@ package httpClient import ( "fmt" "net/http" + + "gitea.kleinsense.nl/DariusKlein/kleinTodo/common" ) type AuthTransport struct { @@ -15,7 +17,7 @@ type CustomClient struct { // RoundTrip transport method implementation with jwt in header func (t *AuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { - req.Header.Add("Authorization", fmt.Sprintf("Beare%s", t.Token)) + req.Header.Add(common.AuthHeader, fmt.Sprintf("Bearer %s", t.Token)) return http.DefaultTransport.RoundTrip(req) } diff --git a/client/todo/login.go b/client/todo/login.go index 596219e..15b46fc 100644 --- a/client/todo/login.go +++ b/client/todo/login.go @@ -95,7 +95,7 @@ func loginAndGetToken(url, username, password string) (string, error) { return "", fmt.Errorf("error marshaling credentials: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) + req, err := http.NewRequest("POST", url+"/login", bytes.NewBuffer(payload)) if err != nil { return "", fmt.Errorf("error creating request: %w", err) } diff --git a/client/todo/main.go b/client/todo/main.go index 7b7478b..b428cfc 100644 --- a/client/todo/main.go +++ b/client/todo/main.go @@ -2,15 +2,12 @@ package main import ( "context" - "fmt" "log" "log/slog" - "maps" "net/mail" "os" "gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo/clientCommon/config" - "gitea.kleinsense.nl/DariusKlein/kleinTodo/common" "github.com/urfave/cli/v3" ) @@ -49,28 +46,7 @@ func commands() []*cli.Command { config.Category(), Login(), Sync(), - { - Name: "todo", - Usage: "print todo items", - Action: printTodo, - HideHelpCommand: true, - }, + Add(), + Todo(), } } - -func printTodo(context context.Context, c *cli.Command) error { - fmt.Printf("Todo items:\n") - store, err := common.GetTodoDataStore() - if err != nil { - return err - } - serverTodos := store.GetTodos(cfg.Server.Credentials.Username) - - var index = 1 - - for todo := range maps.Values(serverTodos) { - todo.PrintIndexed(index) - index++ - } - return nil -} diff --git a/client/todo/sync.go b/client/todo/sync.go index 074bee8..473654a 100644 --- a/client/todo/sync.go +++ b/client/todo/sync.go @@ -6,8 +6,6 @@ import ( "encoding/json" "fmt" "io" - "log" - "maps" "net/http" "gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo/httpClient" @@ -21,7 +19,6 @@ func Sync() *cli.Command { Name: "sync", Usage: "sync with kleinTodo server", Action: syncAction, - Flags: loginFlags(), } } @@ -31,20 +28,19 @@ func syncAction(context context.Context, c *cli.Command) error { if err != nil { return err } - serverTodos := store.GetTodos(cfg.Server.Credentials.Username) + serverTodos := store.GetTodoList(cfg.Server.Credentials.Username) var todos []common.Todo - for todo := range maps.Values(serverTodos) { - todos = append(todos, todo) + for _, t := range serverTodos { + todos = append(todos, t) } payload, err := json.Marshal(common.TodoList{Todos: todos}) if err != nil { return fmt.Errorf("error marshaling credentials: %w", err) } - - req, err := http.NewRequest("POST", cfg.Server.Url, bytes.NewBuffer(payload)) + req, err := http.NewRequest("GET", cfg.Server.Url+"/sync", bytes.NewBuffer(payload)) if err != nil { return fmt.Errorf("error creating request: %w", err) } @@ -68,16 +64,35 @@ func syncAction(context context.Context, c *cli.Command) error { var response common.SyncResponse if err := json.Unmarshal(body, &response); err != nil { - return fmt.Errorf("failed to decode successful response: %w", err) + return fmt.Errorf("failed to decode successful response: %w\n%s", err, string(body)) } - prettyJSON, err := json.MarshalIndent(response, "", " ") - if err != nil { - log.Fatalf("Failed to generate json: %s", err) + var index = 1 + + if len(response.MisMatchingTodos) > 0 { + for _, todo := range response.MisMatchingTodos { + fmt.Println("Mismatch between server and client") + fmt.Print("local:") + todo.LocalTodo.PrintIndexed(1) + fmt.Print("server:") + todo.ServerTodo.PrintIndexed(2) + if common.AskUserBool("Do you wish to override you local version with the server version?") { + response.SyncedTodos = append(response.SyncedTodos, todo.ServerTodo) + } else { + response.SyncedTodos = append(response.SyncedTodos, todo.LocalTodo) + } + } } - // Print the string version of the byte slice. - fmt.Printf("%s\n", prettyJSON) + fmt.Println("Successfully synced with the server:") + for _, todo := range response.SyncedTodos { + err := todo.Store(store, cfg.Server.Credentials.Username) + if err != nil { + return err + } + todo.PrintIndexed(index) + index++ + } return nil } diff --git a/client/todo/todo.go b/client/todo/todo.go new file mode 100644 index 0000000..bb4441d --- /dev/null +++ b/client/todo/todo.go @@ -0,0 +1,173 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "log/slog" + "os" + "strconv" + "strings" + + "gitea.kleinsense.nl/DariusKlein/kleinTodo/common" + "github.com/urfave/cli/v3" +) + +// Todo Command +func Todo() *cli.Command { + return &cli.Command{ + Name: "todo", + Usage: "print todo items and allow for updating", + Action: todo, + HideHelpCommand: true, + } +} + +func todo(context context.Context, c *cli.Command) error { + // bufio.Scanner is a great way to read user input line by line. + scanner := bufio.NewScanner(os.Stdin) + + store, err := common.GetTodoDataStore() + if err != nil { + return err + } + + todos := store.GetTodoList(cfg.Server.Credentials.Username) + if err != nil { + return err + } + + // This is the main application loop for the interactive mode. + for { + clearScreen() + printTodos(todos) + + fmt.Print("What would you like to do? (update, delete, add, quit) [u/d/a/q]: ") + + // Wait for and read the user's next command. + scanner.Scan() + command := strings.ToLower(strings.TrimSpace(scanner.Text())) + + switch command { + case "u", "update": + todos = handleUpdate(scanner, todos, store) + case "d", "delete": + todos = handleDelete(scanner, todos, store) + case "a", "add": + todos = handleAdd(scanner, todos, store) + case "q", "quit": + fmt.Println("Goodbye!") + return nil // Exit the program + default: + fmt.Println("Invalid command. Please try again.") + } + } +} + +func clearScreen() { + fmt.Print("\033[H\033[2J") +} + +func printTodos(todos []common.Todo) { + fmt.Printf("Todo items:\n") + for i, t := range todos { + t.PrintIndexed(i + 1) + } +} + +// handleDelete prompts for an index and removes the item. +func handleDelete(scanner *bufio.Scanner, todos []common.Todo, store *common.BoltStore) []common.Todo { + fmt.Print("Enter the number of the item to delete: ") + scanner.Scan() + input := strings.TrimSpace(scanner.Text()) + index, err := strconv.Atoi(input) + + if err != nil || index < 1 || index > len(todos) { + fmt.Println("Invalid number. Returning to main menu.") + return todos + } + + removedItem := todos[index-1] + + err = store.RemoveValueFromBucket(cfg.Server.Credentials.Username, removedItem.Name) + if err != nil { + slog.Error(err.Error()) + return todos + } + fmt.Printf("Item '%s' deleted.\n", removedItem.Name) + + todos = append(todos[:index-1], todos[index:]...) + return todos +} + +// handleUpdate prompts for an index and new values. +func handleUpdate(scanner *bufio.Scanner, todos []common.Todo, store *common.BoltStore) []common.Todo { + fmt.Print("Enter the number of the item to update: ") + scanner.Scan() + input := strings.TrimSpace(scanner.Text()) + index, err := strconv.Atoi(input) + + if err != nil || index < 1 || index > len(todos) { + fmt.Println("Invalid number. Returning to main menu.") + return todos + } + + // Adjust for 0-based slice index + itemToUpdate := todos[index-1] + + fmt.Printf("Updating '%s'. Press Enter to keep current value.\n", itemToUpdate.Name) + + fmt.Printf("New name [%s]: ", itemToUpdate.Name) + scanner.Scan() + newName := strings.TrimSpace(scanner.Text()) + if newName != "" { + itemToUpdate.Name = newName + } + + fmt.Printf("New description [%s]: ", itemToUpdate.Description) + scanner.Scan() + newDescription := strings.TrimSpace(scanner.Text()) + if newDescription != "" { + itemToUpdate.Description = newDescription + } + + fmt.Printf("New status [%s]: ", itemToUpdate.Status) + scanner.Scan() + newStatus := strings.TrimSpace(scanner.Text()) + if newStatus != "" { + itemToUpdate.Status = newStatus + } + + err = itemToUpdate.Store(store, cfg.Server.Credentials.Username) + if err != nil { + slog.Error(err.Error()) + return todos + } + + fmt.Println("Item updated.") + todos[index-1] = itemToUpdate + return todos +} + +// handleAdd prompts for the details of a new item. +func handleAdd(scanner *bufio.Scanner, todos []common.Todo, store *common.BoltStore) []common.Todo { + fmt.Print("Enter the name of the new task: ") + scanner.Scan() + name := strings.TrimSpace(scanner.Text()) + + fmt.Print("Enter the description: ") + scanner.Scan() + description := strings.TrimSpace(scanner.Text()) + + newTodo := common.Todo{ + Name: name, Description: description, Status: common.NotStarted, Owner: cfg.Server.Credentials.Username, + } + err := newTodo.Store(store, cfg.Server.Credentials.Username) + if err != nil { + slog.Error(err.Error()) + return todos + } + + fmt.Println("New item added.") + return append(todos, newTodo) +} diff --git a/common/bolt.go b/common/bolt.go index a31ade2..7e08d7a 100644 --- a/common/bolt.go +++ b/common/bolt.go @@ -180,7 +180,7 @@ func (s *BoltStore) ExistsByKey(bucket, key string) (bool, error) { return exists, err } -func (s *BoltStore) GetTodos(user string) map[string]Todo { +func (s *BoltStore) GetTodoMap(user string) map[string]Todo { storedTodoJsons := s.GetAllFromBucket(user) serverTodos := make(map[string]Todo) @@ -192,3 +192,16 @@ func (s *BoltStore) GetTodos(user string) map[string]Todo { } return serverTodos } + +func (s *BoltStore) GetTodoList(user string) []Todo { + storedTodoJsons := s.GetAllFromBucket(user) + + var serverTodos []Todo + for _, val := range storedTodoJsons { + var todo Todo + if json.Unmarshal([]byte(val), &todo) == nil { + serverTodos = append(serverTodos, todo) + } + } + return serverTodos +} diff --git a/common/const.go b/common/const.go index fde0440..f869c3c 100644 --- a/common/const.go +++ b/common/const.go @@ -16,9 +16,10 @@ const ( // statuses const ( - Done = "done" - WIP = "work in progress" - Pending = "pending" - Blocked = "blocked" - Failed = "failed" + NotStarted = "not started" + Done = "done" + WIP = "in progress" + Pending = "pending" + Blocked = "blocked" + Failed = "failed" ) diff --git a/common/todo.go b/common/todo.go index ccd5d8e..9ac4479 100644 --- a/common/todo.go +++ b/common/todo.go @@ -49,7 +49,7 @@ func (todo Todo) PrintIndexed(index int) { statusColor = ColorGreen case WIP: statusColor = ColorYellow - case Pending: + case Pending, NotStarted: statusColor = ColorBlue case Blocked, Failed: statusColor = ColorRed diff --git a/server/handler/syncHandler.go b/server/handler/syncHandler.go index c6710b3..14ea561 100644 --- a/server/handler/syncHandler.go +++ b/server/handler/syncHandler.go @@ -26,7 +26,7 @@ func SyncHandler(w http.ResponseWriter, r *http.Request) { return } - serverTodos := store.GetTodos(user) + serverTodos := store.GetTodoMap(user) var response = common.SyncResponse{ SyncedTodos: []common.Todo{},