fix: Scrolling in detail view didnt work after the rework, this now works
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user