Skip to content

Commit

Permalink
Add rudimentary WeatherClient UI
Browse files Browse the repository at this point in the history
  • Loading branch information
calvinmclean committed Apr 29, 2024
1 parent 6dbd59a commit c731d2b
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 50 deletions.
3 changes: 3 additions & 0 deletions garden-app/server/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const (
waterScheduleDetailModalTemplate html.Template = "WaterScheduleDetailModal"
zoneModalTemplate html.Template = "ZoneModal"
zoneActionModalTemplate html.Template = "ZoneActionModal"
weatherClientsPageTemplate html.Template = "WeatherClientsPage"
weatherClientsTemplate html.Template = "WeatherClients"
weatherClientModalTemplate html.Template = "WeatherClientModal"
)

func templateFuncs(r *http.Request) map[string]any {
Expand Down
25 changes: 25 additions & 0 deletions garden-app/server/templates/weather_client_modal.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{{ define "WeatherClientModal" }}
<div id="modal" class="uk-modal" style="display:block;">
<div class="uk-modal-dialog uk-modal-body uk-text-center">
<h3 class="uk-modal-title">{{ if .Type }}{{ .Type }}{{ else }}Create Weather Client{{ end }}</h3>

<form _="on submit take .uk-open from #modal" hx-put="/weather_clients/{{ .ID }}"
hx-headers='{"Accept": "text/html"}' hx-swap="none">
<input type="hidden" value="{{ .ID }}" name="ID">
<div class="uk-margin">
<input class="uk-input" value="{{ .Type }}" placeholder="Type" name="Type">
</div>

<!-- TODO: use select/dropdown for Type and use selection to render a form with relevant inputs -->

{{ template "modalSubmitButton" }}
{{ if .Type }}
{{ template "deleteButton" (
args "HXDelete" (print "/weather_clients/" .ID) "HXTarget" (print "#weather-client-card-" .ID)
) }}
{{ end }}
{{ template "modalCloseButton" }}
</form>
</div>
</div>
{{ end }}
30 changes: 30 additions & 0 deletions garden-app/server/templates/weather_clients.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{{ define "WeatherClientsPage" }}
{{ template "start" }}
{{ template "WeatherClients" . }}
{{ template "end" }}
{{ end }}

{{ define "WeatherClients" }}
<div hx-swap="outerHTML" hx-get="/weather_clients?refresh=true" hx-headers='{"Accept": "text/html"}'
hx-trigger="{{ if NotRefresh }}load, {{ end }}newWeatherClient from:body" uk-grid>
{{ range .Items }}
{{ template "weatherClientCard" . }}
{{ end }}
</div>
{{ end }}

{{ define "weatherClientCard" }}
<div class="uk-width-1-2@m" id="weather-client-card-{{ .ID }}">
<div class="uk-card uk-card-default" style="margin: 5%;">
<div class="uk-card-header uk-text-center">
<h3 class="uk-card-title uk-margin-remove-bottom">
{{ .ID }}
</h3>
{{ template "cardEditButton" (print "/weather_clients/" .ID "/components?type=edit_modal") }}
</div>
<div class="uk-card-body">
{{ .Type }}
</div>
</div>
</div>
{{ end }}
64 changes: 64 additions & 0 deletions garden-app/server/weather_client_responses.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package server

import (
"fmt"
"net/http"
"slices"
"strings"

"github.com/calvinmclean/automated-garden/garden-app/pkg/weather"
"github.com/calvinmclean/babyapi"
"github.com/go-chi/render"
)

type WeatherClientTestResponse struct {
WeatherData
}

func (resp *WeatherClientTestResponse) Render(_ http.ResponseWriter, _ *http.Request) error {
return nil
}

type WeatherClientResponse struct {
*weather.Config

Links []Link `json:"links,omitempty"`
}

// Render ...
func (resp *WeatherClientResponse) Render(w http.ResponseWriter, r *http.Request) error {
if resp != nil {
resp.Links = append(resp.Links,
Link{
"self",
fmt.Sprintf("%s/%s", weatherClientsBasePath, resp.ID),
},
)
}

if render.GetAcceptedContentType(r) == render.ContentTypeHTML && r.Method == http.MethodPut {
w.Header().Add("HX-Trigger", "newWeatherClient")
}

return nil
}

type AllWeatherClientsResponse struct {
babyapi.ResourceList[*WeatherClientResponse]
}

func (aws AllWeatherClientsResponse) Render(w http.ResponseWriter, r *http.Request) error {
return aws.ResourceList.Render(w, r)
}

func (aws AllWeatherClientsResponse) HTML(r *http.Request) string {
slices.SortFunc(aws.Items, func(w *WeatherClientResponse, x *WeatherClientResponse) int {
return strings.Compare(w.Type, x.Type)
})

if r.URL.Query().Get("refresh") == "true" {
return weatherClientsTemplate.Render(r, aws)
}

return weatherClientsPageTemplate.Render(r, aws)
}
99 changes: 49 additions & 50 deletions garden-app/server/weather_clients.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package server

import (
"context"
"fmt"
"net/http"
"time"
Expand Down Expand Up @@ -46,8 +47,37 @@ func NewWeatherClientsAPI(storageClient *storage.Client) (*WeatherClientsAPI, er
api.SetResponseWrapper(func(wc *weather.Config) render.Renderer {
return &WeatherClientResponse{Config: wc}
})
api.SetGetAllResponseWrapper(func(wcs []*weather.Config) render.Renderer {
resp := AllWeatherClientsResponse{ResourceList: babyapi.ResourceList[*WeatherClientResponse]{}}

api.AddCustomIDRoute(http.MethodGet, "/test", http.HandlerFunc(api.testWeatherClient))
for _, wc := range wcs {
resp.ResourceList.Items = append(resp.ResourceList.Items, &WeatherClientResponse{Config: wc})
}

return resp
})

api.AddCustomIDRoute(http.MethodGet, "/test", babyapi.Handler(api.testWeatherClient))

api.AddCustomRoute(http.MethodGet, "/components", babyapi.Handler(func(_ http.ResponseWriter, r *http.Request) render.Renderer {
switch r.URL.Query().Get("type") {
case "create_modal":
return weatherClientModalTemplate.Renderer(&weather.Config{
ID: babyapi.NewID(),
})
default:
return babyapi.ErrInvalidRequest(fmt.Errorf("invalid component: %s", r.URL.Query().Get("type")))
}
}))

api.AddCustomIDRoute(http.MethodGet, "/components", api.GetRequestedResourceAndDo(func(r *http.Request, wc *weather.Config) (render.Renderer, *babyapi.ErrResponse) {
switch r.URL.Query().Get("type") {
case "edit_modal":
return weatherClientModalTemplate.Renderer(wc), nil
default:
return nil, babyapi.ErrInvalidRequest(fmt.Errorf("invalid component: %s", r.URL.Query().Get("type")))
}
}))

api.SetBeforeDelete(func(r *http.Request) *babyapi.ErrResponse {
id := api.GetIDParam(r)
Expand All @@ -67,81 +97,50 @@ func NewWeatherClientsAPI(storageClient *storage.Client) (*WeatherClientsAPI, er
return api, nil
}

func (api *WeatherClientsAPI) testWeatherClient(w http.ResponseWriter, r *http.Request) {
func (api *WeatherClientsAPI) testWeatherClient(_ http.ResponseWriter, r *http.Request) render.Renderer {
logger := babyapi.GetLoggerFromContext(r.Context())
logger.Info("received request to test WeatherClient")

weatherClient, httpErr := api.GetRequestedResource(r)
if httpErr != nil {
logger.Error("error getting requested resource", "error", httpErr.Error())
render.Render(w, r, httpErr)
return
return httpErr
}

weatherData, err := api.getWeatherData(r.Context(), weatherClient)
if err != nil {
logger.Error("unable to get weather data", "error", err)
return InternalServerError(err)
}

return &WeatherClientTestResponse{WeatherData: weatherData}
}

func (api *WeatherClientsAPI) getWeatherData(ctx context.Context, weatherClient *weather.Config) (WeatherData, error) {
wc, err := weather.NewClient(weatherClient, func(weatherClientOptions map[string]interface{}) error {
weatherClient.Options = weatherClientOptions
return api.storageClient.WeatherClientConfigs.Set(r.Context(), weatherClient)
return api.storageClient.WeatherClientConfigs.Set(ctx, weatherClient)
})
if err != nil {
logger.Error("unable to get WeatherClient", "error", err)
render.Render(w, r, InternalServerError(err))
return
return WeatherData{}, fmt.Errorf("error getting weather client: %w", err)
}

rd, err := wc.GetTotalRain(72 * time.Hour)
if err != nil {
logger.Error("unable to get total rain in the last 72 hours", "error", err)
render.Render(w, r, InternalServerError(err))
return
return WeatherData{}, fmt.Errorf("unable to get total rain in the last 72 hours: %w", err)
}

td, err := wc.GetAverageHighTemperature(72 * time.Hour)
if err != nil {
logger.Error("unable to get average high temperature in the last 72 hours", "error", err)
render.Render(w, r, InternalServerError(err))
return
return WeatherData{}, fmt.Errorf("unable to get average high temperature in the last 72 hours: %w", err)
}

resp := &WeatherClientTestResponse{WeatherData: WeatherData{
return WeatherData{
Rain: &RainData{
MM: rd,
},
Temperature: &TemperatureData{
Celsius: td,
},
}}

if err := render.Render(w, r, resp); err != nil {
logger.Error("unable to render WeatherClientResponse", "error", err)
render.Render(w, r, ErrRender(err))
}
}

// WeatherClientTestResponse is used to return WeatherData from testing that the client works
type WeatherClientTestResponse struct {
WeatherData
}

// Render ...
func (resp *WeatherClientTestResponse) Render(_ http.ResponseWriter, _ *http.Request) error {
return nil
}

type WeatherClientResponse struct {
*weather.Config

Links []Link `json:"links,omitempty"`
}

// Render ...
func (resp *WeatherClientResponse) Render(_ http.ResponseWriter, _ *http.Request) error {
if resp != nil {
resp.Links = append(resp.Links,
Link{
"self",
fmt.Sprintf("%s/%s", weatherClientsBasePath, resp.ID),
},
)
}
return nil
}, nil
}

0 comments on commit c731d2b

Please sign in to comment.