From 9ad5d8afdd71c437dd0c285aff0d7f018ccc04b3 Mon Sep 17 00:00:00 2001 From: Tigor Hutasuhut Date: Thu, 2 May 2024 23:16:28 +0700 Subject: [PATCH] subreddit-view: added subreddit name input --- api/reddit/check_subreddit.go | 4 +- api/reddit/get_posts.go | 20 +++- server/routes/page_subreddits_add.go | 20 ++++ server/routes/routes.go | 2 + server/routes/subreddit_check.go | 44 +++++++++ views/subredditsview/addview/addview.templ | 91 +++++++++++++++++++ .../detailsview/detailsview.templ | 2 +- views/utils/cx.go | 17 ++++ 8 files changed, 191 insertions(+), 9 deletions(-) create mode 100644 server/routes/page_subreddits_add.go create mode 100644 views/subredditsview/addview/addview.templ create mode 100644 views/utils/cx.go diff --git a/api/reddit/check_subreddit.go b/api/reddit/check_subreddit.go index 2553e3f..062c8b8 100644 --- a/api/reddit/check_subreddit.go +++ b/api/reddit/check_subreddit.go @@ -39,9 +39,7 @@ func (reddit *Reddit) CheckSubreddit(ctx context.Context, params CheckSubredditP defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { - // This happens for user pages. - // For subreddits, they will be 200 or 301/302 status code and has to be specially handled below. - return actual, errs.Wrapw(err, "user not found", "url", url, "params", params).Code(http.StatusNotFound) + return actual, errs.Wrapw(err, "user or subreddit not found", "url", url, "params", params).Code(http.StatusNotFound) } if resp.StatusCode >= 400 { diff --git a/api/reddit/get_posts.go b/api/reddit/get_posts.go index 729d811..ec5c327 100644 --- a/api/reddit/get_posts.go +++ b/api/reddit/get_posts.go @@ -7,6 +7,7 @@ import ( "io" "log/slog" "net/http" + "strconv" "strings" "github.com/tigorlazuardi/redmage/pkg/errs" @@ -15,19 +16,28 @@ import ( type SubredditType int func (su *SubredditType) UnmarshalJSON(b []byte) error { - switch string(b) { - case "null": + if len(b) == 4 && string(b) == "null" { return nil + } + s, err := strconv.Unquote(string(b)) + if err != nil { + return errs.Wrapw(err, "failed to unquote string json value").Code(http.StatusBadRequest) + } + return su.Parse(s) +} + +func (su *SubredditType) Parse(s string) error { + switch s { case `"user"`, `"u"`, "1": *su = SubredditTypeUser return nil - case `"r"`, `"subreddit"`, "0": + case `"r"`, `"subreddit"`, "0", "": *su = SubredditTypeSub return nil } return errs. - Fail("subreddit type not recognized. Valid values are 'user', 'u', 'r', 'subreddit', 0, 1, and null", - "got", string(b), + Fail("subreddit type not recognized. Valid values are '' (empty), 'user', 'u', 'r', 'subreddit', 0, 1, and null", + "got", s, ). Code(http.StatusBadRequest) } diff --git a/server/routes/page_subreddits_add.go b/server/routes/page_subreddits_add.go new file mode 100644 index 0000000..281c334 --- /dev/null +++ b/server/routes/page_subreddits_add.go @@ -0,0 +1,20 @@ +package routes + +import ( + "net/http" + + "github.com/tigorlazuardi/redmage/pkg/log" + "github.com/tigorlazuardi/redmage/views" + "github.com/tigorlazuardi/redmage/views/subredditsview/addview" +) + +func (routes *Routes) PageSubredditsAdd(rw http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "Routes.PageSubredditsAdd") + defer span.End() + + c := views.NewContext(routes.Config, r) + + if err := addview.Addview(c).Render(ctx, rw); err != nil { + log.New(ctx).Err(err).Error("failed to render subreddits add page") + } +} diff --git a/server/routes/routes.go b/server/routes/routes.go index c89b9e9..46d51ca 100644 --- a/server/routes/routes.go +++ b/server/routes/routes.go @@ -59,6 +59,7 @@ func (routes *Routes) registerHTMXRoutes(router chi.Router) { router.Use(chimiddleware.SetHeader("Content-Type", "text/html; charset=utf-8")) router.Post("/subreddits/start", routes.SubredditStartDownloadHTMX) + router.Post("/subreddits/check", routes.SubredditCheckHTMX) } func (routes *Routes) registerWWWRoutes(router chi.Router) { @@ -76,6 +77,7 @@ func (routes *Routes) registerWWWRoutes(router chi.Router) { r.Get("/", routes.PageHome) r.Get("/subreddits", routes.PageSubreddits) r.Get("/subreddits/details/{name}", routes.PageSubredditsDetails) + r.Get("/subreddits/add", routes.PageSubredditsAdd) r.Get("/config", routes.PageConfig) }) } diff --git a/server/routes/subreddit_check.go b/server/routes/subreddit_check.go index 7dcaa94..d2ca2c5 100644 --- a/server/routes/subreddit_check.go +++ b/server/routes/subreddit_check.go @@ -7,8 +7,10 @@ import ( "net/http" "github.com/tigorlazuardi/redmage/api" + "github.com/tigorlazuardi/redmage/api/reddit" "github.com/tigorlazuardi/redmage/pkg/errs" "github.com/tigorlazuardi/redmage/pkg/log" + "github.com/tigorlazuardi/redmage/views/subredditsview/addview" ) func (routes *Routes) SubredditsCheckAPI(rw http.ResponseWriter, r *http.Request) { @@ -45,6 +47,48 @@ func (routes *Routes) SubredditsCheckAPI(rw http.ResponseWriter, r *http.Request _ = enc.Encode(map[string]string{"subreddit": actual}) } +func (routes *Routes) SubredditCheckHTMX(rw http.ResponseWriter, r *http.Request) { + var data addview.SubredditInputData + name := r.FormValue("name") + data.Value = name + + if name == "" { + if err := addview.SubredditInputForm(data).Render(r.Context(), rw); err != nil { + log.New(r.Context()).Err(err).Error("failed to render subreddit input form") + } + return + } + + ctx, span := tracer.Start(r.Context(), "*Routes.SubredditCheckHTMX") + defer span.End() + + var t reddit.SubredditType + _ = t.Parse(r.FormValue("type")) + + params := api.SubredditCheckParam{ + Subreddit: name, + SubredditType: t, + } + + actual, err := routes.API.SubredditCheck(ctx, params) + if err != nil { + log.New(ctx).Err(err).Error("failed to check subreddit") + code, message := errs.HTTPMessage(err) + rw.WriteHeader(code) + data.Error = message + if err := addview.SubredditInputForm(data).Render(r.Context(), rw); err != nil { + log.New(r.Context()).Err(err).Error("failed to render subreddit input form") + } + return + } + data.Value = actual + data.Valid = true + + if err := addview.SubredditInputForm(data).Render(r.Context(), rw); err != nil { + log.New(r.Context()).Err(err).Error("failed to render subreddit input form") + } +} + func validateSubredditCheckParam(body api.SubredditCheckParam) error { if body.Subreddit == "" { return errors.New("subreddit name is required") diff --git a/views/subredditsview/addview/addview.templ b/views/subredditsview/addview/addview.templ new file mode 100644 index 0000000..0a41353 --- /dev/null +++ b/views/subredditsview/addview/addview.templ @@ -0,0 +1,91 @@ +package addview + +import "github.com/tigorlazuardi/redmage/views" +import "github.com/tigorlazuardi/redmage/views/components" +import "github.com/tigorlazuardi/redmage/views/utils" + +templ Addview(c *views.Context) { + @components.Doctype() { + @components.Head(c, components.HeadTitle("Redmage - Subreddits")) + @components.Body(c) { + @AddviewContent(c) + @components.NotificationContainer() + } + } +} + +templ AddviewContent(c *views.Context) { +
+ @components.Container() { +

Add Subreddit

+
+
+ +
+ } +
+} + +type SubredditInputData struct { + Value string + Error string + Valid bool +} + +templ SubredditInputForm(data SubredditInputData) { +
+ Subreddit Name +
+ @subredditInputField("/htmx/subreddits/check", data) +
+ + if data.Valid { + Subreddit / User target is valid + } else { + { data.Error } + } + +
+} + +templ subredditInputField(target string, data SubredditInputData) { + +} diff --git a/views/subredditsview/detailsview/detailsview.templ b/views/subredditsview/detailsview/detailsview.templ index d79713b..37f9d4e 100644 --- a/views/subredditsview/detailsview/detailsview.templ +++ b/views/subredditsview/detailsview/detailsview.templ @@ -17,7 +17,7 @@ type Data struct { templ Detailsview(c *views.Context, data Data) { @components.Doctype() { - @components.Head(c, components.HeadTitle("Redmage - Subreddits")) + @components.Head(c, components.HeadTitle(fmt.Sprintf("Subreddit - %s", data.Subreddit.Name))) @components.Body(c) { @DetailsContent(c, data) @components.NotificationContainer() diff --git a/views/utils/cx.go b/views/utils/cx.go new file mode 100644 index 0000000..2e6320e --- /dev/null +++ b/views/utils/cx.go @@ -0,0 +1,17 @@ +package utils + +import "strings" + +// CX is a helper function to generate a string of class names based +// on a map of class names and their conditions. +func CX(classes map[string]bool) string { + b := strings.Builder{} + for class, condition := range classes { + if condition { + b.WriteString(class) + b.WriteString(" ") + } + } + + return b.String() +}