fix: Scrolling in detail view didnt work after the rework, this now works

This commit is contained in:
2026-03-14 17:24:21 +01:00
parent cb2225f1aa
commit 0a6015b154
4 changed files with 136 additions and 84 deletions

View File

@@ -3,7 +3,13 @@ package shippo
import (
"bytes"
"encoding/json"
"fmt" "io" "net/http" "time") const baseURL = "https://api.goshippo.com"
"fmt"
"io"
"net/http"
"time"
)
const baseURL = "https://api.goshippo.com"
// Client handles communication with the Shippo API.
type Client struct {
@@ -115,7 +121,7 @@ func (c *Client) ValidateAddress(addr Address) (*Address, error) {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
// Only accept addresses that Shippo explicitly validates as valid.
// Only accept addresses that Shippo explicitly validates as valid.
// International addressees where Shippo cannot validate are rejected.
// Address validation for addresses in other countries need to be added on a per-country API basis.
// Going to add Bring validation for Norway next

View File

@@ -132,9 +132,44 @@ func (m Model) BuildAccountView(availableHeight int) string {
}
}
// Apply internal scrolling to just the detail view
// Scrolling is needed for: order detail/list and FAQ when focused
if m.OrderViewState >= 1 || m.FaqFocused {
m.AccountDetailVP.SetContent(detailView)
detailView = m.AccountDetailVP.View()
detailLines := strings.Split(detailView, "\n")
totalLines := len(detailLines)
// In stacked mode, reduce available height by left panel
scrollHeight := availableHeight
if m.size < large {
leftPanelHeight := len(models.AccountMenuItems) + 1
scrollHeight = availableHeight - leftPanelHeight
}
if scrollHeight < 3 {
scrollHeight = 3
}
// Clamp scroll offset locally (don't mutate m since value receiver)
offset := m.ScrollOffset
maxScroll := totalLines - scrollHeight
if maxScroll < 0 {
maxScroll = 0
}
if offset > maxScroll {
offset = maxScroll
}
if offset < 0 {
offset = 0
}
// Slice to visible window
if totalLines > scrollHeight {
end := offset + scrollHeight
if end > totalLines {
end = totalLines
}
detailLines = detailLines[offset:end]
}
detailView = strings.Join(detailLines, "\n")
}
detailContainer := lipgloss.NewStyle().
@@ -316,3 +351,53 @@ func (m Model) buildOrderDetailView(order models.Order, width int) string {
return b.String()
}
// computeFaqScrollMax calculates the maximum scroll offset for the FAQ content.
// This renders the FAQ the same way BuildAccountView does, counts the lines,
// and returns the max scroll value. Returns 0 if no scrolling is needed.
func (m Model) computeFaqScrollMax() int {
leftWidth := 18
rightWidth := m.widthContent - leftWidth - 2
detailContentWidth := rightWidth - 2
if m.size < large {
detailContentWidth = m.widthContent - 2
}
questionStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#4682B4")).
Bold(true)
contentStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#AAAAAA")).
Width(detailContentWidth)
faqContent := ""
for i, faq := range m.FAQs {
faqContent += questionStyle.Render(wordWrap(faq.Question, detailContentWidth)) + "\n"
faqContent += contentStyle.Render(wordWrap(faq.Answer, detailContentWidth))
if i < len(m.FAQs)-1 {
faqContent += "\n\n"
}
}
titleStyle := lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#FFFFFF")).
MarginBottom(2)
detailView := titleStyle.Render("FAQ") + "\n\n" + faqContent
totalLines := len(strings.Split(detailView, "\n"))
scrollHeight := m.heightContainer - 7
if m.size < large {
scrollHeight -= len(models.AccountMenuItems) + 1
}
if scrollHeight < 3 {
scrollHeight = 3
}
maxScroll := totalLines - scrollHeight
if maxScroll < 0 {
maxScroll = 0
}
return maxScroll
}

View File

@@ -10,7 +10,6 @@ import (
"strings"
"time"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
gossh "golang.org/x/crypto/ssh"
@@ -108,9 +107,6 @@ type Model struct {
FAQs []FAQ // loaded from embedded faq.json
FaqFocused bool // true when FAQ detail is focused
// Account detail viewport - handles scrolling for the right panel
AccountDetailVP viewport.Model
accountDetailVPReady bool
}
// updateLayout recalculates all layout variables from the raw viewport dimensions.
@@ -139,23 +135,6 @@ func (m *Model) updateLayout(width, height int) {
}
m.widthContent = m.widthContainer - 2
if m.accountDetailVPReady {
vpHeight := m.heightContainer - 7
if m.size < large {
vpHeight -= len(models.AccountMenuItems) + 1
}
if vpHeight < 3 {
vpHeight = 3
}
leftWidth := 18
vpWidth := m.widthContent - leftWidth - 2
if m.size < large {
vpWidth = m.widthContent
}
m.AccountDetailVP.Width = vpWidth
m.AccountDetailVP.Height = vpHeight
}
}
// ProductsMsg is sent when products are fetched from API
@@ -239,7 +218,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.ScrollOffset = 0
m.OrderViewState = 0
m.FaqFocused = false
m.AccountDetailVP.GotoTop()
case "a":
m.ShowingMenu = false
m.ViewingCart = false
@@ -250,7 +228,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.ScrollOffset = 0
m.OrderViewState = 0
m.FaqFocused = false
m.AccountDetailVP.GotoTop()
if !m.OrdersLoaded {
return m, m.fetchOrdersCmd()
}
@@ -264,7 +241,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.ScrollOffset = 0
m.OrderViewState = 0
m.FaqFocused = false
m.AccountDetailVP.GotoTop()
case "esc":
m.ShowingMenu = false
case "?":
@@ -748,7 +724,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.ScrollOffset = 0
m.OrderViewState = 0
m.FaqFocused = false
m.AccountDetailVP.GotoTop()
case "c":
m.ViewingCart = true
@@ -759,7 +734,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.ScrollOffset = 0
m.OrderViewState = 0
m.FaqFocused = false
m.AccountDetailVP.GotoTop()
case "a":
m.ViewingCart = false
@@ -770,7 +744,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.ScrollOffset = 0
m.OrderViewState = 0
m.FaqFocused = false
m.AccountDetailVP.GotoTop()
if !m.OrdersLoaded {
return m, m.fetchOrdersCmd()
}
@@ -812,7 +786,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.ViewingAccount && m.FaqFocused {
m.FaqFocused = false
m.ScrollOffset = 0
m.AccountDetailVP.GotoTop()
return m, nil
}
// Navigate back through order view states
@@ -850,8 +823,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "up", "k":
if m.ViewingAccount {
if m.FaqFocused {
// Scroll FAQ content up via viewport
m.AccountDetailVP.LineUp(3)
// Scroll FAQ content up
m.ScrollOffset -= 3
if m.ScrollOffset < 0 {
m.ScrollOffset = 0
}
} else if m.OrderViewState == 1 && m.OrderCursor > 0 {
// Navigate up within order list
m.OrderCursor--
@@ -869,7 +845,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Navigate up within account tabs
m.AccountCursor--
m.ScrollOffset = 0
m.AccountDetailVP.GotoTop()
}
} else if !m.ViewingCart && !m.ViewingAccount && m.Cursor > 0 {
m.Cursor--
@@ -886,8 +861,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "down", "j":
if m.ViewingAccount {
if m.FaqFocused {
// Scroll FAQ content down via viewport
m.AccountDetailVP.LineDown(3)
// Scroll FAQ content down
m.ScrollOffset += 3
maxScroll := m.computeFaqScrollMax()
if m.ScrollOffset > maxScroll {
m.ScrollOffset = maxScroll
}
} else if m.OrderViewState == 1 && m.OrderCursor < len(m.Orders)-1 {
// Navigate down within order list
m.OrderCursor++
@@ -914,7 +893,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Navigate down within account tabs
m.AccountCursor++
m.ScrollOffset = 0
m.AccountDetailVP.GotoTop()
}
} else if !m.ViewingCart && !m.ViewingAccount && m.Cursor < len(m.Coffees)-1 {
m.Cursor++
@@ -941,15 +919,36 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
case "pgup", "ctrl+u":
// Scroll up
m.ScrollOffset -= 3
if m.ScrollOffset < 0 {
m.ScrollOffset = 0
if m.ViewingAccount {
// Only scroll when focused into scrollable content
if m.FaqFocused || m.OrderViewState >= 2 {
m.ScrollOffset -= 3
if m.ScrollOffset < 0 {
m.ScrollOffset = 0
}
}
} else {
// Scroll up in shop/cart views
m.ScrollOffset -= 3
if m.ScrollOffset < 0 {
m.ScrollOffset = 0
}
}
case "pgdown", "ctrl+d":
// Scroll down
m.ScrollOffset += 3
if m.ViewingAccount {
// Only scroll when focused into scrollable content
if m.FaqFocused || m.OrderViewState >= 2 {
m.ScrollOffset += 3
maxScroll := m.computeFaqScrollMax()
if m.FaqFocused && maxScroll >= 0 && m.ScrollOffset > maxScroll {
m.ScrollOffset = maxScroll
}
}
} else {
// Scroll down in shop/cart views
m.ScrollOffset += 3
}
case "+", "=":
if !m.ViewingCart && !m.ViewingAccount {

View File

@@ -2,9 +2,7 @@ package tui
import (
"strings"
"terminalShop/pkg/models"
"github.com/charmbracelet/bubbles/viewport"
"github.com/charmbracelet/lipgloss"
)
@@ -86,23 +84,6 @@ func (m Model) View() string {
errorBanner := errorStyle.Render("⚠ " + m.ErrorMsg)
if m.ViewingAccount {
if !m.accountDetailVPReady {
leftWidth := 18
vpWidth := m.widthContent - leftWidth - 2
if m.size < large {
vpWidth = m.widthContent
}
vpHeight := availableContentHeight
if m.size < large {
vpHeight -= len(models.AccountMenuItems) + 1
}
if vpHeight < 3 {
vpHeight = 3
}
m.AccountDetailVP = viewport.New(vpWidth, vpHeight)
m.AccountDetailVP.KeyMap = viewport.KeyMap{}
m.accountDetailVPReady = true
}
content = errorBanner + "\n" + m.BuildAccountView(availableContentHeight)
} else if m.ViewingCart && m.CheckoutStep == 1 {
if m.ShippingView == 0 && m.ShippingForm == nil {
@@ -110,8 +91,6 @@ func (m Model) View() string {
} else if m.ShippingForm != nil {
content = errorBanner + "\n" + m.RenderShippingForm(m.ShippingForm)
}
// TODO! THIS NEEDS TO BE REFACTORED - JESUS CHIRST
} else if m.ViewingCart && m.CheckoutStep == 2 && m.CheckingOut {
content = errorBanner + "\n submitting order..."
} else if m.ViewingCart && m.CheckoutStep == 2 && m.PaymentView == 0 && m.PaymentForm == nil {
@@ -126,23 +105,6 @@ func (m Model) View() string {
content = errorBanner + "\n" + m.BuildShopView()
}
} else if m.ViewingAccount {
if !m.accountDetailVPReady {
leftWidth := 18
vpWidth := m.widthContent - leftWidth - 2
if m.size < large {
vpWidth = m.widthContent
}
vpHeight := availableContentHeight
if m.size < large {
vpHeight -= len(models.AccountMenuItems) + 1
}
if vpHeight < 3 {
vpHeight = 3
}
m.AccountDetailVP = viewport.New(vpWidth, vpHeight)
m.AccountDetailVP.KeyMap = viewport.KeyMap{} // we handle keys manually
m.accountDetailVPReady = true
}
content = m.BuildAccountView(availableContentHeight)
} else if m.ViewingCart && m.CheckoutStep == 1 {
if m.ShippingView == 0 && m.ShippingForm == nil {
@@ -169,10 +131,10 @@ func (m Model) View() string {
totalLines := len(contentLines)
// Skip global scroll when account view handles its own scrolling
// (order list/detail and FAQ focused use the viewport in BuildAccountView)
// (order list/detail and FAQ focused use internal scrolling in BuildAccountView)
accountHandlesScroll := m.ViewingAccount && (m.OrderViewState >= 1 || m.FaqFocused)
if !accountHandlesScroll {
// Ensure scroll offset is without bounds
// Ensure scroll offset is within bounds
if m.ScrollOffset < 0 {
m.ScrollOffset = 0
}