fix and feature: made checkout page cleaner, and added card and address modification to account page
the checkout flow was a bit wonky because it would display the same info twice which looked weird and confusing. this is now changed to only show once and update one of the pages independantly. Now added the possibility to modify card and address list in the account page and not in the checkout flow
This commit is contained in:
@@ -29,6 +29,8 @@ func (Coffee) TableName() string {
|
||||
// AccountMenuItems
|
||||
var AccountMenuItems = []string{
|
||||
"order history",
|
||||
"addresses",
|
||||
"cards",
|
||||
"faq",
|
||||
"about",
|
||||
}
|
||||
|
||||
@@ -84,6 +84,58 @@ func (m Model) BuildAccountView(availableHeight int) string {
|
||||
}
|
||||
detailView = lines
|
||||
}
|
||||
|
||||
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.AddressListFocused && i == m.AccountAddressCursor
|
||||
label := addr.Name + " " + addr.Street + ", " + addr.City
|
||||
if len(label) > detailContentWidth-4 {
|
||||
label = label[:detailContentWidth-4]
|
||||
}
|
||||
if m.AccountAddressDeleting != nil && *m.AccountAddressDeleting == 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.AddressListFocused {
|
||||
lines += "\n" + m.theme.TextDim().Render("x: delete esc: back")
|
||||
} else {
|
||||
lines += "\n" + m.theme.TextDim().Render("enter: manage")
|
||||
}
|
||||
detailView = lines
|
||||
|
||||
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.CardListFocused && i == m.AccountCardCursor
|
||||
label := fmt.Sprintf("**** **** **** %s %s exp %02d/%02d",
|
||||
card.Last4, card.Brand, card.ExpMonth, card.ExpYear%100)
|
||||
if m.AccountCardDeleting != nil && *m.AccountCardDeleting == 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.CardListFocused {
|
||||
lines += "\n" + m.theme.TextDim().Render("x: delete esc: back")
|
||||
} else {
|
||||
lines += "\n" + m.theme.TextDim().Render("enter: manage")
|
||||
}
|
||||
detailView = lines
|
||||
case "faq":
|
||||
questionStyle := m.theme.TextHighlight().Bold(true)
|
||||
|
||||
@@ -380,9 +432,23 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
if !ok {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
selectedItem := ""
|
||||
if m.AccountCursor < len(models.AccountMenuItems) {
|
||||
selectedItem = models.AccountMenuItems[m.AccountCursor]
|
||||
}
|
||||
|
||||
switch keyMsg.String() {
|
||||
case "up", "k":
|
||||
if m.FaqFocused {
|
||||
if m.AddressListFocused && m.AccountAddressDeleting == nil {
|
||||
if m.AccountAddressCursor > 0 {
|
||||
m.AccountAddressCursor--
|
||||
}
|
||||
} else if m.CardListFocused && m.AccountCardDeleting == nil {
|
||||
if m.AccountCardCursor > 0 {
|
||||
m.AccountCardCursor--
|
||||
}
|
||||
} else if m.FaqFocused {
|
||||
m.ScrollOffset -= 3
|
||||
if m.ScrollOffset < 0 {
|
||||
m.ScrollOffset = 0
|
||||
@@ -401,42 +467,45 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
m.AccountCursor--
|
||||
m.ScrollOffset = 0
|
||||
}
|
||||
|
||||
case "down", "j":
|
||||
if m.FaqFocused {
|
||||
m.ScrollOffset += 3
|
||||
maxScroll := m.computeFaqScrollMax()
|
||||
if m.ScrollOffset > maxScroll {
|
||||
m.ScrollOffset = maxScroll
|
||||
if m.AddressListFocused && m.AccountAddressDeleting == nil {
|
||||
if m.AccountAddressCursor < len(m.SavedAddresses)-1 {
|
||||
m.AccountAddressCursor++
|
||||
}
|
||||
} else if m.OrderViewState == 1 && m.OrderCursor < len(m.Orders)-1 {
|
||||
m.OrderCursor++
|
||||
} else if m.CardListFocused && m.AccountCardDeleting == nil {
|
||||
if m.AccountCardCursor < len(m.SavedCards)-1 {
|
||||
m.AccountCardCursor++
|
||||
}
|
||||
} else if m.FaqFocused {
|
||||
m.ScrollOffset -= 3
|
||||
if m.ScrollOffset < 0 {
|
||||
m.ScrollOffset = 0
|
||||
}
|
||||
} else if m.OrderViewState == 1 && m.OrderCursor > 0 {
|
||||
m.OrderCursor--
|
||||
cardHeight := 5
|
||||
if m.heightContainer >= 25 {
|
||||
cardHeight = 7
|
||||
}
|
||||
viewportHeight := m.heightContainer - 7
|
||||
if m.size < large {
|
||||
viewportHeight -= len(models.AccountMenuItems) + 1
|
||||
targetTop := 3 + m.OrderCursor*cardHeight
|
||||
if targetTop < m.ScrollOffset {
|
||||
m.ScrollOffset = targetTop
|
||||
}
|
||||
if viewportHeight < 5 {
|
||||
viewportHeight = 5
|
||||
}
|
||||
targetBottom := 3 + (m.OrderCursor+1)*cardHeight
|
||||
if targetBottom > m.ScrollOffset+viewportHeight {
|
||||
m.ScrollOffset = targetBottom - viewportHeight
|
||||
}
|
||||
} else if m.OrderViewState == 0 && m.AccountCursor < len(models.AccountMenuItems)-1 {
|
||||
m.AccountCursor++
|
||||
} else if m.OrderViewState == 0 && m.AccountCursor > 0 {
|
||||
m.AccountCursor--
|
||||
m.ScrollOffset = 0
|
||||
}
|
||||
case "pgup", "ctrl+u":
|
||||
|
||||
case "pgup", "ctrl-u":
|
||||
if m.FaqFocused || m.OrderViewState >= 2 {
|
||||
m.ScrollOffset -= 3
|
||||
if m.ScrollOffset < 0 {
|
||||
m.ScrollOffset = 0
|
||||
}
|
||||
}
|
||||
case "pgdown", "ctrl+d":
|
||||
|
||||
case "pgdown", "ctrl-d":
|
||||
if m.FaqFocused || m.OrderViewState >= 2 {
|
||||
m.ScrollOffset += 3
|
||||
maxScroll := m.computeFaqScrollMax()
|
||||
@@ -444,22 +513,81 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
m.ScrollOffset = maxScroll
|
||||
}
|
||||
}
|
||||
|
||||
case "x", "d":
|
||||
if m.AddressListFocused && m.AccountAddressDeleting == nil && m.AccountAddressCursor < len(m.SavedAddresses) {
|
||||
m.AccountAddressDeleting = &m.AccountAddressCursor
|
||||
} else if m.CardListFocused && m.AccountCardDeleting == nil && m.AccountCardCursor < len(m.SavedCards) {
|
||||
m.AccountCardDeleting = &m.AccountCardCursor
|
||||
}
|
||||
|
||||
case "y":
|
||||
if m.AccountAddressDeleting != nil {
|
||||
cursor := *m.AccountAddressDeleting
|
||||
addr := m.SavedAddresses[cursor]
|
||||
m.AccountAddressDeleting = nil
|
||||
m.SavedAddresses = append(m.SavedAddresses[:cursor], m.SavedAddresses[cursor+1:]...)
|
||||
if m.AccountAddressCursor >= len(m.SavedAddresses) && m.AccountAddressCursor > 0 {
|
||||
m.AccountAddressCursor--
|
||||
}
|
||||
return m, m.deleteAddressCmd(addr.ID)
|
||||
} else if m.AccountCardDeleting != nil {
|
||||
cursor := *m.AccountCardDeleting
|
||||
card := m.SavedCards[cursor]
|
||||
m.AccountCardDeleting = nil
|
||||
m.SavedCards = append(m.SavedCards[:cursor], m.SavedCards[cursor+1:]...)
|
||||
if m.AccountCardCursor >= len(m.SavedCards) && m.AccountCardCursor > 0 {
|
||||
m.AccountCardCursor--
|
||||
}
|
||||
return m, m.deleteAddressCmd(card.ID)
|
||||
}
|
||||
|
||||
case "n":
|
||||
m.AccountAddressDeleting = nil
|
||||
m.AccountCardDeleting = nil
|
||||
|
||||
case "p", "enter":
|
||||
if m.AccountCursor == 0 && len(m.Orders) > 0 {
|
||||
if m.OrderViewState == 0 {
|
||||
m.OrderViewState = 1
|
||||
m.OrderCursor = 0
|
||||
m.ScrollOffset = 0
|
||||
} else if m.OrderViewState == 1 {
|
||||
m.OrderViewState = 2
|
||||
switch selectedItem {
|
||||
case "order history":
|
||||
if len(m.Orders) > 0 {
|
||||
if m.OrderViewState == 0 {
|
||||
m.OrderViewState = 1
|
||||
m.OrderCursor = 0
|
||||
m.ScrollOffset = 0
|
||||
} else if m.OrderViewState == 1 {
|
||||
m.OrderViewState = 2
|
||||
m.ScrollOffset = 0
|
||||
}
|
||||
}
|
||||
case "addresses":
|
||||
if !m.AddressListFocused {
|
||||
m.AddressListFocused = true
|
||||
m.AccountAddressCursor = 0
|
||||
}
|
||||
case "cards":
|
||||
if !m.CardListFocused {
|
||||
m.CardListFocused = true
|
||||
m.AccountCardCursor = 0
|
||||
}
|
||||
case "faq":
|
||||
if !m.FaqFocused {
|
||||
m.FaqFocused = true
|
||||
m.ScrollOffset = 0
|
||||
}
|
||||
} else if m.AccountCursor == 1 && !m.FaqFocused {
|
||||
m.FaqFocused = true
|
||||
m.ScrollOffset = 0
|
||||
}
|
||||
|
||||
case "esc":
|
||||
if m.FaqFocused {
|
||||
if m.AccountAddressDeleting != nil {
|
||||
m.AccountAddressDeleting = nil
|
||||
} else if m.AddressListFocused {
|
||||
m.AddressListFocused = false
|
||||
m.AccountAddressCursor = 0
|
||||
} else if m.AccountCardDeleting != nil {
|
||||
m.AccountCardDeleting = nil
|
||||
} else if m.CardListFocused {
|
||||
m.CardListFocused = false
|
||||
m.AccountCardCursor = 0
|
||||
} else if m.FaqFocused {
|
||||
m.FaqFocused = false
|
||||
m.ScrollOffset = 0
|
||||
} else if m.OrderViewState > 0 {
|
||||
|
||||
@@ -132,6 +132,10 @@ func formatUSD(cents int) string {
|
||||
func (m Model) generateReviewContent() string {
|
||||
view := strings.Builder{}
|
||||
|
||||
if m.CardJustAdded {
|
||||
view.WriteString(m.theme.TextSuccess().Bold(true).Render("card added successfully") + "\n\n")
|
||||
}
|
||||
|
||||
if m.ShippingInfo != nil {
|
||||
view.WriteString(m.ShippingInfo.Name + "\n")
|
||||
view.WriteString(m.ShippingInfo.Street + "\n")
|
||||
@@ -151,7 +155,7 @@ func (m Model) generateReviewContent() string {
|
||||
for _, item := range m.GetCartItemsSlice() {
|
||||
subtotal += item.Coffee.Price * item.Quantity
|
||||
}
|
||||
view.WriteString(fmt.Sprintf("subtotal: %\n", formatUSD(subtotal)))
|
||||
view.WriteString(fmt.Sprintf("subtotal: %s\n", formatUSD(subtotal)))
|
||||
view.WriteString(m.theme.TextAccent().Render(fmt.Sprintf("total: %s", formatUSD(subtotal))) + "\n")
|
||||
view.WriteString("\n")
|
||||
view.WriteString(m.theme.TextDim().Render("press enter to confirm") + "\n")
|
||||
@@ -163,6 +167,15 @@ func (m Model) ReviewView() string {
|
||||
if m.CheckingOut {
|
||||
return m.theme.TextAccent().Bold(true).Padding(1).Render(" submitting order...")
|
||||
}
|
||||
if m.ReviewSuccess {
|
||||
content := m.theme.TextSuccess().Bold(true).Padding(1).Render("order placed successfully!")
|
||||
return lipgloss.Place(
|
||||
m.widthContainer,
|
||||
lipgloss.Height(content),
|
||||
lipgloss.Center, lipgloss.Center,
|
||||
content,
|
||||
)
|
||||
}
|
||||
content := m.generateReviewContent()
|
||||
return lipgloss.Place(
|
||||
m.widthContainer,
|
||||
@@ -183,22 +196,29 @@ func (m Model) ReviewUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
m.PaymentForm = nil
|
||||
return m, nil
|
||||
}
|
||||
m.ConfirmTotal = msg.Total
|
||||
for _, item := range m.GetCartItemsSlice() {
|
||||
m.ConfirmItems = append(m.ConfirmItems, *item)
|
||||
}
|
||||
m.ConfirmShipping = m.ShippingInfo
|
||||
m.Cart = make(map[uint]*models.CartItem)
|
||||
m.CartCursor = 0
|
||||
m.ShippingInfo = nil
|
||||
m.OrdersLoaded = false
|
||||
m = m.SwitchPage(confirmPage)
|
||||
m.ReviewSuccess = true
|
||||
return m, nil
|
||||
|
||||
case tea.KeyMsg:
|
||||
if m.CheckingOut {
|
||||
return m, nil
|
||||
}
|
||||
if m.ReviewSuccess {
|
||||
m = m.SwitchPage(shopPage)
|
||||
m.ReviewSuccess = false
|
||||
m.CardJustAdded = false
|
||||
m = m.resetPageState()
|
||||
return m, func() tea.Msg {
|
||||
if m.APIClient != nil {
|
||||
_ = m.APIClient.ClearCart()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
m = m.SwitchPage(paymentPage)
|
||||
|
||||
@@ -132,12 +132,22 @@ type Model struct {
|
||||
FAQs []FAQ // loaded from embedded faq.json
|
||||
FaqFocused bool // true when FAQ detail is focused
|
||||
|
||||
// Account manegement state
|
||||
AddressListFocused bool // true when browsing addresses in account
|
||||
CardListFocused bool // true when browsing cards in account
|
||||
AccountAddressCursor int // cursor within address list in AccountAddressCursor
|
||||
AccountCardCursor int // cursor within card list in account
|
||||
AccountAddressDeleting *int // index of address pending deletion
|
||||
AccountCardDeleting *int // index of card pending deletion
|
||||
|
||||
// Splash screen
|
||||
splashDataReady bool // true when products have loaded
|
||||
splashDelayDone bool // true when minimum display time has elapsed
|
||||
splashCursor bool // toggles for blinking cursor animation
|
||||
|
||||
// Confirmation screen — snapshot populated on checkout, cleared when leaving confirm page
|
||||
// Review / confirmation state
|
||||
CardJustAdded bool // true when arriving at review page after adding a new card
|
||||
ReviewSuccess bool // true after a successful order, shows success msg on review page
|
||||
ConfirmTotal int // server-confirmed order total in cents
|
||||
ConfirmItems []models.CartItem // cart items at time of purchase
|
||||
ConfirmShipping *models.Address // shipping address at time of purchase
|
||||
@@ -271,6 +281,10 @@ func (m Model) resetPageState() Model {
|
||||
m.ScrollOffset = 0
|
||||
m.OrderViewState = 0
|
||||
m.FaqFocused = false
|
||||
m.AddressListFocused = false
|
||||
m.CardListFocused = false
|
||||
m.AccountAddressDeleting = nil
|
||||
m.AccountCardDeleting = nil
|
||||
return m
|
||||
}
|
||||
|
||||
@@ -606,7 +620,6 @@ func (m Model) saveCardOnlyCmd(form PaymentFormCompleteMsg) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// loadCartFromAPI populates the in-memory cart map from an API CartData response
|
||||
// It matches cart items to local Coffees by ID so the Coffee struct is populated
|
||||
func (m *Model) loadCartFromAPI(cartData *api.CartData) {
|
||||
|
||||
@@ -431,7 +431,7 @@ func (m Model) PaymentUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
m.PaymentForm.submitting = false
|
||||
}
|
||||
if msg.Err != nil {
|
||||
m.ErrorMsg = fmt.Sprintf("failed to save card: %w", msg.Err)
|
||||
m.ErrorMsg = fmt.Sprintf("failed to save card: %v", msg.Err)
|
||||
m.PaymentView = 1
|
||||
m.PaymentForm = m.InitPaymentForm()
|
||||
return m, m.PaymentForm.form.Init()
|
||||
@@ -440,6 +440,7 @@ func (m Model) PaymentUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
selected := msg.Card
|
||||
m.SelectedCard = &selected
|
||||
m.PaymentForm = nil
|
||||
m.CardJustAdded = true
|
||||
m = m.SwitchPage(reviewPage)
|
||||
return m, nil
|
||||
|
||||
@@ -463,6 +464,7 @@ func (m Model) PaymentUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
m.CollectURL = nil
|
||||
selected := m.SavedCards[len(m.SavedCards)-1]
|
||||
m.SelectedCard = &selected
|
||||
m.CardJustAdded = true
|
||||
m = m.SwitchPage(reviewPage)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user