Skip to content

Instantly share code, notes, and snippets.

@bashbunni
Last active January 11, 2023 15:15
Show Gist options
  • Save bashbunni/fed91563900a9f6e20cde881fe68ac31 to your computer and use it in GitHub Desktop.
Save bashbunni/fed91563900a9f6e20cde881fe68ac31 to your computer and use it in GitHub Desktop.
Validate Min Length Credit Card Example Bubble Tea
131a132,145
> func (m model) checkMinLen() error {
> var err error
>
> c := m.inputs[m.focused]
> if len(c.Value()) != c.CharLimit {
> err = fmt.Errorf(
> "%s should be at least %d characters",
> c.Value(),
> c.CharLimit,
> )
> }
> return err
> }
>
141a156
> m.err = m.checkMinLen()
167a183,201
> if m.err != nil {
> 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" +
> m.err.Error() + "\n"
> }
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.Run(); err != nil {
log.Fatal(err)
}
}
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) checkMinLen() error {
var err error
c := m.inputs[m.focused]
if len(c.Value()) != c.CharLimit {
err = fmt.Errorf(
"%s should be at least %d characters",
c.Value(),
c.CharLimit,
)
}
return err
}
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
}
m.err = m.checkMinLen()
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 {
if m.err != nil {
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" +
m.err.Error() + "\n"
}
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
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment