subreddit-view: added subreddit name input

This commit is contained in:
Tigor Hutasuhut 2024-05-02 23:16:28 +07:00
parent 530723503e
commit 9ad5d8afdd
8 changed files with 191 additions and 9 deletions

View file

@ -39,9 +39,7 @@ func (reddit *Reddit) CheckSubreddit(ctx context.Context, params CheckSubredditP
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound { if resp.StatusCode == http.StatusNotFound {
// This happens for user pages. return actual, errs.Wrapw(err, "user or subreddit not found", "url", url, "params", params).Code(http.StatusNotFound)
// 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)
} }
if resp.StatusCode >= 400 { if resp.StatusCode >= 400 {

View file

@ -7,6 +7,7 @@ import (
"io" "io"
"log/slog" "log/slog"
"net/http" "net/http"
"strconv"
"strings" "strings"
"github.com/tigorlazuardi/redmage/pkg/errs" "github.com/tigorlazuardi/redmage/pkg/errs"
@ -15,19 +16,28 @@ import (
type SubredditType int type SubredditType int
func (su *SubredditType) UnmarshalJSON(b []byte) error { func (su *SubredditType) UnmarshalJSON(b []byte) error {
switch string(b) { if len(b) == 4 && string(b) == "null" {
case "null":
return nil 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": case `"user"`, `"u"`, "1":
*su = SubredditTypeUser *su = SubredditTypeUser
return nil return nil
case `"r"`, `"subreddit"`, "0": case `"r"`, `"subreddit"`, "0", "":
*su = SubredditTypeSub *su = SubredditTypeSub
return nil return nil
} }
return errs. return errs.
Fail("subreddit type not recognized. Valid values are 'user', 'u', 'r', 'subreddit', 0, 1, and null", Fail("subreddit type not recognized. Valid values are '' (empty), 'user', 'u', 'r', 'subreddit', 0, 1, and null",
"got", string(b), "got", s,
). ).
Code(http.StatusBadRequest) Code(http.StatusBadRequest)
} }

View file

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

View file

@ -59,6 +59,7 @@ func (routes *Routes) registerHTMXRoutes(router chi.Router) {
router.Use(chimiddleware.SetHeader("Content-Type", "text/html; charset=utf-8")) router.Use(chimiddleware.SetHeader("Content-Type", "text/html; charset=utf-8"))
router.Post("/subreddits/start", routes.SubredditStartDownloadHTMX) router.Post("/subreddits/start", routes.SubredditStartDownloadHTMX)
router.Post("/subreddits/check", routes.SubredditCheckHTMX)
} }
func (routes *Routes) registerWWWRoutes(router chi.Router) { func (routes *Routes) registerWWWRoutes(router chi.Router) {
@ -76,6 +77,7 @@ func (routes *Routes) registerWWWRoutes(router chi.Router) {
r.Get("/", routes.PageHome) r.Get("/", routes.PageHome)
r.Get("/subreddits", routes.PageSubreddits) r.Get("/subreddits", routes.PageSubreddits)
r.Get("/subreddits/details/{name}", routes.PageSubredditsDetails) r.Get("/subreddits/details/{name}", routes.PageSubredditsDetails)
r.Get("/subreddits/add", routes.PageSubredditsAdd)
r.Get("/config", routes.PageConfig) r.Get("/config", routes.PageConfig)
}) })
} }

View file

@ -7,8 +7,10 @@ import (
"net/http" "net/http"
"github.com/tigorlazuardi/redmage/api" "github.com/tigorlazuardi/redmage/api"
"github.com/tigorlazuardi/redmage/api/reddit"
"github.com/tigorlazuardi/redmage/pkg/errs" "github.com/tigorlazuardi/redmage/pkg/errs"
"github.com/tigorlazuardi/redmage/pkg/log" "github.com/tigorlazuardi/redmage/pkg/log"
"github.com/tigorlazuardi/redmage/views/subredditsview/addview"
) )
func (routes *Routes) SubredditsCheckAPI(rw http.ResponseWriter, r *http.Request) { 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}) _ = 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 { func validateSubredditCheckParam(body api.SubredditCheckParam) error {
if body.Subreddit == "" { if body.Subreddit == "" {
return errors.New("subreddit name is required") return errors.New("subreddit name is required")

View file

@ -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) {
<main class="prose min-w-full">
@components.Container() {
<h1>Add Subreddit</h1>
<div class="divider"></div>
<form hx-post="/htmx/subreddit/add" class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<label id="subreddit-input" class="form-control w-full">
@SubredditInputForm(SubredditInputData{})
</label>
</form>
}
</main>
}
type SubredditInputData struct {
Value string
Error string
Valid bool
}
templ SubredditInputForm(data SubredditInputData) {
<div class="label">
<span
class={ utils.CX(map[string]bool{
"label-text": true,
"text-error": data.Error != "",
"text-success": data.Valid,
"text-base": true,
}) }
>Subreddit Name</span>
</div>
@subredditInputField("/htmx/subreddits/check", data)
<div class="label">
<span
class={ utils.CX(map[string]bool{
"label-text": true,
"text-error": data.Error != "",
"text-success": data.Valid,
"min-h-[1rem]": true,
}) }
>
if data.Valid {
Subreddit / User target is valid
} else {
{ data.Error }
}
</span>
</div>
}
templ subredditInputField(target string, data SubredditInputData) {
<input
type="text"
id="name"
name="name"
hx-post={ target }
hx-target="#subreddit-input"
hx-target-4xx="#subreddit-input"
hx-trigger="keyup changed delay:1s, on-demand"
hx-target-5x={ components.NotificationContainerID }
value={ data.Value }
placeholder="e.g. 'wallpaper' or 'EarthPorn'"
class={ utils.CX(map[string]bool{
"input": true,
"input-bordered": true,
"input-error": data.Error != "",
"text-error": data.Error != "",
"input-success": data.Valid,
"text-success": data.Valid,
}) }
required
data-error={ data.Error }
hx-on::load="this.setCustomValidity(this.getAttribute('data-error'))"
/>
}

View file

@ -17,7 +17,7 @@ type Data struct {
templ Detailsview(c *views.Context, data Data) { templ Detailsview(c *views.Context, data Data) {
@components.Doctype() { @components.Doctype() {
@components.Head(c, components.HeadTitle("Redmage - Subreddits")) @components.Head(c, components.HeadTitle(fmt.Sprintf("Subreddit - %s", data.Subreddit.Name)))
@components.Body(c) { @components.Body(c) {
@DetailsContent(c, data) @DetailsContent(c, data)
@components.NotificationContainer() @components.NotificationContainer()

17
views/utils/cx.go Normal file
View file

@ -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()
}