subreddit: implemented edit page and api
Some checks failed
/ push (push) Has been cancelled

This commit is contained in:
Tigor Hutasuhut 2024-05-28 00:30:29 +07:00
parent 5957648ec3
commit 3fdb78362b
22 changed files with 329 additions and 426 deletions

View file

@ -20,20 +20,22 @@ func (api *API) scheduleStatusUpsert(ctx context.Context, exec bob.Executor, par
ctx, span := tracer.Start(ctx, "*API.createNewScheduleStatus") ctx, span := tracer.Start(ctx, "*API.createNewScheduleStatus")
defer span.End() defer span.End()
now := time.Now() now := time.Now()
ss, err := models.ScheduleStatuses.Upsert(ctx, exec, true, []string{"subreddit"}, []string{ api.lockf(func() {
"subreddit", schedule, err = models.ScheduleStatuses.Upsert(ctx, exec, true, []string{"subreddit"}, []string{
"status", "subreddit",
"error_message", "status",
"updated_at", "error_message",
}, &models.ScheduleStatusSetter{ "updated_at",
Subreddit: omit.FromCond(params.Subreddit, params.Subreddit != ""), }, &models.ScheduleStatusSetter{
Status: omit.From(params.Status.Int8()), Subreddit: omit.FromCond(params.Subreddit, params.Subreddit != ""),
ErrorMessage: omit.From(params.ErrorMessage), Status: omit.From(params.Status.Int8()),
CreatedAt: omit.From(now.Unix()), ErrorMessage: omit.From(params.ErrorMessage),
UpdatedAt: omit.From(now.Unix()), CreatedAt: omit.From(now.Unix()),
UpdatedAt: omit.From(now.Unix()),
})
}) })
if err != nil { if err != nil {
return ss, errs.Wrapw(err, "failed to upsert schedule status", "params", params) return schedule, errs.Wrapw(err, "failed to upsert schedule status", "params", params)
} }
return ss, err return schedule, err
} }

56
api/subreddits_edit.go Normal file
View file

@ -0,0 +1,56 @@
package api
import (
"context"
"time"
"github.com/aarondl/opt/omit"
"github.com/tigorlazuardi/redmage/models"
"github.com/tigorlazuardi/redmage/pkg/errs"
)
type SubredditEditParams struct {
Name string
EnableSchedule *int32
Schedule *string
Countback *int32
}
func (api *API) SubredditsEdit(ctx context.Context, params SubredditEditParams) (subreddit *models.Subreddit, err error) {
ctx, span := tracer.Start(ctx, "*API.SubredditsEdit")
defer span.End()
now := time.Now()
subreddit = &models.Subreddit{
Name: params.Name,
}
set := &models.SubredditSetter{
EnableSchedule: omit.FromPtr(params.EnableSchedule),
Schedule: omit.FromPtr(params.Schedule),
Countback: omit.FromPtr(params.Countback),
UpdatedAt: omit.From(now.Unix()),
}
api.lockf(func() {
err = models.Subreddits.Update(ctx, api.db, set, subreddit)
})
if err != nil {
return subreddit, errs.Wrapw(err, "failed to update subreddit", "set", set)
}
if err := subreddit.Reload(ctx, api.db); err != nil {
if err.Error() == "sql: no rows in result set" {
return subreddit, errs.Wrapw(err, "subreddit not found", "subreddit", subreddit.Name).Code(404)
}
return subreddit, errs.Wrapw(err, "failed to reload subreddit")
}
if params.Schedule != nil {
_, _ = api.scheduler.Put(params.Name, *params.Schedule)
}
return subreddit, nil
}

View file

