diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..be3dad1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{css,html,js,json}] +indent_style = space +indent_size = 2 + +[*.{ex,exs}] +indent_style = space +indent_size = 2 + +[*.go] +indent_style = tab +indent_size = 4 + +[*.py] +indent_style = space +indent_size = 4 diff --git a/.gitignore b/.gitignore index 9ed3fe2..ace45ec 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,6 @@ erl_crash.dump *.beam config/*.secret.exs elixir_ls/ + +# ---> Project +config/data/addresses.csv diff --git a/.tool-versions b/.tool-versions index bb5f805..5450066 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,5 +1,5 @@ -erlang 26.2.1 -elixir 1.16.0-otp-26 -poetry 1.7.1 -python 3.12.1 -go 1.21.6 +erlang 27.0.1 +elixir 1.17.2-otp-27 +poetry 1.8.3 +python 3.12.5 +go 1.22.6 diff --git a/Dockerfile b/Dockerfile index 777b719..a58caed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:bookworm-20240110-slim AS builder +FROM debian:bookworm-20240722-slim AS builder RUN apt-get update \ && apt-get install -y \ autoconf \ @@ -10,14 +10,18 @@ RUN apt-get update \ pkg-config \ && rm -rf /var/lib/apt/lists/* WORKDIR /opt/src -RUN git clone https://github.com/openvenues/libpostal.git \ +RUN git config --global http.version HTTP/1.1 \ + && git config --global http.postBuffer 524288000 \ + && git config --global http.lowSpeedLimit 0 \ + && git config --global http.lowSpeedTime 999999 \ + && git clone https://github.com/openvenues/libpostal.git \ && cd libpostal \ && ./bootstrap.sh \ && ./configure --datadir=/usr/local/share \ && make -j4 \ && make install -FROM hexpm/elixir:1.16.0-erlang-26.2.1-debian-bookworm-20231009-slim AS elixir +FROM hexpm/elixir:1.17.2-erlang-27.0.1-debian-bookworm-20240722-slim AS elixir RUN apt-get update \ && apt-get install -y \ build-essential \ @@ -31,13 +35,14 @@ COPY --from=builder /usr/local/lib/pkgconfig/libpostal.pc /usr/local/lib/pkgconf COPY --from=builder /usr/local/share/libpostal /usr/local/share/libpostal COPY --from=builder /usr/local/bin/libpostal_data /usr/local/bin/libpostal_data RUN ln -s /usr/local/lib/libpostal.so.1.0.1 /usr/local/lib/libpostal.so.1 \ - && ln -s /usr/local/lib/libpostal.so.1.0.1 /usr/local/lib/libpostal.so + && ln -s /usr/local/lib/libpostal.so.1.0.1 /usr/local/lib/libpostal.so \ + && ldconfig WORKDIR /opt/src/app VOLUME ["/opt/src/app/_build", "/opt/src/app/deps"] COPY ./ex . RUN mix do deps.get, deps.compile -FROM python:3.12.1-slim-bookworm AS python +FROM python:3.12.5-slim-bookworm AS python ENV PATH /root/.local/bin:${PATH} RUN apt-get update \ && apt-get install -y \ @@ -53,14 +58,15 @@ COPY --from=builder /usr/local/lib/pkgconfig/libpostal.pc /usr/local/lib/pkgconf COPY --from=builder /usr/local/share/libpostal /usr/local/share/libpostal COPY --from=builder /usr/local/bin/libpostal_data /usr/local/bin/libpostal_data RUN ln -s /usr/local/lib/libpostal.so.1.0.1 /usr/local/lib/libpostal.so.1 \ - && ln -s /usr/local/lib/libpostal.so.1.0.1 /usr/local/lib/libpostal.so + && ln -s /usr/local/lib/libpostal.so.1.0.1 /usr/local/lib/libpostal.so \ + && ldconfig WORKDIR /opt/src/app COPY ./py/pyproject.toml . COPY ./py/poetry.lock . RUN poetry install COPY ./py . -FROM golang:1.21.6-bookworm AS go +FROM golang:1.22.6-bookworm AS go COPY --from=builder /usr/local/include/libpostal /usr/local/include/libpostal COPY --from=builder /usr/local/lib/libpostal.a /usr/local/lib/ COPY --from=builder /usr/local/lib/libpostal.la /usr/local/lib/ @@ -69,7 +75,11 @@ COPY --from=builder /usr/local/lib/pkgconfig/libpostal.pc /usr/local/lib/pkgconf COPY --from=builder /usr/local/share/libpostal /usr/local/share/libpostal COPY --from=builder /usr/local/bin/libpostal_data /usr/local/bin/libpostal_data RUN ln -s /usr/local/lib/libpostal.so.1.0.1 /usr/local/lib/libpostal.so.1 \ - && ln -s /usr/local/lib/libpostal.so.1.0.1 /usr/local/lib/libpostal.so + && ln -s /usr/local/lib/libpostal.so.1.0.1 /usr/local/lib/libpostal.so \ + && ldconfig +WORKDIR /opt/src/config/data +COPY ./config/data/addresses.csv ./ WORKDIR /opt/src/app COPY ./go . -RUN go get ... +RUN go mod download \ + && go build diff --git a/config/data/addresses.csv.age b/config/data/addresses.csv.age new file mode 100644 index 0000000..88b221a Binary files /dev/null and b/config/data/addresses.csv.age differ diff --git a/config/meili/mapping/addressex.json b/config/meili/mapping/addressex.json new file mode 100644 index 0000000..d5e0d16 --- /dev/null +++ b/config/meili/mapping/addressex.json @@ -0,0 +1,30 @@ +{ + "displayedAttributes": [ + "country", + "state", + "city", + "neighborhood", + "road", + "house_number", + "house", + "unit", + "postal_code" + ], + "filterableAttributes": [ + "country", + "state", + "city", + "neighborhood", + "road", + "house_number", + "house", + "unit", + "postal_code" + ], + "searchableAttributes": [ + "house_number", + "house", + "unit", + "postal_code" + ], +} diff --git a/docker-compose.yml b/docker-compose.yml index ad9f561..ac68dc5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,23 +6,60 @@ services: image: 'joaodubas/addressex:builder' profiles: - build + hostname: libpostal + init: true + restart: unless-stopped entrypoint: sleep command: infinity + search: + image: 'getmeili/meilisearch:v1.9.0' + hostname: meili + restart: unless-stopped + environment: + MEILI_ENV: development + MEILI_MASTER_KEY: &meili_master_key ${ADDRESSEX_MEILI_KEY:-59EDmQofBp8vGT8kMvJJADPHRWHEAsWzZjCCqBFpVeuBmC2kWgCiBEgG7vfZ3ArY} + volumes: + - 'meili_data:/meili_data' + ports: + - '${ADDRESSEX_MEILI_PORT:-7700}:7700' + search_ui: + image: 'riccoxie/meilisearch-ui:v0.6.16' + hostname: search-ui + init: true + restart: unless-stopped + ports: + - '${ADDRESSEX_MEILI_UI_PORT:-24900}:24900' ex: build: target: elixir image: 'joaodubas/addressex:elixir' + hostname: ex + init: true + restart: unless-stopped entrypoint: sleep command: infinity py: build: target: python image: 'joaodubas/addressex:python' + hostname: py + init: true + restart: unless-stopped entrypoint: sleep command: infinity go: build: target: go image: 'joaodubas/addressex:go' + hostname: go + init: true + restart: unless-stopped + environment: + MEILI_MASTER_KEY: *meili_master_key + ports: + - '9000:9000' entrypoint: sleep command: infinity + +volumes: + meili_data: {} diff --git a/go/addresses.go b/go/addresses.go new file mode 100644 index 0000000..e48ee2c --- /dev/null +++ b/go/addresses.go @@ -0,0 +1,83 @@ +package main + +import ( + "crypto/sha1" + "encoding/hex" + "fmt" + + parser "github.com/openvenues/gopostal/parser" +) + +type Address struct { + ID string `json:"id"` + Country string `json:"country"` + State string `json:"state"` + City string `json:"city"` + Neighborhood string `json:"neighborhood"` + Road string `json:"road"` + HouseNumber string `json:"house_number"` + House string `json:"house"` + Unit string `json:"unit"` + PostalCode string `json:"postal_code"` + Raw string `json:"raw_address"` +} + +func (a Address) String() string { + return fmt.Sprintf( + "%s, %s, %s %s. %s, %s, %s, %s.", + a.Road, + a.HouseNumber, + a.Unit, + a.House, + a.Neighborhood, + a.City, + a.State, + a.PostalCode, + ) +} + +func (a Address) id() string { + hasher := sha1.New() + hasher.Write([]byte(a.String())) + return hex.EncodeToString(hasher.Sum(nil)) +} + +func parseAddress(address string) Address { + // log.Printf("Address to parse: %s", address) + components := parser.ParseAddress(address) + // log.Printf("Address components: %s", components) + return newFromComponents(address, components) +} + +func newFromComponents(raw string, components []parser.ParsedComponent) Address { + address := Address{Raw: raw} + for _, component := range components { + switch component.Label { + case "house": + address.House = component.Value + case "house_number": + address.HouseNumber = component.Value + case "road": + address.Road = component.Value + case "unit": + address.Unit = component.Value + case "postcode": + address.PostalCode = component.Value + case "suburb": + address.Neighborhood = component.Value + case "city_district": + address.Neighborhood = component.Value + case "city": + address.City = component.Value + case "state_district": + address.Neighborhood = component.Value + case "state": + address.State = component.Value + case "country": + address.Country = component.Value + } + } + address.ID = address.id() + // log.Printf("Address parsed: %s", address) + return address +} diff --git a/go/go.mod b/go/go.mod index b77888a..7915187 100644 --- a/go/go.mod +++ b/go/go.mod @@ -2,4 +2,18 @@ module joaodubas.dev/addressex go 1.21.6 -require github.com/openvenues/gopostal v0.0.0-20171226154602-e0184512a45d // indirect +require ( + github.com/meilisearch/meilisearch-go v0.26.1 + github.com/openvenues/gopostal v0.0.0-20171226154602-e0184512a45d +) + +require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.17.5 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect +) diff --git a/go/go.sum b/go/go.sum index d36fe51..aeb9608 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,2 +1,53 @@ +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/compress v1.17.5 h1:d4vBd+7CHydUqpFBgUEKkSdtSugf9YFmSkvUYPquI5E= +github.com/klauspost/compress v1.17.5/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/meilisearch/meilisearch-go v0.26.1 h1:3bmo2uLijX7kvBmiZ9LupVfC95TFcRJDgrRTzbOoE4A= +github.com/meilisearch/meilisearch-go v0.26.1/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0= github.com/openvenues/gopostal v0.0.0-20171226154602-e0184512a45d h1:KJ+N55d9zLN8fTg3NchLdmmAmPieXC5E6UNJ8zFFttU= github.com/openvenues/gopostal v0.0.0-20171226154602-e0184512a45d/go.mod h1:Ycrd7XnwQdumHzpB/6WEa85B4WNdbLC6Wz4FAQNkaV0= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.37.1-0.20220607072126-8a320890c08d/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/main.go b/go/main.go index 7f8d9b9..ed563b8 100644 --- a/go/main.go +++ b/go/main.go @@ -1,12 +1,25 @@ package main import ( - "fmt" - - parser "github.com/openvenues/gopostal/parser" + "html/template" + "log" + "net/http" ) func main() { - addressComponents := parser.ParseAddress("rua pedroso xavier 277, apto 51 torre 2. vila albertina, 02732-020, são paulo, sp, brasil") - fmt.Println(addressComponents) + http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + tmpl := template.Must(template.ParseFiles("./templates/index.html")) + tmpl.Execute(w, nil) + }) + + http.HandleFunc("/parse", func(w http.ResponseWriter, r *http.Request) { + tmpl := template.Must(template.ParseFiles("./templates/fragments/results.html")) + data := map[string]Address{"Result": parseAddress(r.URL.Query().Get("address"))} + tmpl.Execute(w, data) + }) + + log.Println("App running on 9000...") + log.Fatal(http.ListenAndServe(":9000", nil)) } diff --git a/go/meilisearch.go b/go/meilisearch.go new file mode 100644 index 0000000..055acd9 --- /dev/null +++ b/go/meilisearch.go @@ -0,0 +1,128 @@ +package main + +import ( + "encoding/csv" + "io" + "log" + "os" + + "github.com/meilisearch/meilisearch-go" +) + +func search() { + // client := newClient() + // _, err := client.Index("addressex").UpdateIndex() + // if err != nil { + // panic(err) + // } +} + +func init() { + // TODO: (jpd) move this logic to their own endopints + createIndex() + updateIndex() + loadAddresses() +} + +func createIndex() { + indexConfig := &meilisearch.IndexConfig{Uid: "addressex", PrimaryKey: "id"} + client := newClient() + _, err := client.CreateIndex(indexConfig) + if err != nil { + panic(err) + } +} + +func updateIndex() { + index := newClientIndex() + _, err := index.UpdateSettings(&meilisearch.Settings{ + DisplayedAttributes: []string{ + "id", + "country", + "state", + "city", + "neighborhood", + "road", + "house_number", + "house", + "unit", + "postal_code", + "raw_address", + }, + FilterableAttributes: []string{ + "country", + "state", + "city", + "neighborhood", + "road", + "house_number", + "house", + "unit", + "postal_code", + }, + SearchableAttributes: []string{ + "house_number", + "house", + "unit", + "postal_code", + }, + Synonyms: map[string][]string{ + "apartamento": []string{"apto", "apt", "ap"}, + "bloco": []string{"bl", "b"}, + "torre": []string{"tr", "t"}, + "conjunto": []string{"conj", "cj"}, + }, + }) + if err != nil { + panic(err) + } +} + +func loadAddresses() { + // TODO: (jpd) use env var to set address file + f, err := os.Open("/opt/src/config/data/addresses.csv") + if err != nil { + panic(err) + } + defer f.Close() + + index := newClientIndex() + + rows := csv.NewReader(f) + data := []Address{} + for { + row, err := rows.Read() + if err == io.EOF { + break + } else if err != nil { + panic(err) + } + rawAddress := row[0] + data = append(data, parseAddress(rawAddress)) + if len(data) == 1000 { + log.Printf("Recorded %d addresses", len(data)) + _, err := index.AddDocuments(data, "id") + if err != nil { + panic(err) + } + data = []Address{} + } + } + log.Printf("Recorded %d addresses", len(data)) + _, err = index.AddDocuments(data, "id") + if err != nil { + panic(err) + } +} + +func newClientIndex() *meilisearch.Index { + client := newClient() + return client.Index("addressex") +} + +func newClient() *meilisearch.Client { + return meilisearch.NewClient(meilisearch.ClientConfig{ + Host: "http://meili:7700", + APIKey: os.Getenv("MEILI_MASTER_KEY"), + }) +} diff --git a/go/static/css/index.css b/go/static/css/index.css new file mode 100644 index 0000000..f7a1249 --- /dev/null +++ b/go/static/css/index.css @@ -0,0 +1,100 @@ +:root { + --bg-color: #131415; + --primary-color: #9575CD; + --margin: 40px; + --margin-sm: 20px; + --radius: 6px; + --text-color: #FFF; + --header-height: 100px; +} + +body { + margin: 0; + background: var(--bg-color); + color: var(--text-color); + font-family: Arial, sans-serif; +} + +ul { + margin: 0; + padding: 0; + list-style-type: none; +} + +header { + background: #24292d; + height: var(--header-height); +} + +header ul { + display: flex; +} + +header ul li { + height: var(--header-height); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 0 var(--margin-sm); +} + +header ul li h5 { + font-size: 24px; + font-weight: 400; + margin: 0; +} + +header ul li h6 { + font-size: 14px; + font-weight: 100; + margin: 5px 0 0; +} + +header ul li h5 strong { + color: #81c784; +} + +section { + height: calc(100vh - var(--header-height)); + display: flex; + flex-direction: column; + align-items: center; +} + +section input { + height: 60px; + line-height: 60px; + background: transparent; + border: 2px solid var(--primary-color); + border-radius: var(--radius); + color: var(--text-color); + font-size: 20px; + outline: none !important; + padding: 0 20px; + width: 300px; + margin-top: var(--margin); +} + +#search-results ul li { + color: #FFF; + display: flex; +} + +#search-results ul li button { + align-self: center; + height: 34px; + padding: 0 15px; + border: 0; + border-radius: var( --radius); + margin-left: var(--margin-sm); + background: var(--primary-color); + color: #FFF; + font-weight: bold; + text-transform: uppercase; + cursor: pointer; +} + +#spinner { + display: none; +} diff --git a/go/templates/fragments/results.html b/go/templates/fragments/results.html new file mode 100644 index 0000000..4ef29d5 --- /dev/null +++ b/go/templates/fragments/results.html @@ -0,0 +1,18 @@ +