frontend: view cart, add to cart, empty cart

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
This commit is contained in:
Ahmet Alp Balkan 2018-06-26 12:54:36 -07:00
parent f3fe6d42ad
commit 289bd4db13
8 changed files with 224 additions and 6 deletions

View file

@ -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

View file

@ -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)
}

View file

@ -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
View 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)}
}

View file

@ -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

View 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 }}

View file

@ -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>

View file

@ -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">