diff --git a/client/emdb.go b/client/emdb.go index 1358f1b..833d0d4 100644 --- a/client/emdb.go +++ b/client/emdb.go @@ -162,3 +162,67 @@ func (e *EMDB) GetReviews(movieID string) ([]moviestore.Review, error) { return reviews, nil } + +func (e *EMDB) GetNextUnratedReview() (moviestore.Review, error) { + url := fmt.Sprintf("%s/review/unrated/next", e.baseURL) + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return moviestore.Review{}, err + } + req.Header.Add("Authorization", e.apiKey) + + resp, err := e.c.Do(req) + if err != nil { + return moviestore.Review{}, err + } + + if resp.StatusCode != http.StatusOK { + return moviestore.Review{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + defer resp.Body.Close() + + var review moviestore.Review + if err := json.Unmarshal(body, &review); err != nil { + return moviestore.Review{}, err + } + + return review, nil +} + +func (e *EMDB) UpdateReview(review moviestore.Review) (moviestore.Review, error) { + body, err := json.Marshal(review) + if err != nil { + return moviestore.Review{}, err + } + + url := fmt.Sprintf("%s/review/%s", e.baseURL, review.ID) + req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(body)) + if err != nil { + return moviestore.Review{}, err + } + req.Header.Add("Authorization", e.apiKey) + + resp, err := e.c.Do(req) + if err != nil { + return moviestore.Review{}, err + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + return moviestore.Review{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + newBody, err := io.ReadAll(resp.Body) + if err != nil { + return moviestore.Review{}, err + } + defer resp.Body.Close() + + var newReview moviestore.Review + if err := json.Unmarshal(newBody, &newReview); err != nil { + return moviestore.Review{}, err + } + + return newReview, nil +} diff --git a/cmd/api-service/handler/review.go b/cmd/api-service/handler/review.go index 1238d2a..3a7b93a 100644 --- a/cmd/api-service/handler/review.go +++ b/cmd/api-service/handler/review.go @@ -27,6 +27,8 @@ func (reviewAPI *ReviewAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) { subPath, subTrail := ShiftPath(r.URL.Path) subSubPath, _ := ShiftPath(subTrail) switch { + case r.Method == http.MethodGet && subPath != "": + reviewAPI.Get(w, r, subPath) case r.Method == http.MethodGet && subPath == "unrated" && subSubPath == "": reviewAPI.ListUnrated(w, r) case r.Method == http.MethodGet && subPath == "unrated" && subSubPath == "next": @@ -38,6 +40,21 @@ func (reviewAPI *ReviewAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } +func (reviewAPI *ReviewAPI) Get(w http.ResponseWriter, r *http.Request, id string) { + logger := reviewAPI.logger.With("method", "get") + + review, err := reviewAPI.repo.FindOne(id) + if err != nil { + Error(w, http.StatusInternalServerError, "could not get review", err, logger) + return + } + + if err := json.NewEncoder(w).Encode(review); err != nil { + Error(w, http.StatusInternalServerError, "could not encode review", err, logger) + return + } +} + func (reviewAPI *ReviewAPI) ListUnrated(w http.ResponseWriter, r *http.Request) { logger := reviewAPI.logger.With("method", "listUnrated") diff --git a/cmd/terminal-client/tui/review.go b/cmd/terminal-client/tui/review.go new file mode 100644 index 0000000..622347a --- /dev/null +++ b/cmd/terminal-client/tui/review.go @@ -0,0 +1,11 @@ +package tui + +type Review struct { + ID string + MovieID string + Source string + URL string + Review string + Quality int + Mentions []string +} diff --git a/cmd/terminal-client/tui/tabemdb.go b/cmd/terminal-client/tui/tabemdb.go index da0b95d..96bab98 100644 --- a/cmd/terminal-client/tui/tabemdb.go +++ b/cmd/terminal-client/tui/tabemdb.go @@ -147,15 +147,6 @@ func (m tabEMDB) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Batch(cmds...) } -func (m *tabEMDB) updateFormInputs(msg tea.Msg) tea.Cmd { - cmds := make([]tea.Cmd, 3) - m.inputWatchedOn, cmds[0] = m.inputWatchedOn.Update(msg) - m.inputRating, cmds[1] = m.inputRating.Update(msg) - m.inputComment, cmds[2] = m.inputComment.Update(msg) - - return tea.Batch(cmds...) -} - func (m tabEMDB) View() string { colLeft := lipgloss.NewStyle(). Width(m.colWidth - 2). @@ -182,6 +173,58 @@ func (m *tabEMDB) UpdateForm() { m.Log(fmt.Sprintf("showing movie %s", movie.m.ID)) } +func (m *tabEMDB) updateFormInputs(msg tea.Msg) tea.Cmd { + var cmd tea.Cmd + + switch m.formFocus { + case 0: + m.inputWatchedOn, cmd = m.inputWatchedOn.Update(msg) + case 1: + m.inputRating, cmd = m.inputRating.Update(msg) + case 2: + m.inputComment, cmd = m.inputComment.Update(msg) + } + return cmd +} + +func (m *tabEMDB) NavigateForm(key string) []tea.Cmd { + order := []string{"Watched on", "Rating", "Comment"} + + var cmds []tea.Cmd + if key == "up" || key == "shift+tab" { + m.formFocus-- + } else { + m.formFocus++ + } + if m.formFocus >= len(order) { + m.formFocus = 0 + } + if m.formFocus < 0 { + m.formFocus = len(order) - 1 + } + + switch order[m.formFocus] { + case "Watched on": + m.inputWatchedOn.PromptStyle = focusedStyle + m.inputWatchedOn.TextStyle = focusedStyle + cmds = append(cmds, m.inputWatchedOn.Focus()) + m.inputRating.Blur() + m.inputComment.Blur() + case "Rating": + m.inputRating.PromptStyle = focusedStyle + m.inputRating.TextStyle = focusedStyle + cmds = append(cmds, m.inputRating.Focus()) + m.inputWatchedOn.Blur() + m.inputComment.Blur() + case "Comment": + cmds = append(cmds, m.inputComment.Focus()) + m.inputWatchedOn.Blur() + m.inputRating.Blur() + } + + return cmds +} + func (m *tabEMDB) ViewForm() string { movie, ok := m.list.SelectedItem().(Movie) if !ok { @@ -215,44 +258,6 @@ func (m *tabEMDB) ViewForm() string { return lipgloss.JoinHorizontal(lipgloss.Top, labelView, fieldsView) } -func (m *tabEMDB) NavigateForm(key string) []tea.Cmd { - order := []string{"Watched on", "Rating", "Comment"} - - var cmds []tea.Cmd - if key == "up" || key == "shift+tab" { - m.formFocus-- - } else { - m.formFocus++ - } - if m.formFocus > len(order) { - m.formFocus = 0 - } - if m.formFocus < 0 { - m.formFocus = len(order) - } - - switch order[m.formFocus] { - case "Watched on": - m.inputWatchedOn.PromptStyle = focusedStyle - m.inputWatchedOn.TextStyle = focusedStyle - cmds = append(cmds, m.inputWatchedOn.Focus()) - m.inputRating.Blur() - m.inputComment.Blur() - case "Rating": - m.inputRating.PromptStyle = focusedStyle - m.inputRating.TextStyle = focusedStyle - cmds = append(cmds, m.inputRating.Focus()) - m.inputWatchedOn.Blur() - m.inputComment.Blur() - case "Comment": - cmds = append(cmds, m.inputComment.Focus()) - m.inputWatchedOn.Blur() - m.inputRating.Blur() - } - - return cmds -} - func (m *tabEMDB) StoreMovie() tea.Cmd { return func() tea.Msg { updatedMovie := m.list.SelectedItem().(Movie) diff --git a/cmd/terminal-client/tui/tabreview.go b/cmd/terminal-client/tui/tabreview.go index 8913531..cb1f1ff 100644 --- a/cmd/terminal-client/tui/tabreview.go +++ b/cmd/terminal-client/tui/tabreview.go @@ -1,23 +1,47 @@ package tui import ( + "fmt" + "strconv" + "strings" + "ewintr.nl/emdb/client" + "ewintr.nl/emdb/cmd/api-service/moviestore" + "github.com/charmbracelet/bubbles/textarea" + "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) type tabReview struct { - initialized bool - emdb *client.EMDB - width int - height int - logger *Logger + initialized bool + emdb *client.EMDB + width int + height int + mode string + selectedReview moviestore.Review + inputQuality textinput.Model + inputMentions textarea.Model + formFocus int + logger *Logger } func NewTabReview(emdb *client.EMDB, logger *Logger) (tea.Model, tea.Cmd) { + inputQuality := textinput.New() + inputQuality.Prompt = "" + inputQuality.Width = 50 + inputQuality.CharLimit = 500 + inputMentions := textarea.New() + inputMentions.SetWidth(30) + inputMentions.SetHeight(5) + inputMentions.CharLimit = 500 + return &tabReview{ - emdb: emdb, - logger: logger, + emdb: emdb, + mode: "view", + inputQuality: inputQuality, + inputMentions: inputMentions, + logger: logger, }, nil } @@ -35,23 +59,155 @@ func (m *tabReview) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.width = msg.Width m.height = msg.Height case tea.KeyMsg: - switch msg.String() { - case "q", "esc", "ctrl+c": - return m, tea.Quit - case "right", "tab": - cmds = append(cmds, SelectNextTab()) - case "left", "shift+tab": - cmds = append(cmds, SelectPrevTab()) + switch m.mode { + case "edit": + switch msg.String() { + case "tab", "shift+tab", "up", "down": + cmds = append(cmds, m.NavigateForm(msg.String())...) + case "esc": + m.mode = "view" + case "enter": + m.mode = "view" + cmds = append(cmds, m.StoreReview()) + default: + cmds = append(cmds, m.updateFormInputs(msg)) + } + default: + switch msg.String() { + case "q", "esc", "ctrl+c": + return m, tea.Quit + case "right", "tab": + cmds = append(cmds, SelectNextTab()) + case "left", "shift+tab": + cmds = append(cmds, SelectPrevTab()) + case "e": + m.mode = "edit" + m.formFocus = 0 + cmds = append(cmds, m.inputQuality.Focus()) + case "n": + m.mode = "edit" + m.formFocus = 0 + m.logger.Log("fetching next unrated review") + cmds = append(cmds, m.inputQuality.Focus()) + cmds = append(cmds, FetchNextUnratedReview(m.emdb)) + } } + case moviestore.Review: + m.logger.Log(fmt.Sprintf("got review %s", msg.ID)) + m.selectedReview = msg + m.UpdateForm() + } return m, tea.Batch(cmds...) } func (m *tabReview) View() string { - return lipgloss.NewStyle(). - Width(m.width - 2). + colReviewWidth := m.width / 2 + colRateWidth := m.width - colReviewWidth + + colReview := lipgloss.NewStyle(). + Width(colReviewWidth - 2). Height(m.height - 2). Padding(1). - Render("Review") + Render(m.ViewReview()) + colRate := lipgloss.NewStyle(). + Width(colRateWidth - 2). + Height(m.height - 2). + Padding(1). + Render(m.ViewForm()) + + return lipgloss.JoinHorizontal(lipgloss.Top, colRate, colReview) +} + +func (m *tabReview) UpdateForm() { + mentions := strings.Join(m.selectedReview.Mentions, ",") + m.inputQuality.SetValue(fmt.Sprintf("%d", m.selectedReview.Quality)) + m.inputMentions.SetValue(mentions) +} + +func (m *tabReview) updateFormInputs(msg tea.Msg) tea.Cmd { + var cmd tea.Cmd + switch m.formFocus { + case 0: + m.inputQuality, cmd = m.inputQuality.Update(msg) + case 1: + m.inputMentions, cmd = m.inputMentions.Update(msg) + } + + return cmd +} + +func (m *tabReview) NavigateForm(key string) []tea.Cmd { + order := []string{"quality", "mentions"} + + var cmds []tea.Cmd + if key == "up" || key == "shift+tab" { + m.formFocus-- + } else { + m.formFocus++ + } + if m.formFocus >= len(order) { + m.formFocus = 0 + } + if m.formFocus < 0 { + m.formFocus = len(order) - 1 + } + + switch order[m.formFocus] { + case "quality": + m.inputQuality.PromptStyle = focusedStyle + m.inputQuality.TextStyle = focusedStyle + cmds = append(cmds, m.inputQuality.Focus()) + m.inputMentions.Blur() + case "mentions": + cmds = append(cmds, m.inputMentions.Focus()) + m.inputQuality.Blur() + } + + return cmds +} + +func (m *tabReview) ViewForm() string { + labels := "Quality:\nMentions:" + fields := fmt.Sprintf("%s\n%s", m.inputQuality.View(), m.inputMentions.View()) + + return lipgloss.JoinHorizontal(lipgloss.Left, labels, fields) +} + +func (m *tabReview) ViewReview() string { + review := strings.ReplaceAll(m.selectedReview.Review, "\n", "\n\n") + + return review +} + +func (m *tabReview) StoreReview() tea.Cmd { + return func() tea.Msg { + quality, err := strconv.Atoi(m.inputQuality.Value()) + if err != nil { + return err + } + mentions := m.inputMentions.Value() + + m.selectedReview.Quality = quality + m.selectedReview.Mentions = strings.Split(mentions, ",") + + review, err := m.emdb.UpdateReview(m.selectedReview) + if err != nil { + return err + } + + return review + } +} + +func FetchNextUnratedReview(emdb *client.EMDB) tea.Cmd { + return func() tea.Msg { + review, err := emdb.GetNextUnratedReview() + if err != nil { + return err + } + + return review + } }