diff --git a/api/schedule_status_upsert.go b/api/schedule_status_upsert.go index b64f45d..0711058 100644 --- a/api/schedule_status_upsert.go +++ b/api/schedule_status_upsert.go @@ -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 } diff --git a/api/subreddits_edit.go b/api/subreddits_edit.go new file mode 100644 index 0000000..058f55a --- /dev/null +++ b/api/subreddits_edit.go @@ -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 +} diff --git a/server/routes/page_subreddits_add.go b/server/routes/page_subreddits_add.go index bb5d364..8ad95e5 100644 --- a/server/routes/page_subreddits_add.go +++ b/server/routes/page_subreddits_add.go @@ -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") - // } } diff --git a/server/routes/page_subreddits_details.go b/server/routes/page_subreddits_details.go index 0d71ce9..a3f80ba 100644 --- a/server/routes/page_subreddits_details.go +++ b/server/routes/page_subreddits_details.go @@ -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 diff --git a/server/routes/page_subreddits_edit.go b/server/routes/page_subreddits_edit.go new file mode 100644 index 0000000..dc990d8 --- /dev/null +++ b/server/routes/page_subreddits_edit.go @@ -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") + } +} diff --git a/server/routes/routes.go b/server/routes/routes.go index 761fec3..062a425 100644 --- a/server/routes/routes.go +++ b/server/routes/routes.go @@ -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) diff --git a/server/routes/subreddit_create.go b/server/routes/subreddit_create.go index 6450436..6e2cf48 100644 --- a/server/routes/subreddit_create.go +++ b/server/routes/subreddit_create.go @@ -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") diff --git a/server/routes/subreddit_edit.go b/server/routes/subreddit_edit.go new file mode 100644 index 0000000..eead679 --- /dev/null +++ b/server/routes/subreddit_edit.go @@ -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) +} diff --git a/views/components/body.templ b/views/components/body.templ index 369f68e..7a7d0df 100644 --- a/views/components/body.templ +++ b/views/components/body.templ @@ -7,7 +7,7 @@ templ Body(c *views.Context) { @Navigation(c) {
@Navbar(c) -
+
{ children... }
diff --git a/views/components/notification.templ b/views/components/notification.templ index 7b6b230..05d1f80 100644 --- a/views/components/notification.templ +++ b/views/components/notification.templ @@ -3,7 +3,9 @@ package components const NotificationContainerID = "#notification-container" templ NotificationContainer() { -
+
+ { children... } +
} templ InfoNotication(messages ...string) { @@ -42,7 +44,7 @@ templ ErrorNotication(messages ...string) { templ SuccessNotification(messages ...string) {
diff --git a/views/components/pagination.templ b/views/components/pagination.templ index f5f5d6a..0c8f252 100644 --- a/views/components/pagination.templ +++ b/views/components/pagination.templ @@ -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 { diff --git a/views/devices/details/view.templ b/views/devices/details/view.templ index 3710f08..9edcc1f 100644 --- a/views/devices/details/view.templ +++ b/views/devices/details/view.templ @@ -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 {
@filter(c, data) diff --git a/views/icons/gear.templ b/views/icons/gear.templ new file mode 100644 index 0000000..245080f --- /dev/null +++ b/views/icons/gear.templ @@ -0,0 +1,22 @@ +package icons + +import "strings" + +templ Gear(class ...string) { + + + + +} diff --git a/views/icons/kebab.templ b/views/icons/kebab.templ new file mode 100644 index 0000000..252e67a --- /dev/null +++ b/views/icons/kebab.templ @@ -0,0 +1,31 @@ +package icons + +import "strings" + +templ Kebab(class ...string) { + + + + + +} diff --git a/views/subreddits/put/name.templ b/views/subreddits/put/name.templ index c9ae255..ce4283f 100644 --- a/views/subreddits/put/name.templ +++ b/views/subreddits/put/name.templ @@ -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) {