todo v1 beta
All checks were successful
build and deploy kleinTodo / build (push) Successful in 12s

This commit is contained in:
Darius klein 2025-08-23 21:51:33 +02:00
parent 673d56903d
commit bb685be5d5
10 changed files with 285 additions and 50 deletions

55
client/todo/add.go Normal file
View File

@ -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,
}
}

View File

@ -3,6 +3,8 @@ package httpClient
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
) )
type AuthTransport struct { type AuthTransport struct {
@ -15,7 +17,7 @@ type CustomClient struct {
// RoundTrip transport method implementation with jwt in header // RoundTrip transport method implementation with jwt in header
func (t *AuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { 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) return http.DefaultTransport.RoundTrip(req)
} }

View File

@ -95,7 +95,7 @@ func loginAndGetToken(url, username, password string) (string, error) {
return "", fmt.Errorf("error marshaling credentials: %w", err) 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 { if err != nil {
return "", fmt.Errorf("error creating request: %w", err) return "", fmt.Errorf("error creating request: %w", err)
} }

View File

@ -2,15 +2,12 @@ package main
import ( import (
"context" "context"
"fmt"
"log" "log"
"log/slog" "log/slog"
"maps"
"net/mail" "net/mail"
"os" "os"
"gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo/clientCommon/config" "gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo/clientCommon/config"
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@ -49,28 +46,7 @@ func commands() []*cli.Command {
config.Category(), config.Category(),
Login(), Login(),
Sync(), Sync(),
{ Add(),
Name: "todo", Todo(),
Usage: "print todo items",
Action: printTodo,
HideHelpCommand: true,
},
} }
} }
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
}

View File

@ -6,8 +6,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log"
"maps"
"net/http" "net/http"
"gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo/httpClient" "gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo/httpClient"
@ -21,7 +19,6 @@ func Sync() *cli.Command {
Name: "sync", Name: "sync",
Usage: "sync with kleinTodo server", Usage: "sync with kleinTodo server",
Action: syncAction, Action: syncAction,
Flags: loginFlags(),
} }
} }
@ -31,20 +28,19 @@ func syncAction(context context.Context, c *cli.Command) error {
if err != nil { if err != nil {
return err return err
} }
serverTodos := store.GetTodos(cfg.Server.Credentials.Username) serverTodos := store.GetTodoList(cfg.Server.Credentials.Username)
var todos []common.Todo var todos []common.Todo
for todo := range maps.Values(serverTodos) { for _, t := range serverTodos {
todos = append(todos, todo) todos = append(todos, t)
} }
payload, err := json.Marshal(common.TodoList{Todos: todos}) payload, err := json.Marshal(common.TodoList{Todos: todos})
if err != nil { if err != nil {
return fmt.Errorf("error marshaling credentials: %w", err) return fmt.Errorf("error marshaling credentials: %w", err)
} }
req, err := http.NewRequest("GET", cfg.Server.Url+"/sync", bytes.NewBuffer(payload))
req, err := http.NewRequest("POST", cfg.Server.Url, bytes.NewBuffer(payload))
if err != nil { if err != nil {
return fmt.Errorf("error creating request: %w", err) 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 var response common.SyncResponse
if err := json.Unmarshal(body, &response); err != nil { 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, "", " ") var index = 1
if err != nil {
log.Fatalf("Failed to generate json: %s", err) 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.Println("Successfully synced with the server:")
fmt.Printf("%s\n", prettyJSON) for _, todo := range response.SyncedTodos {
err := todo.Store(store, cfg.Server.Credentials.Username)
if err != nil {
return err
}
todo.PrintIndexed(index)
index++
}
return nil return nil
} }

173
client/todo/todo.go Normal file
View File

@ -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)
}

View File

@ -180,7 +180,7 @@ func (s *BoltStore) ExistsByKey(bucket, key string) (bool, error) {
return exists, err 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) storedTodoJsons := s.GetAllFromBucket(user)
serverTodos := make(map[string]Todo) serverTodos := make(map[string]Todo)
@ -192,3 +192,16 @@ func (s *BoltStore) GetTodos(user string) map[string]Todo {
} }
return serverTodos 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
}

View File

@ -16,9 +16,10 @@ const (
// statuses // statuses
const ( const (
Done = "done" NotStarted = "not started"
WIP = "work in progress" Done = "done"
Pending = "pending" WIP = "in progress"
Blocked = "blocked" Pending = "pending"
Failed = "failed" Blocked = "blocked"
Failed = "failed"
) )

View File

@ -49,7 +49,7 @@ func (todo Todo) PrintIndexed(index int) {
statusColor = ColorGreen statusColor = ColorGreen
case WIP: case WIP:
statusColor = ColorYellow statusColor = ColorYellow
case Pending: case Pending, NotStarted:
statusColor = ColorBlue statusColor = ColorBlue
case Blocked, Failed: case Blocked, Failed:
statusColor = ColorRed statusColor = ColorRed

View File

@ -26,7 +26,7 @@ func SyncHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
serverTodos := store.GetTodos(user) serverTodos := store.GetTodoMap(user)
var response = common.SyncResponse{ var response = common.SyncResponse{
SyncedTodos: []common.Todo{}, SyncedTodos: []common.Todo{},