subreddit-view: added subreddit name input
This commit is contained in:
parent
530723503e
commit
9ad5d8afdd
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
20
server/routes/page_subreddits_add.go
Normal file
20
server/routes/page_subreddits_add.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
91
views/subredditsview/addview/addview.templ
Normal file
91
views/subredditsview/addview/addview.templ
Normal 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'))"
|
||||||
|
/>
|
||||||
|
}
|
|
@ -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
17
views/utils/cx.go
Normal 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()
|
||||||
|
}
|
Loading…
Reference in a new issue