change the BuildAccountView inline sub-views to use 4 different files to make it better
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
@@ -45,124 +44,15 @@ func (m Model) BuildAccountView(availableHeight int) string {
|
||||
selectedItem := models.AccountMenuItems[m.account.cursor]
|
||||
switch selectedItem {
|
||||
case "order history":
|
||||
if !m.OrdersLoaded {
|
||||
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.account.orderViewState == 2 {
|
||||
// State 2: full detail for the selected order
|
||||
detailView = m.buildOrderDetailView(m.Orders[m.account.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 i, order := range m.Orders {
|
||||
isSelected := m.account.orderViewState == 1 && i == m.account.orderCursor
|
||||
lines += m.buildOrderCard(order, boxWidth, isSelected, boxPadding, detailContentWidth)
|
||||
if i < len(m.Orders)-1 {
|
||||
lines += "\n"
|
||||
}
|
||||
}
|
||||
|
||||
if m.account.orderViewState == 0 {
|
||||
hintStyle := m.theme.TextDim()
|
||||
lines += "\n" + hintStyle.Render("enter: browse orders")
|
||||
}
|
||||
detailView = lines
|
||||
}
|
||||
|
||||
detailView = m.OrdersView(detailContentWidth)
|
||||
case "addresses":
|
||||
lines := titleStyle.Render("Address") + "\n\n"
|
||||
if len(m.SavedAddresses) == 0 {
|
||||
lines += contentStyle.Render("No saved addresses")
|
||||
} else {
|
||||
for i, addr := range m.SavedAddresses {
|
||||
isSelected := m.account.addressListFocused && i == m.account.addressCursor
|
||||
label := addr.Name + " " + addr.Street + ", " + addr.City
|
||||
if len(label) > detailContentWidth-4 {
|
||||
label = label[:detailContentWidth-4]
|
||||
}
|
||||
if m.account.addressDeleting != nil && *m.account.addressDeleting == i {
|
||||
lines += m.theme.TextError().Bold(true).Render(" deletes? (y/n)") + "\n"
|
||||
} else if isSelected {
|
||||
lines += m.theme.TextAccent().Bold(true).Render("> "+label) + "\n"
|
||||
} else {
|
||||
lines += contentStyle.Render(" "+label) + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
if m.account.addressListFocused {
|
||||
lines += "\n" + m.theme.TextDim().Render("x: delete esc: back")
|
||||
} else {
|
||||
lines += "\n" + m.theme.TextDim().Render("enter: manage")
|
||||
}
|
||||
detailView = lines
|
||||
|
||||
detailView = m.AddressesView(detailContentWidth)
|
||||
case "cards":
|
||||
lines := titleStyle.Render("Cards") + "\n\n"
|
||||
if len(m.SavedCards) == 0 {
|
||||
lines += contentStyle.Render("No saved cards.")
|
||||
} else {
|
||||
for i, card := range m.SavedCards {
|
||||
isSelected := m.account.cardListFocused && i == m.account.cardCursor
|
||||
label := fmt.Sprintf("**** **** **** %s %s exp %02d/%02d",
|
||||
card.Last4, card.Brand, card.ExpMonth, card.ExpYear%100)
|
||||
if m.account.cardDeleting != nil && *m.account.cardDeleting == i {
|
||||
lines += m.theme.TextError().Bold(true).Render(" delete? (y/n)") + "\n"
|
||||
} else if isSelected {
|
||||
lines += m.theme.TextAccent().Bold(true).Render("> "+label) + "\n"
|
||||
} else {
|
||||
lines += contentStyle.Render(" "+label) + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
if m.account.cardListFocused {
|
||||
lines += "\n" + m.theme.TextDim().Render("x: delete esc: back")
|
||||
} else {
|
||||
lines += "\n" + m.theme.TextDim().Render("enter: manage")
|
||||
}
|
||||
detailView = lines
|
||||
|
||||
detailView = m.CardsView(detailContentWidth)
|
||||
case "ssh keys":
|
||||
lines := titleStyle.Render("SSH keys") + "\n\n"
|
||||
if !m.SSHKeysLoaded {
|
||||
lines += contentStyle.Render("Loading ssh keys...")
|
||||
} else if len(m.SSHKeys) == 0 {
|
||||
lines += contentStyle.Render("No saved ssh keys.")
|
||||
} else {
|
||||
for i, key := range m.SSHKeys {
|
||||
isSelected := m.account.sshKeyListFocused && i == m.account.sshKeyCursor
|
||||
label := key.Fingerprint
|
||||
if key.Comment != "" {
|
||||
label += " " + key.Comment
|
||||
}
|
||||
if len(label) > detailContentWidth-4 {
|
||||
label = label[:detailContentWidth-4]
|
||||
}
|
||||
if m.account.sshKeyDeleting != nil && *m.account.sshKeyDeleting == i {
|
||||
lines += m.theme.TextError().Bold(true).Render(" delete? (y/n)") + "\n"
|
||||
} else if isSelected {
|
||||
lines += m.theme.TextAccent().Bold(true).Render("> "+label) + "\n"
|
||||
} else {
|
||||
lines += contentStyle.Render(" "+label) + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
if m.account.sshKeyListFocused {
|
||||
lines += "\n" + m.theme.TextDim().Render("x: delete esc: back")
|
||||
} else {
|
||||
lines += "\n" + m.theme.TextDim().Render("enter: manage")
|
||||
}
|
||||
detailView = lines
|
||||
|
||||
detailView = m.SSHKeysView(detailContentWidth)
|
||||
case "faq":
|
||||
questionStyle := m.theme.TextHighlight().Bold(true)
|
||||
|
||||
faqContent := ""
|
||||
for i, faq := range m.FAQs {
|
||||
faqContent += questionStyle.Render(wordWrap(faq.Question, detailContentWidth)) + "\n"
|
||||
@@ -177,26 +67,7 @@ func (m Model) BuildAccountView(availableHeight int) string {
|
||||
detailView += "\n\n" + hintStyle.Render("enter: scroll faq")
|
||||
}
|
||||
case "about":
|
||||
accentStyle := m.theme.TextHighlight().Bold(true)
|
||||
userStyle := m.theme.TextAccent().Bold(true)
|
||||
|
||||
aboutContent := contentStyle.Render(wordWrap(
|
||||
"This project is heavily inspired by the wonderful team who made TerminalDotShop. I want to thank them for open-sourcing the code for that project. Cudos to those smart, funny, amazing looking devs:",
|
||||
detailContentWidth,
|
||||
)) + "\n\n"
|
||||
aboutContent += userStyle.Render("@thdxr") + "\n"
|
||||
aboutContent += userStyle.Render("@adamdotdev") + "\n"
|
||||
aboutContent += userStyle.Render("@theprimeagen") + "\n"
|
||||
aboutContent += userStyle.Render("@teej_dv") + "\n"
|
||||
aboutContent += userStyle.Render("@iamdavidhill") + "\n\n"
|
||||
aboutContent += contentStyle.Render("And me:") + "\n\n"
|
||||
aboutContent += userStyle.Render("@marcusDenslow") + "\n\n"
|
||||
aboutContent += contentStyle.Render(wordWrap("This project tries to copy the idea of terminalDotShop, but with one twist: write everything in GO. this is my humble attempt",
|
||||
detailContentWidth)) + "\n"
|
||||
aboutContent += accentStyle.Render("Terminal Products, Inc.")
|
||||
|
||||
detailView = titleStyle.Render("About") + "\n\n" + aboutContent
|
||||
|
||||
detailView = m.AboutView(detailContentWidth)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,152 +131,31 @@ func (m Model) BuildAccountView(availableHeight int) string {
|
||||
)
|
||||
}
|
||||
|
||||
// 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 := m.theme.TextAccent().Bold(true)
|
||||
func (m Model) AboutView(width int) string {
|
||||
titleStyle := m.theme.TextAccent().Bold(true).MarginBottom(2)
|
||||
contentStyle := m.theme.TextBody().Width(width)
|
||||
accentStyle := m.theme.TextHighlight().Bold(true)
|
||||
userStyle := m.theme.TextAccent().Bold(true)
|
||||
|
||||
dimStyle := m.theme.TextDim()
|
||||
aboutContent := contentStyle.Render(wordWrap(
|
||||
"This project is heavily inspired by the wonderful team who made TerminalDotShop. I want to thank them for open-sourcing the code for that project. Cudos to those smart, funny, amazing looking devs:",
|
||||
width,
|
||||
)) + "\n\n"
|
||||
aboutContent += userStyle.Render("@thdxr") + "\n"
|
||||
aboutContent += userStyle.Render("@adamdotdev") + "\n"
|
||||
aboutContent += userStyle.Render("@theprimeagen") + "\n"
|
||||
aboutContent += userStyle.Render("@teej_dv") + "\n"
|
||||
aboutContent += userStyle.Render("@iamdavidhill") + "\n\n"
|
||||
aboutContent += contentStyle.Render("And me:") + "\n\n"
|
||||
aboutContent += userStyle.Render("@marcusDenslow") + "\n\n"
|
||||
aboutContent += contentStyle.Render(wordWrap(
|
||||
"This project tries to copy the idea of terminalDotShop, but with one twist: write everything in GO. this is my humble attempt",
|
||||
width,
|
||||
)) + "\n"
|
||||
aboutContent += accentStyle.Render("Terminal Products, Inc.")
|
||||
|
||||
statusStyle := m.theme.TextHighlight().Bold(true)
|
||||
return titleStyle.Render("About") + "\n\n" + aboutContent
|
||||
|
||||
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(m.theme.Accent()).
|
||||
Padding(boxPadding, 2).
|
||||
Width(boxWidth)
|
||||
} else {
|
||||
itemBox = lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(m.theme.Border()).
|
||||
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 := m.theme.TextAccent().Bold(true).MarginBottom(1)
|
||||
|
||||
labelStyle := m.theme.TextHighlight().Bold(true)
|
||||
|
||||
valueStyle := m.theme.TextBody()
|
||||
|
||||
dimStyle := m.theme.TextDim()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// computeFaqScrollMax calculates the maximum scroll offset for the FAQ content.
|
||||
|
||||
35
pkg/tui/account_addresses.go
Normal file
35
pkg/tui/account_addresses.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package tui
|
||||
|
||||
|
||||
func (m Model) AddressesView(width int) string {
|
||||
titleStyle := m.theme.TextAccent().Bold(true).MarginBottom(2)
|
||||
activeStyle := m.theme.TextAccent().Bold(true)
|
||||
contentStyle := m.theme.TextBody().Width(width)
|
||||
|
||||
lines := titleStyle.Render("Address") + "\n\n"
|
||||
if len(m.SavedAddresses) == 0 {
|
||||
lines += contentStyle.Render("No saved addresses")
|
||||
} else {
|
||||
for i, addr := range m.SavedAddresses {
|
||||
isSelected := m.account.addressListFocused && i == m.account.addressCursor
|
||||
label := addr.Name + " " + addr.Street + ", " + addr.City
|
||||
if len(label) > width-4 {
|
||||
label = label[:width-4]
|
||||
}
|
||||
if m.account.addressDeleting != nil && *m.account.addressDeleting == i {
|
||||
lines += m.theme.TextError().Bold(true).Render(" deletes? (y/n)") + "\n"
|
||||
} else if isSelected {
|
||||
lines += activeStyle.Render("> "+label) + "\n"
|
||||
} else {
|
||||
lines += contentStyle.Render(" "+label) + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
if m.account.addressListFocused {
|
||||
lines += "\n" + m.theme.TextDim().Render("x: delete esc: back")
|
||||
} else {
|
||||
lines += "\n" + m.theme.TextDim().Render("enter: manage")
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
36
pkg/tui/account_cards.go
Normal file
36
pkg/tui/account_cards.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func (m Model) CardsView(width int) string {
|
||||
titleSTyle := m.theme.TextAccent().Bold(true).MarginBottom(2)
|
||||
contentStyle := m.theme.TextBody().Width(width)
|
||||
activeStyle := m.theme.TextAccent().Bold(true)
|
||||
|
||||
|
||||
lines := titleSTyle.Render("Cards") + "\n\n"
|
||||
if m.SavedCardsIsEmpty() {
|
||||
lines += contentStyle.Render("No saved cards")
|
||||
} else {
|
||||
for i, card := range m.SavedCards {
|
||||
isSelected := m.account.cardListFocused && i == m.account.cardCursor
|
||||
label := fmt.Sprintf("**** **** **** %s %s exp %02d/%02d", card.Last4, card.Brand, card.ExpMonth, card.ExpYear%100)
|
||||
if m.account.cardDeleting != nil && *m.account.cardDeleting == i {
|
||||
lines += m.theme.TextError().Bold(true).Render(" delete? (y/s)") + "\n"
|
||||
} else if isSelected {
|
||||
lines += activeStyle.Render("> "+label) + "\n"
|
||||
} else {
|
||||
lines += contentStyle.Render(" "+label) + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
if m.account.cardListFocused {
|
||||
lines += "\n" + m.theme.TextDim().Render("x: delete esc: back")
|
||||
} else {
|
||||
lines += "\n" + m.theme.TextDim().Render("enter: manage")
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
194
pkg/tui/account_orders.go
Normal file
194
pkg/tui/account_orders.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"terminalShop/pkg/models"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) OrdersView(width int) string {
|
||||
titleStyle := m.theme.TextAccent().Bold(true).MarginBottom(2)
|
||||
contentStyle := m.theme.TextBody().Width(width)
|
||||
|
||||
if !m.OrdersLoaded {
|
||||
return titleStyle.Render("Order History") + "\n\n" + contentStyle.Render("Loading orders...")
|
||||
}
|
||||
if len(m.Orders) == 0 {
|
||||
return titleStyle.Render("Order History") + "\n\n" + contentStyle.Render("No orders yet")
|
||||
}
|
||||
if m.account.orderViewState == 2 {
|
||||
return m.buildOrderDetailView(m.Orders[m.account.orderCursor], width)
|
||||
}
|
||||
|
||||
boxWidth := width - 4
|
||||
boxPadding := 0
|
||||
if m.heightContainer >= 25 {
|
||||
boxPadding = 1
|
||||
}
|
||||
|
||||
lines := titleStyle.Render("Order History") + "\n\n"
|
||||
for i, order := range m.Orders {
|
||||
isSelected := m.account.orderViewState == 1 && i == m.account.orderCursor
|
||||
lines += m.buildOrderCard(order, boxWidth, isSelected, boxPadding, width)
|
||||
if i < len(m.Orders)-1 {
|
||||
lines += "\n"
|
||||
}
|
||||
}
|
||||
|
||||
if m.account.orderViewState == 0 {
|
||||
hintStyle := m.theme.TextDim()
|
||||
lines += "\n" + hintStyle.Render("enter: browse orders")
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
// 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 := m.theme.TextAccent().Bold(true)
|
||||
|
||||
dimStyle := m.theme.TextDim()
|
||||
|
||||
statusStyle := m.theme.TextHighlight().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(m.theme.Accent()).
|
||||
Padding(boxPadding, 2).
|
||||
Width(boxWidth)
|
||||
} else {
|
||||
itemBox = lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(m.theme.Border()).
|
||||
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 := m.theme.TextAccent().Bold(true).MarginBottom(1)
|
||||
|
||||
labelStyle := m.theme.TextHighlight().Bold(true)
|
||||
|
||||
valueStyle := m.theme.TextBody()
|
||||
|
||||
dimStyle := m.theme.TextDim()
|
||||
|
||||
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()
|
||||
}
|
||||
41
pkg/tui/account_sshkeys.go
Normal file
41
pkg/tui/account_sshkeys.go
Normal file
@@ -0,0 +1,41 @@
|
||||
|
||||
package tui
|
||||
|
||||
func (m Model) SSHKeysView(width int) string {
|
||||
titleSTyle := m.theme.TextAccent().Bold(true).MarginBottom(2)
|
||||
contentStyle := m.theme.TextBody().Width(width)
|
||||
activeStyle := m.theme.TextAccent().Bold(true)
|
||||
|
||||
|
||||
lines := titleSTyle.Render("SSH keys") + "\n\n"
|
||||
if !m.SSHKeysLoaded {
|
||||
lines += contentStyle.Render("Loading ssh keys...")
|
||||
} else if len(m.SSHKeys) == 0 {
|
||||
lines += contentStyle.Render("No saved ssh keys")
|
||||
} else {
|
||||
for i, key := range m.SSHKeys {
|
||||
isSelected := m.account.sshKeyListFocused && i == m.account.sshKeyCursor
|
||||
label := key.Fingerprint
|
||||
if key.Comment != "" {
|
||||
label += " " + key.Comment
|
||||
}
|
||||
if len(label) > width-4 {
|
||||
label += label[:width-4]
|
||||
}
|
||||
if m.account.sshKeyDeleting != nil && *m.account.sshKeyDeleting == i {
|
||||
lines += m.theme.TextError().Bold(true).Render(" delete? (y/n)") + "\n"
|
||||
} else if isSelected {
|
||||
lines += activeStyle.Render("> "+label) + "\n"
|
||||
} else {
|
||||
lines += contentStyle.Render(" "+label) + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
if m.account.sshKeyListFocused {
|
||||
lines += "\n" + m.theme.TextDim().Render("x: delete esc: back")
|
||||
} else {
|
||||
lines += "\n" + m.theme.TextDim().Render("enter: manage")
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
@@ -778,6 +778,14 @@ func (m Model) CalculateSubtotal() int {
|
||||
return subtotal
|
||||
}
|
||||
|
||||
func (m Model) SavedAddressesIsEmpty() bool {
|
||||
return len(m.SavedAddresses) == 0
|
||||
}
|
||||
|
||||
func (m Model) SavedCardsIsEmpty() bool {
|
||||
return len(m.SavedCards) == 0
|
||||
}
|
||||
|
||||
// NewModel creates a new model using the default renderer.
|
||||
func NewModel(username string) Model {
|
||||
return newModelWithRenderer(username, lipgloss.DefaultRenderer())
|
||||
|
||||
Reference in New Issue
Block a user