From bea355c9c70396800e1a0807b387f0e6d1704e2e Mon Sep 17 00:00:00 2001 From: Calvin McLean Date: Sun, 28 Apr 2024 21:14:42 -0700 Subject: [PATCH] Add rudimentary WeatherClient UI --- garden-app/server/templates.go | 3 + .../templates/weather_client_modal.html | 23 +++++ .../server/templates/weather_clients.html | 30 ++++++ garden-app/server/weather_client_responses.go | 64 +++++++++++++ garden-app/server/weather_clients.go | 93 +++++++++---------- 5 files changed, 163 insertions(+), 50 deletions(-) create mode 100644 garden-app/server/templates/weather_client_modal.html create mode 100644 garden-app/server/templates/weather_clients.html create mode 100644 garden-app/server/weather_client_responses.go diff --git a/garden-app/server/templates.go b/garden-app/server/templates.go index b286b85c..df2e2471 100644 --- a/garden-app/server/templates.go +++ b/garden-app/server/templates.go @@ -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 { diff --git a/garden-app/server/templates/weather_client_modal.html b/garden-app/server/templates/weather_client_modal.html new file mode 100644 index 00000000..abbaf588 --- /dev/null +++ b/garden-app/server/templates/weather_client_modal.html @@ -0,0 +1,23 @@ +{{ define "WeatherClientModal" }} + +{{ end }} \ No newline at end of file diff --git a/garden-app/server/templates/weather_clients.html b/garden-app/server/templates/weather_clients.html new file mode 100644 index 00000000..849f9246 --- /dev/null +++ b/garden-app/server/templates/weather_clients.html @@ -0,0 +1,30 @@ +{{ define "WeatherClientsPage" }} +{{ template "start" }} +{{ template "WeatherClients" . }} +{{ template "end" }} +{{ end }} + +{{ define "WeatherClients" }} +
+ {{ range .Items }} + {{ template "weatherClientCard" . }} + {{ end }} +
+{{ end }} + +{{ define "weatherClientCard" }} +
+
+
+

+ {{ .ID }} +

+ {{ template "cardEditButton" (print "/weather_clients/" .ID "/components?type=edit_modal") }} +
+
+ {{ .Type }} +
+
+
+{{ end }} \ No newline at end of file diff --git a/garden-app/server/weather_client_responses.go b/garden-app/server/weather_client_responses.go new file mode 100644 index 00000000..286b139f --- /dev/null +++ b/garden-app/server/weather_client_responses.go @@ -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[*weather.Config] +} + +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 *weather.Config, x *weather.Config) 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) +} diff --git a/garden-app/server/weather_clients.go b/garden-app/server/weather_clients.go index a4bc84b9..03eb4cdd 100644 --- a/garden-app/server/weather_clients.go +++ b/garden-app/server/weather_clients.go @@ -1,6 +1,7 @@ package server import ( + "context" "fmt" "net/http" "time" @@ -46,8 +47,31 @@ 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 { + return AllWeatherClientsResponse{ResourceList: babyapi.ResourceList[*weather.Config]{wcs}} + }) + + api.AddCustomIDRoute(http.MethodGet, "/test", babyapi.Handler(api.testWeatherClient)) - api.AddCustomIDRoute(http.MethodGet, "/test", http.HandlerFunc(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) @@ -67,81 +91,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(w 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 }