diff --git a/api/reddit/check_subreddit.go b/api/reddit/check_subreddit.go index 949a14a..5598a02 100644 --- a/api/reddit/check_subreddit.go +++ b/api/reddit/check_subreddit.go @@ -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) diff --git a/api/reddit/get_posts.go b/api/reddit/get_posts.go index ec5c327..b319656 100644 --- a/api/reddit/get_posts.go +++ b/api/reddit/get_posts.go @@ -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 { diff --git a/db/migrations/20240527205312_trigger_refactors.sql b/db/migrations/20240527205312_trigger_refactors.sql new file mode 100644 index 0000000..7fa4e5b --- /dev/null +++ b/db/migrations/20240527205312_trigger_refactors.sql @@ -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 diff --git a/server/routes/page_subreddits.go b/server/routes/page_subreddits.go index 7df6208..8ed0ef5 100644 --- a/server/routes/page_subreddits.go +++ b/server/routes/page_subreddits.go @@ -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) } diff --git a/server/routes/page_subreddits_add.go b/server/routes/page_subreddits_add.go index 281c334..bb5d364 100644 --- a/server/routes/page_subreddits_add.go +++ b/server/routes/page_subreddits_add.go @@ -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") + // } } diff --git a/server/routes/routes.go b/server/routes/routes.go index a4edc4a..761fec3 100644 --- a/server/routes/routes.go +++ b/server/routes/routes.go @@ -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) diff --git a/server/routes/subreddit_check.go b/server/routes/subreddit_check.go index 962e8c5..4ad37d4 100644 --- a/server/routes/subreddit_check.go +++ b/server/routes/subreddit_check.go @@ -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") } } diff --git a/server/routes/subreddit_create.go b/server/routes/subreddit_create.go index 91857de..6450436 100644 --- a/server/routes/subreddit_create.go +++ b/server/routes/subreddit_create.go @@ -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) diff --git a/server/routes/subreddit_validate_schedule.go b/server/routes/subreddit_validate_schedule.go index 91e873d..5f7628f 100644 --- a/server/routes/subreddit_validate_schedule.go +++ b/server/routes/subreddit_validate_schedule.go @@ -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") } } diff --git a/views/subreddits/put/countback.templ b/views/subreddits/put/countback.templ new file mode 100644 index 0000000..0659345 --- /dev/null +++ b/views/subreddits/put/countback.templ @@ -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) { + +} diff --git a/views/subreddits/put/fetch.templ b/views/subreddits/put/fetch.templ new file mode 100644 index 0000000..cbfd672 --- /dev/null +++ b/views/subreddits/put/fetch.templ @@ -0,0 +1,32 @@ +package put + +templ FetchCheckbox() { +
+ + + Fetch images immediately after creation + +
+} diff --git a/views/subreddits/put/name.templ b/views/subreddits/put/name.templ new file mode 100644 index 0000000..c9ae255 --- /dev/null +++ b/views/subreddits/put/name.templ @@ -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) { + +} diff --git a/views/subreddits/put/put.templ b/views/subreddits/put/put.templ new file mode 100644 index 0000000..e4e0f7f --- /dev/null +++ b/views/subreddits/put/put.templ @@ -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) { +
+ @components.Container() { +

Add Subreddit

+
+
+
+ @NameInput(data.NameInput) + @TypeInput(data.TypeInput) +
+ @ScheduleInput(data.ScheduleInput) +
+
+ @CountbackInput(data.CountbackInput) +
+
+
+ @FetchCheckbox() +
+
+
+ +
+ } +
+} diff --git a/views/subreddits/put/schedule.templ b/views/subreddits/put/schedule.templ new file mode 100644 index 0000000..b076f08 --- /dev/null +++ b/views/subreddits/put/schedule.templ @@ -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) { +
+ + +
+ + 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'. + } + +
+
+ @scheduleDatalist() +} + +templ scheduleDatalist() { + + + + + + + + + + + + + + + for i := 1; i < 24; i++ { + + } + +} diff --git a/views/subreddits/put/type.templ b/views/subreddits/put/type.templ new file mode 100644 index 0000000..69f40ce --- /dev/null +++ b/views/subreddits/put/type.templ @@ -0,0 +1,26 @@ +package put + +import "github.com/tigorlazuardi/redmage/api/reddit" + +type TypeInputData struct { + Value reddit.SubredditType +} + +templ TypeInput(data TypeInputData) { + +} diff --git a/views/subreddits/view.templ b/views/subreddits/view.templ index c5514cc..5ffc455 100644 --- a/views/subreddits/view.templ +++ b/views/subreddits/view.templ @@ -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) { +
+ @components.Container() { +

Subreddits

+ if data.Subreddits.Total == 0 { +
+

No Subreddits Found

+

Click here to add a new subreddit.

+ } else { +
+

{ strconv.FormatInt(data.Subreddits.Total, 10) } Subreddits Registered

+ Add Subreddit +
+
+ } +
+ for _, subreddit := range data.Subreddits.Data { + @SubredditCard(c, subreddit) + } +
+ } +
+} + +templ SubredditCard(c *views.Context, data *models.Subreddit) { + + if len(data.R.Images) > 0 { +
+ { +
+ } else { +
+ @imagePlaceholder() +
+ } +
+
+

{ data.Name }

+
+
+} + +templ imagePlaceholder() { + + + + +} diff --git a/views/subredditsview/subredditsview.templ b/views/subredditsview/subredditsview.templ deleted file mode 100644 index a6f3347..0000000 --- a/views/subredditsview/subredditsview.templ +++ /dev/null @@ -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) { -
- @components.Container() { -

Subreddits

- if data.Subreddits.Total == 0 { -
-

No Subreddits Found

-

Click here to add a new subreddit.

- } else { -
-

{ strconv.FormatInt(data.Subreddits.Total, 10) } Subreddits Registered

- Add Subreddit -
-
- } -
- for _, subreddit := range data.Subreddits.Data { - @SubredditCard(c, subreddit) - } -
- } -
-} - -templ SubredditCard(c *views.Context, data *models.Subreddit) { - - if len(data.R.Images) > 0 { -
- { -
- } else { -
- @imagePlaceholder() -
- } -
-
-

{ data.Name }

-
-
-} - -templ imagePlaceholder() { - - - - -} diff --git a/views/subredditsview/subredditsview_data.go b/views/subredditsview/subredditsview_data.go deleted file mode 100644 index 982924e..0000000 --- a/views/subredditsview/subredditsview_data.go +++ /dev/null @@ -1,10 +0,0 @@ -package subredditsview - -import ( - "github.com/tigorlazuardi/redmage/api" -) - -type Data struct { - Subreddits api.ListSubredditsResult - Error string -}