From c9aee8ef20faf08e12b616746c47a5655ef7941d Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sat, 8 Nov 2025 18:33:43 -0800 Subject: [PATCH 01/73] initial era selection screen (#201) --- cmd/card/card.go | 134 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 cmd/card/card.go diff --git a/cmd/card/card.go b/cmd/card/card.go new file mode 100644 index 00000000..f5072b04 --- /dev/null +++ b/cmd/card/card.go @@ -0,0 +1,134 @@ +package card + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/digitalghost-dev/poke-cli/cmd/utils" + "github.com/digitalghost-dev/poke-cli/styling" +) + +func CardCommand() (string, error) { + var output strings.Builder + + flag.Usage = func() { + helpMessage := styling.HelpBorder.Render( + "View data about cards from the TCG!\n\n", + styling.StyleBold.Render("USAGE:"), + fmt.Sprintf("\n\t%s %s %s", "poke-cli", styling.StyleBold.Render("card"), "[flag]"), + "\n\n", + styling.StyleBold.Render("FLAGS:"), + fmt.Sprintf("\n\t%-30s %s", "-h, --help", "Prints out the help menu"), + ) + output.WriteString(helpMessage) + } + + flag.Parse() + + // Handle help flag + if len(os.Args) == 3 && (os.Args[2] == "-h" || os.Args[2] == "--help") { + flag.Usage() + return output.String(), nil + } + + // Validate arguments + if err := utils.ValidateCardArgs(os.Args); err != nil { + output.WriteString(err.Error()) + return output.String(), err + } + + tableGeneration() + + return output.String(), nil +} + +type model struct { + quitting bool + table table.Model + selectedOption string +} + +// Init initializes the model +func (m model) Init() tea.Cmd { + return nil +} + +// Update handles user input and updates the model state +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var bubbleCmd tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "esc", "ctrl+c": + m.quitting = true + return m, tea.Quit + case "enter": + m.selectedOption = m.table.SelectedRow()[0] + return m, tea.Quit + } + } + + // Handle other updates (like navigation) + m.table, bubbleCmd = m.table.Update(msg) + return m, bubbleCmd +} + +// View renders the current UI +func (m model) View() string { + if m.quitting { + return "\n Goodbye! \n" + } + + // Don't render anything if a selection has been made + if m.selectedOption != "" { + return "" + } + + // Render the type selection table with instructions + return fmt.Sprintf("Select an era\n%s\n%s", + styling.TypesTableBorder.Render(m.table.View()), + styling.KeyMenu.Render("↑ (move up) • ↓ (move down)\nenter (select) • ctrl+c | esc (quit)")) +} + +func tableGeneration() { + namesList := []string{ + "Sword & Shield", + "Scarlet & Violet", + } + + rows := make([]table.Row, len(namesList)) + for i, n := range namesList { + rows[i] = []string{n} + } + + t := table.New( + table.WithColumns([]table.Column{{Title: "Era", Width: 16}}), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(10), + ) + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#FFCC00")). + BorderBottom(true) + s.Selected = s.Selected. + Foreground(lipgloss.Color("#000")). + Background(lipgloss.Color("#FFCC00")) + t.SetStyles(s) + + m := model{table: t} + _, err := tea.NewProgram(m, tea.WithAltScreen()).Run() + + if err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} From 44e6ea9cbcde36e96193f0968d2b4dc0fc9beab4 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sat, 8 Nov 2025 21:29:31 -0800 Subject: [PATCH 02/73] sorting imports --- cmd/search/model_input.go | 3 ++- cmd/search/model_input_test.go | 3 ++- cmd/search/model_selection.go | 1 + cmd/search/parse.go | 3 ++- cmd/search/search.go | 3 ++- cmd/search/search_test.go | 5 +++-- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/cmd/search/model_input.go b/cmd/search/model_input.go index 82457e32..2eaf57bb 100644 --- a/cmd/search/model_input.go +++ b/cmd/search/model_input.go @@ -2,11 +2,12 @@ package search import ( "fmt" + "strings" + "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/digitalghost-dev/poke-cli/styling" - "strings" ) // UpdateInput handles text input updates. diff --git a/cmd/search/model_input_test.go b/cmd/search/model_input_test.go index 4b3be154..3d71a251 100644 --- a/cmd/search/model_input_test.go +++ b/cmd/search/model_input_test.go @@ -1,9 +1,10 @@ package search import ( + "testing" + "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" - "testing" ) func TestUpdateInput(t *testing.T) { diff --git a/cmd/search/model_selection.go b/cmd/search/model_selection.go index 02f4a65e..02f5d573 100644 --- a/cmd/search/model_selection.go +++ b/cmd/search/model_selection.go @@ -2,6 +2,7 @@ package search import ( "fmt" + "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/digitalghost-dev/poke-cli/styling" diff --git a/cmd/search/parse.go b/cmd/search/parse.go index 284d3416..50b7e7b9 100644 --- a/cmd/search/parse.go +++ b/cmd/search/parse.go @@ -1,8 +1,9 @@ package search import ( - "github.com/digitalghost-dev/poke-cli/connections" "strings" + + "github.com/digitalghost-dev/poke-cli/connections" ) type Resource struct { diff --git a/cmd/search/search.go b/cmd/search/search.go index b99370a0..d86f6af6 100644 --- a/cmd/search/search.go +++ b/cmd/search/search.go @@ -3,11 +3,12 @@ package search import ( "flag" "fmt" + "os" + "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/styling" - "os" ) func SearchCommand() { diff --git a/cmd/search/search_test.go b/cmd/search/search_test.go index 9033437b..93807740 100644 --- a/cmd/search/search_test.go +++ b/cmd/search/search_test.go @@ -3,12 +3,13 @@ package search import ( "bytes" "fmt" - tea "github.com/charmbracelet/bubbletea" - "github.com/digitalghost-dev/poke-cli/styling" "os" "strings" "testing" + tea "github.com/charmbracelet/bubbletea" + "github.com/digitalghost-dev/poke-cli/styling" + "github.com/stretchr/testify/assert" ) From ee1e7e9884a53cb312033e56e5d1ab437d43b1a1 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sat, 8 Nov 2025 21:59:38 -0800 Subject: [PATCH 03/73] switching to list for expansions instead of table (#201) --- cmd/card/card.go | 101 +++++++------------------------------ cmd/card/expansion_list.go | 91 +++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 83 deletions(-) create mode 100644 cmd/card/expansion_list.go diff --git a/cmd/card/card.go b/cmd/card/card.go index f5072b04..750b6eff 100644 --- a/cmd/card/card.go +++ b/cmd/card/card.go @@ -6,9 +6,8 @@ import ( "os" "strings" - "github.com/charmbracelet/bubbles/table" + "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/styling" ) @@ -42,93 +41,29 @@ func CardCommand() (string, error) { return output.String(), err } - tableGeneration() - - return output.String(), nil -} - -type model struct { - quitting bool - table table.Model - selectedOption string -} - -// Init initializes the model -func (m model) Init() tea.Cmd { - return nil -} - -// Update handles user input and updates the model state -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var bubbleCmd tea.Cmd - - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "esc", "ctrl+c": - m.quitting = true - return m, tea.Quit - case "enter": - m.selectedOption = m.table.SelectedRow()[0] - return m, tea.Quit - } + items := []list.Item{ + item("Mega Evolution"), + item("Scarlet & Violet"), + item("Sword & Shield"), } - // Handle other updates (like navigation) - m.table, bubbleCmd = m.table.Update(msg) - return m, bubbleCmd -} + const listWidth = 20 + const listHeight = 10 -// View renders the current UI -func (m model) View() string { - if m.quitting { - return "\n Goodbye! \n" - } + l := list.New(items, itemDelegate{}, listWidth, listHeight) + l.Title = "First, pick an expansion" + l.SetShowStatusBar(false) + l.SetFilteringEnabled(true) + l.Styles.Title = titleStyle + l.Styles.PaginationStyle = paginationStyle + l.Styles.HelpStyle = helpStyle - // Don't render anything if a selection has been made - if m.selectedOption != "" { - return "" - } + m := ExpansionModel{list: l} - // Render the type selection table with instructions - return fmt.Sprintf("Select an era\n%s\n%s", - styling.TypesTableBorder.Render(m.table.View()), - styling.KeyMenu.Render("↑ (move up) • ↓ (move down)\nenter (select) • ctrl+c | esc (quit)")) -} - -func tableGeneration() { - namesList := []string{ - "Sword & Shield", - "Scarlet & Violet", - } - - rows := make([]table.Row, len(namesList)) - for i, n := range namesList { - rows[i] = []string{n} - } - - t := table.New( - table.WithColumns([]table.Column{{Title: "Era", Width: 16}}), - table.WithRows(rows), - table.WithFocused(true), - table.WithHeight(10), - ) - - s := table.DefaultStyles() - s.Header = s.Header. - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("#FFCC00")). - BorderBottom(true) - s.Selected = s.Selected. - Foreground(lipgloss.Color("#000")). - Background(lipgloss.Color("#FFCC00")) - t.SetStyles(s) - - m := model{table: t} - _, err := tea.NewProgram(m, tea.WithAltScreen()).Run() - - if err != nil { + if _, err := tea.NewProgram(m, tea.WithAltScreen()).Run(); err != nil { fmt.Println("Error running program:", err) os.Exit(1) } + + return output.String(), nil } diff --git a/cmd/card/expansion_list.go b/cmd/card/expansion_list.go new file mode 100644 index 00000000..02e14add --- /dev/null +++ b/cmd/card/expansion_list.go @@ -0,0 +1,91 @@ +package card + +import ( + "fmt" + "io" + "strings" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +var ( + titleStyle = lipgloss.NewStyle().MarginLeft(2) + itemStyle = lipgloss.NewStyle().PaddingLeft(4) + selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.AdaptiveColor{Light: "#E1AD01", Dark: "#FFDE00"}) + paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) + helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) + quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4) +) + +type item string + +func (i item) FilterValue() string { return "" } + +type itemDelegate struct{} + +func (d itemDelegate) Height() int { return 1 } +func (d itemDelegate) Spacing() int { return 0 } +func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } +func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + i, ok := listItem.(item) + if !ok { + return + } + + str := fmt.Sprintf("%d. %s", index+1, i) + + fn := itemStyle.Render + if index == m.Index() { + fn = func(s ...string) string { + return selectedItemStyle.Render("> " + strings.Join(s, " ")) + } + } + + fmt.Fprint(w, fn(str)) +} + +type ExpansionModel struct { + list list.Model + choice string + quitting bool +} + +func (m ExpansionModel) Init() tea.Cmd { + return nil +} + +func (m ExpansionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.list.SetWidth(msg.Width) + return m, nil + + case tea.KeyMsg: + switch keypress := msg.String(); keypress { + case "q", "ctrl+c": + m.quitting = true + return m, tea.Quit + + case "enter": + i, ok := m.list.SelectedItem().(item) + if ok { + m.choice = string(i) + } + return m, tea.Quit + } + } + + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd +} + +func (m ExpansionModel) View() string { + if m.choice != "" { + return quitTextStyle.Render(fmt.Sprintf("%s? Sounds good to me.", m.choice)) + } + + return "\n" + m.list.View() +} From 2c98e6cecdc87159fe7105868470ad0f95a55974 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sun, 9 Nov 2025 11:04:10 -0800 Subject: [PATCH 04/73] standardizing key handling patterns (#202) --- cmd/card/expansion_list.go | 18 ++++++++++-------- cmd/search/search.go | 12 ++++++------ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/cmd/card/expansion_list.go b/cmd/card/expansion_list.go index 02e14add..e46aff40 100644 --- a/cmd/card/expansion_list.go +++ b/cmd/card/expansion_list.go @@ -34,7 +34,7 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list return } - str := fmt.Sprintf("%d. %s", index+1, i) + str := fmt.Sprintf("%s", i) fn := itemStyle.Render if index == m.Index() { @@ -58,16 +58,11 @@ func (m ExpansionModel) Init() tea.Cmd { func (m ExpansionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.list.SetWidth(msg.Width) - return m, nil - case tea.KeyMsg: - switch keypress := msg.String(); keypress { - case "q", "ctrl+c": + switch msg.String() { + case "ctrl+c", "esc": m.quitting = true return m, tea.Quit - case "enter": i, ok := m.list.SelectedItem().(item) if ok { @@ -75,6 +70,10 @@ func (m ExpansionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, tea.Quit } + + case tea.WindowSizeMsg: + m.list.SetWidth(msg.Width) + return m, nil } var cmd tea.Cmd @@ -83,6 +82,9 @@ func (m ExpansionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m ExpansionModel) View() string { + if m.quitting { + return "\n Quitting card search...\n\n" + } if m.choice != "" { return quitTextStyle.Render(fmt.Sprintf("%s? Sounds good to me.", m.choice)) } diff --git a/cmd/search/search.go b/cmd/search/search.go index d86f6af6..7c3cd1b3 100644 --- a/cmd/search/search.go +++ b/cmd/search/search.go @@ -73,17 +73,17 @@ func (m Model) Init() tea.Cmd { // Update handles keypresses and updates the state. func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - if msg, ok := msg.(tea.KeyMsg); ok { - k := msg.String() - if k == "esc" || k == "ctrl+c" { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "esc": m.Quitting = true return m, tea.Quit + case "enter": + return UpdateSelection(msg, m) } } - if !m.Chosen { - return UpdateSelection(msg, m) - } return UpdateInput(msg, m) } From cc57ad8e3f50d4769cf63adfceacaeae990b6914 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sun, 9 Nov 2025 11:31:13 -0800 Subject: [PATCH 05/73] fixing key navigation issues --- cmd/search/search.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/search/search.go b/cmd/search/search.go index 7c3cd1b3..c103fc83 100644 --- a/cmd/search/search.go +++ b/cmd/search/search.go @@ -79,11 +79,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "ctrl+c", "esc": m.Quitting = true return m, tea.Quit - case "enter": - return UpdateSelection(msg, m) } } + if !m.Chosen { + return UpdateSelection(msg, m) + } return UpdateInput(msg, m) } From 80be6c14cc888b1173d4af0facde01724b8d6c53 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sun, 9 Nov 2025 13:49:09 -0800 Subject: [PATCH 06/73] updating version numbers --- .github/workflows/ci.yml | 2 +- .goreleaser.yml | 2 +- Dockerfile | 2 +- README.md | 6 +++--- card_data/pipelines/poke_cli_dbt/dbt_project.yml | 2 +- nfpm.yaml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a95243e8..155c5c75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ on: - main env: - VERSION_NUMBER: 'v1.7.4' + VERSION_NUMBER: 'v1.8.0' DOCKERHUB_REGISTRY_NAME: 'digitalghostdev/poke-cli' AWS_REGION: 'us-west-2' diff --git a/.goreleaser.yml b/.goreleaser.yml index 04c0950f..16995b20 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -14,7 +14,7 @@ builds: - windows - darwin ldflags: - - -s -w -X main.version=v1.7.4 + - -s -w -X main.version=v1.8.0 archives: - formats: [ 'zip' ] diff --git a/Dockerfile b/Dockerfile index 8db3b266..7605a0c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN go mod download COPY . . -RUN go build -ldflags "-X main.version=v1.7.4" -o poke-cli . +RUN go build -ldflags "-X main.version=v1.8.0" -o poke-cli . # build 2 FROM --platform=$BUILDPLATFORM alpine:3.22 diff --git a/README.md b/README.md index a4529df4..f1a6d95f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ pokemon-logo

Pokémon CLI

version-label - docker-image-size + docker-image-size ci-status-badge
@@ -94,11 +94,11 @@ Cloudsmith is a fully cloud-based service that lets you easily create, store, an 3. Choose how to interact with the container: * Run a single command and exit: ```bash - docker run --rm -it digitalghostdev/poke-cli:v1.7.4 [subcommand] flag] + docker run --rm -it digitalghostdev/poke-cli:v1.8.0 [subcommand] flag] ``` * Enter the container and use its shell: ```bash - docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.7.4 -c "cd /app && exec sh" + docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.8.0 -c "cd /app && exec sh" # placed into the /app directory, run the program with './poke-cli' # example: ./poke-cli ability swift-swim ``` diff --git a/card_data/pipelines/poke_cli_dbt/dbt_project.yml b/card_data/pipelines/poke_cli_dbt/dbt_project.yml index 104f295f..06c038e4 100644 --- a/card_data/pipelines/poke_cli_dbt/dbt_project.yml +++ b/card_data/pipelines/poke_cli_dbt/dbt_project.yml @@ -1,5 +1,5 @@ name: 'poke_cli_dbt' -version: '1.7.4' +version: '1.8.0' profile: 'poke_cli_dbt' diff --git a/nfpm.yaml b/nfpm.yaml index d5ff4ad5..dca2fa6d 100644 --- a/nfpm.yaml +++ b/nfpm.yaml @@ -1,7 +1,7 @@ name: "poke-cli" arch: "arm64" platform: "linux" -version: "v1.7.4" +version: "v1.8.0" section: "default" version_schema: semver maintainer: "Christian S" From 988f42a5396d2568960b1fc232ac1aef40a955ab Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sun, 9 Nov 2025 14:28:07 -0800 Subject: [PATCH 07/73] renaming model name (#201) --- cmd/card/expansion_list.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/cmd/card/expansion_list.go b/cmd/card/expansion_list.go index e46aff40..cc18facc 100644 --- a/cmd/card/expansion_list.go +++ b/cmd/card/expansion_list.go @@ -46,48 +46,48 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list fmt.Fprint(w, fn(str)) } -type ExpansionModel struct { - list list.Model - choice string - quitting bool +type SeriesModel struct { + List list.Model + Choice string + Quitting bool } -func (m ExpansionModel) Init() tea.Cmd { +func (m SeriesModel) Init() tea.Cmd { return nil } -func (m ExpansionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m SeriesModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "esc": - m.quitting = true + m.Quitting = true return m, tea.Quit case "enter": - i, ok := m.list.SelectedItem().(item) + i, ok := m.List.SelectedItem().(item) if ok { - m.choice = string(i) + m.Choice = string(i) } return m, tea.Quit } case tea.WindowSizeMsg: - m.list.SetWidth(msg.Width) + m.List.SetWidth(msg.Width) return m, nil } var cmd tea.Cmd - m.list, cmd = m.list.Update(msg) + m.List, cmd = m.List.Update(msg) return m, cmd } -func (m ExpansionModel) View() string { - if m.quitting { +func (m SeriesModel) View() string { + if m.Quitting { return "\n Quitting card search...\n\n" } - if m.choice != "" { - return quitTextStyle.Render(fmt.Sprintf("%s? Sounds good to me.", m.choice)) + if m.Choice != "" { + return quitTextStyle.Render(fmt.Sprintf("%s? Sounds good to me.", m.Choice)) } - return "\n" + m.list.View() + return "\n" + m.List.View() } From 2e7d22df02fc37bb2834655f7d9b7608fb86728e Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sun, 9 Nov 2025 14:32:36 -0800 Subject: [PATCH 08/73] renaming file (#201) --- cmd/card/{expansion_list.go => set_list.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cmd/card/{expansion_list.go => set_list.go} (100%) diff --git a/cmd/card/expansion_list.go b/cmd/card/set_list.go similarity index 100% rename from cmd/card/expansion_list.go rename to cmd/card/set_list.go From 7336deb03a44b6a518971ea3a729feb9e28a5eee Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sun, 9 Nov 2025 14:33:25 -0800 Subject: [PATCH 09/73] renaming file (#201) --- cmd/card/{set_list.go => series_list.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cmd/card/{set_list.go => series_list.go} (100%) diff --git a/cmd/card/set_list.go b/cmd/card/series_list.go similarity index 100% rename from cmd/card/set_list.go rename to cmd/card/series_list.go From 06c650b7dcbc4ac57533bdfe2bb3151e9ba98ea0 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sun, 9 Nov 2025 17:39:09 -0800 Subject: [PATCH 10/73] adding test file --- cmd/card/series_list_test.go | 180 +++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 cmd/card/series_list_test.go diff --git a/cmd/card/series_list_test.go b/cmd/card/series_list_test.go new file mode 100644 index 00000000..7a0bd18c --- /dev/null +++ b/cmd/card/series_list_test.go @@ -0,0 +1,180 @@ +package card + +import ( + "testing" + "time" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/x/exp/teatest" +) + +func TestSeriesModelInit(t *testing.T) { + items := []list.Item{ + item("Mega Evolution"), + item("Scarlet & Violet"), + item("Sword & Shield"), + } + l := list.New(items, itemDelegate{}, 20, 12) + model := SeriesModel{List: l} + + cmd := model.Init() + if cmd != nil { + t.Errorf("Expected Init() to return nil, got %v", cmd) + } +} + +func TestSeriesModelQuit(t *testing.T) { + items := []list.Item{ + item("Mega Evolution"), + item("Scarlet & Violet"), + item("Sword & Shield"), + } + l := list.New(items, itemDelegate{}, 20, 12) + model := SeriesModel{List: l} + + testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(80, 24)) + + // Test ctrl+c quit + testModel.Send(tea.KeyMsg{Type: tea.KeyCtrlC}) + testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) + + final := testModel.FinalModel(t).(SeriesModel) + + if !final.Quitting { + t.Errorf("Expected model to be quitting after ctrl+c") + } +} + +func TestSeriesModelEscQuit(t *testing.T) { + items := []list.Item{ + item("Mega Evolution"), + item("Scarlet & Violet"), + item("Sword & Shield"), + } + l := list.New(items, itemDelegate{}, 20, 12) + model := SeriesModel{List: l} + + testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(80, 24)) + + // Test esc quit + testModel.Send(tea.KeyMsg{Type: tea.KeyEsc}) + testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) + + final := testModel.FinalModel(t).(SeriesModel) + + if !final.Quitting { + t.Errorf("Expected model to be quitting after esc") + } +} + +func TestSeriesModelSelection(t *testing.T) { + items := []list.Item{ + item("Mega Evolution"), + item("Scarlet & Violet"), + item("Sword & Shield"), + } + l := list.New(items, itemDelegate{}, 20, 12) + model := SeriesModel{List: l} + + testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(80, 24)) + + // Navigate and select + testModel.Send(tea.KeyMsg{Type: tea.KeyDown}) // Move to second item + testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) // Select it + testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) + + final := testModel.FinalModel(t).(SeriesModel) + + if final.Choice == "" { + t.Errorf("Expected a choice to be made, got empty string") + } + if final.Choice != "Scarlet & Violet" { + t.Errorf("Expected choice to be 'Scarlet & Violet', got '%s'", final.Choice) + } +} + +func TestSeriesModelWindowResize(t *testing.T) { + items := []list.Item{ + item("Mega Evolution"), + item("Scarlet & Violet"), + item("Sword & Shield"), + } + l := list.New(items, itemDelegate{}, 20, 12) + model := SeriesModel{List: l} + + // Send window resize message + updatedModel, _ := model.Update(tea.WindowSizeMsg{Width: 100, Height: 30}) + finalModel := updatedModel.(SeriesModel) + + if finalModel.List.Width() != 100 { + t.Errorf("Expected list width to be 100 after resize, got %d", finalModel.List.Width()) + } +} + +func TestSeriesModelView(t *testing.T) { + items := []list.Item{ + item("Mega Evolution"), + item("Scarlet & Violet"), + item("Sword & Shield"), + } + l := list.New(items, itemDelegate{}, 20, 12) + + // Test normal view + model := SeriesModel{List: l} + view := model.View() + if view == "" { + t.Errorf("Expected non-empty view, got empty string") + } + + // Test quitting view + model.Quitting = true + view = model.View() + if view != "\n Quitting card search...\n\n" { + t.Errorf("Expected quitting message, got '%s'", view) + } + + // Test choice made view + model.Quitting = false + model.Choice = "Scarlet & Violet" + view = model.View() + if view == "" { + t.Errorf("Expected non-empty view for choice, got empty string") + } +} + +func TestItemFilterValue(t *testing.T) { + testItem := item("Test Item") + filterValue := testItem.FilterValue() + + if filterValue != "" { + t.Errorf("Expected FilterValue to return empty string, got '%s'", filterValue) + } +} + +func TestItemDelegateHeight(t *testing.T) { + delegate := itemDelegate{} + height := delegate.Height() + + if height != 1 { + t.Errorf("Expected Height to return 1, got %d", height) + } +} + +func TestItemDelegateSpacing(t *testing.T) { + delegate := itemDelegate{} + spacing := delegate.Spacing() + + if spacing != 0 { + t.Errorf("Expected Spacing to return 0, got %d", spacing) + } +} + +func TestItemDelegateUpdate(t *testing.T) { + delegate := itemDelegate{} + cmd := delegate.Update(tea.KeyMsg{}, &list.Model{}) + + if cmd != nil { + t.Errorf("Expected Update to return nil, got %v", cmd) + } +} From fb77b44f164067a2fdd02593f71b63a247db2da1 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sun, 9 Nov 2025 17:52:56 -0800 Subject: [PATCH 11/73] removing `tea.WithAltScreen()` and editing settings (#201) --- cmd/card/card.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/card/card.go b/cmd/card/card.go index 750b6eff..21f5b0f5 100644 --- a/cmd/card/card.go +++ b/cmd/card/card.go @@ -48,19 +48,19 @@ func CardCommand() (string, error) { } const listWidth = 20 - const listHeight = 10 + const listHeight = 12 l := list.New(items, itemDelegate{}, listWidth, listHeight) - l.Title = "First, pick an expansion" + l.Title = "First, pick a series" l.SetShowStatusBar(false) - l.SetFilteringEnabled(true) + l.SetFilteringEnabled(false) l.Styles.Title = titleStyle l.Styles.PaginationStyle = paginationStyle l.Styles.HelpStyle = helpStyle - m := ExpansionModel{list: l} + m := SeriesModel{List: l} - if _, err := tea.NewProgram(m, tea.WithAltScreen()).Run(); err != nil { + if _, err := tea.NewProgram(m).Run(); err != nil { fmt.Println("Error running program:", err) os.Exit(1) } From fdc9984f01437098e019451f597e47de4534a542 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 10 Nov 2025 09:41:40 -0800 Subject: [PATCH 12/73] capture series ID for set filtering, adding list function (#201) --- cmd/card/series_list.go | 64 +++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 37 deletions(-) diff --git a/cmd/card/series_list.go b/cmd/card/series_list.go index cc18facc..4960fa6f 100644 --- a/cmd/card/series_list.go +++ b/cmd/card/series_list.go @@ -2,53 +2,21 @@ package card import ( "fmt" - "io" - "strings" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" ) -var ( - titleStyle = lipgloss.NewStyle().MarginLeft(2) - itemStyle = lipgloss.NewStyle().PaddingLeft(4) - selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.AdaptiveColor{Light: "#E1AD01", Dark: "#FFDE00"}) - paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) - helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) - quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4) -) - -type item string - -func (i item) FilterValue() string { return "" } - -type itemDelegate struct{} - -func (d itemDelegate) Height() int { return 1 } -func (d itemDelegate) Spacing() int { return 0 } -func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } -func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { - i, ok := listItem.(item) - if !ok { - return - } - - str := fmt.Sprintf("%s", i) - - fn := itemStyle.Render - if index == m.Index() { - fn = func(s ...string) string { - return selectedItemStyle.Render("> " + strings.Join(s, " ")) - } - } - - fmt.Fprint(w, fn(str)) +var seriesIDMap = map[string]string{ + "Mega Evolution": "me", + "Scarlet & Violet": "sv", + "Sword & Shield": "swsh", } type SeriesModel struct { List list.Model Choice string + SeriesID string Quitting bool } @@ -67,6 +35,7 @@ func (m SeriesModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { i, ok := m.List.SelectedItem().(item) if ok { m.Choice = string(i) + m.SeriesID = seriesIDMap[string(i)] } return m, tea.Quit } @@ -91,3 +60,24 @@ func (m SeriesModel) View() string { return "\n" + m.List.View() } + +func SeriesList() SeriesModel { + items := []list.Item{ + item("Mega Evolution"), + item("Scarlet & Violet"), + item("Sword & Shield"), + } + + const listWidth = 20 + const listHeight = 12 + + l := list.New(items, itemDelegate{}, listWidth, listHeight) + l.Title = "First, pick a series" + l.SetShowStatusBar(false) + l.SetFilteringEnabled(false) + l.Styles.Title = titleStyle + l.Styles.PaginationStyle = paginationStyle + l.Styles.HelpStyle = helpStyle + + return SeriesModel{List: l} +} From 5415478c357ff736a0b9ed00de30bf77ca8739c7 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 10 Nov 2025 09:42:20 -0800 Subject: [PATCH 13/73] moving list design into own file (#201) --- cmd/card/design.go | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 cmd/card/design.go diff --git a/cmd/card/design.go b/cmd/card/design.go new file mode 100644 index 00000000..e16424d0 --- /dev/null +++ b/cmd/card/design.go @@ -0,0 +1,47 @@ +package card + +import ( + "fmt" + "io" + "strings" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +var ( + titleStyle = lipgloss.NewStyle().MarginLeft(2) + itemStyle = lipgloss.NewStyle().PaddingLeft(4) + selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.AdaptiveColor{Light: "#E1AD01", Dark: "#FFDE00"}) + paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) + helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) + quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4) +) + +type item string + +func (i item) FilterValue() string { return "" } + +type itemDelegate struct{} + +func (d itemDelegate) Height() int { return 1 } +func (d itemDelegate) Spacing() int { return 0 } +func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } +func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + i, ok := listItem.(item) + if !ok { + return + } + + str := fmt.Sprintf("%s", i) + + fn := itemStyle.Render + if index == m.Index() { + fn = func(s ...string) string { + return selectedItemStyle.Render("> " + strings.Join(s, " ")) + } + } + + fmt.Fprint(w, fn(str)) +} From 7b50c3bbc1ccf29342a598ddab753be736cdc6b9 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 10 Nov 2025 10:29:58 -0800 Subject: [PATCH 14/73] updating paths to ignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4e571b13..30e3fbd4 100644 --- a/.gitignore +++ b/.gitignore @@ -50,7 +50,6 @@ dbt_packages/ logs/ target/ -dbt_packages/ card_data/infrastructure/supabase/access-token /card_data/infrastructure/supabase/access-token @@ -61,5 +60,6 @@ card_data/.tmp*/** card_data/pipelines/poke_cli_dbt/.user.yml /card_data/supabase/ +/card_data/sample_scripts/ card_data/~/ From 878b6ab7156b9a750b69aae1d01f1baadbb18dc9 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 10 Nov 2025 13:42:51 -0800 Subject: [PATCH 15/73] updating dependencies with critical/high CVE warnings --- card_data/pyproject.toml | 6 ++++++ card_data/uv.lock | 28 +++++++++++++++++----------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/card_data/pyproject.toml b/card_data/pyproject.toml index e14f49f5..5f2118a3 100644 --- a/card_data/pyproject.toml +++ b/card_data/pyproject.toml @@ -41,3 +41,9 @@ root_module = "pipelines" registry_modules = [ "pipelines.components.*", ] + +[tool.uv] +override-dependencies = [ + "deepdiff==8.6.1", + "starlette==0.49.1", +] \ No newline at end of file diff --git a/card_data/uv.lock b/card_data/uv.lock index c4547e07..aeae284c 100644 --- a/card_data/uv.lock +++ b/card_data/uv.lock @@ -2,6 +2,12 @@ version = 1 revision = 2 requires-python = ">=3.12" +[manifest] +overrides = [ + { name = "deepdiff", specifier = "==8.6.1" }, + { name = "starlette", specifier = "==0.49.1" }, +] + [[package]] name = "agate" version = "1.9.1" @@ -703,14 +709,14 @@ wheels = [ [[package]] name = "deepdiff" -version = "7.0.1" +version = "8.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ordered-set" }, + { name = "orderly-set" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/10/6f4b0bd0627d542f63a24f38e29d77095dc63d5f45bc1a7b4a6ca8750fa9/deepdiff-7.0.1.tar.gz", hash = "sha256:260c16f052d4badbf60351b4f77e8390bee03a0b516246f6839bc813fb429ddf", size = 421718, upload-time = "2024-04-08T22:59:24.578Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/76/36c9aab3d5c19a94091f7c6c6e784efca50d87b124bf026c36e94719f33c/deepdiff-8.6.1.tar.gz", hash = "sha256:ec56d7a769ca80891b5200ec7bd41eec300ced91ebcc7797b41eb2b3f3ff643a", size = 634054, upload-time = "2025-09-03T19:40:41.461Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/e6/d27d37dc55dbf40cdbd665aa52844b065ac760c9a02a02265f97ea7a4256/deepdiff-7.0.1-py3-none-any.whl", hash = "sha256:447760081918216aa4fd4ca78a4b6a848b81307b2ea94c810255334b759e1dc3", size = 80825, upload-time = "2024-04-08T22:59:21.885Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl", hash = "sha256:ee8708a7f7d37fb273a541fa24ad010ed484192cd0c4ffc0fa0ed5e2d4b9e78b", size = 91378, upload-time = "2025-09-03T19:40:39.679Z" }, ] [[package]] @@ -1374,12 +1380,12 @@ wheels = [ ] [[package]] -name = "ordered-set" -version = "4.1.0" +name = "orderly-set" +version = "5.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/ca/bfac8bc689799bcca4157e0e0ced07e70ce125193fc2e166d2e685b7e2fe/ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8", size = 12826, upload-time = "2022-01-26T14:38:56.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/88/39c83c35d5e97cc203e9e77a4f93bf87ec89cf6a22ac4818fdcc65d66584/orderly_set-5.5.0.tar.gz", hash = "sha256:e87185c8e4d8afa64e7f8160ee2c542a475b738bc891dc3f58102e654125e6ce", size = 27414, upload-time = "2025-07-10T20:10:55.885Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/55/af02708f230eb77084a299d7b08175cff006dea4f2721074b92cdb0296c0/ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562", size = 7634, upload-time = "2022-01-26T14:38:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl", hash = "sha256:46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7", size = 13068, upload-time = "2025-07-10T20:10:54.377Z" }, ] [[package]] @@ -2211,15 +2217,15 @@ wheels = [ [[package]] name = "starlette" -version = "0.47.2" +version = "0.49.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/3f/507c21db33b66fb027a332f2cb3abbbe924cc3a79ced12f01ed8645955c9/starlette-0.49.1.tar.gz", hash = "sha256:481a43b71e24ed8c43b11ea02f5353d77840e01480881b8cb5a26b8cae64a8cb", size = 2654703, upload-time = "2025-10-28T17:34:10.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" }, + { url = "https://files.pythonhosted.org/packages/51/da/545b75d420bb23b5d494b0517757b351963e974e79933f01e05c929f20a6/starlette-0.49.1-py3-none-any.whl", hash = "sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875", size = 74175, upload-time = "2025-10-28T17:34:09.13Z" }, ] [[package]] From 90e6991e684fbd1b8f514aa506c68043a69e330b Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 10 Nov 2025 13:43:12 -0800 Subject: [PATCH 16/73] updating testing data --- testdata/main_latest_flag.golden | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata/main_latest_flag.golden b/testdata/main_latest_flag.golden index 262d4786..4166dd4b 100644 --- a/testdata/main_latest_flag.golden +++ b/testdata/main_latest_flag.golden @@ -2,6 +2,6 @@ ┃ ┃ ┃ Latest available release ┃ ┃ on GitHub: ┃ -┃ • v1.7.3 ┃ +┃ • v1.7.4 ┃ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ From 6629cd5d48a4495fc2f5ca01c1fd08537dc284bc Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 10 Nov 2025 14:09:53 -0800 Subject: [PATCH 17/73] editing RLS ruling --- card_data/pipelines/poke_cli_dbt/macros/create_rls.sql | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/card_data/pipelines/poke_cli_dbt/macros/create_rls.sql b/card_data/pipelines/poke_cli_dbt/macros/create_rls.sql index 89b55a15..0bbdbda7 100644 --- a/card_data/pipelines/poke_cli_dbt/macros/create_rls.sql +++ b/card_data/pipelines/poke_cli_dbt/macros/create_rls.sql @@ -1,4 +1,9 @@ {% macro enable_rls() %} ALTER TABLE {{ this }} ENABLE ROW LEVEL SECURITY; - CREATE POLICY "Enable read access for all users" ON {{ this }} TO PUBLIC USING (true); + CREATE POLICY "Enable Read Access for All Users" + ON {{ this }} + AS PERMISSIVE + FOR SELECT + TO PUBLIC + USING (true); {% endmacro %} \ No newline at end of file From 69bc3120fe38832188f50ce3360d3b304881bdb6 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Tue, 11 Nov 2025 16:40:20 -0800 Subject: [PATCH 18/73] initial commit --- .gitleaksignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitleaksignore diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 00000000..1df3e9e1 --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1 @@ +cmd/card/sets_list.go:generic-api-key:107 \ No newline at end of file From 3dabd58b59971ee38582500041e7e696c342ca0b Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Tue, 11 Nov 2025 16:51:04 -0800 Subject: [PATCH 19/73] removing unneeded tests (#201) --- cmd/card/series_list_test.go | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/cmd/card/series_list_test.go b/cmd/card/series_list_test.go index 7a0bd18c..e77926b9 100644 --- a/cmd/card/series_list_test.go +++ b/cmd/card/series_list_test.go @@ -142,39 +142,3 @@ func TestSeriesModelView(t *testing.T) { t.Errorf("Expected non-empty view for choice, got empty string") } } - -func TestItemFilterValue(t *testing.T) { - testItem := item("Test Item") - filterValue := testItem.FilterValue() - - if filterValue != "" { - t.Errorf("Expected FilterValue to return empty string, got '%s'", filterValue) - } -} - -func TestItemDelegateHeight(t *testing.T) { - delegate := itemDelegate{} - height := delegate.Height() - - if height != 1 { - t.Errorf("Expected Height to return 1, got %d", height) - } -} - -func TestItemDelegateSpacing(t *testing.T) { - delegate := itemDelegate{} - spacing := delegate.Spacing() - - if spacing != 0 { - t.Errorf("Expected Spacing to return 0, got %d", spacing) - } -} - -func TestItemDelegateUpdate(t *testing.T) { - delegate := itemDelegate{} - cmd := delegate.Update(tea.KeyMsg{}, &list.Model{}) - - if cmd != nil { - t.Errorf("Expected Update to return nil, got %v", cmd) - } -} From 464db352fd1d8004bfd4ca027072d2bf3ac368d9 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Tue, 11 Nov 2025 16:54:54 -0800 Subject: [PATCH 20/73] renaming files (#201) --- cmd/card/{series_list.go => serieslist.go} | 0 cmd/card/{series_list_test.go => serieslist_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename cmd/card/{series_list.go => serieslist.go} (100%) rename cmd/card/{series_list_test.go => serieslist_test.go} (100%) diff --git a/cmd/card/series_list.go b/cmd/card/serieslist.go similarity index 100% rename from cmd/card/series_list.go rename to cmd/card/serieslist.go diff --git a/cmd/card/series_list_test.go b/cmd/card/serieslist_test.go similarity index 100% rename from cmd/card/series_list_test.go rename to cmd/card/serieslist_test.go From af8538d39b8e96de8167e77b62f233abdee7aa42 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Tue, 11 Nov 2025 16:59:32 -0800 Subject: [PATCH 21/73] removing comment --- cmd/berry/berryinfo.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/berry/berryinfo.go b/cmd/berry/berryinfo.go index 534dc4ee..3b284dfa 100644 --- a/cmd/berry/berryinfo.go +++ b/cmd/berry/berryinfo.go @@ -11,7 +11,6 @@ import ( "github.com/disintegration/imaging" ) -// BerryName prints information based on currently selected berry. func BerryName(berryName string) string { return "Berry: " + berryName } From 378b9f0fbd089b33e828a6094420e6d13edd7110 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Tue, 11 Nov 2025 20:29:17 -0800 Subject: [PATCH 22/73] adding first function for card details (#201) --- cmd/card/cardinfo.go | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 cmd/card/cardinfo.go diff --git a/cmd/card/cardinfo.go b/cmd/card/cardinfo.go new file mode 100644 index 00000000..93eb8d23 --- /dev/null +++ b/cmd/card/cardinfo.go @@ -0,0 +1,5 @@ +package card + +func CardName(cardName string) string { + return "Name: " + cardName +} From bd1b97789911b6f032c369c638970f7bd3436d59 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Tue, 11 Nov 2025 20:37:14 -0800 Subject: [PATCH 23/73] successfully chaining all three BubbleTea programs (#201) --- cmd/card/card.go | 46 ++++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/cmd/card/card.go b/cmd/card/card.go index 21f5b0f5..95cc1a77 100644 --- a/cmd/card/card.go +++ b/cmd/card/card.go @@ -6,7 +6,6 @@ import ( "os" "strings" - "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/styling" @@ -41,28 +40,39 @@ func CardCommand() (string, error) { return output.String(), err } - items := []list.Item{ - item("Mega Evolution"), - item("Scarlet & Violet"), - item("Sword & Shield"), + seriesModel := SeriesList() + // Program 1: Series selection + finalModel, err := tea.NewProgram(seriesModel).Run() + if err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) } - const listWidth = 20 - const listHeight = 12 + result := finalModel.(SeriesModel) - l := list.New(items, itemDelegate{}, listWidth, listHeight) - l.Title = "First, pick a series" - l.SetShowStatusBar(false) - l.SetFilteringEnabled(false) - l.Styles.Title = titleStyle - l.Styles.PaginationStyle = paginationStyle - l.Styles.HelpStyle = helpStyle + if result.SeriesID != "" { + // Program 2: Sets selection + setsModel := SetsList(result.SeriesID) + finalSetsModel, err := tea.NewProgram(setsModel).Run() + if err != nil { + fmt.Println("Error running sets program:", err) + os.Exit(1) + } - m := SeriesModel{List: l} + setsResult := finalSetsModel.(SetsModel) - if _, err := tea.NewProgram(m).Run(); err != nil { - fmt.Println("Error running program:", err) - os.Exit(1) + if setsResult.Quitting { + return output.String(), nil + } + + // Program 3: Cards display + if setsResult.SetID != "" { + cardsModel := CardsList(setsResult.SetID) + if _, err := tea.NewProgram(cardsModel).Run(); err != nil { + fmt.Println("Error running cards program:", err) + os.Exit(1) + } + } } return output.String(), nil From 5319d2af5579f1dc1c2bf7e0aa24b3dd546f66e5 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 12 Nov 2025 08:42:17 -0800 Subject: [PATCH 24/73] updating paths to ignore --- .gitleaksignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitleaksignore b/.gitleaksignore index 1df3e9e1..bbbbbdd7 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -1 +1,3 @@ -cmd/card/sets_list.go:generic-api-key:107 \ No newline at end of file +cmd/card/setslist.go:generic-api-key:116 +cmd/card/cardlist.go:generic-api-key:131 +codecov.yml:generic-api-key:2 \ No newline at end of file From bf414d1e6bc909fd3c5ad6a5247c8c2ee8f6be08 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 12 Nov 2025 17:31:22 -0800 Subject: [PATCH 25/73] first working version (#201) --- cmd/card/setslist.go | 133 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 cmd/card/setslist.go diff --git a/cmd/card/setslist.go b/cmd/card/setslist.go new file mode 100644 index 00000000..2d15fbfe --- /dev/null +++ b/cmd/card/setslist.go @@ -0,0 +1,133 @@ +package card + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" +) + +type SetsModel struct { + List list.Model + Choice string + SetID string + Quitting bool + SeriesName string + setsIDMap map[string]string // Maps set name -> set_id +} + +func (m SetsModel) Init() tea.Cmd { + return nil +} + +func (m SetsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "esc": + m.Quitting = true + return m, tea.Quit + case "enter": + i, ok := m.List.SelectedItem().(item) + if ok { + m.Choice = string(i) + m.SetID = m.setsIDMap[string(i)] // Look up the set_id + } + return m, tea.Quit + } + + case tea.WindowSizeMsg: + m.List.SetWidth(msg.Width) + return m, nil + } + + var cmd tea.Cmd + m.List, cmd = m.List.Update(msg) + return m, cmd +} + +func (m SetsModel) View() string { + if m.Quitting { + return "\n Quitting card search...\n\n" + } + if m.Choice != "" { + return quitTextStyle.Render(fmt.Sprintf("Set selected: %s", m.Choice)) + } + + return "\n" + m.List.View() +} + +type setData struct { + SeriesID string `json:"series_id"` + SetID string `json:"set_id"` + SetName string `json:"set_name"` + OfficialCardCount int `json:"official_card_count"` + TotalCardCount int `json:"total_card_count"` + Logo string `json:"logo"` + Symbol string `json:"symbol"` +} + +func SetsList(seriesID string) SetsModel { + body, _ := callSetsData("https://uoddayfnfkebrijlpfbh.supabase.co/rest/v1/sets") + var allSets []setData + err := json.Unmarshal(body, &allSets) + if err != nil { + log.Fatal(err) + } + + // Filter sets by series_id and build ID map + var items []list.Item + setsIDMap := make(map[string]string) + for _, set := range allSets { + if set.SeriesID == seriesID { + items = append(items, item(set.SetName)) + setsIDMap[set.SetName] = set.SetID // Map name -> ID + } + } + + const listWidth = 20 + const listHeight = 20 + + l := list.New(items, itemDelegate{}, listWidth, listHeight) + l.Title = fmt.Sprintf("Pick a set from the %s series", seriesID) + l.SetShowStatusBar(false) + l.SetFilteringEnabled(false) + l.Styles.Title = titleStyle + l.Styles.PaginationStyle = paginationStyle + l.Styles.HelpStyle = helpStyle + + return SetsModel{ + List: l, + SeriesName: seriesID, + setsIDMap: setsIDMap, + } +} + +func callSetsData(url string) ([]byte, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Fatalf("Error creating request: %v", err) + } + + req.Header.Add("apikey", "sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j") + req.Header.Add("Authorization", "Bearer sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j") + req.Header.Add("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatalf("Error making GET request: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading response body: %v", err) + } + + return body, nil +} From fba59ace2f1b1b0d56a7d5ece4725ea8d6557c4f Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 12 Nov 2025 17:31:55 -0800 Subject: [PATCH 26/73] first working version (#201) --- cmd/card/cardlist.go | 148 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 cmd/card/cardlist.go diff --git a/cmd/card/cardlist.go b/cmd/card/cardlist.go new file mode 100644 index 00000000..93b9079b --- /dev/null +++ b/cmd/card/cardlist.go @@ -0,0 +1,148 @@ +package card + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/digitalghost-dev/poke-cli/styling" +) + +type CardsModel struct { + Table table.Model + Choice string + Quitting bool + SeriesName string + SelectedOption string +} + +func (m CardsModel) Init() tea.Cmd { + return nil +} + +// Update handles user input and updates the model state +func (m CardsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var bubbleCmd tea.Cmd + + // TODO: update to match card/search command method + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "esc", "ctrl+c": + m.Quitting = true + return m, tea.Quit + } + } + + m.Table, bubbleCmd = m.Table.Update(msg) + + // Keep the selected option in sync on every update + if row := m.Table.SelectedRow(); len(row) > 0 { + name := row[0] + if name != m.SelectedOption { + m.SelectedOption = name + } + } + + return m, bubbleCmd +} + +func (m CardsModel) View() string { + if m.Quitting { + return "\n Quitting card search...\n\n" + } + + selectedCard := "" + if row := m.Table.SelectedRow(); len(row) > 0 { + selectedCard = CardName(row[0]) + } + + leftPanel := styling.TypesTableBorder.Render(m.Table.View()) + + rightPanel := lipgloss.NewStyle(). + Width(50). + Height(29). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#FFCC00")). + Padding(1). + Render(selectedCard) + + screen := lipgloss.JoinHorizontal(lipgloss.Top, leftPanel, rightPanel) + + return fmt.Sprintf("Highlight a card!\n%s\n%s", + screen, + styling.KeyMenu.Render("↑ (move up) • ↓ (move down)\nctrl+c | esc (quit)")) +} + +type cardData struct { + Name string `json:"name"` + HP int `json:"hp"` +} + +// CardsList creates and returns a new CardsModel with cards from a specific set +func CardsList(setID string) CardsModel { + // Fetch card data from Supabase, filtered by set_id + url := fmt.Sprintf("https://uoddayfnfkebrijlpfbh.supabase.co/rest/v1/cards?set_id=eq.%s&select=name,hp&order=id", setID) + body, _ := callCardData(url) + + var allCards []cardData + err := json.Unmarshal(body, &allCards) + if err != nil { + log.Fatal(err) + } + + // Extract card names and build table rows + rows := make([]table.Row, len(allCards)) + for i, card := range allCards { + rows[i] = []string{card.Name} + } + + t := table.New( + table.WithColumns([]table.Column{{Title: "Card Name", Width: 40}}), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(28), + ) + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#FFCC00")). + BorderBottom(true) + s.Selected = s.Selected. + Foreground(lipgloss.Color("#000")). + Background(lipgloss.Color("#FFCC00")) + t.SetStyles(s) + + return CardsModel{Table: t} +} + +func callCardData(url string) ([]byte, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Fatalf("Error creating request: %v", err) + } + + req.Header.Add("apikey", "sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j") + req.Header.Add("Authorization", "Bearer sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j") + req.Header.Add("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatalf("Error making GET request: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading response body: %v", err) + } + + return body, nil +} From 2744e5bf32a117e91bffa33dae60e323cb665727 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 12 Nov 2025 17:35:19 -0800 Subject: [PATCH 27/73] updating dependencies --- go.mod | 41 +++++++++++++------------ go.sum | 95 ++++++++++++++++++++++++++++++++-------------------------- 2 files changed, 74 insertions(+), 62 deletions(-) diff --git a/go.mod b/go.mod index 6dfd4ea1..be4cc39a 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,16 @@ module github.com/digitalghost-dev/poke-cli go 1.24.9 require ( - github.com/charmbracelet/bubbles v0.21.0 - github.com/charmbracelet/bubbletea v1.3.6 - github.com/charmbracelet/huh v0.7.0 + github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 + github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/huh v0.8.0 github.com/charmbracelet/lipgloss v1.1.0 - github.com/charmbracelet/x/exp/strings v0.0.0-20250708181618-a60a724ba6c3 - github.com/charmbracelet/x/exp/teatest v0.0.0-20250702191427-5bdfc8f2e4ff - github.com/charmbracelet/x/term v0.2.1 + github.com/charmbracelet/x/exp/strings v0.0.0-20251110210702-903592506081 + github.com/charmbracelet/x/exp/teatest v0.0.0-20251110210702-903592506081 + github.com/charmbracelet/x/term v0.2.2 github.com/disintegration/imaging v1.6.2 - github.com/stretchr/testify v1.10.0 - golang.org/x/text v0.27.0 + github.com/stretchr/testify v1.11.1 + golang.org/x/text v0.31.0 modernc.org/sqlite v1.39.1 ) @@ -21,31 +21,34 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-udiff v0.3.1 // indirect github.com/catppuccin/go v0.3.0 // indirect - github.com/charmbracelet/colorprofile v0.3.1 // indirect - github.com/charmbracelet/x/ansi v0.9.3 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13 // indirect - github.com/charmbracelet/x/exp/golden v0.0.0-20250702191427-5bdfc8f2e4ff // indirect + github.com/charmbracelet/colorprofile v0.3.3 // indirect + github.com/charmbracelet/x/ansi v0.11.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.14 // indirect + github.com/charmbracelet/x/exp/golden v0.0.0-20251110210702-903592506081 // indirect + github.com/clipperhouse/displaywidth v0.5.0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/google/uuid v1.6.0 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect - github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/sahilm/fuzzy v0.1.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/image v0.28.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/image v0.33.0 // indirect + golang.org/x/sys v0.38.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.66.10 // indirect modernc.org/mathutil v1.7.1 // indirect diff --git a/go.sum b/go.sum index 1cb34172..c4a17901 100644 --- a/go.sum +++ b/go.sum @@ -8,36 +8,42 @@ github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3v github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= -github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= -github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= -github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= -github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= -github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= -github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc= -github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI= +github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4= +github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY= +github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= -github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= -github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= -github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/ansi v0.11.0 h1:uuIVK7GIplwX6UBIz8S2TF8nkr7xRlygSsBRjSJqIvA= +github.com/charmbracelet/x/ansi v0.11.0/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE= +github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4= +github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA= github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= -github.com/charmbracelet/x/exp/golden v0.0.0-20250702191427-5bdfc8f2e4ff h1:mEllIwjDs9aKqnzckYmNZqxoULwp4afFLVgH9x9QAGA= -github.com/charmbracelet/x/exp/golden v0.0.0-20250702191427-5bdfc8f2e4ff/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= -github.com/charmbracelet/x/exp/strings v0.0.0-20250708181618-a60a724ba6c3 h1:QTf5vyE6CO+zZiF83Je/r3zWol31h3sKRJ7Vt2PwJVg= -github.com/charmbracelet/x/exp/strings v0.0.0-20250708181618-a60a724ba6c3/go.mod h1:Rgw3/F+xlcUc5XygUtimVSxAqCOsqyvJjqF5UHRvc5k= -github.com/charmbracelet/x/exp/teatest v0.0.0-20250702191427-5bdfc8f2e4ff h1:DKxPeQDSnQPDxD27Bq8nUDwiSecy2Yf+nT3U/TlPXs8= -github.com/charmbracelet/x/exp/teatest v0.0.0-20250702191427-5bdfc8f2e4ff/go.mod h1:RXbDhep1qKL/SEz2IuOhOUrsNHDKGqRmGks1nZStKyU= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/exp/golden v0.0.0-20251110210702-903592506081 h1:0pHMO3V29SZJuasznHm3s3XkQZUgoBXQM+VILYoVj50= +github.com/charmbracelet/x/exp/golden v0.0.0-20251110210702-903592506081/go.mod h1:V8n/g3qVKNxr2FR37Y+otCsMySvZr601T0C7coEP0bw= +github.com/charmbracelet/x/exp/strings v0.0.0-20251110210702-903592506081 h1:pTHy/fb1lG8MTw0FizbBQV9HHXEO2+MtPXkcE0S44nM= +github.com/charmbracelet/x/exp/strings v0.0.0-20251110210702-903592506081/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8= +github.com/charmbracelet/x/exp/teatest v0.0.0-20251110210702-903592506081 h1:4V7nggB2MvTMnI03immNNETBuRZHZE9N/awjP77IooY= +github.com/charmbracelet/x/exp/teatest v0.0.0-20251110210702-903592506081/go.mod h1:aPVjFrBwbJgj5Qz1F0IXsnbcOVJcMKgu1ySUfTAxh7k= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= +github.com/clipperhouse/displaywidth v0.5.0 h1:AIG5vQaSL2EKqzt0M9JMnvNxOCRTKUc4vUnLWGgP89I= +github.com/clipperhouse/displaywidth v0.5.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -52,14 +58,16 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17k github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= @@ -68,37 +76,38 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE= -golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ= +golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From e256be362bed3fcfb6d53caeb38d67117fea1262 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 12 Nov 2025 17:42:59 -0800 Subject: [PATCH 28/73] adding card command reference (#201) --- cli.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cli.go b/cli.go index cca5a412..906856bc 100644 --- a/cli.go +++ b/cli.go @@ -9,6 +9,7 @@ import ( "github.com/digitalghost-dev/poke-cli/cmd/ability" "github.com/digitalghost-dev/poke-cli/cmd/berry" + "github.com/digitalghost-dev/poke-cli/cmd/card" "github.com/digitalghost-dev/poke-cli/cmd/item" "github.com/digitalghost-dev/poke-cli/cmd/move" "github.com/digitalghost-dev/poke-cli/cmd/natures" @@ -71,6 +72,7 @@ func runCLI(args []string) int { "\n\n", styling.StyleBold.Render("COMMANDS:"), fmt.Sprintf("\n\t%-15s %s", "ability", "Get details about an ability"), fmt.Sprintf("\n\t%-15s %s", "berry", "Get details about a berry"), + fmt.Sprintf("\n\t%-15s %s", "card", "Get details about a TCG card"), fmt.Sprintf("\n\t%-15s %s", "item", "Get details about an item"), fmt.Sprintf("\n\t%-15s %s", "move", "Get details about a move"), fmt.Sprintf("\n\t%-15s %s", "natures", "Get details about all natures"), @@ -107,6 +109,7 @@ func runCLI(args []string) int { commands := map[string]func() int{ "ability": utils.HandleCommandOutput(ability.AbilityCommand), "berry": utils.HandleCommandOutput(berry.BerryCommand), + "card": utils.HandleCommandOutput(card.CardCommand), "item": utils.HandleCommandOutput(item.ItemCommand), "move": utils.HandleCommandOutput(move.MoveCommand), "natures": utils.HandleCommandOutput(natures.NaturesCommand), @@ -147,6 +150,7 @@ func runCLI(args []string) int { styling.StyleBold.Render("\nCommands:"), fmt.Sprintf("\n\t%-15s %s", "ability", "Get details about an ability"), fmt.Sprintf("\n\t%-15s %s", "berry", "Get details about a berry"), + fmt.Sprintf("\n\t%-15s %s", "card", "Get details about a TCG card"), fmt.Sprintf("\n\t%-15s %s", "item", "Get details about an item"), fmt.Sprintf("\n\t%-15s %s", "move", "Get details about a move"), fmt.Sprintf("\n\t%-15s %s", "natures", "Get details about all natures"), From 5eec50527e412c0b8898c99c001118988bc5380c Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Thu, 13 Nov 2025 10:08:48 -0800 Subject: [PATCH 29/73] correctly calling card price data (#201) --- cmd/card/cardinfo.go | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/cmd/card/cardinfo.go b/cmd/card/cardinfo.go index 93eb8d23..e7239813 100644 --- a/cmd/card/cardinfo.go +++ b/cmd/card/cardinfo.go @@ -1,5 +1,32 @@ package card +import ( + "encoding/json" + "fmt" + "net/url" +) + func CardName(cardName string) string { - return "Name: " + cardName + return cardName +} + +func CardPrice(cardName string) string { + // URL encode the card name (spaces become %20, / becomes %2F, etc.) + encodedName := url.QueryEscape(cardName) + + apiURL := fmt.Sprintf("https://uoddayfnfkebrijlpfbh.supabase.co/rest/v1/card_pricing_view?select=market_price&number_plus_name=eq.%s", encodedName) + body, err := CallCardData(apiURL) + if err != nil { + return "Price: Not available" + } + + var results []struct { + MarketPrice float64 `json:"market_price"` + } + err = json.Unmarshal(body, &results) + if err != nil || len(results) == 0 { + return "Price: Not available" + } + + return fmt.Sprintf("Price: $%.2f", results[0].MarketPrice) } From c70f0465138faca45c0b310466c9109282b34adb Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sat, 15 Nov 2025 00:43:01 -0800 Subject: [PATCH 30/73] adding pricing data to table (#201) --- cmd/card/cardlist.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cmd/card/cardlist.go b/cmd/card/cardlist.go index 93b9079b..21ccdcca 100644 --- a/cmd/card/cardlist.go +++ b/cmd/card/cardlist.go @@ -59,7 +59,7 @@ func (m CardsModel) View() string { selectedCard := "" if row := m.Table.SelectedRow(); len(row) > 0 { - selectedCard = CardName(row[0]) + selectedCard = CardName(row[0]) + "\n---\n" + CardPrice(row[0]) } leftPanel := styling.TypesTableBorder.Render(m.Table.View()) @@ -80,15 +80,16 @@ func (m CardsModel) View() string { } type cardData struct { - Name string `json:"name"` - HP int `json:"hp"` + Name string `json:"name"` + NumberPlusName string `json:"number_plus_name"` + MarketPrice float64 `json:"market_price"` } // CardsList creates and returns a new CardsModel with cards from a specific set func CardsList(setID string) CardsModel { // Fetch card data from Supabase, filtered by set_id - url := fmt.Sprintf("https://uoddayfnfkebrijlpfbh.supabase.co/rest/v1/cards?set_id=eq.%s&select=name,hp&order=id", setID) - body, _ := callCardData(url) + url := fmt.Sprintf("https://uoddayfnfkebrijlpfbh.supabase.co/rest/v1/card_pricing_view?set_id=eq.%s&select=number_plus_name,market_price&order=localId", setID) + body, _ := CallCardData(url) var allCards []cardData err := json.Unmarshal(body, &allCards) @@ -99,7 +100,7 @@ func CardsList(setID string) CardsModel { // Extract card names and build table rows rows := make([]table.Row, len(allCards)) for i, card := range allCards { - rows[i] = []string{card.Name} + rows[i] = []string{card.NumberPlusName} } t := table.New( @@ -122,7 +123,7 @@ func CardsList(setID string) CardsModel { return CardsModel{Table: t} } -func callCardData(url string) ([]byte, error) { +func CallCardData(url string) ([]byte, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { log.Fatalf("Error creating request: %v", err) From 6070182c5edbcc5132a675ab8cb47c9ad3e98b5d Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sat, 15 Nov 2025 11:19:04 -0800 Subject: [PATCH 31/73] adding priceMap for fewer API calls and smoother rendering (#201) --- cmd/card/cardlist.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/cmd/card/cardlist.go b/cmd/card/cardlist.go index 21ccdcca..5d29f4a6 100644 --- a/cmd/card/cardlist.go +++ b/cmd/card/cardlist.go @@ -19,6 +19,7 @@ type CardsModel struct { Quitting bool SeriesName string SelectedOption string + priceMap map[string]string // Maps card name to price } func (m CardsModel) Init() tea.Cmd { @@ -59,7 +60,12 @@ func (m CardsModel) View() string { selectedCard := "" if row := m.Table.SelectedRow(); len(row) > 0 { - selectedCard = CardName(row[0]) + "\n---\n" + CardPrice(row[0]) + cardName := row[0] + price := m.priceMap[cardName] + if price == "" { + price = "Price: Not available" + } + selectedCard = CardName(cardName) + "\n---\n" + price } leftPanel := styling.TypesTableBorder.Render(m.Table.View()) @@ -97,10 +103,12 @@ func CardsList(setID string) CardsModel { log.Fatal(err) } - // Extract card names and build table rows + // Extract card names and build table rows + price map rows := make([]table.Row, len(allCards)) + priceMap := make(map[string]string) for i, card := range allCards { rows[i] = []string{card.NumberPlusName} + priceMap[card.NumberPlusName] = fmt.Sprintf("Price: $%.2f", card.MarketPrice) } t := table.New( @@ -120,7 +128,10 @@ func CardsList(setID string) CardsModel { Background(lipgloss.Color("#FFCC00")) t.SetStyles(s) - return CardsModel{Table: t} + return CardsModel{ + Table: t, + priceMap: priceMap, + } } func CallCardData(url string) ([]byte, error) { From c97724d88533e8ad0ec20baf16c5279f772ecac1 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sat, 15 Nov 2025 11:19:46 -0800 Subject: [PATCH 32/73] initial commit (#201) --- .../poke_cli_dbt/macros/create_view.sql | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 card_data/pipelines/poke_cli_dbt/macros/create_view.sql diff --git a/card_data/pipelines/poke_cli_dbt/macros/create_view.sql b/card_data/pipelines/poke_cli_dbt/macros/create_view.sql new file mode 100644 index 00000000..b347d5a8 --- /dev/null +++ b/card_data/pipelines/poke_cli_dbt/macros/create_view.sql @@ -0,0 +1,38 @@ +{% macro create_view() %} + CREATE OR REPLACE VIEW public.card_pricing_view + WITH (security_invoker = true) AS + WITH cards_cte AS ( + SELECT + set_id, + name, + "localId", + "set_cardCount_official", + CONCAT(name, ' - ', "localId", '/', "set_cardCount_official") AS card_combined_name, + set_name + FROM public.cards + ), + + cards_pricing_cte AS ( + SELECT + product_id, + market_price, + CONCAT(name, ' - ', card_number) AS card_combined_name, + card_number + FROM public.pricing_data + ) + + SELECT + c.set_id, + c.name, + CONCAT(p.card_number, ' - ', c.name) AS number_plus_name, + c.set_name, + c."localId", + p."market_price", + p."card_number" + FROM + cards_cte AS c + INNER JOIN + cards_pricing_cte AS p + ON c.card_combined_name = p.card_combined_name + ORDER BY c."localId" +{% endmacro %} \ No newline at end of file From 8f958fedb4d76b00c4e7a4fabd79e0d2de7a2f69 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sat, 15 Nov 2025 22:47:17 -0800 Subject: [PATCH 33/73] adding `create_view()` post hook (#201) --- card_data/pipelines/poke_cli_dbt/models/pricing_data.sql | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/card_data/pipelines/poke_cli_dbt/models/pricing_data.sql b/card_data/pipelines/poke_cli_dbt/models/pricing_data.sql index dff35155..abfd7392 100644 --- a/card_data/pipelines/poke_cli_dbt/models/pricing_data.sql +++ b/card_data/pipelines/poke_cli_dbt/models/pricing_data.sql @@ -1,6 +1,9 @@ {{ config( materialized='table', - post_hook="{{ enable_rls() }}" + post_hook=[ + "{{ enable_rls() }}", + "{{ create_view() }}" + ] ) }} SELECT product_id, name, card_number, market_price From 5e0ca16df7e4b79d6ff0fae49175d62f32045721 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sat, 15 Nov 2025 22:47:52 -0800 Subject: [PATCH 34/73] initial test file (#201) --- cmd/card/design_test.go | 88 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 cmd/card/design_test.go diff --git a/cmd/card/design_test.go b/cmd/card/design_test.go new file mode 100644 index 00000000..f938972e --- /dev/null +++ b/cmd/card/design_test.go @@ -0,0 +1,88 @@ +package card + +import ( + "bytes" + "testing" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" +) + +func TestItemFilterValue(t *testing.T) { + testItem := item("Test Item") + filterValue := testItem.FilterValue() + + if filterValue != "" { + t.Errorf("Expected FilterValue to return empty string, got '%s'", filterValue) + } +} + +func TestItemDelegateHeight(t *testing.T) { + delegate := itemDelegate{} + height := delegate.Height() + + if height != 1 { + t.Errorf("Expected Height to return 1, got %d", height) + } +} + +func TestItemDelegateSpacing(t *testing.T) { + delegate := itemDelegate{} + spacing := delegate.Spacing() + + if spacing != 0 { + t.Errorf("Expected Spacing to return 0, got %d", spacing) + } +} + +func TestItemDelegateUpdate(t *testing.T) { + delegate := itemDelegate{} + cmd := delegate.Update(tea.KeyMsg{}, &list.Model{}) + + if cmd != nil { + t.Error("Expected Update to return nil, got non-nil value") + } +} + +func TestItemDelegateRender(t *testing.T) { + delegate := itemDelegate{} + + items := []list.Item{ + item("First Item"), + item("Second Item"), + item("Third Item"), + } + + l := list.New(items, delegate, 20, 10) + + var buf bytes.Buffer + delegate.Render(&buf, l, 0, items[0]) + + output := buf.String() + if output == "" { + t.Error("Expected non-empty output from Render") + } +} + +func TestItemDelegateRenderSelected(t *testing.T) { + delegate := itemDelegate{} + + items := []list.Item{ + item("First Item"), + item("Second Item"), + } + + l := list.New(items, delegate, 20, 10) + + var buf bytes.Buffer + delegate.Render(&buf, l, l.Index(), items[l.Index()]) + + output := buf.String() + if output == "" { + t.Error("Expected non-empty output for selected item") + } + + if len(output) == 0 { + t.Error("Selected item should produce rendered output") + } +} From c90d340b9b6fe0b865f1e0fff2669d44a8b22e0a Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sat, 15 Nov 2025 22:56:39 -0800 Subject: [PATCH 35/73] validating card command args (#201) --- cmd/utils/validateargs.go | 14 ++++++++++++ cmd/utils/validateargs_test.go | 42 +++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/cmd/utils/validateargs.go b/cmd/utils/validateargs.go index c76f194e..e13af568 100644 --- a/cmd/utils/validateargs.go +++ b/cmd/utils/validateargs.go @@ -42,6 +42,7 @@ func ValidateAbilityArgs(args []string) error { return nil } +// ValidateBerryArgs validates the command line arguments func ValidateBerryArgs(args []string) error { if err := checkLength(args, 3); err != nil { return err @@ -54,6 +55,19 @@ func ValidateBerryArgs(args []string) error { return nil } +// ValidateCardArgs validates the command line arguments +func ValidateCardArgs(args []string) error { + if err := checkLength(args, 3); err != nil { + return err + } + + if err := checkNoOtherOptions(args, 3, ""); err != nil { + return err + } + + return nil +} + // ValidateItemArgs validates the command line arguments func ValidateItemArgs(args []string) error { if err := checkLength(args, 3); err != nil { diff --git a/cmd/utils/validateargs_test.go b/cmd/utils/validateargs_test.go index 99c60dc0..c2617162 100644 --- a/cmd/utils/validateargs_test.go +++ b/cmd/utils/validateargs_test.go @@ -1,10 +1,11 @@ package utils import ( + "testing" + "github.com/digitalghost-dev/poke-cli/styling" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" ) func TestCheckLength(t *testing.T) { @@ -221,6 +222,45 @@ func TestValidateBerryArgs(t *testing.T) { } } +// TestValidateCardArgs tests the ValidateCardArgs function +func TestValidateCardArgs(t *testing.T) { + validInputs := [][]string{ + {"poke-cli", "card"}, + {"poke-cli", "card", "--help"}, + } + + for _, input := range validInputs { + err := ValidateCardArgs(input) + require.NoError(t, err, "Expected no error for valid input") + } + + invalidInputs := [][]string{ + {"poke-cli", "card", "scarlet"}, + } + + for _, input := range invalidInputs { + err := ValidateCardArgs(input) + require.Error(t, err, "Expected error for invalid input") + } + + tooManyArgs := [][]string{ + {"poke-cli", "card", "scarlet", "violet"}, + } + + expectedError := styling.StripANSI("╭──────────────────╮\n│✖ Error! │\n│Too many arguments│\n╰──────────────────╯") + + for _, input := range tooManyArgs { + err := ValidateCardArgs(input) + + if err == nil { + t.Fatalf("Expected an error for input %v, but got nil", input) + } + + strippedErr := styling.StripANSI(err.Error()) + assert.Equal(t, expectedError, strippedErr, "Unexpected error message for invalid input") + } +} + // TestValidateItemArgs tests the ValidateItemArgs function func TestValidateItemArgs(t *testing.T) { validInputs := [][]string{ From 1d043e10fc0130427adb5465a244d2a7d3d98bd8 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Sun, 16 Nov 2025 23:09:45 -0800 Subject: [PATCH 36/73] creating image BubbleTea program (#201) --- cmd/card/imageviewer.go | 38 ++++++++++++++ cmd/card/imageviewer_test.go | 96 ++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 cmd/card/imageviewer.go create mode 100644 cmd/card/imageviewer_test.go diff --git a/cmd/card/imageviewer.go b/cmd/card/imageviewer.go new file mode 100644 index 00000000..c196c42c --- /dev/null +++ b/cmd/card/imageviewer.go @@ -0,0 +1,38 @@ +package card + +import ( + tea "github.com/charmbracelet/bubbletea" +) + +type ImageModel struct { + CardName string + ImageURL string +} + +func (m ImageModel) Init() tea.Cmd { + return nil +} + +func (m ImageModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "esc": + return m, tea.Quit + } + } + return m, nil +} + +func (m ImageModel) View() string { + return m.ImageURL +} + +func ImageRenderer(cardName string, imageURL string) ImageModel { + imageData := CardImage(imageURL) + + return ImageModel{ + CardName: cardName, + ImageURL: imageData, + } +} diff --git a/cmd/card/imageviewer_test.go b/cmd/card/imageviewer_test.go new file mode 100644 index 00000000..13f736ae --- /dev/null +++ b/cmd/card/imageviewer_test.go @@ -0,0 +1,96 @@ +package card + +import ( + "testing" + + tea "github.com/charmbracelet/bubbletea" +) + +func TestImageModel_Init(t *testing.T) { + model := ImageModel{ + CardName: "001/198 - Pineco", + ImageURL: "test-sixel-data", + } + + cmd := model.Init() + if cmd != nil { + t.Error("Init() should return nil") + } +} + +func TestImageModel_Update_EscKey(t *testing.T) { + model := ImageModel{ + CardName: "001/198 - Pineco", + ImageURL: "test-sixel-data", + } + + // Test ESC key + msg := tea.KeyMsg{Type: tea.KeyEsc} + newModel, cmd := model.Update(msg) + + // Should return quit command + if cmd == nil { + t.Error("Update with ESC should return tea.Quit command") + } + + // Model should be returned (even if quitting) + if _, ok := newModel.(ImageModel); !ok { + t.Error("Update should return ImageModel") + } +} + +func TestImageModel_Update_CtrlC(t *testing.T) { + model := ImageModel{ + CardName: "001/198 - Pineco", + ImageURL: "test-sixel-data", + } + + msg := tea.KeyMsg{Type: tea.KeyCtrlC} + _, cmd := model.Update(msg) + + // Should return quit command + if cmd == nil { + t.Error("Update with Ctrl+C should return tea.Quit command") + } +} + +func TestImageModel_Update_DifferentKey(t *testing.T) { + model := ImageModel{ + CardName: "001/198 - Pineco", + ImageURL: "test-sixel-data", + } + + msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'a'}} + _, cmd := model.Update(msg) + + if cmd != nil { + t.Error("Update with non-quit key should not return a command") + } +} + +func TestImageModel_View(t *testing.T) { + expectedURL := "test-sixel-data-123" + model := ImageModel{ + CardName: "001/198 - Pineco", + ImageURL: expectedURL, + } + + result := model.View() + + if result != expectedURL { + t.Errorf("View() = %v, want %v", result, expectedURL) + } +} + +func TestImageModel_View_Empty(t *testing.T) { + model := ImageModel{ + CardName: "001/198 - Pineco", + ImageURL: "", + } + + result := model.View() + + if result != "" { + t.Errorf("View() with empty ImageURL should return empty string, got %v", result) + } +} From fea7348fb86820c5df4e34bb7d3103680e04f8b6 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 17 Nov 2025 17:47:37 -0800 Subject: [PATCH 37/73] adding new tests (#201) --- cmd/card/card_test.go | 54 ++++++++++++ cmd/card/setslist_test.go | 179 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 cmd/card/card_test.go create mode 100644 cmd/card/setslist_test.go diff --git a/cmd/card/card_test.go b/cmd/card/card_test.go new file mode 100644 index 00000000..209e0df0 --- /dev/null +++ b/cmd/card/card_test.go @@ -0,0 +1,54 @@ +package card + +import ( + "os" + "strings" + "testing" +) + +func TestCardCommand(t *testing.T) { + tests := []struct { + name string + args []string + wantErr bool + contains string + }{ + { + name: "help flag short", + args: []string{"poke-cli", "card", "-h"}, + wantErr: false, + contains: "USAGE:", + }, + { + name: "help flag long", + args: []string{"poke-cli", "card", "--help"}, + wantErr: false, + contains: "FLAGS:", + }, + { + name: "invalid args", + args: []string{"poke-cli", "card", "invalid-arg"}, + wantErr: true, + contains: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + oldArgs := os.Args + os.Args = tt.args + defer func() { os.Args = oldArgs }() + + output, err := CardCommand() + + if (err != nil) != tt.wantErr { + t.Errorf("CardCommand() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.contains != "" && !strings.Contains(output, tt.contains) { + t.Errorf("CardCommand() output should contain %q, got %q", tt.contains, output) + } + }) + } +} diff --git a/cmd/card/setslist_test.go b/cmd/card/setslist_test.go new file mode 100644 index 00000000..b1f2b724 --- /dev/null +++ b/cmd/card/setslist_test.go @@ -0,0 +1,179 @@ +package card + +import ( + "strings" + "testing" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" +) + +func TestSetsModel_Init(t *testing.T) { + model := SetsModel{ + SeriesName: "sv", + Quitting: false, + } + + cmd := model.Init() + if cmd != nil { + t.Error("Init() should return nil") + } +} + +func TestSetsModel_Update_EscKey(t *testing.T) { + items := []list.Item{ + item("Scarlet & Violet"), + item("Paldea Evolved"), + } + l := list.New(items, itemDelegate{}, 20, 20) + + model := SetsModel{ + List: l, + SeriesName: "sv", + Quitting: false, + } + + msg := tea.KeyMsg{Type: tea.KeyEsc} + newModel, cmd := model.Update(msg) + + resultModel := newModel.(SetsModel) + + if !resultModel.Quitting { + t.Error("Quitting should be set to true when ESC is pressed") + } + + if cmd == nil { + t.Error("Update with ESC should return tea.Quit command") + } +} + +func TestSetsModel_Update_CtrlC(t *testing.T) { + items := []list.Item{ + item("Scarlet & Violet"), + } + l := list.New(items, itemDelegate{}, 20, 20) + + model := SetsModel{ + List: l, + SeriesName: "sv", + Quitting: false, + } + + msg := tea.KeyMsg{Type: tea.KeyCtrlC} + newModel, cmd := model.Update(msg) + + resultModel := newModel.(SetsModel) + + if !resultModel.Quitting { + t.Error("Quitting should be set to true when Ctrl+C is pressed") + } + + if cmd == nil { + t.Error("Update with Ctrl+C should return tea.Quit command") + } +} + +func TestSetsModel_Update_WindowSizeMsg(t *testing.T) { + items := []list.Item{ + item("Scarlet & Violet"), + } + l := list.New(items, itemDelegate{}, 20, 20) + + model := SetsModel{ + List: l, + SeriesName: "sv", + } + + msg := tea.WindowSizeMsg{Width: 100, Height: 50} + newModel, cmd := model.Update(msg) + + resultModel := newModel.(SetsModel) + + if cmd != nil { + t.Error("WindowSizeMsg should not return a command") + } + + if resultModel.Quitting { + t.Error("WindowSizeMsg should not set Quitting to true") + } +} + +func TestSetsModel_View_Quitting(t *testing.T) { + items := []list.Item{ + item("Scarlet & Violet"), + } + l := list.New(items, itemDelegate{}, 20, 20) + + model := SetsModel{ + List: l, + Quitting: true, + } + + result := model.View() + + if !strings.Contains(result, "Quitting card search") { + t.Errorf("View() when quitting should contain 'Quitting card search', got: %s", result) + } +} + +func TestSetsModel_View_ChoiceMade(t *testing.T) { + items := []list.Item{ + item("Scarlet & Violet"), + } + l := list.New(items, itemDelegate{}, 20, 20) + + model := SetsModel{ + List: l, + Choice: "Scarlet & Violet", + } + + result := model.View() + + if !strings.Contains(result, "Set selected: Scarlet & Violet") { + t.Errorf("View() with choice should contain 'Set selected: Scarlet & Violet', got: %s", result) + } +} + +func TestSetsModel_View_Normal(t *testing.T) { + items := []list.Item{ + item("Scarlet & Violet"), + } + l := list.New(items, itemDelegate{}, 20, 20) + + model := SetsModel{ + List: l, + Quitting: false, + Choice: "", + } + + result := model.View() + + if result == "" { + t.Error("View() should not return empty string in normal state") + } +} + +func TestSetsModel_Update_EnterKey(t *testing.T) { + items := []list.Item{ + item("Scarlet & Violet"), + item("Paldea Evolved"), + } + l := list.New(items, itemDelegate{}, 20, 20) + + setsIDMap := map[string]string{ + "Scarlet & Violet": "sv01", + "Paldea Evolved": "sv02", + } + + model := SetsModel{ + List: l, + setsIDMap: setsIDMap, + } + + msg := tea.KeyMsg{Type: tea.KeyEnter} + _, cmd := model.Update(msg) + + if cmd == nil { + t.Error("Update with Enter should return tea.Quit command") + } +} From d6a9a720bf5d85870abf494e2a0a40ecf1372ffc Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 17 Nov 2025 18:39:10 -0800 Subject: [PATCH 38/73] adding image column (#201) --- card_data/pipelines/poke_cli_dbt/macros/create_view.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/card_data/pipelines/poke_cli_dbt/macros/create_view.sql b/card_data/pipelines/poke_cli_dbt/macros/create_view.sql index b347d5a8..4eeef207 100644 --- a/card_data/pipelines/poke_cli_dbt/macros/create_view.sql +++ b/card_data/pipelines/poke_cli_dbt/macros/create_view.sql @@ -4,6 +4,7 @@ WITH cards_cte AS ( SELECT set_id, + image, name, "localId", "set_cardCount_official", @@ -25,6 +26,7 @@ c.set_id, c.name, CONCAT(p.card_number, ' - ', c.name) AS number_plus_name, + CONCAT(c.image, '/high.png') AS image_url, c.set_name, c."localId", p."market_price", From 392e03efde5d413c87c8f8a256fcb43a212352f8 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 17 Nov 2025 22:42:07 -0800 Subject: [PATCH 39/73] removing comments --- card_data/pipelines/defs/extract/extract_data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/card_data/pipelines/defs/extract/extract_data.py b/card_data/pipelines/defs/extract/extract_data.py index 3aa7c9a4..b523b1cf 100644 --- a/card_data/pipelines/defs/extract/extract_data.py +++ b/card_data/pipelines/defs/extract/extract_data.py @@ -90,10 +90,10 @@ def extract_set_data() -> pl.DataFrame: @dg.asset(kinds={"API"}, name="extract_card_url_from_set_data") def extract_card_url_from_set() -> list: urls = [ - "https://api.tcgdex.net/v2/en/sets/swsh3" + "https://api.tcgdex.net/v2/en/sets/me02" ] - all_card_urls = [] # Initialize empty list to collect all URLs + all_card_urls = [] for url in urls: try: @@ -103,7 +103,7 @@ def extract_card_url_from_set() -> list: data = r.json()["cards"] set_card_urls = [f"https://api.tcgdex.net/v2/en/cards/{card['id']}" for card in data] - all_card_urls.extend(set_card_urls) # Add all URLs from this set + all_card_urls.extend(set_card_urls) time.sleep(0.1) From d2155a470b959666838f9c0827fa19d9e948d656 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 17 Nov 2025 22:42:22 -0800 Subject: [PATCH 40/73] adding all sets from SV and ME --- .../defs/extract/extract_pricing_data.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/card_data/pipelines/defs/extract/extract_pricing_data.py b/card_data/pipelines/defs/extract/extract_pricing_data.py index 15e68cfe..377af301 100644 --- a/card_data/pipelines/defs/extract/extract_pricing_data.py +++ b/card_data/pipelines/defs/extract/extract_pricing_data.py @@ -10,6 +10,22 @@ SET_PRODUCT_MATCHING = { "sv01": "22873", "sv02": "23120", + "sv03": "23228", + "sv03.5": "23237", + "sv04": "23286", + "sv04.5": "23353", + "sv05": "23381", + "sv06": "23473", + "sv06.5": "23529", + "sv07": "23537", + "sv08": "23651", + "sv08.5": "23821", + "sv09": "24073", + "sv10": "24269", + "sv10.5b": "24325", + "sv10.5w": "24326", + "me01": "24380", + "me02": "24448" } @@ -49,16 +65,17 @@ def pull_product_information(set_number: str) -> pl.DataFrame: product_id = SET_PRODUCT_MATCHING[set_number] # Fetch product data - products_url = (f"https://tcgcsv.com/tcgplayer/3/{product_id}/products") + products_url = f"https://tcgcsv.com/tcgplayer/3/{product_id}/products" products_data = requests.get(products_url, timeout=30).json() # Fetch pricing data - prices_url = (f"https://tcgcsv.com/tcgplayer/3/{product_id}/prices") + prices_url = f"https://tcgcsv.com/tcgplayer/3/{product_id}/prices" prices_data = requests.get(prices_url, timeout=30).json() price_dict = { price["productId"]: price.get("marketPrice") for price in prices_data.get("results", []) + if price.get("subTypeName") != "Reverse Holofoil" } cards_data = [] From 80c167968082820eaab5bc3888ab5cc63a23e335 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 17 Nov 2025 22:44:03 -0800 Subject: [PATCH 41/73] adding leading 0 to sets that have fewer than 100 cards to make `JOIN` work --- .../poke_cli_dbt/macros/create_view.sql | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/card_data/pipelines/poke_cli_dbt/macros/create_view.sql b/card_data/pipelines/poke_cli_dbt/macros/create_view.sql index 4eeef207..c9b29529 100644 --- a/card_data/pipelines/poke_cli_dbt/macros/create_view.sql +++ b/card_data/pipelines/poke_cli_dbt/macros/create_view.sql @@ -8,19 +8,19 @@ name, "localId", "set_cardCount_official", - CONCAT(name, ' - ', "localId", '/', "set_cardCount_official") AS card_combined_name, + CONCAT(name, ' - ', "localId", '/', LPAD("set_cardCount_official"::text, 3, '0')) AS card_combined_name, set_name FROM public.cards ), - cards_pricing_cte AS ( - SELECT - product_id, - market_price, - CONCAT(name, ' - ', card_number) AS card_combined_name, - card_number - FROM public.pricing_data - ) + cards_pricing_cte AS ( + SELECT + product_id, + market_price, + CONCAT(name, ' - ', card_number) AS card_combined_name, + card_number + FROM public.pricing_data + ) SELECT c.set_id, From 4164fc5d028407fefb7b82fc455b3af4ce50756e Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 17 Nov 2025 22:44:19 -0800 Subject: [PATCH 42/73] disabling telemetry --- card_data/dagster.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/card_data/dagster.yaml b/card_data/dagster.yaml index bad89452..8ddd96d0 100644 --- a/card_data/dagster.yaml +++ b/card_data/dagster.yaml @@ -9,4 +9,7 @@ storage: db_name: postgres port: 5432 params: - sslmode: require \ No newline at end of file + sslmode: require + +telemetry: + enabled: false \ No newline at end of file From 4716e3e947d653522fa8c862d2237247105d0ece Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Tue, 18 Nov 2025 10:36:07 -0800 Subject: [PATCH 43/73] removing `CardPrice` function, adding `Cardimage` in its replacement (#201) --- cmd/card/cardinfo.go | 50 +++++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/cmd/card/cardinfo.go b/cmd/card/cardinfo.go index e7239813..f3c49414 100644 --- a/cmd/card/cardinfo.go +++ b/cmd/card/cardinfo.go @@ -1,32 +1,54 @@ package card import ( - "encoding/json" + "bytes" "fmt" - "net/url" + "image" + "net/http" + "os" + + "github.com/charmbracelet/x/ansi/sixel" + "golang.org/x/image/draw" ) func CardName(cardName string) string { return cardName } -func CardPrice(cardName string) string { - // URL encode the card name (spaces become %20, / becomes %2F, etc.) - encodedName := url.QueryEscape(cardName) +func resizeImage(img image.Image, width, height int) image.Image { + dst := image.NewRGBA(image.Rect(0, 0, width, height)) + draw.CatmullRom.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil) + return dst +} - apiURL := fmt.Sprintf("https://uoddayfnfkebrijlpfbh.supabase.co/rest/v1/card_pricing_view?select=market_price&number_plus_name=eq.%s", encodedName) - body, err := CallCardData(apiURL) +func CardImage(imageURL string) string { + resp, err := http.Get(imageURL) if err != nil { - return "Price: Not available" + fmt.Fprintf(os.Stderr, "failed to fetch image: %v\n", err) + os.Exit(1) } + defer resp.Body.Close() - var results []struct { - MarketPrice float64 `json:"market_price"` + if resp.StatusCode != http.StatusOK { + fmt.Fprintf(os.Stderr, "non-200 response: %d\n", resp.StatusCode) + os.Exit(1) } - err = json.Unmarshal(body, &results) - if err != nil || len(results) == 0 { - return "Price: Not available" + + img, _, err := image.Decode(resp.Body) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to decode image: %v\n", err) + os.Exit(1) + } + + resized := resizeImage(img, 500, 675) + + // Build Sixel string to return + var buf bytes.Buffer + buf.WriteString("\x1bPq") + if err := new(sixel.Encoder).Encode(&buf, resized); err != nil { + return fmt.Sprintf("Image not available: %v", err) } + buf.WriteString("\x1b\\") - return fmt.Sprintf("Price: $%.2f", results[0].MarketPrice) + return buf.String() } From 125824b614233b31d9fae300ae611c7dd17f04fa Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Tue, 18 Nov 2025 10:49:23 -0800 Subject: [PATCH 44/73] initial cardinfo test file (#201) --- cmd/card/cardinfo_test.go | 188 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 cmd/card/cardinfo_test.go diff --git a/cmd/card/cardinfo_test.go b/cmd/card/cardinfo_test.go new file mode 100644 index 00000000..aeecd868 --- /dev/null +++ b/cmd/card/cardinfo_test.go @@ -0,0 +1,188 @@ +package card + +import ( + "image" + "image/color" + "image/png" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestCardName(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "simple card name", + input: "Pikachu", + expected: "Pikachu", + }, + { + name: "card with number", + input: "001/198 - Pineco", + expected: "001/198 - Pineco", + }, + { + name: "empty string", + input: "", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := CardName(tt.input) + if result != tt.expected { + t.Errorf("CardName() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestResizeImage(t *testing.T) { + // Create a simple test image (100x100 red square) + testImg := image.NewRGBA(image.Rect(0, 0, 100, 100)) + red := color.RGBA{R: 255, G: 0, B: 0, A: 255} + for y := 0; y < 100; y++ { + for x := 0; x < 100; x++ { + testImg.Set(x, y, red) + } + } + + tests := []struct { + name string + img image.Image + width int + height int + wantWidth int + wantHeight int + }{ + { + name: "resize to smaller dimensions", + img: testImg, + width: 50, + height: 50, + wantWidth: 50, + wantHeight: 50, + }, + { + name: "resize to larger dimensions", + img: testImg, + width: 200, + height: 200, + wantWidth: 200, + wantHeight: 200, + }, + { + name: "resize to card dimensions", + img: testImg, + width: 500, + height: 675, + wantWidth: 500, + wantHeight: 675, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := resizeImage(tt.img, tt.width, tt.height) + bounds := result.Bounds() + + if bounds.Dx() != tt.wantWidth { + t.Errorf("resizeImage() width = %v, want %v", bounds.Dx(), tt.wantWidth) + } + if bounds.Dy() != tt.wantHeight { + t.Errorf("resizeImage() height = %v, want %v", bounds.Dy(), tt.wantHeight) + } + }) + } +} + +func TestCardImage_Success(t *testing.T) { + // Create a test HTTP server that serves a small PNG image + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Create a minimal 10x10 PNG image + img := image.NewRGBA(image.Rect(0, 0, 10, 10)) + blue := color.RGBA{R: 0, G: 0, B: 255, A: 255} + for y := 0; y < 10; y++ { + for x := 0; x < 10; x++ { + img.Set(x, y, blue) + } + } + + w.Header().Set("Content-Type", "image/png") + w.WriteHeader(http.StatusOK) + png.Encode(w, img) + })) + defer server.Close() + + result, err := CardImage(server.URL) + + if err != nil { + t.Errorf("CardImage() error = %v, want nil", err) + return + } + + // Check that result is a valid Sixel string + if !strings.HasPrefix(result, "\x1bPq") { + t.Error("CardImage() should return string starting with Sixel header") + } + + if !strings.HasSuffix(result, "\x1b\\") { + t.Error("CardImage() should return string ending with Sixel terminator") + } + + if len(result) == 0 { + t.Error("CardImage() should return non-empty string") + } +} + +func TestCardImage_EncodingError(t *testing.T) { + // Create a test HTTP server that serves invalid image data + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "image/png") + w.WriteHeader(http.StatusOK) + w.Write([]byte("not a valid PNG")) + })) + defer server.Close() + + result, err := CardImage(server.URL) + + if err == nil { + t.Error("CardImage() should return error for invalid image data") + } + + if result != "" { + t.Errorf("CardImage() on error should return empty string, got %v", result) + } + + if !strings.Contains(err.Error(), "failed to decode image") { + t.Errorf("Error message should mention 'failed to decode image', got: %v", err) + } +} + +func TestCardImage_Non200Response(t *testing.T) { + // Create a test HTTP server that returns 404 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + result, err := CardImage(server.URL) + + if err == nil { + t.Error("CardImage() should return error for non-200 response") + } + + if result != "" { + t.Errorf("CardImage() on error should return empty string, got %v", result) + } + + if !strings.Contains(err.Error(), "non-200 response") { + t.Errorf("Error message should mention 'non-200 response', got: %v", err) + } +} From b9617dcce50676e6870f2bd286e99d18d3053e8a Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Tue, 18 Nov 2025 10:53:16 -0800 Subject: [PATCH 45/73] adding `ImageMap` to `CardsModel` struct (#201) --- cmd/card/cardlist.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/cmd/card/cardlist.go b/cmd/card/cardlist.go index 5d29f4a6..9b7bc1fe 100644 --- a/cmd/card/cardlist.go +++ b/cmd/card/cardlist.go @@ -19,24 +19,27 @@ type CardsModel struct { Quitting bool SeriesName string SelectedOption string - priceMap map[string]string // Maps card name to price + PriceMap map[string]string + ViewImage bool + ImageMap map[string]string } func (m CardsModel) Init() tea.Cmd { return nil } -// Update handles user input and updates the model state func (m CardsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var bubbleCmd tea.Cmd - // TODO: update to match card/search command method switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "esc", "ctrl+c": m.Quitting = true return m, tea.Quit + case " ": + m.ViewImage = true + return m, tea.Quit } } @@ -61,7 +64,7 @@ func (m CardsModel) View() string { selectedCard := "" if row := m.Table.SelectedRow(); len(row) > 0 { cardName := row[0] - price := m.priceMap[cardName] + price := m.PriceMap[cardName] if price == "" { price = "Price: Not available" } @@ -89,12 +92,13 @@ type cardData struct { Name string `json:"name"` NumberPlusName string `json:"number_plus_name"` MarketPrice float64 `json:"market_price"` + ImageURL string `json:"image_url"` } // CardsList creates and returns a new CardsModel with cards from a specific set func CardsList(setID string) CardsModel { // Fetch card data from Supabase, filtered by set_id - url := fmt.Sprintf("https://uoddayfnfkebrijlpfbh.supabase.co/rest/v1/card_pricing_view?set_id=eq.%s&select=number_plus_name,market_price&order=localId", setID) + url := fmt.Sprintf("https://uoddayfnfkebrijlpfbh.supabase.co/rest/v1/card_pricing_view?set_id=eq.%s&select=number_plus_name,market_price,image_url&order=localId", setID) body, _ := CallCardData(url) var allCards []cardData @@ -106,9 +110,11 @@ func CardsList(setID string) CardsModel { // Extract card names and build table rows + price map rows := make([]table.Row, len(allCards)) priceMap := make(map[string]string) + imageMap := make(map[string]string) for i, card := range allCards { rows[i] = []string{card.NumberPlusName} priceMap[card.NumberPlusName] = fmt.Sprintf("Price: $%.2f", card.MarketPrice) + imageMap[card.NumberPlusName] = card.ImageURL } t := table.New( @@ -130,7 +136,8 @@ func CardsList(setID string) CardsModel { return CardsModel{ Table: t, - priceMap: priceMap, + PriceMap: priceMap, + ImageMap: imageMap, } } From 11d43de16a8879d6439285702e73c7ec6efae5af Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 19 Nov 2025 08:37:46 -0800 Subject: [PATCH 46/73] adding illustrator column --- card_data/pipelines/poke_cli_dbt/models/cards.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/card_data/pipelines/poke_cli_dbt/models/cards.sql b/card_data/pipelines/poke_cli_dbt/models/cards.sql index ef5be21b..b9087784 100644 --- a/card_data/pipelines/poke_cli_dbt/models/cards.sql +++ b/card_data/pipelines/poke_cli_dbt/models/cards.sql @@ -3,5 +3,5 @@ post_hook="{{ enable_rls() }}" ) }} -SELECT id, set_id, image, name, "localId", category, hp, "set_cardCount_official", set_name +SELECT id, set_id, image, name, "localId", category, hp, "set_cardCount_official", set_name, illustrator FROM {{ source('staging', 'cards') }} \ No newline at end of file From f2631829e9fc6ad57d7c21d4ee5f2707e1c8d236 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 19 Nov 2025 08:38:37 -0800 Subject: [PATCH 47/73] adjusting width (#201) --- cmd/card/cardlist.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/card/cardlist.go b/cmd/card/cardlist.go index 9b7bc1fe..cdcd4ee2 100644 --- a/cmd/card/cardlist.go +++ b/cmd/card/cardlist.go @@ -74,7 +74,7 @@ func (m CardsModel) View() string { leftPanel := styling.TypesTableBorder.Render(m.Table.View()) rightPanel := lipgloss.NewStyle(). - Width(50). + Width(40). Height(29). Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("#FFCC00")). @@ -97,7 +97,6 @@ type cardData struct { // CardsList creates and returns a new CardsModel with cards from a specific set func CardsList(setID string) CardsModel { - // Fetch card data from Supabase, filtered by set_id url := fmt.Sprintf("https://uoddayfnfkebrijlpfbh.supabase.co/rest/v1/card_pricing_view?set_id=eq.%s&select=number_plus_name,market_price,image_url&order=localId", setID) body, _ := CallCardData(url) @@ -118,7 +117,7 @@ func CardsList(setID string) CardsModel { } t := table.New( - table.WithColumns([]table.Column{{Title: "Card Name", Width: 40}}), + table.WithColumns([]table.Column{{Title: "Card Name", Width: 35}}), table.WithRows(rows), table.WithFocused(true), table.WithHeight(28), From 77b38208690b5c640e30c01b594670220d16bc86 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 19 Nov 2025 08:39:04 -0800 Subject: [PATCH 48/73] updating text (#201) --- cmd/card/serieslist.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/card/serieslist.go b/cmd/card/serieslist.go index 4960fa6f..d7a2cfd7 100644 --- a/cmd/card/serieslist.go +++ b/cmd/card/serieslist.go @@ -55,7 +55,7 @@ func (m SeriesModel) View() string { return "\n Quitting card search...\n\n" } if m.Choice != "" { - return quitTextStyle.Render(fmt.Sprintf("%s? Sounds good to me.", m.Choice)) + return quitTextStyle.Render(fmt.Sprintf("Series selected: %s", m.Choice)) } return "\n" + m.List.View() From a89a07fc0b7c583bb5d00024d76ea7916694f4d5 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 19 Nov 2025 08:39:34 -0800 Subject: [PATCH 49/73] adding blank identifier --- cmd/card/imageviewer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/card/imageviewer.go b/cmd/card/imageviewer.go index c196c42c..2c5f9b1b 100644 --- a/cmd/card/imageviewer.go +++ b/cmd/card/imageviewer.go @@ -29,7 +29,7 @@ func (m ImageModel) View() string { } func ImageRenderer(cardName string, imageURL string) ImageModel { - imageData := CardImage(imageURL) + imageData, _ := CardImage(imageURL) return ImageModel{ CardName: cardName, From 957686f460847c95ab407a1c02af56b5cb16bbbe Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 19 Nov 2025 08:40:39 -0800 Subject: [PATCH 50/73] returning an error rather than using `os.Exit(1)` (#201) --- cmd/card/cardinfo.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/cmd/card/cardinfo.go b/cmd/card/cardinfo.go index f3c49414..93a2f057 100644 --- a/cmd/card/cardinfo.go +++ b/cmd/card/cardinfo.go @@ -5,7 +5,6 @@ import ( "fmt" "image" "net/http" - "os" "github.com/charmbracelet/x/ansi/sixel" "golang.org/x/image/draw" @@ -21,23 +20,20 @@ func resizeImage(img image.Image, width, height int) image.Image { return dst } -func CardImage(imageURL string) string { +func CardImage(imageURL string) (string, error) { resp, err := http.Get(imageURL) if err != nil { - fmt.Fprintf(os.Stderr, "failed to fetch image: %v\n", err) - os.Exit(1) + return "", fmt.Errorf("failed to fetch image: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - fmt.Fprintf(os.Stderr, "non-200 response: %d\n", resp.StatusCode) - os.Exit(1) + return "", fmt.Errorf("non-200 response: %d\n", resp.StatusCode) } img, _, err := image.Decode(resp.Body) if err != nil { - fmt.Fprintf(os.Stderr, "failed to decode image: %v\n", err) - os.Exit(1) + return "", fmt.Errorf("failed to decode image: %v\n", err) } resized := resizeImage(img, 500, 675) @@ -46,9 +42,9 @@ func CardImage(imageURL string) string { var buf bytes.Buffer buf.WriteString("\x1bPq") if err := new(sixel.Encoder).Encode(&buf, resized); err != nil { - return fmt.Sprintf("Image not available: %v", err) + return "", fmt.Errorf("failed to encode sixel: %w", err) } buf.WriteString("\x1b\\") - return buf.String() + return buf.String(), nil } From 1c9a47c1e97f243fca50170453775efcd3baaea4 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 19 Nov 2025 08:49:33 -0800 Subject: [PATCH 51/73] opening card image in separate window (#201) --- cmd/card/card.go | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/cmd/card/card.go b/cmd/card/card.go index 95cc1a77..5f59134b 100644 --- a/cmd/card/card.go +++ b/cmd/card/card.go @@ -68,9 +68,27 @@ func CardCommand() (string, error) { // Program 3: Cards display if setsResult.SetID != "" { cardsModel := CardsList(setsResult.SetID) - if _, err := tea.NewProgram(cardsModel).Run(); err != nil { - fmt.Println("Error running cards program:", err) - os.Exit(1) + + for { + finalCardsModel, err := tea.NewProgram(cardsModel, tea.WithAltScreen()).Run() + if err != nil { + return "", fmt.Errorf("error running cards program: %w", err) + } + + cardsResult := finalCardsModel.(CardsModel) + + if cardsResult.ViewImage { + // Launch image viewer + imageURL := cardsResult.ImageMap[cardsResult.SelectedOption] + imageModel := ImageRenderer(cardsResult.SelectedOption, imageURL) + tea.NewProgram(imageModel, tea.WithAltScreen()).Run() + + // Re-launch cards with same state + cardsResult.ViewImage = false + cardsModel = cardsResult + } else { + break + } } } } From c04988fde0b32318043dbce71f1d68f6143d8c75 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 19 Nov 2025 14:14:55 -0800 Subject: [PATCH 52/73] adding timeout and limiting image body size (#201) --- cmd/card/cardinfo.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/cmd/card/cardinfo.go b/cmd/card/cardinfo.go index 93a2f057..85038c93 100644 --- a/cmd/card/cardinfo.go +++ b/cmd/card/cardinfo.go @@ -4,16 +4,15 @@ import ( "bytes" "fmt" "image" + "io" "net/http" + "net/url" + "time" "github.com/charmbracelet/x/ansi/sixel" "golang.org/x/image/draw" ) -func CardName(cardName string) string { - return cardName -} - func resizeImage(img image.Image, width, height int) image.Image { dst := image.NewRGBA(image.Rect(0, 0, width, height)) draw.CatmullRom.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil) @@ -21,19 +20,27 @@ func resizeImage(img image.Image, width, height int) image.Image { } func CardImage(imageURL string) (string, error) { - resp, err := http.Get(imageURL) + client := &http.Client{ + Timeout: time.Second * 15, + } + parsedURL, err := url.Parse(imageURL) + if err != nil || (parsedURL.Scheme != "http" && parsedURL.Scheme != "https") { + return "", fmt.Errorf("invalid URL scheme") + } + resp, err := client.Get(imageURL) if err != nil { return "", fmt.Errorf("failed to fetch image: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("non-200 response: %d\n", resp.StatusCode) + return "", fmt.Errorf("non-200 response: %d", resp.StatusCode) } - img, _, err := image.Decode(resp.Body) + limitedBody := io.LimitReader(resp.Body, 10*1024*1024) + img, _, err := image.Decode(limitedBody) if err != nil { - return "", fmt.Errorf("failed to decode image: %v\n", err) + return "", fmt.Errorf("failed to decode image: %w", err) } resized := resizeImage(img, 500, 675) From 633309619f98c00c087242b6b8a4aea5eec36e80 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 19 Nov 2025 14:18:24 -0800 Subject: [PATCH 53/73] adding illustrator to right panel (#201) --- cmd/card/cardlist.go | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/cmd/card/cardlist.go b/cmd/card/cardlist.go index cdcd4ee2..55bed307 100644 --- a/cmd/card/cardlist.go +++ b/cmd/card/cardlist.go @@ -14,14 +14,15 @@ import ( ) type CardsModel struct { - Table table.Model Choice string + IllustratorMap map[string]string + ImageMap map[string]string + PriceMap map[string]string Quitting bool - SeriesName string SelectedOption string - PriceMap map[string]string + SeriesName string + Table table.Model ViewImage bool - ImageMap map[string]string } func (m CardsModel) Init() tea.Cmd { @@ -68,7 +69,8 @@ func (m CardsModel) View() string { if price == "" { price = "Price: Not available" } - selectedCard = CardName(cardName) + "\n---\n" + price + illustrator := m.IllustratorMap[cardName] + selectedCard = cardName + "\n---\n" + price + "\n---\n" + illustrator } leftPanel := styling.TypesTableBorder.Render(m.Table.View()) @@ -89,15 +91,16 @@ func (m CardsModel) View() string { } type cardData struct { + Illustrator string `json:"illustrator"` + ImageURL string `json:"image_url"` + MarketPrice float64 `json:"market_price"` Name string `json:"name"` NumberPlusName string `json:"number_plus_name"` - MarketPrice float64 `json:"market_price"` - ImageURL string `json:"image_url"` } // CardsList creates and returns a new CardsModel with cards from a specific set func CardsList(setID string) CardsModel { - url := fmt.Sprintf("https://uoddayfnfkebrijlpfbh.supabase.co/rest/v1/card_pricing_view?set_id=eq.%s&select=number_plus_name,market_price,image_url&order=localId", setID) + url := fmt.Sprintf("https://uoddayfnfkebrijlpfbh.supabase.co/rest/v1/card_pricing_view?set_id=eq.%s&select=number_plus_name,market_price,image_url,illustrator&order=localId", setID) body, _ := CallCardData(url) var allCards []cardData @@ -110,9 +113,11 @@ func CardsList(setID string) CardsModel { rows := make([]table.Row, len(allCards)) priceMap := make(map[string]string) imageMap := make(map[string]string) + illustratorMap := make(map[string]string) for i, card := range allCards { rows[i] = []string{card.NumberPlusName} priceMap[card.NumberPlusName] = fmt.Sprintf("Price: $%.2f", card.MarketPrice) + illustratorMap[card.NumberPlusName] = fmt.Sprintf("Illustrator: %s", card.Illustrator) imageMap[card.NumberPlusName] = card.ImageURL } @@ -134,9 +139,10 @@ func CardsList(setID string) CardsModel { t.SetStyles(s) return CardsModel{ - Table: t, - PriceMap: priceMap, - ImageMap: imageMap, + IllustratorMap: illustratorMap, + ImageMap: imageMap, + PriceMap: priceMap, + Table: t, } } From e48c1fb29b04daa84b5ffc5c3bcccd5ab5d63880 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 19 Nov 2025 14:30:49 -0800 Subject: [PATCH 54/73] updating tests --- cmd/card/cardinfo_test.go | 2 +- testdata/cli_help.golden | 1 + testdata/cli_incorrect_command.golden | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/card/cardinfo_test.go b/cmd/card/cardinfo_test.go index aeecd868..5cc06080 100644 --- a/cmd/card/cardinfo_test.go +++ b/cmd/card/cardinfo_test.go @@ -35,7 +35,7 @@ func TestCardName(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := CardName(tt.input) + result := tt.input if result != tt.expected { t.Errorf("CardName() = %v, want %v", result, tt.expected) } diff --git a/testdata/cli_help.golden b/testdata/cli_help.golden index f2840b8d..a86fca94 100644 --- a/testdata/cli_help.golden +++ b/testdata/cli_help.golden @@ -14,6 +14,7 @@ │ COMMANDS: │ │ ability Get details about an ability │ │ berry Get details about a berry │ +│ card Get details about a TCG card │ │ item Get details about an item │ │ move Get details about a move │ │ natures Get details about all natures │ diff --git a/testdata/cli_incorrect_command.golden b/testdata/cli_incorrect_command.golden index e57f2eb9..67df9721 100644 --- a/testdata/cli_incorrect_command.golden +++ b/testdata/cli_incorrect_command.golden @@ -5,6 +5,7 @@ │Commands: │ │ ability Get details about an ability │ │ berry Get details about a berry │ +│ card Get details about a TCG card │ │ item Get details about an item │ │ move Get details about a move │ │ natures Get details about all natures │ From 77833ab145bd54cc930b4c7b20706e897ed71ab7 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 19 Nov 2025 14:32:04 -0800 Subject: [PATCH 55/73] returning error instead of crashing the program with `log.Fatal()` (#201) --- cmd/card/card.go | 5 ++++- cmd/card/cardlist.go | 20 +++++++++++--------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/cmd/card/card.go b/cmd/card/card.go index 5f59134b..c1e55375 100644 --- a/cmd/card/card.go +++ b/cmd/card/card.go @@ -67,7 +67,10 @@ func CardCommand() (string, error) { // Program 3: Cards display if setsResult.SetID != "" { - cardsModel := CardsList(setsResult.SetID) + cardsModel, err := CardsList(setsResult.SetID) + if err != nil { + return "", fmt.Errorf("error loading cards: %w", err) + } for { finalCardsModel, err := tea.NewProgram(cardsModel, tea.WithAltScreen()).Run() diff --git a/cmd/card/cardlist.go b/cmd/card/cardlist.go index 55bed307..2412cfb2 100644 --- a/cmd/card/cardlist.go +++ b/cmd/card/cardlist.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "github.com/charmbracelet/bubbles/table" @@ -99,14 +98,17 @@ type cardData struct { } // CardsList creates and returns a new CardsModel with cards from a specific set -func CardsList(setID string) CardsModel { +func CardsList(setID string) (CardsModel, error) { url := fmt.Sprintf("https://uoddayfnfkebrijlpfbh.supabase.co/rest/v1/card_pricing_view?set_id=eq.%s&select=number_plus_name,market_price,image_url,illustrator&order=localId", setID) - body, _ := CallCardData(url) + body, err := CallCardData(url) + if err != nil { + return CardsModel{}, fmt.Errorf("failed to fetch card data: %w", err) + } var allCards []cardData - err := json.Unmarshal(body, &allCards) + err = json.Unmarshal(body, &allCards) if err != nil { - log.Fatal(err) + return CardsModel{}, fmt.Errorf("failed to unmarshal card data: %w", err) } // Extract card names and build table rows + price map @@ -143,13 +145,13 @@ func CardsList(setID string) CardsModel { ImageMap: imageMap, PriceMap: priceMap, Table: t, - } + }, nil } func CallCardData(url string) ([]byte, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { - log.Fatalf("Error creating request: %v", err) + return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Add("apikey", "sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j") @@ -159,13 +161,13 @@ func CallCardData(url string) ([]byte, error) { client := &http.Client{} resp, err := client.Do(req) if err != nil { - log.Fatalf("Error making GET request: %v", err) + return nil, fmt.Errorf("error making GET request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - log.Fatalf("Error reading response body: %v", err) + return nil, fmt.Errorf("error reading response body: %w", err) } return body, nil From 2efbcb8555747638d2cf5066a52c0a0d76d66165 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 19 Nov 2025 14:48:50 -0800 Subject: [PATCH 56/73] updating tests --- cmd/card/cardlist_test.go | 256 ++++++++++++++++++++++++++++++++++++++ cmd/card/setslist_test.go | 15 ++- cmd/utils/validateargs.go | 2 +- 3 files changed, 269 insertions(+), 4 deletions(-) create mode 100644 cmd/card/cardlist_test.go diff --git a/cmd/card/cardlist_test.go b/cmd/card/cardlist_test.go new file mode 100644 index 00000000..7df05944 --- /dev/null +++ b/cmd/card/cardlist_test.go @@ -0,0 +1,256 @@ +package card + +import ( + "strings" + "testing" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" +) + +func TestCardsModel_Init(t *testing.T) { + model := CardsModel{ + SeriesName: "sv", + } + + cmd := model.Init() + if cmd != nil { + t.Error("Init() should return nil") + } +} + +func TestCardsModel_Update_EscKey(t *testing.T) { + rows := []table.Row{ + {"001/198 - Bulbasaur"}, + {"002/198 - Ivysaur"}, + } + columns := []table.Column{ + {Title: "Card Name", Width: 35}, + } + tbl := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + ) + + model := CardsModel{ + Table: tbl, + Quitting: false, + } + + msg := tea.KeyMsg{Type: tea.KeyEsc} + newModel, cmd := model.Update(msg) + + resultModel := newModel.(CardsModel) + + if !resultModel.Quitting { + t.Error("Quitting should be set to true when ESC is pressed") + } + + if cmd == nil { + t.Error("Update with ESC should return tea.Quit command") + } +} + +func TestCardsModel_Update_CtrlC(t *testing.T) { + rows := []table.Row{ + {"001/198 - Bulbasaur"}, + } + columns := []table.Column{ + {Title: "Card Name", Width: 35}, + } + tbl := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + ) + + model := CardsModel{ + Table: tbl, + Quitting: false, + } + + msg := tea.KeyMsg{Type: tea.KeyCtrlC} + newModel, cmd := model.Update(msg) + + resultModel := newModel.(CardsModel) + + if !resultModel.Quitting { + t.Error("Quitting should be set to true when Ctrl+C is pressed") + } + + if cmd == nil { + t.Error("Update with Ctrl+C should return tea.Quit command") + } +} + +func TestCardsModel_Update_SpaceBar(t *testing.T) { + rows := []table.Row{ + {"001/198 - Bulbasaur"}, + } + columns := []table.Column{ + {Title: "Card Name", Width: 35}, + } + tbl := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + ) + + model := CardsModel{ + Table: tbl, + ViewImage: false, + } + + msg := tea.KeyMsg{Type: tea.KeySpace} + newModel, cmd := model.Update(msg) + + resultModel := newModel.(CardsModel) + + if !resultModel.ViewImage { + t.Error("ViewImage should be set to true when SPACE is pressed") + } + + if cmd == nil { + t.Error("Update with SPACE should return tea.Quit command") + } +} + +func TestCardsModel_Update_SelectionSync(t *testing.T) { + rows := []table.Row{ + {"001/198 - Bulbasaur"}, + {"002/198 - Ivysaur"}, + } + columns := []table.Column{ + {Title: "Card Name", Width: 35}, + } + tbl := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + ) + + model := CardsModel{ + Table: tbl, + SelectedOption: "", + } + + // Simulate a key press that won't quit (e.g., arrow down) + msg := tea.KeyMsg{Type: tea.KeyDown} + newModel, _ := model.Update(msg) + + resultModel := newModel.(CardsModel) + + // The selected option should be updated to the current row + if resultModel.SelectedOption == "" { + t.Error("SelectedOption should be synced after table update") + } +} + +func TestCardsModel_View_Quitting(t *testing.T) { + model := CardsModel{ + Quitting: true, + } + + result := model.View() + + if !strings.Contains(result, "Quitting card search") { + t.Errorf("View() when quitting should contain 'Quitting card search', got: %s", result) + } +} + +func TestCardsModel_View_Normal(t *testing.T) { + rows := []table.Row{ + {"001/198 - Bulbasaur"}, + } + columns := []table.Column{ + {Title: "Card Name", Width: 35}, + } + tbl := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + ) + + priceMap := map[string]string{ + "001/198 - Bulbasaur": "Price: $1.50", + } + + model := CardsModel{ + Table: tbl, + PriceMap: priceMap, + Quitting: false, + } + + result := model.View() + + if result == "" { + t.Error("View() should not return empty string in normal state") + } + + // Check that it contains the key menu + if !strings.Contains(result, "move up") { + t.Error("View() should contain key menu instructions") + } +} + +func TestCardsModel_View_PriceDisplay(t *testing.T) { + rows := []table.Row{ + {"001/198 - Bulbasaur"}, + } + columns := []table.Column{ + {Title: "Card Name", Width: 35}, + } + tbl := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + ) + + priceMap := map[string]string{ + "001/198 - Bulbasaur": "Price: $1.50", + } + + model := CardsModel{ + Table: tbl, + PriceMap: priceMap, + Quitting: false, + } + + result := model.View() + + // The view should include the card name + if !strings.Contains(result, "001/198 - Bulbasaur") { + t.Error("View() should display selected card name") + } +} + +func TestCardsModel_View_MissingPrice(t *testing.T) { + rows := []table.Row{ + {"001/198 - Bulbasaur"}, + } + columns := []table.Column{ + {Title: "Card Name", Width: 35}, + } + tbl := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + ) + + // Empty price map - simulates missing price data + priceMap := map[string]string{} + + model := CardsModel{ + Table: tbl, + PriceMap: priceMap, + Quitting: false, + } + + result := model.View() + + // Should show "Price: Not available" when price is missing + if !strings.Contains(result, "Price: Not available") { + t.Error("View() should display 'Price: Not available' for cards without pricing") + } +} diff --git a/cmd/card/setslist_test.go b/cmd/card/setslist_test.go index b1f2b724..e01befdb 100644 --- a/cmd/card/setslist_test.go +++ b/cmd/card/setslist_test.go @@ -36,7 +36,10 @@ func TestSetsModel_Update_EscKey(t *testing.T) { msg := tea.KeyMsg{Type: tea.KeyEsc} newModel, cmd := model.Update(msg) - resultModel := newModel.(SetsModel) + resultModel, ok := newModel.(SetsModel) + if !ok { + t.Fatalf("expected SetsModel, got %T", newModel) + } if !resultModel.Quitting { t.Error("Quitting should be set to true when ESC is pressed") @@ -62,7 +65,10 @@ func TestSetsModel_Update_CtrlC(t *testing.T) { msg := tea.KeyMsg{Type: tea.KeyCtrlC} newModel, cmd := model.Update(msg) - resultModel := newModel.(SetsModel) + resultModel, ok := newModel.(SetsModel) + if !ok { + t.Fatalf("expected SetsModel, got %T", newModel) + } if !resultModel.Quitting { t.Error("Quitting should be set to true when Ctrl+C is pressed") @@ -87,7 +93,10 @@ func TestSetsModel_Update_WindowSizeMsg(t *testing.T) { msg := tea.WindowSizeMsg{Width: 100, Height: 50} newModel, cmd := model.Update(msg) - resultModel := newModel.(SetsModel) + resultModel, ok := newModel.(SetsModel) + if !ok { + t.Fatalf("expected SetsModel, got %T", newModel) + } if cmd != nil { t.Error("WindowSizeMsg should not return a command") diff --git a/cmd/utils/validateargs.go b/cmd/utils/validateargs.go index e13af568..ab2d80c6 100644 --- a/cmd/utils/validateargs.go +++ b/cmd/utils/validateargs.go @@ -61,7 +61,7 @@ func ValidateCardArgs(args []string) error { return err } - if err := checkNoOtherOptions(args, 3, ""); err != nil { + if err := checkNoOtherOptions(args, 3, ""); err != nil { return err } From 67a8d3607a0dae29a5233fa4ff712246934fc245 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 19 Nov 2025 14:53:24 -0800 Subject: [PATCH 57/73] fixing linting issues --- cmd/card/cardlist.go | 2 +- cmd/card/serieslist.go | 4 +--- cmd/card/setslist.go | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/cmd/card/cardlist.go b/cmd/card/cardlist.go index 2412cfb2..027f131c 100644 --- a/cmd/card/cardlist.go +++ b/cmd/card/cardlist.go @@ -119,7 +119,7 @@ func CardsList(setID string) (CardsModel, error) { for i, card := range allCards { rows[i] = []string{card.NumberPlusName} priceMap[card.NumberPlusName] = fmt.Sprintf("Price: $%.2f", card.MarketPrice) - illustratorMap[card.NumberPlusName] = fmt.Sprintf("Illustrator: %s", card.Illustrator) + illustratorMap[card.NumberPlusName] = "Illustrator: " + card.Illustrator imageMap[card.NumberPlusName] = card.ImageURL } diff --git a/cmd/card/serieslist.go b/cmd/card/serieslist.go index d7a2cfd7..ef6e1c38 100644 --- a/cmd/card/serieslist.go +++ b/cmd/card/serieslist.go @@ -1,8 +1,6 @@ package card import ( - "fmt" - "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" ) @@ -55,7 +53,7 @@ func (m SeriesModel) View() string { return "\n Quitting card search...\n\n" } if m.Choice != "" { - return quitTextStyle.Render(fmt.Sprintf("Series selected: %s", m.Choice)) + return quitTextStyle.Render("Series selected:", m.Choice) } return "\n" + m.List.View() diff --git a/cmd/card/setslist.go b/cmd/card/setslist.go index 2d15fbfe..be1ccc6e 100644 --- a/cmd/card/setslist.go +++ b/cmd/card/setslist.go @@ -55,7 +55,7 @@ func (m SetsModel) View() string { return "\n Quitting card search...\n\n" } if m.Choice != "" { - return quitTextStyle.Render(fmt.Sprintf("Set selected: %s", m.Choice)) + return quitTextStyle.Render("Set selected:", m.Choice) } return "\n" + m.List.View() From 91c91abc5226db8360dd976a3be70577e647b084 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Wed, 19 Nov 2025 15:14:23 -0800 Subject: [PATCH 58/73] fixing linting issues --- cmd/card/card.go | 5 ++++- cmd/card/cardinfo.go | 3 ++- cmd/card/cardinfo_test.go | 10 ++++++++-- cmd/card/design.go | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/cmd/card/card.go b/cmd/card/card.go index c1e55375..3f91794d 100644 --- a/cmd/card/card.go +++ b/cmd/card/card.go @@ -84,7 +84,10 @@ func CardCommand() (string, error) { // Launch image viewer imageURL := cardsResult.ImageMap[cardsResult.SelectedOption] imageModel := ImageRenderer(cardsResult.SelectedOption, imageURL) - tea.NewProgram(imageModel, tea.WithAltScreen()).Run() + _, err := tea.NewProgram(imageModel, tea.WithAltScreen()).Run() + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: image viewer error: %v\n", err) + } // Re-launch cards with same state cardsResult.ViewImage = false diff --git a/cmd/card/cardinfo.go b/cmd/card/cardinfo.go index 85038c93..18d4705d 100644 --- a/cmd/card/cardinfo.go +++ b/cmd/card/cardinfo.go @@ -2,6 +2,7 @@ package card import ( "bytes" + "errors" "fmt" "image" "io" @@ -25,7 +26,7 @@ func CardImage(imageURL string) (string, error) { } parsedURL, err := url.Parse(imageURL) if err != nil || (parsedURL.Scheme != "http" && parsedURL.Scheme != "https") { - return "", fmt.Errorf("invalid URL scheme") + return "", errors.New("invalid URL scheme") } resp, err := client.Get(imageURL) if err != nil { diff --git a/cmd/card/cardinfo_test.go b/cmd/card/cardinfo_test.go index 5cc06080..7b23820c 100644 --- a/cmd/card/cardinfo_test.go +++ b/cmd/card/cardinfo_test.go @@ -116,7 +116,10 @@ func TestCardImage_Success(t *testing.T) { w.Header().Set("Content-Type", "image/png") w.WriteHeader(http.StatusOK) - png.Encode(w, img) + err := png.Encode(w, img) + if err != nil { + return + } })) defer server.Close() @@ -146,7 +149,10 @@ func TestCardImage_EncodingError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "image/png") w.WriteHeader(http.StatusOK) - w.Write([]byte("not a valid PNG")) + _, err := w.Write([]byte("not a valid PNG")) + if err != nil { + return + } })) defer server.Close() diff --git a/cmd/card/design.go b/cmd/card/design.go index e16424d0..097144c1 100644 --- a/cmd/card/design.go +++ b/cmd/card/design.go @@ -34,7 +34,7 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list return } - str := fmt.Sprintf("%s", i) + str := string(i) fn := itemStyle.Render if index == m.Index() { From 76c2df93d01b2ef3a1e07e7411709f6a32785c85 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Thu, 20 Nov 2025 16:07:45 -0800 Subject: [PATCH 59/73] adding `ansi` and `image` libraries (#201) --- go.mod | 5 +++-- go.sum | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index be4cc39a..920a8a2f 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,13 @@ require ( github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/huh v0.8.0 github.com/charmbracelet/lipgloss v1.1.0 + github.com/charmbracelet/x/ansi v0.11.0 github.com/charmbracelet/x/exp/strings v0.0.0-20251110210702-903592506081 github.com/charmbracelet/x/exp/teatest v0.0.0-20251110210702-903592506081 github.com/charmbracelet/x/term v0.2.2 github.com/disintegration/imaging v1.6.2 github.com/stretchr/testify v1.11.1 + golang.org/x/image v0.33.0 golang.org/x/text v0.31.0 modernc.org/sqlite v1.39.1 ) @@ -20,9 +22,9 @@ require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-udiff v0.3.1 // indirect + github.com/bits-and-blooms/bitset v1.24.3 // indirect github.com/catppuccin/go v0.3.0 // indirect github.com/charmbracelet/colorprofile v0.3.3 // indirect - github.com/charmbracelet/x/ansi v0.11.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.14 // indirect github.com/charmbracelet/x/exp/golden v0.0.0-20251110210702-903592506081 // indirect github.com/clipperhouse/displaywidth v0.5.0 // indirect @@ -47,7 +49,6 @@ require ( github.com/sahilm/fuzzy v0.1.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect - golang.org/x/image v0.33.0 // indirect golang.org/x/sys v0.38.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.66.10 // indirect diff --git a/go.sum b/go.sum index c4a17901..a31baba8 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= +github.com/bits-and-blooms/bitset v1.24.3 h1:Bte86SlO3lwPQqww+7BE9ZuUCKIjfqnG5jtEyqA9y9Y= +github.com/bits-and-blooms/bitset v1.24.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= From 40fc057c2a884ed358f4c4bacaf3c5923cb5efac Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Fri, 21 Nov 2025 17:43:47 -0800 Subject: [PATCH 60/73] updating layout, adding card cmd reference --- README.md | 53 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index f1a6d95f..76bd518f 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,6 @@ docker-image-size ci-status-badge
-
- coderabbit-review-count-badge -
tests-label go-version @@ -18,17 +15,21 @@ `poke-cli` is a hybrid of a classic CLI and a modern TUI tool for viewing data about Pokémon! This is my first Go project. View the [documentation](https://docs.poke-cli.com)! -The architecture behind how the tool works is straight forward: -1. Each command indicates which [API](https://pokeapi.co/) endpoint to use. -2. Flags provide more information and can be stacked together or used individually. -3. Each command has a `-h | --help` flag that is built-in with Golang's `flag` package. - -View future plans in the [Roadmap](#roadmap) section. +* [Demo](#demo) +* [Installation](#installation) +* [Usage](#usage) +* [Roadmap](#roadmap) +* [Tested Terminals](#tested-terminals) --- ## Demo -![demo](https://poke-cli-s3-bucket.s3.us-west-2.amazonaws.com/demo-v1.6.0.gif) +### Video Game Data + +![demo-vg](https://poke-cli-s3-bucket.s3.us-west-2.amazonaws.com/demo-v1.6.0.gif) + +### Trading Card Game Data +![demo-tcg](https://poke-cli-s3-bucket.s3.us-west-2.amazonaws.com/poke-cli-v1.8.0.gif) --- ## Installation @@ -162,6 +163,7 @@ By running `poke-cli [-h | --help]`, it'll display information on how to use the │ COMMANDS: │ │ ability Get details about an ability │ │ berry Get details about a berry │ +│ card Get details about a TCG card │ │ item Get details about an item │ │ move Get details about a move │ │ natures Get details about all natures │ @@ -187,6 +189,12 @@ Below is a list of the planned/completed commands and flags: - [x] `ability`: get data about an ability. - [x] `-p | --pokemon`: display Pokémon that learn this ability. - [x] `berry`: get data about a berry. +- [ ] `card`: get data about a TCG card. + - [x] add mega evolution data + - [x] add scarlet & violet data + - [ ] add sword & shield data + - [ ] add sun & moon data + - [ ] add x & y data - [x] `item`: get data about an item. - [x] `move`: get data about a move. - [ ] `-p | --pokemon`: display Pokémon that learn this move. @@ -208,15 +216,16 @@ Below is a list of the planned/completed commands and flags: --- ## Tested Terminals -| Terminal | OS | Status | Issues | -|-------------------|:-------------------------:|:------:|---------------------------------------------------------------------------------| -| Alacritty | macOS, Ubuntu,
Windows | ✅ | None | -| Ghostty | macOS | ✅ | None | -| HyperJS | macOS | ✅ | None | -| iTerm2 | macOS | ✅ | None | -| Built-in Terminal | Ubuntu, Debian,
Fedora | ✅ | None | -| Built-in Terminal | Alpine | ⚠️ | Some colors aren't supported.
`pokemon --image=xx` flag pixel issues. | -| Built-in Terminal | macOS | ⚠️ | `pokemon --image=xx` flag pixel issues. | -| Tabby | Ubuntu | ✅ | None | -| WezTerm | macOS, Windows | ✅ | None | -| Built-in Terminal | Windows | ✅ | None | \ No newline at end of file +| Terminal | OS | Status | Issues | +|-------------------|:-------------------------:|:------:|-----------------------------------------------------------------------------------------------| +| Alacritty | macOS, Ubuntu,
Windows | 🟡 | - Does not support sixel for TCG images. | +| Ghostty | macOS | 🟡 | - Does not support sixel for TCG images. | +| HyperJS | macOS | 🟡 | - Does not support sixel for TCG images. | +| iTerm2 | macOS | ✅ | - None | +| Built-in Terminal | Ubuntu, Debian,
Fedora | ✅ | - None | +| Built-in Terminal | Alpine | 🟡 | - Some colors aren't supported.
- `pokemon --image=xx` flag pixel issues. | +| Built-in Terminal | macOS | 🟠 | - `pokemon --image=xx` flag pixel issues.
- Does not support sixel for TCG images. | +| Foot | Ubuntu | 🟢 | - None | +| Tabby | Ubuntu | 🟢 | - None | +| WezTerm | macOS, Windows | 🟢 | - None | +| Built-in Terminal | Windows | 🟢 | - None | \ No newline at end of file From 474275b788afae825c58e3fdb5916c0d232b9fd3 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 24 Nov 2025 16:09:35 -0800 Subject: [PATCH 61/73] adding data infra diagram --- docs/Infrastructure_Guide/index.md | 15 ++++++++++++++- docs/assets/data_infrastructure_diagram.svg | 5 +++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 docs/assets/data_infrastructure_diagram.svg diff --git a/docs/Infrastructure_Guide/index.md b/docs/Infrastructure_Guide/index.md index 115afb55..67189a63 100644 --- a/docs/Infrastructure_Guide/index.md +++ b/docs/Infrastructure_Guide/index.md @@ -4,7 +4,7 @@ weight: 1 # 1 // Overview -This section serves as a knowledge base for the project’s backend infrastructure. It was created for a few purposes: +This section serves as a knowledge base for the project’s backend data infrastructure. It was created for a few purposes: 1. To document how I built everything, so I can easily reference it later. 2. To help others learn how to build something similar. @@ -21,6 +21,19 @@ The VGC data simply calls one API. If building on a different operating system, please find the equivalent command. Links will be provided for install guides for all operating systems when possible. +## Data Infrastructure Diagram +![data_infrastructure_diagram](../assets/data_infrastructure_diagram.svg) + +1. TCGPlayer pricing data and TCGDex card data are called and processed through a data pipeline orchestrated by Dagster +and hosted on AWS. +2. When the pipeline starts, Pydantic validates the incoming API data against a pre-defined schema, ensuring the data +types match the expected structure. +3. Polars is used to create DataFrames. +4. The data is loaded into a Supabase staging schema. +5. Soda data quality checks are performed. +6. `dbt` runs tests and builds the final tables in a Supabase production schema. +7. Users are then able to query the `pokeapi.co` or `supabase` APIs for either video game or trading card data, respectively. + ## Tools & Services Below is a list of all the tools and services used in this project's infrastructure: diff --git a/docs/assets/data_infrastructure_diagram.svg b/docs/assets/data_infrastructure_diagram.svg new file mode 100644 index 00000000..933e228b --- /dev/null +++ b/docs/assets/data_infrastructure_diagram.svg @@ -0,0 +1,5 @@ + + +dagsterelastic compute cloud (virtual machine)Dagster connected to RDSinstancecreate dataframespolarssupabaseload into stagingdatabaseverify data typesfrom APIspydanticdata quality checksperformtransformationsus-west-2read data through APIsUservirtual private cloudstore dagstermetadataRDSIaCterraformstatemanangementhashicorp cloudsupabaseload into proddatabaseus-east-2EventBridgeschedule instancedowntimeTrack RDS, EC2,and VPC \ No newline at end of file From 4e4ede4587971e64e5b240c188e22417ffc7d3a8 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 24 Nov 2025 16:09:54 -0800 Subject: [PATCH 62/73] removing Sword & Shield option for now --- cmd/card/serieslist.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/card/serieslist.go b/cmd/card/serieslist.go index ef6e1c38..520fc6ed 100644 --- a/cmd/card/serieslist.go +++ b/cmd/card/serieslist.go @@ -63,7 +63,6 @@ func SeriesList() SeriesModel { items := []list.Item{ item("Mega Evolution"), item("Scarlet & Violet"), - item("Sword & Shield"), } const listWidth = 20 From b603977159cf5d2e721ca9d8a2de23d775422506 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 24 Nov 2025 16:16:53 -0800 Subject: [PATCH 63/73] adding soda data quality checks to pricing data pipeline --- card_data/pipelines/definitions.py | 6 +- .../pipelines/defs/load/load_pricing_data.py | 36 +++++++++ .../defs/transformation/transform_data.py | 2 +- card_data/pipelines/soda/checks_pricing.yml | 75 +++++++++++++++++++ 4 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 card_data/pipelines/soda/checks_pricing.yml diff --git a/card_data/pipelines/definitions.py b/card_data/pipelines/definitions.py index dc7a2a64..3c3bf81b 100644 --- a/card_data/pipelines/definitions.py +++ b/card_data/pipelines/definitions.py @@ -5,7 +5,7 @@ import dagster as dg from .defs.extract.extract_pricing_data import build_dataframe -from .defs.load.load_pricing_data import load_pricing_data +from .defs.load.load_pricing_data import load_pricing_data, data_quality_checks_on_pricing @definitions @@ -17,7 +17,7 @@ def defs(): # Define the pricing pipeline job that materializes the assets and downstream dbt model pricing_pipeline_job = dg.define_asset_job( name="pricing_pipeline_job", - selection=dg.AssetSelection.assets(build_dataframe, load_pricing_data).downstream(include_self=True), + selection=dg.AssetSelection.assets(build_dataframe).downstream(include_self=True), ) price_schedule = dg.ScheduleDefinition( @@ -28,7 +28,7 @@ def defs(): ) defs_pricing = dg.Definitions( - assets=[build_dataframe, load_pricing_data], + assets=[build_dataframe, load_pricing_data, data_quality_checks_on_pricing], jobs=[pricing_pipeline_job], schedules=[price_schedule], ) \ No newline at end of file diff --git a/card_data/pipelines/defs/load/load_pricing_data.py b/card_data/pipelines/defs/load/load_pricing_data.py index 5f15f859..cce644f4 100644 --- a/card_data/pipelines/defs/load/load_pricing_data.py +++ b/card_data/pipelines/defs/load/load_pricing_data.py @@ -1,3 +1,6 @@ +import subprocess +from pathlib import Path + import dagster as dg import polars as pl from dagster import RetryPolicy, Backoff @@ -23,3 +26,36 @@ def load_pricing_data(build_pricing_dataframe: pl.DataFrame) -> None: except OperationalError as e: print(colored(" ✖", "red"), "Connection error in load_pricing_data():", e) raise + + +@dg.asset( + deps=[load_pricing_data], + kinds={"Soda"}, + name="data_quality_checks_on_pricing", +) +def data_quality_checks_on_pricing() -> None: + current_file_dir = Path(__file__).parent + print(f"Setting cwd to: {current_file_dir}") + + result = subprocess.run( + [ + "soda", + "scan", + "-d", + "supabase", + "-c", + "../../soda/configuration.yml", + "../../soda/checks_pricing.yml", + ], + capture_output=True, + text=True, + cwd=current_file_dir, + ) + + if result.stdout: + print(result.stdout) + if result.stderr: + print(result.stderr) + + if result.returncode != 0: + raise Exception(f"Soda data quality checks failed with return code {result.returncode}") diff --git a/card_data/pipelines/defs/transformation/transform_data.py b/card_data/pipelines/defs/transformation/transform_data.py index c47c8fb3..dcb85347 100644 --- a/card_data/pipelines/defs/transformation/transform_data.py +++ b/card_data/pipelines/defs/transformation/transform_data.py @@ -16,7 +16,7 @@ def get_asset_key(self, dbt_resource_props): "series": "quality_checks_series", "sets": "load_set_data", "cards": "load_card_data", - "pricing_data": "load_pricing_data", + "pricing_data": "data_quality_checks_on_pricing", } if name in source_mapping: return dg.AssetKey([source_mapping[name]]) diff --git a/card_data/pipelines/soda/checks_pricing.yml b/card_data/pipelines/soda/checks_pricing.yml new file mode 100644 index 00000000..cf5e5f07 --- /dev/null +++ b/card_data/pipelines/soda/checks_pricing.yml @@ -0,0 +1,75 @@ +checks for pricing_data: + # Row count validation - currently have 4216 rows + # Expect at least 4000 cards + - row_count > 4000: + name: Minimum row count check + + # Warn if row count drops significantly + - row_count > 4200: + warn: + when fail + name: Row count sanity check (warn if below expected) + + # Schema validation checks + - schema: + fail: + when required column missing: [product_id, name, card_number, market_price] + when wrong column type: + product_id: bigint + name: text + card_number: text + market_price: double precision + + # Completeness checks - product_id, name, card_number should always be present + - missing_count(product_id) = 0: + name: Product ID completeness + + - missing_count(name) = 0: + name: Card name completeness + + - missing_count(card_number) = 0: + name: Card number completeness + + # Data uniqueness checks + - duplicate_count(product_id) = 0: + name: Product ID uniqueness + + # Data format validation + # Card numbers should be alphanumeric with slashes (e.g., "013/198", "4", "005/086") + - invalid_count(card_number) = 0: + valid regex: '^[A-Za-z0-9/]+$' + name: Card number format validation + + # Card names should not be empty and should be reasonable length (<100 chars) + - invalid_count(name) = 0: + valid min length: 1 + valid max length: 100 + name: Card name length validation + + # Data range validation + # Product IDs should be positive 6-digit numbers (observed range: 475k-642k) + - invalid_count(product_id) = 0: + valid min: 100000 + valid max: 999999999 + name: Product ID range validation + + # Market prices (when present) should be positive and reasonable + # Current range: $0.02 to $1119.08 + - invalid_percent(market_price) < 1%: + valid min: 0.01 + valid max: 10000 + name: Market price range validation ($0.01-$10,000) + + # Statistical validation - average price should be reasonable + # Current average is ~$6.01, allow range of $2-$20 for sanity + - avg(market_price): + warn: + when < 2 + when > 20 + name: Average market price sanity check + + # Anomaly detection - check for extreme outliers + - max(market_price) < 5000: + warn: + when fail + name: Maximum price outlier detection (warn if >$5000) \ No newline at end of file From 63ced12e4510db5b4b7bf3cabea1433e7a267b48 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 24 Nov 2025 17:09:30 -0800 Subject: [PATCH 64/73] updating tested terminals section --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 76bd518f..fb4e1e77 100644 --- a/README.md +++ b/README.md @@ -216,16 +216,16 @@ Below is a list of the planned/completed commands and flags: --- ## Tested Terminals -| Terminal | OS | Status | Issues | -|-------------------|:-------------------------:|:------:|-----------------------------------------------------------------------------------------------| -| Alacritty | macOS, Ubuntu,
Windows | 🟡 | - Does not support sixel for TCG images. | -| Ghostty | macOS | 🟡 | - Does not support sixel for TCG images. | -| HyperJS | macOS | 🟡 | - Does not support sixel for TCG images. | -| iTerm2 | macOS | ✅ | - None | -| Built-in Terminal | Ubuntu, Debian,
Fedora | ✅ | - None | -| Built-in Terminal | Alpine | 🟡 | - Some colors aren't supported.
- `pokemon --image=xx` flag pixel issues. | -| Built-in Terminal | macOS | 🟠 | - `pokemon --image=xx` flag pixel issues.
- Does not support sixel for TCG images. | -| Foot | Ubuntu | 🟢 | - None | -| Tabby | Ubuntu | 🟢 | - None | -| WezTerm | macOS, Windows | 🟢 | - None | -| Built-in Terminal | Windows | 🟢 | - None | \ No newline at end of file +| Terminal | OS | Status | Issues | +|-------------------|:-------------------------:|:------:|----------------------------------------------------------------------------------------------| +| Alacritty | macOS, Ubuntu,
Windows | 🟡 | - Does not support sixel for TCG images. | +| Ghostty | macOS | 🟡 | - Does not support sixel for TCG images. | +| HyperJS | macOS | 🟡 | - Does not support sixel for TCG images. | +| iTerm2 | macOS | 🟢 | - None | +| Built-in Terminal | Ubuntu, Debian,
Fedora | 🟡 | - Does not support sixel for TCG images. | +| Built-in Terminal | Alpine | 🟡 | - Some colors aren't supported.
- `pokemon --image=xx` flag pixel issues. | +| Built-in Terminal | macOS | 🟠 | - Does not support sixel for TCG images.
- `pokemon --image=xx` flag pixel issues. | +| Foot | Ubuntu | 🟢 | - None | +| Tabby | Ubuntu | 🟢 | - None | +| WezTerm | macOS, Windows | 🟡 | - Windows version has issues with displaying TCG images. | +| Built-in Terminal | Windows | 🟢 | - None | \ No newline at end of file From 7c10ba03242319786519c7abaed6af4dca705103 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 24 Nov 2025 21:41:28 -0800 Subject: [PATCH 65/73] updating line location --- .gitleaksignore | 2 +- gitleaks.json | 62 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 gitleaks.json diff --git a/.gitleaksignore b/.gitleaksignore index bbbbbdd7..46497dcd 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -1,3 +1,3 @@ cmd/card/setslist.go:generic-api-key:116 -cmd/card/cardlist.go:generic-api-key:131 +cmd/card/cardlist.go:generic-api-key:157 codecov.yml:generic-api-key:2 \ No newline at end of file diff --git a/gitleaks.json b/gitleaks.json new file mode 100644 index 00000000..75126a90 --- /dev/null +++ b/gitleaks.json @@ -0,0 +1,62 @@ +[ + { + "RuleID": "generic-api-key", + "Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", + "StartLine": 9, + "EndLine": 9, + "StartColumn": 7, + "EndColumn": 63, + "Match": "apikey\": \"sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j\"", + "Secret": "sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j", + "File": "card_data/sample_scripts/get_data.py", + "SymlinkFile": "", + "Commit": "", + "Entropy": 4.6318326, + "Author": "", + "Email": "", + "Date": "", + "Message": "", + "Tags": [], + "Fingerprint": "card_data/sample_scripts/get_data.py:generic-api-key:9" + }, + { + "RuleID": "generic-api-key", + "Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", + "StartLine": 6, + "EndLine": 6, + "StartColumn": 2, + "EndColumn": 55, + "Match": "client_secret = \"PRD-0be0b74c3cb7-5716-44cb-b065-25d9\"", + "Secret": "PRD-0be0b74c3cb7-5716-44cb-b065-25d9", + "File": "card_data/sample_scripts/ebay.py", + "SymlinkFile": "", + "Commit": "", + "Entropy": 3.8089883, + "Author": "", + "Email": "", + "Date": "", + "Message": "", + "Tags": [], + "Fingerprint": "card_data/sample_scripts/ebay.py:generic-api-key:6" + }, + { + "RuleID": "generic-api-key", + "Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", + "StartLine": 157, + "EndLine": 157, + "StartColumn": 19, + "EndColumn": 75, + "Match": "apikey\", \"sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j\"", + "Secret": "sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j", + "File": "cmd/card/cardlist.go", + "SymlinkFile": "", + "Commit": "", + "Entropy": 4.6318326, + "Author": "", + "Email": "", + "Date": "", + "Message": "", + "Tags": [], + "Fingerprint": "cmd/card/cardlist.go:generic-api-key:157" + } +] From 9ee532e8f1c7beea8245a9348bfab57f937680b2 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 24 Nov 2025 21:50:17 -0800 Subject: [PATCH 66/73] removing unused test (#201) --- cmd/card/cardinfo_test.go | 33 --------------------- gitleaks.json | 62 --------------------------------------- 2 files changed, 95 deletions(-) delete mode 100644 gitleaks.json diff --git a/cmd/card/cardinfo_test.go b/cmd/card/cardinfo_test.go index 7b23820c..ba23f7d3 100644 --- a/cmd/card/cardinfo_test.go +++ b/cmd/card/cardinfo_test.go @@ -10,39 +10,6 @@ import ( "testing" ) -func TestCardName(t *testing.T) { - tests := []struct { - name string - input string - expected string - }{ - { - name: "simple card name", - input: "Pikachu", - expected: "Pikachu", - }, - { - name: "card with number", - input: "001/198 - Pineco", - expected: "001/198 - Pineco", - }, - { - name: "empty string", - input: "", - expected: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := tt.input - if result != tt.expected { - t.Errorf("CardName() = %v, want %v", result, tt.expected) - } - }) - } -} - func TestResizeImage(t *testing.T) { // Create a simple test image (100x100 red square) testImg := image.NewRGBA(image.Rect(0, 0, 100, 100)) diff --git a/gitleaks.json b/gitleaks.json deleted file mode 100644 index 75126a90..00000000 --- a/gitleaks.json +++ /dev/null @@ -1,62 +0,0 @@ -[ - { - "RuleID": "generic-api-key", - "Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", - "StartLine": 9, - "EndLine": 9, - "StartColumn": 7, - "EndColumn": 63, - "Match": "apikey\": \"sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j\"", - "Secret": "sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j", - "File": "card_data/sample_scripts/get_data.py", - "SymlinkFile": "", - "Commit": "", - "Entropy": 4.6318326, - "Author": "", - "Email": "", - "Date": "", - "Message": "", - "Tags": [], - "Fingerprint": "card_data/sample_scripts/get_data.py:generic-api-key:9" - }, - { - "RuleID": "generic-api-key", - "Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", - "StartLine": 6, - "EndLine": 6, - "StartColumn": 2, - "EndColumn": 55, - "Match": "client_secret = \"PRD-0be0b74c3cb7-5716-44cb-b065-25d9\"", - "Secret": "PRD-0be0b74c3cb7-5716-44cb-b065-25d9", - "File": "card_data/sample_scripts/ebay.py", - "SymlinkFile": "", - "Commit": "", - "Entropy": 3.8089883, - "Author": "", - "Email": "", - "Date": "", - "Message": "", - "Tags": [], - "Fingerprint": "card_data/sample_scripts/ebay.py:generic-api-key:6" - }, - { - "RuleID": "generic-api-key", - "Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", - "StartLine": 157, - "EndLine": 157, - "StartColumn": 19, - "EndColumn": 75, - "Match": "apikey\", \"sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j\"", - "Secret": "sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j", - "File": "cmd/card/cardlist.go", - "SymlinkFile": "", - "Commit": "", - "Entropy": 4.6318326, - "Author": "", - "Email": "", - "Date": "", - "Message": "", - "Tags": [], - "Fingerprint": "cmd/card/cardlist.go:generic-api-key:157" - } -] From fdf24b2ef2c810cbac57c9efbca0097f95d4c005 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 24 Nov 2025 21:50:52 -0800 Subject: [PATCH 67/73] returning an error instead of calling `os.Exit(1)` (#201) --- cmd/card/card.go | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/cmd/card/card.go b/cmd/card/card.go index 3f91794d..b2aae454 100644 --- a/cmd/card/card.go +++ b/cmd/card/card.go @@ -44,22 +44,26 @@ func CardCommand() (string, error) { // Program 1: Series selection finalModel, err := tea.NewProgram(seriesModel).Run() if err != nil { - fmt.Println("Error running program:", err) - os.Exit(1) + return "", fmt.Errorf("error running series selection program: %w", err) } - result := finalModel.(SeriesModel) + result, ok := finalModel.(SeriesModel) + if !ok { + return "", fmt.Errorf("unexpected model type from series selection: got %T, want SeriesModel", finalModel) + } if result.SeriesID != "" { // Program 2: Sets selection setsModel := SetsList(result.SeriesID) finalSetsModel, err := tea.NewProgram(setsModel).Run() if err != nil { - fmt.Println("Error running sets program:", err) - os.Exit(1) + return "", fmt.Errorf("error running sets selection program: %w", err) } - setsResult := finalSetsModel.(SetsModel) + setsResult, ok := finalSetsModel.(SetsModel) + if !ok { + return "", fmt.Errorf("unexpected model type from sets selection: got %T, want SetsModel", finalSetsModel) + } if setsResult.Quitting { return output.String(), nil @@ -78,7 +82,10 @@ func CardCommand() (string, error) { return "", fmt.Errorf("error running cards program: %w", err) } - cardsResult := finalCardsModel.(CardsModel) + cardsResult, ok := finalCardsModel.(CardsModel) + if !ok { + return "", fmt.Errorf("unexpected model type from cards display: got %T, want CardsModel", finalCardsModel) + } if cardsResult.ViewImage { // Launch image viewer From bbe6cac45c7db184b067293e501b31381c5e1b98 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 24 Nov 2025 21:51:14 -0800 Subject: [PATCH 68/73] adding timeout to `http` call (#201) --- cmd/card/cardlist.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cmd/card/cardlist.go b/cmd/card/cardlist.go index 027f131c..ca8da2ad 100644 --- a/cmd/card/cardlist.go +++ b/cmd/card/cardlist.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "time" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" @@ -158,13 +159,17 @@ func CallCardData(url string) ([]byte, error) { req.Header.Add("Authorization", "Bearer sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j") req.Header.Add("Content-Type", "application/json") - client := &http.Client{} + client := &http.Client{Timeout: 15 * time.Second} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("error making GET request: %w", err) } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("error reading response body: %w", err) From c94c97982e6e1ac1b09f4959c2ded9b8c6f6f786 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 24 Nov 2025 21:57:42 -0800 Subject: [PATCH 69/73] adding timeout to `http` call, replacing `log.Fatalf()` by returning an error (#201) --- cmd/card/setslist.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cmd/card/setslist.go b/cmd/card/setslist.go index be1ccc6e..cd5b40fa 100644 --- a/cmd/card/setslist.go +++ b/cmd/card/setslist.go @@ -6,6 +6,7 @@ import ( "io" "log" "net/http" + "time" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" @@ -110,23 +111,27 @@ func SetsList(seriesID string) SetsModel { func callSetsData(url string) ([]byte, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { - log.Fatalf("Error creating request: %v", err) + return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Add("apikey", "sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j") req.Header.Add("Authorization", "Bearer sb_publishable_oondaaAIQC-wafhEiNgpSQ_reRiEp7j") req.Header.Add("Content-Type", "application/json") - client := &http.Client{} + client := &http.Client{Timeout: 15 * time.Second} resp, err := client.Do(req) if err != nil { - log.Fatalf("Error making GET request: %v", err) + return nil, fmt.Errorf("error making GET request: %w", err) } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + body, err := io.ReadAll(resp.Body) if err != nil { - log.Fatalf("Error reading response body: %v", err) + return nil, fmt.Errorf("error reading response body: %w", err) } return body, nil From 47266b8871967c62b124d11762442a8f2d644c30 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 24 Nov 2025 22:06:54 -0800 Subject: [PATCH 70/73] updating help menu (#201) --- cmd/card/cardlist.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/card/cardlist.go b/cmd/card/cardlist.go index ca8da2ad..ac86f22c 100644 --- a/cmd/card/cardlist.go +++ b/cmd/card/cardlist.go @@ -87,7 +87,7 @@ func (m CardsModel) View() string { return fmt.Sprintf("Highlight a card!\n%s\n%s", screen, - styling.KeyMenu.Render("↑ (move up) • ↓ (move down)\nctrl+c | esc (quit)")) + styling.KeyMenu.Render("↑ (move up)\n↓ (move down)\nspace (view image)\nctrl+c | esc (quit)")) } type cardData struct { From 3b307f2dea3d8ab33b907697149cfe36921dc3b3 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 24 Nov 2025 22:07:57 -0800 Subject: [PATCH 71/73] replacing `log.Fatalf()` by returning an error (#201) --- cmd/card/card.go | 2 +- cmd/card/setslist.go | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/cmd/card/card.go b/cmd/card/card.go index b2aae454..57fed8bd 100644 --- a/cmd/card/card.go +++ b/cmd/card/card.go @@ -54,7 +54,7 @@ func CardCommand() (string, error) { if result.SeriesID != "" { // Program 2: Sets selection - setsModel := SetsList(result.SeriesID) + setsModel, _ := SetsList(result.SeriesID) finalSetsModel, err := tea.NewProgram(setsModel).Run() if err != nil { return "", fmt.Errorf("error running sets selection program: %w", err) diff --git a/cmd/card/setslist.go b/cmd/card/setslist.go index cd5b40fa..4c55e701 100644 --- a/cmd/card/setslist.go +++ b/cmd/card/setslist.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "time" @@ -72,12 +71,16 @@ type setData struct { Symbol string `json:"symbol"` } -func SetsList(seriesID string) SetsModel { - body, _ := callSetsData("https://uoddayfnfkebrijlpfbh.supabase.co/rest/v1/sets") +func SetsList(seriesID string) (SetsModel, error) { + body, err := callSetsData("https://uoddayfnfkebrijlpfbh.supabase.co/rest/v1/sets") + if err != nil { + return SetsModel{}, fmt.Errorf("error getting sets data: %v", err) + } var allSets []setData - err := json.Unmarshal(body, &allSets) + + err = json.Unmarshal(body, &allSets) if err != nil { - log.Fatal(err) + return SetsModel{}, fmt.Errorf("error parsing sets data: %v", err) } // Filter sets by series_id and build ID map @@ -102,10 +105,11 @@ func SetsList(seriesID string) SetsModel { l.Styles.HelpStyle = helpStyle return SetsModel{ - List: l, - SeriesName: seriesID, - setsIDMap: setsIDMap, - } + List: l, + SeriesName: seriesID, + setsIDMap: setsIDMap, + }, + nil } func callSetsData(url string) ([]byte, error) { From bbc6f4811b061905593ed953f0ca44d40b7470c7 Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Mon, 24 Nov 2025 22:19:19 -0800 Subject: [PATCH 72/73] adding error handling to image rendering (#201) --- cmd/card/imageviewer.go | 4 +- cmd/card/imageviewer_test.go | 83 ++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/cmd/card/imageviewer.go b/cmd/card/imageviewer.go index 2c5f9b1b..e8f00846 100644 --- a/cmd/card/imageviewer.go +++ b/cmd/card/imageviewer.go @@ -7,6 +7,7 @@ import ( type ImageModel struct { CardName string ImageURL string + Error error } func (m ImageModel) Init() tea.Cmd { @@ -29,10 +30,11 @@ func (m ImageModel) View() string { } func ImageRenderer(cardName string, imageURL string) ImageModel { - imageData, _ := CardImage(imageURL) + imageData, err := CardImage(imageURL) return ImageModel{ CardName: cardName, ImageURL: imageData, + Error: err, } } diff --git a/cmd/card/imageviewer_test.go b/cmd/card/imageviewer_test.go index 13f736ae..7b2efa5f 100644 --- a/cmd/card/imageviewer_test.go +++ b/cmd/card/imageviewer_test.go @@ -1,6 +1,11 @@ package card import ( + "image" + "image/color" + "image/png" + "net/http" + "net/http/httptest" "testing" tea "github.com/charmbracelet/bubbletea" @@ -94,3 +99,81 @@ func TestImageModel_View_Empty(t *testing.T) { t.Errorf("View() with empty ImageURL should return empty string, got %v", result) } } + +func TestImageRenderer_Success(t *testing.T) { + // Create a test HTTP server that serves a valid PNG image + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + img := image.NewRGBA(image.Rect(0, 0, 10, 10)) + blue := color.RGBA{R: 0, G: 0, B: 255, A: 255} + for y := 0; y < 10; y++ { + for x := 0; x < 10; x++ { + img.Set(x, y, blue) + } + } + + w.Header().Set("Content-Type", "image/png") + w.WriteHeader(http.StatusOK) + _ = png.Encode(w, img) + })) + defer server.Close() + + model := ImageRenderer("Pikachu", server.URL) + + if model.CardName != "Pikachu" { + t.Errorf("ImageRenderer() CardName = %v, want %v", model.CardName, "Pikachu") + } + + if model.Error != nil { + t.Errorf("ImageRenderer() Error should be nil on success, got %v", model.Error) + } + + if model.ImageURL == "" { + t.Error("ImageRenderer() ImageURL should not be empty on success") + } +} + +func TestImageRenderer_Error(t *testing.T) { + // Create a test HTTP server that returns an error + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + model := ImageRenderer("Charizard", server.URL) + + if model.CardName != "Charizard" { + t.Errorf("ImageRenderer() CardName = %v, want %v", model.CardName, "Charizard") + } + + if model.Error == nil { + t.Error("ImageRenderer() Error should not be nil when image fetch fails") + } + + if model.ImageURL != "" { + t.Errorf("ImageRenderer() ImageURL should be empty on error, got %v", model.ImageURL) + } +} + +func TestImageRenderer_InvalidImage(t *testing.T) { + // Create a test HTTP server that returns invalid image data + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "image/png") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("not a valid image")) + })) + defer server.Close() + + model := ImageRenderer("Mewtwo", server.URL) + + if model.CardName != "Mewtwo" { + t.Errorf("ImageRenderer() CardName = %v, want %v", model.CardName, "Mewtwo") + } + + if model.Error == nil { + t.Error("ImageRenderer() Error should not be nil when image decoding fails") + } + + if model.ImageURL != "" { + t.Errorf("ImageRenderer() ImageURL should be empty on error, got %v", model.ImageURL) + } +} From f5a3282e1a13e7dcc21d7ebe80cfe45c42320b0c Mon Sep 17 00:00:00 2001 From: Christian Sanchez Date: Tue, 25 Nov 2025 09:58:41 -0800 Subject: [PATCH 73/73] adding error handling to set list loading (#201) --- cmd/card/card.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cmd/card/card.go b/cmd/card/card.go index 57fed8bd..dc3ed96e 100644 --- a/cmd/card/card.go +++ b/cmd/card/card.go @@ -54,7 +54,12 @@ func CardCommand() (string, error) { if result.SeriesID != "" { // Program 2: Sets selection - setsModel, _ := SetsList(result.SeriesID) + setsModel, err := SetsList(result.SeriesID) + + if err != nil { + return "", fmt.Errorf("error loading sets: %w", err) + } + finalSetsModel, err := tea.NewProgram(setsModel).Run() if err != nil { return "", fmt.Errorf("error running sets selection program: %w", err)