frontend: view cart, add to cart, empty cart
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
This commit is contained in:
parent
f3fe6d42ad
commit
289bd4db13
8 changed files with 224 additions and 6 deletions
|
@ -9,7 +9,13 @@ import (
|
|||
func sum(m1, m2 pb.MoneyAmount) pb.MoneyAmount {
|
||||
f1, f2 := float64(m1.Fractional), float64(m2.Fractional)
|
||||
lg1 := math.Max(1, math.Ceil(math.Log10(f1)))
|
||||
if f1 == math.Pow(10, lg1) {
|
||||
lg1++
|
||||
}
|
||||
lg2 := math.Max(1, math.Ceil(math.Log10(f2)))
|
||||
if f2 == math.Pow(10, lg2) {
|
||||
lg2++
|
||||
}
|
||||
lgMax := math.Max(lg1, lg2)
|
||||
|
||||
dSum := m1.Decimal + m2.Decimal
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
@ -43,7 +44,7 @@ func ensureSessionID(next http.HandlerFunc) http.HandlerFunc {
|
|||
}
|
||||
|
||||
func (fe *frontendServer) homeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("[home] session_id=%+v", sessionID(r))
|
||||
log.Printf("[home] session_id=%+v currency=%s", sessionID(r), currentCurrency(r))
|
||||
currencies, err := fe.getCurrencies(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("could not retrieve currencies: %+v", err), http.StatusInternalServerError)
|
||||
|
@ -100,12 +101,16 @@ func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request)
|
|||
http.Error(w, fmt.Sprintf("could not retrieve product: %+v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
currencies, err := fe.getCurrencies(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("could not retrieve currencies: %+v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
cart, err := fe.getCart(r.Context(), sessionID(r))
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("could not retrieve cart: %+v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
price, err := fe.convertCurrency(r.Context(), &pb.Money{
|
||||
Amount: p.GetPriceUsd(),
|
||||
|
@ -125,6 +130,92 @@ func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request)
|
|||
"currencies": currencies,
|
||||
"product": product,
|
||||
"session_id": sessionID(r),
|
||||
"cart_size": len(cart),
|
||||
}); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (fe *frontendServer) addToCartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
quantity, _ := strconv.ParseUint(r.FormValue("quantity"), 10, 32)
|
||||
productID := r.FormValue("product_id")
|
||||
if productID == "" || quantity == 0 {
|
||||
http.Error(w, "invalid form input", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
log.Printf("[addToCart] product_id=%s qty=%d session_id=%+v", productID, quantity, sessionID(r))
|
||||
|
||||
p, err := fe.getProduct(r.Context(), productID)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("could not retrieve product: %+v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := fe.insertCart(r.Context(), sessionID(r), p.GetId(), int32(quantity)); err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to add to cart: %+v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("location", "/cart")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}
|
||||
|
||||
func (fe *frontendServer) emptyCartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("[emptyCart] session_id=%+v", sessionID(r))
|
||||
|
||||
if err := fe.emptyCart(r.Context(), sessionID(r)); err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to empty cart: %+v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("location", "/")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}
|
||||
|
||||
func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("[viewCart] session_id=%+v", sessionID(r))
|
||||
currencies, err := fe.getCurrencies(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("could not retrieve currencies: %+v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
cart, err := fe.getCart(r.Context(), sessionID(r))
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("could not retrieve cart: %+v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
type cartItemView struct {
|
||||
Item *pb.Product
|
||||
Quantity int32
|
||||
Price *pb.Money
|
||||
}
|
||||
items := make([]cartItemView, len(cart))
|
||||
for i, item := range cart {
|
||||
p, err := fe.getProduct(r.Context(), item.GetProductId())
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("could not retrieve product #%s: %+v", item.GetProductId(), err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
price, err := fe.convertCurrency(r.Context(), &pb.Money{
|
||||
Amount: p.GetPriceUsd(),
|
||||
CurrencyCode: defaultCurrency}, currentCurrency(r))
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("could not convert currency for product #%s: %+v", item.GetProductId(), err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
multPrice := multMoney(*price.GetAmount(), uint32(item.GetQuantity()))
|
||||
items[i] = cartItemView{
|
||||
Item: p,
|
||||
Quantity: item.GetQuantity(),
|
||||
Price: &pb.Money{Amount: &multPrice, CurrencyCode: price.GetCurrencyCode()}}
|
||||
}
|
||||
|
||||
if err := templates.ExecuteTemplate(w, "cart", map[string]interface{}{
|
||||
"user_currency": currentCurrency(r),
|
||||
"currencies": currencies,
|
||||
"session_id": sessionID(r),
|
||||
"cart_size": len(cart),
|
||||
"items": items,
|
||||
}); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
|
|
@ -69,9 +69,12 @@ func main() {
|
|||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", ensureSessionID(svc.homeHandler)).Methods(http.MethodGet, http.MethodHead)
|
||||
r.HandleFunc("/product/{id}", ensureSessionID(svc.productHandler)).Methods(http.MethodGet, http.MethodHead)
|
||||
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
|
||||
r.HandleFunc("/logout", svc.logoutHandler).Methods(http.MethodGet)
|
||||
r.HandleFunc("/cart", ensureSessionID(svc.viewCartHandler)).Methods(http.MethodGet, http.MethodHead)
|
||||
r.HandleFunc("/cart", ensureSessionID(svc.addToCartHandler)).Methods(http.MethodPost)
|
||||
r.HandleFunc("/cart/empty", ensureSessionID(svc.emptyCartHandler)).Methods(http.MethodPost)
|
||||
r.HandleFunc("/setCurrency", ensureSessionID(svc.setCurrencyHandler)).Methods(http.MethodPost)
|
||||
r.HandleFunc("/logout", svc.logoutHandler).Methods(http.MethodGet)
|
||||
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
|
||||
log.Printf("starting server on :" + srvPort)
|
||||
log.Fatal(http.ListenAndServe("localhost:"+srvPort, r))
|
||||
}
|
||||
|
|
50
src/frontend/money.go
Normal file
50
src/frontend/money.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
pb "frontend/genproto"
|
||||
)
|
||||
|
||||
// TODO(ahmetb): any logic below is flawed because I just realized we have no
|
||||
// way of representing amounts like 17.07 because Fractional cannot store 07.
|
||||
func multMoney(m pb.MoneyAmount, n uint32) pb.MoneyAmount {
|
||||
out := m
|
||||
for n > 1 {
|
||||
out = sum(out, m)
|
||||
n--
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func sum(m1, m2 pb.MoneyAmount) pb.MoneyAmount {
|
||||
// TODO(ahmetb) this is copied from ./checkoutservice/money.go, find a
|
||||
// better mult function.
|
||||
f1, f2 := float64(m1.Fractional), float64(m2.Fractional)
|
||||
lg1 := math.Max(1, math.Ceil(math.Log10(f1)))
|
||||
if f1 == math.Pow(10, lg1) {
|
||||
lg1++
|
||||
}
|
||||
lg2 := math.Max(1, math.Ceil(math.Log10(f2)))
|
||||
if f2 == math.Pow(10, lg2) {
|
||||
lg2++
|
||||
}
|
||||
lgMax := math.Max(lg1, lg2)
|
||||
|
||||
dSum := m1.Decimal + m2.Decimal
|
||||
o1 := f1 * math.Pow(10, lgMax-lg1)
|
||||
o2 := f2 * math.Pow(10, lgMax-lg2)
|
||||
fSum := o1 + o2
|
||||
if fSum >= math.Pow(10, lgMax) {
|
||||
fSum -= math.Pow(10, lgMax)
|
||||
dSum++
|
||||
}
|
||||
|
||||
for int(fSum)%10 == 0 && fSum != 0 {
|
||||
fSum = float64(int(fSum) / 10)
|
||||
}
|
||||
|
||||
return pb.MoneyAmount{
|
||||
Decimal: dSum,
|
||||
Fractional: uint32(fSum)}
|
||||
}
|
|
@ -50,6 +50,21 @@ func (fe *frontendServer) getCart(ctx context.Context, userID string) ([]*pb.Car
|
|||
return resp.GetItems(), err
|
||||
}
|
||||
|
||||
func (fe *frontendServer) emptyCart(ctx context.Context, userID string) error {
|
||||
_, err := pb.NewCartServiceClient(fe.cartSvcConn).EmptyCart(ctx, &pb.EmptyCartRequest{UserId: userID})
|
||||
return err
|
||||
}
|
||||
|
||||
func (fe *frontendServer) insertCart(ctx context.Context, userID, productID string, quantity int32) error {
|
||||
_, err := pb.NewCartServiceClient(fe.cartSvcConn).AddItem(ctx, &pb.AddItemRequest{
|
||||
UserId: userID,
|
||||
Item: &pb.CartItem{
|
||||
ProductId: productID,
|
||||
Quantity: quantity},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (fe *frontendServer) convertCurrency(ctx context.Context, money *pb.Money, currency string) (*pb.Money, error) {
|
||||
if avoidNoopCurrencyConversionRPC && money.GetCurrencyCode() == currency {
|
||||
return money, nil
|
||||
|
|
53
src/frontend/templates/cart.html
Normal file
53
src/frontend/templates/cart.html
Normal file
|
@ -0,0 +1,53 @@
|
|||
{{ define "cart" }}
|
||||
{{ template "header" . }}
|
||||
|
||||
<main role="main">
|
||||
<div class="py-5">
|
||||
<div class="container bg-light py-3 px-lg-5 py-lg-5">
|
||||
<h1>Shopping Cart</h1>
|
||||
|
||||
{{ if eq (len $.items) 0 }}
|
||||
<p>Your shopping cart is empty.</p>
|
||||
<a class="btn btn-primary" href="/" role="button">Browse Products</a>
|
||||
{{ end }}
|
||||
|
||||
{{ range $.items }}
|
||||
<div class="row pt-2 mb-2">
|
||||
<div class="col text-right">
|
||||
<img class="img-fluid" style="width: auto; max-height: 60px;"
|
||||
src="{{.Item.Picture}}" />
|
||||
</div>
|
||||
<div class="col align-middle">
|
||||
<strong>{{.Item.Name}}</strong><br/>
|
||||
<small class="text-muted">SKU: #{{.Item.Id}}</small>
|
||||
</div>
|
||||
<div class="col text-left">
|
||||
Qty: {{.Quantity}}<br/>
|
||||
<strong>
|
||||
{{.Price.CurrencyCode}}
|
||||
{{.Price.Amount.Decimal}}.
|
||||
{{- .Price.Amount.Fractional}}{{- if lt .Price.Amount.Fractional 10}}0{{end}}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if $.items }}
|
||||
<div class="row mt-5">
|
||||
<div class="col-8 offset-2">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<form method="POST" action="/cart/empty">
|
||||
<button class="btn btn-secondary" type="submit">Empty Cart</button>
|
||||
</form>
|
||||
<a class="btn btn-primary" href="/checkout" role="button">Proceed to Checkout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{ template "footer" . }}
|
||||
{{ end }}
|
|
@ -23,7 +23,7 @@
|
|||
<option value="{{.}}" {{if eq . $.user_currency}}selected="selected"{{end}}>{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<a class="btn btn-primary ml-1" href="/cart" role="button">View Cart ({{$.cart_size}})</a>
|
||||
<a class="btn btn-primary ml-2" href="/cart" role="button">View Cart ({{$.cart_size}})</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
</p>
|
||||
<hr/>
|
||||
|
||||
<form method="POST" action="/addToCart" class="form-inline text-muted">
|
||||
<form method="POST" action="/cart" class="form-inline text-muted">
|
||||
<input type="hidden" name="product_id" value="{{$.product.Item.Id}}"/>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
|
|
Loading…
Reference in a new issue