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