forked from Mirrors/bubbletea
Example: Credit Card Input Form (#338)
* feat(cc): Add Credit Card Input Form Example and `ValidatorFuncs` to ensure credit cards are valid
This commit is contained in:
parent
21de41ac02
commit
d56d8ae854
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ module examples
|
||||||
go 1.13
|
go 1.13
|
||||||
|
|
||||||
require (
|
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/bubbletea v0.21.0
|
||||||
github.com/charmbracelet/glamour v0.5.0
|
github.com/charmbracelet/glamour v0.5.0
|
||||||
github.com/charmbracelet/lipgloss v0.5.0
|
github.com/charmbracelet/lipgloss v0.5.0
|
||||||
|
|
|
@ -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/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 h1:fBLyY0PvJnd56Vlu5L84JJH6f4axhgIJ9P3NET78f0Q=
|
||||||
github.com/charmbracelet/bubbles v0.11.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=
|
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 h1:wu15ykPdB7X6chxugG/NNfDUbyyrCLV9XBalj5wdu3g=
|
||||||
github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc=
|
github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc=
|
||||||
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||||
|
|
Loading…
Reference in New Issue