phase 0.3
This commit is contained in:
commit
4a94bfc4e7
52 changed files with 11653 additions and 0 deletions
226
internal/adminui/countries.go
Normal file
226
internal/adminui/countries.go
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
package adminui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"skyrama-tui/internal/db"
|
||||
)
|
||||
|
||||
// ── Messages ──────────────────────────────────────────────────────────────────
|
||||
|
||||
type cntryLoadedMsg []db.AdminCountry
|
||||
type cntryDoneMsg struct{ err error }
|
||||
|
||||
// ── Field indices ─────────────────────────────────────────────────────────────
|
||||
|
||||
const (
|
||||
cfName = 0
|
||||
cfContinent = 1
|
||||
cfP1 = 2
|
||||
cfP2 = 3
|
||||
cfP3 = 4
|
||||
)
|
||||
|
||||
var continentOptions = []string{
|
||||
"void", "Africa", "Antarctica", "Asia",
|
||||
"Europe", "North America", "Oceania", "South America",
|
||||
}
|
||||
|
||||
// ── Model ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
type countriesTab struct {
|
||||
client *db.AdminClient
|
||||
items []db.AdminCountry
|
||||
cursor int
|
||||
inForm bool
|
||||
isEdit bool
|
||||
editID uint16
|
||||
fields []field
|
||||
focusIdx int
|
||||
status string
|
||||
isErr bool
|
||||
w, h int
|
||||
}
|
||||
|
||||
func newCountriesTab(client *db.AdminClient) *countriesTab {
|
||||
return &countriesTab{client: client}
|
||||
}
|
||||
|
||||
func (t *countriesTab) InForm() bool { return t.inForm }
|
||||
func (t *countriesTab) Init() tea.Cmd { return t.load() }
|
||||
|
||||
func (t *countriesTab) load() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
items, err := t.client.ListCountries()
|
||||
if err != nil {
|
||||
return cntryDoneMsg{err}
|
||||
}
|
||||
return cntryLoadedMsg(items)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *countriesTab) openForm() {
|
||||
t.fields = []field{
|
||||
newTextField("Name", "e.g. Turkey"),
|
||||
newEnumField("Continent", continentOptions),
|
||||
newTextField("Product 1 ID", "1"),
|
||||
newTextField("Product 2 ID", "2"),
|
||||
newTextField("Product 3 ID", "3"),
|
||||
}
|
||||
t.fields[cfName].input.Focus()
|
||||
t.focusIdx = cfName
|
||||
t.isEdit = false
|
||||
t.inForm = true
|
||||
t.status = ""
|
||||
}
|
||||
|
||||
func (t *countriesTab) openEditForm(c db.AdminCountry) {
|
||||
t.fields = []field{
|
||||
prefill("Name", c.Name),
|
||||
newEnumField("Continent", continentOptions),
|
||||
prefill("Product 1 ID", strconv.Itoa(int(c.P1))),
|
||||
prefill("Product 2 ID", strconv.Itoa(int(c.P2))),
|
||||
prefill("Product 3 ID", strconv.Itoa(int(c.P3))),
|
||||
}
|
||||
setEnumSel(&t.fields[cfContinent], c.Continent)
|
||||
t.fields[cfName].input.Focus()
|
||||
t.focusIdx = cfName
|
||||
t.isEdit = true
|
||||
t.editID = c.ID
|
||||
t.inForm = true
|
||||
t.status = ""
|
||||
}
|
||||
|
||||
func (t *countriesTab) submit() tea.Cmd {
|
||||
name := strings.TrimSpace(t.fields[cfName].value())
|
||||
continent := t.fields[cfContinent].value()
|
||||
p1s := strings.TrimSpace(t.fields[cfP1].value())
|
||||
p2s := strings.TrimSpace(t.fields[cfP2].value())
|
||||
p3s := strings.TrimSpace(t.fields[cfP3].value())
|
||||
|
||||
if name == "" {
|
||||
t.status = "name is required"
|
||||
t.isErr = true
|
||||
return nil
|
||||
}
|
||||
p1, e1 := strconv.ParseUint(p1s, 10, 16)
|
||||
p2, e2 := strconv.ParseUint(p2s, 10, 16)
|
||||
p3, e3 := strconv.ParseUint(p3s, 10, 16)
|
||||
if e1 != nil || e2 != nil || e3 != nil {
|
||||
t.status = "product IDs must be valid numbers"
|
||||
t.isErr = true
|
||||
return nil
|
||||
}
|
||||
if t.isEdit {
|
||||
id := t.editID
|
||||
return func() tea.Msg {
|
||||
return cntryDoneMsg{t.client.UpdateCountry(id, name, continent, uint16(p1), uint16(p2), uint16(p3))}
|
||||
}
|
||||
}
|
||||
return func() tea.Msg {
|
||||
return cntryDoneMsg{t.client.AddCountry(name, continent, uint16(p1), uint16(p2), uint16(p3))}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *countriesTab) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch m := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
t.w, t.h = m.Width, m.Height
|
||||
return t, nil
|
||||
|
||||
case cntryLoadedMsg:
|
||||
t.items = []db.AdminCountry(m)
|
||||
if t.cursor >= len(t.items) && t.cursor > 0 {
|
||||
t.cursor = len(t.items) - 1
|
||||
}
|
||||
return t, nil
|
||||
|
||||
case cntryDoneMsg:
|
||||
if m.err != nil {
|
||||
t.status = m.err.Error()
|
||||
t.isErr = true
|
||||
return t, nil
|
||||
}
|
||||
t.inForm = false
|
||||
t.status = "saved"
|
||||
t.isErr = false
|
||||
return t, t.load()
|
||||
|
||||
case tea.KeyMsg:
|
||||
if t.inForm {
|
||||
switch m.String() {
|
||||
case "esc":
|
||||
t.inForm = false
|
||||
t.status = ""
|
||||
case "ctrl+s":
|
||||
return t, t.submit()
|
||||
default:
|
||||
var cmd tea.Cmd
|
||||
t.focusIdx, cmd = handleFormKey(t.fields, t.focusIdx, m)
|
||||
return t, cmd
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
switch m.String() {
|
||||
case "q":
|
||||
return t, tea.Quit
|
||||
case "j", "down":
|
||||
if t.cursor < len(t.items)-1 {
|
||||
t.cursor++
|
||||
}
|
||||
case "k", "up":
|
||||
if t.cursor > 0 {
|
||||
t.cursor--
|
||||
}
|
||||
case "a":
|
||||
t.openForm()
|
||||
case "e":
|
||||
if len(t.items) > 0 {
|
||||
t.openEditForm(t.items[t.cursor])
|
||||
}
|
||||
case "r":
|
||||
return t, t.load()
|
||||
}
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (t *countriesTab) View() string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(hdrSt.Render(" Countries") + "\n\n")
|
||||
|
||||
if t.inForm {
|
||||
formTitle := "Add Country"
|
||||
if t.isEdit {
|
||||
formTitle = fmt.Sprintf("Edit Country #%d", t.editID)
|
||||
}
|
||||
sb.WriteString(titleSt.Render(" "+formTitle) + "\n\n")
|
||||
sb.WriteString(renderForm(t.fields, t.focusIdx))
|
||||
sb.WriteString("\n " + renderStatus(t.status, t.isErr))
|
||||
sb.WriteString("\n\n " + dimSt.Render(helpForm))
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
header := fmt.Sprintf(" %-5s %-20s %-14s %s", "ID", "Name", "Continent", "Products (P1/P2/P3)")
|
||||
sb.WriteString(dimSt.Render(header) + "\n")
|
||||
sb.WriteString(dimSt.Render(" " + strings.Repeat("─", 60)) + "\n")
|
||||
|
||||
if len(t.items) == 0 {
|
||||
sb.WriteString(dimSt.Render(" (no countries)") + "\n")
|
||||
}
|
||||
for i, c := range t.items {
|
||||
line := fmt.Sprintf(" %-5d %-20s %-14s %d / %d / %d",
|
||||
c.ID, c.Name, c.Continent, c.P1, c.P2, c.P3)
|
||||
if i == t.cursor {
|
||||
sb.WriteString(cursorSt.Render("▸") + activeTS.Render(line[1:]) + "\n")
|
||||
} else {
|
||||
sb.WriteString(line + "\n")
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n " + renderStatus(t.status, t.isErr))
|
||||
sb.WriteString("\n\n " + dimSt.Render("a:add e:edit j/k:nav r:reload q:quit"))
|
||||
return sb.String()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue