restructure and email validation

This commit is contained in:
GitBluub
2024-05-05 19:54:18 +02:00
parent 0ad421b10c
commit 8baaf5bae0
11 changed files with 370 additions and 113 deletions
+182
View File
@@ -0,0 +1,182 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
tea "github.com/charmbracelet/bubbletea"
"github.com/zoryia/vex/tui/models"
)
type statusMsg int
type errMsg struct{ error }
type missingJwtMsg struct{}
type noJwtMsg struct{}
type invalidJwtMsg struct{}
type httpErrorMsg error
func (e errMsg) Error() string { return e.error.Error() }
const serverUrl = "http://localhost:1597"
type loginSuccessMsg struct{ string }
type registerSuccessMsg struct{ string }
func getData(req *http.Request) ([]byte, error) {
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Print("err", err)
return nil, httpErrorMsg(err)
}
defer resp.Body.Close() // nolint: errcheck
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, httpErrorMsg(err)
}
return data, nil
}
func checkJwt(jwt *string) tea.Cmd {
return func() tea.Msg {
if jwt == nil || *jwt == "" {
return noJwtMsg{}
}
url := fmt.Sprintf("%s/me", serverUrl)
req, _ := http.NewRequest(http.MethodPost, url, nil)
req.Header.Add("Content-type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", *jwt))
data, err := getData(req)
if err != nil {
return invalidJwtMsg{}
}
var resp struct {
Id string `json:"id"`
Name string `json:"name"`
}
err = json.Unmarshal(data, &resp)
if err != nil {
return invalidJwtMsg{}
}
return nil
}
}
func login(username string, password string) tea.Cmd {
return func() tea.Msg {
url := fmt.Sprintf("%s/login", serverUrl)
body := struct {
Name string `json:"email"`
Password string `json:"password"`
}{
Name: username, Password: password,
}
out, err := json.Marshal(body)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(out))
req.Header.Add("Content-type", "application/json")
data, err := getData(req)
if err != nil {
return err
}
var loginResp AuthRes
err = json.Unmarshal(data, &loginResp)
if err != nil {
return httpErrorMsg(err)
}
return loginSuccessMsg{loginResp.Token}
}
}
type AuthRes struct {
Token string `json:"token"`
}
func register(username string, password string, email string) tea.Cmd {
return func() tea.Msg {
url := fmt.Sprintf("%s/register", serverUrl)
body := struct {
Name string `json:"name"`
Password string `json:"password"`
Email string `json:"email"`
}{
Name: username, Password: password, Email: email,
}
out, err := json.Marshal(body)
if err != nil {
log.Fatal(err)
}
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(out))
req.Header.Add("Content-type", "application/json")
data, err := getData(req)
if err != nil {
return err
}
var registerResp AuthRes
err = json.Unmarshal(data, &registerResp)
if err != nil {
return httpErrorMsg(err)
}
return registerSuccessMsg{registerResp.Token}
}
}
type getEntriesSuccessMsg []models.Entry
func getEntries(jwt *string) tea.Cmd {
return func() tea.Msg {
url := fmt.Sprintf("%s/entries", serverUrl)
req, _ := http.NewRequest(http.MethodGet, url, nil)
if jwt == nil {
return missingJwtMsg{}
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", *jwt))
req.Header.Add("Content-type", "application/json")
data, err := getData(req)
if err != nil {
return httpErrorMsg(err)
}
var entries []models.Entry
err = json.Unmarshal(data, &entries)
if err != nil {
return httpErrorMsg(err)
}
return getEntriesSuccessMsg(entries)
}
}
type getFeedsSuccessMsg []models.Feed
func getFeeds(jwt *string) tea.Cmd {
return func() tea.Msg {
url := fmt.Sprintf("%s/feeds", serverUrl)
req, _ := http.NewRequest(http.MethodGet, url, nil)
if jwt == nil {
return missingJwtMsg{}
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", *jwt))
data, err := getData(req)
if err != nil {
return httpErrorMsg(err)
}
var feeds []models.Feed
err = json.Unmarshal(data, &feeds)
if err != nil {
return httpErrorMsg(err)
}
return getFeedsSuccessMsg(feeds)
}
}
+285
View File
@@ -0,0 +1,285 @@
package main
import (
"log"
_ "log"
"os"
"strings"
"time"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
huh "github.com/charmbracelet/huh"
. "github.com/zoryia/vex/tui/models"
. "github.com/zoryia/vex/tui/pages"
"github.com/zoryia/vex/tui/pages/auth"
"github.com/zoryia/vex/tui/pages/preview"
)
type Model struct {
list list.Model
textInput textinput.Model
err error
auth auth.Model
page VexPage
query string
feeds []Feed
entries []Entry
tags []string
keys *ListKeyMap
Preview preview.Model
}
func New() *Model {
ti := textinput.New()
ti.Placeholder = "Pikachu"
ti.Focus()
ti.CharLimit = 156
ti.Width = 56
return &Model{textInput: ti, auth: auth.New(), page: ENTRIES, keys: NewListKeyMap(), Preview: preview.Model{Viewport: viewport.New(0, 0)}}
}
func (m Model) getEverything() tea.Cmd {
return func() tea.Msg {
return tea.Batch(getEntries(m.auth.Jwt)) // getTags, getFeeds)
}
}
func (m *Model) initList(width int, height int) {
m.list = list.New([]list.Item{}, list.NewDefaultDelegate(), width, height)
m.list.Title = "Posts"
m.list.SetFilteringEnabled(false)
var f = Feed{Id: "1", Tags: []string{"Devops", "Kubernetes"}, Name: "zwindler", Url: "zwindler.blog", FaviconUrl: "zwindler.blog.favicon"}
m.list.SetItems([]list.Item{
Entry{Id: "1", ArticleTitle: "yay", Content: "ouin ouin ouinouin ouin ouinouin ouin ouinouin ouin ouinouin ouin ouinouin ouin ouinouin ouin ouinouin ouin ouinouin ouin ouinouin ouin ouin", Link: "awd", Date: time.Now(), IsRead: false, IsIgnored: false, IsReadLater: false, IsBookmarked: false, Feed: f},
Entry{Id: "2", ArticleTitle: "grrrrr", Content: "ouin ouin ouin", Link: "awd", Date: time.Now(), IsRead: false, IsIgnored: false, IsReadLater: false, IsBookmarked: false, Feed: f},
Entry{Id: "3", ArticleTitle: "my life is pain", Content: "ouin ouin ouin", Link: "awd", Date: time.Now(), IsRead: false, IsIgnored: false, IsReadLater: false, IsBookmarked: false, Feed: f},
})
}
func (m Model) Init() tea.Cmd {
return tea.Batch(m.auth.LoginForm.Init(), m.auth.RegisterForm.Init(), checkJwt(m.auth.Jwt))
}
func (m Model) handleSearchCompletion() (Model, tea.Cmd) {
var cmd tea.Cmd
queryWords := strings.Split(m.textInput.Value(), " ")
if len(queryWords) == 0 {
return m, cmd
}
lastWord := queryWords[len(queryWords)-1]
if lastWord == "tag:" {
var feed string
huh.NewSelect[string]().
Title("Pick a feed.").
Options(
huh.NewOption("United States", "US"),
huh.NewOption("Germany", "DE"),
huh.NewOption("Brazil", "BR"),
huh.NewOption("Canada", "CA"),
).
Value(&feed).Run()
m.textInput.SetValue(m.textInput.Value() + feed)
m.textInput.CursorEnd()
}
if lastWord == "feed:" {
var feed string
var feeds = []string{"Devops", "System", "Angular"}
huh.NewSelect[string]().
Title("Pick a feed.").
Options(
huh.NewOptions(feeds...)...,
).
Value(&feed).Run()
m.textInput.SetValue(m.textInput.Value() + feed)
m.textInput.CursorEnd()
}
return m, cmd
}
func (m *Model) deleteWordBackward() {
if m.textInput.Position() == 0 || len(m.textInput.Value()) == 0 {
return
}
// TODO: wtf are other echo modes, dont care
//if m.textInput.EchoMode != textinput.EchoNormal {
// m.deleteBeforeCursor()
// return
//}
// Linter note: it's critical that we acquire the initial cursor position
// here prior to altering it via SetCursor() below. As such, moving this
// call into the corresponding if clause does not apply here.
oldPos := m.textInput.Position() //nolint:ifshort
m.textInput.SetCursor(oldPos - 1)
// ECHO character?
for m.textInput.Value()[m.textInput.Position()] == ' ' {
if m.textInput.Position() <= 0 {
break
}
// ignore series of whitespace before cursor
m.textInput.SetCursor(m.textInput.Position() - 1)
}
for m.textInput.Position() > 0 {
if m.textInput.Value()[m.textInput.Position()] != ' ' {
m.textInput.SetCursor(m.textInput.Position() - 1)
} else {
if m.textInput.Position() > 0 {
// keep the previous space
m.textInput.SetCursor(m.textInput.Position() + 1)
}
break
}
}
if oldPos > len(m.textInput.Value()) {
m.textInput.SetValue(m.textInput.Value()[:m.textInput.Position()])
} else {
m.textInput.SetValue(m.textInput.Value()[:m.textInput.Position()] + m.textInput.Value()[oldPos:])
}
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.initList(msg.Width, msg.Height)
m.Preview.Viewport.Width = msg.Width
m.Preview.Viewport.Height = msg.Height - m.Preview.VerticalMarginHeight()
case invalidJwtMsg:
m.auth.Jwt = new(string)
m.page = LOGIN
return m, nil
case loginSuccessMsg:
*m.auth.Jwt = msg.string
m.page = FEEDS
return m, m.getEverything()
case registerSuccessMsg:
*m.auth.Jwt = msg.string
m.page = FEEDS
return m, m.getEverything()
case tea.KeyMsg:
switch msg.Type {
case tea.KeyCtrlC, tea.KeyEsc:
return m, tea.Quit
case tea.KeyCtrlT:
if m.page == LOGIN {
m.page = REGISTER
} else if m.page == REGISTER {
m.page = LOGIN
}
}
switch {
case key.Matches(msg, m.textInput.KeyMap.DeleteCharacterBackward): //TODO: add only when query
words := strings.Split(m.textInput.Value(), " ")
if len(words) > 0 && (strings.HasPrefix(words[len(words)-1], "tag:") || strings.HasPrefix(words[len(words)-1], "feed:")) {
m.deleteWordBackward()
}
case key.Matches(msg, m.keys.IgnoreToggle) && m.page == "FEEDS":
// TODO: ignore the post
return m, nil
case key.Matches(msg, m.keys.ReadToggle):
// TODO: mark as read
return m, nil
case key.Matches(msg, m.keys.ReadLaterToggle):
// TODO: add to read later
return m, nil
case key.Matches(msg, m.keys.BookmarkToggle):
// TODO: toggle bookmark
return m, nil
case key.Matches(msg, m.keys.Query):
// TODO: launch query input
return m, nil
case key.Matches(msg, m.keys.PreviewPost):
var e = m.list.SelectedItem()
entry := e.(Entry)
m.Preview.Entry = entry
m.Preview.Viewport.SetContent(entry.Content)
m.page = PREVIEW
log.Print(entry.Content)
log.Print(m.Preview.Viewport.VisibleLineCount())
}
}
// Process the form
// LOGIN
if m.page == LOGIN {
form, cmd := m.auth.LoginForm.Update(msg)
if f, ok := form.(*huh.Form); ok {
m.auth.LoginForm = f
cmds = append(cmds, cmd)
}
if m.auth.LoginForm.State == huh.StateCompleted {
username := m.auth.LoginForm.GetString("email")
password := m.auth.LoginForm.GetString("password")
cmds = append(cmds, login(username, password))
}
}
if m.page == REGISTER {
// Process the form
// LOGIN
registerForm, cmd := m.auth.RegisterForm.Update(msg)
if f, ok := registerForm.(*huh.Form); ok {
m.auth.RegisterForm = f
cmds = append(cmds, cmd)
}
if m.auth.RegisterForm.State == huh.StateCompleted {
username := m.auth.RegisterForm.GetString("username")
password := m.auth.RegisterForm.GetString("password")
email := m.auth.RegisterForm.GetString("email")
cmds = append(cmds, register(username, password, email))
}
}
var cmd tea.Cmd
m.Preview.Viewport, cmd = m.Preview.Viewport.Update(msg)
cmds = append(cmds, cmd)
m.list, cmd = m.list.Update(msg)
cmds = append(cmds, cmd)
m.textInput, cmd = m.textInput.Update(msg)
cmds = append(cmds, cmd)
switch msg := msg.(type) {
case tea.KeyMsg:
_ = msg
m, cmd = m.handleSearchCompletion()
cmds = append(cmds, cmd)
}
return m, tea.Batch(cmds...)
}
func main() {
tea.LogToFile("vex.log", "")
m := New()
p := tea.NewProgram(m,
tea.WithAltScreen(), // use the full size of the terminal in its "alternate screen buffer"
tea.WithMouseCellMotion(),
)
if _, err := p.Run(); err != nil {
os.Exit(1)
}
}
+42
View File
@@ -0,0 +1,42 @@
package main
import (
"fmt"
"github.com/charmbracelet/lipgloss"
. "github.com/zoryia/vex/tui/pages"
)
func (m Model) AuthView() string {
return lipgloss.JoinHorizontal(
lipgloss.Left,
m.auth.LoginForm.View(),
m.auth.RegisterForm.View(),
)
}
func (m Model) EntriesView() string {
return m.list.View()
}
func (m Model) FeedsView() string {
return fmt.Sprintf("%s ", *m.auth.Jwt)
}
func (m Model) TagsView() string {
return ""
}
func (m Model) View() string {
switch m.page {
case LOGIN:
return m.AuthView()
case REGISTER:
return m.AuthView()
case ENTRIES:
return m.EntriesView()
case FEEDS:
return m.FeedsView()
case TAGS:
return m.TagsView()
case PREVIEW:
return m.Preview.View()
}
return "Really unexpected state, get help"
}