@ -14,13 +14,12 @@ func (routes *Routes) PageSubredditsAdd(rw http.ResponseWriter, r *http.Request)
c := views.NewContext(routes.Config, r) c := views.NewContext(routes.Config, r)
data := put.Data{Title: "Add Subreddit"} data := put.Data{
Title: "Add Subreddit",
PostAction: "/htmx/subreddits/add",
}
if err := put.View(c, data).Render(ctx, rw); err != nil { 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

@ -21,6 +21,7 @@ func (routes *Routes) PageSubredditsDetails(rw http.ResponseWriter, r *http.Requ
params.FillFromQuery(r.URL.Query()) params.FillFromQuery(r.URL.Query())
var data detailsview.Data var data detailsview.Data
data.FlashMessageSuccess = r.Header.Get("X-Flash-Message-Success")
var err error var err error
data.Params = params data.Params = params

View file

@ -0,0 +1,50 @@
package routes
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/tigorlazuardi/redmage/api/reddit"
"github.com/tigorlazuardi/redmage/pkg/errs"
"github.com/tigorlazuardi/redmage/pkg/log"
"github.com/tigorlazuardi/redmage/views"
"github.com/tigorlazuardi/redmage/views/components"
"github.com/tigorlazuardi/redmage/views/subreddits/put"
)
func (routes *Routes) PageSubredditsEdit(rw http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "*Routes.PageSubredditsEdit")
defer span.End()
c := views.NewContext(routes.Config, r)
name := chi.URLParam(r, "name")
sub, err := routes.API.SubredditsGetByName(ctx, name)
if err != nil {
code, message := errs.HTTPMessage(err)
if code >= 500 {
log.New(ctx).Err(err).Error("failed to get device by slug")
}
rw.WriteHeader(code)
msg := fmt.Sprintf("%d: %s", code, message)
if err := components.PageError(c, msg).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render subreddit edit page")
}
}
data := put.Data{
Title: fmt.Sprintf("Edit %s", sub.Name),
EditMode: true,
PostAction: fmt.Sprintf("/subreddits/edit/%s", sub.Name),
NameInput: put.NameInputData{Value: sub.Name},
TypeInput: put.TypeInputData{Value: reddit.SubredditType(sub.Subtype)},
ScheduleInput: put.ScheduleInputData{Value: sub.Schedule},
CountbackInput: put.CountbackInputData{Value: int64(sub.Countback)},
}
if err := put.View(c, data).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render subreddit edit page")
}
}

View file

