frontend: integrate recommmendationservice
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
This commit is contained in:
parent
9889e4904d
commit
400d51a9fe
7 changed files with 99 additions and 17 deletions
|
@ -95,7 +95,7 @@ func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request)
|
||||||
http.Error(w, "product id not specified", http.StatusBadRequest)
|
http.Error(w, "product id not specified", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("[productHandler] id=%s currency=%s", id, currentCurrency(r))
|
log.Printf("[productHandler] id=%s currency=%s session=%s", id, currentCurrency(r), sessionID(r))
|
||||||
p, err := fe.getProduct(r.Context(), id)
|
p, err := fe.getProduct(r.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
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)
|
||||||
|
@ -106,6 +106,7 @@ func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request)
|
||||||
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))
|
cart, err := fe.getCart(r.Context(), sessionID(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("could not retrieve cart: %+v", err), http.StatusInternalServerError)
|
http.Error(w, fmt.Sprintf("could not retrieve cart: %+v", err), http.StatusInternalServerError)
|
||||||
|
@ -120,6 +121,13 @@ func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recommendations, err := fe.getRecommendations(r.Context(), sessionID(r), []string{id})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to get product recommendations: %+v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("cart size=%d", len(cart))
|
||||||
|
|
||||||
product := struct {
|
product := struct {
|
||||||
Item *pb.Product
|
Item *pb.Product
|
||||||
Price *pb.Money
|
Price *pb.Money
|
||||||
|
@ -130,6 +138,7 @@ 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),
|
||||||
|
"recommendations": recommendations,
|
||||||
"cart_size": len(cart),
|
"cart_size": len(cart),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
|
@ -183,6 +192,12 @@ func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recommendations, err := fe.getRecommendations(r.Context(), sessionID(r), cartIDs(cart))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to get product recommendations: %+v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type cartItemView struct {
|
type cartItemView struct {
|
||||||
Item *pb.Product
|
Item *pb.Product
|
||||||
Quantity int32
|
Quantity int32
|
||||||
|
@ -214,6 +229,7 @@ func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request
|
||||||
"user_currency": currentCurrency(r),
|
"user_currency": currentCurrency(r),
|
||||||
"currencies": currencies,
|
"currencies": currencies,
|
||||||
"session_id": sessionID(r),
|
"session_id": sessionID(r),
|
||||||
|
"recommendations": recommendations,
|
||||||
"cart_size": len(cart),
|
"cart_size": len(cart),
|
||||||
"items": items,
|
"items": items,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
@ -265,3 +281,11 @@ func sessionID(r *http.Request) string {
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cartIDs(c []*pb.CartItem) []string {
|
||||||
|
out := make([]string, len(c))
|
||||||
|
for i, v := range c {
|
||||||
|
out[i] = v.GetProductId()
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
|
@ -37,6 +37,9 @@ type frontendServer struct {
|
||||||
|
|
||||||
cartSvcAddr string
|
cartSvcAddr string
|
||||||
cartSvcConn *grpc.ClientConn
|
cartSvcConn *grpc.ClientConn
|
||||||
|
|
||||||
|
recommendationSvcAddr string
|
||||||
|
recommendationSvcConn *grpc.ClientConn
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -51,6 +54,7 @@ func main() {
|
||||||
mustMapEnv(&svc.productCatalogSvcAddr, "PRODUCT_CATALOG_SERVICE_ADDR")
|
mustMapEnv(&svc.productCatalogSvcAddr, "PRODUCT_CATALOG_SERVICE_ADDR")
|
||||||
mustMapEnv(&svc.currencySvcAddr, "CURRENCY_SERVICE_ADDR")
|
mustMapEnv(&svc.currencySvcAddr, "CURRENCY_SERVICE_ADDR")
|
||||||
mustMapEnv(&svc.cartSvcAddr, "CART_SERVICE_ADDR")
|
mustMapEnv(&svc.cartSvcAddr, "CART_SERVICE_ADDR")
|
||||||
|
mustMapEnv(&svc.recommendationSvcAddr, "RECOMMENDATION_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())
|
||||||
|
@ -65,6 +69,10 @@ func main() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to connect cart service at %s: %+v", svc.cartSvcAddr, err)
|
log.Fatalf("failed to connect cart service at %s: %+v", svc.cartSvcAddr, err)
|
||||||
}
|
}
|
||||||
|
svc.recommendationSvcConn, err = grpc.DialContext(ctx, svc.recommendationSvcAddr, grpc.WithInsecure())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to connect recommendation service at %s: %+v", svc.recommendationSvcConn, 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)
|
||||||
|
|
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
|
|
||||||
|
@ -74,3 +75,23 @@ func (fe *frontendServer) convertCurrency(ctx context.Context, money *pb.Money,
|
||||||
From: money,
|
From: money,
|
||||||
ToCode: currency})
|
ToCode: currency})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fe *frontendServer) getRecommendations(ctx context.Context, userID string, productIDs []string) ([]*pb.Product, error) {
|
||||||
|
resp, err := pb.NewRecommendationServiceClient(fe.recommendationSvcConn).ListRecommendations(ctx,
|
||||||
|
&pb.ListRecommendationsRequest{UserId: userID, ProductIds: productIDs})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := make([]*pb.Product, len(resp.GetProductIds()))
|
||||||
|
for i, v := range resp.GetProductIds() {
|
||||||
|
p, err := fe.getProduct(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get recommended product info (#%s): %+v", v, err)
|
||||||
|
}
|
||||||
|
out[i] = p
|
||||||
|
}
|
||||||
|
if len(out) > 4 {
|
||||||
|
out = out[:4] // take only first four to fit the UI
|
||||||
|
}
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
|
|
@ -14,8 +14,8 @@
|
||||||
{{ range $.items }}
|
{{ range $.items }}
|
||||||
<div class="row pt-2 mb-2">
|
<div class="row pt-2 mb-2">
|
||||||
<div class="col text-right">
|
<div class="col text-right">
|
||||||
<img class="img-fluid" style="width: auto; max-height: 60px;"
|
<a href="/product/{{.Item.Id}}"><img class="img-fluid" style="width: auto; max-height: 60px;"
|
||||||
src="{{.Item.Picture}}" />
|
src="{{.Item.Picture}}" /></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col align-middle">
|
<div class="col align-middle">
|
||||||
<strong>{{.Item.Name}}</strong><br/>
|
<strong>{{.Item.Name}}</strong><br/>
|
||||||
|
@ -45,6 +45,11 @@
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if $.recommendations}}
|
||||||
|
<hr/>
|
||||||
|
{{ template "recommendations" $.recommendations }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
{{ define "footer" }}
|
{{ define "footer" }}
|
||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
|
||||||
|
<small class="text-muted mx-auto">This is a demo application.</small>
|
||||||
session-id: {{$.session_id}}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<div class="container bg-light py-3 px-lg-5 py-lg-5">
|
<div class="container bg-light py-3 px-lg-5 py-lg-5">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-5">
|
<div class="col-12 col-lg-5">
|
||||||
<img class="img-fluid" style="width: 100%;"
|
<img class="img-fluid border" style="width: 100%;"
|
||||||
src="{{$.product.Item.Picture}}" />
|
src="{{$.product.Item.Picture}}" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-7">
|
<div class="col-12 col-lg-7">
|
||||||
|
@ -43,6 +43,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{ if $.recommendations}}
|
||||||
|
<hr/>
|
||||||
|
{{ template "recommendations" $.recommendations }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
21
src/frontend/templates/recommendations.html
Normal file
21
src/frontend/templates/recommendations.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{{ define "recommendations" }}
|
||||||
|
<h5 class="text-muted">Similar products</h5>
|
||||||
|
<div class="row">
|
||||||
|
{{range . }}
|
||||||
|
<div class="col-3">
|
||||||
|
<div class="card mb-3 box-shadow">
|
||||||
|
<a href="/product/{{.Id}}">
|
||||||
|
<img class="card-img-top border-bottom" alt =""
|
||||||
|
style="width: 100%; height: auto;"
|
||||||
|
src="{{.Picture}}">
|
||||||
|
</a>
|
||||||
|
<div class="card-body text-center py-2">
|
||||||
|
<small class="card-title text-muted">
|
||||||
|
{{ .Name }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
Loading…
Reference in a new issue