From bad11358da147209913f7c7ad3cb793bbd9587c4 Mon Sep 17 00:00:00 2001 From: marcusDenslow Date: Mon, 2 Mar 2026 20:41:05 +0100 Subject: [PATCH] feat: view previous order details in account tab feat: view previous order details in account tab in the account tab you could view previous orders and how much they cost but not the entire order and what got ordered and so on. this is not added where you can go and click on a previous order and get all the info about that order --- TODO.md | 86 ++++------------ pkg/tui/account.go | 230 +++++++++++++++++++++++++++++++++++++++++-- pkg/tui/footer.go | 50 +++++++--- pkg/tui/model.go | 74 +++++++++++++- pkg/tui/views.go | 149 ++++++++++++++-------------- terminal-shop-source | 2 +- 6 files changed, 423 insertions(+), 168 deletions(-) diff --git a/TODO.md b/TODO.md index d9b4869..af65f7a 100644 --- a/TODO.md +++ b/TODO.md @@ -1,72 +1,20 @@ -# Current Work - Quick TODO +# TODO -> This is your working scratchpad. Update this frequently as you work on features. -> For the complete roadmap, see README.md +## In Progress +- [ ] Order detail view — press Enter on an order in account view to see full details -## 🔥 Currently Working On +## Up Next +- [ ] User profile editing — set name/email from account view +- [ ] Cart persistence across sessions — sync cart to server, fetch on startup +- [ ] Token refresh — auto-refresh JWT before expiry +- [ ] Address update — PUT /api/v1/addresses/:id endpoint +- [ ] Loading spinner — replace plain text with animated bubbles/spinner +- [ ] Help screen — press ? to show keybindings -**Phase**: Phase 1 - Enhanced UI & Navigation -**Task**: Setting up project documentation -**Status**: ✅ Complete - -### Completed Today -- [x] Created comprehensive README.md with full roadmap -- [x] Created MODELS.md with technical specs -- [x] Created CONTRIBUTING.md for Claude Code workflow -- [x] Set up .env.example -- [x] Created .gitignore - -## 📋 Next Up - -### Immediate Next Task -**Phase 1.1 - Add Account Page View** - -Steps: -1. [ ] Add `viewingAccount bool` to model struct -2. [ ] Add "a" keybind to switch to account view -3. [ ] Create `buildAccountView()` function -4. [ ] Add mock user data (username, email) -5. [ ] Update header to include account tab -6. [ ] Update footer with account keybind -7. [ ] Test navigation between shop, cart, and account - -### After That -**Phase 1.2 - Implement Breadcrumb Navigation** -- Reference: `terminal-shop-source/packages/go/pkg/tui/breadcrumbs.go` - -## 🐛 Known Issues - -_None currently_ - -## 💡 Ideas / Notes - -- Consider adding a loading spinner for future API calls -- Might want to add a help screen (press 'h' or '?') -- Color scheme could be customizable in future - -## 📝 Questions / Decisions Needed - -_None currently_ - ---- - -## Quick Commands Reference - -```bash -# Run the app -go run server.go model.go - -# Clean build artifacts -rm terminalshop server_bin - -# View git status -git status - -# Check TODO items -cat TODO.md -``` - ---- - -**Last Updated**: 2026-02-12 -**By**: Setup session with Claude Code +## Backlog +- [ ] Shipping rate calculation — actual costs from Shippo/Bring, not always $0 +- [ ] Product filtering/pagination — filter by roast type, paginate results +- [ ] Single order detail API endpoint — GET /api/v1/orders/:id +- [ ] Price model consistency — Coffee.Price float64 vs Order.Total int cents +- [ ] Test coverage — auth, cards, orders, addresses, bring/shippo, validators +- [ ] Update stale documentation — README, CLAUDE.md phase references diff --git a/pkg/tui/account.go b/pkg/tui/account.go index bb66302..29854dc 100644 --- a/pkg/tui/account.go +++ b/pkg/tui/account.go @@ -2,7 +2,6 @@ package tui import ( "fmt" - "strings" "github.com/charmbracelet/lipgloss" @@ -10,7 +9,7 @@ import ( "terminalShop/pkg/models" ) -func (m Model) BuildAccountView() string { +func (m Model) BuildAccountView(availableHeight int) string { leftWidth := 18 rightWidth := m.widthContent - leftWidth - 2 // 2 for gap @@ -63,17 +62,29 @@ func (m Model) BuildAccountView() string { detailView = titleStyle.Render("Order History") + "\n\n" + contentStyle.Render("Loading orders...") } else if len(m.Orders) == 0 { detailView = titleStyle.Render("Order History") + "\n\n" + contentStyle.Render("No orders yet.") + } else if m.OrderViewState == 2 { + // State 2: full detail for the selected order + detailView = m.buildOrderDetailView(m.Orders[m.OrderCursor], detailContentWidth) } else { + // State 0 (preview) or State 1 (browsing with cursor) + boxWidth := detailContentWidth - 4 + boxPadding := 0 + if m.heightContainer >= 25 { + boxPadding = 1 + } + lines := titleStyle.Render("Order History") + "\n\n" - for _, order := range m.Orders { - status := strings.ToUpper(string(order.Status)) - date := order.CreatedAt.Format("Jan 02 2006") - total := fmt.Sprintf("$%.2f", float64(order.Total)/100.0) - lines += contentStyle.Render(fmt.Sprintf("Order #%d %s %s %s", order.ID, date, total, status)) + "\n" - for _, item := range order.Items { - lines += contentStyle.Render(fmt.Sprintf(" %dx %s - $%.2f", item.Quantity, item.Name, float64(item.Price)/100.0)) + "\n" + for i, order := range m.Orders { + isSelected := m.OrderViewState == 1 && i == m.OrderCursor + lines += m.buildOrderCard(order, boxWidth, isSelected, boxPadding, detailContentWidth) + if i < len(m.Orders)-1 { + lines += "\n" } - lines += "\n" + } + + if m.OrderViewState == 0 { + hintStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#666666")) + lines += "\n" + hintStyle.Render("enter: browse orders") } detailView = lines } @@ -86,6 +97,45 @@ func (m Model) BuildAccountView() string { } } + // Apply internal scrolling to just the detail view + if m.OrderViewState >= 1 { + 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(). Width(rightWidth) @@ -105,3 +155,163 @@ func (m Model) BuildAccountView() string { detailContainer.Render(detailView), ) } + +// buildOrderCard renders a single order as a bordered box for the order list. +// Shows order number, date, status, total, and the first 2 items as a preview. +func (m Model) buildOrderCard(order models.Order, boxWidth int, isSelected bool, boxPadding int, containerWidth int) string { + // Order header line: "Order #123" left, "Jan 02 2026 PAID" right + nameStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#FFFFFF")) + + dimStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#666666")) + + statusStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#4682B4")). + Bold(true) + + orderLabel := nameStyle.Render(fmt.Sprintf("Order #%d", order.ID)) + total := nameStyle.Render(fmt.Sprintf("$%.2f", float64(order.Total)/100.0)) + date := dimStyle.Render(order.CreatedAt.Format("Jan 02 2006")) + status := statusStyle.Render(strings.ToUpper(string(order.Status))) + + rightSide := date + " " + status + " " + total + rightWidth := lipgloss.Width(rightSide) + leftWidth := lipgloss.Width(orderLabel) + spacing := boxWidth - leftWidth - rightWidth - 4 // 4 for padding + if spacing < 1 { + spacing = 1 + } + + line1 := orderLabel + lipgloss.NewStyle().Width(spacing).Render("") + rightSide + + // Item preview lines (show first 2 items) + var lines []string + lines = append(lines, line1) + + previewCount := 2 + if len(order.Items) < previewCount { + previewCount = len(order.Items) + } + for i := 0; i < previewCount; i++ { + item := order.Items[i] + itemLine := dimStyle.Render(fmt.Sprintf("%dx %s - $%.2f", item.Quantity, item.Name, float64(item.Price)/100.0)) + lines = append(lines, itemLine) + } + if len(order.Items) > 2 { + extra := len(order.Items) - 2 + moreText := "item" + if extra > 1 { + moreText = "items" + } + lines = append(lines, dimStyle.Render(fmt.Sprintf("+ %d more %s", extra, moreText))) + } + + boxContent := strings.Join(lines, "\n") + + // Create bordered box — highlight if selected + var itemBox lipgloss.Style + if isSelected { + itemBox = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder(), true). + BorderForeground(lipgloss.Color("#FFFFFF")). + Padding(boxPadding, 2). + Width(boxWidth) + } else { + itemBox = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#666666")). + Padding(boxPadding, 2). + Width(boxWidth) + } + + centered := lipgloss.NewStyle(). + Width(containerWidth). + Align(lipgloss.Center) + + return centered.Render(itemBox.Render(boxContent)) +} + +// buildOrderDetailView renders the full detail view for a single order. +func (m Model) buildOrderDetailView(order models.Order, width int) string { + titleStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#FFFFFF")). + MarginBottom(1) + + labelStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#4682B4")). + Bold(true) + + valueStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#AAAAAA")) + + dimStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#666666")) + + var b strings.Builder + + // Header + b.WriteString(titleStyle.Render(fmt.Sprintf("Order #%d", order.ID))) + b.WriteString("\n\n") + + // Status and date + b.WriteString(labelStyle.Render("Status: ")) + b.WriteString(valueStyle.Render(strings.ToUpper(string(order.Status)))) + b.WriteString("\n") + b.WriteString(labelStyle.Render("Date: ")) + b.WriteString(valueStyle.Render(order.CreatedAt.Format("Jan 02 2006 3:04 PM"))) + b.WriteString("\n\n") + + // Items + b.WriteString(labelStyle.Render("Items")) + b.WriteString("\n") + b.WriteString(dimStyle.Render(strings.Repeat("-", 40))) + b.WriteString("\n") + for _, item := range order.Items { + price := fmt.Sprintf("$%.2f", float64(item.Price)/100.0) + line := fmt.Sprintf(" %dx %-20s %s", item.Quantity, item.Name, price) + b.WriteString(valueStyle.Render(line)) + b.WriteString("\n") + } + b.WriteString(dimStyle.Render(strings.Repeat("-", 40))) + b.WriteString("\n") + + // Totals + subtotal := fmt.Sprintf("$%.2f", float64(order.Subtotal)/100.0) + shipping := fmt.Sprintf("$%.2f", float64(order.ShippingCost)/100.0) + total := fmt.Sprintf("$%.2f", float64(order.Total)/100.0) + b.WriteString(valueStyle.Render(fmt.Sprintf(" %-22s %s", "Subtotal", subtotal))) + b.WriteString("\n") + b.WriteString(valueStyle.Render(fmt.Sprintf(" %-22s %s", "Shipping", shipping))) + b.WriteString("\n") + b.WriteString(labelStyle.Render(fmt.Sprintf(" %-22s %s", "Total", total))) + b.WriteString("\n\n") + + // Shipping address + b.WriteString(labelStyle.Render("Ship To")) + b.WriteString("\n") + b.WriteString(valueStyle.Render(" " + order.ShippingName)) + b.WriteString("\n") + b.WriteString(valueStyle.Render(" " + order.ShippingStreet)) + b.WriteString("\n") + if order.ShippingStreet2 != "" { + b.WriteString(valueStyle.Render(" " + order.ShippingStreet2)) + b.WriteString("\n") + } + cityLine := order.ShippingCity + if order.ShippingState != "" { + cityLine += ", " + order.ShippingState + } + cityLine += " " + order.ShippingZip + b.WriteString(valueStyle.Render(" " + cityLine)) + b.WriteString("\n") + b.WriteString(valueStyle.Render(" " + order.ShippingCountry)) + b.WriteString("\n\n") + + // Footer hint + b.WriteString(dimStyle.Render("esc: back to orders")) + + return b.String() +} diff --git a/pkg/tui/footer.go b/pkg/tui/footer.go index 5cf5df0..c9fb2f9 100644 --- a/pkg/tui/footer.go +++ b/pkg/tui/footer.go @@ -16,7 +16,7 @@ func (m Model) BuildFooter() string { descStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("#666666")) - // On small terminals, only show the menu hint + // On small terminals, only show the menu hint if m.size == small { footerText := keybindStyle.Render("m") + " " + descStyle.Render("menu") return lipgloss.NewStyle().Align(lipgloss.Center).Width(m.widthContainer).Render(footerText) @@ -24,16 +24,44 @@ func (m Model) BuildFooter() string { var footerText string if m.ViewingAccount { - footerText = fmt.Sprintf("%s %s %s %s %s %s %s %s", - keybindStyle.Render("j/k"), - descStyle.Render("menu"), - keybindStyle.Render("s"), - descStyle.Render("shop"), - keybindStyle.Render("c"), - descStyle.Render("cart"), - keybindStyle.Render("q"), - descStyle.Render("quit"), - ) + switch m.OrderViewState { + case 2: + // Viewing single order detail + 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"), + ) + case 1: + // Browsing order list + footerText = fmt.Sprintf("%s %s %s %s %s %s %s %s", + keybindStyle.Render("j/k"), + descStyle.Render("orders"), + keybindStyle.Render("enter"), + descStyle.Render("details"), + keybindStyle.Render("esc"), + descStyle.Render("back"), + keybindStyle.Render("q"), + descStyle.Render("quit"), + ) + default: + // Account tabs + footerText = fmt.Sprintf("%s %s %s %s %s %s %s %s %s %s", + keybindStyle.Render("j/k"), + descStyle.Render("navigate"), + keybindStyle.Render("enter"), + descStyle.Render("select"), + keybindStyle.Render("s"), + descStyle.Render("shop"), + keybindStyle.Render("c"), + descStyle.Render("cart"), + keybindStyle.Render("q"), + descStyle.Render("quit"), + ) + } } else if m.ViewingCart && m.CheckoutStep == 3 { // Confirmation screen footerText = fmt.Sprintf("%s %s %s %s", diff --git a/pkg/tui/model.go b/pkg/tui/model.go index 7b370ba..d43a883 100644 --- a/pkg/tui/model.go +++ b/pkg/tui/model.go @@ -90,8 +90,10 @@ type Model struct { StripeKey string // Stripe publishable key for client-side tokenization // Order history - Orders []models.Order - OrdersLoaded bool + Orders []models.Order + OrdersLoaded bool + OrderCursor int // which order is selected in the list + OrderViewState int // 0=preview, 1=browsing list, 2=viewing detail } // updateLayout recalculates all layout variables from the raw viewport dimensions. @@ -180,6 +182,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.ShippingForm = nil m.PaymentForm = nil m.ScrollOffset = 0 + m.OrderViewState = 0 case "a": m.ShowingMenu = false m.ViewingCart = false @@ -188,6 +191,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.ShippingForm = nil m.PaymentForm = nil m.ScrollOffset = 0 + m.OrderViewState = 0 if !m.OrdersLoaded { return m, m.fetchOrdersCmd() } @@ -199,6 +203,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.ShippingForm = nil m.PaymentForm = nil m.ScrollOffset = 0 + m.OrderViewState = 0 case "esc": m.ShowingMenu = false case "q", "ctrl+c": @@ -535,6 +540,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.ShippingForm = nil m.PaymentForm = nil m.ScrollOffset = 0 + m.OrderViewState = 0 case "c": m.ViewingCart = true @@ -543,6 +549,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.ShippingForm = nil m.PaymentForm = nil m.ScrollOffset = 0 + m.OrderViewState = 0 case "a": m.ViewingCart = false @@ -551,6 +558,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.ShippingForm = nil m.PaymentForm = nil m.ScrollOffset = 0 + m.OrderViewState = 0 if !m.OrdersLoaded { return m, m.fetchOrdersCmd() } @@ -562,6 +570,19 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 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.OrderViewState == 0 { + m.OrderViewState = 1 + m.OrderCursor = 0 + m.ScrollOffset = 0 + return m, nil + } else if m.OrderViewState == 1 { + m.OrderViewState = 2 + m.ScrollOffset = 0 + return m, nil + } + } // Proceed to checkout from cart if m.ViewingCart && m.CheckoutStep == 0 && len(m.Cart) > 0 { m.CheckoutStep = 1 @@ -569,6 +590,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case "esc": + // Navigate back through order view states + if m.ViewingAccount && 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 @@ -592,7 +619,21 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "up", "k": if m.ViewingAccount { - if m.AccountCursor > 0 { + if m.OrderViewState == 1 && m.OrderCursor > 0 { + // Navigate up within order list + m.OrderCursor-- + // Auto-scroll to keep cursor visible + cardHeight := 5 // 2 border + 3 content (no padding) + if m.heightContainer >= 25 { + cardHeight = 7 // 2 border + 2 padding + 3 content + } + titleOffset := 3 // "Order History" + blank lines + targetTop := titleOffset + m.OrderCursor*cardHeight + if targetTop < m.ScrollOffset { + m.ScrollOffset = targetTop + } + } else if m.OrderViewState == 0 && m.AccountCursor > 0 { + // Navigate up within account tabs m.AccountCursor-- } } else if !m.ViewingCart && !m.ViewingAccount && m.Cursor > 0 { @@ -609,7 +650,30 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "down", "j": if m.ViewingAccount { - if m.AccountCursor < len(models.AccountMenuItems)-1 { + if m.OrderViewState == 1 && m.OrderCursor < len(m.Orders)-1 { + // Navigate down within order list + m.OrderCursor++ + // Auto-scroll to keep cursor visible + cardHeight := 5 // 2 border + 3 content (no padding) + if m.heightContainer >= 25 { + cardHeight = 7 // 2 border + 2 padding + 3 content + } + titleOffset := 3 + // Account view: header + footer + margins + buffer = 7, no breadcrumbs + viewportHeight := m.heightContainer - 7 + if m.size < large { + // Stacked mode: subtract left panel height + viewportHeight -= len(models.AccountMenuItems) + 1 + } + if viewportHeight < 5 { + viewportHeight = 5 + } + targetBottom := titleOffset + (m.OrderCursor+1)*cardHeight + if targetBottom > m.ScrollOffset+viewportHeight { + m.ScrollOffset = targetBottom - viewportHeight + } + } else if m.OrderViewState == 0 && m.AccountCursor < len(models.AccountMenuItems)-1 { + // Navigate down within account tabs m.AccountCursor++ } } else if !m.ViewingCart && !m.ViewingAccount && m.Cursor < len(m.Coffees)-1 { @@ -1012,6 +1076,8 @@ func NewModel(username string) Model { Cart: make(map[uint]*models.CartItem), CartCursor: 0, AccountCursor: 0, + OrderCursor: 0, + OrderViewState: 0, ViewingCart: false, ViewingAccount: false, Loading: true, diff --git a/pkg/tui/views.go b/pkg/tui/views.go index 30718ae..4ef266d 100644 --- a/pkg/tui/views.go +++ b/pkg/tui/views.go @@ -43,60 +43,6 @@ func (m Model) View() string { // Build breadcrumbs breadcrumbs := m.BuildBreadcrumbs() - // Build main content based on view - var content string - if m.Loading { - 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() - } 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.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() - } 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.PaymentForm != nil { - content = m.RenderPaymentForm(m.PaymentForm) - } else if m.ViewingCart && m.CheckoutStep == 3 { - content = m.RenderConfirmation() - } else if m.ViewingCart { - content = m.BuildCartView() - } else { - content = m.BuildShopView() - } - // Calculate available content height within the container headerHeight := lipgloss.Height(header) breadcrumbsHeight := 0 @@ -117,30 +63,87 @@ 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) + 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.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.PaymentForm != nil { + content = m.RenderPaymentForm(m.PaymentForm) + } else if m.ViewingCart && m.CheckoutStep == 3 { + content = m.RenderConfirmation() + } else if m.ViewingCart { + content = m.BuildCartView() + } else { + content = m.BuildShopView() + } + // Split content into lines and handle scrolling contentLines := strings.Split(content, "\n") totalLines := len(contentLines) - // Ensure scroll offset is within bounds - if m.ScrollOffset < 0 { - m.ScrollOffset = 0 - } - maxScroll := totalLines - availableContentHeight - if maxScroll < 0 { - maxScroll = 0 - } - if m.ScrollOffset > maxScroll { - m.ScrollOffset = maxScroll - } - - // Apply scroll offset and truncate to fit available height - if totalLines > availableContentHeight { - start := m.ScrollOffset - end := start + availableContentHeight - if end > totalLines { - end = totalLines + // Skip global scroll when account view handles its own scrolling + if !(m.ViewingAccount && m.OrderViewState >= 1) { + // Ensure scroll offset is without bounds + if m.ScrollOffset < 0 { + m.ScrollOffset = 0 + } + + maxScroll := totalLines - availableContentHeight + if maxScroll < 0 { + maxScroll = 0 + } + if m.ScrollOffset > maxScroll { + m.ScrollOffset = maxScroll + } + + if totalLines > availableContentHeight { + start := m.ScrollOffset + end := start + availableContentHeight + if end > totalLines { + end = totalLines + } + contentLines = contentLines[start:end] } - contentLines = contentLines[start:end] } content = strings.Join(contentLines, "\n") diff --git a/terminal-shop-source b/terminal-shop-source index caa24e7..697730e 160000 --- a/terminal-shop-source +++ b/terminal-shop-source @@ -1 +1 @@ -Subproject commit caa24e7ffaaf6fa49289ebaf08a1b8c116fd6b0c +Subproject commit 697730e73cba0ef0bab943bbd75e5bcd64f5f5d3