Files
terminalShop-old/pkg/tui/shop.go
marcusDenslow 18c7a93e0b feat: implement server-side cart persistance with Stripe PaymentIntents
feat: implement server-side cart persistence with Stripe PaymentIntents

Replaced the client-only in-mem cart with a database-backed cart system

- Add Cart and CartItem GORM models (two-table design)
- Add 6 cart API endpoints (get, set item, set address, set card, clear,
conver)
- Switch from Stripe Charge API to PaymentIntents with confirm:true
- Add 9 API client methods for cart and card operations
- Refactor TUI cart from map[int] (slice index) to map[uint] (CoffeeID)
- Replace single-shot checkout with saveCardAndConvert multi-step flow
- Add CheckingOut state with "submitting order..." processing indicator
- Fix Stripe tokenization to use secret key for server-side SSH apps
- Remove admin product CRUD endpoints and old checkout handler
2026-02-28 15:01:06 +01:00

115 lines
3.0 KiB
Go

package tui
import (
"fmt"
"github.com/charmbracelet/lipgloss"
)
func (m Model) BuildShopView() string {
leftWidth := 18
rightWidth := m.widthContent - leftWidth - 2 // 2 for gap
// On small/medium, items are centered and span the full container width.
// On large, items are left-aligned in the narrow left column.
itemWidth := leftWidth - 2
itemAlign := lipgloss.Left
if m.size < large {
itemWidth = m.widthContainer
itemAlign = lipgloss.Center
}
// Build the left panel (coffee list)
leftPanel := ""
for i, coffee := range m.Coffees {
if m.Cursor == i {
style := lipgloss.NewStyle().
Background(lipgloss.Color(coffee.Color)).
Foreground(lipgloss.Color("#FFFFFF")).
Padding(0, 1).
Width(itemWidth).
Align(itemAlign)
leftPanel += style.Render(coffee.Name) + "\n"
} else {
style := lipgloss.NewStyle().
Foreground(lipgloss.Color("#AAAAAA")).
Padding(0, 1).
Width(itemWidth).
Align(itemAlign)
leftPanel += style.Render(coffee.Name) + "\n"
}
}
var leftContainer lipgloss.Style
if m.size < large {
leftContainer = lipgloss.NewStyle().Width(m.widthContainer)
} else {
leftContainer = lipgloss.NewStyle().Width(leftWidth)
}
// Build the detail view (right panel)
detailView := ""
if m.Cursor >= 0 && m.Cursor < len(m.Coffees) {
selectedCoffee := m.Coffees[m.Cursor]
nameStyle := lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#FFFFFF"))
infoStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#AAAAAA"))
priceStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color(selectedCoffee.Color)).
Bold(true)
// On small terminals, use full content width for description
descWidth := rightWidth - 2
if m.size < large {
descWidth = m.widthContent - 2
}
descStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFFF")).
Width(descWidth)
quantityStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFFF")).
Bold(true)
// Get current quantity (look up by CoffeeID)
quantity := 0
if item, exists := m.Cart[selectedCoffee.ID]; exists {
quantity = item.Quantity
}
detailView += nameStyle.Render(selectedCoffee.Name) + "\n"
detailView += infoStyle.Render(fmt.Sprintf("%s | %doz | %s",
selectedCoffee.RoastType,
selectedCoffee.Ounces,
selectedCoffee.BeanType)) + "\n\n"
detailView += priceStyle.Render(fmt.Sprintf("$%.2f", selectedCoffee.Price)) + "\n\n"
detailView += descStyle.Render(selectedCoffee.Description) + "\n\n"
detailView += quantityStyle.Render(fmt.Sprintf("- %d +", quantity)) + "\n"
}
detailContainer := lipgloss.NewStyle().
Width(rightWidth)
// Responsive layout: side-by-side on large, stacked on small/medium
if m.size < large {
return lipgloss.JoinVertical(
lipgloss.Left,
leftContainer.Render(leftPanel),
detailContainer.Width(m.widthContainer).Render(detailView),
)
}
return lipgloss.JoinHorizontal(
lipgloss.Top,
leftContainer.Render(leftPanel),
" ",
detailContainer.Render(detailView),
)
}