Last active
January 11, 2023 15:15
-
-
Save bashbunni/fed91563900a9f6e20cde881fe68ac31 to your computer and use it in GitHub Desktop.
Validate Min Length Credit Card Example Bubble Tea
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | |
> } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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