mirror of
https://github.com/zoriya/vex.git
synced 2026-08-16 02:44:55 +00:00
restructure and email validation
This commit is contained in:
+182
@@ -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, ®isterResp)
|
||||
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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user