initial commit
Change-Id: I12a20fc994c2a94df96de9d3393b06bf6687f77a
1
src/frontend/.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
vendor/
|
||||
0
src/frontend/.gitkeep
Normal file
44
src/frontend/Dockerfile
Normal file
@@ -0,0 +1,44 @@
|
||||
# Copyright 2020 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Define a default value so it's not empty if the builder fails to provide it
|
||||
ARG BUILDPLATFORM=linux/amd64
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS builder
|
||||
ARG TARGETOS=linux
|
||||
ARG TARGETARCH=amd64
|
||||
WORKDIR /src
|
||||
|
||||
# restore dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
|
||||
# Skaffold passes in debug-oriented compiler flags
|
||||
ARG SKAFFOLD_GO_GCFLAGS
|
||||
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -ldflags="-s -w" -gcflags="${SKAFFOLD_GO_GCFLAGS}" -o /go/bin/frontend .
|
||||
|
||||
FROM gcr.io/distroless/static
|
||||
WORKDIR /src
|
||||
COPY --from=builder /go/bin/frontend /src/server
|
||||
COPY ./templates ./templates
|
||||
COPY ./static ./static
|
||||
|
||||
# Definition of this variable is used by 'skaffold debug' to identify a golang binary.
|
||||
# Default behavior - a failure prints a stack trace for the current goroutine.
|
||||
# See https://golang.org/pkg/runtime/
|
||||
ENV GOTRACEBACK=single
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/src/server"]
|
||||
5
src/frontend/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# frontend
|
||||
|
||||
Run the following command to restore dependencies to `vendor/` directory:
|
||||
|
||||
dep ensure --vendor-only
|
||||
64
src/frontend/deployment_details.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var deploymentDetailsMap map[string]string
|
||||
var log *logrus.Logger
|
||||
|
||||
func init() {
|
||||
initializeLogger()
|
||||
// Use a goroutine to ensure loadDeploymentDetails()'s GCP API
|
||||
// calls don't block non-GCP deployments. See issue #685.
|
||||
go loadDeploymentDetails()
|
||||
}
|
||||
|
||||
func initializeLogger() {
|
||||
log = logrus.New()
|
||||
log.Level = logrus.DebugLevel
|
||||
log.Formatter = &logrus.JSONFormatter{
|
||||
FieldMap: logrus.FieldMap{
|
||||
logrus.FieldKeyTime: "timestamp",
|
||||
logrus.FieldKeyLevel: "severity",
|
||||
logrus.FieldKeyMsg: "message",
|
||||
},
|
||||
TimestampFormat: time.RFC3339Nano,
|
||||
}
|
||||
log.Out = os.Stdout
|
||||
}
|
||||
|
||||
func loadDeploymentDetails() {
|
||||
deploymentDetailsMap = make(map[string]string)
|
||||
var metaServerClient = metadata.NewClient(&http.Client{})
|
||||
|
||||
podHostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
log.Error("Failed to fetch the hostname for the Pod", err)
|
||||
}
|
||||
|
||||
podCluster, err := metaServerClient.InstanceAttributeValue("cluster-name")
|
||||
if err != nil {
|
||||
log.Error("Failed to fetch the name of the cluster in which the pod is running", err)
|
||||
}
|
||||
|
||||
podZone, err := metaServerClient.Zone()
|
||||
if err != nil {
|
||||
log.Error("Failed to fetch the Zone of the node where the pod is scheduled", err)
|
||||
}
|
||||
|
||||
deploymentDetailsMap["HOSTNAME"] = podHostname
|
||||
deploymentDetailsMap["CLUSTERNAME"] = podCluster
|
||||
deploymentDetailsMap["ZONE"] = podZone
|
||||
|
||||
log.WithFields(logrus.Fields{
|
||||
"cluster": podCluster,
|
||||
"zone": podZone,
|
||||
"hostname": podHostname,
|
||||
}).Debug("Loaded deployment details")
|
||||
}
|
||||
25
src/frontend/genproto.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash -eu
|
||||
#
|
||||
# Copyright 2018 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# [START gke_frontend_genproto]
|
||||
|
||||
PATH=$PATH:$(go env GOPATH)/bin
|
||||
protodir=../../protos
|
||||
outdir=./genproto
|
||||
|
||||
protoc --proto_path=$protodir --go_out=./$outdir --go_opt=paths=source_relative --go-grpc_out=./$outdir --go-grpc_opt=paths=source_relative $protodir/demo.proto
|
||||
|
||||
# [END gke_frontend_genproto]
|
||||
2610
src/frontend/genproto/demo.pb.go
Normal file
1179
src/frontend/genproto/demo_grpc.pb.go
Normal file
58
src/frontend/go.mod
Normal file
@@ -0,0 +1,58 @@
|
||||
module github.com/GoogleCloudPlatform/microservices-demo/src/frontend
|
||||
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.26.1
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute/metadata v0.9.0
|
||||
cloud.google.com/go/profiler v0.4.3
|
||||
github.com/go-playground/validator/v10 v10.30.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0
|
||||
go.opentelemetry.io/otel v1.42.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0
|
||||
go.opentelemetry.io/otel/sdk v1.42.0
|
||||
google.golang.org/grpc v1.79.3
|
||||
google.golang.org/protobuf v1.36.11
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.42.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/oauth2 v0.35.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/api v0.256.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20251124214823-79d6a2a48846 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
)
|
||||
146
src/frontend/go.sum
Normal file
@@ -0,0 +1,146 @@
|
||||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
|
||||
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
||||
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
||||
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
|
||||
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
|
||||
cloud.google.com/go/profiler v0.4.3 h1:IY3QNKlr8VbXwGWHcZbJQsMA/83ZTH6uAHf8jYyj7OI=
|
||||
cloud.google.com/go/profiler v0.4.3/go.mod h1:3xFodugWfPIQZWFcXdUmfa+yTiiyQ8fWrdT+d2Sg4J0=
|
||||
cloud.google.com/go/storage v1.56.0 h1:iixmq2Fse2tqxMbWhLWC9HfBj1qdxqAmiK8/eqtsLxI=
|
||||
cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
|
||||
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 h1:3DsUAV+VNEQa2CUVLxCY3f87278uWfIDhJnbdvDjvmE=
|
||||
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
|
||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
|
||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
||||
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
|
||||
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
|
||||
google.golang.org/genproto v0.0.0-20251124214823-79d6a2a48846 h1:dDbsTLIK7EzwUq36kCSAsk0slouq/S0tWHeeGi97cD8=
|
||||
google.golang.org/genproto v0.0.0-20251124214823-79d6a2a48846/go.mod h1:PP0g88Dz3C7hRAfbQCQggeWAXjuqGsNPLE4s7jh0RGU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
635
src/frontend/handlers.go
Normal file
@@ -0,0 +1,635 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
pb "github.com/GoogleCloudPlatform/microservices-demo/src/frontend/genproto"
|
||||
"github.com/GoogleCloudPlatform/microservices-demo/src/frontend/money"
|
||||
"github.com/GoogleCloudPlatform/microservices-demo/src/frontend/validator"
|
||||
)
|
||||
|
||||
type platformDetails struct {
|
||||
css string
|
||||
provider string
|
||||
}
|
||||
|
||||
var (
|
||||
frontendMessage = strings.TrimSpace(os.Getenv("FRONTEND_MESSAGE"))
|
||||
isCymbalBrand = "true" == strings.ToLower(os.Getenv("CYMBAL_BRANDING"))
|
||||
assistantEnabled = "true" == strings.ToLower(os.Getenv("ENABLE_ASSISTANT"))
|
||||
templates = template.Must(template.New("").
|
||||
Funcs(template.FuncMap{
|
||||
"renderMoney": renderMoney,
|
||||
"renderCurrencyLogo": renderCurrencyLogo,
|
||||
}).ParseGlob("templates/*.html"))
|
||||
plat platformDetails
|
||||
)
|
||||
|
||||
var validEnvs = []string{"local", "gcp", "azure", "aws", "onprem", "alibaba"}
|
||||
|
||||
func (fe *frontendServer) homeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
||||
log.WithField("currency", currentCurrency(r)).Info("home")
|
||||
currencies, err := fe.getCurrencies(r.Context())
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
products, err := fe.getProducts(r.Context())
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve products"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
cart, err := fe.getCart(r.Context(), sessionID(r))
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
type productView struct {
|
||||
Item *pb.Product
|
||||
Price *pb.Money
|
||||
}
|
||||
ps := make([]productView, len(products))
|
||||
for i, p := range products {
|
||||
price, err := fe.convertCurrency(r.Context(), p.GetPriceUsd(), currentCurrency(r))
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrapf(err, "failed to do currency conversion for product %s", p.GetId()), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
ps[i] = productView{p, price}
|
||||
}
|
||||
|
||||
// Set ENV_PLATFORM (default to local if not set; use env var if set; otherwise detect GCP, which overrides env)_
|
||||
var env = os.Getenv("ENV_PLATFORM")
|
||||
// Only override from env variable if set + valid env
|
||||
if env == "" || stringinSlice(validEnvs, env) == false {
|
||||
fmt.Println("env platform is either empty or invalid")
|
||||
env = "local"
|
||||
}
|
||||
// Autodetect GCP
|
||||
addrs, err := net.LookupHost("metadata.google.internal.")
|
||||
if err == nil && len(addrs) >= 0 {
|
||||
log.Debugf("Detected Google metadata server: %v, setting ENV_PLATFORM to GCP.", addrs)
|
||||
env = "gcp"
|
||||
}
|
||||
|
||||
log.Debugf("ENV_PLATFORM is: %s", env)
|
||||
plat = platformDetails{}
|
||||
plat.setPlatformDetails(strings.ToLower(env))
|
||||
|
||||
if err := templates.ExecuteTemplate(w, "home", injectCommonTemplateData(r, map[string]interface{}{
|
||||
"show_currency": true,
|
||||
"currencies": currencies,
|
||||
"products": ps,
|
||||
"cart_size": cartSize(cart),
|
||||
"banner_color": os.Getenv("BANNER_COLOR"), // illustrates canary deployments
|
||||
"ad": fe.chooseAd(r.Context(), []string{}, log),
|
||||
})); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (plat *platformDetails) setPlatformDetails(env string) {
|
||||
if env == "aws" {
|
||||
plat.provider = "AWS"
|
||||
plat.css = "aws-platform"
|
||||
} else if env == "onprem" {
|
||||
plat.provider = "On-Premises"
|
||||
plat.css = "onprem-platform"
|
||||
} else if env == "azure" {
|
||||
plat.provider = "Azure"
|
||||
plat.css = "azure-platform"
|
||||
} else if env == "gcp" {
|
||||
plat.provider = "Google Cloud"
|
||||
plat.css = "gcp-platform"
|
||||
} else if env == "alibaba" {
|
||||
plat.provider = "Alibaba Cloud"
|
||||
plat.css = "alibaba-platform"
|
||||
} else {
|
||||
plat.provider = "local"
|
||||
plat.css = "local"
|
||||
}
|
||||
}
|
||||
|
||||
func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
renderHTTPError(log, r, w, errors.New("product id not specified"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
log.WithField("id", id).WithField("currency", currentCurrency(r)).
|
||||
Debug("serving product page")
|
||||
|
||||
p, err := fe.getProduct(r.Context(), id)
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve product"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
currencies, err := fe.getCurrencies(r.Context())
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
cart, err := fe.getCart(r.Context(), sessionID(r))
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
price, err := fe.convertCurrency(r.Context(), p.GetPriceUsd(), currentCurrency(r))
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "failed to convert currency"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// ignores the error retrieving recommendations since it is not critical
|
||||
recommendations, err := fe.getRecommendations(r.Context(), sessionID(r), []string{id})
|
||||
if err != nil {
|
||||
log.WithField("error", err).Warn("failed to get product recommendations")
|
||||
}
|
||||
|
||||
product := struct {
|
||||
Item *pb.Product
|
||||
Price *pb.Money
|
||||
}{p, price}
|
||||
|
||||
// Fetch packaging info (weight/dimensions) of the product
|
||||
// The packaging service is an optional microservice you can run as part of a Google Cloud demo.
|
||||
var packagingInfo *PackagingInfo = nil
|
||||
if isPackagingServiceConfigured() {
|
||||
packagingInfo, err = httpGetPackagingInfo(id)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to obtain product's packaging info:", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := templates.ExecuteTemplate(w, "product", injectCommonTemplateData(r, map[string]interface{}{
|
||||
"ad": fe.chooseAd(r.Context(), p.Categories, log),
|
||||
"show_currency": true,
|
||||
"currencies": currencies,
|
||||
"product": product,
|
||||
"recommendations": recommendations,
|
||||
"cart_size": cartSize(cart),
|
||||
"packagingInfo": packagingInfo,
|
||||
})); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (fe *frontendServer) addToCartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
||||
quantity, _ := strconv.ParseUint(r.FormValue("quantity"), 10, 32)
|
||||
productID := r.FormValue("product_id")
|
||||
payload := validator.AddToCartPayload{
|
||||
Quantity: quantity,
|
||||
ProductID: productID,
|
||||
}
|
||||
if err := payload.Validate(); err != nil {
|
||||
renderHTTPError(log, r, w, validator.ValidationErrorResponse(err), http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
log.WithField("product", payload.ProductID).WithField("quantity", payload.Quantity).Debug("adding to cart")
|
||||
|
||||
p, err := fe.getProduct(r.Context(), payload.ProductID)
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve product"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := fe.insertCart(r.Context(), sessionID(r), p.GetId(), int32(payload.Quantity)); err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "failed to add to cart"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("location", baseUrl + "/cart")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}
|
||||
|
||||
func (fe *frontendServer) emptyCartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
||||
log.Debug("emptying cart")
|
||||
|
||||
if err := fe.emptyCart(r.Context(), sessionID(r)); err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "failed to empty cart"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("location", baseUrl + "/")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}
|
||||
|
||||
func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
||||
log.Debug("view user cart")
|
||||
currencies, err := fe.getCurrencies(r.Context())
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
cart, err := fe.getCart(r.Context(), sessionID(r))
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// ignores the error retrieving recommendations since it is not critical
|
||||
recommendations, err := fe.getRecommendations(r.Context(), sessionID(r), cartIDs(cart))
|
||||
if err != nil {
|
||||
log.WithField("error", err).Warn("failed to get product recommendations")
|
||||
}
|
||||
|
||||
shippingCost, err := fe.getShippingQuote(r.Context(), cart, currentCurrency(r))
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "failed to get shipping quote"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
type cartItemView struct {
|
||||
Item *pb.Product
|
||||
Quantity int32
|
||||
Price *pb.Money
|
||||
}
|
||||
items := make([]cartItemView, len(cart))
|
||||
totalPrice := pb.Money{CurrencyCode: currentCurrency(r)}
|
||||
for i, item := range cart {
|
||||
p, err := fe.getProduct(r.Context(), item.GetProductId())
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrapf(err, "could not retrieve product #%s", item.GetProductId()), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
price, err := fe.convertCurrency(r.Context(), p.GetPriceUsd(), currentCurrency(r))
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrapf(err, "could not convert currency for product #%s", item.GetProductId()), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
multPrice := money.MultiplySlow(*price, uint32(item.GetQuantity()))
|
||||
items[i] = cartItemView{
|
||||
Item: p,
|
||||
Quantity: item.GetQuantity(),
|
||||
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", injectCommonTemplateData(r, map[string]interface{}{
|
||||
"currencies": currencies,
|
||||
"recommendations": recommendations,
|
||||
"cart_size": cartSize(cart),
|
||||
"shipping_cost": shippingCost,
|
||||
"show_currency": true,
|
||||
"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 := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
||||
log.Debug("placing order")
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
payload := validator.PlaceOrderPayload{
|
||||
Email: email,
|
||||
StreetAddress: streetAddress,
|
||||
ZipCode: zipCode,
|
||||
City: city,
|
||||
State: state,
|
||||
Country: country,
|
||||
CcNumber: ccNumber,
|
||||
CcMonth: ccMonth,
|
||||
CcYear: ccYear,
|
||||
CcCVV: ccCVV,
|
||||
}
|
||||
if err := payload.Validate(); err != nil {
|
||||
renderHTTPError(log, r, w, validator.ValidationErrorResponse(err), http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
order, err := pb.NewCheckoutServiceClient(fe.checkoutSvcConn).
|
||||
PlaceOrder(r.Context(), &pb.PlaceOrderRequest{
|
||||
Email: payload.Email,
|
||||
CreditCard: &pb.CreditCardInfo{
|
||||
CreditCardNumber: payload.CcNumber,
|
||||
CreditCardExpirationMonth: int32(payload.CcMonth),
|
||||
CreditCardExpirationYear: int32(payload.CcYear),
|
||||
CreditCardCvv: int32(payload.CcCVV)},
|
||||
UserId: sessionID(r),
|
||||
UserCurrency: currentCurrency(r),
|
||||
Address: &pb.Address{
|
||||
StreetAddress: payload.StreetAddress,
|
||||
City: payload.City,
|
||||
State: payload.State,
|
||||
ZipCode: int32(payload.ZipCode),
|
||||
Country: payload.Country},
|
||||
})
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "failed to complete the order"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.WithField("order", order.GetOrder().GetOrderId()).Info("order placed")
|
||||
|
||||
order.GetOrder().GetItems()
|
||||
recommendations, _ := fe.getRecommendations(r.Context(), sessionID(r), nil)
|
||||
|
||||
totalPaid := *order.GetOrder().GetShippingCost()
|
||||
for _, v := range order.GetOrder().GetItems() {
|
||||
multPrice := money.MultiplySlow(*v.GetCost(), uint32(v.GetItem().GetQuantity()))
|
||||
totalPaid = money.Must(money.Sum(totalPaid, multPrice))
|
||||
}
|
||||
|
||||
currencies, err := fe.getCurrencies(r.Context())
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := templates.ExecuteTemplate(w, "order", injectCommonTemplateData(r, map[string]interface{}{
|
||||
"show_currency": false,
|
||||
"currencies": currencies,
|
||||
"order": order.GetOrder(),
|
||||
"total_paid": &totalPaid,
|
||||
"recommendations": recommendations,
|
||||
})); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (fe *frontendServer) assistantHandler(w http.ResponseWriter, r *http.Request) {
|
||||
currencies, err := fe.getCurrencies(r.Context())
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := templates.ExecuteTemplate(w, "assistant", injectCommonTemplateData(r, map[string]interface{}{
|
||||
"show_currency": false,
|
||||
"currencies": currencies,
|
||||
})); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (fe *frontendServer) logoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
||||
log.Debug("logging out")
|
||||
for _, c := range r.Cookies() {
|
||||
c.Expires = time.Now().Add(-time.Hour * 24 * 365)
|
||||
c.MaxAge = -1
|
||||
http.SetCookie(w, c)
|
||||
}
|
||||
w.Header().Set("Location", baseUrl + "/")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}
|
||||
|
||||
func (fe *frontendServer) getProductByID(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["ids"]
|
||||
if id == "" {
|
||||
return
|
||||
}
|
||||
|
||||
p, err := fe.getProduct(r.Context(), id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(jsonData)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (fe *frontendServer) chatBotHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
||||
type Response struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type LLMResponse struct {
|
||||
Content string `json:"content"`
|
||||
Details map[string]any `json:"details"`
|
||||
}
|
||||
|
||||
var response LLMResponse
|
||||
|
||||
url := "http://" + fe.shoppingAssistantSvcAddr
|
||||
req, err := http.NewRequest(http.MethodPost, url, r.Body)
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "failed to create request"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "failed to send request"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "failed to read response"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%+v\n", body)
|
||||
fmt.Printf("%+v\n", res)
|
||||
|
||||
err = json.Unmarshal(body, &response)
|
||||
if err != nil {
|
||||
renderHTTPError(log, r, w, errors.Wrap(err, "failed to unmarshal body"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// respond with the same message
|
||||
json.NewEncoder(w).Encode(Response{Message: response.Content})
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (fe *frontendServer) setCurrencyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
||||
cur := r.FormValue("currency_code")
|
||||
payload := validator.SetCurrencyPayload{Currency: cur}
|
||||
if err := payload.Validate(); err != nil {
|
||||
renderHTTPError(log, r, w, validator.ValidationErrorResponse(err), http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
log.WithField("curr.new", payload.Currency).WithField("curr.old", currentCurrency(r)).
|
||||
Debug("setting currency")
|
||||
|
||||
if payload.Currency != "" {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: cookieCurrency,
|
||||
Value: payload.Currency,
|
||||
MaxAge: cookieMaxAge,
|
||||
})
|
||||
}
|
||||
referer := r.Header.Get("referer")
|
||||
if referer == "" {
|
||||
referer = baseUrl + "/"
|
||||
}
|
||||
w.Header().Set("Location", referer)
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}
|
||||
|
||||
// chooseAd queries for advertisements available and randomly chooses one, if
|
||||
// available. It ignores the error retrieving the ad since it is not critical.
|
||||
func (fe *frontendServer) chooseAd(ctx context.Context, ctxKeys []string, log logrus.FieldLogger) *pb.Ad {
|
||||
ads, err := fe.getAd(ctx, ctxKeys)
|
||||
if err != nil {
|
||||
log.WithField("error", err).Warn("failed to retrieve ads")
|
||||
return nil
|
||||
}
|
||||
return ads[rand.Intn(len(ads))]
|
||||
}
|
||||
|
||||
func renderHTTPError(log logrus.FieldLogger, r *http.Request, w http.ResponseWriter, err error, code int) {
|
||||
log.WithField("error", err).Error("request error")
|
||||
errMsg := fmt.Sprintf("%+v", err)
|
||||
|
||||
w.WriteHeader(code)
|
||||
|
||||
if templateErr := templates.ExecuteTemplate(w, "error", injectCommonTemplateData(r, map[string]interface{}{
|
||||
"error": errMsg,
|
||||
"status_code": code,
|
||||
"status": http.StatusText(code),
|
||||
})); templateErr != nil {
|
||||
log.Println(templateErr)
|
||||
}
|
||||
}
|
||||
|
||||
func injectCommonTemplateData(r *http.Request, payload map[string]interface{}) map[string]interface{} {
|
||||
data := map[string]interface{}{
|
||||
"session_id": sessionID(r),
|
||||
"request_id": r.Context().Value(ctxKeyRequestID{}),
|
||||
"user_currency": currentCurrency(r),
|
||||
"platform_css": plat.css,
|
||||
"platform_name": plat.provider,
|
||||
"is_cymbal_brand": isCymbalBrand,
|
||||
"assistant_enabled": assistantEnabled,
|
||||
"deploymentDetails": deploymentDetailsMap,
|
||||
"frontendMessage": frontendMessage,
|
||||
"currentYear": time.Now().Year(),
|
||||
"baseUrl": baseUrl,
|
||||
}
|
||||
|
||||
for k, v := range payload {
|
||||
data[k] = v
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func currentCurrency(r *http.Request) string {
|
||||
c, _ := r.Cookie(cookieCurrency)
|
||||
if c != nil {
|
||||
return c.Value
|
||||
}
|
||||
return defaultCurrency
|
||||
}
|
||||
|
||||
func sessionID(r *http.Request) string {
|
||||
v := r.Context().Value(ctxKeySessionID{})
|
||||
if v != nil {
|
||||
return v.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func cartIDs(c []*pb.CartItem) []string {
|
||||
out := make([]string, len(c))
|
||||
for i, v := range c {
|
||||
out[i] = v.GetProductId()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// get total # of items in cart
|
||||
func cartSize(c []*pb.CartItem) int {
|
||||
cartSize := 0
|
||||
for _, item := range c {
|
||||
cartSize += int(item.GetQuantity())
|
||||
}
|
||||
return cartSize
|
||||
}
|
||||
|
||||
func renderMoney(money pb.Money) string {
|
||||
currencyLogo := renderCurrencyLogo(money.GetCurrencyCode())
|
||||
return fmt.Sprintf("%s%d.%02d", currencyLogo, money.GetUnits(), money.GetNanos()/10000000)
|
||||
}
|
||||
|
||||
func renderCurrencyLogo(currencyCode string) string {
|
||||
logos := map[string]string{
|
||||
"USD": "$",
|
||||
"CAD": "$",
|
||||
"JPY": "¥",
|
||||
"EUR": "€",
|
||||
"TRY": "₺",
|
||||
"GBP": "£",
|
||||
}
|
||||
|
||||
logo := "$" //default
|
||||
if val, ok := logos[currencyCode]; ok {
|
||||
logo = val
|
||||
}
|
||||
return logo
|
||||
}
|
||||
|
||||
func stringinSlice(slice []string, val string) bool {
|
||||
for _, item := range slice {
|
||||
if item == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
235
src/frontend/main.go
Normal file
@@ -0,0 +1,235 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/profiler"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
const (
|
||||
port = "8080"
|
||||
defaultCurrency = "USD"
|
||||
cookieMaxAge = 60 * 60 * 48
|
||||
|
||||
cookiePrefix = "shop_"
|
||||
cookieSessionID = cookiePrefix + "session-id"
|
||||
cookieCurrency = cookiePrefix + "currency"
|
||||
)
|
||||
|
||||
var (
|
||||
whitelistedCurrencies = map[string]bool{
|
||||
"USD": true,
|
||||
"EUR": true,
|
||||
"CAD": true,
|
||||
"JPY": true,
|
||||
"GBP": true,
|
||||
"TRY": true,
|
||||
}
|
||||
|
||||
baseUrl = ""
|
||||
)
|
||||
|
||||
type ctxKeySessionID struct{}
|
||||
|
||||
type frontendServer struct {
|
||||
productCatalogSvcAddr string
|
||||
productCatalogSvcConn *grpc.ClientConn
|
||||
|
||||
currencySvcAddr string
|
||||
currencySvcConn *grpc.ClientConn
|
||||
|
||||
cartSvcAddr string
|
||||
cartSvcConn *grpc.ClientConn
|
||||
|
||||
recommendationSvcAddr string
|
||||
recommendationSvcConn *grpc.ClientConn
|
||||
|
||||
checkoutSvcAddr string
|
||||
checkoutSvcConn *grpc.ClientConn
|
||||
|
||||
shippingSvcAddr string
|
||||
shippingSvcConn *grpc.ClientConn
|
||||
|
||||
adSvcAddr string
|
||||
adSvcConn *grpc.ClientConn
|
||||
|
||||
collectorAddr string
|
||||
collectorConn *grpc.ClientConn
|
||||
|
||||
shoppingAssistantSvcAddr string
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
log := logrus.New()
|
||||
log.Level = logrus.DebugLevel
|
||||
log.Formatter = &logrus.JSONFormatter{
|
||||
FieldMap: logrus.FieldMap{
|
||||
logrus.FieldKeyTime: "timestamp",
|
||||
logrus.FieldKeyLevel: "severity",
|
||||
logrus.FieldKeyMsg: "message",
|
||||
},
|
||||
TimestampFormat: time.RFC3339Nano,
|
||||
}
|
||||
log.Out = os.Stdout
|
||||
|
||||
svc := new(frontendServer)
|
||||
|
||||
otel.SetTextMapPropagator(
|
||||
propagation.NewCompositeTextMapPropagator(
|
||||
propagation.TraceContext{}, propagation.Baggage{}))
|
||||
|
||||
baseUrl = os.Getenv("BASE_URL")
|
||||
|
||||
if os.Getenv("ENABLE_TRACING") == "1" {
|
||||
log.Info("Tracing enabled.")
|
||||
initTracing(log, ctx, svc)
|
||||
} else {
|
||||
log.Info("Tracing disabled.")
|
||||
}
|
||||
|
||||
if os.Getenv("ENABLE_PROFILER") == "1" {
|
||||
log.Info("Profiling enabled.")
|
||||
go initProfiling(log, "frontend", "1.0.0")
|
||||
} else {
|
||||
log.Info("Profiling disabled.")
|
||||
}
|
||||
|
||||
srvPort := port
|
||||
if os.Getenv("PORT") != "" {
|
||||
srvPort = os.Getenv("PORT")
|
||||
}
|
||||
addr := os.Getenv("LISTEN_ADDR")
|
||||
mustMapEnv(&svc.productCatalogSvcAddr, "PRODUCT_CATALOG_SERVICE_ADDR")
|
||||
mustMapEnv(&svc.currencySvcAddr, "CURRENCY_SERVICE_ADDR")
|
||||
mustMapEnv(&svc.cartSvcAddr, "CART_SERVICE_ADDR")
|
||||
mustMapEnv(&svc.recommendationSvcAddr, "RECOMMENDATION_SERVICE_ADDR")
|
||||
mustMapEnv(&svc.checkoutSvcAddr, "CHECKOUT_SERVICE_ADDR")
|
||||
mustMapEnv(&svc.shippingSvcAddr, "SHIPPING_SERVICE_ADDR")
|
||||
mustMapEnv(&svc.adSvcAddr, "AD_SERVICE_ADDR")
|
||||
mustMapEnv(&svc.shoppingAssistantSvcAddr, "SHOPPING_ASSISTANT_SERVICE_ADDR")
|
||||
|
||||
mustConnGRPC(ctx, &svc.currencySvcConn, svc.currencySvcAddr)
|
||||
mustConnGRPC(ctx, &svc.productCatalogSvcConn, svc.productCatalogSvcAddr)
|
||||
mustConnGRPC(ctx, &svc.cartSvcConn, svc.cartSvcAddr)
|
||||
mustConnGRPC(ctx, &svc.recommendationSvcConn, svc.recommendationSvcAddr)
|
||||
mustConnGRPC(ctx, &svc.shippingSvcConn, svc.shippingSvcAddr)
|
||||
mustConnGRPC(ctx, &svc.checkoutSvcConn, svc.checkoutSvcAddr)
|
||||
mustConnGRPC(ctx, &svc.adSvcConn, svc.adSvcAddr)
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc(baseUrl+"/", svc.homeHandler).Methods(http.MethodGet, http.MethodHead)
|
||||
r.HandleFunc(baseUrl+"/product/{id}", svc.productHandler).Methods(http.MethodGet, http.MethodHead)
|
||||
r.HandleFunc(baseUrl+"/cart", svc.viewCartHandler).Methods(http.MethodGet, http.MethodHead)
|
||||
r.HandleFunc(baseUrl+"/cart", svc.addToCartHandler).Methods(http.MethodPost)
|
||||
r.HandleFunc(baseUrl+"/cart/empty", svc.emptyCartHandler).Methods(http.MethodPost)
|
||||
r.HandleFunc(baseUrl+"/setCurrency", svc.setCurrencyHandler).Methods(http.MethodPost)
|
||||
r.HandleFunc(baseUrl+"/logout", svc.logoutHandler).Methods(http.MethodGet)
|
||||
r.HandleFunc(baseUrl+"/cart/checkout", svc.placeOrderHandler).Methods(http.MethodPost)
|
||||
r.HandleFunc(baseUrl+"/assistant", svc.assistantHandler).Methods(http.MethodGet)
|
||||
r.PathPrefix(baseUrl + "/static/").Handler(http.StripPrefix(baseUrl+"/static/", http.FileServer(http.Dir("./static/"))))
|
||||
r.HandleFunc(baseUrl+"/robots.txt", func(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, "User-agent: *\nDisallow: /") })
|
||||
r.HandleFunc(baseUrl+"/_healthz", func(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, "ok") })
|
||||
r.HandleFunc(baseUrl+"/product-meta/{ids}", svc.getProductByID).Methods(http.MethodGet)
|
||||
r.HandleFunc(baseUrl+"/bot", svc.chatBotHandler).Methods(http.MethodPost)
|
||||
|
||||
var handler http.Handler = r
|
||||
handler = &logHandler{log: log, next: handler} // add logging
|
||||
handler = ensureSessionID(handler) // add session ID
|
||||
handler = otelhttp.NewHandler(handler, "frontend") // add OTel tracing
|
||||
|
||||
log.Infof("starting server on %s:%s", addr, srvPort)
|
||||
log.Fatal(http.ListenAndServe(addr+":"+srvPort, handler))
|
||||
}
|
||||
func initStats(log logrus.FieldLogger) {
|
||||
// TODO(arbrown) Implement OpenTelemtry stats
|
||||
}
|
||||
|
||||
func initTracing(log logrus.FieldLogger, ctx context.Context, svc *frontendServer) (*sdktrace.TracerProvider, error) {
|
||||
mustMapEnv(&svc.collectorAddr, "COLLECTOR_SERVICE_ADDR")
|
||||
mustConnGRPC(ctx, &svc.collectorConn, svc.collectorAddr)
|
||||
exporter, err := otlptracegrpc.New(
|
||||
ctx,
|
||||
otlptracegrpc.WithGRPCConn(svc.collectorConn))
|
||||
if err != nil {
|
||||
log.Warnf("warn: Failed to create trace exporter: %v", err)
|
||||
}
|
||||
tp := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithBatcher(exporter),
|
||||
sdktrace.WithSampler(sdktrace.AlwaysSample()))
|
||||
otel.SetTracerProvider(tp)
|
||||
|
||||
return tp, err
|
||||
}
|
||||
|
||||
func initProfiling(log logrus.FieldLogger, service, version string) {
|
||||
// TODO(ahmetb) this method is duplicated in other microservices using Go
|
||||
// since they are not sharing packages.
|
||||
for i := 1; i <= 3; i++ {
|
||||
log = log.WithField("retry", i)
|
||||
if err := profiler.Start(profiler.Config{
|
||||
Service: service,
|
||||
ServiceVersion: version,
|
||||
// ProjectID must be set if not running on GCP.
|
||||
// ProjectID: "my-project",
|
||||
}); err != nil {
|
||||
log.Warnf("warn: failed to start profiler: %+v", err)
|
||||
} else {
|
||||
log.Info("started Stackdriver profiler")
|
||||
return
|
||||
}
|
||||
d := time.Second * 10 * time.Duration(i)
|
||||
log.Debugf("sleeping %v to retry initializing Stackdriver profiler", d)
|
||||
time.Sleep(d)
|
||||
}
|
||||
log.Warn("warning: could not initialize Stackdriver profiler after retrying, giving up")
|
||||
}
|
||||
|
||||
func mustMapEnv(target *string, envKey string) {
|
||||
v := os.Getenv(envKey)
|
||||
if v == "" {
|
||||
panic(fmt.Sprintf("environment variable %q not set", envKey))
|
||||
}
|
||||
*target = v
|
||||
}
|
||||
|
||||
func mustConnGRPC(ctx context.Context, conn **grpc.ClientConn, addr string) {
|
||||
var err error
|
||||
_, cancel := context.WithTimeout(ctx, time.Second*3)
|
||||
defer cancel()
|
||||
*conn, err = grpc.NewClient(addr,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithStatsHandler(otelgrpc.NewClientHandler()))
|
||||
if err != nil {
|
||||
panic(errors.Wrapf(err, "grpc: failed to connect %s", addr))
|
||||
}
|
||||
}
|
||||
111
src/frontend/middleware.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
"os"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ctxKeyLog struct{}
|
||||
type ctxKeyRequestID struct{}
|
||||
|
||||
type logHandler struct {
|
||||
log *logrus.Logger
|
||||
next http.Handler
|
||||
}
|
||||
|
||||
type responseRecorder struct {
|
||||
b int
|
||||
status int
|
||||
w http.ResponseWriter
|
||||
}
|
||||
|
||||
func (r *responseRecorder) Header() http.Header { return r.w.Header() }
|
||||
|
||||
func (r *responseRecorder) Write(p []byte) (int, error) {
|
||||
if r.status == 0 {
|
||||
r.status = http.StatusOK
|
||||
}
|
||||
n, err := r.w.Write(p)
|
||||
r.b += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *responseRecorder) WriteHeader(statusCode int) {
|
||||
r.status = statusCode
|
||||
r.w.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func (lh *logHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
requestID, _ := uuid.NewRandom()
|
||||
ctx = context.WithValue(ctx, ctxKeyRequestID{}, requestID.String())
|
||||
|
||||
start := time.Now()
|
||||
rr := &responseRecorder{w: w}
|
||||
log := lh.log.WithFields(logrus.Fields{
|
||||
"http.req.path": r.URL.Path,
|
||||
"http.req.method": r.Method,
|
||||
"http.req.id": requestID.String(),
|
||||
})
|
||||
if v, ok := r.Context().Value(ctxKeySessionID{}).(string); ok {
|
||||
log = log.WithField("session", v)
|
||||
}
|
||||
log.Debug("request started")
|
||||
defer func() {
|
||||
log.WithFields(logrus.Fields{
|
||||
"http.resp.took_ms": int64(time.Since(start) / time.Millisecond),
|
||||
"http.resp.status": rr.status,
|
||||
"http.resp.bytes": rr.b}).Debugf("request complete")
|
||||
}()
|
||||
|
||||
ctx = context.WithValue(ctx, ctxKeyLog{}, log)
|
||||
r = r.WithContext(ctx)
|
||||
lh.next.ServeHTTP(rr, r)
|
||||
}
|
||||
|
||||
func ensureSessionID(next http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var sessionID string
|
||||
c, err := r.Cookie(cookieSessionID)
|
||||
if err == http.ErrNoCookie {
|
||||
if os.Getenv("ENABLE_SINGLE_SHARED_SESSION") == "true" {
|
||||
// Hard coded user id, shared across sessions
|
||||
sessionID = "12345678-1234-1234-1234-123456789123"
|
||||
} else {
|
||||
u, _ := uuid.NewRandom()
|
||||
sessionID = u.String()
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: cookieSessionID,
|
||||
Value: sessionID,
|
||||
MaxAge: cookieMaxAge,
|
||||
})
|
||||
} else if err != nil {
|
||||
return
|
||||
} else {
|
||||
sessionID = c.Value
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), ctxKeySessionID{}, sessionID)
|
||||
r = r.WithContext(ctx)
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
132
src/frontend/money/money.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package money
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
pb "github.com/GoogleCloudPlatform/microservices-demo/src/frontend/genproto"
|
||||
)
|
||||
|
||||
const (
|
||||
nanosMin = -999999999
|
||||
nanosMax = +999999999
|
||||
nanosMod = 1000000000
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidValue = errors.New("one of the specified money values is invalid")
|
||||
ErrMismatchingCurrency = errors.New("mismatching currency codes")
|
||||
)
|
||||
|
||||
// IsValid checks if specified value has a valid units/nanos signs and ranges.
|
||||
func IsValid(m pb.Money) bool {
|
||||
return signMatches(m) && validNanos(m.GetNanos())
|
||||
}
|
||||
|
||||
func signMatches(m pb.Money) bool {
|
||||
return m.GetNanos() == 0 || m.GetUnits() == 0 || (m.GetNanos() < 0) == (m.GetUnits() < 0)
|
||||
}
|
||||
|
||||
func validNanos(nanos int32) bool { return nanosMin <= nanos && nanos <= nanosMax }
|
||||
|
||||
// IsZero returns true if the specified money value is equal to zero.
|
||||
func IsZero(m pb.Money) bool { return m.GetUnits() == 0 && m.GetNanos() == 0 }
|
||||
|
||||
// IsPositive returns true if the specified money value is valid and is
|
||||
// positive.
|
||||
func IsPositive(m pb.Money) bool {
|
||||
return IsValid(m) && m.GetUnits() > 0 || (m.GetUnits() == 0 && m.GetNanos() > 0)
|
||||
}
|
||||
|
||||
// IsNegative returns true if the specified money value is valid and is
|
||||
// negative.
|
||||
func IsNegative(m pb.Money) bool {
|
||||
return IsValid(m) && m.GetUnits() < 0 || (m.GetUnits() == 0 && m.GetNanos() < 0)
|
||||
}
|
||||
|
||||
// AreSameCurrency returns true if values l and r have a currency code and
|
||||
// they are the same values.
|
||||
func AreSameCurrency(l, r pb.Money) bool {
|
||||
return l.GetCurrencyCode() == r.GetCurrencyCode() && l.GetCurrencyCode() != ""
|
||||
}
|
||||
|
||||
// AreEquals returns true if values l and r are the equal, including the
|
||||
// currency. This does not check validity of the provided values.
|
||||
func AreEquals(l, r pb.Money) bool {
|
||||
return l.GetCurrencyCode() == r.GetCurrencyCode() &&
|
||||
l.GetUnits() == r.GetUnits() && l.GetNanos() == r.GetNanos()
|
||||
}
|
||||
|
||||
// Negate returns the same amount with the sign negated.
|
||||
func Negate(m pb.Money) pb.Money {
|
||||
return pb.Money{
|
||||
Units: -m.GetUnits(),
|
||||
Nanos: -m.GetNanos(),
|
||||
CurrencyCode: m.GetCurrencyCode()}
|
||||
}
|
||||
|
||||
// Must panics if the given error is not nil. This can be used with other
|
||||
// functions like: "m := Must(Sum(a,b))".
|
||||
func Must(v pb.Money, err error) pb.Money {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Sum adds two values. Returns an error if one of the values are invalid or
|
||||
// currency codes are not matching (unless currency code is unspecified for
|
||||
// both).
|
||||
func Sum(l, r pb.Money) (pb.Money, error) {
|
||||
if !IsValid(l) || !IsValid(r) {
|
||||
return pb.Money{}, ErrInvalidValue
|
||||
} else if l.GetCurrencyCode() != r.GetCurrencyCode() {
|
||||
return pb.Money{}, ErrMismatchingCurrency
|
||||
}
|
||||
units := l.GetUnits() + r.GetUnits()
|
||||
nanos := l.GetNanos() + r.GetNanos()
|
||||
|
||||
if (units == 0 && nanos == 0) || (units > 0 && nanos >= 0) || (units < 0 && nanos <= 0) {
|
||||
// same sign <units, nanos>
|
||||
units += int64(nanos / nanosMod)
|
||||
nanos = nanos % nanosMod
|
||||
} else {
|
||||
// different sign. nanos guaranteed to not to go over the limit
|
||||
if units > 0 {
|
||||
units--
|
||||
nanos += nanosMod
|
||||
} else {
|
||||
units++
|
||||
nanos -= nanosMod
|
||||
}
|
||||
}
|
||||
|
||||
return pb.Money{
|
||||
Units: units,
|
||||
Nanos: nanos,
|
||||
CurrencyCode: l.GetCurrencyCode()}, nil
|
||||
}
|
||||
|
||||
// MultiplySlow is a slow multiplication operation done through adding the value
|
||||
// to itself n-1 times.
|
||||
func MultiplySlow(m pb.Money, n uint32) pb.Money {
|
||||
out := m
|
||||
for n > 1 {
|
||||
out = Must(Sum(out, m))
|
||||
n--
|
||||
}
|
||||
return out
|
||||
}
|
||||
245
src/frontend/money/money_test.go
Normal file
@@ -0,0 +1,245 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package money
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
pb "github.com/GoogleCloudPlatform/microservices-demo/src/frontend/genproto"
|
||||
)
|
||||
|
||||
func mmc(u int64, n int32, c string) pb.Money { return pb.Money{Units: u, Nanos: n, CurrencyCode: c} }
|
||||
func mm(u int64, n int32) pb.Money { return mmc(u, n, "") }
|
||||
|
||||
func TestIsValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in pb.Money
|
||||
want bool
|
||||
}{
|
||||
{"valid -/-", mm(-981273891273, -999999999), true},
|
||||
{"invalid -/+", mm(-981273891273, +999999999), false},
|
||||
{"valid +/+", mm(981273891273, 999999999), true},
|
||||
{"invalid +/-", mm(981273891273, -999999999), false},
|
||||
{"invalid +/+overflow", mm(3, 1000000000), false},
|
||||
{"invalid +/-overflow", mm(3, -1000000000), false},
|
||||
{"invalid -/+overflow", mm(-3, 1000000000), false},
|
||||
{"invalid -/-overflow", mm(-3, -1000000000), false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsValid(tt.in); got != tt.want {
|
||||
t.Errorf("IsValid(%v) = %v, want %v", tt.in, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsZero(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in pb.Money
|
||||
want bool
|
||||
}{
|
||||
{"zero", mm(0, 0), true},
|
||||
{"not-zero (-/+)", mm(-1, +1), false},
|
||||
{"not-zero (-/-)", mm(-1, -1), false},
|
||||
{"not-zero (+/+)", mm(+1, +1), false},
|
||||
{"not-zero (+/-)", mm(+1, -1), false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsZero(tt.in); got != tt.want {
|
||||
t.Errorf("IsZero(%v) = %v, want %v", tt.in, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPositive(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in pb.Money
|
||||
want bool
|
||||
}{
|
||||
{"zero", mm(0, 0), false},
|
||||
{"positive (+/+)", mm(+1, +1), true},
|
||||
{"invalid (-/+)", mm(-1, +1), false},
|
||||
{"negative (-/-)", mm(-1, -1), false},
|
||||
{"invalid (+/-)", mm(+1, -1), false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsPositive(tt.in); got != tt.want {
|
||||
t.Errorf("IsPositive(%v) = %v, want %v", tt.in, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNegative(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in pb.Money
|
||||
want bool
|
||||
}{
|
||||
{"zero", mm(0, 0), false},
|
||||
{"positive (+/+)", mm(+1, +1), false},
|
||||
{"invalid (-/+)", mm(-1, +1), false},
|
||||
{"negative (-/-)", mm(-1, -1), true},
|
||||
{"invalid (+/-)", mm(+1, -1), false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsNegative(tt.in); got != tt.want {
|
||||
t.Errorf("IsNegative(%v) = %v, want %v", tt.in, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAreSameCurrency(t *testing.T) {
|
||||
type args struct {
|
||||
l pb.Money
|
||||
r pb.Money
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{"both empty currency", args{mmc(1, 0, ""), mmc(2, 0, "")}, false},
|
||||
{"left empty currency", args{mmc(1, 0, ""), mmc(2, 0, "USD")}, false},
|
||||
{"right empty currency", args{mmc(1, 0, "USD"), mmc(2, 0, "")}, false},
|
||||
{"mismatching", args{mmc(1, 0, "USD"), mmc(2, 0, "CAD")}, false},
|
||||
{"matching", args{mmc(1, 0, "USD"), mmc(2, 0, "USD")}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := AreSameCurrency(tt.args.l, tt.args.r); got != tt.want {
|
||||
t.Errorf("AreSameCurrency([%v],[%v]) = %v, want %v", tt.args.l, tt.args.r, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAreEquals(t *testing.T) {
|
||||
type args struct {
|
||||
l pb.Money
|
||||
r pb.Money
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{"equals", args{mmc(1, 2, "USD"), mmc(1, 2, "USD")}, true},
|
||||
{"mismatching currency", args{mmc(1, 2, "USD"), mmc(1, 2, "CAD")}, false},
|
||||
{"mismatching units", args{mmc(10, 20, "USD"), mmc(1, 20, "USD")}, false},
|
||||
{"mismatching nanos", args{mmc(1, 2, "USD"), mmc(1, 20, "USD")}, false},
|
||||
{"negated", args{mmc(1, 2, "USD"), mmc(-1, -2, "USD")}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := AreEquals(tt.args.l, tt.args.r); got != tt.want {
|
||||
t.Errorf("AreEquals([%v],[%v]) = %v, want %v", tt.args.l, tt.args.r, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNegate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in pb.Money
|
||||
want pb.Money
|
||||
}{
|
||||
{"zero", mm(0, 0), mm(0, 0)},
|
||||
{"negative", mm(-1, -200), mm(1, 200)},
|
||||
{"positive", mm(1, 200), mm(-1, -200)},
|
||||
{"carries currency code", mmc(0, 0, "XXX"), mmc(0, 0, "XXX")},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := Negate(tt.in); !AreEquals(got, tt.want) {
|
||||
t.Errorf("Negate([%v]) = %v, want %v", tt.in, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMust_pass(t *testing.T) {
|
||||
v := Must(mm(2, 3), nil)
|
||||
if !AreEquals(v, mm(2, 3)) {
|
||||
t.Errorf("returned the wrong value: %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMust_panic(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Logf("panic captured: %v", r)
|
||||
}
|
||||
}()
|
||||
Must(mm(2, 3), fmt.Errorf("some error"))
|
||||
t.Fatal("this should not have executed due to the panic above")
|
||||
}
|
||||
|
||||
func TestSum(t *testing.T) {
|
||||
type args struct {
|
||||
l pb.Money
|
||||
r pb.Money
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want pb.Money
|
||||
wantErr error
|
||||
}{
|
||||
{"0+0=0", args{mm(0, 0), mm(0, 0)}, mm(0, 0), nil},
|
||||
{"Error: currency code on left", args{mmc(0, 0, "XXX"), mm(0, 0)}, mm(0, 0), ErrMismatchingCurrency},
|
||||
{"Error: currency code on right", args{mm(0, 0), mmc(0, 0, "YYY")}, mm(0, 0), ErrMismatchingCurrency},
|
||||
{"Error: currency code mismatch", args{mmc(0, 0, "AAA"), mmc(0, 0, "BBB")}, mm(0, 0), ErrMismatchingCurrency},
|
||||
{"Error: invalid +/-", args{mm(+1, -1), mm(0, 0)}, mm(0, 0), ErrInvalidValue},
|
||||
{"Error: invalid -/+", args{mm(0, 0), mm(-1, +2)}, mm(0, 0), ErrInvalidValue},
|
||||
{"Error: invalid nanos", args{mm(0, 1000000000), mm(1, 0)}, mm(0, 0), ErrInvalidValue},
|
||||
{"both positive (no carry)", args{mm(2, 200000000), mm(2, 200000000)}, mm(4, 400000000), nil},
|
||||
{"both positive (nanos=max)", args{mm(2, 111111111), mm(2, 888888888)}, mm(4, 999999999), nil},
|
||||
{"both positive (carry)", args{mm(2, 200000000), mm(2, 900000000)}, mm(5, 100000000), nil},
|
||||
{"both negative (no carry)", args{mm(-2, -200000000), mm(-2, -200000000)}, mm(-4, -400000000), nil},
|
||||
{"both negative (carry)", args{mm(-2, -200000000), mm(-2, -900000000)}, mm(-5, -100000000), nil},
|
||||
{"mixed (larger positive, just decimals)", args{mm(11, 0), mm(-2, 0)}, mm(9, 0), nil},
|
||||
{"mixed (larger negative, just decimals)", args{mm(-11, 0), mm(2, 0)}, mm(-9, 0), nil},
|
||||
{"mixed (larger positive, no borrow)", args{mm(11, 100000000), mm(-2, -100000000)}, mm(9, 0), nil},
|
||||
{"mixed (larger positive, with borrow)", args{mm(11, 100000000), mm(-2, -9000000 /*.09*/)}, mm(9, 91000000 /*.091*/), nil},
|
||||
{"mixed (larger negative, no borrow)", args{mm(-11, -100000000), mm(2, 100000000)}, mm(-9, 0), nil},
|
||||
{"mixed (larger negative, with borrow)", args{mm(-11, -100000000), mm(2, 9000000 /*.09*/)}, mm(-9, -91000000 /*.091*/), nil},
|
||||
{"0+negative", args{mm(0, 0), mm(-2, -100000000)}, mm(-2, -100000000), nil},
|
||||
{"negative+0", args{mm(-2, -100000000), mm(0, 0)}, mm(-2, -100000000), nil},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Sum(tt.args.l, tt.args.r)
|
||||
if err != tt.wantErr {
|
||||
t.Errorf("Sum([%v],[%v]): expected err=\"%v\" got=\"%v\"", tt.args.l, tt.args.r, tt.wantErr, err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Sum([%v],[%v]) = %v, want %v", tt.args.l, tt.args.r, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
79
src/frontend/packaging_info.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright 2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
/*
|
||||
As part of an optional Google Cloud demo, you can run an additional "packaging" microservice (HTTP server).
|
||||
This file contains code related to the frontend and the "packaging" microservice.
|
||||
*/
|
||||
|
||||
var (
|
||||
packagingServiceUrl string
|
||||
)
|
||||
|
||||
type PackagingInfo struct {
|
||||
Weight float32 `json:"weight"`
|
||||
Width float32 `json:"width"`
|
||||
Height float32 `json:"height"`
|
||||
Depth float32 `json:"depth"`
|
||||
}
|
||||
|
||||
// init() is a special function in Golang that will run when this package is imported.
|
||||
func init() {
|
||||
packagingServiceUrl = os.Getenv("PACKAGING_SERVICE_URL")
|
||||
}
|
||||
|
||||
func isPackagingServiceConfigured() bool {
|
||||
return packagingServiceUrl != ""
|
||||
}
|
||||
|
||||
func httpGetPackagingInfo(productId string) (*PackagingInfo, error) {
|
||||
// Make the GET request
|
||||
url := packagingServiceUrl + "/" + productId
|
||||
fmt.Println("Requesting packaging info from URL: ", url)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check the response status code
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Read the JSON response body
|
||||
responseBody, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Decode the JSON response into a PackagingInfo struct
|
||||
var packagingInfo PackagingInfo
|
||||
err = json.Unmarshal(responseBody, &packagingInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &packagingInfo, nil
|
||||
}
|
||||
127
src/frontend/rpc.go
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
pb "github.com/GoogleCloudPlatform/microservices-demo/src/frontend/genproto"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
avoidNoopCurrencyConversionRPC = false
|
||||
)
|
||||
|
||||
func (fe *frontendServer) getCurrencies(ctx context.Context) ([]string, error) {
|
||||
currs, err := pb.NewCurrencyServiceClient(fe.currencySvcConn).
|
||||
GetSupportedCurrencies(ctx, &pb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []string
|
||||
for _, c := range currs.CurrencyCodes {
|
||||
if _, ok := whitelistedCurrencies[c]; ok {
|
||||
out = append(out, c)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (fe *frontendServer) getProducts(ctx context.Context) ([]*pb.Product, error) {
|
||||
resp, err := pb.NewProductCatalogServiceClient(fe.productCatalogSvcConn).
|
||||
ListProducts(ctx, &pb.Empty{})
|
||||
return resp.GetProducts(), err
|
||||
}
|
||||
|
||||
func (fe *frontendServer) getProduct(ctx context.Context, id string) (*pb.Product, error) {
|
||||
resp, err := pb.NewProductCatalogServiceClient(fe.productCatalogSvcConn).
|
||||
GetProduct(ctx, &pb.GetProductRequest{Id: id})
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (fe *frontendServer) getCart(ctx context.Context, userID string) ([]*pb.CartItem, error) {
|
||||
resp, err := pb.NewCartServiceClient(fe.cartSvcConn).GetCart(ctx, &pb.GetCartRequest{UserId: userID})
|
||||
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
|
||||
}
|
||||
return pb.NewCurrencyServiceClient(fe.currencySvcConn).
|
||||
Convert(ctx, &pb.CurrencyConversionRequest{
|
||||
From: money,
|
||||
ToCode: currency})
|
||||
}
|
||||
|
||||
func (fe *frontendServer) getShippingQuote(ctx context.Context, items []*pb.CartItem, currency string) (*pb.Money, error) {
|
||||
quote, err := pb.NewShippingServiceClient(fe.shippingSvcConn).GetQuote(ctx,
|
||||
&pb.GetQuoteRequest{
|
||||
Address: nil,
|
||||
Items: items})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
localized, err := fe.convertCurrency(ctx, quote.GetCostUsd(), currency)
|
||||
return localized, errors.Wrap(err, "failed to convert currency for shipping cost")
|
||||
}
|
||||
|
||||
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, errors.Wrapf(err, "failed to get recommended product info (#%s)", v)
|
||||
}
|
||||
out[i] = p
|
||||
}
|
||||
if len(out) > 4 {
|
||||
out = out[:4] // take only first four to fit the UI
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (fe *frontendServer) getAd(ctx context.Context, ctxKeys []string) ([]*pb.Ad, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Millisecond*100)
|
||||
defer cancel()
|
||||
|
||||
resp, err := pb.NewAdServiceClient(fe.adSvcConn).GetAds(ctx, &pb.AdRequest{
|
||||
ContextKeys: ctxKeys,
|
||||
})
|
||||
return resp.GetAds(), errors.Wrap(err, "failed to get ads")
|
||||
}
|
||||
BIN
src/frontend/static/favicon-cymbal.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src/frontend/static/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
170
src/frontend/static/icons/Cymbal_NavLogo.svg
Normal file
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:version="1.0 (4035a4f, 2020-05-01)"
|
||||
sodipodi:docname="Cymbal_NavLogo.svg"
|
||||
id="svg835"
|
||||
version="1.1"
|
||||
fill="none"
|
||||
viewBox="0 0 85.633156 28.251238"
|
||||
height="28.251238"
|
||||
width="85.633156">
|
||||
<metadata
|
||||
id="metadata841">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs839" />
|
||||
<sodipodi:namedview
|
||||
inkscape:current-layer="svg835"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-x="0"
|
||||
inkscape:cy="17.327124"
|
||||
inkscape:cx="59.485955"
|
||||
inkscape:zoom="13.692308"
|
||||
fit-margin-bottom="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-top="0"
|
||||
inkscape:snap-global="false"
|
||||
showguides="false"
|
||||
showgrid="false"
|
||||
id="namedview837"
|
||||
inkscape:window-height="1096"
|
||||
inkscape:window-width="2277"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff" />
|
||||
<path
|
||||
id="path833"
|
||||
fill="#000000"
|
||||
d="m 38.248749,11.252164 c -3.156207,0 -5.719697,-2.45769 -5.719697,-5.64087 C 32.529052,2.428104 35.062912,0 38.219119,0 c 1.95596,0 3.689611,0.903134 4.771411,2.546554 0.3556,0.53299 0.6371,1.14002 0.726,1.77666 h -2.904281 c -0.42972,-1.11042 -1.4077,-1.89511 -2.62277,-1.89511 -1.76333,0 -2.904307,1.49536 -2.904307,3.16838 0,1.67303 1.140977,3.2276 2.919127,3.2276 1.21507,0 2.11896,-0.75508 2.60795,-1.83588 h 2.904281 c -0.6223,2.5761596 -2.845011,4.26396 -5.467781,4.26396 z" />
|
||||
<g
|
||||
transform="translate(63.326481,0.033206)"
|
||||
id="g867"
|
||||
style="fill:none">
|
||||
<path
|
||||
d="m 5.56306,11.2706 c -0.8298,0 -1.9115,-0.3553 -2.3264,-1.14 H 3.20702 v 0.8587 H 0.880615 V 0.166504 H 3.31075 V 3.51255 C 3.94792,2.8463 4.7629,2.59461 5.66679,2.59461 c 0.59271,0 1.17061,0.14805 1.71887,0.39975 1.5707,0.72547 2.28196,2.20602 2.28196,3.89385 0,2.44291 -1.5707,4.38239 -4.10456,4.38239 z M 5.25189,4.85985 c -1.15579,0 -2.01523,0.90314 -2.01523,2.04316 0,1.15483 0.78534,2.08758 1.97077,2.08758 1.17062,0 2.03006,-0.91794 2.03006,-2.07277 0,-1.11042 -0.84462,-2.05797 -1.9856,-2.05797 z"
|
||||
fill="#000000"
|
||||
id="path856" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(42.505839,2.180044)"
|
||||
id="g854"
|
||||
style="fill:none">
|
||||
<path
|
||||
d="M 20.8287,4.56236 V 9.00402 H 18.3986 V 4.68081 c 0,-0.88833 -0.1186,-1.80628 -1.2447,-1.80628 -1.0817,0 -1.3484,0.75509 -1.3484,1.68783 V 9.00402 H 13.3753 V 4.57717 c 0,-0.82911 -0.1926,-1.70264 -1.2151,-1.70264 -1.1261,0 -1.378,0.84392 -1.378,1.80628 V 9.00402 H 8.35205 V 3.8517 L 2.9287,11.7134 H 0.0688477 L 1.684,9.38897 V 0.875788 H 4.11413 V 5.80603 H 4.18822 L 7.53706,0.875788 H 10.6192 V 1.73451 h 0.0296 c 0.4742,-0.829111 1.3188,-1.140028 2.2375,-1.140028 1.0521,0 1.8523,0.532998 2.3412,1.450938 0.5928,-0.91794 1.4818,-1.450938 2.5784,-1.450938 0.8001,0 1.5707,0.236889 2.1337,0.814308 0.9336,0.94755 0.8891,1.90991 0.8891,3.15357 z"
|
||||
fill="#000000"
|
||||
id="path843" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(73.255785,2.027234)"
|
||||
id="g880"
|
||||
style="fill:none">
|
||||
<path
|
||||
d="M 6.4988,8.98937 V 8.11585 H 6.46917 C 6.02463,8.94496 4.97256,9.27068 4.0983,9.27068 c -2.50423,0 -4.08975508,-1.93953 -4.08975508,-4.35283 0,-2.36888 1.64479508,-4.338016 4.08975508,-4.338016 0.88907,0 1.8967,0.340527 2.37087,1.140026 H 6.4988 V 0.861139 H 8.92895 V 8.98937 Z M 4.45393,2.85988 c -1.18544,0 -2.00042,0.93275 -2.00042,2.08758 0,1.12522 0.87426,2.04317 2.01524,2.04317 1.18544,0 2.03005,-0.90314 2.03005,-2.07278 0,-1.16963 -0.85944,-2.05797 -2.04487,-2.05797 z"
|
||||
fill="#000000"
|
||||
id="path869" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(83.133197,-0.02268)"
|
||||
id="g893"
|
||||
style="fill:none">
|
||||
<path
|
||||
d="M 0.0698242,10.9893 V 0.166504 H 2.49996 V 11.0041 H 0.0698242 Z"
|
||||
fill="#000000"
|
||||
id="path882" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(32.708522,14.032818)"
|
||||
id="g906"
|
||||
style="fill:none">
|
||||
<path
|
||||
d="m 4.18626,11.7607 c -2.19305,0 -3.926739,-1.1253 -3.926739,-3.44973 V 7.95564 H 3.03047 c 0,0.71067 0.32599,1.48056 1.12616,1.48056 0.57789,0 1.05206,-0.45898 1.05206,-1.03639 C 5.20869,7.68914 4.61598,7.46706 4.0529,7.21537 3.7269,7.06731 3.40091,6.93406 3.08974,6.80081 1.68204,6.20859 0.437336,5.39428 0.437336,3.69165 c 0,-2.02836 1.955964,-3.197998 3.808194,-3.197998 1.05207,0 2.25232,0.39975 2.93394,1.228858 0.56308,0.69586 0.69644,1.28808 0.72608,2.1468 H 5.16424 C 5.07533,3.26229 4.86788,2.81812 4.17144,2.81812 c -0.48899,0 -0.94834,0.34053 -0.94834,0.85872 0,0.16286 0.01482,0.32572 0.10372,0.45897 0.26672,0.44417 1.68924,0.99198 2.1486,1.19925 C 6.91275,6.00131 8.02409,6.74159 8.02409,8.44422 7.97964,10.7095 6.33485,11.7607 4.18626,11.7607 Z"
|
||||
fill="#000000"
|
||||
id="path895" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(40.443303,14.020917)"
|
||||
id="g919"
|
||||
style="fill:none">
|
||||
<path
|
||||
d="M 5.89196,11.5535 V 7.12668 c 0,-0.88833 -0.25191,-1.70264 -1.30398,-1.70264 -1.05207,0 -1.36324,0.69586 -1.36324,1.61381 V 11.5535 H 0.779785 V 0.686279 H 3.22474 V 4.15077 H 3.25437 C 3.68409,3.38088 4.52871,3.12919 5.35852,3.12919 c 0.80016,0 1.68923,0.2813 2.22268,0.88833 0.78535,0.88833 0.77053,1.8655 0.77053,2.97591 V 11.5387 H 5.89196 Z"
|
||||
fill="#000000"
|
||||
id="path908" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(48.369792,16.988328)"
|
||||
id="g932"
|
||||
style="fill:none">
|
||||
<path
|
||||
d="m 5.38968,8.80519 c -2.50422,0 -4.40091,-1.83589 -4.40091,-4.33802 0,-2.50214 1.89669,-4.33802 4.40091,-4.33802 2.50423,0 4.40092,1.83588 4.40092,4.33802 0,2.50213 -1.89669,4.33802 -4.40092,4.33802 z m 0,-6.38118 c -1.12616,0 -1.95596,0.94755 -1.95596,2.05796 0,1.11042 0.8298,2.05797 1.95596,2.05797 1.12616,0 1.95597,-0.94755 1.95597,-2.05797 0,-1.11041 -0.81499,-2.05796 -1.95597,-2.05796 z"
|
||||
fill="#000000"
|
||||
id="path921" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(58.368261,16.973537)"
|
||||
id="g945"
|
||||
style="fill:none">
|
||||
<path
|
||||
d="m 5.40643,8.83479 c -0.96316,0 -1.79296,-0.29611 -2.45977,-1.00677 V 11.2777 H 0.501709 V 0.410455 H 2.8133 V 1.29879 H 2.82812 2.85775 C 3.49492,0.499289 4.39882,0.12915 5.40643,0.12915 7.9403,0.12915 9.39245,2.20192 9.39245,4.556 9.37763,6.83605 7.79212,8.83479 5.40643,8.83479 Z M 4.90263,2.4092 c -1.17062,0 -2.04488,0.91794 -2.04488,2.08758 0,1.14002 0.87426,2.07277 2.04488,2.07277 1.15579,0 2.04487,-0.91794 2.04487,-2.07277 C 6.93268,3.32714 6.07324,2.4092 4.90263,2.4092 Z"
|
||||
fill="#000000"
|
||||
id="path934" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(67.2903,17.010531)"
|
||||
id="g958"
|
||||
style="fill:none">
|
||||
<path
|
||||
d="m 3.94884,8.83463 c -1.70405,0 -3.126569,-0.90314 -3.319201,-2.6946 H 3.05977 c 0.07409,0.50338 0.44454,0.75508 0.91871,0.75508 0.37045,0 0.85943,-0.20728 0.85943,-0.63664 0,-0.59222 -0.6668,-0.76988 -1.09652,-0.93274 C 3.38576,5.20728 3.03013,5.08884 2.68932,4.95559 1.72616,4.60026 0.74818,3.97843 0.74818,2.8236 c 0,-1.70264 1.58551,-2.679801 3.15621,-2.679801 1.62997,0 2.96357,0.843913 3.12657,2.576161 H 4.63046 C 4.61565,2.26098 4.28965,2.09812 3.85993,2.09812 c -0.32599,0 -0.69643,0.17767 -0.69643,0.54781 0,1.27327 4.10454,0.56261 4.10454,3.43488 0,1.82107 -1.6596,2.75382 -3.3192,2.75382 z"
|
||||
fill="#000000"
|
||||
id="path947" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(12.600322,11.81969)"
|
||||
id="g1017"
|
||||
style="fill:none">
|
||||
<path
|
||||
d="M 13.0939,13.6186 V 4.09905 c 0,-1.7213 -1.408,-3.143239 -3.14566,-3.143239 H 3.29732 c -1.72265,0 -3.145709,1.406979 -3.145709,3.143239 v 6.39125 c 0,1.7213 1.408079,3.1433 3.145709,3.1433 z"
|
||||
fill="#840237"
|
||||
id="path1006" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(5.816672,0.035276)"
|
||||
id="g1030"
|
||||
style="fill:none">
|
||||
<path
|
||||
d="M 13.6377,9.82748 V 3.48112 c 0,-1.75124 -1.4231,-3.188151 -3.1907,-3.188151 H 3.84102 c -1.72265,0 -3.145708,1.406971 -3.145708,3.143241 V 12.9558 H 10.477 c 1.7376,0 3.1607,-1.407 3.1607,-3.12832 z"
|
||||
fill="#ce0631"
|
||||
id="path1019" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(-0.239258,12.021229)"
|
||||
id="g1043"
|
||||
style="fill:none">
|
||||
<path
|
||||
d="M 13.1666,10.4903 V 4.09905 c 0,-1.7213 -1.4081,-3.143239 -3.1457,-3.143239 H 0.239258 V 10.4753 c 0,1.7213 1.408082,3.1433 3.145712,3.1433 h 6.65093 c 1.7226,0 3.1307,-1.407 3.1307,-3.1283 z"
|
||||
fill="#ff6631"
|
||||
id="path1032" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.9 KiB |
1
src/frontend/static/icons/Hipster_Advert2.svg
Normal file
|
After Width: | Height: | Size: 19 KiB |
69
src/frontend/static/icons/Hipster_CartIcon.svg
Normal file
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:version="1.0 (4035a4f, 2020-05-01)"
|
||||
height="11.002236"
|
||||
width="10.434605"
|
||||
sodipodi:docname="Hipster_CartIcon.svg"
|
||||
version="1.1"
|
||||
viewBox="0 0 10.434605 11.002236"
|
||||
data-name="Layer 1"
|
||||
id="Layer_1">
|
||||
<metadata
|
||||
id="metadata1201">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>Hipster</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
inkscape:current-layer="g1196"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-x="625"
|
||||
inkscape:cy="-0.89051508"
|
||||
inkscape:cx="22.434662"
|
||||
inkscape:zoom="20.841972"
|
||||
fit-margin-bottom="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-top="0"
|
||||
showgrid="false"
|
||||
id="namedview1199"
|
||||
inkscape:window-height="1387"
|
||||
inkscape:window-width="1935"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff" />
|
||||
<defs
|
||||
id="defs1188">
|
||||
<style
|
||||
id="style1186">.cls-1{fill:#b4b2bb}</style>
|
||||
</defs>
|
||||
<title
|
||||
id="title1190">Hipster</title>
|
||||
<g
|
||||
transform="translate(-4.4609375,-5.5)"
|
||||
id="g1196">
|
||||
<path
|
||||
sodipodi:nodetypes="ccccccsccccccccccccccccccssssssss"
|
||||
d="m 4.4609375,5.5 v 1.0996094 h 1.0996094 l 2,4.1699216 -0.75,1.34961 c -0.083803,0.164211 -0.1291659,0.346898 -0.1308594,0.53125 0,0.607513 0.4920962,1.099609 1.0996094,1.099609 H 14.380859 V 12.650391 H 8 c -0.081192,0.0058 -0.1505985,-0.05923 -0.1503906,-0.140625 v -0.06055 l 0.4902344,-0.898438 h 4.0996092 c 0.414076,0.01348 0.800604,-0.207156 1,-0.570312 l 1.456089,-4.4008431 -8.1260108,0.019984 v 0 L 6.25,5.5 Z M 7.1729534,7.576157 13.673828,7.54223 12.681641,10.603516 8.5214844,10.589846 Z m 0.5868122,6.724624 c -0.9800635,0 -1.4702693,1.184027 -0.7773437,1.876953 0.6929255,0.692925 1.8769531,0.20272 1.8769531,-0.777343 0,-0.607513 -0.4920962,-1.09961 -1.0996094,-1.09961 z m 5.5000004,0 c -0.980063,0 -1.470269,1.184027 -0.777344,1.876953 0.692926,0.692925 1.876953,0.20272 1.876953,-0.777343 0,-0.607513 -0.492096,-1.09961 -1.099609,-1.09961 z"
|
||||
id="path1192" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
1
src/frontend/static/icons/Hipster_CheckOutIcon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 20 20"><defs><style>.cls-1{fill:#b4b2bb}</style></defs><title>Hipster</title><g><path d="M14.5,5.5h-9A1.12,1.12,0,0,0,4.38,6.62v6.76A1.12,1.12,0,0,0,5.5,14.5h9a1.12,1.12,0,0,0,1.12-1.12V6.62A1.12,1.12,0,0,0,14.5,5.5Zm0,7.88h-9V10h9Zm0-5.63h-9V6.62h9Z" class="cls-1"/><path d="M10,2a8,8,0,1,1-8,8,8,8,0,0,1,8-8m0-2A10,10,0,1,0,20,10,10,10,0,0,0,10,0Z" class="cls-1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 462 B |
1
src/frontend/static/icons/Hipster_CurrencyIcon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 20 20"><defs><style>.cls-1{fill:#605f64}</style></defs><title>Hipster</title><g><path d="M10.28,9.21c-1.64-.43-2.16-.87-2.16-1.56s.72-1.33,2-1.33,1.76.61,1.8,1.51h1.6a2.88,2.88,0,0,0-2.32-2.75V3.5H9V5.06A2.82,2.82,0,0,0,6.45,7.67c0,1.67,1.38,2.5,3.4,3,1.8.43,2.16,1.07,2.16,1.74,0,.5-.35,1.29-1.95,1.29S8,13,7.91,12.17H6.32A3,3,0,0,0,9,14.93V16.5h2.17V15c1.41-.27,2.53-1.09,2.53-2.57C13.68,10.33,11.92,9.63,10.28,9.21Z" class="cls-1"/><path d="M10,2a8,8,0,1,1-8,8,8,8,0,0,1,8-8m0-2A10,10,0,1,0,20,10,10,10,0,0,0,10,0Z" class="cls-1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 630 B |
63
src/frontend/static/icons/Hipster_DownArrow.svg
Normal file
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:version="1.0 (4035a4f, 2020-05-01)"
|
||||
sodipodi:docname="Hipster_DownArrow.svg"
|
||||
version="1.1"
|
||||
viewBox="0 0 10 6"
|
||||
data-name="Layer 1"
|
||||
id="Layer_1">
|
||||
<metadata
|
||||
id="metadata1241">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
inkscape:current-layer="g1805"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:window-y="432"
|
||||
inkscape:window-x="453"
|
||||
inkscape:cy="7.8920446"
|
||||
inkscape:cx="9.2114456"
|
||||
inkscape:zoom="27.4"
|
||||
showgrid="false"
|
||||
id="namedview1239"
|
||||
inkscape:window-height="815"
|
||||
inkscape:window-width="1338"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff" />
|
||||
<defs
|
||||
id="defs1232">
|
||||
<style
|
||||
id="style1230">.cls-1{fill:#605f64}</style>
|
||||
</defs>
|
||||
<title
|
||||
id="title1234">Hipster</title>
|
||||
<g
|
||||
style="opacity:1"
|
||||
id="g1805">
|
||||
<path
|
||||
sodipodi:nodetypes="cccc"
|
||||
id="path1841"
|
||||
d="M 0.04897564,0.08494176 5.0087697,6.0165613 9.9685637,0.08494176 Z"
|
||||
style="fill:#5c6063;stroke:none;stroke-width:0.08082460000000000px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
src/frontend/static/icons/Hipster_FacebookIcon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#111}</style></defs><title>Hipster</title><g><path d="M24,0A24,24,0,1,0,48,24,24,24,0,0,0,24,0ZM14.42,17.76H10.24v4.87h3.28v2.31H10.24v6.68H7.69V15.45h6.73ZM21,31.62l-.44-2.94H17.44L17,31.62H14.67l2.59-16.17H21l2.58,16.17ZM32.11,20.9h-2.4V19.16c0-1.15-.51-1.59-1.32-1.59s-1.32.44-1.32,1.59V27.9c0,1.15.51,1.57,1.32,1.57s1.32-.42,1.32-1.57V25.59h2.4v2.15c0,2.58-1.29,4.06-3.79,4.06s-3.79-1.48-3.79-4.06V19.33c0-2.59,1.29-4.07,3.79-4.07s3.79,1.48,3.79,4.07Zm8.47-3.14H36.19v4.5h3.49v2.31H36.19v4.74h4.39v2.31H33.65V15.45h6.93Z" class="cls-1"/><polygon points="17.77 26.49 20.21 26.49 18.99 18.31 17.77 26.49" class="cls-1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 750 B |
1
src/frontend/static/icons/Hipster_GooglePlayIcon.svg
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
1
src/frontend/static/icons/Hipster_HelpIcon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 20 20"><defs><style>.cls-1{fill:#605f64}</style></defs><title>Hipster</title><g><path d="M9,16h2V14H9ZM10,4A4,4,0,0,0,6,8H8a2,2,0,0,1,4,0c0,2-3,1.75-3,5h2c0-2.25,3-2.5,3-5A4,4,0,0,0,10,4Z" class="cls-1"/><path d="M10,2a8,8,0,1,1-8,8,8,8,0,0,1,8-8m0-2A10,10,0,1,0,20,10,10,10,0,0,0,10,0Z" class="cls-1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 399 B |
1
src/frontend/static/icons/Hipster_HeroLogo.svg
Normal file
|
After Width: | Height: | Size: 16 KiB |
235
src/frontend/static/icons/Hipster_HeroLogoMaroon.svg
Normal file
@@ -0,0 +1,235 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
id="Layer_1"
|
||||
data-name="Layer 1"
|
||||
viewBox="0 0 625.15 469.57"
|
||||
version="1.1"
|
||||
sodipodi:docname="Hipster_HeroLogoMaroon.svg"
|
||||
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<sodipodi:namedview
|
||||
id="namedview213"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.0051749"
|
||||
inkscape:cx="299.94779"
|
||||
inkscape:cy="275.0765"
|
||||
inkscape:window-width="1724"
|
||||
inkscape:window-height="905"
|
||||
inkscape:window-x="204"
|
||||
inkscape:window-y="1141"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="g210" />
|
||||
<defs
|
||||
id="defs134">
|
||||
<style
|
||||
id="style132">.cls-1{fill:#4bc7c7}</style>
|
||||
</defs>
|
||||
<title
|
||||
id="title136">Hipster</title>
|
||||
<g
|
||||
id="g210"
|
||||
style="stroke-width:1.75748031;stroke-dasharray:none;paint-order:stroke markers fill;stroke:#ffffff;stroke-opacity:1">
|
||||
<g
|
||||
id="g1317-3"
|
||||
transform="matrix(1.0146883,0,0,1.0241514,-4.2853293,-6.1372338)"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.72401905;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill">
|
||||
<g
|
||||
id="g196-5"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
|
||||
transform="matrix(0.97135514,0,0,0.96186606,8.9536653,8.9521229)">
|
||||
<path
|
||||
d="m 214.76,32.83 -4.44,-9 a 231,231 0 0 1 202.19,-1.14 l -4.34,9 a 221,221 0 0 0 -193.41,1.09 z"
|
||||
class="cls-1"
|
||||
id="path138-2"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<g
|
||||
id="g144-2"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill">
|
||||
<path
|
||||
d="m 120.84,339.47 q -1.55,-2.72 -3,-5.47 h -11.27 q 2.66,5.28 5.58,10.42 a 232,232 0 0 0 79.29,82.22 l 5.26,-8.5 a 222,222 0 0 1 -75.86,-78.67 z"
|
||||
class="cls-1"
|
||||
id="path140-6"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 117.78,127 a 221.75,221.75 0 0 1 21.57,-32.94 l -7.85,-6.2 a 231.18,231.18 0 0 0 -25,39.14 z"
|
||||
class="cls-1"
|
||||
id="path142-6"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
</g>
|
||||
<g
|
||||
id="g158-7"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill">
|
||||
<path
|
||||
d="m 198.23,116.53 -0.38,-0.86 -0.29,-0.77 -0.58,-1.73 -0.19,-0.86 -0.1,-0.87 -0.19,-0.86 -0.29,-2.6 v -1.82 l 0.1,-0.87 v -2.69 l 0.19,-1.72 0.19,-1 0.1,-0.87 0.19,-0.77 0.19,-0.86 0.29,-1 0.39,-1.06 0.57,-1.92 0.39,-1.06 0.38,-0.86 0.39,-0.77 0.38,-0.86 0.38,-0.77 0.39,-0.87 0.48,-0.76 0.48,-0.68 1,-0.57 1.06,-0.29 0.86,0.09 0.77,0.29 0.77,0.48 0.76,0.77 0.49,1.06 v 0.86 l -0.49,1.15 -0.47,1.25 -0.29,0.58 -0.39,1 -0.38,0.77 -0.29,0.86 -0.19,0.87 -0.29,1.15 -0.29,1.25 -0.19,0.86 -0.1,0.87 -0.19,0.86 -0.09,0.87 v 0.76 l -0.2,1.73 v 0.87 l 0.1,1 v 1.73 l 0.1,0.86 0.19,0.87 0.19,1.24 0.29,1.54 0.38,1.25 0.29,0.86 0.77,1.54 0.58,0.77 0.47,0.58 0.58,0.67 1.35,1.15 0.67,0.48 0.77,0.38 0.86,0.29 1.25,0.29 1.15,0.19 0.87,0.2 h 2.49 l 0.87,-0.2 0.86,-0.09 0.87,-0.29 1,-0.39 0.77,-0.38 1.44,-0.86 0.77,-0.48 1.35,-1 1.92,-1.93 0.57,-0.67 0.48,-0.67 0.48,-0.77 0.58,-0.86 0.38,-0.48 1.06,-1.54 0.38,-0.67 0.48,-0.77 0.39,-0.86 0.48,-0.87 0.67,-1.44 0.3,-0.83 0.38,-0.77 0.1,-0.77 -0.29,-0.39 -0.57,-0.19 -0.87,-0.09 -1,-0.1 -0.86,-0.19 -0.77,-0.19 -0.87,-0.29 -0.86,-0.19 -0.77,-0.29 -0.77,-0.39 -2.3,-0.86 -1.06,-0.38 -2.88,-1.73 -0.77,-0.48 -1.44,-1 -0.67,-0.58 -3.65,-3.65 -0.48,-0.67 -0.58,-0.67 -1,-1.45 -0.38,-0.67 -0.39,-0.86 -0.77,-1.54 -0.38,-1.06 -0.38,-1 -0.29,-1 -0.39,-1.05 -0.28,-0.87 -0.2,-1.73 -0.09,-1 v -0.4 l -0.19,-1.92 0.09,-0.87 0.1,-1 0.09,-0.57 0.2,-1.06 0.24,-1.08 0.38,-1.25 0.29,-0.87 0.48,-1.15 0.48,-1.06 0.38,-0.86 0.48,-0.67 1.16,-1.35 0.57,-0.57 1.38,-1.22 1.05,-0.76 1.35,-0.74 2.69,-1.35 0.86,-0.29 1.25,-0.28 1.15,-0.29 1,-0.19 1.16,-0.1 h 2.21 l 1.53,0.19 1.92,0.1 1.16,0.19 1.25,0.29 0.86,0.38 1.73,0.58 1.05,0.57 1.64,1 1,0.57 0.67,0.48 0.67,0.58 1.15,1.34 0.49,0.68 0.48,0.76 0.48,0.68 0.38,0.76 0.38,0.87 0.77,1.54 0.29,0.76 0.29,0.87 0.38,1.73 0.19,1.25 0.29,1.15 0.1,1 0.1,0.58 0.09,1 0.1,0.87 v 0.86 l 0.19,1.73 v 0.86 l -0.1,1 -0.09,0.87 -0.19,0.77 v 1 l -0.1,0.58 v 1.06 l -0.19,0.86 -0.28,1.14 -0.19,1.25 -0.19,1 -0.1,0.87 -0.09,0.76 -0.19,0.87 -0.29,0.86 v 1.06 l 0.57,0.58 h 1.06 l 0.87,-0.2 1.05,-0.28 0.58,-0.2 1.82,-0.48 1.06,-0.38 1.25,-0.48 1,-0.38 0.87,-0.49 0.58,-0.28 0.86,-0.48 0.86,-0.39 0.77,-0.09 0.48,0.38 0.1,0.86 -0.19,0.77 -0.29,0.87 -0.39,0.77 -0.57,0.57 -0.77,0.48 -1,0.39 -0.48,0.28 -1.73,0.87 -0.86,0.38 -0.77,0.29 -1,0.39 -0.58,0.19 -1,0.48 -0.77,0.29 -0.77,0.19 -0.86,0.19 -1,0.29 -1,0.19 -0.87,0.1 -0.76,0.19 -0.58,0.48 -0.49,1.49 -0.29,0.86 -0.51,1.14 -1.44,2.88 -0.29,0.48 -0.48,0.87 -0.38,0.86 -1,1.54 -0.38,0.48 -0.58,0.86 -0.48,0.68 -1.11,1.35 -0.48,0.77 -0.58,0.67 -0.67,0.58 -0.68,0.67 -0.57,0.48 -1.25,1.25 -1.34,1.15 -0.77,0.48 -1.06,0.58 -1.15,0.67 -0.87,0.48 -0.76,0.39 -0.87,0.28 -0.77,0.2 -0.86,0.28 -0.87,0.2 -0.86,0.28 -1,0.1 -0.58,0.1 -1,0.09 -0.86,0.1 h -2.61 l -1.15,-0.19 -1.06,-0.1 -2.39,-0.51 -1.15,-0.29 -0.87,-0.29 -0.77,-0.28 -0.86,-0.49 -1.92,-1.34 -0.77,-0.67 -0.58,-0.58 -0.67,-0.57 -0.58,-0.58 -0.57,-0.77 -0.67,-1 -0.58,-1.06 -0.48,-0.86 z m 17.1,-39.86 v 2.6 l 0.09,1 0.2,0.86 0.09,0.86 0.58,1.73 0.29,0.77 0.38,0.87 0.38,0.76 0.48,0.68 0.48,0.86 0.39,0.77 1.15,1.34 0.67,0.68 0.39,0.48 1.34,1.34 0.67,0.58 0.77,0.57 0.68,0.48 0.76,0.48 0.68,0.48 1,0.49 0.57,0.19 1.06,0.48 0.58,0.19 1,0.38 2.4,0.77 1,0.19 0.86,0.2 0.77,-0.1 0.67,-0.67 0.39,-1.06 0.57,-2.59 0.19,-1.25 0.1,-1.15 0.1,-1 0.29,-2.6 v -0.86 l 0.19,-1.15 v -5.19 l -0.1,-1.15 -0.09,-1 -0.2,-1.16 -0.19,-1.24 -0.38,-1.73 -0.29,-0.87 -0.48,-1.15 -0.39,-1.06 -0.38,-0.86 -0.48,-0.77 -0.48,-0.67 -0.48,-0.48 -0.58,-0.67 -0.76,-0.58 -0.68,-0.48 -0.77,-0.38 -0.86,-0.39 -1.25,-0.29 -1.7,-0.15 -1.2,-0.05 -1,0.1 -1.25,0.19 -1.44,0.48 -1.44,0.77 -1.35,0.87 -1.15,1 -0.87,1 -0.38,0.77 -0.48,1.15 -0.44,1.4 -0.29,1.27 -0.19,1 z"
|
||||
class="cls-1"
|
||||
id="path146-8"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 275.16,91.85 -0.76,0.38 -0.68,0.39 -0.67,0.48 -0.77,0.57 -0.67,0.58 -1.54,1.06 -0.67,0.48 -0.67,0.57 -0.67,0.67 -1.16,1.35 -0.67,0.67 -1.15,1.35 -1,1.53 -0.58,0.77 -1.11,2.3 -1.06,3.17 -0.28,1.24 -0.39,1.44 -0.38,1.25 -0.58,2.6 -0.38,1.24 -0.39,1.45 -0.28,1.24 -0.29,0.87 -0.29,1.25 -0.19,1 -0.29,1 -0.1,0.87 -0.19,0.77 -0.29,0.77 -0.57,0.48 -0.87,0.28 -0.86,0.1 -1.25,0.1 -1.46,-0.12 -1.25,-0.19 -0.77,-0.39 -0.58,-0.86 -0.09,-1.06 0.19,-1 0.29,-1.25 0.29,-1.15 0.19,-0.86 0.29,-0.87 0.19,-0.86 0.29,-0.77 0.29,-0.87 0.19,-0.86 0.19,-0.77 0.19,-0.86 0.29,-0.87 0.29,-0.77 0.38,-1.72 0.19,-0.77 0.1,-0.87 0.29,-0.86 0.19,-0.87 0.29,-0.86 0.29,-1.15 0.38,-1.25 0.48,-1.73 0.19,-0.77 0.2,-0.86 0.28,-0.87 0.2,-0.86 0.28,-0.77 0.2,-0.86 0.28,-0.87 0.29,-1.25 0.29,-1.15 0.19,-0.86 0.58,-1.73 0.19,-0.77 0.29,-0.87 0.38,-1.72 0.68,-1 1.24,-0.67 1.54,-0.19 1.54,0.19 1.44,0.48 1,1 0.19,1.16 -0.19,1 -0.19,0.67 -0.29,0.86 -0.19,0.87 -0.29,0.77 -0.19,0.86 -0.29,0.86 -0.19,0.87 -0.1,0.58 0.1,0.09 0.38,-0.48 0.58,-0.67 0.48,-0.67 1.15,-1.35 0.67,-0.57 1.25,-1.06 0.67,-0.58 1.25,-1.25 1.92,-1.53 0.77,-0.48 0.77,-0.39 0.77,-0.48 0.77,-0.29 0.86,-0.28 1.25,-0.29 1.15,-0.19 h 2.21 l 1.54,0.19 1.05,0.19 0.87,0.38 0.86,0.48 0.67,0.68 0.48,0.67 0.48,0.77 0.39,0.77 0.29,0.86 0.29,1.15 0.19,1.16 v 1.82 l -0.1,0.87 v 0.86 l -0.09,0.77 -0.2,0.86 -0.19,1 -0.19,0.58 -0.29,1 -0.29,1.25 -0.38,1.4 -0.39,1.25 -0.28,0.86 -0.29,1.25 -0.39,1.63 -0.38,1.54 -0.38,1.34 -0.2,0.87 -0.19,0.67 -0.19,0.87 -0.29,0.86 -0.19,0.77 -0.58,2.59 -0.09,0.87 v 1.72 l 0.38,0.77 0.67,0.48 0.87,0.1 0.86,-0.29 0.87,-0.38 0.76,-0.48 0.68,-0.48 0.77,-0.58 1.24,-1.25 0.48,-0.67 0.58,-0.77 0.48,-0.67 0.67,-0.77 0.48,-0.67 0.58,-0.87 0.86,-1.44 0.58,-0.86 0.29,-0.48 0.57,-0.87 0.48,-0.86 0.77,-1.54 0.39,-0.86 0.29,-0.77 0.24,-0.88 0.48,-0.77 0.87,-0.58 1.25,0.29 0.76,1 v 1 l -0.28,0.86 -0.39,0.77 -0.38,0.87 -0.39,0.77 -0.48,0.76 -0.38,0.77 -0.58,0.87 -0.28,0.57 -1,1.73 -0.19,0.58 -0.48,0.86 -1,1.54 -0.57,0.86 -0.39,0.48 -0.57,0.77 -0.68,0.87 -0.57,0.67 -1.06,1.15 -0.58,0.67 -1.34,1.16 -0.58,0.57 -0.76,0.58 -0.77,0.48 -1.54,0.77 -0.77,0.29 -0.86,0.28 -0.87,0.2 -0.77,0.09 -1,0.1 h -0.86 l -0.86,-0.1 -0.77,-0.19 -0.87,-0.29 -0.77,-0.29 -0.76,-0.48 -0.58,-0.48 -0.58,-0.67 -0.38,-0.86 -0.29,-0.77 -0.19,-0.87 -0.19,-0.76 V 119 l 0.19,-1.25 0.09,-1 0.29,-1.25 0.87,-3.16 0.29,-1.64 0.52,-1.7 0.38,-1.54 0.39,-1.25 0.29,-0.86 0.38,-1.25 0.38,-1.54 0.29,-1.53 0.39,-1.25 0.38,-1.73 0.1,-0.86 0.38,-1.83 0.1,-0.86 -0.19,-1 -0.58,-1 -0.87,-0.87 -0.86,-0.29 -0.86,-0.09 -0.87,0.09 z"
|
||||
class="cls-1"
|
||||
id="path148-4"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 327,79.65 -0.48,0.86 -0.77,1.54 -0.48,1.06 -0.67,1 -0.48,0.68 -0.48,0.76 -0.48,0.68 -0.48,0.86 -0.39,0.58 -0.48,0.77 -0.67,0.86 -0.86,1 -0.87,0.87 -0.38,0.48 -0.67,0.76 -1.25,1.25 -0.58,0.67 -1.03,1.17 -0.67,0.57 -0.77,0.77 -1.05,0.77 -1.44,1.25 -0.77,0.48 -2.11,1.34 -0.69,0.32 -0.77,0.48 -1.05,0.87 -0.67,0.86 -0.39,0.77 -0.19,1.06 -0.29,1.72 v 0.58 l -0.19,1 -0.19,0.87 -0.1,1 v 2 l -0.09,1.25 v 1.92 l 0.19,1.06 0.09,0.58 0.2,1 0.38,0.86 0.38,0.77 0.58,0.67 0.77,0.48 0.77,0.29 1,0.19 H 310 l 0.77,-0.09 0.86,-0.29 1.16,-0.48 1.05,-0.58 0.77,-0.58 1,-0.76 0.87,-0.87 1.15,-1.06 0.77,-0.67 0.6,-0.9 0.39,-0.48 0.57,-0.76 0.39,-0.68 0.57,-0.86 0.39,-0.48 0.57,-0.87 0.77,-1.44 0.48,-0.76 0.58,-0.87 0.48,-0.86 0.48,-1 0.86,-1.25 1,-0.77 1.25,0.19 0.77,1 v 1 l -0.29,0.87 -0.38,0.76 -0.48,0.77 -0.39,0.87 -0.38,0.77 -0.29,0.76 -0.48,0.77 -0.38,0.68 -0.48,0.76 -0.58,0.87 -0.19,0.48 -0.48,0.86 -0.58,0.77 -1,1.35 -0.67,0.86 -0.39,0.38 -0.67,0.87 -0.57,0.67 -0.77,1 -0.87,0.77 -0.67,0.67 -0.57,0.48 -0.68,0.58 -0.86,0.48 -0.77,0.38 -0.86,0.39 -0.77,0.38 -0.77,0.29 -0.87,0.38 -1.15,0.39 -1.92,0.38 -1.15,0.1 -1.06,-0.1 h -0.57 l -1,-0.09 -0.87,-0.2 -0.77,-0.28 -0.86,-0.29 -0.77,-0.39 -0.77,-0.48 -0.67,-0.57 -0.58,-0.58 -0.57,-0.67 -0.39,-0.77 -0.38,-0.86 -0.29,-0.77 -0.19,-0.87 -0.29,-0.86 -0.1,-0.87 -0.19,-1 v -0.57 l -0.19,-1.06 -0.1,-0.86 v -0.87 l 0.1,-0.86 0.1,-1 v -0.58 l 0.09,-1.25 0.29,-2.11 0.1,-1.15 v -0.87 l 0.19,-0.86 0.19,-1 0.67,-3.17 0.29,-1.15 0.29,-1.73 0.19,-0.48 0.29,-1.16 0.19,-0.57 0.39,-1.44 0.28,-1.25 0.2,-0.87 0.28,-1.15 0.39,-1.34 0.09,-0.68 0.39,-1.34 0.38,-1.25 0.29,-0.67 0.38,-1.25 0.39,-1.15 0.38,-0.87 1,-2.49 0.2,-0.58 0.57,-1.35 0.67,-1.53 0.48,-1.15 0.39,-0.87 0.29,-0.57 0.48,-0.87 0.67,-1.63 0.48,-1 0.48,-0.87 0.58,-0.86 0.76,-1.25 1.21,-1.67 0.58,-0.77 1.25,-1.23 0.38,-0.48 0.67,-0.67 0.68,-0.48 1.53,-1.06 1.06,-0.67 1.06,-0.48 0.86,-0.29 0.87,-0.19 h 1.72 l 1,0.19 0.58,0.1 1,0.29 0.77,0.38 1.34,1.16 0.48,0.76 0.68,1 0.48,1.16 0.38,0.86 0.38,1.15 0.2,1.25 v 1.73 l -0.2,2.59 -0.41,1.7 -0.29,0.77 -0.19,1 -0.82,2.23 -0.38,0.77 z m -15.94,2.69 -1.35,4 -0.19,0.87 -0.29,0.86 -0.38,1.25 -0.67,2 -0.29,1 -0.1,0.57 -0.29,1 -0.29,0.87 -0.19,1 0.19,0.58 0.58,-0.39 1,-0.77 3.94,-3.93 0.48,-0.68 0.48,-0.77 0.58,-0.86 0.38,-0.38 0.58,-0.87 0.67,-0.86 0.67,-1 0.67,-1.06 0.48,-0.77 0.39,-0.67 0.48,-0.67 0.77,-1.54 0.38,-1 0.29,-0.58 0.38,-1 0.39,-0.86 0.19,-0.87 0.29,-0.86 0.29,-1.06 0.38,-1 0.29,-1.06 0.38,-1.06 0.1,-1 0.09,-0.86 v -1.79 l -0.19,-0.76 -0.29,-0.77 -0.67,-0.48 -0.77,-0.19 -0.86,0.19 -0.87,0.67 -0.67,0.58 -0.48,0.57 -0.48,0.67 -0.48,0.77 -1,1.44 -0.48,0.87 -0.38,0.86 -0.39,0.77 -0.38,0.87 -0.39,0.76 -0.38,0.87 -0.29,0.77 -0.38,1 -0.58,1.44 -0.38,0.87 z"
|
||||
class="cls-1"
|
||||
id="path150-1"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 348.16,105.1 v 1.06 l -0.29,0.86 -0.77,1.54 -0.86,1.44 -0.48,0.86 -0.39,0.77 -0.57,0.87 -0.29,0.57 -0.48,0.87 -0.48,0.77 -0.48,0.67 -0.58,0.77 -0.48,0.67 -1,1.54 -1.73,2 -1.15,1.25 -0.67,0.67 -0.58,0.68 -0.67,0.57 -2.11,1.64 -0.68,0.48 -0.86,0.38 -0.86,0.29 -0.77,0.19 -1.73,0.38 h -1.63 l -1.06,-0.09 -0.77,-0.29 -0.86,-0.29 -0.77,-0.48 -0.67,-0.57 -0.48,-0.68 -0.48,-0.77 -0.39,-0.86 -0.19,-0.77 -0.19,-0.86 -0.1,-0.87 v -0.86 l 0.19,-1.73 0.2,-0.86 0.28,-0.87 0.39,-1.73 0.29,-0.77 0.19,-0.86 0.29,-0.87 0.28,-0.76 0.87,-2.6 0.09,-0.77 0.2,-0.86 0.28,-0.86 0.2,-0.87 0.19,-0.77 0.57,-1.73 0.29,-1.15 0.29,-1.25 0.58,-1.73 0.19,-0.86 0.19,-0.67 0.19,-0.87 0.39,-1 0.29,-0.77 0.57,-2.59 0.29,-0.86 0.29,-1.16 0.38,-1.24 0.29,-0.87 0.39,-0.77 0.57,-0.48 0.87,-0.38 0.76,-0.1 1,0.1 1.73,0.19 1.54,0.58 0.48,0.67 v 0.86 l -0.19,0.87 -0.29,1.15 -0.29,1.25 -0.57,1.73 -0.2,0.77 -0.19,0.86 -0.29,1 -0.28,1.13 -1,2.89 -0.19,0.86 -0.38,1.25 -0.67,2 -0.2,0.86 -0.19,0.77 -0.19,0.86 -0.67,1.64 -0.19,0.86 -0.68,2.4 -0.57,1.73 -0.2,0.86 -0.28,1.25 -0.29,1.54 -0.19,1.63 0.19,1.44 0.77,1.06 1.05,0.29 0.87,-0.1 0.86,-0.38 0.67,-0.48 0.77,-0.58 3.17,-3.17 0.58,-0.67 0.48,-0.67 0.58,-0.77 1,-1.54 0.57,-0.67 0.39,-0.58 0.48,-0.77 0.57,-0.86 0.39,-0.86 0.77,-1.64 1.15,-2.3 0.48,-0.77 0.86,-0.48 1.25,0.19 z m -15.46,-29.48 0.48,-0.77 0.76,-0.67 0.77,-0.49 1,-0.28 0.87,-0.2 h 1 l 0.86,0.2 0.77,0.48 0.67,0.57 0.68,0.68 0.48,0.76 0.28,0.87 0.1,0.86 v 1 l -0.29,0.87 -0.48,0.77 -0.65,0.73 -0.68,0.58 -0.77,0.58 -0.86,0.38 -1,0.19 -0.86,-0.09 -1,-0.2 -1.54,-1 -0.58,-0.67 -0.48,-0.77 -0.19,-0.86 -0.1,-0.87 0.1,-0.86 0.19,-1 z"
|
||||
class="cls-1"
|
||||
id="path152-7"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 365.93,91.85 -0.77,0.38 -0.67,0.39 -0.67,0.48 -0.77,0.57 -0.68,0.58 -1.53,1.06 -0.67,0.48 -0.68,0.57 -0.67,0.67 -1.15,1.35 -0.67,0.67 -1.16,1.35 -1,1.53 -0.57,0.77 -1.12,2.3 -1,3.17 -0.29,1.24 -0.38,1.44 -0.39,1.25 -0.57,2.6 -0.39,1.24 -0.38,1.45 -0.29,1.24 -0.29,0.87 -0.29,1.25 -0.19,1 -0.29,1 -0.09,0.87 -0.2,0.77 -0.28,0.77 -0.58,0.48 -0.86,0.28 -0.87,0.1 -1.25,0.1 -1.44,-0.1 -1.25,-0.19 -0.77,-0.39 -0.57,-0.86 -0.1,-1.06 0.19,-1 0.29,-1.25 0.29,-1.15 0.19,-0.86 0.29,-0.87 0.19,-0.86 0.29,-0.77 0.29,-0.87 0.19,-0.86 0.19,-0.77 0.19,-0.86 0.29,-0.87 0.29,-0.77 0.39,-1.72 0.19,-0.77 0.09,-0.87 0.29,-0.86 0.19,-0.87 0.29,-0.86 0.29,-1.15 0.38,-1.25 0.48,-1.73 0.2,-0.77 0.19,-0.86 0.29,-0.87 0.19,-0.86 0.29,-0.77 0.19,-0.86 0.29,-0.87 0.29,-1.25 0.28,-1.15 0.2,-0.86 0.57,-1.73 0.19,-0.77 0.29,-0.87 0.39,-1.72 0.67,-1 1.25,-0.67 1.53,-0.19 1.54,0.19 1.44,0.48 1,1 0.19,1.16 -0.19,1 -0.19,0.67 -0.29,0.86 -0.19,0.87 -0.29,0.77 -0.19,0.86 -0.29,0.86 -0.19,0.87 -0.1,0.58 0.1,0.09 0.38,-0.48 0.58,-0.67 0.48,-0.67 1.15,-1.35 0.67,-0.57 1.19,-1.05 0.68,-0.58 1.24,-1.25 1.93,-1.53 0.76,-0.48 0.77,-0.39 0.77,-0.48 0.77,-0.29 0.86,-0.28 1.25,-0.29 1.15,-0.19 h 2.21 l 1.54,0.19 1.06,0.19 0.86,0.38 0.87,0.48 0.67,0.68 0.48,0.67 0.48,0.77 0.38,0.77 0.29,0.86 0.29,1.15 0.19,1.16 v 1.82 l -0.1,0.87 v 0.86 l -0.09,0.77 -0.19,0.86 -0.2,1 -0.19,0.58 -0.29,1 -0.28,1.25 -0.39,1.44 -0.38,1.25 -0.29,0.86 -0.29,1.25 -0.41,1.57 -0.39,1.54 -0.38,1.34 -0.19,0.87 -0.2,0.67 -0.19,0.87 -0.29,0.86 -0.19,0.77 -0.57,2.59 -0.1,0.87 v 1.72 l 0.38,0.77 0.68,0.48 0.86,0.1 0.87,-0.29 0.86,-0.38 0.77,-0.48 0.67,-0.48 0.77,-0.58 1.24,-1.24 0.48,-0.67 0.57,-0.77 0.48,-0.67 0.68,-0.77 0.48,-0.67 0.57,-0.87 0.87,-1.44 0.57,-0.86 0.29,-0.48 0.58,-0.87 0.48,-0.86 0.77,-1.54 0.38,-0.86 0.29,-0.77 0.29,-0.86 0.48,-0.77 0.86,-0.58 1.25,0.29 0.77,1 v 1 l -0.29,0.86 -0.38,0.77 -0.39,0.87 -0.38,0.77 -0.48,0.76 -0.39,0.77 -0.57,0.87 -0.29,0.57 -1,1.73 -0.19,0.58 -0.48,0.86 -1,1.54 -0.58,0.86 -0.38,0.48 -0.58,0.77 -0.67,0.87 -0.58,0.67 -1.06,1.15 -0.57,0.67 -1.35,1.16 -0.57,0.57 -0.77,0.58 -0.77,0.48 -1.54,0.77 -0.76,0.29 -0.87,0.28 -0.86,0.2 -0.77,0.09 -1,0.1 h -0.87 l -0.86,-0.1 -0.77,-0.19 -0.86,-0.29 -0.77,-0.29 -0.77,-0.48 -0.58,-0.48 -0.57,-0.67 -0.39,-0.86 -0.29,-0.77 -0.19,-0.87 -0.19,-0.76 v -2.12 l 0.19,-1.25 0.1,-1 0.29,-1.25 0.86,-3.16 0.29,-1.64 0.38,-1.63 0.39,-1.54 0.38,-1.25 0.29,-0.86 0.38,-1.25 0.39,-1.54 0.29,-1.53 0.38,-1.25 0.38,-1.73 0.1,-0.86 0.38,-1.83 0.1,-0.86 -0.19,-1 -0.58,-1 -0.86,-0.87 -0.87,-0.29 -0.86,-0.09 -0.87,0.09 z"
|
||||
class="cls-1"
|
||||
id="path154-1"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 419.71,102.8 -0.48,0.67 -1.15,1.34 -1.34,1.19 -0.68,0.48 -0.86,0.48 -0.58,0.19 -1,0.57 -0.57,0.29 -0.87,0.39 -1,0.29 -0.86,0.28 -0.87,0.1 -1,0.19 -1.05,0.1 -1.16,0.19 -0.86,0.1 h -0.87 l -1.72,-0.2 -1.73,-0.09 -0.87,-0.1 -2.59,-0.57 h -0.67 l -0.48,0.38 -0.2,0.67 v 0.87 l -0.09,1 v 1.63 l 0.09,1 0.2,0.77 0.57,1.73 1,1.53 0.58,0.68 0.58,0.57 1.53,1 1,0.29 0.87,0.29 0.67,0.1 0.86,0.19 0.87,0.09 h 1 l 0.86,-0.09 2.5,-0.48 0.87,-0.2 0.86,-0.28 0.77,-0.29 1.63,-0.67 1.54,-0.77 1.34,-1.06 0.65,-0.64 0.68,-0.58 1.34,-1.24 1.15,-1.35 0.48,-0.67 0.58,-0.77 1,-1.34 0.48,-0.77 0.39,-0.77 0.48,-0.77 0.38,-0.77 0.48,-0.67 0.38,-0.77 0.39,-0.86 0.38,-0.77 0.48,-0.77 0.68,-0.48 h 0.86 l 0.86,0.58 0.48,0.86 -0.09,0.87 -0.39,0.86 -0.48,1.06 -0.57,1 -0.48,1.06 -0.58,1.06 -0.58,1 -1,1.53 -0.28,0.58 -0.58,0.86 -1,1.35 -0.58,0.67 -0.57,0.58 -0.58,0.67 -0.67,0.77 -0.29,0.38 -0.67,0.77 -0.58,0.67 -1.28,1.17 -1,0.76 -0.87,0.77 -1.53,1 -2.41,1.06 -0.76,0.29 -0.87,0.29 -1.25,0.19 -1.15,0.29 -2,0.19 -1.25,0.09 -1.05,0.1 h -0.87 l -3.45,-0.38 -0.77,-0.2 -0.87,-0.28 -0.86,-0.39 -0.77,-0.38 -1.54,-0.87 -0.76,-0.38 -0.68,-0.58 -0.67,-0.67 -0.38,-0.48 -0.68,-0.77 -0.57,-0.67 -0.48,-0.77 -0.39,-0.67 -0.38,-0.87 -0.29,-0.76 -0.29,-0.87 -0.19,-0.86 -0.19,-0.77 -0.19,-0.87 -0.29,-2.59 v -1.85 l 0.09,-0.87 v -0.86 l 0.58,-2.6 0.33,-0.77 0.19,-0.87 0.29,-0.86 0.19,-0.87 0.29,-0.86 0.38,-1.06 0.48,-1.15 0.48,-0.87 0.58,-1 0.67,-1 0.39,-0.87 0.48,-0.67 0.57,-0.67 1.44,-1.54 1.45,-1.44 0.76,-0.67 0.87,-0.67 1,-0.68 0.86,-0.67 1,-0.57 1,-0.48 0.87,-0.39 1.25,-0.58 2,-0.67 1.73,-0.38 1.25,-0.19 h 2.11 l 1.25,0.19 1.54,0.29 1.25,0.28 0.86,0.29 0.87,0.39 0.76,0.38 0.58,0.39 1.35,1.15 0.57,0.67 0.58,1 0.48,1.15 0.29,0.87 0.19,0.86 0.09,0.87 v 1.15 l -0.24,1.21 -0.1,1 -0.29,1.15 -0.38,1.15 -0.38,1 -0.29,0.48 -0.48,0.87 z M 408,91.27 l -1.35,0.58 -1.15,0.57 -0.77,0.48 -1.34,1.16 -0.87,0.86 -0.77,0.87 -0.67,0.76 -0.67,1.06 -0.67,1 -0.48,0.86 -0.58,1.06 -0.38,1.15 -0.39,0.87 -0.29,0.77 -0.09,0.67 0.48,0.48 1.25,0.29 1.92,0.09 1,0.1 h 2.69 l 1,-0.19 0.86,-0.19 1,-0.39 0.58,-0.19 1,-0.48 0.67,-0.48 0.67,-0.58 0.68,-0.67 0.67,-0.58 0.57,-0.76 0.48,-0.68 0.39,-0.77 0.48,-0.86 0.58,-1.73 0.19,-1 0.09,-0.86 -0.09,-1 -0.39,-0.87 -0.57,-0.67 -0.77,-0.58 -0.87,-0.38 -0.86,-0.19 -0.86,-0.1 -1,0.19 z"
|
||||
class="cls-1"
|
||||
id="path156-8"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
</g>
|
||||
<g
|
||||
id="g176-7"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill">
|
||||
<path
|
||||
d="m 72.42,188.94 v 5.5 c 0,15.85 -4.84,26 -15.63,31 13,5.06 18.05,16.73 18.05,33 v 12.55 c 0,23.77 -12.55,36.54 -36.76,36.54 H 0 V 153.5 h 36.54 c 25.09,0 35.88,11.67 35.88,35.44 z M 24.21,175.51 v 40.72 h 9.47 c 9,0 14.53,-4 14.53,-16.28 v -8.59 c 0,-11 -3.75,-15.85 -12.33,-15.85 z m 0,62.74 v 47.32 h 13.87 c 8.15,0 12.55,-3.74 12.55,-15.18 V 257 c 0,-14.31 -4.62,-18.71 -15.63,-18.71 z"
|
||||
class="cls-1"
|
||||
id="path160-8"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 87.39,190.48 c 0,-24.65 13,-38.74 36.76,-38.74 23.76,0 36.76,14.09 36.76,38.74 v 80.13 c 0,24.65 -13,38.74 -36.76,38.74 -23.76,0 -36.76,-14.09 -36.76,-38.74 z m 24.21,81.67 c 0,11 4.84,15.19 12.55,15.19 7.71,0 12.55,-4.19 12.55,-15.19 v -83.21 c 0,-11 -4.85,-15.19 -12.55,-15.19 -7.7,0 -12.55,4.18 -12.55,15.19 z"
|
||||
class="cls-1"
|
||||
id="path162-7"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 199.65,153.5 v 118.87 c 0,11 4.85,15 12.55,15 7.7,0 12.55,-4 12.55,-15 V 153.5 h 22.89 v 117.33 c 0,24.65 -12.33,38.74 -36.1,38.74 -23.77,0 -36.1,-14.09 -36.1,-38.74 V 153.5 Z"
|
||||
class="cls-1"
|
||||
id="path164-7"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="M 257.11,153.5 H 332 v 22 H 306.63 V 307.59 H 282.42 V 175.51 h -25.31 z"
|
||||
class="cls-1"
|
||||
id="path166-4"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 342.51,153.5 h 24.21 v 154.09 h -24.21 z"
|
||||
class="cls-1"
|
||||
id="path168-4"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 382.35,190.48 c 0,-24.65 13,-38.74 36.76,-38.74 23.76,0 36.76,14.09 36.76,38.74 v 80.13 c 0,8.58 -1.54,15.84 -4.62,21.57 1.1,2.86 2.86,3.3 6.82,3.3 h 2.21 v 21.57 H 457 c -10.78,0 -17.61,-4 -20.91,-10.56 a 49.64,49.64 0 0 1 -16.95,2.86 c -23.77,0 -36.76,-14.09 -36.76,-38.74 z m 24.22,81.67 c 0,11 4.84,15.19 12.54,15.19 7.7,0 12.55,-4.19 12.55,-15.19 v -83.21 c 0,-11 -4.84,-15.19 -12.55,-15.19 -7.71,0 -12.54,4.18 -12.54,15.19 z"
|
||||
class="cls-1"
|
||||
id="path170-9"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 494.62,153.5 v 118.87 c 0,11 4.84,15 12.54,15 7.7,0 12.55,-4 12.55,-15 V 153.5 h 22.89 v 117.33 c 0,24.65 -12.32,38.74 -36.1,38.74 -23.78,0 -36.1,-14.09 -36.1,-38.74 V 153.5 Z"
|
||||
class="cls-1"
|
||||
id="path172-1"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 583.33,218.44 h 33.23 v 22 h -33.23 v 45.12 h 41.82 v 22 h -66 V 153.5 h 66 v 22 h -41.82 z"
|
||||
class="cls-1"
|
||||
id="path174-5"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
</g>
|
||||
<g
|
||||
id="g194-5"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill">
|
||||
<path
|
||||
d="m 232.18,435.63 c 2.18,1.13 3.27,1.67 5.48,2.72 l -1.53,3.24 c -2.26,-1.07 -3.38,-1.62 -5.6,-2.77 l -3.38,6.53 c 2.92,1.51 4.39,2.23 7.35,3.6 l -1.5,3.26 c -4.76,-2.21 -7.12,-3.4 -11.76,-5.95 l 12.09,-22 c 4,2.22 6.08,3.25 10.21,5.16 l -1.5,3.26 c -2.68,-1.24 -4,-1.89 -6.64,-3.25 -1.3,2.47 -1.94,3.72 -3.22,6.2 z"
|
||||
class="cls-1"
|
||||
id="path178-5"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 258.19,435.05 c 3.8,1.33 5.09,4.1 3.9,7.94 l -0.23,0.75 c -1.55,-0.48 -2.33,-0.73 -3.87,-1.25 l 0.33,-1 c 0.57,-1.7 0.06,-2.58 -1.21,-3 -1.27,-0.42 -2.22,-0.07 -2.83,1.62 -1.77,4.85 6.11,8.53 4,15.38 -1.19,3.84 -4.2,5.36 -8.62,3.8 -4.42,-1.56 -5.85,-4.62 -4.38,-8.36 l 0.57,-1.44 c 1.62,0.64 2.43,0.95 4.06,1.55 l -0.62,1.68 a 2.4,2.4 0 1 0 4.52,1.59 c 1.65,-4.89 -6.25,-8.81 -3.63,-15.49 1.46,-3.76 4.22,-5.1 8.01,-3.77 z"
|
||||
class="cls-1"
|
||||
id="path180-8"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 272.24,439.58 c 4.93,1.25 7.42,1.77 12.41,2.63 -0.24,1.42 -0.37,2.12 -0.61,3.54 -1.73,-0.3 -2.59,-0.46 -4.31,-0.8 l -4.2,21.11 c -1.86,-0.37 -2.78,-0.56 -4.63,-1 l 4.73,-21 c -1.71,-0.39 -2.56,-0.59 -4.27,-1 z"
|
||||
class="cls-1"
|
||||
id="path182-2"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 293.27,464.91 -0.4,3.78 c -1.8,-0.19 -2.71,-0.3 -4.51,-0.53 0.2,-1.51 0.29,-2.26 0.49,-3.77 1.76,0.23 2.65,0.33 4.42,0.52 z"
|
||||
class="cls-1"
|
||||
id="path184-9"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 325.25,447.42 c -1.32,0.1 -2.11,0.83 -2,2.62 0.07,1.07 0.1,1.61 0.17,2.69 -1.62,0.1 -2.43,0.14 -4.06,0.2 0,-1 -0.05,-1.46 -0.09,-2.43 -0.16,-4 1.83,-6.36 5.82,-6.66 3.99,-0.3 6.32,1.7 6.77,5.69 0.89,7.85 -7.71,11.56 -7.44,15.68 a 2.33,2.33 0 0 0 0.07,0.53 c 3.49,-0.22 5.23,-0.37 8.71,-0.76 0.16,1.43 0.24,2.14 0.39,3.57 -5.41,0.6 -8.13,0.8 -13.57,1 l -0.12,-3.09 c -0.29,-7.38 8.4,-9.16 7.76,-16.38 -0.2,-2.23 -1.09,-2.76 -2.41,-2.66 z"
|
||||
class="cls-1"
|
||||
id="path186-8"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 341.75,448.11 c -0.69,-3.95 1.09,-6.56 5,-7.4 3.91,-0.84 6.63,0.82 7.61,4.71 1.28,5.07 1.93,7.6 3.21,12.67 1,3.89 -0.92,6.78 -5.47,7.75 -4.55,0.97 -7.45,-0.9 -8.13,-4.86 -0.88,-5.15 -1.33,-7.72 -2.22,-12.87 z m 6.78,12.28 c 0.35,1.76 1.4,2.25 2.85,1.94 1.45,-0.31 2.2,-1.18 1.8,-2.93 -1.2,-5.29 -1.8,-7.94 -3,-13.23 -0.4,-1.75 -1.37,-2.22 -2.67,-1.95 -1.3,0.27 -2,1.11 -1.64,2.87 z"
|
||||
class="cls-1"
|
||||
id="path188-2"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 363.19,439.94 c 3.1,-1 3.31,-2.64 3.36,-4.45 1.05,-0.36 1.58,-0.55 2.63,-0.93 3.45,9.43 5.18,14.15 8.64,23.58 -1.8,0.66 -2.7,1 -4.5,1.58 -2.46,-7.26 -3.68,-10.89 -6.14,-18.15 -1.25,0.43 -1.88,0.64 -3.14,1 z"
|
||||
class="cls-1"
|
||||
id="path190-0"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 391.63,443.48 1,2.1 a 2.43,2.43 0 1 0 4.34,-2.17 c -1,-2 -1.52,-3 -2.54,-4.91 -0.82,-1.59 -1.93,-1.78 -3.2,-1.14 -1.27,0.64 -1.79,1.63 -1,3.25 l 0.33,0.68 c -1.55,0.75 -2.33,1.11 -3.89,1.82 -2.11,-5.44 -3.18,-8.15 -5.34,-13.57 4,-1.84 6,-2.83 9.88,-5 0.69,1.26 1,1.89 1.71,3.15 -2.62,1.43 -3.95,2.12 -6.62,3.42 0.93,2.21 1.39,3.32 2.31,5.54 a 4.45,4.45 0 0 1 2.62,-3.36 c 2.88,-1.47 5.3,-0.5 7.08,2.74 L 401,441 c 1.94,3.52 0.89,6.76 -3.36,8.89 -4.25,2.13 -7.45,1 -9.11,-2.64 l -0.86,-1.89 c 1.6,-0.75 2.39,-1.12 3.96,-1.88 z"
|
||||
class="cls-1"
|
||||
id="path192-7"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g202-4"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
|
||||
transform="matrix(0.97135514,0,0,0.96186606,8.9536653,8.9521229)">
|
||||
<g
|
||||
id="g208-8"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill">
|
||||
<path
|
||||
d="M 518.64,127 A 231.92,231.92 0 0 0 493.72,88 l -7.85,6.19 a 222.67,222.67 0 0 1 21.5,32.81 z"
|
||||
class="cls-1"
|
||||
id="path204-3"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<path
|
||||
d="m 507.32,334 q -1.47,2.76 -3,5.47 a 222,222 0 0 1 -75.87,78.67 l 5.27,8.5 A 232.17,232.17 0 0 0 513,344.42 q 2.93,-5.15 5.58,-10.42 z"
|
||||
class="cls-1"
|
||||
id="path206-0"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
</g>
|
||||
<polygon
|
||||
points="81.35,17.5 144.56,127 167.66,127 115.99,37.5 509.16,37.5 457.49,127 480.58,127 543.8,17.5 "
|
||||
class="cls-1"
|
||||
id="polygon198-8"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
<polygon
|
||||
points="287.17,334 264.08,334 312.57,418 361.07,334 337.98,334 312.57,378 "
|
||||
class="cls-1"
|
||||
id="polygon200-5"
|
||||
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<metadata
|
||||
id="metadata4195">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:title>Hipster</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 30 KiB |
1
src/frontend/static/icons/Hipster_InstagramIcon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#111}</style></defs><title>Hipster</title><g><path d="M17.6,17.76H16.38v5h1c.95,0,1.53-.41,1.53-1.71V19.42C18.9,18.26,18.51,17.76,17.6,17.76Z" class="cls-1"/><path d="M24,0A24,24,0,1,0,48,24,24,24,0,0,0,24,0ZM12.2,20.9H9.8V19.16c0-1.15-.51-1.59-1.32-1.59S7.16,18,7.16,19.16V27.9c0,1.15.51,1.57,1.32,1.57S9.8,29.05,9.8,27.9v-3H8.64V22.61H12.2v5.13c0,2.58-1.29,4.06-3.79,4.06s-3.79-1.48-3.79-4.06V19.33c0-2.59,1.3-4.07,3.79-4.07s3.79,1.48,3.79,4.07Zm7,10.72a5,5,0,0,1-.23-2V27.09c0-1.5-.51-2.06-1.66-2.06h-.88v6.59H13.84V15.45h3.83c2.64,0,3.77,1.22,3.77,3.71v1.28c0,1.66-.53,2.74-1.66,3.28,1.27.53,1.68,1.75,1.68,3.44v2.49a4.68,4.68,0,0,0,.28,2Zm9.63,0-.44-2.94H25.22l-.43,2.94H22.45L25,15.45h3.72l2.59,16.17Zm12,0V20L39,31.62h-2.4L34.72,20.18V31.62H32.5V15.45H36L37.9,26.93l1.74-11.48h3.53V31.62Z" class="cls-1"/><polygon points="25.55 26.49 28 26.49 26.77 18.31 25.55 26.49" class="cls-1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1018 B |
1
src/frontend/static/icons/Hipster_KitchenwareOffer.svg
Normal file
|
After Width: | Height: | Size: 20 KiB |
149
src/frontend/static/icons/Hipster_NavLogo.svg
Normal file
@@ -0,0 +1,149 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
inkscape:version="1.2 (dc2aeda, 2022-05-15)"
|
||||
sodipodi:docname="Hipster_NavLogo.svg"
|
||||
version="1.1"
|
||||
viewBox="0 0 316.16 60"
|
||||
data-name="Layer 1"
|
||||
id="Layer_1"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<metadata
|
||||
id="metadata1078">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>Hipster</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
inkscape:current-layer="g1073"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-x="0"
|
||||
inkscape:cy="-23.534382"
|
||||
inkscape:cx="158.74607"
|
||||
inkscape:zoom="2.2520243"
|
||||
showgrid="false"
|
||||
id="namedview1076"
|
||||
inkscape:window-height="622"
|
||||
inkscape:window-width="1370"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ff54ff"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#ff54ff" />
|
||||
<defs
|
||||
id="defs1033">
|
||||
<style
|
||||
id="style1031">.cls-1{fill:#4cc8c6}.cls-2{fill:#fff}</style>
|
||||
</defs>
|
||||
<title
|
||||
id="title1035">Hipster</title>
|
||||
<g
|
||||
id="g1073">
|
||||
<g
|
||||
id="g1041"
|
||||
style="fill:#fe9a9b;fill-opacity:1">
|
||||
<path
|
||||
id="path1037"
|
||||
class="cls-1"
|
||||
d="M28.65,5.77A22.07,22.07,0,1,1,6.58,27.84,22.09,22.09,0,0,1,28.65,5.77m0-5.77A27.84,27.84,0,1,0,56.48,27.84,27.83,27.83,0,0,0,28.65,0Z"
|
||||
style="fill:#fe9a9b;fill-opacity:1" />
|
||||
<path
|
||||
id="path1039"
|
||||
class="cls-1"
|
||||
d="M47.3,16.15,28.65,48.46,10,16.15H47.3m10-5.77H0L28.65,60,57.29,10.38Z"
|
||||
style="fill:#fe9a9b;fill-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
id="g1071"
|
||||
style="fill:#570d2d;fill-opacity:1">
|
||||
<path
|
||||
style="fill:#570d2d;fill-opacity:1"
|
||||
id="path1043"
|
||||
class="cls-2"
|
||||
d="M76.11,20.82c0-5.61,3-8.82,8.38-8.82s8.37,3.21,8.37,8.82V39.08c0,5.61-3,8.82-8.37,8.82s-8.38-3.21-8.38-8.82Zm5.52,18.61c0,2.5,1.1,3.46,2.86,3.46s2.85-1,2.85-3.46v-19c0-2.5-1.1-3.46-2.85-3.46s-2.86,1-2.86,3.46Z" />
|
||||
<path
|
||||
style="fill:#570d2d;fill-opacity:1"
|
||||
id="path1045"
|
||||
class="cls-2"
|
||||
d="M101.33,22.08V47.5h-5V12.4h6.92l5.66,21v-21h4.92V47.5H108.2Z" />
|
||||
<path
|
||||
style="fill:#570d2d;fill-opacity:1"
|
||||
id="path1047"
|
||||
class="cls-2"
|
||||
d="M117.82,12.4h5.52V42.48h9.08v5h-14.6Z" />
|
||||
<path
|
||||
style="fill:#570d2d;fill-opacity:1"
|
||||
id="path1049"
|
||||
class="cls-2"
|
||||
d="M134.82,12.4h5.52V47.5h-5.52Z" />
|
||||
<path
|
||||
style="fill:#570d2d;fill-opacity:1"
|
||||
id="path1051"
|
||||
class="cls-2"
|
||||
d="M149.26,22.08V47.5h-5V12.4h6.92l5.66,21v-21h4.91V47.5h-5.66Z" />
|
||||
<path
|
||||
style="fill:#570d2d;fill-opacity:1"
|
||||
id="path1053"
|
||||
class="cls-2"
|
||||
d="M171.27,27.19h7.57v5h-7.57V42.48h9.53v5H165.75V12.4H180.8v5h-9.53Z" />
|
||||
<path
|
||||
style="fill:#570d2d;fill-opacity:1"
|
||||
id="path1055"
|
||||
class="cls-2"
|
||||
d="M191.22,12.4c5.12,0,6.77,2.56,6.77,7.27v2.26c0,3.91-1.05,6.27-5,6.87,4.16.6,5.72,3.46,5.72,7.52v3.11c0,5-2.11,8.07-7.42,8.07H184.2V12.4Zm-1.35,16c4.86,0,7-1.16,7-6.27V19.72c0-4.11-1.25-6.27-5.67-6.27h-5.91v14.9Zm1.41,18.1c4.61,0,6.31-2.61,6.31-7V36.27c0-5.07-2.4-6.92-7.22-6.92h-5.06v17.1Z" />
|
||||
<path
|
||||
style="fill:#570d2d;fill-opacity:1"
|
||||
id="path1057"
|
||||
class="cls-2"
|
||||
d="M202.55,21c0-5.81,2.56-8.87,7.43-8.87s7.52,3.06,7.52,8.87V38.87c0,5.82-2.56,8.88-7.52,8.88s-7.43-3.06-7.43-8.88Zm1.11,17.91c0,5.16,2.15,7.82,6.32,7.82s6.41-2.66,6.41-7.82V21c0-5.17-2.2-7.83-6.41-7.83s-6.32,2.66-6.32,7.83Z" />
|
||||
<path
|
||||
style="fill:#570d2d;fill-opacity:1"
|
||||
id="path1059"
|
||||
class="cls-2"
|
||||
d="M222.51,38.87c0,4.22,1.5,7.88,6.17,7.88s6.16-3.66,6.16-7.88V12.4h1.06V38.82c0,4.82-1.91,8.93-7.22,8.93s-7.27-4.11-7.27-8.93V12.4h1.1Z" />
|
||||
<path
|
||||
style="fill:#570d2d;fill-opacity:1"
|
||||
id="path1061"
|
||||
class="cls-2"
|
||||
d="M246.63,47.5v-34H239v-1H255.4v1h-7.67V47.5Z" />
|
||||
<path
|
||||
style="fill:#570d2d;fill-opacity:1"
|
||||
id="path1063"
|
||||
class="cls-2"
|
||||
d="M259.71,12.4V47.5h-1.1V12.4Z" />
|
||||
<path
|
||||
style="fill:#570d2d;fill-opacity:1"
|
||||
id="path1065"
|
||||
class="cls-2"
|
||||
d="M278.66,21V38.87a10.14,10.14,0,0,1-1.9,6.47A3.33,3.33,0,0,0,280,47h.55v1H280a4.16,4.16,0,0,1-3.92-2,7,7,0,0,1-4.91,1.71c-5.36,0-7.47-4-7.47-8.88V21c0-4.91,2.11-8.87,7.47-8.87S278.66,16.11,278.66,21Zm-13.89,0v18c0,4.31,1.71,7.82,6.37,7.82s6.42-3.51,6.42-7.82V21c0-4.32-1.76-7.83-6.42-7.83S264.77,16.66,264.77,21Z" />
|
||||
<path
|
||||
style="fill:#570d2d;fill-opacity:1"
|
||||
id="path1067"
|
||||
class="cls-2"
|
||||
d="M285.43,38.87c0,4.22,1.5,7.88,6.17,7.88s6.16-3.66,6.16-7.88V12.4h1.06V38.82c0,4.82-1.91,8.93-7.22,8.93s-7.27-4.11-7.27-8.93V12.4h1.1Z" />
|
||||
<path
|
||||
style="fill:#570d2d;fill-opacity:1"
|
||||
id="path1069"
|
||||
class="cls-2"
|
||||
d="M314.21,29.2v1H303.88V46.45h12.28v1H302.78V12.4h13.38v1H303.88V29.2Z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
1
src/frontend/static/icons/Hipster_PinterestIcon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#111}</style></defs><title>Hipster</title><g><path d="M17.13,17.76h-1.2v5.47h1.2c.81,0,1.25-.37,1.25-1.52V19.28C18.38,18.13,17.94,17.76,17.13,17.76Z" class="cls-1"/><path d="M24,0A24,24,0,1,0,48,24,24,24,0,0,0,24,0ZM20.92,21.54c0,2.59-1.25,4-3.79,4h-1.2v6.08H13.39V15.45h3.74c2.54,0,3.79,1.4,3.79,4Zm3.83,10.08H22.21V15.45h2.54Zm9.89,0H32L28.86,19.9V31.62H26.57V15.45h3.19l2.61,9.68V15.45h2.27Z" class="cls-1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 539 B |
1
src/frontend/static/icons/Hipster_ProfileIcon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 20 20"><defs><style>.cls-1{fill:#605f64}</style></defs><title>Hipster</title><g><path d="M10,3.6A2.75,2.75,0,1,1,7.25,6.35,2.75,2.75,0,0,1,10,3.6Zm0,13a6.58,6.58,0,0,1-5.49-3c0-1.82,3.66-2.82,5.49-2.82s5.47,1,5.49,2.82A6.58,6.58,0,0,1,10,16.6Z" class="cls-1"/><path d="M10,2a8,8,0,1,1-8,8,8,8,0,0,1,8-8m0-2A10,10,0,1,0,20,10,10,10,0,0,0,10,0Z" class="cls-1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 455 B |
1
src/frontend/static/icons/Hipster_SearchIcon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 12 12"><defs><style>.cls-1{fill:#605f64}</style></defs><title>Hipster</title><path d="M8.58,7.55H8l-.19-.19a4.48,4.48,0,1,0-.48.48L7.55,8v.55L11,12l1-1Zm-4.12,0A3.09,3.09,0,1,1,7.55,4.46,3.09,3.09,0,0,1,4.46,7.55Z" class="cls-1"/></svg>
|
||||
|
After Width: | Height: | Size: 322 B |
1
src/frontend/static/icons/Hipster_TwitterIcon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#111}</style></defs><title>Hipster</title><path d="M24,0A24,24,0,1,0,48,24,24,24,0,0,0,24,0ZM15.12,17.76H12.47V31.62H9.92V17.76H7.27V15.45h7.85Zm11,13.86H22.79L21.87,23l-.93,8.62H17.45l-1.8-16.17h2.47L19.49,28.2l1.22-12.75h2.45l1.27,12.84,1.32-12.84H28Zm5.61,0H29.23V15.45h2.54Zm9-13.86H38.08V31.62H35.54V17.76H32.88V15.45h7.85Z" class="cls-1"/></svg>
|
||||
|
After Width: | Height: | Size: 469 B |
7
src/frontend/static/icons/Hipster_UpDownControl.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 6.75 11.75">
|
||||
<title>Hipster</title>
|
||||
<g>
|
||||
<polygon points="3.38 0 6.75 3.38 0 3.38 3.38 0"/>
|
||||
<polyline points="0 8.38 6.75 8.38 3.38 11.75"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 252 B |
7
src/frontend/static/icons/Hipster_WandIcon.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24.00 24.00" fill="none" xmlns="http://www.w3.org/2000/svg" transform="rotate(0)">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
|
After Width: | Height: | Size: 749 B |
1
src/frontend/static/icons/Hipster_YoutubeIcon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#111}</style></defs><title>Hipster</title><g><path d="M28.37,24.34H27.23v5h1.46c.85,0,1.32-.4,1.32-1.6V26.3C30,24.8,29.52,24.34,28.37,24.34Z" class="cls-1"/><path d="M29.75,20.32v-.9c0-1.16-.39-1.66-1.29-1.66H27.23V22h1C29.17,22,29.75,21.61,29.75,20.32Z" class="cls-1"/><path d="M24,0A24,24,0,1,0,48,24,24,24,0,0,0,24,0ZM14.4,17.76H11.74V31.62H9.2V17.76H6.54V15.45H14.4Zm8.56,10c0,2.59-1.29,4.06-3.79,4.06s-3.78-1.47-3.78-4.06V15.45h2.54V27.92c0,1.16.51,1.57,1.31,1.57s1.32-.41,1.32-1.57V15.45H23Zm9.59,0c0,2.5-1.32,3.84-3.86,3.84h-4V15.45h3.84c2.63,0,3.76,1.22,3.76,3.71v.58c0,1.67-.51,2.73-1.64,3.26,1.37.53,1.9,1.76,1.9,3.47Zm7.55-5.52v2.31H36.61v4.74H41v2.31H34.07V15.45H41v2.31H36.61v4.5Z" class="cls-1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 838 B |
BIN
src/frontend/static/images/Advert2BannerImage.png
Normal file
|
After Width: | Height: | Size: 653 KiB |
BIN
src/frontend/static/images/AdvertBannerImage.png
Normal file
|
After Width: | Height: | Size: 510 KiB |
BIN
src/frontend/static/images/HeroBannerImage.png
Normal file
|
After Width: | Height: | Size: 654 KiB |
BIN
src/frontend/static/images/HeroBannerImage2.png
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
src/frontend/static/images/VRHeadsets.png
Normal file
|
After Width: | Height: | Size: 357 KiB |
2
src/frontend/static/images/credits.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
folded-clothes-on-white-chair.jpg,,https://unsplash.com/photos/fr0J5-GIVyg
|
||||
folded-clothes-on-white-chair-wide.jpg,,https://unsplash.com/photos/fr0J5-GIVyg
|
||||
|
After Width: | Height: | Size: 155 KiB |
BIN
src/frontend/static/images/folded-clothes-on-white-chair.jpg
Normal file
|
After Width: | Height: | Size: 526 KiB |
BIN
src/frontend/static/img/products/bamboo-glass-jar.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
src/frontend/static/img/products/candle-holder.jpg
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
src/frontend/static/img/products/hairdryer.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
src/frontend/static/img/products/loafers.jpg
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
src/frontend/static/img/products/mug.jpg
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
src/frontend/static/img/products/salt-and-pepper-shakers.jpg
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
src/frontend/static/img/products/sunglasses.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
src/frontend/static/img/products/tank-top.jpg
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
src/frontend/static/img/products/watch.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
166
src/frontend/static/styles/bot.css
Normal file
@@ -0,0 +1,166 @@
|
||||
.chat-modal {
|
||||
width: 100%;
|
||||
height: 85vh;
|
||||
margin-top: 50px;
|
||||
background-color: #EEE;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
overflow: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes scale-in {
|
||||
0% {
|
||||
scale: 0;
|
||||
}
|
||||
100% {
|
||||
scale: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.bot-messages {
|
||||
overflow: auto;
|
||||
height: calc(100% - 100px);
|
||||
scroll-snap-align: end;
|
||||
}
|
||||
|
||||
.bot-message {
|
||||
position: relative;
|
||||
margin: 16px;
|
||||
padding: 16px;
|
||||
margin-right: 20%;
|
||||
border-radius: 16px;
|
||||
background-color: white;
|
||||
min-height: 55px;
|
||||
}
|
||||
|
||||
.bot-message-loading {
|
||||
-webkit-mask:linear-gradient(-60deg,#000 30%,#0005,#000 70%) right/300% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.user-message {
|
||||
position: relative;
|
||||
margin: 16px;
|
||||
margin-left: 20%;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
background-color: var(--blue);
|
||||
}
|
||||
|
||||
.bot-input {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: -webkit-fill-available;
|
||||
margin: 16px;
|
||||
margin-right: 32px;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.bot-input-file-button {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.bot-input-text {
|
||||
border: none;
|
||||
border-bottom: 1px solid #9AA0A6;
|
||||
padding: 0 0 8px 16px;
|
||||
outline: none;
|
||||
color: #1E2021;
|
||||
width: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.user-message-text {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.user-image-div {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.user-image {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
border-radius: 16px;
|
||||
margin-right: 16px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.bot-input-button {
|
||||
display: inline-block;
|
||||
border: solid 1px var(--blue);
|
||||
padding: 8px 16px;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
border-radius: 22px;
|
||||
cursor: pointer;
|
||||
background-color: var(--blue);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.bot-input-button:disabled,
|
||||
button[disabled]{
|
||||
border: 1px solid #999999;
|
||||
background-color: #cccccc;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.bot-products {
|
||||
margin-left: 16px;
|
||||
margin-right: 20%;
|
||||
}
|
||||
|
||||
.bot-product {
|
||||
height: 150px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 16px;
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.bot-product-img {
|
||||
height: 150px;
|
||||
border-top-left-radius: 16px;
|
||||
border-bottom-left-radius: 16px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.bot-product-description {
|
||||
float: right;
|
||||
width: calc(100% - 180px);
|
||||
}
|
||||
|
||||
@keyframes typing {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.shimmer {
|
||||
color: grey;
|
||||
display:inline-block;
|
||||
-webkit-mask:linear-gradient(-60deg,#000 30%,#0005,#000 70%) right/300% 100%;
|
||||
background-repeat: no-repeat;
|
||||
animation: shimmer 2.5s infinite;
|
||||
font-size: 50px;
|
||||
max-width:200px;
|
||||
filter: invert(21%) sepia(100%) saturate(7414%) hue-rotate(210deg) brightness(50%) contrast(117%);
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
100% {-webkit-mask-position:left}
|
||||
}
|
||||
110
src/frontend/static/styles/cart.css
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.cart-sections {
|
||||
padding-bottom: 120px;
|
||||
padding-top: 56px;
|
||||
background-color: #F9F9F9;
|
||||
}
|
||||
|
||||
.cart-sections h3 {
|
||||
font-size: 36px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.cart-sections a.cymbal-button-primary:hover {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Empty Cart Section */
|
||||
|
||||
.empty-cart-section {
|
||||
max-width: 458px;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-cart-section a {
|
||||
display: inline-block; /* So margin-top works. */
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.empty-cart-section a:hover {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Cart Summary Section */
|
||||
|
||||
.cart-summary-empty-cart-button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.cart-summary-item-row,
|
||||
.cart-summary-shipping-row,
|
||||
.cart-summary-total-row {
|
||||
padding-bottom: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: solid 1px rgba(154, 160, 166, 0.5);
|
||||
}
|
||||
|
||||
.cart-summary-item-row img {
|
||||
border-radius: 20% 0 20% 20%;
|
||||
}
|
||||
|
||||
.cart-summary-item-row-item-id-row {
|
||||
font-size: 12px;
|
||||
color: #5C6063;
|
||||
}
|
||||
|
||||
.cart-summary-item-row h4 {
|
||||
font-size: 18px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* Stick item quantity and cost to the bottom (for wider screens). */
|
||||
@media (min-width: 768px) {
|
||||
.cart-summary-item-row .row:last-child {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Item cost (price). */
|
||||
.cart-summary-item-row .row:last-child strong {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cart-summary-total-row {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
/* Cart Checkout Form */
|
||||
|
||||
.cart-checkout-form h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.payment-method-heading {
|
||||
margin-top: 36px;
|
||||
}
|
||||
|
||||
/* "Place Order" button */
|
||||
.cart-checkout-form .cymbal-button-primary {
|
||||
margin-top: 36px;
|
||||
}
|
||||
53
src/frontend/static/styles/order.css
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.order {
|
||||
background: #F9F9F9;
|
||||
}
|
||||
|
||||
.order-complete-section {
|
||||
max-width: 487px;
|
||||
padding-top: 56px;
|
||||
padding-bottom: 120px;
|
||||
}
|
||||
|
||||
.order-complete-section h3 {
|
||||
margin: 0;
|
||||
font-size: 36px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.order-complete-section p {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.order-complete-section .padding-y-24 {
|
||||
padding-bottom: 24px;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.order-complete-section .border-bottom-solid {
|
||||
border-bottom: 1px solid rgba(154, 160, 166, 0.5);
|
||||
}
|
||||
|
||||
.order-complete-section .cymbal-button-primary {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.order-complete-section a.cymbal-button-primary:hover {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
}
|
||||
677
src/frontend/static/styles/styles.css
Normal file
@@ -0,0 +1,677 @@
|
||||
/**
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* General */
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #111111;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
|
||||
header {
|
||||
background-color: #853B5C;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/*
|
||||
This allows the sub-navbar (white strip containing logo)
|
||||
to be as wide as the browser window.
|
||||
*/
|
||||
header > div:nth-child(2).navbar.sub-navbar {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
header > div:nth-child(2) > .container {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
header .cart-link {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin-left: 25px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
header .cart-size-circle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
left: 11px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 11px;
|
||||
border-radius: 4px 4px 0 4px;
|
||||
color: white;
|
||||
background-color: #853B5C;
|
||||
}
|
||||
|
||||
header .navbar {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
header .h-free-shipping {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
header .h-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
header .h-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
margin-left: 40px;
|
||||
color: #605f64;
|
||||
}
|
||||
|
||||
header .h-control:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
header .h-control input {
|
||||
border: none;
|
||||
padding: 0 31px 0 31px;
|
||||
width: 250px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
background-color: #f2f2f2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header .h-control input:focus {
|
||||
outline: 0;
|
||||
border: 0;
|
||||
box-shadow: 0;
|
||||
}
|
||||
|
||||
header .icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
header .icon.search-icon {
|
||||
width: 12px;
|
||||
height: 13px;
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
/* The currency drop-down. */
|
||||
|
||||
header img.currency-icon, header span.currency-icon {
|
||||
position: relative;
|
||||
left: 35px;
|
||||
top: -1px;
|
||||
width: 20px;
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
header span.currency-icon {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header .h-control select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
border: 1px solid #acacac;
|
||||
width: 130px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
padding: 1px 0 0 45px;
|
||||
font-size: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
header .icon.arrow {
|
||||
position: absolute;
|
||||
right: 25px;
|
||||
width: 10px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
header .h-control::-webkit-input-placeholder {
|
||||
/* Chrome/Opera/Safari */
|
||||
font-size: 12px;
|
||||
color: #605f64;
|
||||
}
|
||||
|
||||
header .h-control::-moz-placeholder {
|
||||
/* Firefox 19+ */
|
||||
font-size: 12px;
|
||||
color: #605f64;
|
||||
}
|
||||
|
||||
header .h-control :-ms-input-placeholder {
|
||||
/* IE 10+ */
|
||||
font-size: 12px;
|
||||
color: #605f64;
|
||||
}
|
||||
|
||||
header .h-control :-moz-placeholder {
|
||||
/* Firefox 18- */
|
||||
font-size: 12px;
|
||||
color: #605f64;
|
||||
}
|
||||
|
||||
header .navbar.sub-navbar {
|
||||
height: 60px;
|
||||
background-color: white;
|
||||
font-size: 15px;
|
||||
color: #b4b2bb;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.25);
|
||||
z-index: 1; /* Need this to see the box-shadow on the home page. */
|
||||
}
|
||||
|
||||
header .navbar.sub-navbar > .container {
|
||||
padding-left: 26px;
|
||||
padding-right: 26px;
|
||||
}
|
||||
|
||||
header .top-left-logo {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
header .top-left-logo-cymbal {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
header .navbar.sub-navbar .navbar-brand {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
header .navbar.sub-navbar a {
|
||||
color: #b4b2bb;
|
||||
}
|
||||
|
||||
header .navbar.sub-navbar nav a {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
header .navbar.sub-navbar .controls {
|
||||
display: flex;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
header .navbar.sub-navbar .controls a img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
|
||||
footer.py-5 {
|
||||
flex-shrink: 0;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
footer .footer-top {
|
||||
padding: 60px 0px;
|
||||
background-color: #570D2E;
|
||||
color: white;
|
||||
}
|
||||
|
||||
footer .footer-top a {
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* The <p> containing the session-id. */
|
||||
footer .footer-top p:nth-child(3) {
|
||||
margin-top: 56px;
|
||||
}
|
||||
|
||||
footer .footer-top .footer-social,
|
||||
footer .footer-top .footer-app,
|
||||
footer .footer-links,
|
||||
footer .footer-top .social,
|
||||
footer .footer-top .app {
|
||||
display: block;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
footer .footer-top .footer-social {
|
||||
padding: 31px;
|
||||
}
|
||||
|
||||
footer .footer-top .footer-social h4 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
footer .footer-top .footer-social div {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
/* Home */
|
||||
|
||||
main {
|
||||
flex: 1 0 auto;
|
||||
background-color: #F9F9F9;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.home .container-fluid {
|
||||
height: calc(100vh - 91px); /* 91px is the height of the top/header bars. */
|
||||
}
|
||||
.home .container-fluid > .row > .col-4 {
|
||||
height: calc(100vh - 91px);
|
||||
}
|
||||
.home .container-fluid > .row > .col-lg-8 {
|
||||
height: calc(100vh - 91px);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.px-10-percent {
|
||||
padding-left: 10%;
|
||||
padding-right: 10%;
|
||||
}
|
||||
}
|
||||
|
||||
.home-mobile-hero-banner {
|
||||
height: 200px;
|
||||
background: url(/static/images/folded-clothes-on-white-chair-wide.jpg) no-repeat top center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.home-desktop-left-image {
|
||||
background: url(/static/images/folded-clothes-on-white-chair.jpg) no-repeat center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.hot-products-row h3 {
|
||||
margin-bottom: 32px;
|
||||
margin-top: 56px;
|
||||
font-size: 36px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.hot-products-row {
|
||||
padding-bottom: 70px;
|
||||
padding-left: 10%;
|
||||
padding-right: 10%;
|
||||
}
|
||||
|
||||
.hot-product-card {
|
||||
margin-bottom: 52px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.hot-product-card img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 20% 0 20% 20%;
|
||||
}
|
||||
|
||||
.hot-product-card-name {
|
||||
margin-top: 8px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.hot-product-card-price {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.hot-product-card > a:first-child {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hot-product-card-img-overlay {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 20% 0 20% 20%;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.hot-product-card:hover .hot-product-card-img-overlay {
|
||||
background-color: rgba(71, 0, 29, 0.2);
|
||||
}
|
||||
|
||||
/*
|
||||
This chunk ensures the left/right padding of the footer is
|
||||
similar to that of the hot-products-row.
|
||||
*/
|
||||
.home-desktop-footer-row {
|
||||
padding-left: 9%;
|
||||
padding-right: 9%;
|
||||
background-color: #570D2E;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Ad */
|
||||
|
||||
.ad {
|
||||
position: relative;
|
||||
background-color: #FF9A9B;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* "Ad" text. */
|
||||
.ad strong {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.ad a {
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* Product */
|
||||
|
||||
.h-product {
|
||||
margin-top: 56px;
|
||||
margin-bottom: 112px;
|
||||
max-width: 1200px;
|
||||
background-color: #F9F9F9;
|
||||
}
|
||||
|
||||
.h-product > .row {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.h-product .product-image {
|
||||
width: 100%;
|
||||
border-radius: 20% 20% 0 20%;
|
||||
}
|
||||
|
||||
.h-product .product-price {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.h-product .product-info .product-wrapper {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.h-product .product-info h2 {
|
||||
margin-bottom: 16px;
|
||||
margin-top: 16px;
|
||||
font-size: 56px;
|
||||
line-height: 1.14;
|
||||
font-weight: normal;
|
||||
color: #111111;
|
||||
}
|
||||
|
||||
.h-product .product-packaging {
|
||||
margin: 0 0 15px 0;
|
||||
}
|
||||
|
||||
.h-product .product-packaging h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.h-product .product-packaging span {
|
||||
display: inline-block;
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
|
||||
.h-product .input-group-text,
|
||||
.h-product .btn.btn-info {
|
||||
font-size: 18px;
|
||||
line-height: 1.89;
|
||||
letter-spacing: 3.6px;
|
||||
text-align: center;
|
||||
color: #111111;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.product-quantity-dropdown {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.product-quantity-dropdown select {
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
border: 1px solid #acacac;
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.product-quantity-dropdown img {
|
||||
position: absolute;
|
||||
right: 25px;
|
||||
top: 20px;
|
||||
width: 10px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
.h-product .cymbal-button-primary {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* Platform Banner */
|
||||
|
||||
.local,
|
||||
.aws-platform,
|
||||
.onprem-platform,
|
||||
.azure-platform,
|
||||
.alibaba-platform,
|
||||
.gcp-platform {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 10px;
|
||||
height: 100vh;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.aws-platform,
|
||||
.aws-platform .platform-flag {
|
||||
background-color: #ff9900;
|
||||
}
|
||||
|
||||
.onprem-platform,
|
||||
.onprem-platform .platform-flag {
|
||||
background-color: #34A853;
|
||||
}
|
||||
|
||||
.gcp-platform,
|
||||
.gcp-platform .platform-flag {
|
||||
background-color: #4285f4;
|
||||
}
|
||||
|
||||
|
||||
.azure-platform,
|
||||
.azure-platform .platform-flag {
|
||||
background-color: #f35426;
|
||||
}
|
||||
|
||||
.alibaba-platform,
|
||||
.alibaba-platform .platform-flag {
|
||||
background-color: #ffC300;
|
||||
}
|
||||
|
||||
.local,
|
||||
.local .platform-flag {
|
||||
background-color: #2c0678;
|
||||
}
|
||||
|
||||
.platform-flag {
|
||||
position: absolute;
|
||||
top: 98px;
|
||||
left: 0;
|
||||
width: 190px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Recommendation */
|
||||
|
||||
.recommendations {
|
||||
background: #F9F9F9;
|
||||
padding-bottom: 55px;
|
||||
}
|
||||
|
||||
.recommendations .container {
|
||||
max-width: 1174px;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.recommendations .container {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.recommendations h2 {
|
||||
border-top: solid 1px;
|
||||
padding: 40px 0;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.recommendations h5 {
|
||||
margin-top: 8px;
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.recommendations img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 20% 0 20% 20%;
|
||||
}
|
||||
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
-webkit-border-radius: 0px;
|
||||
}
|
||||
|
||||
/* Cymbal */
|
||||
|
||||
/*
|
||||
If we ever decide to create a separate Cymbal CSS library for Cymbal components,
|
||||
the rules below could be extracted.
|
||||
*/
|
||||
|
||||
.cymbal-button-primary, .cymbal-button-secondary {
|
||||
display: inline-block;
|
||||
border: solid 1px #CE0631;
|
||||
padding: 8px 16px;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
border-radius: 22px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cymbal-button-primary:focus, .cymbal-button-secondary:focus {
|
||||
outline: none; /* To override browser (Chrome) default blue outline. */
|
||||
}
|
||||
|
||||
.cymbal-button-primary {
|
||||
background-color: #CE0631;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cymbal-button-primary:active,
|
||||
.cymbal-button-primary:focus,
|
||||
.cymbal-button-primary:hover {
|
||||
border: solid 1px #7b031d;
|
||||
background-color: #7b031d;
|
||||
box-shadow: 0px 2px 2px 0px rgb(0 0 0 / 30%);
|
||||
}
|
||||
|
||||
.cymbal-button-primary:active {
|
||||
box-shadow: 0px 3px 6px 0px rgb(0 0 0 / 30%);
|
||||
}
|
||||
|
||||
.cymbal-button-secondary {
|
||||
background: none;
|
||||
color: #CE0631;
|
||||
}
|
||||
|
||||
.cymbal-button-secondary:active,
|
||||
.cymbal-button-secondary:focus,
|
||||
.cymbal-button-secondary:hover {
|
||||
color: #7b031d;
|
||||
border: solid 1px #7b031d;
|
||||
}
|
||||
|
||||
.cymbal-button-secondary:active {
|
||||
background-color: #f5ccd5;
|
||||
}
|
||||
|
||||
.cymbal-form-field {
|
||||
position: relative;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.cymbal-form-field label {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 8px 16px 0 16px;
|
||||
font-size: 12px;
|
||||
line-height: 1.8em; /* Without this, there might be a 1px gap underneath. */
|
||||
font-weight: normal;
|
||||
border-radius: 4px 4px 0px 0px;
|
||||
color: #5C6063;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.cymbal-form-field input[type='email'],
|
||||
.cymbal-form-field input[type='password'],
|
||||
.cymbal-form-field select,
|
||||
.cymbal-form-field input[type='text'] {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-bottom: 1px solid #9AA0A6;
|
||||
padding: 0 16px 8px 16px;
|
||||
outline: none;
|
||||
color: #1E2021;
|
||||
}
|
||||
|
||||
.cymbal-form-field .cymbal-dropdown-chevron {
|
||||
position: absolute;
|
||||
right: 25px;
|
||||
width: 10px;
|
||||
height: 5px;
|
||||
}
|
||||
26
src/frontend/templates/ad.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!--
|
||||
Copyright 2020 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
{{ define "text_ad" }}
|
||||
<div class="container py-3 px-lg-5 py-lg-5">
|
||||
<div role="alert">
|
||||
<strong>Ad</strong>
|
||||
<a href="{{$.baseUrl}}{{.ad.RedirectUrl}}" rel="nofollow noopener noreferrer" target="_blank">
|
||||
{{.ad.Text}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
213
src/frontend/templates/assistant.html
Normal file
@@ -0,0 +1,213 @@
|
||||
<!--
|
||||
Copyright 2020 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
{{ define "assistant" }}
|
||||
|
||||
{{ template "header" . }}
|
||||
<div {{ with $.platform_css }} class="{{.}}" {{ end }}>
|
||||
<span class="platform-flag">
|
||||
{{$.platform_name}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<main role="main">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div id="chat-modal" class="chat-modal">
|
||||
<div id="bot-messages" class="bot-messages">
|
||||
<p class="bot-message">
|
||||
<span class="bot-message-text">Hi, I'm the Cymbal Shops assistant. I can help you with your shopping experience.</span>
|
||||
</p>
|
||||
<p class="bot-message">
|
||||
<span class="bot-message-text">What can I help you with?</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="bot-input">
|
||||
<input id="bot-input-text" type="text" style="margin-right: 30px;" class="bot-input-text" placeholder="Recommend me items...">
|
||||
<input type="file" class="bot-input-file-button" onchange="getBase64()">
|
||||
<button id="bot-input-button" class="bot-input-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
var image;
|
||||
function getBase64 () {
|
||||
var file = document.querySelector('input[type=file]')['files'][0];
|
||||
var reader = new FileReader();
|
||||
var baseString;
|
||||
reader.onloadend = function () {
|
||||
baseString = reader.result;
|
||||
console.log(baseString);
|
||||
image = baseString;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function extractIdsFromString(message) {
|
||||
const idPattern = /\[([a-zA-Z0-9-]+)\]/g;
|
||||
const matches = message.matchAll(idPattern);
|
||||
const ids = [];
|
||||
for (const match of matches) {
|
||||
ids.push(match[1]);
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
const chatModal = document.getElementById("chat-modal");
|
||||
const botMessages = document.getElementById("bot-messages");
|
||||
const botbutton = document.getElementById("bot-input-button");
|
||||
const botinput = document.getElementById("bot-input-text");
|
||||
|
||||
async function main() {
|
||||
botbutton.addEventListener("click", handleButtonClick);
|
||||
|
||||
botinput.addEventListener("keypress", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
botbutton.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function handleButtonClick() {
|
||||
if(!botinput.value || !botinput.value.trim){
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct and render user message
|
||||
console.log("bot button clicked");
|
||||
const message = botinput.value;
|
||||
console.log("message: " + message);
|
||||
const usermessage = document.createElement("p");
|
||||
const userMessageSpan = document.createElement("span");
|
||||
const imageDivElement = document.createElement("div");
|
||||
imageDivElement.classList.add("user-image-div")
|
||||
const imageElement = document.createElement("img");
|
||||
imageElement.src = image;
|
||||
imageElement.classList.add("user-image");
|
||||
imageElement.onerror = function() { this.style.display = 'none'; };
|
||||
userMessageSpan.innerText = message;
|
||||
userMessageSpan.classList.add("user-message-text");
|
||||
usermessage.classList.add("user-message");
|
||||
usermessage.appendChild(userMessageSpan);
|
||||
botMessages.appendChild(usermessage);
|
||||
imageDivElement.appendChild(imageElement);
|
||||
botMessages.appendChild(imageDivElement);
|
||||
botMessages.scrollTo(0, botMessages.scrollHeight);
|
||||
botinput.value = "";
|
||||
|
||||
// Disable send button and input field
|
||||
botbutton.disabled = true;
|
||||
botinput.disabled = true;
|
||||
console.log("bot is typing");
|
||||
|
||||
// Construct and render placeholder bot message
|
||||
const botMessage = document.createElement("p");
|
||||
botMessage.classList.add("bot-message-loading");
|
||||
const botMessageSpan = document.createElement("span");
|
||||
botMessageSpan.innerText = ""
|
||||
botMessageSpan.classList.add("bot-message-text");
|
||||
botMessage.classList.add("bot-message");
|
||||
botMessage.appendChild(botMessageSpan);
|
||||
botMessages.appendChild(botMessage);
|
||||
botMessages.scrollTo(0, botMessages.scrollHeight);
|
||||
|
||||
// Request a response from the Shopping Assistant
|
||||
const response = await fetch("{{ $.baseUrl }}/bot", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: message,
|
||||
image: image
|
||||
}),
|
||||
});
|
||||
const responseJson = await response.json();
|
||||
console.log(responseJson);
|
||||
|
||||
// Fetch the product IDs from the response
|
||||
const extractedIds = extractIdsFromString(responseJson.message);
|
||||
console.log(extractedIds);
|
||||
|
||||
// Replace the placeholder bot message text with the real response
|
||||
// Making sure to remove any lists or product IDs from that message
|
||||
botMessageSpan.innerText = responseJson.message.replace(/\n+[-*\d][\S\s]*/g, "");
|
||||
botMessage.classList.remove("bot-message-loading");
|
||||
|
||||
// If there are any product IDs...
|
||||
if (extractedIds.length > 0) {
|
||||
// Construct root products div
|
||||
const botProductsDiv = document.createElement("div");
|
||||
botProductsDiv.classList.add("bot-products");
|
||||
|
||||
// For each product...
|
||||
for (const id of extractedIds) {
|
||||
// Retrieve product metadata from the Product Catalog
|
||||
const productResponse = await fetch("{{ $.baseUrl }}/product-meta/" + id, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const product = await productResponse.json();
|
||||
|
||||
// Construct main product div
|
||||
const botProductDiv = document.createElement("a");
|
||||
botProductDiv.classList.add("bot-product");
|
||||
botProductDiv.href = "{{ $.baseUrl }}/product/" + id;
|
||||
|
||||
// Construct product image
|
||||
const botProductImg = document.createElement("img");
|
||||
botProductImg.src = product["picture"];
|
||||
botProductImg.classList.add("bot-product-img");
|
||||
botProductImg.onerror = function() { this.style.display = 'none'; };
|
||||
botProductDiv.appendChild(botProductImg);
|
||||
|
||||
// Construct product description div
|
||||
const botProductDescription = document.createElement("div");
|
||||
botProductDescription.classList.add("bot-product-description");
|
||||
let productDescription = product["description"];
|
||||
if (productDescription.length > 350) { // Shorten descriptions that are too long
|
||||
productDescription = productDescription.substring(0, 330) + '...';
|
||||
}
|
||||
botProductDescription.innerHTML = "<b>" + product["name"] + "</b><br>" + productDescription;
|
||||
botProductDiv.appendChild(botProductDescription);
|
||||
|
||||
// Append main product div into the root products div
|
||||
botProductsDiv.appendChild(botProductDiv);
|
||||
}
|
||||
// Render products
|
||||
botMessages.appendChild(botProductsDiv)
|
||||
}
|
||||
|
||||
botMessages.scrollTo(0, botMessages.scrollHeight);
|
||||
|
||||
// Re-enable button and input field
|
||||
botbutton.disabled = false;
|
||||
botinput.disabled = false;
|
||||
botinput.focus();
|
||||
}
|
||||
|
||||
main();
|
||||
</script>
|
||||
|
||||
{{ end }}
|
||||
233
src/frontend/templates/cart.html
Normal file
@@ -0,0 +1,233 @@
|
||||
<!--
|
||||
Copyright 2020 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
{{ define "cart" }}
|
||||
{{ template "header" . }}
|
||||
|
||||
<div {{ with $.platform_css }} class="{{.}}" {{ end }}>
|
||||
<span class="platform-flag">
|
||||
{{$.platform_name}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<main role="main" class="cart-sections">
|
||||
|
||||
{{ if eq (len $.items) 0 }}
|
||||
<section class="empty-cart-section">
|
||||
<h3>Your shopping cart is empty!</h3>
|
||||
<p>Items you add to your shopping cart will appear here.</p>
|
||||
<a class="cymbal-button-primary" href="{{ $.baseUrl }}/" role="button">Continue Shopping</a>
|
||||
</section>
|
||||
{{ else }}
|
||||
<section class="container">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-lg-6 col-xl-5 offset-xl-1 cart-summary-section">
|
||||
|
||||
<div class="row mb-3 py-2">
|
||||
<div class="col-4 pl-md-0">
|
||||
<h3>Cart ({{ $.cart_size }})</h3>
|
||||
</div>
|
||||
<div class="col-8 pr-md-0 text-right">
|
||||
<form method="POST" action="{{ $.baseUrl }}/cart/empty">
|
||||
<button class="cymbal-button-secondary cart-summary-empty-cart-button" type="submit">
|
||||
Empty Cart
|
||||
</button>
|
||||
<a class="cymbal-button-primary" href="{{ $.baseUrl }}/" role="button">
|
||||
Continue Shopping
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ range $.items }}
|
||||
<div class="row cart-summary-item-row">
|
||||
<div class="col-md-4 pl-md-0">
|
||||
<a href="{{ $.baseUrl }}/product/{{.Item.Id}}">
|
||||
<img class="img-fluid" alt="" src="{{ $.baseUrl }}{{.Item.Picture}}" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-8 pr-md-0">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h4>{{ .Item.Name }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row cart-summary-item-row-item-id-row">
|
||||
<div class="col">
|
||||
SKU #{{ .Item.Id }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Quantity: {{ .Quantity }}
|
||||
</div>
|
||||
<div class="col pr-md-0 text-right">
|
||||
<strong>
|
||||
{{ renderMoney .Price }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div class="row cart-summary-shipping-row">
|
||||
<div class="col pl-md-0">Shipping</div>
|
||||
<div class="col pr-md-0 text-right">{{ renderMoney .shipping_cost }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row cart-summary-total-row">
|
||||
<div class="col pl-md-0">Total</div>
|
||||
<div class="col pr-md-0 text-right">{{ renderMoney .total_cost }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5 offset-lg-1 col-xl-4">
|
||||
|
||||
<form class="cart-checkout-form" action="{{ $.baseUrl }}/cart/checkout" method="POST">
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>Shipping Address</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col cymbal-form-field">
|
||||
<label for="email">E-mail Address</label>
|
||||
<input type="email" id="email"
|
||||
name="email" value="someone@example.com" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col cymbal-form-field">
|
||||
<label for="street_address">Street Address</label>
|
||||
<input type="text" name="street_address"
|
||||
id="street_address" value="1600 Amphitheatre Parkway" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col cymbal-form-field">
|
||||
<label for="zip_code">Zip Code</label>
|
||||
<input type="text"
|
||||
name="zip_code" id="zip_code" value="94043" required pattern="\d{4,5}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col cymbal-form-field">
|
||||
<label for="city">City</label>
|
||||
<input type="text" name="city" id="city"
|
||||
value="Mountain View" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col-md-5 cymbal-form-field">
|
||||
<label for="state">State</label>
|
||||
<input type="text" name="state" id="state"
|
||||
value="CA" required>
|
||||
</div>
|
||||
<div class="col-md-7 cymbal-form-field">
|
||||
<label for="country">Country</label>
|
||||
<input type="text" id="country"
|
||||
placeholder="Country Name"
|
||||
name="country" value="United States" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3 class="payment-method-heading">Payment Method</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col cymbal-form-field">
|
||||
<label for="credit_card_number">Credit Card Number</label>
|
||||
<input type="text" id="credit_card_number"
|
||||
name="credit_card_number"
|
||||
placeholder="0000000000000000"
|
||||
value="4432801561520454"
|
||||
required pattern="\d{16}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col-md-5 cymbal-form-field">
|
||||
<label for="credit_card_expiration_month">Month</label>
|
||||
<select name="credit_card_expiration_month" id="credit_card_expiration_month">
|
||||
<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>
|
||||
<img src="{{ $.baseUrl }}/static/icons/Hipster_DownArrow.svg" alt="" class="cymbal-dropdown-chevron">
|
||||
</div>
|
||||
<div class="col-md-4 cymbal-form-field">
|
||||
<label for="credit_card_expiration_year">Year</label>
|
||||
<select name="credit_card_expiration_year" id="credit_card_expiration_year">
|
||||
{{ range $i, $y := $.expiration_years}}<option value="{{$y}}"
|
||||
{{if eq $i 1 -}}
|
||||
selected="selected"
|
||||
{{- end}}
|
||||
>{{$y}}</option>{{end}}
|
||||
</select>
|
||||
<img src="{{ $.baseUrl }}/static/icons/Hipster_DownArrow.svg" alt="" class="cymbal-dropdown-chevron">
|
||||
</div>
|
||||
<div class="col-md-3 cymbal-form-field">
|
||||
<label for="credit_card_cvv">CVV</label>
|
||||
<input type="password" id="credit_card_cvv"
|
||||
name="credit_card_cvv" value="672" required pattern="\d{3}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row justify-content-center">
|
||||
<div class="col text-center">
|
||||
<button class="cymbal-button-primary" type="submit">
|
||||
Place Order
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
{{ end }} <!-- end if $.items -->
|
||||
|
||||
</main>
|
||||
|
||||
{{ if $.recommendations }}
|
||||
{{ template "recommendations" $ }}
|
||||
{{ end }}
|
||||
|
||||
{{ template "footer" . }}
|
||||
{{ end }}
|
||||
40
src/frontend/templates/error.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!--
|
||||
Copyright 2020 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
{{ define "error" }}
|
||||
{{ template "header" . }}
|
||||
<div {{ with $.platform_css }} class="{{.}}" {{ end }}>
|
||||
<span class="platform-flag">
|
||||
{{$.platform_name}}
|
||||
</span>
|
||||
</div>
|
||||
<main role="main">
|
||||
<div class="py-5">
|
||||
<div class="container bg-light py-3 px-lg-5 py-lg-5">
|
||||
<h1>Uh, oh!</h1>
|
||||
<p>Something has failed. Below are some details for debugging.</p>
|
||||
|
||||
<p><strong>HTTP Status:</strong> {{.status_code}} {{.status}}</p>
|
||||
<pre class="border border-danger p-3"
|
||||
style="white-space: pre-wrap; word-break: keep-all;">
|
||||
{{- .error -}}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{ template "footer" . }}
|
||||
{{ end }}
|
||||
56
src/frontend/templates/footer.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<!--
|
||||
Copyright 2020 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
{{ define "footer" }}
|
||||
|
||||
<footer class="py-5">
|
||||
<div class="footer-top">
|
||||
<div class="container footer-social">
|
||||
<p class="footer-text">This website is hosted for demo purposes only. It is not an actual shop. This is not a Google product.</p>
|
||||
<p class="footer-text">© 2020-{{ .currentYear }} Google LLC (<a href="https://github.com/GoogleCloudPlatform/microservices-demo">Source Code</a>)</p>
|
||||
<p class="footer-text">
|
||||
<small>
|
||||
{{ if $.session_id }}session-id: {{ $.session_id }} — {{end}}
|
||||
{{ if $.request_id }}request-id: {{ $.request_id }}{{end}}
|
||||
</small>
|
||||
<br/>
|
||||
<small>
|
||||
{{ if $.deploymentDetails }}
|
||||
{{ if index .deploymentDetails "CLUSTERNAME" }}
|
||||
<b>Cluster: </b>{{ index .deploymentDetails "CLUSTERNAME" }}<br/>
|
||||
{{ end }}
|
||||
{{ if index .deploymentDetails "ZONE" }}
|
||||
<b>Zone: </b>{{ index .deploymentDetails "ZONE" }}<br/>
|
||||
{{ end }}
|
||||
{{ if index .deploymentDetails "HOSTNAME" }}
|
||||
<b>Pod: </b>{{ index .deploymentDetails "HOSTNAME" }}
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
Deployment details are still loading.
|
||||
Try refreshing this page.
|
||||
{{ end }}
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"
|
||||
integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous">
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{ end }}
|
||||
102
src/frontend/templates/header.html
Normal file
@@ -0,0 +1,102 @@
|
||||
<!--
|
||||
Copyright 2020 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
{{ define "header" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>
|
||||
{{ if $.is_cymbal_brand }}
|
||||
Cymbal Shops
|
||||
{{ else }}
|
||||
Online Boutique
|
||||
{{ end }}
|
||||
</title>
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB"
|
||||
crossorigin="anonymous">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Google+Symbols:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ $.baseUrl }}/static/styles/styles.css">
|
||||
<link rel="stylesheet" type="text/css" href="{{ $.baseUrl }}/static/styles/cart.css">
|
||||
<link rel="stylesheet" type="text/css" href="{{ $.baseUrl }}/static/styles/order.css">
|
||||
<link rel="stylesheet" type="text/css" href="{{ $.baseUrl }}/static/styles/bot.css">
|
||||
{{ if $.is_cymbal_brand }}
|
||||
<link rel='shortcut icon' type='image/x-icon' href='{{ $.baseUrl }}/static/favicon-cymbal.ico' />
|
||||
{{ else }}
|
||||
<link rel='shortcut icon' type='image/x-icon' href='{{ $.baseUrl }}/static/favicon.ico' />
|
||||
{{ end }}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
{{ if $.frontendMessage }}
|
||||
<div class="navbar">
|
||||
<div class="container d-flex justify-content-center">
|
||||
<div class="h-free-shipping">{{ $.frontendMessage }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="navbar sub-navbar">
|
||||
<div class="container d-flex justify-content-between">
|
||||
<a href="{{ $.baseUrl }}/" class="navbar-brand d-flex align-items-center">
|
||||
{{ if $.is_cymbal_brand }}
|
||||
<img src="{{ $.baseUrl }}/static/icons/Cymbal_NavLogo.svg" alt="" class="top-left-logo-cymbal" />
|
||||
{{ else }}
|
||||
<img src="{{ $.baseUrl }}/static/icons/Hipster_NavLogo.svg" alt="" class="top-left-logo" />
|
||||
{{ end }}
|
||||
</a>
|
||||
<div class="controls">
|
||||
|
||||
{{ if $.show_currency }}
|
||||
<div class="h-controls">
|
||||
<div class="h-control">
|
||||
<span class="icon currency-icon"> {{ renderCurrencyLogo $.user_currency}}</span>
|
||||
<form method="POST" class="controls-form" action="{{ $.baseUrl }}/setCurrency" id="currency_form" >
|
||||
<select name="currency_code" onchange="document.getElementById('currency_form').submit();">
|
||||
{{range $.currencies}}
|
||||
<option value="{{.}}" {{if eq . $.user_currency}}selected="selected"{{end}}>{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</form>
|
||||
<img src="{{ $.baseUrl }}/static/icons/Hipster_DownArrow.svg" alt="" class="icon arrow" />
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if $.assistant_enabled }}
|
||||
<a href="{{ $.baseUrl }}/assistant" class="cart-link">
|
||||
<img src="{{ $.baseUrl }}/static/icons/Hipster_WandIcon.svg" style="width: 22px; height: 22px;" alt="Assistant icon" class="logo" title="Assistant" />
|
||||
</a>
|
||||
{{ end }}
|
||||
|
||||
<a href="{{ $.baseUrl }}/cart" class="cart-link">
|
||||
<img src="{{ $.baseUrl }}/static/icons/Hipster_CartIcon.svg" alt="Cart icon" class="logo" title="Cart" />
|
||||
{{ if $.cart_size }}
|
||||
<span class="cart-size-circle">{{$.cart_size}}</span>
|
||||
{{ end }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
{{end}}
|
||||
80
src/frontend/templates/home.html
Normal file
@@ -0,0 +1,80 @@
|
||||
<!--
|
||||
Copyright 2020 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
{{ define "home" }}
|
||||
|
||||
{{ template "header" . }}
|
||||
<div {{ with $.platform_css }} class="{{.}}" {{ end }}>
|
||||
<span class="platform-flag">
|
||||
{{$.platform_name}}
|
||||
</span>
|
||||
</div>
|
||||
<main role="main" class="home">
|
||||
|
||||
<!-- The image at the top of the home page, displayed on smaller screens. -->
|
||||
<div class="home-mobile-hero-banner d-lg-none"></div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<!-- The image on the left of the home page, displayed on larger screens. -->
|
||||
<!--<div class="col-4 d-none d-lg-block home-desktop-left-image"></div>-->
|
||||
<!-- @TODO: removed temporarily. When uncommenting, also replace below div with this -->
|
||||
<!--<div class="col-12 col-lg-8">-->
|
||||
|
||||
<div class="col-12 col-lg-12 px-10-percent">
|
||||
|
||||
<div class="row hot-products-row px-xl-6">
|
||||
|
||||
<div class="col-12">
|
||||
<h3>Hot Products</h3>
|
||||
</div>
|
||||
|
||||
{{ range $.products }}
|
||||
<div class="col-md-4 hot-product-card">
|
||||
<a href="{{ $.baseUrl }}/product/{{.Item.Id}}">
|
||||
<img loading="lazy" src="{{ $.baseUrl }}{{.Item.Picture}}">
|
||||
<div class="hot-product-card-img-overlay"></div>
|
||||
</a>
|
||||
<div>
|
||||
<div class="hot-product-card-name">{{ .Item.Name }}</div>
|
||||
<div class="hot-product-card-price">{{ renderMoney .Price }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Footer for larger screens. -->
|
||||
<div class="row d-none d-lg-block home-desktop-footer-row">
|
||||
<div class="col-12 p-0">
|
||||
{{ template "footer" . }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Footer for smaller screens. -->
|
||||
<div class="d-lg-none">
|
||||
{{ template "footer" . }}
|
||||
</div>
|
||||
|
||||
{{ end }}
|
||||
80
src/frontend/templates/order.html
Normal file
@@ -0,0 +1,80 @@
|
||||
<!--
|
||||
Copyright 2020 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
{{ define "order" }}
|
||||
|
||||
{{ template "header" . }}
|
||||
|
||||
<div {{ with $.platform_css }} class="{{.}}" {{ end }}>
|
||||
<span class="platform-flag">
|
||||
{{$.platform_name}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<main role="main" class="order">
|
||||
|
||||
<section class="container order-complete-section">
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<h3>
|
||||
Your order is complete!
|
||||
</h3>
|
||||
</div>
|
||||
<div class="col-12 text-center">
|
||||
<p>We've sent you a confirmation email.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row border-bottom-solid padding-y-24">
|
||||
<div class="col-6 pl-md-0">
|
||||
Confirmation #
|
||||
</div>
|
||||
<div class="col-6 pr-md-0 text-right">
|
||||
{{.order.OrderId}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row border-bottom-solid padding-y-24">
|
||||
<div class="col-6 pl-md-0">
|
||||
Tracking #
|
||||
</div>
|
||||
<div class="col-6 pr-md-0 text-right">
|
||||
{{.order.ShippingTrackingId}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row padding-y-24">
|
||||
<div class="col-6 pl-md-0">
|
||||
Total Paid
|
||||
</div>
|
||||
<div class="col-6 pr-md-0 text-right">
|
||||
{{renderMoney .total_paid}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<a class="cymbal-button-primary" href="{{ $.baseUrl }}/" role="button">
|
||||
Continue Shopping
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{ if $.recommendations }}
|
||||
{{ template "recommendations" $ }}
|
||||
{{ end }}
|
||||
|
||||
</main>
|
||||
|
||||
{{ template "footer" . }}
|
||||
{{ end }}
|
||||
86
src/frontend/templates/product.html
Normal file
@@ -0,0 +1,86 @@
|
||||
<!--
|
||||
Copyright 2020 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
{{ define "product" }}
|
||||
{{ template "header" . }}
|
||||
<div {{ with $.platform_css }} class="{{.}}" {{ end }}>
|
||||
<span class="platform-flag">
|
||||
{{$.platform_name}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<main role="main">
|
||||
<div class="h-product container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<img class="product-image" alt="" src="{{ $.baseUrl }}{{$.product.Item.Picture}}" />
|
||||
</div>
|
||||
<div class="product-info col-md-5">
|
||||
<div class="product-wrapper">
|
||||
|
||||
<h2>{{ $.product.Item.Name }}</h2>
|
||||
<p class="product-price">{{ renderMoney $.product.Price }}</p>
|
||||
<p>{{ $.product.Item.Description }}</p>
|
||||
|
||||
{{ if $.packagingInfo }}
|
||||
<div class="product-packaging">
|
||||
<h3>Packaging</h3>
|
||||
<span>
|
||||
Weight: {{ if $.packagingInfo.Weight }}{{ $.packagingInfo.Weight }}lb{{ else }}n/a{{ end }}
|
||||
</span>
|
||||
<span>
|
||||
Width: {{ if $.packagingInfo.Width }}{{ $.packagingInfo.Width }}cm{{ else }}n/a{{ end }}
|
||||
</span>
|
||||
<span>
|
||||
Height: {{ if $.packagingInfo.Height }}{{ $.packagingInfo.Height }}cm{{ else }}n/a{{ end }}
|
||||
</span>
|
||||
<span>
|
||||
Depth: {{ if $.packagingInfo.Depth }}{{ $.packagingInfo.Depth }}cm{{ else }}n/a{{ end }}
|
||||
</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<form method="POST" action="{{ $.baseUrl }}/cart">
|
||||
<input type="hidden" name="product_id" value="{{$.product.Item.Id}}" />
|
||||
<div class="product-quantity-dropdown">
|
||||
<select name="quantity" id="quantity">
|
||||
<option>1</option>
|
||||
<option>2</option>
|
||||
<option>3</option>
|
||||
<option>4</option>
|
||||
<option>5</option>
|
||||
<option>10</option>
|
||||
</select>
|
||||
<img src="{{ $.baseUrl }}/static/icons/Hipster_DownArrow.svg" alt="">
|
||||
</div>
|
||||
<button type="submit" class="cymbal-button-primary">Add To Cart</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{{ if $.recommendations}}
|
||||
{{ template "recommendations" $ }}
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="ad">
|
||||
{{ if $.ad }}{{ template "text_ad" $ }}{{ end }}
|
||||
</div>
|
||||
|
||||
</main>
|
||||
{{ template "footer" . }}
|
||||
{{ end }}
|
||||
43
src/frontend/templates/recommendations.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!--
|
||||
Copyright 2020 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
{{ define "recommendations" }}
|
||||
<section class="recommendations">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-xl-10 offset-xl-1">
|
||||
<h2>You May Also Like</h2>
|
||||
<div class="row">
|
||||
{{ range .recommendations }}
|
||||
<div class="col-md-3">
|
||||
<div>
|
||||
<a href="{{ $.baseUrl }}/product/{{.Id}}">
|
||||
<img alt="" src="{{ $.baseUrl }}{{.Picture}}">
|
||||
</a>
|
||||
<div>
|
||||
<h5>
|
||||
{{ .Name }}
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{ end }}
|
||||
83
src/frontend/validator/validator.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright 2024 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
var validate *validator.Validate
|
||||
|
||||
// init() is a special function that will run when this package is imported.
|
||||
// It instantiates a SINGLE instance of *validator.Validate with the added
|
||||
// benefit of caching struct info and validations.
|
||||
func init() {
|
||||
validate = validator.New(validator.WithRequiredStructEnabled())
|
||||
}
|
||||
|
||||
type Payload interface {
|
||||
Validate() error
|
||||
}
|
||||
|
||||
type AddToCartPayload struct {
|
||||
Quantity uint64 `validate:"required,gte=1,lte=10"`
|
||||
ProductID string `validate:"required"`
|
||||
}
|
||||
|
||||
type PlaceOrderPayload struct {
|
||||
Email string `validate:"required,email"`
|
||||
StreetAddress string `validate:"required,max=512"`
|
||||
ZipCode int64 `validate:"required"`
|
||||
City string `validate:"required,max=128"`
|
||||
State string `validate:"required,max=128"`
|
||||
Country string `validate:"required,max=128"`
|
||||
CcNumber string `validate:"required,credit_card"`
|
||||
CcMonth int64 `validate:"required,gte=1,lte=12"`
|
||||
CcYear int64 `validate:"required"`
|
||||
CcCVV int64 `validate:"required"`
|
||||
}
|
||||
|
||||
type SetCurrencyPayload struct {
|
||||
Currency string `validate:"required,iso4217"`
|
||||
}
|
||||
|
||||
// Implementations of the 'Payload' interface.
|
||||
func (ad *AddToCartPayload) Validate() error {
|
||||
return validate.Struct(ad)
|
||||
}
|
||||
|
||||
func (po *PlaceOrderPayload) Validate() error {
|
||||
return validate.Struct(po)
|
||||
}
|
||||
|
||||
func (sc *SetCurrencyPayload) Validate() error {
|
||||
return validate.Struct(sc)
|
||||
}
|
||||
|
||||
// Reusable error response function.
|
||||
func ValidationErrorResponse(err error) error {
|
||||
validationErrs, ok := err.(validator.ValidationErrors)
|
||||
if !ok {
|
||||
return errors.New("invalid validation error format")
|
||||
}
|
||||
var msg string
|
||||
for _, err := range validationErrs {
|
||||
msg += fmt.Sprintf("Field '%s' is invalid: %s\n", err.Field(), err.Tag())
|
||||
}
|
||||
return fmt.Errorf("%s", msg)
|
||||
}
|
||||
185
src/frontend/validator/validator_test.go
Normal file
@@ -0,0 +1,185 @@
|
||||
// Copyright 2024 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPlaceOrderPassesValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
email string
|
||||
streetAddress string
|
||||
zipCode int64
|
||||
city string
|
||||
state string
|
||||
country string
|
||||
ccNumber string
|
||||
ccMonth int64
|
||||
ccYear int64
|
||||
ccCVV int64
|
||||
}{
|
||||
{"valid", "test@example.com", "12345 example street", 10004, "New York", "New York", "United States", "5272940000751666", 4, 2024, 584},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
payload := PlaceOrderPayload{
|
||||
Email: tt.email,
|
||||
StreetAddress: tt.streetAddress,
|
||||
ZipCode: tt.zipCode,
|
||||
City: tt.city,
|
||||
State: tt.state,
|
||||
Country: tt.country,
|
||||
CcNumber: tt.ccNumber,
|
||||
CcMonth: tt.ccMonth,
|
||||
CcYear: tt.ccYear,
|
||||
CcCVV: tt.ccCVV,
|
||||
}
|
||||
if err := payload.Validate(); err != nil {
|
||||
t.Errorf("want validation on %v, got %v", payload, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaceOrderFailsValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
email string
|
||||
streetAddress string
|
||||
zipCode int64
|
||||
city string
|
||||
state string
|
||||
country string
|
||||
ccNumber string
|
||||
ccMonth int64
|
||||
ccYear int64
|
||||
ccCVV int64
|
||||
}{
|
||||
{"invalid email", "test@example", "12345 example street", 10004, "New York", "New York", "United States", "5272940000751666", 4, 2024, 584},
|
||||
{"invalid address (too long)", "test@example.com", strings.Repeat("12345 example street", 513), 10004, "New York", "New York", "United States", "5272940000751666", 4, 2024, 584},
|
||||
{"invalid zip code", "test@example.com", "12345 example street", 0, "New York", "New York", "United States", "5272940000751666", 4, 2024, 584},
|
||||
{"invalid city", "test@example.com", "12345 example street", 10004, "", "New York", "United States", "5272940000751666", 4, 2024, 584},
|
||||
{"invalid state", "test@example.com", "12345 example street", 10004, "New York", "", "United States", "5272940000751666", 4, 2024, 584},
|
||||
{"invalid country", "test@example.com", "12345 example street", 10004, "New York", "New York", "", "5272940000751666", 4, 2024, 584},
|
||||
{"invalid ccNumber", "test@example.com", "12345 example street", 10004, "New York", "New York", "United States", "5272940000", 4, 2024, 584},
|
||||
{"invalid ccMonth (month < 1)", "test@example.com", "12345 example street", 10004, "New York", "New York", "United States", "5272940000751666", 0, 2024, 584},
|
||||
{"invalid ccMonth (month > 12)", "test@example.com", "12345 example street", 10004, "New York", "New York", "United States", "5272940000751666", 13, 2024, 584},
|
||||
{"invalid ccYear (not provided)", "test@example.com", "12345 example street", 10004, "New York", "New York", "United States", "5272940000751666", 12, 0, 584},
|
||||
{"invalid ccCVV (not provided)", "test@example.com", "12345 example street", 10004, "New York", "New York", "United States", "5272940000751666", 12, 2024, 0},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
payload := PlaceOrderPayload{
|
||||
Email: tt.email,
|
||||
StreetAddress: tt.streetAddress,
|
||||
ZipCode: tt.zipCode,
|
||||
City: tt.city,
|
||||
State: tt.state,
|
||||
Country: tt.country,
|
||||
CcNumber: tt.ccNumber,
|
||||
CcMonth: tt.ccMonth,
|
||||
CcYear: tt.ccYear,
|
||||
CcCVV: tt.ccCVV,
|
||||
}
|
||||
if err := payload.Validate(); err == nil {
|
||||
t.Errorf("want validation on %v, got %v", payload, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddToCartPassesValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
quantity uint64
|
||||
productID string
|
||||
}{
|
||||
{"valid min quantity and product id", 1, "OLJCESPC7Z"},
|
||||
{"valid max quantity and product id", 10, "OLJCESPC7Z"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
payload := AddToCartPayload{Quantity: tt.quantity, ProductID: tt.productID}
|
||||
if err := payload.Validate(); err != nil {
|
||||
t.Errorf("want validation on %v, got %v", payload, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddToCartFailsValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
quantity uint64
|
||||
productID string
|
||||
}{
|
||||
{"invalid min quantity", 0, "OLJCESPC7Z"},
|
||||
{"invalid max quantity", 11, "OLJCESPC7Z"},
|
||||
{"invalid product id", 1, ""},
|
||||
{"invalid quantity and product id", 0, ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
payload := AddToCartPayload{Quantity: tt.quantity, ProductID: tt.productID}
|
||||
if err := payload.Validate(); err == nil {
|
||||
t.Errorf("want validation on %v, got %v", payload, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetCurrencyPassesValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
currency string
|
||||
}{
|
||||
{"valid currency (EUR)", "EUR"},
|
||||
{"valid currency (USD)", "USD"},
|
||||
{"valid currency (JPY)", "JPY"},
|
||||
{"valid currency (GBP)", "GBP"},
|
||||
{"valid currency (TRY)", "TRY"},
|
||||
{"valid currency (CAD)", "CAD"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
payload := SetCurrencyPayload{Currency: tt.currency}
|
||||
if err := payload.Validate(); err != nil {
|
||||
t.Errorf("want validation on %v, got %v", payload, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetCurrencyFailsValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
currency string
|
||||
}{
|
||||
{"invalid currency", "ABC"},
|
||||
{"invalid currency (symbol)", "$"},
|
||||
{"invalid (no currency)", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
payload := SetCurrencyPayload{Currency: tt.currency}
|
||||
if err := payload.Validate(); err == nil {
|
||||
t.Errorf("want validation on %v, got %v", payload, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||