This commit is contained in:
parent
5957648ec3
commit
3fdb78362b
|
@ -20,20 +20,22 @@ func (api *API) scheduleStatusUpsert(ctx context.Context, exec bob.Executor, par
|
|||
ctx, span := tracer.Start(ctx, "*API.createNewScheduleStatus")
|
||||
defer span.End()
|
||||
now := time.Now()
|
||||
ss, err := models.ScheduleStatuses.Upsert(ctx, exec, true, []string{"subreddit"}, []string{
|
||||
"subreddit",
|
||||
"status",
|
||||
"error_message",
|
||||
"updated_at",
|
||||
}, &models.ScheduleStatusSetter{
|
||||
Subreddit: omit.FromCond(params.Subreddit, params.Subreddit != ""),
|
||||
Status: omit.From(params.Status.Int8()),
|
||||
ErrorMessage: omit.From(params.ErrorMessage),
|
||||
CreatedAt: omit.From(now.Unix()),
|
||||
UpdatedAt: omit.From(now.Unix()),
|
||||
api.lockf(func() {
|
||||
schedule, err = models.ScheduleStatuses.Upsert(ctx, exec, true, []string{"subreddit"}, []string{
|
||||
"subreddit",
|
||||
"status",
|
||||
"error_message",
|
||||
"updated_at",
|
||||
}, &models.ScheduleStatusSetter{
|
||||
Subreddit: omit.FromCond(params.Subreddit, params.Subreddit != ""),
|
||||
Status: omit.From(params.Status.Int8()),
|
||||
ErrorMessage: omit.From(params.ErrorMessage),
|
||||
CreatedAt: omit.From(now.Unix()),
|
||||
UpdatedAt: omit.From(now.Unix()),
|
||||
})
|
||||
})
|
||||
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
56
api/subreddits_edit.go
Normal 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
|
||||
}
|
|
@ -14,13 +14,12 @@ func (routes *Routes) PageSubredditsAdd(rw http.ResponseWriter, r *http.Request)
|
|||
|
||||
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 {
|
||||
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")
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ func (routes *Routes) PageSubredditsDetails(rw http.ResponseWriter, r *http.Requ
|
|||
params.FillFromQuery(r.URL.Query())
|
||||
|
||||
var data detailsview.Data
|
||||
data.FlashMessageSuccess = r.Header.Get("X-Flash-Message-Success")
|
||||
var err error
|
||||
data.Params = params
|
||||
|
||||
|
|
50
server/routes/page_subreddits_edit.go
Normal file
50
server/routes/page_subreddits_edit.go
Normal 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")
|
||||
}
|
||||
}
|
|
@ -83,6 +83,8 @@ func (routes *Routes) registerWWWRoutes(router chi.Router) {
|
|||
r.Get("/subreddits", routes.PageSubreddits)
|
||||
r.Get("/subreddits/details/{name}", routes.PageSubredditsDetails)
|
||||
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("/devices", routes.PageDevices)
|
||||
r.Get("/devices/add", routes.PageDevicesAdd)
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
"github.com/tigorlazuardi/redmage/pkg/errs"
|
||||
"github.com/tigorlazuardi/redmage/pkg/log"
|
||||
"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) {
|
||||
|
@ -87,11 +87,9 @@ func (routes *Routes) SubredditsCreateHTMX(rw http.ResponseWriter, r *http.Reque
|
|||
if err != nil {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
log.New(ctx).Err(err).Error("subreddit check returns error")
|
||||
renderer := addview.SubredditInputForm(addview.SubredditInputData{
|
||||
Value: sub.Name,
|
||||
Error: err.Error(),
|
||||
Type: reddit.SubredditType(sub.Subtype),
|
||||
HXSwapOOB: "true",
|
||||
renderer := put.NameInput(put.NameInputData{
|
||||
Value: sub.Name,
|
||||
Error: err.Error(),
|
||||
})
|
||||
if err := renderer.Render(ctx, rw); err != nil {
|
||||
log.New(ctx).Err(err).Error("failed to render error")
|
||||
|
|
63
server/routes/subreddit_edit.go
Normal file
63
server/routes/subreddit_edit.go
Normal 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)
|
||||
}
|
|
@ -7,7 +7,7 @@ templ Body(c *views.Context) {
|
|||
@Navigation(c) {
|
||||
<div class="flex">
|
||||
@Navbar(c)
|
||||
<div class="flex-grow">
|
||||
<div id="root-content" class="flex-grow">
|
||||
{ children... }
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,9 @@ package components
|
|||
const NotificationContainerID = "#notification-container"
|
||||
|
||||
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) {
|
||||
|
@ -42,7 +44,7 @@ templ ErrorNotication(messages ...string) {
|
|||
|
||||
templ SuccessNotification(messages ...string) {
|
||||
<div
|
||||
hx-on::load="setTimeout(() => this.remove(), 5000)"
|
||||
x-data="{ init() { setTimeout(() => $el.remove(), 5000) } }"
|
||||
class="toast"
|
||||
onclick="this.remove()"
|
||||
>
|
||||
|
|
|
@ -85,7 +85,7 @@ func (pgdata PaginationData) getMobilePageStatus(page int) pageStatus {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -2,6 +2,7 @@ package details
|
|||
|
||||
import "github.com/tigorlazuardi/redmage/views"
|
||||
import "github.com/tigorlazuardi/redmage/views/components"
|
||||
import "github.com/tigorlazuardi/redmage/views/icons"
|
||||
import "fmt"
|
||||
import "github.com/tigorlazuardi/redmage/models"
|
||||
import "strconv"
|
||||
|
@ -27,10 +28,14 @@ templ Content(c *views.Context, data Data) {
|
|||
} else {
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="my-auto">{ data.Device.Name }</h1>
|
||||
<a
|
||||
href={ templ.SafeURL(fmt.Sprintf("/devices/edit/%s", data.Device.Slug)) }
|
||||
class="btn btn-primary no-underline sm:w-24"
|
||||
>Edit</a>
|
||||
<div class="tooltip" data-tip="Edit">
|
||||
<a
|
||||
href={ templ.SafeURL(fmt.Sprintf("/devices/edit/%s", data.Device.Slug)) }
|
||||
class="btn btn-primary no-underline"
|
||||
>
|
||||
@icons.Gear("w-8 h-8 text-primary-content")
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
@filter(c, data)
|
||||
|
|
22
views/icons/gear.templ
Normal file
22
views/icons/gear.templ
Normal 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
31
views/icons/kebab.templ
Normal 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>
|
||||
}
|
|
@ -4,19 +4,15 @@ import "github.com/tigorlazuardi/redmage/views/utils"
|
|||
import "fmt"
|
||||
|
||||
type NameInputData struct {
|
||||
Value string
|
||||
Error string
|
||||
Valid string
|
||||
HXSwapOOB string
|
||||
Value string
|
||||
Error string
|
||||
Valid 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
|
||||
|
|
|
@ -5,6 +5,8 @@ import "github.com/tigorlazuardi/redmage/views/components"
|
|||
|
||||
type Data struct {
|
||||
Title string
|
||||
EditMode bool
|
||||
PostAction string
|
||||
NameInput NameInputData
|
||||
TypeInput TypeInputData
|
||||
ScheduleInput ScheduleInputData
|
||||
|
@ -24,33 +26,44 @@ templ View(c *views.Context, data Data) {
|
|||
templ Content(c *views.Context, data Data) {
|
||||
<main class="prose min-w-full">
|
||||
@components.Container() {
|
||||
<h1>Add Subreddit</h1>
|
||||
<h1>{ data.Title }</h1>
|
||||
<div class="divider"></div>
|
||||
<form
|
||||
action="/htmx/subreddits/add"
|
||||
action={ templ.SafeURL(data.PostAction) }
|
||||
method="POST"
|
||||
onkeydown="return event.key !== 'Enter'"
|
||||
hx-post="/htmx/subreddits/add"
|
||||
hx-post={ data.PostAction }
|
||||
hx-target-error={ components.NotificationContainerID }
|
||||
>
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 gap-4"
|
||||
>
|
||||
@NameInput(data.NameInput)
|
||||
@TypeInput(data.TypeInput)
|
||||
if !data.EditMode {
|
||||
@NameInput(data.NameInput)
|
||||
}
|
||||
if !data.EditMode {
|
||||
@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()
|
||||
if !data.EditMode {
|
||||
<div class="sm:col-span-2">
|
||||
<div class="max-w-xs mx-auto">
|
||||
@FetchCheckbox()
|
||||
</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>
|
||||
}
|
||||
</main>
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -6,14 +6,16 @@ import "github.com/tigorlazuardi/redmage/views/components"
|
|||
import "strconv"
|
||||
import "github.com/tigorlazuardi/redmage/api"
|
||||
import "fmt"
|
||||
import "github.com/tigorlazuardi/redmage/views/icons"
|
||||
|
||||
type Data struct {
|
||||
Subreddit *models.Subreddit
|
||||
Devices models.DeviceSlice
|
||||
Images models.ImageSlice
|
||||
TotalImages int64
|
||||
Error string
|
||||
Params api.SubredditGetByNameImageParams
|
||||
Subreddit *models.Subreddit
|
||||
Devices models.DeviceSlice
|
||||
Images models.ImageSlice
|
||||
TotalImages int64
|
||||
Error string
|
||||
Params api.SubredditGetByNameImageParams
|
||||
FlashMessageSuccess string
|
||||
}
|
||||
|
||||
templ Detailsview(c *views.Context, data Data) {
|
||||
|
@ -25,7 +27,11 @@ templ Detailsview(c *views.Context, data Data) {
|
|||
}
|
||||
@components.Body(c) {
|
||||
@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:
|
||||
{ strconv.FormatInt(data.TotalImages, 10) }
|
||||
</h2>
|
||||
<button
|
||||
hx-post="/htmx/subreddits/start"
|
||||
hx-include="this"
|
||||
class="btn btn-primary text-base-100"
|
||||
hx-target={ components.NotificationContainerID }
|
||||
hx-target-error={ components.NotificationContainerID }
|
||||
hx-swap="afterbegin"
|
||||
>
|
||||
Start Download
|
||||
<input type="hidden" name="subreddit" value={ data.Subreddit.Name }/>
|
||||
</button>
|
||||
<div class="dropdown dropdown-hover dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn m-1">
|
||||
@icons.Kebab("h-8 w-8")
|
||||
</div>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52 mt-0">
|
||||
<li>
|
||||
<button
|
||||
hx-post="/htmx/subreddits/start"
|
||||
hx-include="this"
|
||||
hx-target={ components.NotificationContainerID }
|
||||
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 class="divider"></div>
|
||||
@FilterBar(c, data)
|
||||
|
|
Loading…
Reference in a new issue