initial commit
Some checks failed
SonarQube Analysis / Build, Test & Analyse (push) Has been cancelled
Build and Publish TechDocs / build-and-publish (push) Has been cancelled

Change-Id: I12a20fc994c2a94df96de9d3393b06bf6687f77a
This commit is contained in:
Scaffolder
2026-04-17 11:20:50 +00:00
commit 4e3fd72697
376 changed files with 53620 additions and 0 deletions

View File

@@ -0,0 +1 @@
vendor/

0
src/frontend/.gitkeep Normal file
View File

44
src/frontend/Dockerfile Normal file
View 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
View File

@@ -0,0 +1,5 @@
# frontend
Run the following command to restore dependencies to `vendor/` directory:
dep ensure --vendor-only

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

58
src/frontend/go.mod Normal file
View 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
View 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
View 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
View 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
View 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
View 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
}

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

View 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

View 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

View 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

View 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

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

View 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

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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