This commit is contained in:
parent
88f4edbaf5
commit
5957648ec3
|
@ -25,7 +25,7 @@ func (reddit *Reddit) CheckSubreddit(ctx context.Context, params CheckSubredditP
|
|||
|
||||
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)
|
||||
if err != nil {
|
||||
return actual, errs.Wrapw(err, "failed to create request", "url", url, "params", params)
|
||||
|
|
|
@ -57,7 +57,12 @@ func (s SubredditType) Code() string {
|
|||
}
|
||||
|
||||
func (s SubredditType) String() string {
|
||||
return s.Code()
|
||||
switch s {
|
||||
case SubredditTypeUser:
|
||||
return "User"
|
||||
default:
|
||||
return "Subreddit"
|
||||
}
|
||||
}
|
||||
|
||||
type GetPostsParam struct {
|
||||
|
|
26
db/migrations/20240527205312_trigger_refactors.sql
Normal file
26
db/migrations/20240527205312_trigger_refactors.sql
Normal 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
|
|
@ -7,7 +7,7 @@ import (
|
|||
"github.com/tigorlazuardi/redmage/pkg/errs"
|
||||
"github.com/tigorlazuardi/redmage/pkg/log"
|
||||
"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) {
|
||||
|
@ -19,7 +19,7 @@ func (routes *Routes) PageSubreddits(rw http.ResponseWriter, r *http.Request) {
|
|||
var params api.ListSubredditsParams
|
||||
params.FillFromQuery(r.URL.Query())
|
||||
|
||||
var data subredditsview.Data
|
||||
var data subreddits.Data
|
||||
var err error
|
||||
|
||||
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)
|
||||
rw.WriteHeader(code)
|
||||
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")
|
||||
}
|
||||
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")
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
|
||||
"github.com/tigorlazuardi/redmage/pkg/log"
|
||||
"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) {
|
||||
|
@ -14,7 +14,13 @@ func (routes *Routes) PageSubredditsAdd(rw http.ResponseWriter, r *http.Request)
|
|||
|
||||
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")
|
||||
}
|
||||
|
||||
// if err := addview.Addview(c).Render(ctx, rw); err != nil {
|
||||
// log.New(ctx).Err(err).Error("failed to render subreddits add page")
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ func (routes *Routes) registerHTMXRoutes(router chi.Router) {
|
|||
|
||||
router.Post("/subreddits/add", routes.SubredditsCreateHTMX)
|
||||
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("/devices/add/validate/slug", routes.DevicesValidateSlugHTMX)
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
"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"
|
||||
"github.com/tigorlazuardi/redmage/views/subreddits/put"
|
||||
)
|
||||
|
||||
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) {
|
||||
var data addview.SubredditInputData
|
||||
name := r.FormValue("name")
|
||||
data.Value = name
|
||||
|
||||
var subtype reddit.SubredditType
|
||||
var (
|
||||
data put.NameInputData
|
||||
subtype reddit.SubredditType
|
||||
)
|
||||
data.Value = r.FormValue("name")
|
||||
_ = subtype.Parse(r.FormValue("type"))
|
||||
|
||||
data.Type = subtype
|
||||
|
||||
if name == "" {
|
||||
if err := addview.SubredditInputForm(data).Render(r.Context(), rw); err != nil {
|
||||
if data.Value == "" {
|
||||
if err := put.NameInput(data).Render(r.Context(), rw); err != nil {
|
||||
log.New(r.Context()).Err(err).Error("failed to render subreddit input form")
|
||||
}
|
||||
return
|
||||
|
@ -68,7 +66,7 @@ func (routes *Routes) SubredditCheckHTMX(rw http.ResponseWriter, r *http.Request
|
|||
defer span.End()
|
||||
|
||||
params := api.SubredditCheckParam{
|
||||
Subreddit: name,
|
||||
Subreddit: data.Value,
|
||||
SubredditType: subtype,
|
||||
}
|
||||
|
||||
|
@ -78,7 +76,7 @@ func (routes *Routes) SubredditCheckHTMX(rw http.ResponseWriter, r *http.Request
|
|||
code, message := errs.HTTPMessage(err)
|
||||
rw.WriteHeader(code)
|
||||
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")
|
||||
}
|
||||
return
|
||||
|
@ -92,7 +90,7 @@ func (routes *Routes) SubredditCheckHTMX(rw http.ResponseWriter, r *http.Request
|
|||
code, message := errs.HTTPMessage(err)
|
||||
rw.WriteHeader(code)
|
||||
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")
|
||||
}
|
||||
return
|
||||
|
@ -101,15 +99,15 @@ func (routes *Routes) SubredditCheckHTMX(rw http.ResponseWriter, r *http.Request
|
|||
if exist {
|
||||
rw.WriteHeader(http.StatusConflict)
|
||||
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")
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/tigorlazuardi/redmage/api"
|
||||
"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")
|
||||
defer span.End()
|
||||
|
||||
sub, errComponents := subredditsDataFromRequest(r)
|
||||
if len(errComponents) > 0 {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
for _, err := range errComponents {
|
||||
if e := err.Render(ctx, rw); e != nil {
|
||||
log.New(ctx).Err(e).Error("failed to render error")
|
||||
}
|
||||
sub, err := subredditsDataFromRequest(r)
|
||||
if err != nil {
|
||||
code, message := errs.HTTPMessage(err)
|
||||
rw.WriteHeader(code)
|
||||
if err := components.ErrorNotication(message).Render(ctx, rw); err != nil {
|
||||
log.New(ctx).Err(err).Error("failed to render error notification")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -113,36 +111,31 @@ func (routes *Routes) SubredditsCreateHTMX(rw http.ResponseWriter, r *http.Reque
|
|||
}
|
||||
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.WriteHeader(http.StatusCreated)
|
||||
_, _ = 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{}
|
||||
|
||||
var t reddit.SubredditType
|
||||
err := t.Parse(r.FormValue("type"))
|
||||
err = t.Parse(r.FormValue("type"))
|
||||
if err != nil {
|
||||
errs = append(errs, addview.SubredditTypeInput(addview.SubredditTypeData{
|
||||
Value: strconv.Itoa(int(t)),
|
||||
Error: err.Error(),
|
||||
HXSwapOOB: "true",
|
||||
}))
|
||||
|
||||
return nil, errs
|
||||
return nil, errs.
|
||||
Wrapw(err, "invalid subreddit type", "type", r.FormValue("type")).
|
||||
Code(http.StatusBadRequest)
|
||||
}
|
||||
sub.Subtype = int32(t)
|
||||
|
||||
sub.Name = r.FormValue("name")
|
||||
if sub.Name == "" {
|
||||
errs = append(errs, addview.SubredditInputForm(addview.SubredditInputData{
|
||||
Value: sub.Name,
|
||||
Error: "name is required",
|
||||
Type: t,
|
||||
HXSwapOOB: "true",
|
||||
}))
|
||||
return nil, errs
|
||||
return nil, errs.Fail("name is required").Code(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
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 {
|
||||
sub.Schedule = r.FormValue("schedule")
|
||||
_, err = cronParser.Parse(sub.Schedule)
|
||||
schedule := r.FormValue("schedule")
|
||||
_, err = cron.ParseStandard(schedule)
|
||||
if err != nil {
|
||||
errs = append(errs, addview.ScheduleInput(addview.ScheduleInputData{
|
||||
Value: sub.Schedule,
|
||||
Error: fmt.Sprintf("invalid cron schedule: %s", err),
|
||||
HXSwapOOB: "true",
|
||||
}))
|
||||
return nil, errs.Wrapf(err, "invalid cron schedule: %s", err).Code(http.StatusBadRequest)
|
||||
}
|
||||
sub.Schedule = schedule
|
||||
}
|
||||
|
||||
countback, _ := strconv.Atoi(r.FormValue("countback"))
|
||||
sub.Countback = int32(countback)
|
||||
if sub.Countback < 1 {
|
||||
errs = append(errs, addview.CountbackInput(addview.CountbackInputData{
|
||||
Value: int64(sub.Countback),
|
||||
Error: "countback must be 1 or higher",
|
||||
}))
|
||||
return nil, errs.Fail("countback must be 1 or higher").Code(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return sub, errs
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
var cronParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
|
||||
|
|
|
@ -7,28 +7,28 @@ import (
|
|||
"time"
|
||||
|
||||
"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) {
|
||||
ctx, span := tracer.Start(r.Context(), "*Routes.SubredditValidateScheduleHTMX")
|
||||
defer span.End()
|
||||
|
||||
var data addview.ScheduleInputData
|
||||
var data put.ScheduleInputData
|
||||
|
||||
enabled, _ := strconv.Atoi(r.FormValue("enable_schedule"))
|
||||
data.Disabled = enabled == 0
|
||||
data.Value = r.FormValue("schedule")
|
||||
|
||||
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")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
return
|
||||
|
@ -37,7 +37,7 @@ func (routes *Routes) SubredditValidateScheduleHTMX(rw http.ResponseWriter, r *h
|
|||
scheduler, err := cronParser.Parse(data.Value)
|
||||
if err != nil {
|
||||
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")
|
||||
}
|
||||
return
|
||||
|
@ -45,9 +45,9 @@ func (routes *Routes) SubredditValidateScheduleHTMX(rw http.ResponseWriter, r *h
|
|||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
44
views/subreddits/put/countback.templ
Normal file
44
views/subreddits/put/countback.templ
Normal 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>
|
||||
}
|
32
views/subreddits/put/fetch.templ
Normal file
32
views/subreddits/put/fetch.templ
Normal 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>
|
||||
}
|
68
views/subreddits/put/name.templ
Normal file
68
views/subreddits/put/name.templ
Normal 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>
|
||||
}
|
57
views/subreddits/put/put.templ
Normal file
57
views/subreddits/put/put.templ
Normal 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>
|
||||
}
|
96
views/subreddits/put/schedule.templ
Normal file
96
views/subreddits/put/schedule.templ
Normal 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>
|
||||
}
|
26
views/subreddits/put/type.templ
Normal file
26
views/subreddits/put/type.templ
Normal 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>
|
||||
}
|
|
@ -3,16 +3,76 @@ package subreddits
|
|||
import "github.com/tigorlazuardi/redmage/views"
|
||||
import "github.com/tigorlazuardi/redmage/views/components"
|
||||
import "github.com/tigorlazuardi/redmage/api"
|
||||
import "github.com/tigorlazuardi/redmage/models"
|
||||
import "fmt"
|
||||
import "strconv"
|
||||
|
||||
type Data struct {
|
||||
Subreddits api.ListSubredditsResult
|
||||
Error string
|
||||
}
|
||||
|
||||
templ View(c *views.Context) {
|
||||
templ View(c *views.Context, data Data) {
|
||||
@components.Doctype() {
|
||||
@components.Head(c, components.HeadTitle("Redmage - Subreddits"))
|
||||
@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>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package subredditsview
|
||||
|
||||
import (
|
||||
"github.com/tigorlazuardi/redmage/api"
|
||||
)
|
||||
|
||||
type Data struct {
|
||||
Subreddits api.ListSubredditsResult
|
||||
Error string
|
||||
}
|
Loading…
Reference in a new issue