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.

Revisions

  1. bashbunni revised this gist Jan 11, 2023. 1 changed file with 37 additions and 0 deletions.
    37 changes: 37 additions & 0 deletions diff.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,37 @@
    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"
    > }
  2. bashbunni created this gist Jan 11, 2023.
    232 changes: 232 additions & 0 deletions main.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,232 @@
    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
    }
    }