mirror of
https://github.com/marcusDenslow/terminalShop.git
synced 2026-10-25 02:52:20 +02:00
fix: store coffee prices as int cents to prevent rounding errors
prices stored as float64 could lead to rounding errors when converting to cents for stripe (19.99 * 100 = 1998.99... truncating to 1998). canged Coffee.Price to int, storing cents directly. Updated the seed data, fallback products, API handlers, and all TUI display code to be in sync with this change.
This commit is contained in:
@@ -50,14 +50,13 @@ func cartResponse(cart *models.Cart) map[string]interface{} {
|
||||
subtotal := 0
|
||||
itemsData := make([]map[string]interface{}, 0, len(items))
|
||||
for _, item := range items {
|
||||
priceInCents := int(item.Coffee.Price * 100)
|
||||
lineTotal := priceInCents * item.Quantity
|
||||
lineTotal := item.Coffee.Price * item.Quantity
|
||||
subtotal += lineTotal
|
||||
itemsData = append(itemsData, map[string]interface{}{
|
||||
"id": item.ID,
|
||||
"coffee_id": item.CoffeeID,
|
||||
"quantity": item.Quantity,
|
||||
"subtotal": lineTotal,
|
||||
"subtotal": lineTotal, // already in cents
|
||||
"coffee": item.Coffee,
|
||||
})
|
||||
}
|
||||
@@ -296,13 +295,12 @@ func (h *CartHandler) ConvertCart(w http.ResponseWriter, r *http.Request) {
|
||||
var subtotal int
|
||||
var orderItems []models.OrderItem
|
||||
for _, item := range items {
|
||||
priceInCents := int(item.Coffee.Price * 100)
|
||||
lineTotal := priceInCents * item.Quantity
|
||||
lineTotal := item.Coffee.Price * item.Quantity
|
||||
subtotal += lineTotal
|
||||
orderItems = append(orderItems, models.OrderItem{
|
||||
CoffeeID: item.CoffeeID,
|
||||
Name: item.Coffee.Name,
|
||||
Price: priceInCents,
|
||||
Price: item.Coffee.Price,
|
||||
Quantity: item.Quantity,
|
||||
})
|
||||
}
|
||||
|
||||
710
model.go.old
710
model.go.old
@@ -1,710 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
|
||||
// Account menu items struct.
|
||||
var accountMenuItems = []string {
|
||||
"order history",
|
||||
"faq",
|
||||
"about",
|
||||
}
|
||||
|
||||
// Coffee represents a coffee product
|
||||
type Coffee struct {
|
||||
Name string
|
||||
RoastType string
|
||||
Ounces int
|
||||
BeanType string
|
||||
Price float64
|
||||
Color string // Hex color for background
|
||||
Description string
|
||||
}
|
||||
|
||||
// CartItem represents an item in the cart
|
||||
type CartItem struct {
|
||||
Coffee Coffee
|
||||
Quantity int
|
||||
}
|
||||
|
||||
type model struct {
|
||||
username string
|
||||
coffees []Coffee
|
||||
cursor int
|
||||
cart map[int]*CartItem // maps coffee index to cart item
|
||||
cartCursor int // cursor position in cart view
|
||||
accountCursor int
|
||||
accountPage string
|
||||
windowWidth int
|
||||
windowHeight int
|
||||
viewingCart bool // true when viewing cart details
|
||||
viewingAccount bool
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.windowWidth = msg.Width
|
||||
m.windowHeight = msg.Height
|
||||
return m, nil
|
||||
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
|
||||
case "s":
|
||||
m.viewingCart = false
|
||||
m.viewingAccount = false
|
||||
m.accountPage = ""
|
||||
|
||||
case "c":
|
||||
m.viewingCart = true
|
||||
m.viewingAccount = false
|
||||
m.accountPage = ""
|
||||
|
||||
case "a":
|
||||
m.viewingCart = false
|
||||
m.viewingAccount = true
|
||||
|
||||
case "up", "k":
|
||||
if m.viewingAccount && m.accountPage == "" {
|
||||
if m.accountCursor > 0 {
|
||||
m.accountCursor--
|
||||
}
|
||||
} else if !m.viewingCart && !m.viewingAccount && m.cursor > 0 {
|
||||
m.cursor--
|
||||
} else if m.viewingCart && m.cartCursor > 0 {
|
||||
m.cartCursor--
|
||||
}
|
||||
|
||||
case "down", "j":
|
||||
if m.viewingAccount && m.accountPage == "" {
|
||||
if m.accountCursor < len(accountMenuItems)-1 {
|
||||
m.accountCursor++
|
||||
}
|
||||
} else if !m.viewingCart && !m.viewingAccount && m.cursor < len(m.coffees)-1 {
|
||||
m.cursor++
|
||||
} else if m.viewingCart && m.cartCursor < len(m.cart)-1 {
|
||||
m.cartCursor++
|
||||
}
|
||||
|
||||
case "+", "=":
|
||||
if !m.viewingCart {
|
||||
// Increment quantity in shop view
|
||||
if item, exists := m.cart[m.cursor]; exists {
|
||||
item.Quantity++
|
||||
} else {
|
||||
m.cart[m.cursor] = &CartItem{
|
||||
Coffee: m.coffees[m.cursor],
|
||||
Quantity: 1,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Increment quantity in cart view
|
||||
cartItems := m.getCartItemsSlice()
|
||||
if m.cartCursor >= 0 && m.cartCursor < len(cartItems) {
|
||||
cartItems[m.cartCursor].Quantity++
|
||||
}
|
||||
}
|
||||
|
||||
case "enter":
|
||||
if m.viewingAccount && m.accountPage == "" {
|
||||
switch accountMenuItems[m.accountCursor]{
|
||||
case "order history":
|
||||
m.accountPage = "orders"
|
||||
case "faq":
|
||||
m.accountPage = "faq"
|
||||
case "about":
|
||||
m.accountPage = "about"
|
||||
}
|
||||
}
|
||||
|
||||
case "esc":
|
||||
if m.viewingAccount && m.accountPage != "" {
|
||||
m.accountPage = ""
|
||||
}
|
||||
|
||||
case "-", "_":
|
||||
if !m.viewingCart {
|
||||
// Decrement quantity in shop view
|
||||
if item, exists := m.cart[m.cursor]; exists {
|
||||
item.Quantity--
|
||||
if item.Quantity <= 0 {
|
||||
delete(m.cart, m.cursor)
|
||||
// Reset cart cursor if we deleted the last item
|
||||
if len(m.cart) == 0 {
|
||||
m.cartCursor = 0
|
||||
} else if m.cartCursor >= len(m.cart) {
|
||||
m.cartCursor = len(m.cart) - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Decrement quantity in cart view
|
||||
cartItems := m.getCartItemsSlice()
|
||||
if m.cartCursor >= 0 && m.cartCursor < len(cartItems) {
|
||||
cartItems[m.cartCursor].Quantity--
|
||||
if cartItems[m.cartCursor].Quantity <= 0 {
|
||||
// Find and delete this item from the cart
|
||||
for idx, item := range m.cart {
|
||||
if item == cartItems[m.cartCursor] {
|
||||
delete(m.cart, idx)
|
||||
break
|
||||
}
|
||||
}
|
||||
// Reset cart cursor
|
||||
if len(m.cart) == 0 {
|
||||
m.cartCursor = 0
|
||||
} else if m.cartCursor >= len(m.cart) {
|
||||
m.cartCursor = len(m.cart) - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
if m.windowWidth == 0 {
|
||||
m.windowWidth = 120
|
||||
m.windowHeight = 30
|
||||
}
|
||||
|
||||
// Build header with cart tab
|
||||
header := m.buildHeader()
|
||||
|
||||
// Build main content based on view
|
||||
var content string
|
||||
if m.viewingAccount {
|
||||
content = m.buildAccountView()
|
||||
} else if m.viewingCart {
|
||||
content = m.buildCartView()
|
||||
} else {
|
||||
content = m.buildShopView()
|
||||
}
|
||||
|
||||
// Add vertical spacing around content
|
||||
contentWithPadding := lipgloss.NewStyle().
|
||||
MarginTop(1).
|
||||
MarginBottom(4).
|
||||
Render(content)
|
||||
|
||||
// Build footer
|
||||
footer := m.buildFooter()
|
||||
|
||||
// Combine all app content
|
||||
appContent := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
header,
|
||||
contentWithPadding,
|
||||
footer,
|
||||
)
|
||||
|
||||
// Calculate vertical centering
|
||||
appHeight := lipgloss.Height(appContent)
|
||||
verticalPadding := 2 // Minimum padding from top and bottom
|
||||
availableSpace := m.windowHeight - appHeight - (verticalPadding * 2)
|
||||
|
||||
if availableSpace < 0 {
|
||||
availableSpace = 0
|
||||
verticalPadding = 0
|
||||
}
|
||||
|
||||
// Split remaining space between top and bottom
|
||||
topPadding := verticalPadding + (availableSpace / 2)
|
||||
|
||||
// Create top spacing
|
||||
topSpacing := ""
|
||||
for i := 0; i < topPadding; i++ {
|
||||
topSpacing += "\n"
|
||||
}
|
||||
|
||||
return topSpacing + appContent
|
||||
}
|
||||
|
||||
func (m model) buildHeader() string {
|
||||
// Calculate cart total and item count
|
||||
total := 0.0
|
||||
itemCount := 0
|
||||
for _, item := range m.cart {
|
||||
total += float64(item.Quantity) * item.Coffee.Price
|
||||
itemCount += item.Quantity
|
||||
}
|
||||
|
||||
// Define colors
|
||||
whiteStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF"))
|
||||
grayStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#AAAAAA"))
|
||||
boldWhiteStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")).Bold(true)
|
||||
|
||||
// Build shop tab
|
||||
var shopKeybind, shopName string
|
||||
if !m.viewingCart {
|
||||
// Active: bold white keybind and name
|
||||
shopKeybind = boldWhiteStyle.Render("s")
|
||||
shopName = boldWhiteStyle.Render("shop")
|
||||
} else {
|
||||
// Inactive: bold gray keybind, gray name
|
||||
shopKeybind = lipgloss.NewStyle().Foreground(lipgloss.Color("#AAAAAA")).Bold(true).Render("s")
|
||||
shopName = grayStyle.Render("shop")
|
||||
}
|
||||
|
||||
// Build cart tab
|
||||
var cartKeybind, cartName, cartInfo string
|
||||
itemCountText := grayStyle.Render(fmt.Sprintf("[%d]", itemCount))
|
||||
|
||||
if m.viewingCart {
|
||||
// Active: bold white keybind and name
|
||||
cartKeybind = boldWhiteStyle.Render("c")
|
||||
cartName = boldWhiteStyle.Render("cart")
|
||||
cartInfo = fmt.Sprintf(" %s %s",
|
||||
boldWhiteStyle.Render(fmt.Sprintf("$%.2f", total)),
|
||||
itemCountText)
|
||||
} else {
|
||||
// Inactive: bold gray keybind, gray name
|
||||
boldGrayStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#AAAAAA")).Bold(true)
|
||||
boldWhitePriceStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")).Bold(true)
|
||||
cartKeybind = boldGrayStyle.Render("c")
|
||||
cartName = grayStyle.Render("cart")
|
||||
cartInfo = fmt.Sprintf(" %s %s",
|
||||
boldWhitePriceStyle.Render(fmt.Sprintf("$%.2f", total)),
|
||||
itemCountText)
|
||||
}
|
||||
|
||||
// Build Account Tabs
|
||||
var accountKeybind, accountName string
|
||||
if m.viewingAccount {
|
||||
accountKeybind = boldWhiteStyle.Render("a")
|
||||
accountName = boldWhiteStyle.Render("account")
|
||||
} else {
|
||||
accountKeybind = lipgloss.NewStyle().Foreground(lipgloss.Color("#AAAAAA")).Bold(true).Render("a")
|
||||
accountName = grayStyle.Render("account")
|
||||
}
|
||||
|
||||
// Build the tabs with separator only in the middle
|
||||
separator := whiteStyle.Render("|")
|
||||
tabsContent := fmt.Sprintf("%s %s %s %s %s%s %s %s %s",
|
||||
shopKeybind,
|
||||
shopName,
|
||||
separator,
|
||||
cartKeybind,
|
||||
cartName,
|
||||
cartInfo,
|
||||
separator,
|
||||
accountKeybind,
|
||||
accountName,
|
||||
)
|
||||
|
||||
// Add box with padding (no vertical padding, just horizontal)
|
||||
tabBox := lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
Padding(0, 2).
|
||||
MarginBottom(1)
|
||||
|
||||
// Center the whole thing
|
||||
centered := lipgloss.NewStyle().
|
||||
Width(m.windowWidth).
|
||||
Align(lipgloss.Center)
|
||||
|
||||
return centered.Render(tabBox.Render(tabsContent))
|
||||
}
|
||||
|
||||
func (m model) buildShopView() string {
|
||||
leftWidth := 18 // Narrower list
|
||||
leftMargin := 4 // Minimal left margin
|
||||
rightWidth := m.windowWidth - leftWidth - leftMargin - 4
|
||||
|
||||
// Build the left panel (coffee list)
|
||||
leftPanel := ""
|
||||
for i, coffee := range m.coffees {
|
||||
// Apply background color when selected, white text
|
||||
if m.cursor == i {
|
||||
style := lipgloss.NewStyle().
|
||||
Background(lipgloss.Color(coffee.Color)).
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Padding(0, 1).
|
||||
Width(leftWidth - 2).
|
||||
Align(lipgloss.Left)
|
||||
leftPanel += style.Render(coffee.Name) + "\n"
|
||||
} else {
|
||||
style := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#AAAAAA")).
|
||||
Padding(0, 1).
|
||||
Width(leftWidth - 2).
|
||||
Align(lipgloss.Left)
|
||||
leftPanel += style.Render(coffee.Name) + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
// Add left margin to the list
|
||||
leftContainer := lipgloss.NewStyle().
|
||||
Width(leftWidth).
|
||||
MarginLeft(leftMargin)
|
||||
|
||||
// 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)
|
||||
|
||||
descStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Width(rightWidth - 4)
|
||||
|
||||
quantityStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true)
|
||||
|
||||
// Get current quantity
|
||||
quantity := 0
|
||||
if item, exists := m.cart[m.cursor]; 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)
|
||||
|
||||
// Layout the panels side by side
|
||||
mainContent := lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
leftContainer.Render(leftPanel),
|
||||
detailContainer.Render(detailView),
|
||||
)
|
||||
|
||||
return mainContent
|
||||
}
|
||||
|
||||
func (m model) buildCartView() string {
|
||||
if len(m.cart) == 0 {
|
||||
emptyStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#666666")).
|
||||
Align(lipgloss.Center).
|
||||
Width(m.windowWidth).
|
||||
Padding(4, 0)
|
||||
return emptyStyle.Render("Your cart is empty\n\nPress s to go back to shop")
|
||||
}
|
||||
|
||||
cartItems := ""
|
||||
boxWidth := m.windowWidth - 20 // Leave some margin
|
||||
itemSlice := m.getCartItemsSlice()
|
||||
|
||||
for idx, item := range itemSlice {
|
||||
itemTotal := float64(item.Quantity) * item.Coffee.Price
|
||||
|
||||
// Left side: name and info
|
||||
nameStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
infoStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#AAAAAA"))
|
||||
|
||||
// Right side: quantity controls and price
|
||||
quantityStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
priceStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true)
|
||||
|
||||
// Build content with proper spacing
|
||||
nameText := nameStyle.Render(item.Coffee.Name)
|
||||
infoText := infoStyle.Render(fmt.Sprintf("%s | %doz | %s",
|
||||
item.Coffee.RoastType,
|
||||
item.Coffee.Ounces,
|
||||
item.Coffee.BeanType))
|
||||
quantityText := quantityStyle.Render(fmt.Sprintf("- %d +", item.Quantity))
|
||||
priceText := priceStyle.Render(fmt.Sprintf("$%.2f", itemTotal))
|
||||
|
||||
// Calculate spacing
|
||||
nameWidth := lipgloss.Width(nameText)
|
||||
quantityWidth := lipgloss.Width(quantityText)
|
||||
priceWidth := lipgloss.Width(priceText)
|
||||
spacing := boxWidth - nameWidth - quantityWidth - priceWidth - 8 // 8 for padding and gaps
|
||||
|
||||
if spacing < 1 {
|
||||
spacing = 1
|
||||
}
|
||||
|
||||
contentLine1 := nameText +
|
||||
lipgloss.NewStyle().Width(spacing).Render("") +
|
||||
quantityText + " " + priceText
|
||||
|
||||
contentLine2 := infoText
|
||||
|
||||
boxContent := contentLine1 + "\n" + contentLine2
|
||||
|
||||
// Create box for this item
|
||||
itemBox := lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
Padding(0, 2).
|
||||
Width(boxWidth).
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
// Center the box
|
||||
centered := lipgloss.NewStyle().
|
||||
Width(m.windowWidth).
|
||||
Align(lipgloss.Center)
|
||||
|
||||
cartItems += centered.Render(itemBox.Render(boxContent))
|
||||
|
||||
// Add spacing between items
|
||||
if idx < len(itemSlice)-1 {
|
||||
cartItems += "\n\n"
|
||||
}
|
||||
}
|
||||
|
||||
return cartItems
|
||||
}
|
||||
|
||||
func (m model) buildAccountView() string {
|
||||
// If we're on a subpage, show that content
|
||||
if m.accountPage != "" {
|
||||
return m.buildAccountSubpage()
|
||||
}
|
||||
|
||||
// Otherwise show the account menu
|
||||
leftWidth := 18
|
||||
leftMargin := 4
|
||||
|
||||
leftPanel := ""
|
||||
for i, item := range accountMenuItems {
|
||||
// Apply background color when selected, white text
|
||||
if m.accountCursor == i {
|
||||
style := lipgloss.NewStyle().
|
||||
Background(lipgloss.Color("#4682B4")).
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Padding(0, 1).
|
||||
Width(leftWidth - 2).
|
||||
Align(lipgloss.Left)
|
||||
leftPanel += style.Render(item) + "\n"
|
||||
} else {
|
||||
style := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#AAAAAA")).
|
||||
Padding(0, 1).
|
||||
Width(leftWidth - 2).
|
||||
Align(lipgloss.Left)
|
||||
leftPanel += style.Render(item) + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
leftContainer := lipgloss.NewStyle().
|
||||
Width(leftWidth).
|
||||
MarginLeft(leftMargin)
|
||||
|
||||
return leftContainer.Render(leftPanel)
|
||||
}
|
||||
|
||||
func (m model) buildAccountSubpage() string {
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
MarginBottom(2)
|
||||
|
||||
contentStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#AAAAAA")).
|
||||
Width(m.windowWidth - 20).
|
||||
MarginLeft(4)
|
||||
|
||||
var content string
|
||||
switch m.accountPage {
|
||||
case "orders":
|
||||
content = titleStyle.Render("Order History") + "\n\n" +
|
||||
contentStyle.Render("No orders yet. Your order history will appear here once you make a purchase.")
|
||||
case "faq":
|
||||
content = titleStyle.Render("FAQ") + "\n\n" +
|
||||
contentStyle.Render("Q: How do I place an order?\nA: Browse products with j/k, adjust quantity with +/-, and checkout via the cart.\n\nQ: What payment methods do you accept?\nA: We accept all major credit cards via Stripe.\n\nPress ESC to go back.")
|
||||
case "about":
|
||||
content = titleStyle.Render("About Terminal Coffee Shop") + "\n\n" +
|
||||
contentStyle.Render("A terminal-based coffee ordering experience.\nBuilt with Go and Bubbletea.\n\nPress ESC to go back.")
|
||||
default:
|
||||
content = "Unknown page"
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
// getCartItemsSlice converts the cart map to a sorted slice for consistent iteration
|
||||
func (m model) getCartItemsSlice() []*CartItem {
|
||||
items := make([]*CartItem, 0, len(m.cart))
|
||||
for _, item := range m.cart {
|
||||
items = append(items, item)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func (m model) buildFooter() string {
|
||||
// Style for keybinds (bold)
|
||||
keybindStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#666666")).
|
||||
Bold(true)
|
||||
|
||||
// Style for descriptions (normal)
|
||||
descStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#666666"))
|
||||
|
||||
var footerText string
|
||||
if m.viewingAccount {
|
||||
if m.accountPage != "" {
|
||||
// In a subpage
|
||||
footerText = fmt.Sprintf("%s %s %s %s %s %s %s %s",
|
||||
keybindStyle.Render("esc"),
|
||||
descStyle.Render("back"),
|
||||
keybindStyle.Render("s"),
|
||||
descStyle.Render("shop"),
|
||||
keybindStyle.Render("c"),
|
||||
descStyle.Render("cart"),
|
||||
keybindStyle.Render("q"),
|
||||
descStyle.Render("quit"),
|
||||
)
|
||||
} else {
|
||||
// In account menu
|
||||
footerText = fmt.Sprintf("%s %s %s %s %s %s %s %s %s %s",
|
||||
keybindStyle.Render("j/k"),
|
||||
descStyle.Render("menu"),
|
||||
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 {
|
||||
footerText = fmt.Sprintf("%s %s %s %s %s %s %s %s %s %s",
|
||||
keybindStyle.Render("j/k"),
|
||||
descStyle.Render("items"),
|
||||
keybindStyle.Render("+/-"),
|
||||
descStyle.Render("qty"),
|
||||
keybindStyle.Render("s"),
|
||||
descStyle.Render("shop"),
|
||||
keybindStyle.Render("a"),
|
||||
descStyle.Render("account"),
|
||||
keybindStyle.Render("q"),
|
||||
descStyle.Render("quit"),
|
||||
)
|
||||
} else {
|
||||
footerText = fmt.Sprintf("%s %s %s %s %s %s %s %s %s %s",
|
||||
keybindStyle.Render("j/k"),
|
||||
descStyle.Render("products"),
|
||||
keybindStyle.Render("+/-"),
|
||||
descStyle.Render("qty"),
|
||||
keybindStyle.Render("c"),
|
||||
descStyle.Render("cart"),
|
||||
keybindStyle.Render("a"),
|
||||
descStyle.Render("account"),
|
||||
keybindStyle.Render("q"),
|
||||
descStyle.Render("quit"),
|
||||
)
|
||||
}
|
||||
|
||||
return lipgloss.NewStyle().
|
||||
Align(lipgloss.Center).
|
||||
Width(m.windowWidth).
|
||||
Render(footerText)
|
||||
}
|
||||
|
||||
// InitialModel creates a new model with default coffee options
|
||||
func InitialModel(username string) model {
|
||||
return model{
|
||||
username: username,
|
||||
coffees: []Coffee{
|
||||
{
|
||||
Name: "Espresso",
|
||||
RoastType: "Dark Roast",
|
||||
Ounces: 2,
|
||||
BeanType: "Arabica",
|
||||
Price: 3.50,
|
||||
Color: "#8B4513",
|
||||
Description: "A bold, concentrated shot of pure coffee bliss. Perfect for those who need an immediate caffeine injection to survive the day.",
|
||||
},
|
||||
{
|
||||
Name: "Latte",
|
||||
RoastType: "Medium Roast",
|
||||
Ounces: 12,
|
||||
BeanType: "Arabica Blend",
|
||||
Price: 5.00,
|
||||
Color: "#D2691E",
|
||||
Description: "Smooth espresso paired with steamed milk and a light layer of foam. For when you want coffee but also want to feel fancy about it.",
|
||||
},
|
||||
{
|
||||
Name: "Cappuccino",
|
||||
RoastType: "Medium Roast",
|
||||
Ounces: 8,
|
||||
BeanType: "Italian Blend",
|
||||
Price: 4.50,
|
||||
Color: "#CD853F",
|
||||
Description: "Equal parts espresso, steamed milk, and foam. The classic Italian choice for people who know what they're doing.",
|
||||
},
|
||||
{
|
||||
Name: "Americano",
|
||||
RoastType: "Dark Roast",
|
||||
Ounces: 16,
|
||||
BeanType: "Arabica",
|
||||
Price: 4.00,
|
||||
Color: "#A0522D",
|
||||
Description: "Espresso with hot water. Simple, strong, and no-nonsense. This is coffee for people who actually like the taste of coffee.",
|
||||
},
|
||||
{
|
||||
Name: "Cold Brew",
|
||||
RoastType: "Medium Roast",
|
||||
Ounces: 16,
|
||||
BeanType: "Colombian",
|
||||
Price: 5.50,
|
||||
Color: "#4682B4",
|
||||
Description: "Steeped for 12 hours to create a smooth, less acidic coffee experience. Served cold because sometimes hot coffee is just too much effort.",
|
||||
},
|
||||
{
|
||||
Name: "Macchiato",
|
||||
RoastType: "Dark Roast",
|
||||
Ounces: 3,
|
||||
BeanType: "Robusta Blend",
|
||||
Price: 4.25,
|
||||
Color: "#DAA520",
|
||||
Description: "Espresso 'marked' with a dollop of foamed milk. Small but mighty, like a tiny caffeinated warrior.",
|
||||
},
|
||||
},
|
||||
cursor: 0,
|
||||
cart: make(map[int]*CartItem),
|
||||
cartCursor: 0,
|
||||
accountCursor: 0,
|
||||
accountPage: "",
|
||||
windowWidth: 120,
|
||||
windowHeight: 30,
|
||||
viewingCart: false,
|
||||
viewingAccount: false,
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ func TestSeed(t *testing.T) {
|
||||
t.Errorf("Unexpected product name: %s", p.Name)
|
||||
}
|
||||
if p.Price <= 0 {
|
||||
t.Errorf("Product %s has invalid price: %f", p.Name, p.Price)
|
||||
t.Errorf("Product %s has invalid price: %d", p.Name, p.Price)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func Seed(db *gorm.DB) error {
|
||||
RoastType: "Dark Roast",
|
||||
Ounces: 2,
|
||||
BeanType: "Arabica",
|
||||
Price: 3.50,
|
||||
Price: 350, // $3.50
|
||||
Color: "#8B4513",
|
||||
Description: "Bold and intense coffee with a rich crema",
|
||||
},
|
||||
@@ -36,7 +36,7 @@ func Seed(db *gorm.DB) error {
|
||||
RoastType: "Medium Roast",
|
||||
Ounces: 8,
|
||||
BeanType: "Arabica",
|
||||
Price: 4.00,
|
||||
Price: 400, // $4.00
|
||||
Color: "#A0522D",
|
||||
Description: "Smooth and balanced espresso diluted with hot water",
|
||||
},
|
||||
@@ -45,7 +45,7 @@ func Seed(db *gorm.DB) error {
|
||||
RoastType: "Medium Roast",
|
||||
Ounces: 6,
|
||||
BeanType: "Arabica",
|
||||
Price: 4.50,
|
||||
Price: 450, // $4.50
|
||||
Color: "#C4A574",
|
||||
Description: "Espresso with steamed milk and a thick layer of foam",
|
||||
},
|
||||
@@ -54,7 +54,7 @@ func Seed(db *gorm.DB) error {
|
||||
RoastType: "Medium Roast",
|
||||
Ounces: 12,
|
||||
BeanType: "Arabica",
|
||||
Price: 5.00,
|
||||
Price: 500, // $5.00
|
||||
Color: "#D2B48C",
|
||||
Description: "Creamy espresso with plenty of steamed milk and a touch of foam",
|
||||
},
|
||||
@@ -63,7 +63,7 @@ func Seed(db *gorm.DB) error {
|
||||
RoastType: "Dark Roast",
|
||||
Ounces: 12,
|
||||
BeanType: "Arabica",
|
||||
Price: 5.50,
|
||||
Price: 550, // $5.50
|
||||
Color: "#8B4513",
|
||||
Description: "Chocolate and espresso combined with steamed milk. Sweet and indulgent.",
|
||||
},
|
||||
@@ -72,7 +72,7 @@ func Seed(db *gorm.DB) error {
|
||||
RoastType: "Dark Roast",
|
||||
Ounces: 3,
|
||||
BeanType: "Robusta Blend",
|
||||
Price: 4.25,
|
||||
Price: 425, // $4.25
|
||||
Color: "#DAA520",
|
||||
Description: "Espresso 'marked' with a dollop of foamed milk. Small but mighty, like a tiny caffeinated warrior.",
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ type Coffee struct {
|
||||
RoastType string `gorm:"size:100" json:"roast_type"`
|
||||
Ounces int `json:"ounces"`
|
||||
BeanType string `gorm:"size:100" json:"bean_type"`
|
||||
Price float64 `gorm:"not null" json:"price"`
|
||||
Price int `gorm:"not null" json:"price"`
|
||||
Color string `gorm:"size:7" json:"color"` // Hex color for background
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
@@ -26,7 +26,6 @@ func (Coffee) TableName() string {
|
||||
return "coffees"
|
||||
}
|
||||
|
||||
|
||||
// AccountMenuItems something
|
||||
var AccountMenuItems = []string{
|
||||
"order history",
|
||||
|
||||
@@ -27,7 +27,7 @@ func (m Model) BuildCartView() string {
|
||||
}
|
||||
|
||||
for idx, item := range itemSlice {
|
||||
itemTotal := float64(item.Quantity) * item.Coffee.Price
|
||||
itemTotal := item.Quantity * item.Coffee.Price
|
||||
|
||||
// Left side: name and individual price
|
||||
nameStyle := lipgloss.NewStyle().
|
||||
@@ -51,13 +51,13 @@ func (m Model) BuildCartView() string {
|
||||
|
||||
// Build content with proper spacing
|
||||
nameText := nameStyle.Render(item.Coffee.Name)
|
||||
individualPriceText := individualPriceStyle.Render(fmt.Sprintf(" $%.2f", item.Coffee.Price))
|
||||
individualPriceText := individualPriceStyle.Render(fmt.Sprintf(" $%.2f", float64(item.Coffee.Price)/100))
|
||||
infoText := infoStyle.Render(fmt.Sprintf("%s | %doz | %s",
|
||||
item.Coffee.RoastType,
|
||||
item.Coffee.Ounces,
|
||||
item.Coffee.BeanType))
|
||||
quantityText := quantityStyle.Render(fmt.Sprintf("- %d +", item.Quantity))
|
||||
totalPriceText := totalPriceStyle.Render(fmt.Sprintf("$%.2f", itemTotal))
|
||||
totalPriceText := totalPriceStyle.Render(fmt.Sprintf("$%.2f", float64(itemTotal)/100))
|
||||
|
||||
// Calculate spacing
|
||||
nameAndPriceWidth := lipgloss.Width(nameText) + lipgloss.Width(individualPriceText)
|
||||
|
||||
@@ -45,15 +45,15 @@ func (m Model) RenderConfirmation() string {
|
||||
|
||||
// Cart summary
|
||||
cartSection := ""
|
||||
total := 0.0
|
||||
total := 0
|
||||
items := m.GetCartItemsSlice()
|
||||
for _, item := range items {
|
||||
line := fmt.Sprintf(" %s x%d $%.2f", item.Coffee.Name, item.Quantity, item.Coffee.Price*float64(item.Quantity))
|
||||
line := fmt.Sprintf(" %s x%d $%.2f", item.Coffee.Name, item.Quantity, float64(item.Coffee.Price * item.Quantity)/100)
|
||||
cartSection += sectionStyle.Render(line) + "\n"
|
||||
total += item.Coffee.Price * float64(item.Quantity)
|
||||
total += item.Coffee.Price * item.Quantity
|
||||
}
|
||||
cartSection += sectionStyle.Render(
|
||||
labelStyle.Render("Total: ") + valueStyle.Render(fmt.Sprintf("$%.2f", total)),
|
||||
labelStyle.Render("Total: ") + valueStyle.Render(fmt.Sprintf("$%.2f", float64(total)/100)),
|
||||
)
|
||||
|
||||
success := successStyle.Render("Order placed successfully!")
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
|
||||
func (m Model) BuildHeader() string {
|
||||
// Calculate cart total and item count
|
||||
total := 0.0
|
||||
total := 0
|
||||
itemCount := 0
|
||||
for _, item := range m.Cart {
|
||||
total += float64(item.Quantity) * item.Coffee.Price
|
||||
total += item.Quantity * item.Coffee.Price
|
||||
itemCount += item.Quantity
|
||||
}
|
||||
|
||||
@@ -29,14 +29,14 @@ func (m Model) BuildHeader() string {
|
||||
cartKeybind = boldWhiteStyle.Render("c")
|
||||
cartName = boldWhiteStyle.Render("cart")
|
||||
cartInfo = fmt.Sprintf(" %s %s",
|
||||
boldWhiteStyle.Render(fmt.Sprintf("$%.2f", total)),
|
||||
boldWhiteStyle.Render(fmt.Sprintf("$%.2f", float64(total)/100)),
|
||||
itemCountText)
|
||||
} else {
|
||||
// Inactive: bold gray keybind, gray name
|
||||
cartKeybind = boldGrayStyle.Render("c")
|
||||
cartName = grayStyle.Render("cart")
|
||||
cartInfo = fmt.Sprintf(" %s %s",
|
||||
boldWhiteStyle.Render(fmt.Sprintf("$%.2f", total)),
|
||||
boldWhiteStyle.Render(fmt.Sprintf("$%.2f", float64(total)/100)),
|
||||
itemCountText)
|
||||
}
|
||||
cartTab := cartKeybind + " " + cartName + cartInfo
|
||||
|
||||
@@ -35,13 +35,13 @@ func (m Model) BuildMenuView() string {
|
||||
hint := base.Render("press esc to close")
|
||||
|
||||
itemCount := 0
|
||||
total := 0.0
|
||||
total := 0
|
||||
for _, item := range m.Cart {
|
||||
total += float64(item.Quantity) * item.Coffee.Price
|
||||
total += item.Quantity * item.Coffee.Price
|
||||
itemCount += item.Quantity
|
||||
}
|
||||
|
||||
cartHint := base.Render(fmt.Sprintf("cart $%.2f [%d]", total, itemCount))
|
||||
cartHint := base.Render(fmt.Sprintf("cart $%.2f [%d]", float64(total)/100, itemCount))
|
||||
|
||||
assembled := lipgloss.JoinVertical(
|
||||
lipgloss.Center,
|
||||
|
||||
@@ -1433,7 +1433,7 @@ func NewModel(username string) Model {
|
||||
RoastType: "Dark Roast",
|
||||
Ounces: 2,
|
||||
BeanType: "Arabica",
|
||||
Price: 3.50,
|
||||
Price: 350, // $3.50
|
||||
Color: "#8B4513",
|
||||
Description: "A bold, concentrated shot of pure coffee bliss. Perfect for those who need an immediate caffeine injection to survive the day.",
|
||||
},
|
||||
@@ -1442,7 +1442,7 @@ func NewModel(username string) Model {
|
||||
RoastType: "Medium Roast",
|
||||
Ounces: 12,
|
||||
BeanType: "Arabica Blend",
|
||||
Price: 5.00,
|
||||
Price: 500, // $5.00
|
||||
Color: "#D2691E",
|
||||
Description: "Smooth espresso paired with steamed milk and a light layer of foam. For when you want coffee but also want to feel fancy about it.",
|
||||
},
|
||||
@@ -1451,7 +1451,7 @@ func NewModel(username string) Model {
|
||||
RoastType: "Medium Roast",
|
||||
Ounces: 8,
|
||||
BeanType: "Italian Blend",
|
||||
Price: 4.50,
|
||||
Price: 450, // $4.50
|
||||
Color: "#CD853F",
|
||||
Description: "Equal parts espresso, steamed milk, and foam. The classic Italian choice for people who know what they're doing.",
|
||||
},
|
||||
@@ -1460,7 +1460,7 @@ func NewModel(username string) Model {
|
||||
RoastType: "Dark Roast",
|
||||
Ounces: 16,
|
||||
BeanType: "Arabica",
|
||||
Price: 4.00,
|
||||
Price: 400, // $4.00
|
||||
Color: "#A0522D",
|
||||
Description: "Espresso with hot water. Simple, strong, and no-nonsense. This is coffee for people who actually like the taste of coffee.",
|
||||
},
|
||||
@@ -1469,7 +1469,7 @@ func NewModel(username string) Model {
|
||||
RoastType: "Medium Roast",
|
||||
Ounces: 16,
|
||||
BeanType: "Colombian",
|
||||
Price: 5.50,
|
||||
Price: 550, // $5.50
|
||||
Color: "#4682B4",
|
||||
Description: "Some really good Mocha",
|
||||
},
|
||||
@@ -1478,7 +1478,7 @@ func NewModel(username string) Model {
|
||||
RoastType: "Dark Roast",
|
||||
Ounces: 3,
|
||||
BeanType: "Robusta Blend",
|
||||
Price: 4.25,
|
||||
Price: 425, // $4.25
|
||||
Color: "#DAA520",
|
||||
Description: "Espresso 'marked' with a dollop of foamed milk. Small but mighty, like a tiny caffeinated warrior.",
|
||||
},
|
||||
|
||||
@@ -88,7 +88,7 @@ func (m Model) BuildShopView() string {
|
||||
selectedCoffee.RoastType,
|
||||
selectedCoffee.Ounces,
|
||||
selectedCoffee.BeanType)) + "\n\n"
|
||||
detailView += priceStyle.Render(fmt.Sprintf("$%.2f", selectedCoffee.Price)) + "\n\n"
|
||||
detailView += priceStyle.Render(fmt.Sprintf("$%.2f", float64(selectedCoffee.Price)/100)) + "\n\n"
|
||||
detailView += descStyle.Render(selectedCoffee.Description) + "\n\n"
|
||||
detailView += quantityStyle.Render(fmt.Sprintf("- %d +", quantity)) + "\n"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user