refactor subreddits creation page
Some checks failed
/ push (push) Has been cancelled

This commit is contained in:
Tigor Hutasuhut 2024-05-27 21:10:37 +07:00
parent 88f4edbaf5
commit 5957648ec3
18 changed files with 474 additions and 151 deletions

View file

@ -25,7 +25,7 @@ func (reddit *Reddit) CheckSubreddit(ctx context.Context, params CheckSubredditP
ctx = caller.WithContext(ctx, caller.New(2)) ctx = caller.WithContext(ctx, caller.New(2))
url := fmt.Sprintf("https://reddit.com/%s/%s.json?limit=1", params.SubredditType, params.Subreddit) url := fmt.Sprintf("https://reddit.com/%s/%s.json?limit=1", params.SubredditType.Code(), params.Subreddit)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil { if err != nil {
return actual, errs.Wrapw(err, "failed to create request", "url", url, "params", params) return actual, errs.Wrapw(err, "failed to create request", "url", url, "params", params)

View file

@ -57,7 +57,12 @@ func (s SubredditType) Code() string {
} }
func (s SubredditType) String() string { func (s SubredditType) String() string {
return s.Code() switch s {
case SubredditTypeUser:
return "User"
default:
return "Subreddit"
}
} }
type GetPostsParam struct { type GetPostsParam struct {

View file

@ -0,0 +1,26 @@
-- +goose Up
-- +goose StatementBegin
DROP TRIGGER IF EXISTS update_subreddits_timestamp; -- Faulty trigger. Must be removed and never recovered.
CREATE TRIGGER subreddits_update_timestamp_on_update AFTER UPDATE ON subreddits FOR EACH ROW
BEGIN
UPDATE subreddits SET updated_at = unixepoch() WHERE name = old.name;
END;
CREATE TRIGGER devices_update_timestamp_on_update AFTER UPDATE ON devices FOR EACH ROW
BEGIN
UPDATE devices SET updated_at = unixepoch() WHERE slug = old.slug;
END;
CREATE TRIGGER subreddits_update_timestamp_on_image_insert AFTER INSERT ON images FOR EACH ROW
BEGIN
UPDATE subreddits SET updated_at = unixepoch() WHERE name = new.subreddit; -- new -> image row.
END;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TRIGGER IF EXISTS subreddits_update_timestamp_on_update;
DROP TRIGGER IF EXISTS devices_update_timestamp_on_update;
DROP TRIGGER IF EXISTS subreddits_update_timestamp_on_image_insert;
-- +goose StatementEnd

View file

@ -7,7 +7,7 @@ import (
"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" "github.com/tigorlazuardi/redmage/views"
"github.com/tigorlazuardi/redmage/views/subredditsview" "github.com/tigorlazuardi/redmage/views/subreddits"
) )
func (routes *Routes) PageSubreddits(rw http.ResponseWriter, r *http.Request) { func (routes *Routes) PageSubreddits(rw http.ResponseWriter, r *http.Request) {
@ -19,7 +19,7 @@ func (routes *Routes) PageSubreddits(rw http.ResponseWriter, r *http.Request) {
var params api.ListSubredditsParams var params api.ListSubredditsParams
params.FillFromQuery(r.URL.Query()) params.FillFromQuery(r.URL.Query())
var data subredditsview.Data var data subreddits.Data
var err error var err error
data.Subreddits, err = routes.API.ListSubredditsWithCover(ctx, params) data.Subreddits, err = routes.API.ListSubredditsWithCover(ctx, params)
@ -28,13 +28,13 @@ func (routes *Routes) PageSubreddits(rw http.ResponseWriter, r *http.Request) {
code, message := errs.HTTPMessage(err) code, message := errs.HTTPMessage(err)
rw.WriteHeader(code) rw.WriteHeader(code)
data.Error = message data.Error = message
if err := subredditsview.Subreddit(c, data).Render(ctx, rw); err != nil { if err := subreddits.View(c, data).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render subreddits") log.New(ctx).Err(err).Error("failed to render subreddits")
} }
return return
} }
if err := subredditsview.Subreddit(c, data).Render(r.Context(), rw); err != nil { if err := subreddits.View(c, data).Render(r.Context(), rw); err != nil {
log.New(ctx).Err(err).Error("failed to render subreddits view") log.New(ctx).Err(err).Error("failed to render subreddits view")
rw.WriteHeader(http.StatusInternalServerError) rw.WriteHeader(http.StatusInternalServerError)
} }

View file

@ -5,7 +5,7 @@ import (
"github.com/tigorlazuardi/redmage/pkg/log" "github.com/tigorlazuardi/redmage/pkg/log"
"github.com/tigorlazuardi/redmage/views" "github.com/tigorlazuardi/redmage/views"
"github.com/tigorlazuardi/redmage/views/subredditsview/addview" "github.com/tigorlazuardi/redmage/views/subreddits/put"
) )
func (routes *Routes) PageSubredditsAdd(rw http.ResponseWriter, r *http.Request) { func (routes *Routes) PageSubredditsAdd(rw http.ResponseWriter, r *http.Request) {
@ -14,7 +14,13 @@ func (routes *Routes) PageSubredditsAdd(rw http.ResponseWriter, r *http.Request)
c := views.NewContext(routes.Config, r) c := views.NewContext(routes.Config, r)
if err := addview.Addview(c).Render(ctx, rw); err != nil { data := put.Data{Title: "Add Subreddit"}
if err := put.View(c, data).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render subreddits add page") log.New(ctx).Err(err).Error("failed to render subreddits add page")
} }
// if err := addview.Addview(c).Render(ctx, rw); err != nil {
// log.New(ctx).Err(err).Error("failed to render subreddits add page")
// }
} }

View file

@ -60,7 +60,7 @@ func (routes *Routes) registerHTMXRoutes(router chi.Router) {
router.Post("/subreddits/add", routes.SubredditsCreateHTMX) router.Post("/subreddits/add", routes.SubredditsCreateHTMX)
router.Post("/subreddits/start", routes.SubredditStartDownloadHTMX) router.Post("/subreddits/start", routes.SubredditStartDownloadHTMX)
router.Post("/subreddits/check", routes.SubredditCheckHTMX) router.Get("/subreddits/check", routes.SubredditCheckHTMX)
router.Get("/subreddits/validate/schedule", routes.SubredditValidateScheduleHTMX) router.Get("/subreddits/validate/schedule", routes.SubredditValidateScheduleHTMX)
router.Get("/devices/add/validate/slug", routes.DevicesValidateSlugHTMX) router.Get("/devices/add/validate/slug", routes.DevicesValidateSlugHTMX)

View file

@ -10,7 +10,7 @@ import (
"github.com/tigorlazuardi/redmage/api/reddit" "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" "github.com/tigorlazuardi/redmage/views/subreddits/put"
) )
func (routes *Routes) SubredditsCheckAPI(rw http.ResponseWriter, r *http.Request) { func (routes *Routes) SubredditsCheckAPI(rw http.ResponseWriter, r *http.Request) {
@ -48,17 +48,15 @@ func (routes *Routes) SubredditsCheckAPI(rw http.ResponseWriter, r *http.Request
} }
func (routes *Routes) SubredditCheckHTMX(rw http.ResponseWriter, r *http.Request) { func (routes *Routes) SubredditCheckHTMX(rw http.ResponseWriter, r *http.Request) {
var data addview.SubredditInputData var (
name := r.FormValue("name") data put.NameInputData
data.Value = name subtype reddit.SubredditType
)
var subtype reddit.SubredditType data.Value = r.FormValue("name")
_ = subtype.Parse(r.FormValue("type")) _ = subtype.Parse(r.FormValue("type"))
data.Type = subtype if data.Value == "" {
if err := put.NameInput(data).Render(r.Context(), rw); err != nil {
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") log.New(r.Context()).Err(err).Error("failed to render subreddit input form")
} }
return return
@ -68,7 +66,7 @@ func (routes *Routes) SubredditCheckHTMX(rw http.ResponseWriter, r *http.Request
defer span.End() defer span.End()
params := api.SubredditCheckParam{ params := api.SubredditCheckParam{
Subreddit: name, Subreddit: data.Value,
SubredditType: subtype, SubredditType: subtype,
} }
@ -78,7 +76,7 @@ func (routes *Routes) SubredditCheckHTMX(rw http.ResponseWriter, r *http.Request
code, message := errs.HTTPMessage(err) code, message := errs.HTTPMessage(err)
rw.WriteHeader(code) rw.WriteHeader(code)
data.Error = message data.Error = message
if err := addview.SubredditInputForm(data).Render(r.Context(), rw); err != nil { if err := put.NameInput(data).Render(r.Context(), rw); err != nil {
log.New(r.Context()).Err(err).Error("failed to render subreddit input form") log.New(r.Context()).Err(err).Error("failed to render subreddit input form")
} }
return return
@ -92,7 +90,7 @@ func (routes *Routes) SubredditCheckHTMX(rw http.ResponseWriter, r *http.Request
code, message := errs.HTTPMessage(err) code, message := errs.HTTPMessage(err)
rw.WriteHeader(code) rw.WriteHeader(code)
data.Error = message data.Error = message
if err := addview.SubredditInputForm(data).Render(r.Context(), rw); err != nil { if err := put.NameInput(data).Render(r.Context(), rw); err != nil {
log.New(r.Context()).Err(err).Error("failed to render subreddit input form") log.New(r.Context()).Err(err).Error("failed to render subreddit input form")
} }
return return
@ -101,15 +99,15 @@ func (routes *Routes) SubredditCheckHTMX(rw http.ResponseWriter, r *http.Request
if exist { if exist {
rw.WriteHeader(http.StatusConflict) rw.WriteHeader(http.StatusConflict)
data.Error = "subreddit already registered" data.Error = "subreddit already registered"
if err := addview.SubredditInputForm(data).Render(r.Context(), rw); err != nil { if err := put.NameInput(data).Render(r.Context(), rw); err != nil {
log.New(r.Context()).Err(err).Error("failed to render subreddit input form") log.New(r.Context()).Err(err).Error("failed to render subreddit input form")
} }
return return
} }
data.Valid = true data.Valid = fmt.Sprintf("%s is valid", subtype)
if err := addview.SubredditInputForm(data).Render(r.Context(), rw); err != nil { if err := put.NameInput(data).Render(r.Context(), rw); err != nil {
log.New(r.Context()).Err(err).Error("failed to render subreddit input form") log.New(r.Context()).Err(err).Error("failed to render subreddit input form")
} }
} }

View file

@ -7,7 +7,6 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"github.com/a-h/templ"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
"github.com/tigorlazuardi/redmage/api" "github.com/tigorlazuardi/redmage/api"
"github.com/tigorlazuardi/redmage/api/reddit" "github.com/tigorlazuardi/redmage/api/reddit"
@ -72,13 +71,12 @@ func (routes *Routes) SubredditsCreateHTMX(rw http.ResponseWriter, r *http.Reque
ctx, span := tracer.Start(r.Context(), "*Routes.SubredditsCreateHTMX") ctx, span := tracer.Start(r.Context(), "*Routes.SubredditsCreateHTMX")
defer span.End() defer span.End()
sub, errComponents := subredditsDataFromRequest(r) sub, err := subredditsDataFromRequest(r)
if len(errComponents) > 0 { if err != nil {
rw.WriteHeader(http.StatusBadRequest) code, message := errs.HTTPMessage(err)
for _, err := range errComponents { rw.WriteHeader(code)
if e := err.Render(ctx, rw); e != nil { if err := components.ErrorNotication(message).Render(ctx, rw); err != nil {
log.New(ctx).Err(e).Error("failed to render error") log.New(ctx).Err(err).Error("failed to render error notification")
}
} }
return return
} }
@ -113,36 +111,31 @@ func (routes *Routes) SubredditsCreateHTMX(rw http.ResponseWriter, r *http.Reque
} }
return return
} }
if fetch, _ := strconv.ParseBool(r.FormValue("fetch")); fetch {
_ = routes.API.PubsubStartDownloadSubreddit(ctx, api.PubsubStartDownloadSubredditParams{
Subreddit: sub.Name,
})
}
rw.Header().Set("HX-Redirect", "/subreddits") rw.Header().Set("HX-Redirect", "/subreddits")
rw.WriteHeader(http.StatusCreated) rw.WriteHeader(http.StatusCreated)
_, _ = rw.Write([]byte("Subreddit created")) _, _ = rw.Write([]byte("Subreddit created"))
} }
func subredditsDataFromRequest(r *http.Request) (sub *models.Subreddit, errs []templ.Component) { func subredditsDataFromRequest(r *http.Request) (sub *models.Subreddit, err error) {
sub = &models.Subreddit{} sub = &models.Subreddit{}
var t reddit.SubredditType var t reddit.SubredditType
err := t.Parse(r.FormValue("type")) err = t.Parse(r.FormValue("type"))
if err != nil { if err != nil {
errs = append(errs, addview.SubredditTypeInput(addview.SubredditTypeData{ return nil, errs.
Value: strconv.Itoa(int(t)), Wrapw(err, "invalid subreddit type", "type", r.FormValue("type")).
Error: err.Error(), Code(http.StatusBadRequest)
HXSwapOOB: "true",
}))
return nil, errs
} }
sub.Subtype = int32(t) sub.Subtype = int32(t)
sub.Name = r.FormValue("name") sub.Name = r.FormValue("name")
if sub.Name == "" { if sub.Name == "" {
errs = append(errs, addview.SubredditInputForm(addview.SubredditInputData{ return nil, errs.Fail("name is required").Code(http.StatusBadRequest)
Value: sub.Name,
Error: "name is required",
Type: t,
HXSwapOOB: "true",
}))
return nil, errs
} }
enableSchedule, _ := strconv.Atoi(r.FormValue("enable_schedule")) enableSchedule, _ := strconv.Atoi(r.FormValue("enable_schedule"))
@ -157,27 +150,21 @@ func subredditsDataFromRequest(r *http.Request) (sub *models.Subreddit, errs []t
} }
if sub.EnableSchedule == 1 { if sub.EnableSchedule == 1 {
sub.Schedule = r.FormValue("schedule") schedule := r.FormValue("schedule")
_, err = cronParser.Parse(sub.Schedule) _, err = cron.ParseStandard(schedule)
if err != nil { if err != nil {
errs = append(errs, addview.ScheduleInput(addview.ScheduleInputData{ return nil, errs.Wrapf(err, "invalid cron schedule: %s", err).Code(http.StatusBadRequest)
Value: sub.Schedule,
Error: fmt.Sprintf("invalid cron schedule: %s", err),
HXSwapOOB: "true",
}))
} }
sub.Schedule = schedule
} }
countback, _ := strconv.Atoi(r.FormValue("countback")) countback, _ := strconv.Atoi(r.FormValue("countback"))
sub.Countback = int32(countback) sub.Countback = int32(countback)
if sub.Countback < 1 { if sub.Countback < 1 {
errs = append(errs, addview.CountbackInput(addview.CountbackInputData{ return nil, errs.Fail("countback must be 1 or higher").Code(http.StatusBadRequest)
Value: int64(sub.Countback),
Error: "countback must be 1 or higher",
}))
} }
return sub, errs return sub, nil
} }
var cronParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) var cronParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)

