711 lines
19 KiB
Go
711 lines
19 KiB
Go
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,
|
|
}
|
|
}
|