@ -83,6 +83,8 @@ func (routes *Routes) registerWWWRoutes(router chi.Router) {
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("/subreddits/add", routes.PageSubredditsAdd)
r.Get("/subreddits/edit/{name}", routes.PageSubredditsEdit)
r.Post("/subreddits/edit/{name}", routes.SubredditsEditHTMX)
r.Get("/config", routes.PageConfig) r.Get("/config", routes.PageConfig)
r.Get("/devices", routes.PageDevices) r.Get("/devices", routes.PageDevices)
r.Get("/devices/add", routes.PageDevicesAdd) r.Get("/devices/add", routes.PageDevicesAdd)

View file

@ -14,7 +14,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/components" "github.com/tigorlazuardi/redmage/views/components"
"github.com/tigorlazuardi/redmage/views/subredditsview/addview" "github.com/tigorlazuardi/redmage/views/subreddits/put"
) )
func (routes *Routes) SubredditsCreateAPI(rw http.ResponseWriter, req *http.Request) { func (routes *Routes) SubredditsCreateAPI(rw http.ResponseWriter, req *http.Request) {
@ -87,11 +87,9 @@ func (routes *Routes) SubredditsCreateHTMX(rw http.ResponseWriter, r *http.Reque
if err != nil { if err != nil {
rw.WriteHeader(http.StatusBadRequest) rw.WriteHeader(http.StatusBadRequest)
log.New(ctx).Err(err).Error("subreddit check returns error") log.New(ctx).Err(err).Error("subreddit check returns error")
renderer := addview.SubredditInputForm(addview.SubredditInputData{ renderer := put.NameInput(put.NameInputData{
Value: sub.Name, Value: sub.Name,
Error: err.Error(), Error: err.Error(),
Type: reddit.SubredditType(sub.Subtype),
HXSwapOOB: "true",
}) })
if err := renderer.Render(ctx, rw); err != nil { if err := renderer.Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render error") log.New(ctx).Err(err).Error("failed to render error")

View file

@ -0,0 +1,63 @@
package routes
import (
"fmt"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/robfig/cron/v3"
"github.com/tigorlazuardi/redmage/api"
"github.com/tigorlazuardi/redmage/pkg/errs"
"github.com/tigorlazuardi/redmage/pkg/log"
"github.com/tigorlazuardi/redmage/views/components"
)
func (routes *Routes) SubredditsEditHTMX(rw http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "*Routes.SubredditsEditHTMX")
defer span.End()
name := chi.URLParam(r, "name")
countbackInt, _ := strconv.Atoi(r.FormValue("countback"))
countback := int32(countbackInt)
schedule := r.FormValue("schedule")
if countback < 1 {
rw.WriteHeader(http.StatusBadRequest)
const msg = "Countback must be greater than 0"
if err := components.ErrorNotication(msg).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render error notification")
}
return
}
if _, err := cron.ParseStandard(schedule); err != nil {
rw.WriteHeader(http.StatusBadRequest)
msg := fmt.Sprintf("Invalid schedule format: %s", err)
if err := components.ErrorNotication(msg).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render error notification")
}
return
}
_, err := routes.API.SubredditsEdit(ctx, api.SubredditEditParams{
Name: name,
Countback: &countback,
Schedule: &schedule,
})
if err != nil {
log.New(ctx).Err(err).Error("failed to update device")
code, message := errs.HTTPMessage(err)
rw.WriteHeader(code)
if err := components.ErrorToast(message).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render error notification")
}
return
}
rw.Header().Set("HX-Retarget", "#root-content")
rw.Header().Set("HX-Reselect", "#root-content")
rw.Header().Set("HX-Push-Url", "/subreddits/details/"+name)
r.Header.Set("X-Flash-Message-Success", fmt.Sprintf("Subreddit %s updated", name))
routes.PageSubredditsDetails(rw, r)
}

View file

@ -7,7 +7,7 @@ templ Body(c *views.Context) {
@Navigation(c) { @Navigation(c) {
<div class="flex"> <div class="flex">
@Navbar(c) @Navbar(c)
<div class="flex-grow"> <div id="root-content" class="flex-grow">
{ children... } { children... }
</div> </div>
</div> </div>

View file

@ -3,7 +3,9 @@ package components
const NotificationContainerID = "#notification-container" const NotificationContainerID = "#notification-container"
templ NotificationContainer() { templ NotificationContainer() {
<div id="notification-container" class="fixed bottom-4 right-4 z-50"></div> <div id="notification-container" class="fixed bottom-4 right-4 z-50">
{ children... }
</div>
} }
templ InfoNotication(messages ...string) { templ InfoNotication(messages ...string) {
@ -42,7 +44,7 @@ templ ErrorNotication(messages ...string) {
templ SuccessNotification(messages ...string) { templ SuccessNotification(messages ...string) {
<div <div
hx-on::load="setTimeout(() => this.remove(), 5000)" x-data="{ init() { setTimeout(() => $el.remove(), 5000) } }"
class="toast" class="toast"
onclick="this.remove()" onclick="this.remove()"
> >

View file

@ -85,7 +85,7 @@ func (pgdata PaginationData) getMobilePageStatus(page int) pageStatus {
} }
func (pgdata PaginationData) GetCurrentPage() int { func (pgdata PaginationData) GetCurrentPage() int {
return int(pgdata.Offset/pgdata.Limit) + 1 return int(pgdata.Offset/max(pgdata.Limit, 1)) + 1
} }
func (pgdata PaginationData) GetTotalPage() int { func (pgdata PaginationData) GetTotalPage() int {

View file

@ -2,6 +2,7 @@ package details
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/views/icons"
import "fmt" import "fmt"
import "github.com/tigorlazuardi/redmage/models" import "github.com/tigorlazuardi/redmage/models"
import "strconv" import "strconv"
@ -27,10 +28,14 @@ templ Content(c *views.Context, data Data) {
} else { } else {
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<h1 class="my-auto">{ data.Device.Name }</h1> <h1 class="my-auto">{ data.Device.Name }</h1>
<a <div class="tooltip" data-tip="Edit">
href={ templ.SafeURL(fmt.Sprintf("/devices/edit/%s", data.Device.Slug)) } <a
class="btn btn-primary no-underline sm:w-24" href={ templ.SafeURL(fmt.Sprintf("/devices/edit/%s", data.Device.Slug)) }
>Edit</a> class="btn btn-primary no-underline"
>
@icons.Gear("w-8 h-8 text-primary-content")
</a>
</div>
</div> </div>
<div class="divider"></div> <div class="divider"></div>
@filter(c, data) @filter(c, data)

22
views/icons/gear.templ Normal file
View file

@ -0,0 +1,22 @@
package icons
import "strings"
templ Gear(class ...string) {
<svg class={ strings.Join(class, " ") } xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path
d="M15 12C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M12.9046 3.06005C12.6988 3 12.4659 3 12 3C11.5341 3 11.3012 3 11.0954 3.06005C10.7942 3.14794 10.5281 3.32808 10.3346 3.57511C10.2024 3.74388 10.1159 3.96016 9.94291 4.39272C9.69419 5.01452 9.00393 5.33471 8.36857 5.123L7.79779 4.93281C7.3929 4.79785 7.19045 4.73036 6.99196 4.7188C6.70039 4.70181 6.4102 4.77032 6.15701 4.9159C5.98465 5.01501 5.83376 5.16591 5.53197 5.4677C5.21122 5.78845 5.05084 5.94882 4.94896 6.13189C4.79927 6.40084 4.73595 6.70934 4.76759 7.01551C4.78912 7.2239 4.87335 7.43449 5.04182 7.85566C5.30565 8.51523 5.05184 9.26878 4.44272 9.63433L4.16521 9.80087C3.74031 10.0558 3.52786 10.1833 3.37354 10.3588C3.23698 10.5141 3.13401 10.696 3.07109 10.893C3 11.1156 3 11.3658 3 11.8663C3 12.4589 3 12.7551 3.09462 13.0088C3.17823 13.2329 3.31422 13.4337 3.49124 13.5946C3.69158 13.7766 3.96395 13.8856 4.50866 14.1035C5.06534 14.3261 5.35196 14.9441 5.16236 15.5129L4.94721 16.1584C4.79819 16.6054 4.72367 16.829 4.7169 17.0486C4.70875 17.3127 4.77049 17.5742 4.89587 17.8067C5.00015 18.0002 5.16678 18.1668 5.5 18.5C5.83323 18.8332 5.99985 18.9998 6.19325 19.1041C6.4258 19.2295 6.68733 19.2913 6.9514 19.2831C7.17102 19.2763 7.39456 19.2018 7.84164 19.0528L8.36862 18.8771C9.00393 18.6654 9.6942 18.9855 9.94291 19.6073C10.1159 20.0398 10.2024 20.2561 10.3346 20.4249C10.5281 20.6719 10.7942 20.8521 11.0954 20.94C11.3012 21 11.5341 21 12 21C12.4659 21 12.6988 21 12.9046 20.94C13.2058 20.8521 13.4719 20.6719 13.6654 20.4249C13.7976 20.2561 13.8841 20.0398 14.0571 19.6073C14.3058 18.9855 14.9961 18.6654 15.6313 18.8773L16.1579 19.0529C16.605 19.2019 16.8286 19.2764 17.0482 19.2832C17.3123 19.2913 17.5738 19.2296 17.8063 19.1042C17.9997 18.9999 18.1664 18.8333 18.4996 18.5001C18.8328 18.1669 18.9994 18.0002 19.1037 17.8068C19.2291 17.5743 19.2908 17.3127 19.2827 17.0487C19.2759 16.8291 19.2014 16.6055 19.0524 16.1584L18.8374 15.5134C18.6477 14.9444 18.9344 14.3262 19.4913 14.1035C20.036 13.8856 20.3084 13.7766 20.5088 13.5946C20.6858 13.4337 20.8218 13.2329 20.9054 13.0088C21 12.7551 21 12.4589 21 11.8663C21 11.3658 21 11.1156 20.9289 10.893C20.866 10.696 20.763 10.5141 20.6265 10.3588C20.4721 10.1833 20.2597 10.0558 19.8348 9.80087L19.5569 9.63416C18.9478 9.26867 18.6939 8.51514 18.9578 7.85558C19.1262 7.43443 19.2105 7.22383 19.232 7.01543C19.2636 6.70926 19.2003 6.40077 19.0506 6.13181C18.9487 5.94875 18.7884 5.78837 18.4676 5.46762C18.1658 5.16584 18.0149 5.01494 17.8426 4.91583C17.5894 4.77024 17.2992 4.70174 17.0076 4.71872C16.8091 4.73029 16.6067 4.79777 16.2018 4.93273L15.6314 5.12287C14.9961 5.33464 14.3058 5.0145 14.0571 4.39272C13.8841 3.96016 13.7976 3.74388 13.6654 3.57511C13.4719 3.32808 13.2058 3.14794 12.9046 3.06005Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
></path>
</svg>
}

31
views/icons/kebab.templ Normal file
View file

@ -0,0 +1,31 @@
package icons
import "strings"
templ Kebab(class ...string) {
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
class={ strings.Join(class, " ") }
>
<circle
cx="12"
cy="6"
r="2"
transform="rotate(90 12 6)"
fill="currentColor"
></circle>
<circle
cx="12"
cy="12"
r="2"
transform="rotate(90 12 12)"
fill="currentColor"
></circle>
<path
d="M12 20C10.8954 20 10 19.1046 10 18C10 16.8954 10.8954 16 12 16C13.1046 16 14 16.8954 14 18C14 19.1046 13.1046 20 12 20Z"
fill="currentColor"
></path>
</svg>
}

View file

@ -4,19 +4,15 @@ import "github.com/tigorlazuardi/redmage/views/utils"
import "fmt" import "fmt"
type NameInputData struct { type NameInputData struct {
Value string Value string
Error string Error string
Valid string Valid string
HXSwapOOB string
} }
templ NameInput(data NameInputData) { templ NameInput(data NameInputData) {
<label <label
id="name-input-label" id="name-input-label"
class="form-control w-full" class="form-control w-full"
if data.HXSwapOOB != "" {
hx-swap-oob={ data.HXSwapOOB }
}
> >
<div class="label"> <div class="label">
<span <span

View file

@ -5,6 +5,8 @@ import "github.com/tigorlazuardi/redmage/views/components"
type Data struct { type Data struct {
Title string Title string
EditMode bool
PostAction string
NameInput NameInputData NameInput NameInputData
TypeInput TypeInputData TypeInput TypeInputData
ScheduleInput ScheduleInputData ScheduleInput ScheduleInputData
@ -24,33 +26,44 @@ templ View(c *views.Context, data Data) {
templ Content(c *views.Context, data Data) { templ Content(c *views.Context, data Data) {
<main class="prose min-w-full"> <main class="prose min-w-full">
@components.Container() { @components.Container() {
<h1>Add Subreddit</h1> <h1>{ data.Title }</h1>
<div class="divider"></div> <div class="divider"></div>
<form <form
action="/htmx/subreddits/add" action={ templ.SafeURL(data.PostAction) }
method="POST" method="POST"
onkeydown="return event.key !== 'Enter'" hx-post={ data.PostAction }
hx-post="/htmx/subreddits/add"
hx-target-error={ components.NotificationContainerID } hx-target-error={ components.NotificationContainerID }
> >
<div <div
class="grid grid-cols-1 sm:grid-cols-2 gap-4" class="grid grid-cols-1 sm:grid-cols-2 gap-4"
> >
@NameInput(data.NameInput) if !data.EditMode {
@TypeInput(data.TypeInput) @NameInput(data.NameInput)
}
if !data.EditMode {
@TypeInput(data.TypeInput)
}
<div class="sm:col-span-2"> <div class="sm:col-span-2">
@ScheduleInput(data.ScheduleInput) @ScheduleInput(data.ScheduleInput)
</div> </div>
<div class="sm:col-span-2"> <div class="sm:col-span-2">
@CountbackInput(data.CountbackInput) @CountbackInput(data.CountbackInput)
</div> </div>
<div class="sm:col-span-2"> if !data.EditMode {
<div class="max-w-xs mx-auto"> <div class="sm:col-span-2">
@FetchCheckbox() <div class="max-w-xs mx-auto">
@FetchCheckbox()
</div>
</div> </div>
</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> <button type="submit" class="block btn btn-primary mx-auto w-full max-w-xs mt-8 text-primary-content">
if data.EditMode {
Save
} else {
Add
}
</button>
</form> </form>
} }
</main> </main>

View file

@ -1,43 +0,0 @@
package addview
import "github.com/tigorlazuardi/redmage/views"
import "github.com/tigorlazuardi/redmage/views/components"
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
action="/htmx/subreddits/add"
method="POST"
onkeydown="return event.key !== 'Enter'"
hx-post="/htmx/subreddits/add"
>
<div
class="grid grid-cols-1 sm:grid-cols-2 gap-4"
>
@SubredditInputForm(SubredditInputData{})
@SubredditTypeInput(SubredditTypeData{})
<div class="sm:col-span-2">
@scheduleInputContainer()
</div>
<div class="sm:col-span-2">
@CountbackInput(CountbackInputData{})
</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

@ -1,73 +0,0 @@
package addview
import "strconv"
import "github.com/tigorlazuardi/redmage/views/utils"
type CountbackInputData struct {
Value int64
Error string
HXSwapOOB string
}
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 sm:col-span-2"
hx-swap-oob={ data.HXSwapOOB }
>
<div class="label">
<span
class={ utils.CX(map[string]bool{
"label-text": true,
"text-error": data.Error != "",
"text-base": true,
}) }
>Countback</span>
<span
class={ utils.CX(map[string]bool{
"label-text-alt": true,
"text-error": data.Error != "",
}) }
>NOTE: Non image posts are also counted in the countback!</span>
</div>
<input
name="countback"
type="number"
class={ utils.CX(map[string]bool{
"input": true,
"input-bordered": true,
"input-error": data.Error != "",
"text-error": data.Error != "",
}) }
value={ data.GetValue() }
min="1"
required
data-error={ data.Error }
hx-on::load="this.setCustomValidity(this.dataset.error)"
onchange="this.setCustomValidity('')"
/>
<div class="label">
<span
class={ utils.CX(map[string]bool{
"label-text": true,
"text-error": data.Error != "",
}) }
>
if data.Error != "" {
{ data.Error }
} else {
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

@ -1,74 +0,0 @@
package addview
import "github.com/tigorlazuardi/redmage/views/utils"
import "github.com/tigorlazuardi/redmage/api/reddit"
type SubredditInputData struct {
Value string
Error string
Valid bool
Type reddit.SubredditType
HXSwapOOB string
}
templ SubredditInputForm(data SubredditInputData) {
<label
id="subreddit-input"
class="form-control w-full"
hx-post="/htmx/subreddits/check"
hx-target-error="this"
hx-trigger="input delay:1s, on-demand"
hx-include="[name='type']"
hx-swap="outerHTML"
hx-swap-oob={ data.HXSwapOOB }
>
<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>
<input
type="text"
id="name"
name="name"
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'))"
/>
<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 {
if data.Type == reddit.SubredditTypeUser {
Username target is valid
} else {
Subreddit is valid
}
} else {
{ data.Error }
}
</span>
</div>
</label>
}

View file

@ -1,116 +0,0 @@
package addview
import "github.com/tigorlazuardi/redmage/views/utils"
import "fmt"
import "strconv"
type ScheduleInputData struct {
Value string
Error string
Valid string
Disabled bool
HXSwapOOB string
}
templ scheduleInputContainer() {
@ScheduleInput(ScheduleInputData{})
<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>
<script>
document.addEventListener('DOMContentLoaded', () => htmx.trigger('#schedule-input-group', 'change'))
</script>
}
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-swap-oob={ data.HXSwapOOB }
>
<label for="schedule" class="label">
<span
class={ utils.CX(map[string]bool{
"label-text": true,
"text-error": data.Error != "",
"text-success": data.Valid != "",
"text-base": true,
}) }
>Schedule</span>
<div class="tooltip tooltip-left" data-tip="Whether to enable the scheduler or not">
if data.Disabled {
<input type="checkbox" name="enable_schedule" value="1" class="toggle toggle-primary my-auto"/>
} else {
<input type="checkbox" name="enable_schedule" value="1" class="toggle toggle-primary my-auto" checked/>
}
</div>
</label>
if data.Disabled {
<input
id="schedule"
name="schedule"
type="text"
placeholder="e.g. '@daily' or '0 0 * * MON'"
class="input input-bordered"
disabled
/>
} else {
<input
id="schedule"
name="schedule"
type="text"
placeholder="e.g. '@daily' or '0 0 * * MON'"
value={ data.Value }
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 != "",
}) }
data-error={ data.Error }
hx-on::load="this.setCustomValidity(this.getAttribute('data-error'))"
list="cron-templates"
required
/>
}
<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 != "" {
{ data.Valid }
} else if data.Error != "" {
{ data.Error }
} 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>
}

View file

@ -1,52 +0,0 @@
package addview
import "github.com/tigorlazuardi/redmage/views/utils"
type SubredditTypeData struct {
Value string
Error string
HXSwapOOB string
}
templ SubredditTypeInput(data SubredditTypeData) {
<label
id="subreddit-type-input"
class="form-control w-full"
hx-swap-oob={ data.HXSwapOOB }
>
<div class="label">
<span
class={ utils.CX(map[string]bool{
"label-text": true,
"text-error": data.Error != "",
"text-base": true,
}) }
>Subreddit Type</span>
</div>
<select
onchange="
htmx.trigger('#name', 'on-demand');
this.setCustomValidity('');
"
name="type"
value={ data.Value }
class="select select-bordered"
class={ utils.CX(map[string]bool{
"select": true,
"select-bordered": true,
"select-error": data.Error != "",
}) }
data-error={ data.Error }
hx-on::load="this.setCustomValidity(this.dataset.error)"
>
if data.Value == "1" {
<option value="0">Subreddit</option>
<option selected value="1">User</option>
} else {
<option selected value="0">Subreddit</option>
<option value="1">User</option>
}
</select>
<script>document.addEventListener('DOMContentLoaded', () => htmx.trigger('#name', 'on-demand')) </script>
</label>
}

View file

@ -6,14 +6,16 @@ import "github.com/tigorlazuardi/redmage/views/components"
import "strconv" import "strconv"
import "github.com/tigorlazuardi/redmage/api" import "github.com/tigorlazuardi/redmage/api"
import "fmt" import "fmt"
import "github.com/tigorlazuardi/redmage/views/icons"
type Data struct { type Data struct {
Subreddit *models.Subreddit Subreddit *models.Subreddit
Devices models.DeviceSlice Devices models.DeviceSlice
Images models.ImageSlice Images models.ImageSlice
TotalImages int64 TotalImages int64
Error string Error string
Params api.SubredditGetByNameImageParams Params api.SubredditGetByNameImageParams
FlashMessageSuccess string
} }
templ Detailsview(c *views.Context, data Data) { templ Detailsview(c *views.Context, data Data) {
@ -25,7 +27,11 @@ templ Detailsview(c *views.Context, data Data) {
} }
@components.Body(c) { @components.Body(c) {
@DetailsContent(c, data) @DetailsContent(c, data)
@components.NotificationContainer() @components.NotificationContainer() {
if data.FlashMessageSuccess != "" {
@components.SuccessNotification(data.FlashMessageSuccess)
}
}
} }
} }
} }
@ -44,17 +50,32 @@ templ DetailsContent(c *views.Context, data Data) {
Total Images: Total Images:
{ strconv.FormatInt(data.TotalImages, 10) } { strconv.FormatInt(data.TotalImages, 10) }
</h2> </h2>
<button <div class="dropdown dropdown-hover dropdown-end">
hx-post="/htmx/subreddits/start" <div tabindex="0" role="button" class="btn m-1">
hx-include="this" @icons.Kebab("h-8 w-8")
class="btn btn-primary text-base-100" </div>
hx-target={ components.NotificationContainerID } <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52 mt-0">
hx-target-error={ components.NotificationContainerID } <li>
hx-swap="afterbegin" <button
> hx-post="/htmx/subreddits/start"
Start Download hx-include="this"
<input type="hidden" name="subreddit" value={ data.Subreddit.Name }/> hx-target={ components.NotificationContainerID }
</button> hx-target-error={ components.NotificationContainerID }
hx-swap="afterbegin"
class="btn btn-ghost"
>
Start Download
<input type="hidden" name="subreddit" value={ data.Subreddit.Name }/>
</button>
</li>
<li>
<a
href={ templ.SafeURL(fmt.Sprintf("/subreddits/edit/%s", data.Subreddit.Name)) }
class="btn btn-ghost no-underline"
>Edit</a>
</li>
</ul>
</div>
</div> </div>
<div class="divider"></div> <div class="divider"></div>
@FilterBar(c, data) @FilterBar(c, data)