feat: throw out the boolean navigation with page routing
replaced ViewingCart/ViewingAccount booleans and CheckoutStep int with a single 'page' type and a SwitchPage() method, this is just almost copying the reference implementation (terminalDotShop)'s routing pattern. Adds inCartFlow(), chechoutStep(), and buildPageContent() helpers to clean up those huge if/else chains in views, footer, header, and breadcrumds. code is looking a lot better now!
This commit is contained in:
@@ -729,22 +729,3 @@ func (c *Client) DeleteAddress(id uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func(c *Client) SetDefaultAddress(id uint) error {
|
||||
url := fmt.Sprintf("%s/api/v1/addresses/%d/default", c.BaseURL, id)
|
||||
req, err := http.NewRequest("PUT", url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.Token)
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("failed to set default address: status %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
func (m Model) BuildBreadcrumbs() string {
|
||||
// Only show breadcrumbs in cart view
|
||||
if !m.ViewingCart {
|
||||
if !m.inCartFlow() {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func (m Model) BuildBreadcrumbs() string {
|
||||
}
|
||||
|
||||
// Get current checkout step from model
|
||||
currentStep := m.CheckoutStep
|
||||
currentStep := m.checkoutStep()
|
||||
|
||||
var items []string
|
||||
for i, label := range labels {
|
||||
|
||||
@@ -36,7 +36,7 @@ func (m Model) BuildFooter() string {
|
||||
keybindStyle.Render("q"),
|
||||
descStyle.Render("quit"),
|
||||
)
|
||||
} else if m.ViewingAccount {
|
||||
} else if m.currentPage == accountPage {
|
||||
switch m.OrderViewState {
|
||||
case 2:
|
||||
// Viewing single order detail
|
||||
@@ -77,7 +77,7 @@ func (m Model) BuildFooter() string {
|
||||
descStyle.Render("quit"),
|
||||
)
|
||||
}
|
||||
} else if m.ViewingCart && m.CheckoutStep == 3 {
|
||||
} else if m.currentPage == confirmPage {
|
||||
// Confirmation screen
|
||||
footerText = fmt.Sprintf("%s %s %s %s",
|
||||
keybindStyle.Render("esc"),
|
||||
@@ -85,7 +85,7 @@ func (m Model) BuildFooter() string {
|
||||
keybindStyle.Render("q"),
|
||||
descStyle.Render("quit"),
|
||||
)
|
||||
} else if m.ViewingCart && m.CheckoutStep == 1 {
|
||||
} else if m.currentPage == shippingPage {
|
||||
if m.ShippingView == 0 && m.ShippingForm == nil {
|
||||
// Address list
|
||||
footerText = fmt.Sprintf("%s %s %s %s %s %s %s %s",
|
||||
@@ -109,7 +109,7 @@ func (m Model) BuildFooter() string {
|
||||
descStyle.Render("back"),
|
||||
)
|
||||
}
|
||||
} else if m.ViewingCart && m.CheckoutStep == 2 {
|
||||
} else if m.currentPage == paymentPage {
|
||||
if m.PaymentView == 0 && m.PaymentForm == nil {
|
||||
// Card list
|
||||
footerText = fmt.Sprintf("%s %s %s %s %s %s %s %s",
|
||||
@@ -136,7 +136,7 @@ func (m Model) BuildFooter() string {
|
||||
descStyle.Render("back"),
|
||||
)
|
||||
}
|
||||
} else if m.ViewingCart && m.CheckoutStep == 0 {
|
||||
} else if m.currentPage == cartPage {
|
||||
// In cart view, show proceed option
|
||||
if len(m.Cart) > 0 {
|
||||
footerText = fmt.Sprintf("%s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s",
|
||||
@@ -173,16 +173,6 @@ func (m Model) BuildFooter() string {
|
||||
descStyle.Render("quit"),
|
||||
)
|
||||
}
|
||||
} else if m.ViewingCart {
|
||||
// Other checkout steps
|
||||
footerText = fmt.Sprintf("%s %s %s %s %s %s",
|
||||
keybindStyle.Render("esc"),
|
||||
descStyle.Render("back"),
|
||||
keybindStyle.Render("s"),
|
||||
descStyle.Render("shop"),
|
||||
keybindStyle.Render("q"),
|
||||
descStyle.Render("quit"),
|
||||
)
|
||||
} else {
|
||||
footerText = fmt.Sprintf("%s %s %s %s %s %s %s %s %s %s %s %s",
|
||||
keybindStyle.Render("j/k"),
|
||||
|
||||
@@ -24,7 +24,7 @@ func (m Model) BuildHeader() string {
|
||||
itemCountText := grayStyle.Render(fmt.Sprintf("[%d]", itemCount))
|
||||
|
||||
var cartKeybind, cartName, cartInfo string
|
||||
if m.ViewingCart {
|
||||
if m.inCartFlow() {
|
||||
// Active: bold white keybind and name
|
||||
cartKeybind = boldWhiteStyle.Render("c")
|
||||
cartName = boldWhiteStyle.Render("cart")
|
||||
@@ -57,7 +57,7 @@ func (m Model) BuildHeader() string {
|
||||
separator := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")).Render("|")
|
||||
|
||||
var shopKeybind, shopName string
|
||||
if !m.ViewingCart && !m.ViewingAccount {
|
||||
if m.currentPage == shopPage {
|
||||
shopKeybind = boldWhiteStyle.Render("s")
|
||||
shopName = boldWhiteStyle.Render("shop")
|
||||
} else {
|
||||
@@ -66,7 +66,7 @@ func (m Model) BuildHeader() string {
|
||||
}
|
||||
|
||||
var accountKeybind, accountName string
|
||||
if m.ViewingAccount {
|
||||
if m.currentPage == accountPage {
|
||||
accountKeybind = boldWhiteStyle.Render("a")
|
||||
accountName = boldWhiteStyle.Render("account")
|
||||
} else {
|
||||
|
||||
@@ -21,10 +21,8 @@ func TestModel_BuildMenuView(t *testing.T) {
|
||||
Cart map[uint]*models.CartItem
|
||||
CartCursor int
|
||||
AccountCursor int
|
||||
CheckoutStep int
|
||||
ScrollOffset int
|
||||
ViewingCart bool
|
||||
ViewingAccount bool
|
||||
currentPage page
|
||||
viewportWidth int
|
||||
viewportHeight int
|
||||
widthContainer int
|
||||
@@ -68,10 +66,8 @@ func TestModel_BuildMenuView(t *testing.T) {
|
||||
Cart: tt.fields.Cart,
|
||||
CartCursor: tt.fields.CartCursor,
|
||||
AccountCursor: tt.fields.AccountCursor,
|
||||
CheckoutStep: tt.fields.CheckoutStep,
|
||||
ScrollOffset: tt.fields.ScrollOffset,
|
||||
ViewingCart: tt.fields.ViewingCart,
|
||||
ViewingAccount: tt.fields.ViewingAccount,
|
||||
currentPage: tt.fields.currentPage,
|
||||
viewportWidth: tt.fields.viewportWidth,
|
||||
viewportHeight: tt.fields.viewportHeight,
|
||||
widthContainer: tt.fields.widthContainer,
|
||||
|
||||
164
pkg/tui/model.go
164
pkg/tui/model.go
@@ -28,6 +28,18 @@ const (
|
||||
large // viewportWidth >= 80
|
||||
)
|
||||
|
||||
// page represetnts the currently active TUI screen
|
||||
type page int
|
||||
|
||||
const (
|
||||
shopPage page = iota
|
||||
cartPage
|
||||
shippingPage
|
||||
paymentPage
|
||||
confirmPage
|
||||
accountPage
|
||||
)
|
||||
|
||||
// This msg is sent after a debounce delay to apply a pending resize.
|
||||
// the seq field makes sure that only the tick from the most recent resize is applied.
|
||||
type resizeTickMsg struct {
|
||||
@@ -54,18 +66,16 @@ type Model struct {
|
||||
Cart map[uint]*models.CartItem // maps CoffeeID to cart item
|
||||
CartCursor int // cursor position in cart view
|
||||
AccountCursor int
|
||||
CheckoutStep int // 0=cart, 1=shipping, 2=payment, 3=confirmation
|
||||
currentPage page // currently active screen
|
||||
switched bool // true when the page just changed (for state resets)
|
||||
ScrollOffset int // scroll position for content
|
||||
ViewingCart bool // true when viewing cart details
|
||||
CheckingOut bool // true while saveCardAndConvert is running
|
||||
lastCartUpdateID int64 // debounce ID for optimistic cart sync
|
||||
ViewingAccount bool
|
||||
|
||||
// Menu modal state
|
||||
ShowingMenu bool // true when full-screen menu is showing
|
||||
ShowingHelp bool // true when help-screen is showing
|
||||
menuLastCart bool // was viewing cart when menu was opened
|
||||
menuLastAcct bool // was viewing account when menu was opened
|
||||
menuLastPage page // page we were on before opening menu
|
||||
|
||||
// Layout fields — responsive container system
|
||||
viewportWidth int // raw terminal width
|
||||
@@ -109,6 +119,34 @@ type Model struct {
|
||||
|
||||
}
|
||||
|
||||
// SwitchPage changed the active page and marks that a switch happened
|
||||
func (m Model) SwitchPage(p page) Model {
|
||||
m.currentPage = p
|
||||
m.switched = true
|
||||
return m
|
||||
}
|
||||
|
||||
// inCartFlow returns true when the user is anywhere in the cart or checkout flow
|
||||
func (m Model) inCartFlow() bool {
|
||||
return m.currentPage == cartPage ||
|
||||
m.currentPage == shippingPage ||
|
||||
m.currentPage == paymentPage ||
|
||||
m.currentPage == confirmPage
|
||||
}
|
||||
|
||||
func (m Model) checkoutStep() int {
|
||||
switch m.currentPage {
|
||||
case shippingPage:
|
||||
return 1
|
||||
case paymentPage:
|
||||
return 2
|
||||
case confirmPage:
|
||||
return 3
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// updateLayout recalculates all layout variables from the raw viewport dimensions.
|
||||
// This should be called whenever the terminal is resized.
|
||||
func (m *Model) updateLayout(width, height int) {
|
||||
@@ -210,9 +248,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch keyMsg.String() {
|
||||
case "s":
|
||||
m.ShowingMenu = false
|
||||
m.ViewingCart = false
|
||||
m.ViewingAccount = false
|
||||
m.CheckoutStep = 0
|
||||
m = m.SwitchPage(shopPage)
|
||||
m.ShippingForm = nil
|
||||
m.PaymentForm = nil
|
||||
m.ScrollOffset = 0
|
||||
@@ -220,9 +256,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.FaqFocused = false
|
||||
case "a":
|
||||
m.ShowingMenu = false
|
||||
m.ViewingCart = false
|
||||
m.ViewingAccount = true
|
||||
m.CheckoutStep = 0
|
||||
m = m.SwitchPage(accountPage)
|
||||
m.ShippingForm = nil
|
||||
m.PaymentForm = nil
|
||||
m.ScrollOffset = 0
|
||||
@@ -233,9 +267,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
case "c":
|
||||
m.ShowingMenu = false
|
||||
m.ViewingCart = true
|
||||
m.ViewingAccount = false
|
||||
m.CheckoutStep = 0
|
||||
m.SwitchPage(cartPage)
|
||||
m.ShippingForm = nil
|
||||
m.PaymentForm = nil
|
||||
m.ScrollOffset = 0
|
||||
@@ -266,7 +298,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if m.ViewingCart && m.CheckoutStep == 1 {
|
||||
if m.currentPage == shippingPage {
|
||||
switch msg := msg.(type) {
|
||||
case ShippingFormCompleteMsg:
|
||||
addr := models.Address{
|
||||
@@ -306,7 +338,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
m.ShippingInfo = &msg.Address
|
||||
m.ShippingForm = nil
|
||||
m.CheckoutStep = 2
|
||||
m = m.SwitchPage(paymentPage)
|
||||
m.SelectedCard = nil
|
||||
return m, m.fetchCardsCmd()
|
||||
|
||||
@@ -329,7 +361,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.ErrorMsg = ""
|
||||
switch keyMsg.String() {
|
||||
case "esc":
|
||||
m.CheckoutStep = 0
|
||||
m = m.SwitchPage(cartPage)
|
||||
m.ErrorMsg = ""
|
||||
return m, nil
|
||||
case "up", "k":
|
||||
@@ -345,7 +377,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
addr := m.SavedAddresses[m.AddressCursor]
|
||||
selected := addr // copy so we have a stable pointer
|
||||
m.ShippingInfo = &selected
|
||||
m.CheckoutStep = 2
|
||||
m = m.SwitchPage(paymentPage)
|
||||
m.SelectedCard = nil
|
||||
return m, m.fetchCardsCmd()
|
||||
}
|
||||
@@ -382,8 +414,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.CheckoutStep = 0
|
||||
m.ShippingForm = nil
|
||||
m = m.SwitchPage(cartPage)
|
||||
return m, nil
|
||||
}
|
||||
m.ErrorMsg = ""
|
||||
@@ -395,7 +427,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
// When the payment form is active, route ALL messages to it.
|
||||
if m.ViewingCart && m.CheckoutStep == 2 {
|
||||
if m.currentPage == paymentPage {
|
||||
switch msg := msg.(type) {
|
||||
|
||||
case CardsMsg:
|
||||
@@ -406,7 +438,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, m.PaymentForm.form.Init()
|
||||
}
|
||||
m.SavedCards = msg.Cards
|
||||
m.PaymentView = 0 // show card list
|
||||
m.PaymentView = 0
|
||||
m.CardCursor = 0
|
||||
m.PaymentForm = nil
|
||||
return m, nil
|
||||
@@ -426,7 +458,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.CheckingOut = false
|
||||
if msg.Err != nil {
|
||||
m.ErrorMsg = fmt.Sprintf("checkout failed: %v", msg.Err)
|
||||
m.CheckoutStep = 2
|
||||
// already on paymentPage, no page switch needed
|
||||
// Return to card list if we have saved cards, otherwise show form
|
||||
if len(m.SavedCards) > 0 {
|
||||
m.PaymentView = 0
|
||||
@@ -438,7 +470,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
m.CheckoutStep = 3
|
||||
m = m.SwitchPage(confirmPage)
|
||||
m.OrdersLoaded = false
|
||||
return m, nil
|
||||
case StripeTokenErrMsg:
|
||||
@@ -479,8 +511,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.ErrorMsg = ""
|
||||
switch keyMsg.String() {
|
||||
case "esc":
|
||||
m.CheckoutStep = 1
|
||||
m.ErrorMsg = ""
|
||||
m = m.SwitchPage(shippingPage)
|
||||
return m, m.fetchAddressesCmd()
|
||||
case "up", "k":
|
||||
if m.CardCursor > 0 {
|
||||
@@ -516,7 +548,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, m.deleteCardCmd(card.ID)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
@@ -533,8 +564,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
// No saved cards - go back to shipping
|
||||
m.CheckoutStep = 1
|
||||
m.PaymentForm = nil
|
||||
m = m.SwitchPage(shippingPage)
|
||||
return m, m.fetchAddressesCmd()
|
||||
}
|
||||
m.ErrorMsg = ""
|
||||
@@ -575,7 +606,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
m.ShippingInfo = &msg.Address
|
||||
m.ShippingForm = nil
|
||||
m.CheckoutStep = 2
|
||||
m = m.SwitchPage(paymentPage)
|
||||
m.SelectedCard = nil
|
||||
return m, m.fetchCardsCmd()
|
||||
|
||||
@@ -583,20 +614,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.ErrorMsg = msg.Message
|
||||
return m, nil
|
||||
|
||||
case PaymentFormCompleteMsg:
|
||||
m.CheckoutStep = 3
|
||||
m.PaymentForm = nil
|
||||
return m, nil
|
||||
|
||||
case PaymentFormErrorMsg:
|
||||
m.ErrorMsg = msg.Message
|
||||
return m, nil
|
||||
|
||||
case StripeTokenMsg:
|
||||
m.CheckoutStep = 3
|
||||
m.PaymentForm = nil
|
||||
return m, nil
|
||||
|
||||
case StripeTokenErrMsg:
|
||||
m.ErrorMsg = fmt.Sprintf("payment failed: %v", msg.Err)
|
||||
return m, nil
|
||||
@@ -716,9 +733,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, tea.Quit
|
||||
|
||||
case "s":
|
||||
m.ViewingCart = false
|
||||
m.ViewingAccount = false
|
||||
m.CheckoutStep = 0
|
||||
m = m.SwitchPage(shopPage)
|
||||
m.ShippingForm = nil
|
||||
m.PaymentForm = nil
|
||||
m.ScrollOffset = 0
|
||||
@@ -726,9 +741,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.FaqFocused = false
|
||||
|
||||
case "c":
|
||||
m.ViewingCart = true
|
||||
m.ViewingAccount = false
|
||||
m.CheckoutStep = 0
|
||||
m = m.SwitchPage(cartPage)
|
||||
m.ShippingForm = nil
|
||||
m.PaymentForm = nil
|
||||
m.ScrollOffset = 0
|
||||
@@ -736,9 +749,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.FaqFocused = false
|
||||
|
||||
case "a":
|
||||
m.ViewingCart = false
|
||||
m.ViewingAccount = true
|
||||
m.CheckoutStep = 0
|
||||
m = m.SwitchPage(accountPage)
|
||||
m.ShippingForm = nil
|
||||
m.PaymentForm = nil
|
||||
m.ScrollOffset = 0
|
||||
@@ -751,13 +762,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
case "m":
|
||||
m.ShowingMenu = true
|
||||
m.menuLastCart = m.ViewingCart
|
||||
m.menuLastAcct = m.ViewingAccount
|
||||
m.menuLastPage = m.currentPage
|
||||
return m, nil
|
||||
|
||||
case "p", "enter":
|
||||
// Account view: enter order list or order detail
|
||||
if m.ViewingAccount && m.AccountCursor == 0 && len(m.Orders) > 0 {
|
||||
if m.currentPage == accountPage && m.AccountCursor == 0 && len(m.Orders) > 0 {
|
||||
if m.OrderViewState == 0 {
|
||||
m.OrderViewState = 1
|
||||
m.OrderCursor = 0
|
||||
@@ -770,34 +780,33 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
// Account view: focus into FAQ for scrolling
|
||||
if m.ViewingAccount && m.AccountCursor == 1 && !m.FaqFocused {
|
||||
if m.currentPage == accountPage && m.AccountCursor == 1 && !m.FaqFocused {
|
||||
m.FaqFocused = true
|
||||
m.ScrollOffset = 0
|
||||
return m, nil
|
||||
}
|
||||
// Proceed to checkout from cart
|
||||
if m.ViewingCart && m.CheckoutStep == 0 && len(m.Cart) > 0 {
|
||||
m.CheckoutStep = 1
|
||||
if m.currentPage == cartPage && len(m.Cart) > 0 {
|
||||
m = m.SwitchPage(shippingPage)
|
||||
return m, m.fetchAddressesCmd()
|
||||
}
|
||||
|
||||
case "esc":
|
||||
// Unfocus FAQ scrolling
|
||||
if m.ViewingAccount && m.FaqFocused {
|
||||
if m.currentPage == accountPage && m.FaqFocused {
|
||||
m.FaqFocused = false
|
||||
m.ScrollOffset = 0
|
||||
return m, nil
|
||||
}
|
||||
// Navigate back through order view states
|
||||
if m.ViewingAccount && m.OrderViewState > 0 {
|
||||
if m.currentPage == accountPage && m.OrderViewState > 0 {
|
||||
m.OrderViewState--
|
||||
m.ScrollOffset = 0
|
||||
return m, nil
|
||||
}
|
||||
// From confirmation, go back to shop
|
||||
if m.ViewingCart && m.CheckoutStep == 3 {
|
||||
m.ViewingCart = false
|
||||
m.CheckoutStep = 0
|
||||
if m.currentPage == confirmPage {
|
||||
m = m.SwitchPage(shopPage)
|
||||
m.ShippingForm = nil
|
||||
m.PaymentForm = nil
|
||||
m.ShippingInfo = nil
|
||||
@@ -812,16 +821,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// Go back in checkout flow
|
||||
if m.ViewingCart && m.CheckoutStep > 0 {
|
||||
m.CheckoutStep--
|
||||
m.ShippingForm = nil
|
||||
m.PaymentForm = nil
|
||||
return m, nil
|
||||
}
|
||||
|
||||
case "up", "k":
|
||||
if m.ViewingAccount {
|
||||
if m.currentPage == accountPage {
|
||||
if m.FaqFocused {
|
||||
// Scroll FAQ content up
|
||||
m.ScrollOffset -= 3
|
||||
@@ -846,9 +848,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.AccountCursor--
|
||||
m.ScrollOffset = 0
|
||||
}
|
||||
} else if !m.ViewingCart && !m.ViewingAccount && m.Cursor > 0 {
|
||||
} else if m.currentPage == shopPage && m.Cursor > 0 {
|
||||
m.Cursor--
|
||||
} else if m.ViewingCart && m.CartCursor > 0 {
|
||||
} else if m.currentPage == cartPage && m.CartCursor > 0 {
|
||||
m.CartCursor--
|
||||
// Auto-scroll to keep cursor visible (each cart item is ~5 lines)
|
||||
itemHeight := 5
|
||||
@@ -859,7 +861,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
case "down", "j":
|
||||
if m.ViewingAccount {
|
||||
if m.currentPage == accountPage {
|
||||
if m.FaqFocused {
|
||||
// Scroll FAQ content down
|
||||
m.ScrollOffset += 3
|
||||
@@ -894,9 +896,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.AccountCursor++
|
||||
m.ScrollOffset = 0
|
||||
}
|
||||
} else if !m.ViewingCart && !m.ViewingAccount && m.Cursor < len(m.Coffees)-1 {
|
||||
} else if m.currentPage == shopPage && m.Cursor < len(m.Coffees)-1 {
|
||||
m.Cursor++
|
||||
} else if m.ViewingCart && m.CartCursor < len(m.Cart)-1 {
|
||||
} else if m.currentPage == cartPage && m.CartCursor < len(m.Cart)-1 {
|
||||
m.CartCursor++
|
||||
// Auto-scroll to keep cursor visible (each cart item is ~5 lines)
|
||||
itemHeight := 5
|
||||
@@ -919,7 +921,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
case "pgup", "ctrl+u":
|
||||
if m.ViewingAccount {
|
||||
if m.currentPage == accountPage {
|
||||
// Only scroll when focused into scrollable content
|
||||
if m.FaqFocused || m.OrderViewState >= 2 {
|
||||
m.ScrollOffset -= 3
|
||||
@@ -936,7 +938,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
case "pgdown", "ctrl+d":
|
||||
if m.ViewingAccount {
|
||||
if m.currentPage == accountPage {
|
||||
// Only scroll when focused into scrollable content
|
||||
if m.FaqFocused || m.OrderViewState >= 2 {
|
||||
m.ScrollOffset += 3
|
||||
@@ -951,7 +953,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
case "+", "=":
|
||||
if !m.ViewingCart && !m.ViewingAccount {
|
||||
if m.currentPage == shopPage {
|
||||
// Increment quantity in shop view (keyed by CoffeeID)
|
||||
coffeeID := m.Coffees[m.Cursor].ID
|
||||
if item, exists := m.Cart[coffeeID]; exists {
|
||||
@@ -964,7 +966,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
return m, m.syncCartItemCmd(coffeeID, m.Cart[coffeeID].Quantity)
|
||||
} else if m.ViewingCart {
|
||||
} else if m.currentPage == cartPage {
|
||||
// Increment quantity in cart view
|
||||
cartItems := m.GetCartItemsSlice()
|
||||
if m.CartCursor >= 0 && m.CartCursor < len(cartItems) {
|
||||
@@ -974,7 +976,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
case "-", "_":
|
||||
if !m.ViewingCart && !m.ViewingAccount {
|
||||
if m.currentPage == shopPage {
|
||||
// Decrement quantity in shop view (keyed by CoffeeID)
|
||||
coffeeID := m.Coffees[m.Cursor].ID
|
||||
if item, exists := m.Cart[coffeeID]; exists {
|
||||
@@ -992,7 +994,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, m.syncCartItemCmd(coffeeID, newQty)
|
||||
}
|
||||
} else if m.ViewingCart {
|
||||
} else if m.currentPage == cartPage {
|
||||
// Decrement quantity in cart view
|
||||
cartItems := m.GetCartItemsSlice()
|
||||
if m.CartCursor >= 0 && m.CartCursor < len(cartItems) {
|
||||
@@ -1487,8 +1489,6 @@ func NewModel(username string) Model {
|
||||
AccountCursor: 0,
|
||||
OrderCursor: 0,
|
||||
OrderViewState: 0,
|
||||
ViewingCart: false,
|
||||
ViewingAccount: false,
|
||||
Loading: true,
|
||||
APIClient: api.NewClient("http://localhost:8080", ""),
|
||||
StripeKey: os.Getenv("STRIPE_PUBLIC_KEY"),
|
||||
|
||||
@@ -21,10 +21,8 @@ func TestModel_updateLayout(t *testing.T) {
|
||||
Cart map[uint]*models.CartItem
|
||||
CartCursor int
|
||||
AccountCursor int
|
||||
CheckoutStep int
|
||||
ScrollOffset int
|
||||
ViewingCart bool
|
||||
ViewingAccount bool
|
||||
currentPage page
|
||||
viewportWidth int
|
||||
viewportHeight int
|
||||
widthContainer int
|
||||
@@ -72,10 +70,8 @@ func TestModel_updateLayout(t *testing.T) {
|
||||
Cart: tt.fields.Cart,
|
||||
CartCursor: tt.fields.CartCursor,
|
||||
AccountCursor: tt.fields.AccountCursor,
|
||||
CheckoutStep: tt.fields.CheckoutStep,
|
||||
ScrollOffset: tt.fields.ScrollOffset,
|
||||
ViewingCart: tt.fields.ViewingCart,
|
||||
ViewingAccount: tt.fields.ViewingAccount,
|
||||
currentPage: tt.fields.currentPage,
|
||||
viewportWidth: tt.fields.viewportWidth,
|
||||
viewportHeight: tt.fields.viewportHeight,
|
||||
widthContainer: tt.fields.widthContainer,
|
||||
|
||||
@@ -68,62 +68,16 @@ func (m Model) View() string {
|
||||
marginBottom = 0
|
||||
}
|
||||
|
||||
// Build main content based on view
|
||||
var content string
|
||||
if m.Loading {
|
||||
loadingStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("205")).
|
||||
Padding(2, 4)
|
||||
loadingStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Padding(2, 4)
|
||||
content = loadingStyle.Render("Loading products from API...")
|
||||
} else if m.ErrorMsg != "" {
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("196")).
|
||||
Background(lipgloss.Color("52")).
|
||||
Padding(0, 1).
|
||||
MarginBottom(1)
|
||||
errorBanner := errorStyle.Render("⚠ " + m.ErrorMsg)
|
||||
|
||||
if m.ViewingAccount {
|
||||
content = errorBanner + "\n" + m.BuildAccountView(availableContentHeight)
|
||||
} else if m.ViewingCart && m.CheckoutStep == 1 {
|
||||
if m.ShippingView == 0 && m.ShippingForm == nil {
|
||||
content = errorBanner + "\n" + m.RenderAddressList()
|
||||
} else if m.ShippingForm != nil {
|
||||
content = errorBanner + "\n" + m.RenderShippingForm(m.ShippingForm)
|
||||
}
|
||||
} 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 {
|
||||
content = errorBanner + "\n" + m.RenderCardList()
|
||||
} else if m.ViewingCart && m.CheckoutStep == 2 && m.PaymentForm != nil {
|
||||
content = errorBanner + "\n" + m.RenderPaymentForm(m.PaymentForm)
|
||||
} else if m.ViewingCart && m.CheckoutStep == 3 {
|
||||
content = errorBanner + "\n" + m.RenderConfirmation()
|
||||
} else if m.ViewingCart {
|
||||
content = errorBanner + "\n" + m.BuildCartView()
|
||||
} else {
|
||||
content = errorBanner + "\n" + m.BuildShopView()
|
||||
}
|
||||
} else if m.ViewingAccount {
|
||||
content = m.BuildAccountView(availableContentHeight)
|
||||
} else if m.ViewingCart && m.CheckoutStep == 1 {
|
||||
if m.ShippingView == 0 && m.ShippingForm == nil {
|
||||
content = m.RenderAddressList()
|
||||
} else if m.ShippingForm != nil {
|
||||
content = m.RenderShippingForm(m.ShippingForm)
|
||||
}
|
||||
} else if m.ViewingCart && m.CheckoutStep == 2 && m.CheckingOut {
|
||||
content = " submitting order..."
|
||||
} else if m.ViewingCart && m.CheckoutStep == 2 && m.PaymentView == 0 && m.PaymentForm == nil {
|
||||
content = m.RenderCardList()
|
||||
} else if m.ViewingCart && m.CheckoutStep == 2 && m.PaymentForm != nil {
|
||||
content = m.RenderPaymentForm(m.PaymentForm)
|
||||
} else if m.ViewingCart && m.CheckoutStep == 3 {
|
||||
content = m.RenderConfirmation()
|
||||
} else if m.ViewingCart {
|
||||
content = m.BuildCartView()
|
||||
errorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Background(lipgloss.Color("52")).Padding(0, 1).MarginBottom(1)
|
||||
errorBanner := errorStyle.Render(m.ErrorMsg)
|
||||
content = errorBanner + "\n" + m.buildPageContent(availableContentHeight)
|
||||
} else {
|
||||
content = m.BuildShopView()
|
||||
content = m.buildPageContent(availableContentHeight)
|
||||
}
|
||||
|
||||
// Split content into lines and handle scrolling
|
||||
@@ -132,7 +86,7 @@ func (m Model) View() string {
|
||||
|
||||
// Skip global scroll when account view handles its own scrolling
|
||||
// (order list/detail and FAQ focused use internal scrolling in BuildAccountView)
|
||||
accountHandlesScroll := m.ViewingAccount && (m.OrderViewState >= 1 || m.FaqFocused)
|
||||
accountHandlesScroll := m.currentPage == accountPage && (m.OrderViewState >= 1 || m.FaqFocused)
|
||||
if !accountHandlesScroll {
|
||||
// Ensure scroll offset is within bounds
|
||||
if m.ScrollOffset < 0 {
|
||||
@@ -202,3 +156,29 @@ func (m Model) View() string {
|
||||
constrained,
|
||||
)
|
||||
}
|
||||
|
||||
func (m Model) buildPageContent(height int) string {
|
||||
switch m.currentPage {
|
||||
case accountPage:
|
||||
return m.BuildAccountView(height)
|
||||
case shippingPage:
|
||||
if m.ShippingView == 0 && m.ShippingForm == nil {
|
||||
return m.RenderAddressList()
|
||||
}
|
||||
return m.RenderShippingForm(m.ShippingForm)
|
||||
case paymentPage:
|
||||
if m.CheckingOut {
|
||||
return " submitting order..."
|
||||
}
|
||||
if m.PaymentView == 0 && m.PaymentForm == nil {
|
||||
return m.RenderCardList()
|
||||
}
|
||||
return m.RenderPaymentForm(m.PaymentForm)
|
||||
case confirmPage:
|
||||
return m.RenderConfirmation()
|
||||
case cartPage:
|
||||
return m.BuildCartView()
|
||||
default:
|
||||
return m.BuildShopView()
|
||||
}
|
||||
}
|
||||
|
||||
1
terminal-shop-source
Submodule
1
terminal-shop-source
Submodule
Submodule terminal-shop-source added at 697730e73c
Reference in New Issue
Block a user