Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example: Credit Card Input Form #338

Merged
merged 2 commits into from Jun 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
203 changes: 203 additions & 0 deletions 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
}
}
2 changes: 1 addition & 1 deletion examples/go.mod
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions examples/go.sum
Expand Up @@ -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=
Expand Down