frontend: place order
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
This commit is contained in:
parent
3eb02c0679
commit
483e113643
6 changed files with 224 additions and 47 deletions
|
@ -48,7 +48,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 currency=%s", sessionID(r), currentCurrency(r))
|
log.Printf("[home] session_id=%s 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)
|
||||||
|
@ -150,7 +150,7 @@ func (fe *frontendServer) addToCartHandler(w http.ResponseWriter, r *http.Reques
|
||||||
http.Error(w, "invalid form input", http.StatusBadRequest)
|
http.Error(w, "invalid form input", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("[addToCart] product_id=%s qty=%d session_id=%+v", productID, quantity, sessionID(r))
|
log.Printf("[addToCart] product_id=%s qty=%d session_id=%s", productID, quantity, sessionID(r))
|
||||||
|
|
||||||
p, err := fe.getProduct(r.Context(), productID)
|
p, err := fe.getProduct(r.Context(), productID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -167,7 +167,7 @@ func (fe *frontendServer) addToCartHandler(w http.ResponseWriter, r *http.Reques
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fe *frontendServer) emptyCartHandler(w http.ResponseWriter, r *http.Request) {
|
func (fe *frontendServer) emptyCartHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("[emptyCart] session_id=%+v", sessionID(r))
|
log.Printf("[emptyCart] session_id=%s", sessionID(r))
|
||||||
|
|
||||||
if err := fe.emptyCart(r.Context(), sessionID(r)); err != nil {
|
if err := fe.emptyCart(r.Context(), sessionID(r)); err != nil {
|
||||||
http.Error(w, fmt.Sprintf("failed to empty cart: %+v", err), http.StatusInternalServerError)
|
http.Error(w, fmt.Sprintf("failed to empty cart: %+v", err), http.StatusInternalServerError)
|
||||||
|
@ -178,7 +178,7 @@ func (fe *frontendServer) emptyCartHandler(w http.ResponseWriter, r *http.Reques
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request) {
|
func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("[viewCart] session_id=%+v", sessionID(r))
|
log.Printf("[viewCart] session_id=%s", sessionID(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)
|
||||||
|
@ -196,12 +196,19 @@ func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shippingCost, err := fe.getShippingQuote(r.Context(), cart, currentCurrency(r))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to get shipping quote: %+v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type cartItemView struct {
|
type cartItemView struct {
|
||||||
Item *pb.Product
|
Item *pb.Product
|
||||||
Quantity int32
|
Quantity int32
|
||||||
Price *pb.Money
|
Price *pb.Money
|
||||||
}
|
}
|
||||||
items := make([]cartItemView, len(cart))
|
items := make([]cartItemView, len(cart))
|
||||||
|
totalPrice := pb.Money{CurrencyCode: currentCurrency(r)}
|
||||||
for i, item := range cart {
|
for i, item := range cart {
|
||||||
p, err := fe.getProduct(r.Context(), item.GetProductId())
|
p, err := fe.getProduct(r.Context(), item.GetProductId())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -219,15 +226,73 @@ func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request
|
||||||
Item: p,
|
Item: p,
|
||||||
Quantity: item.GetQuantity(),
|
Quantity: item.GetQuantity(),
|
||||||
Price: &multPrice}
|
Price: &multPrice}
|
||||||
|
totalPrice = money.Must(money.Sum(totalPrice, multPrice))
|
||||||
}
|
}
|
||||||
|
totalPrice = money.Must(money.Sum(totalPrice, *shippingCost))
|
||||||
|
|
||||||
|
year := time.Now().Year()
|
||||||
if err := templates.ExecuteTemplate(w, "cart", map[string]interface{}{
|
if err := templates.ExecuteTemplate(w, "cart", map[string]interface{}{
|
||||||
"user_currency": currentCurrency(r),
|
"user_currency": currentCurrency(r),
|
||||||
"currencies": currencies,
|
"currencies": currencies,
|
||||||
|
"session_id": sessionID(r),
|
||||||
|
"recommendations": recommendations,
|
||||||
|
"cart_size": len(cart),
|
||||||
|
"shipping_cost": shippingCost,
|
||||||
|
"total_cost": totalPrice,
|
||||||
|
"items": items,
|
||||||
|
"expiration_years": []int{year, year + 1, year + 2, year + 3, year + 4},
|
||||||
|
}); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fe *frontendServer) placeOrderHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Printf("[checkout] session_id=%s", sessionID(r))
|
||||||
|
|
||||||
|
var (
|
||||||
|
email = r.FormValue("email")
|
||||||
|
streetAddress = r.FormValue("street_address")
|
||||||
|
zipCode, _ = strconv.ParseInt(r.FormValue("zip_code"), 10, 32)
|
||||||
|
city = r.FormValue("city")
|
||||||
|
state = r.FormValue("state")
|
||||||
|
country = r.FormValue("country")
|
||||||
|
ccNumber = r.FormValue("credit_card_number")
|
||||||
|
ccMonth, _ = strconv.ParseInt(r.FormValue("credit_card_expiration_month"), 10, 32)
|
||||||
|
ccYear, _ = strconv.ParseInt(r.FormValue("credit_card_expiration_year"), 10, 32)
|
||||||
|
ccCVV, _ = strconv.ParseInt(r.FormValue("credit_card_cvv"), 10, 32)
|
||||||
|
)
|
||||||
|
|
||||||
|
order, err := pb.NewCheckoutServiceClient(fe.checkoutSvcConn).
|
||||||
|
PlaceOrder(r.Context(), &pb.PlaceOrderRequest{
|
||||||
|
Email: email,
|
||||||
|
CreditCard: &pb.CreditCardInfo{
|
||||||
|
CreditCardNumber: ccNumber,
|
||||||
|
CreditCardExpirationMonth: int32(ccMonth),
|
||||||
|
CreditCardExpirationYear: int32(ccYear),
|
||||||
|
CreditCardCvv: int32(ccCVV)},
|
||||||
|
UserId: sessionID(r),
|
||||||
|
UserCurrency: currentCurrency(r),
|
||||||
|
Address: &pb.Address{
|
||||||
|
StreetAddress: streetAddress,
|
||||||
|
City: city,
|
||||||
|
State: state,
|
||||||
|
ZipCode: int32(zipCode),
|
||||||
|
Country: country},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to complete the order: %+v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("order #%s completed", order.GetOrder().GetOrderId())
|
||||||
|
|
||||||
|
order.GetOrder().GetItems()
|
||||||
|
recommendations, _ := fe.getRecommendations(r.Context(), sessionID(r), nil)
|
||||||
|
|
||||||
|
if err := templates.ExecuteTemplate(w, "order", map[string]interface{}{
|
||||||
"session_id": sessionID(r),
|
"session_id": sessionID(r),
|
||||||
|
"user_currency": currentCurrency(r),
|
||||||
|
"order": order.GetOrder(),
|
||||||
"recommendations": recommendations,
|
"recommendations": recommendations,
|
||||||
"cart_size": len(cart),
|
|
||||||
"items": items,
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
|
@ -256,7 +321,7 @@ func (fe *frontendServer) prepareCheckoutHandler(w http.ResponseWriter, r *http.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fe *frontendServer) logoutHandler(w http.ResponseWriter, r *http.Request) {
|
func (fe *frontendServer) logoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("[home] session_id=%+v", sessionID(r))
|
log.Printf("[logout] session_id=%s", sessionID(r))
|
||||||
for _, c := range r.Cookies() {
|
for _, c := range r.Cookies() {
|
||||||
c.Expires = time.Now().Add(-time.Hour * 24 * 365)
|
c.Expires = time.Now().Add(-time.Hour * 24 * 365)
|
||||||
c.MaxAge = -1
|
c.MaxAge = -1
|
||||||
|
@ -268,7 +333,7 @@ func (fe *frontendServer) logoutHandler(w http.ResponseWriter, r *http.Request)
|
||||||
|
|
||||||
func (fe *frontendServer) setCurrencyHandler(w http.ResponseWriter, r *http.Request) {
|
func (fe *frontendServer) setCurrencyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
cur := r.FormValue("currency_code")
|
cur := r.FormValue("currency_code")
|
||||||
log.Printf("[setCurrency] session_id=%+v code=%s", sessionID(r), cur)
|
log.Printf("[setCurrency] session_id=%s code=%s", sessionID(r), cur)
|
||||||
if cur != "" {
|
if cur != "" {
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: cookieCurrency,
|
Name: cookieCurrency,
|
||||||
|
|
|
@ -43,6 +43,9 @@ type frontendServer struct {
|
||||||
|
|
||||||
checkoutSvcAddr string
|
checkoutSvcAddr string
|
||||||
checkoutSvcConn *grpc.ClientConn
|
checkoutSvcConn *grpc.ClientConn
|
||||||
|
|
||||||
|
shippingSvcAddr string
|
||||||
|
shippingSvcConn *grpc.ClientConn
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -59,6 +62,7 @@ func main() {
|
||||||
mustMapEnv(&svc.cartSvcAddr, "CART_SERVICE_ADDR")
|
mustMapEnv(&svc.cartSvcAddr, "CART_SERVICE_ADDR")
|
||||||
mustMapEnv(&svc.recommendationSvcAddr, "RECOMMENDATION_SERVICE_ADDR")
|
mustMapEnv(&svc.recommendationSvcAddr, "RECOMMENDATION_SERVICE_ADDR")
|
||||||
mustMapEnv(&svc.checkoutSvcAddr, "CHECKOUT_SERVICE_ADDR")
|
mustMapEnv(&svc.checkoutSvcAddr, "CHECKOUT_SERVICE_ADDR")
|
||||||
|
mustMapEnv(&svc.shippingSvcAddr, "SHIPPING_SERVICE_ADDR")
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
svc.currencySvcConn, err = grpc.DialContext(ctx, svc.currencySvcAddr, grpc.WithInsecure())
|
svc.currencySvcConn, err = grpc.DialContext(ctx, svc.currencySvcAddr, grpc.WithInsecure())
|
||||||
|
@ -77,6 +81,14 @@ func main() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to connect recommendation service at %s: %+v", svc.recommendationSvcAddr, err)
|
log.Fatalf("failed to connect recommendation service at %s: %+v", svc.recommendationSvcAddr, err)
|
||||||
}
|
}
|
||||||
|
svc.shippingSvcConn, err = grpc.DialContext(ctx, svc.shippingSvcAddr, grpc.WithInsecure())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to connect shipping service at %s: %+v", svc.shippingSvcAddr, err)
|
||||||
|
}
|
||||||
|
svc.checkoutSvcConn, err = grpc.DialContext(ctx, svc.checkoutSvcAddr, grpc.WithInsecure())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to connect checkout service at %s: %+v", svc.checkoutSvcAddr, err)
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
@ -86,7 +98,7 @@ func main() {
|
||||||
r.HandleFunc("/cart/empty", ensureSessionID(svc.emptyCartHandler)).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.HandleFunc("/logout", svc.logoutHandler).Methods(http.MethodGet)
|
||||||
r.HandleFunc("/checkout", ensureSessionID(svc.prepareCheckoutHandler)).Methods(http.MethodGet, http.MethodHead)
|
r.HandleFunc("/cart/checkout", ensureSessionID(svc.placeOrderHandler)).Methods(http.MethodPost)
|
||||||
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
|
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))
|
||||||
|
|
|
@ -76,6 +76,21 @@ func (fe *frontendServer) convertCurrency(ctx context.Context, money *pb.Money,
|
||||||
ToCode: currency})
|
ToCode: currency})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fe *frontendServer) getShippingQuote(ctx context.Context, items []*pb.CartItem, currency string) (*pb.Money, error) {
|
||||||
|
quote, err := pb.NewShippingServiceClient(fe.currencySvcConn).GetQuote(ctx,
|
||||||
|
&pb.GetQuoteRequest{
|
||||||
|
Address: nil,
|
||||||
|
Items: items})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
localized, err := fe.convertCurrency(ctx, quote.GetCostUsd(), currency)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert currency for shipping cost: %+v", err)
|
||||||
|
}
|
||||||
|
return localized, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (fe *frontendServer) getRecommendations(ctx context.Context, userID string, productIDs []string) ([]*pb.Product, error) {
|
func (fe *frontendServer) getRecommendations(ctx context.Context, userID string, productIDs []string) ([]*pb.Product, error) {
|
||||||
resp, err := pb.NewRecommendationServiceClient(fe.recommendationSvcConn).ListRecommendations(ctx,
|
resp, err := pb.NewRecommendationServiceClient(fe.recommendationSvcConn).ListRecommendations(ctx,
|
||||||
&pb.ListRecommendationsRequest{UserId: userID, ProductIds: productIDs})
|
&pb.ListRecommendationsRequest{UserId: userID, ProductIds: productIDs})
|
||||||
|
|
|
@ -10,10 +10,21 @@
|
||||||
<a class="btn btn-primary" href="/" role="button">Browse Products → </a>
|
<a class="btn btn-primary" href="/" role="button">Browse Products → </a>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
|
|
||||||
<h3>{{ len $.items }} item {{- if gt (len $.items) 1}}s{{end}}
|
<div class="row mb-3 py-2">
|
||||||
in your Shopping Cart</h3> <form method="POST" action="/cart/empty">
|
<div class="col">
|
||||||
<button class="btn btn-secondary" type="submit">Empty Cart</button>
|
<h3>{{ len $.items }} item
|
||||||
</form>
|
{{- if gt (len $.items) 1}}s{{end}}
|
||||||
|
in your Shopping Cart</h3>
|
||||||
|
</div>
|
||||||
|
<div class="col text-right">
|
||||||
|
<form method="POST" action="/cart/empty">
|
||||||
|
<button class="btn btn-secondary" type="submit">Empty cart</button>
|
||||||
|
<a class="btn btn-info" href="/" role="button">Browse more products → </a>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
|
||||||
{{ range $.items }}
|
{{ range $.items }}
|
||||||
<div class="row pt-2 mb-2">
|
<div class="row pt-2 mb-2">
|
||||||
|
@ -33,45 +44,97 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }} <!-- range $.items-->
|
{{ end }} <!-- range $.items-->
|
||||||
|
<div class="row pt-2 my-3">
|
||||||
|
<div class="col text-center">
|
||||||
|
<p class="text-muted my-0">Shipping Cost: <strong>{{ renderMoney .shipping_cost }}</strong></p>
|
||||||
|
Total Cost: <strong>{{ renderMoney .total_cost }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
<div class="row py-3 my-2">
|
<div class="row py-3 my-2">
|
||||||
<div class="col-12 col-lg-8 offset-lg-2">
|
<div class="col-12 col-lg-8 offset-lg-2">
|
||||||
<h3>Prepare to checkout</h3>
|
<h3>Checkout</h3>
|
||||||
<form class="needs-validation">
|
<form action="/cart/checkout" method="POST">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="col-md-5 mb-3">
|
<div class="col-md-5 mb-4">
|
||||||
<label for="street_address_1">Street Address</label>
|
<label for="email">E-mail Address</label>
|
||||||
<input type="text" class="form-control" id="street_address_1"
|
<input type="email" class="form-control" id="email"
|
||||||
value="1600 Amphitheatre Parkway" required>
|
name="email" value="someone@example.com" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5 mb-4">
|
||||||
|
<label for="street_address">Street Address</label>
|
||||||
|
<input type="text" class="form-control" name="street_address"
|
||||||
|
id="street_address" value="1600 Amphitheatre Parkway" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 mb-4">
|
||||||
|
<label for="zip_code">Zip Code</label>
|
||||||
|
<input type="text" class="form-control"
|
||||||
|
name="zip_code" id="zip_code" value="94043" required pattern="\d{4,5}">
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-5 mb-3">
|
<div class="form-row">
|
||||||
<label for="street_address_2">Street Address (cont.)</label>
|
<div class="col-md-5 mb-3">
|
||||||
<input type="text" class="form-control" id="street_address_2" placeholder="...your address here">
|
<label for="city">City</label>
|
||||||
|
<input type="text" class="form-control" name="city" id="city"
|
||||||
|
value="Mountain View" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 mb-3">
|
||||||
|
<label for="state">State</label>
|
||||||
|
<input type="text" class="form-control" name="state" id="state"
|
||||||
|
value="CA" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5 mb-3">
|
||||||
|
<label for="country">Country</label>
|
||||||
|
<input type="text" class="form-control" id="country"
|
||||||
|
placeholder="Country Name"
|
||||||
|
name="country" value="United States" required>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2 mb-3">
|
<div class="form-row">
|
||||||
<label for="zip_code">Zip Code</label>
|
<div class="col-md-6 mb-3">
|
||||||
<input type="text" class="form-control" id="zip_code" value="94043" required>
|
<label for="credit_card_number">Credit Card Number</label>
|
||||||
|
<input type="text" class="form-control" id="credit_card_number"
|
||||||
|
name="credit_card_number"
|
||||||
|
placeholder="0000-0000-0000-0000"
|
||||||
|
value="4432-8015-6152-0454"
|
||||||
|
required pattern="\d{4}-\d{4}-\d{4}-\d{4}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 mb-3">
|
||||||
|
<label for="credit_card_expiration_month">Month</label>
|
||||||
|
<select name="credit_card_expiration_month" id="credit_card_expiration_month"
|
||||||
|
class="form-control">
|
||||||
|
<option value="1">January</option>
|
||||||
|
<option value="2">February</option>
|
||||||
|
<option value="3">March</option>
|
||||||
|
<option value="4">April</option>
|
||||||
|
<option value="5">May</option>
|
||||||
|
<option value="6">June</option>
|
||||||
|
<option value="7">July</option>
|
||||||
|
<option value="8">August</option>
|
||||||
|
<option value="9">September</option>
|
||||||
|
<option value="10">October</option>
|
||||||
|
<option value="11">November</option>
|
||||||
|
<option value="12">January</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 mb-3">
|
||||||
|
<label for="credit_card_expiration_year">Year</label>
|
||||||
|
<select name="credit_card_expiration_year" id="credit_card_expiration_year"
|
||||||
|
class="form-control">
|
||||||
|
{{range $.expiration_years}}<option value="{{.}}">{{.}}</option>{{end}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 mb-3">
|
||||||
|
<label for="credit_card_cvv">CVV</label>
|
||||||
|
<input type="text" class="form-control" id="credit_card_cvv"
|
||||||
|
name="credit_card_cvv" value="672" required pattern="\d{3}">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="form-row">
|
||||||
<div class="form-row">
|
<button class="btn btn-primary" type="submit">Place your order →</button>
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label for="city">City</label>
|
|
||||||
<input type="text" class="form-control" id="city" placeholder="City" value="Mountain View" required>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 mb-3">
|
|
||||||
<label for="state">State</label>
|
|
||||||
<input type="text" class="form-control" id="state" placeholder="State" value="CA" required>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 mb-3">
|
|
||||||
<label for="country">Country</label>
|
|
||||||
<input type="text" class="form-control" id="country" placeholder="Country Name"
|
|
||||||
value="United States" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
|
||||||
<a class="btn btn-primary" href="/checkout" role="button">Proceed to Checkout →</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
<a href="/" class="navbar-brand d-flex align-items-center">
|
<a href="/" class="navbar-brand d-flex align-items-center">
|
||||||
Hipster Shop
|
Hipster Shop
|
||||||
</a>
|
</a>
|
||||||
|
{{ if $.currencies }}
|
||||||
<form class="form-inline ml-auto" method="POST" action="/setCurrency" id="currency_form">
|
<form class="form-inline ml-auto" method="POST" action="/setCurrency" id="currency_form">
|
||||||
<select name="currency_code" class="form-control"
|
<select name="currency_code" class="form-control"
|
||||||
onchange="document.getElementById('currency_form').submit();">
|
onchange="document.getElementById('currency_form').submit();">
|
||||||
|
@ -25,6 +26,7 @@
|
||||||
</select>
|
</select>
|
||||||
<a class="btn btn-primary btn-light ml-2" href="/cart" role="button">View Cart ({{$.cart_size}})</a>
|
<a class="btn btn-primary btn-light ml-2" href="/cart" role="button">View Cart ({{$.cart_size}})</a>
|
||||||
</form>
|
</form>
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
20
src/frontend/templates/order.html
Normal file
20
src/frontend/templates/order.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{{ define "order" }}
|
||||||
|
{{ template "header" . }}
|
||||||
|
|
||||||
|
<main role="main">
|
||||||
|
<div class="py-5">
|
||||||
|
<div class="container bg-light py-3 px-lg-5 py-lg-5">
|
||||||
|
<h3>
|
||||||
|
Your order is complete!
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
Order Confirmation ID: {{ $.order }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{ template "recommendations" $.recommendations }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{{ template "footer" . }}
|
||||||
|
{{ end }}
|
Loading…
Reference in a new issue