From d56d8ae854515dff7a7ebc02d8c9cbd93146b708 Mon Sep 17 00:00:00 2001 From: Maas Lalani Date: Mon, 13 Jun 2022 09:39:01 -0400 Subject: [PATCH] Example: Credit Card Input Form (#338) * feat(cc): Add Credit Card Input Form Example and `ValidatorFuncs` to ensure credit cards are valid --- examples/credit-card-form/main.go | 203 ++++++++++++++++++++++++++++++ examples/go.mod | 2 +- examples/go.sum | 2 + 3 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 examples/credit-card-form/main.go diff --git a/examples/credit-card-form/main.go b/examples/credit-card-form/main.go new file mode 100644 index 0000000000..f4a9293654 --- /dev/null +++ b/examples/credit-card-form/main.go @@ -0,0 +1,203 @@ +package main + +import ( + "fmt" + "log" + "strconv" + "strings" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +func main() { + p := tea.NewProgram(initialModel()) + + if err := p.Start(); err != nil { + log.Fatal(err) + } +} + +type tickMsg struct{} +type errMsg error + +const ( + ccn = iota + exp + cvv +) + +const ( + hotPink = lipgloss.Color("#FF06B7") + darkGray = lipgloss.Color("#767676") +) + +var ( + inputStyle = lipgloss.NewStyle().Foreground(hotPink) + continueStyle = lipgloss.NewStyle().Foreground(darkGray) +) + +type model struct { + inputs []textinput.Model + focused int + err error +} + +// Validator functions to ensure valid input +func ccnValidator(s string) error { + // Credit Card Number should a string less than 20 digits + // It should include 16 integers and 3 spaces + if len(s) > 16+3 { + return fmt.Errorf("CCN is too long") + } + + // The last digit should be a number unless it is a multiple of 4 in which + // case it should be a space + if len(s)%5 == 0 && s[len(s)-1] != ' ' { + return fmt.Errorf("CCN must separate groups with spaces") + } + if len(s)%5 != 0 && (s[len(s)-1] < '0' || s[len(s)-1] > '9') { + return fmt.Errorf("CCN is invalid") + } + + // The remaining digits should be integers + c := strings.ReplaceAll(s, " ", "") + _, err := strconv.ParseInt(c, 10, 64) + + return err +} + +func expValidator(s string) error { + // The 3 character should be a slash (/) + // The rest thould be numbers + e := strings.ReplaceAll(s, "/", "") + _, err := strconv.ParseInt(e, 10, 64) + if err != nil { + return fmt.Errorf("EXP is invalid") + } + + // There should be only one slash and it should be in the 2nd index (3rd character) + if len(s) >= 3 && (strings.Index(s, "/") != 2 || strings.LastIndex(s, "/") != 2) { + return fmt.Errorf("EXP is invalid") + } + + return nil +} + +func cvvValidator(s string) error { + // The CVV should be a number of 3 digits + // Since the input will already ensure that the CVV is a string of length 3, + // All we need to do is check that it is a number + _, err := strconv.ParseInt(s, 10, 64) + return err +} + +func initialModel() model { + var inputs []textinput.Model = make([]textinput.Model, 3) + inputs[ccn] = textinput.New() + inputs[ccn].Placeholder = "4505 **** **** 1234" + inputs[ccn].Focus() + inputs[ccn].CharLimit = 20 + inputs[ccn].Width = 30 + inputs[ccn].Prompt = "" + inputs[ccn].Validate = ccnValidator + + inputs[exp] = textinput.New() + inputs[exp].Placeholder = "MM/YY " + inputs[exp].CharLimit = 5 + inputs[exp].Width = 5 + inputs[exp].Prompt = "" + inputs[exp].Validate = expValidator + + inputs[cvv] = textinput.New() + inputs[cvv].Placeholder = "XXX" + inputs[cvv].CharLimit = 3 + inputs[cvv].Width = 5 + inputs[cvv].Prompt = "" + inputs[cvv].Validate = cvvValidator + + return model{ + inputs: inputs, + focused: 0, + err: nil, + } +} + +func (m model) Init() tea.Cmd { + return textinput.Blink +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var ( + cmds []tea.Cmd = make([]tea.Cmd, len(m.inputs)) + ) + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.Type { + case tea.KeyEnter: + if m.focused == len(m.inputs)-1 { + return m, tea.Quit + } else { + m.nextInput() + } + case tea.KeyCtrlC, tea.KeyEsc: + return m, tea.Quit + case tea.KeyShiftTab, tea.KeyCtrlP: + m.prevInput() + case tea.KeyTab, tea.KeyCtrlN: + m.nextInput() + } + for i := range m.inputs { + m.inputs[i].Blur() + } + m.inputs[m.focused].Focus() + + // We handle errors just like any other message + case errMsg: + m.err = msg + return m, nil + } + + for i := range m.inputs { + m.inputs[i], cmds[i] = m.inputs[i].Update(msg) + } + return m, tea.Batch(cmds...) +} + +func (m model) View() string { + return fmt.Sprintf( + ` Total: $21.50: + + %s + %s + + %s %s + %s %s + + %s +`, + inputStyle.Width(30).Render("Card Number"), + m.inputs[ccn].View(), + inputStyle.Width(6).Render("EXP"), + inputStyle.Width(6).Render("CVV"), + m.inputs[exp].View(), + m.inputs[cvv].View(), + continueStyle.Render("Continue ->"), + ) + "\n" +} + +// nextInput focuses the next input field +func (m *model) nextInput() { + m.focused = (m.focused + 1) % len(m.inputs) +} + +// prevInput focuses the previous input field +func (m *model) prevInput() { + m.focused-- + // Wrap around + if m.focused < 0 { + m.focused = len(m.inputs) - 1 + } +} diff --git a/examples/go.mod b/examples/go.mod index 7851f35f21..54d3cc6a7e 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -3,7 +3,7 @@ module examples go 1.13 require ( - github.com/charmbracelet/bubbles v0.11.0 + github.com/charmbracelet/bubbles v0.11.1-0.20220610161724-e57fd292cc68 github.com/charmbracelet/bubbletea v0.21.0 github.com/charmbracelet/glamour v0.5.0 github.com/charmbracelet/lipgloss v0.5.0 diff --git a/examples/go.sum b/examples/go.sum index 19af6250a3..b5de6a0084 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -6,6 +6,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/charmbracelet/bubbles v0.11.0 h1:fBLyY0PvJnd56Vlu5L84JJH6f4axhgIJ9P3NET78f0Q= github.com/charmbracelet/bubbles v0.11.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc= +github.com/charmbracelet/bubbles v0.11.1-0.20220610161724-e57fd292cc68 h1:oDxdCcM/JreVa7RTt2NQLdp06PwkApSL3huTwrOl/ww= +github.com/charmbracelet/bubbles v0.11.1-0.20220610161724-e57fd292cc68/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc= github.com/charmbracelet/glamour v0.5.0 h1:wu15ykPdB7X6chxugG/NNfDUbyyrCLV9XBalj5wdu3g= github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=