View file

@ -7,28 +7,28 @@ import (
"time" "time"
"github.com/tigorlazuardi/redmage/pkg/log" "github.com/tigorlazuardi/redmage/pkg/log"
"github.com/tigorlazuardi/redmage/views/subredditsview/addview" "github.com/tigorlazuardi/redmage/views/subreddits/put"
) )
func (routes *Routes) SubredditValidateScheduleHTMX(rw http.ResponseWriter, r *http.Request) { func (routes *Routes) SubredditValidateScheduleHTMX(rw http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "*Routes.SubredditValidateScheduleHTMX") ctx, span := tracer.Start(r.Context(), "*Routes.SubredditValidateScheduleHTMX")
defer span.End() defer span.End()
var data addview.ScheduleInputData var data put.ScheduleInputData
enabled, _ := strconv.Atoi(r.FormValue("enable_schedule")) enabled, _ := strconv.Atoi(r.FormValue("enable_schedule"))
data.Disabled = enabled == 0 data.Disabled = enabled == 0
data.Value = r.FormValue("schedule") data.Value = r.FormValue("schedule")
if data.Value == "" { if data.Value == "" {
if err := addview.ScheduleInput(data).Render(ctx, rw); err != nil { if err := put.ScheduleInput(data).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render schedule input") log.New(ctx).Err(err).Error("failed to render schedule input")
} }
return return
} }
if data.Disabled { if data.Disabled {
if err := addview.ScheduleInput(data).Render(ctx, rw); err != nil { if err := put.ScheduleInput(data).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render schedule input") log.New(ctx).Err(err).Error("failed to render schedule input")
} }
return return
@ -37,7 +37,7 @@ func (routes *Routes) SubredditValidateScheduleHTMX(rw http.ResponseWriter, r *h
scheduler, err := cronParser.Parse(data.Value) scheduler, err := cronParser.Parse(data.Value)
if err != nil { if err != nil {
data.Error = fmt.Sprintf("Invalid schedule format: %s", err.Error()) data.Error = fmt.Sprintf("Invalid schedule format: %s", err.Error())
if err := addview.ScheduleInput(data).Render(ctx, rw); err != nil { if err := put.ScheduleInput(data).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render schedule input") log.New(ctx).Err(err).Error("failed to render schedule input")
} }
return return
@ -45,9 +45,9 @@ func (routes *Routes) SubredditValidateScheduleHTMX(rw http.ResponseWriter, r *h
next := scheduler.Next(time.Now()) next := scheduler.Next(time.Now())
data.Valid = fmt.Sprintf("Syntax is valid. Next run at: %s", next.Format("Monday, _2 January 2006 15:04 MST")) data.Valid = fmt.Sprintf("Syntax is valid. Next run at: %s", next.Format("Monday, _2 January 2006 15:04:05 MST"))
if err := addview.ScheduleInput(data).Render(ctx, rw); err != nil { if err := put.ScheduleInput(data).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render schedule input") log.New(ctx).Err(err).Error("failed to render schedule input")
} }
} }

View file

@ -0,0 +1,44 @@
package put
import "strconv"
type CountbackInputData struct {
Value int64
}
func (c *CountbackInputData) GetValue() string {
if c.Value < 1 {
return "100"
}
return strconv.FormatInt(c.Value, 10)
}
templ CountbackInput(data CountbackInputData) {
<label
id="countback-input"
class="form-control w-full"
>
<div class="label">
<span class="label-text text-base">Countback</span>
<span class="label-text-alt">
NOTE: Non image posts are also counted in the countback!
</span>
</div>
<input
name="countback"
type="number"
class="input input-bordered"
value={ data.GetValue() }
placeholder="100"
min="1"
required
/>
<div class="label">
<span class="label-text">
Number of posts to lookup for whenever the scheduler runs this task or triggered manually by you.
The bigger the number, the longer it takes to finish the task.
You should adjust this number based on how active the subreddit is, how often the scheduler runs this task (if enabled), and your internet speed.
</span>
</div>
</label>
}

View file

@ -0,0 +1,32 @@
package put
templ FetchCheckbox() {
<div
x-data="{checked: false}"
class="form-control"
>
<label
class="label cursor-pointer input input-bordered"
:class="{'input-secondary': checked}"
>
<span
class="label-text"
:class="{'text-secondary': checked}"
>Fetch</span>
<input
type="checkbox"
class="checkbox"
:class="{'checkbox-secondary': checked}"
name="fetch"
@change="checked = !checked"
value="true"
/>
</label>
<span
class="label-text pl-1 mt-2"
:class="{'text-secondary': checked}"
>
Fetch images immediately after creation
</span>
</div>
}

View file

@ -0,0 +1,68 @@
package put
import "github.com/tigorlazuardi/redmage/views/utils"
import "fmt"
type NameInputData struct {
Value string
Error string
Valid string
HXSwapOOB string
}
templ NameInput(data NameInputData) {
<label
id="name-input-label"
class="form-control w-full"
if data.HXSwapOOB != "" {
hx-swap-oob={ data.HXSwapOOB }
}
>
<div class="label">
<span
class={ utils.CXX(
"label-text text-base", true,
"text-error", data.Error != "",
"text-success", data.Valid != "",
) }
>
Subreddit Name
</span>
</div>
<input
type="text"
id="name"
name="name"
hx-get="/htmx/subreddits/check"
hx-target-error="#name-input-label"
hx-target="#name-input-label"
hx-trigger="change, on-demand"
hx-include="[name='type']"
hx-swap="outerHTML"
value={ data.Value }
placeholder="e.g. 'wallpaper' or 'EarthPorn'"
class={ utils.CXX(
"input input-bordered", true,
"input-error text-error", data.Error != "",
"input-success text-success", data.Valid != "",
) }
required
x-data={ fmt.Sprintf(`{ init() {$el.setCustomValidity(%q)} }`, data.Error) }
/>
<div class="label">
<span
class={ utils.CXX(
"label-text min-h-[1rem]", true,
"text-error", data.Error != "",
"text-success", data.Valid != "",
) }
>
if data.Error != "" {
{ data.Error }
} else {
{ data.Valid }
}
</span>
</div>
</label>
}

View file

@ -0,0 +1,57 @@
package put
import "github.com/tigorlazuardi/redmage/views"
import "github.com/tigorlazuardi/redmage/views/components"
type Data struct {
Title string
NameInput NameInputData
TypeInput TypeInputData
ScheduleInput ScheduleInputData
CountbackInput CountbackInputData
}
templ View(c *views.Context, data Data) {
@components.Doctype() {
@components.Head(c, components.HeadTitle(data.Title))
@components.Body(c) {
@Content(c, data)
@components.NotificationContainer()
}
}
}
templ Content(c *views.Context, data Data) {
<main class="prose min-w-full">
@components.Container() {
<h1>Add Subreddit</h1>
<div class="divider"></div>
<form
action="/htmx/subreddits/add"
method="POST"
onkeydown="return event.key !== 'Enter'"
hx-post="/htmx/subreddits/add"
hx-target-error={ components.NotificationContainerID }
>
<div
class="grid grid-cols-1 sm:grid-cols-2 gap-4"
>
@NameInput(data.NameInput)
@TypeInput(data.TypeInput)
<div class="sm:col-span-2">
@ScheduleInput(data.ScheduleInput)
</div>
<div class="sm:col-span-2">
@CountbackInput(data.CountbackInput)
</div>
<div class="sm:col-span-2">
<div class="max-w-xs mx-auto">
@FetchCheckbox()
</div>
</div>
</div>
<button type="submit" class="block btn btn-primary mx-auto w-full max-w-xs mt-8 text-primary-content">Add</button>
</form>
}
</main>
}

View file

@ -0,0 +1,96 @@
package put
import "fmt"
import "strconv"
import "github.com/tigorlazuardi/redmage/views/utils"
type ScheduleInputData struct {
Value string
Error string
Valid string
Disabled bool
}
templ ScheduleInput(data ScheduleInputData) {
<div
id="schedule-input-group"
class="form-control w-full"
hx-get="/htmx/subreddits/validate/schedule"
hx-trigger="change"
hx-include="this"
hx-swap="outerHTML"
hx-target="this"
hx-select="#schedule-input-group"
>
<label for="schedule" class="label">
<span
class={ utils.CXX(
"label-text text-base", true,
"text-error", data.Error != "",
"text-success", data.Valid != "",
) }
>Schedule</span>
<div class="tooltip tooltip-left" data-tip="Whether to enable the scheduler or not">
<input type="checkbox" name="enable_schedule" value="1" class="toggle toggle-primary my-auto" checked?={ !data.Disabled }/>
</div>
</label>
<input
id="schedule"
name="schedule"
type="text"
placeholder="e.g. '@daily' or '0 0 * * MON'"
value={ data.Value }
class={ utils.CXX(
"input input-bordered", true,
"input-error text-error", data.Error != "" && !data.Disabled,
"input-success text-success", data.Valid != "" && !data.Disabled,
) }
x-data={ fmt.Sprintf(`{ init() { $el.setCustomValidity(%q) } }`, data.Error) }
list="cron-templates"
if data.Disabled {
disabled
} else {
required
}
/>
<div class="label">
<span
class={ utils.CXX(
"label-text min-h-[1rem]", true,
"text-error", data.Error != "" && !data.Disabled,
"text-success", data.Valid != "" && !data.Disabled,
) }
>
if data.Valid != "" {
{ data.Valid }
} else if data.Error != "" {
{ data.Error }. TIP: Try using the dropdown for examples and common expressions.
} else if !data.Disabled {
Uses cron syntax. Tip: Start by typing 'every' to get suggestions or search custom expressions via Google like 'cron every 6 hours'.
}
</span>
</div>
</div>
@scheduleDatalist()
}
templ scheduleDatalist() {
<datalist id="cron-templates">
<option value="@hourly">Every hour</option>
<option value="@daily">Every day at midnight</option>
<option value="@weekly">Every Sunday at midnight</option>
<option value="@monthly">Every start of month</option>
<option value="@yearly">Every start of year</option>
<option value="@annually">Every start of year</option>
<option value="0 0 * * MON">Every Monday at midnight</option>
<option value="0 0 * * TUE">Every Tuesday at midnight</option>
<option value="0 0 * * WED">Every Wednesday at midnight</option>
<option value="0 0 * * THU">Every Thursday at midnight</option>
<option value="0 0 * * FRI">Every Friday at midnight</option>
<option value="0 0 * * SAT">Every Saturday at midnight</option>
<option value="0 0 * * SUN">Every Sunday at midnight</option>
for i := 1; i < 24; i++ {
<option value={ fmt.Sprintf("0 %d * * *", i) }>Every day at { strconv.Itoa(i) } o'clock</option>
}
</datalist>
}

View file

@ -0,0 +1,26 @@
package put
import "github.com/tigorlazuardi/redmage/api/reddit"
type TypeInputData struct {
Value reddit.SubredditType
}
templ TypeInput(data TypeInputData) {
<label
id="type-input"
class="form-control w-full"
>
<div class="label">
<span class="label-text text-base">Subreddit Type</span>
</div>
<select
onchange="htmx.trigger('#name', 'on-demand')"
name="type"
class="select select-bordered"
>
<option value="0" selected?={ data.Value == 0 }>Subreddit</option>
<option value="1" selected?={ data.Value == 1 }>User</option>
</select>
</label>
}

View file

@ -3,16 +3,76 @@ package subreddits
import "github.com/tigorlazuardi/redmage/views" import "github.com/tigorlazuardi/redmage/views"
import "github.com/tigorlazuardi/redmage/views/components" import "github.com/tigorlazuardi/redmage/views/components"
import "github.com/tigorlazuardi/redmage/api" import "github.com/tigorlazuardi/redmage/api"
import "github.com/tigorlazuardi/redmage/models"
import "fmt"
import "strconv"
type Data struct { type Data struct {
Subreddits api.ListSubredditsResult Subreddits api.ListSubredditsResult
Error string Error string
} }
templ View(c *views.Context) { templ View(c *views.Context, data Data) {
@components.Doctype() { @components.Doctype() {
@components.Head(c, components.HeadTitle("Redmage - Subreddits")) @components.Head(c, components.HeadTitle("Redmage - Subreddits"))
@components.Body(c) { @components.Body(c) {
@Content(c, data)
} }
} }
} }
templ Content(c *views.Context, data Data) {
<main class="prose min-w-full">
@components.Container() {
<h1>Subreddits</h1>
if data.Subreddits.Total == 0 {
<div class="divider"></div>
<h3>No Subreddits Found</h3>
<p>Click <a class="text-primary" href="/subreddits/add">here</a> to add a new subreddit.</p>
} else {
<div class="flex justify-center sm:justify-between flex-wrap gap-4">
<h2 class="my-auto">{ strconv.FormatInt(data.Subreddits.Total, 10) } Subreddits Registered</h2>
<a class="btn btn-primary text-base-100 no-underline" href="/subreddits/add">Add Subreddit</a>
</div>
<div class="divider"></div>
}
<div class="flex flex-wrap gap-1 justify-around" hx-boost="true">
for _, subreddit := range data.Subreddits.Data {
@SubredditCard(c, subreddit)
}
</div>
}
</main>
}
templ SubredditCard(c *views.Context, data *models.Subreddit) {
<a
href={ templ.URL(fmt.Sprintf("/subreddits/details/%s", data.Name)) }
class="not-prose card card-bordered bg-base-100 hover:bg-base-200 shadow-xl xs:w-80 w-full max-w-full top-0 hover:-top-1 transition-all rounded-none"
>
if len(data.R.Images) > 0 {
<figure class="p-8">
<img
class="object-contain xs:max-w-[16rem] max-h-[16rem]"
src={ fmt.Sprintf("/img/%s", data.R.Images[0].ThumbnailRelativePath) }
alt={ data.Name }
/>
</figure>
} else {
<figure class="p-8 mx-auto">
@imagePlaceholder()
</figure>
}
<div class="card-body">
<div class="flex-1"></div>
<p class="text-center my-4 underline text-primary">{ data.Name }</p>
</div>
</a>
}
templ imagePlaceholder() {
<svg xmlns="http://www.w3.org/2000/svg" class="w-full" viewBox="0 0 120 120" fill="none">
<rect width="120" height="120" fill="#EFF1F3"></rect>
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.2503 38.4816C33.2603 37.0472 34.4199 35.8864 35.8543 35.875H83.1463C84.5848 35.875 85.7503 37.0431 85.7503 38.4816V80.5184C85.7403 81.9528 84.5807 83.1136 83.1463 83.125H35.8543C34.4158 83.1236 33.2503 81.957 33.2503 80.5184V38.4816ZM80.5006 41.1251H38.5006V77.8751L62.8921 53.4783C63.9172 52.4536 65.5788 52.4536 66.6039 53.4783L80.5006 67.4013V41.1251ZM43.75 51.6249C43.75 54.5244 46.1005 56.8749 49 56.8749C51.8995 56.8749 54.25 54.5244 54.25 51.6249C54.25 48.7254 51.8995 46.3749 49 46.3749C46.1005 46.3749 43.75 48.7254 43.75 51.6249Z" fill="#687787"></path>
</svg>
}

View file

@ -1,72 +0,0 @@
package subredditsview
import "github.com/tigorlazuardi/redmage/views"
import "github.com/tigorlazuardi/redmage/views/components"
import "github.com/tigorlazuardi/redmage/models"
import "strconv"
import "fmt"
templ Subreddit(c *views.Context, data Data) {
@components.Doctype() {
@components.Head(c, components.HeadTitle("Redmage - Subreddits"))
@components.Body(c) {
@SubredditContent(c, data)
}
}
}
templ SubredditContent(c *views.Context, data Data) {
<main class="prose min-w-full">
@components.Container() {
<h1>Subreddits</h1>
if data.Subreddits.Total == 0 {
<div class="divider"></div>
<h3>No Subreddits Found</h3>
<p>Click <a class="text-primary" href="/subreddits/add">here</a> to add a new subreddit.</p>
} else {
<div class="flex justify-center sm:justify-between flex-wrap gap-4">
<h2 class="my-auto">{ strconv.FormatInt(data.Subreddits.Total, 10) } Subreddits Registered</h2>
<a class="btn btn-primary text-base-100 no-underline" href="/subreddits/add">Add Subreddit</a>
</div>
<div class="divider"></div>
}
<div class="flex flex-wrap gap-1 justify-around" hx-boost="true">
for _, subreddit := range data.Subreddits.Data {
@SubredditCard(c, subreddit)
}
</div>
}
</main>
}
templ SubredditCard(c *views.Context, data *models.Subreddit) {
<a
href={ templ.URL(fmt.Sprintf("/subreddits/details/%s", data.Name)) }
class="not-prose card card-bordered bg-base-100 hover:bg-base-200 shadow-xl xs:w-80 w-full max-w-full top-0 hover:-top-1 transition-all rounded-none"
>
if len(data.R.Images) > 0 {
<figure class="p-8">
<img
class="object-contain xs:max-w-[16rem] max-h-[16rem]"
src={ fmt.Sprintf("/img/%s", data.R.Images[0].ThumbnailRelativePath) }
alt={ data.Name }
/>
</figure>
} else {
<figure class="p-8 mx-auto">
@imagePlaceholder()
</figure>
}
<div class="card-body">
<div class="flex-1"></div>
<p class="text-center my-4 underline text-primary">{ data.Name }</p>
</div>
</a>
}
templ imagePlaceholder() {
<svg xmlns="http://www.w3.org/2000/svg" class="w-full" viewBox="0 0 120 120" fill="none">
<rect width="120" height="120" fill="#EFF1F3"></rect>
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.2503 38.4816C33.2603 37.0472 34.4199 35.8864 35.8543 35.875H83.1463C84.5848 35.875 85.7503 37.0431 85.7503 38.4816V80.5184C85.7403 81.9528 84.5807 83.1136 83.1463 83.125H35.8543C34.4158 83.1236 33.2503 81.957 33.2503 80.5184V38.4816ZM80.5006 41.1251H38.5006V77.8751L62.8921 53.4783C63.9172 52.4536 65.5788 52.4536 66.6039 53.4783L80.5006 67.4013V41.1251ZM43.75 51.6249C43.75 54.5244 46.1005 56.8749 49 56.8749C51.8995 56.8749 54.25 54.5244 54.25 51.6249C54.25 48.7254 51.8995 46.3749 49 46.3749C46.1005 46.3749 43.75 48.7254 43.75 51.6249Z" fill="#687787"></path>
</svg>
}

View file

@ -1,10 +0,0 @@
package subredditsview
import (
"github.com/tigorlazuardi/redmage/api"
)
type Data struct {
Subreddits api.ListSubredditsResult
Error string
}