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 {
|
func sum(m1, m2 pb.MoneyAmount) pb.MoneyAmount {
|
||||||
f1, f2 := float64(m1.Fractional), float64(m2.Fractional)
|
f1, f2 := float64(m1.Fractional), float64(m2.Fractional)
|
||||||
lg1 := math.Max(1, math.Ceil(math.Log10(f1)))
|
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)))
|
lg2 := math.Max(1, math.Ceil(math.Log10(f2)))
|
||||||
|
if f2 == math.Pow(10, lg2) {
|
||||||
|
lg2++
|
||||||
|
}
|
||||||
lgMax := math.Max(lg1, lg2)
|
lgMax := math.Max(lg1, lg2)
|
||||||
|
|
||||||
dSum := m1.Decimal + m2.Decimal
|
dSum := m1.Decimal + m2.Decimal
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"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) {
|
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())
|
currencies, err := fe.getCurrencies(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("could not retrieve currencies: %+v", err), http.StatusInternalServerError)
|
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)
|
http.Error(w, fmt.Sprintf("could not retrieve product: %+v", err), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
currencies, err := fe.getCurrencies(r.Context())
|
currencies, err := fe.getCurrencies(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("could not retrieve currencies: %+v", err), http.StatusInternalServerError)
|
http.Error(w, fmt.Sprintf("could not retrieve currencies: %+v", err), http.StatusInternalServerError)
|
||||||
return
|
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{
|
price, err := fe.convertCurrency(r.Context(), &pb.Money{
|
||||||
Amount: p.GetPriceUsd(),
|
Amount: p.GetPriceUsd(),
|
||||||
|
@ -125,6 +130,92 @@ func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request)
|
||||||
"currencies": currencies,
|
"currencies": currencies,
|
||||||
"product": product,
|
"product": product,
|
||||||
"session_id": sessionID(r),
|
"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 {
|
}); err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,9 +69,12 @@ func main() {
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
r.HandleFunc("/", ensureSessionID(svc.homeHandler)).Methods(http.MethodGet, http.MethodHead)
|
r.HandleFunc("/", ensureSessionID(svc.homeHandler)).Methods(http.MethodGet, http.MethodHead)
|
||||||
r.HandleFunc("/product/{id}", ensureSessionID(svc.productHandler)).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("/cart", ensureSessionID(svc.viewCartHandler)).Methods(http.MethodGet, http.MethodHead)
|
||||||
r.HandleFunc("/logout", svc.logoutHandler).Methods(http.MethodGet)
|
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("/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.Printf("starting server on :" + srvPort)
|
||||||
log.Fatal(http.ListenAndServe("localhost:"+srvPort, r))
|
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
|
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) {
|
func (fe *frontendServer) convertCurrency(ctx context.Context, money *pb.Money, currency string) (*pb.Money, error) {
|
||||||
if avoidNoopCurrencyConversionRPC && money.GetCurrencyCode() == currency {
|
if avoidNoopCurrencyConversionRPC && money.GetCurrencyCode() == currency {
|
||||||
return money, nil
|
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>
|
<option value="{{.}}" {{if eq . $.user_currency}}selected="selected"{{end}}>{{.}}</option>
|
||||||
{{end}}
|
{{end}}
|
||||||
</select>
|
</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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
</p>
|
</p>
|
||||||
<hr/>
|
<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}}"/>
|
<input type="hidden" name="product_id" value="{{$.product.Item.Id}}"/>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
|
|
Loading…
Add table
Reference in a new issue