subreddit: implemented add page
This commit is contained in:
parent
f4399555b4
commit
0d68bbead0
|
@ -49,6 +49,17 @@ func (reddit *Reddit) CheckSubreddit(ctx context.Context, params CheckSubredditP
|
|||
return actual, errs.Wrapw(err, msg, "url", url, "params", params).Code(http.StatusNotFound)
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusForbidden {
|
||||
var msg string
|
||||
if params.SubredditType == SubredditTypeUser {
|
||||
msg = "user has set their profile to private"
|
||||
}
|
||||
if params.SubredditType == SubredditTypeSub {
|
||||
msg = "subreddit is private"
|
||||
}
|
||||
return actual, errs.Wrapw(err, msg, "url", url, "params", params).Code(http.StatusForbidden)
|
||||
}
|
||||
|
||||
if params.SubredditType == SubredditTypeUser && resp.StatusCode == http.StatusOK {
|
||||
return params.Subreddit, nil
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ func (routes *Routes) registerHTMXRoutes(router chi.Router) {
|
|||
router.Use(chimiddleware.RequestLogger(middleware.ChiLogger{}))
|
||||
router.Use(chimiddleware.SetHeader("Content-Type", "text/html; charset=utf-8"))
|
||||
|
||||
router.Post("/subreddits/add", routes.SubredditsCreateHTMX)
|
||||
router.Post("/subreddits/start", routes.SubredditStartDownloadHTMX)
|
||||
router.Post("/subreddits/check", routes.SubredditCheckHTMX)
|
||||
router.Get("/subreddits/validate/schedule", routes.SubredditValidateScheduleHTMX)
|
||||
|
|
|
@ -5,13 +5,17 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/tigorlazuardi/redmage/api"
|
||||
"github.com/tigorlazuardi/redmage/api/reddit"
|
||||
"github.com/tigorlazuardi/redmage/models"
|
||||
"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"
|
||||
)
|
||||
|
||||
func (routes *Routes) SubredditsCreateAPI(rw http.ResponseWriter, req *http.Request) {
|
||||
|
@ -64,6 +68,118 @@ func (routes *Routes) SubredditsCreateAPI(rw http.ResponseWriter, req *http.Requ
|
|||
}
|
||||
}
|
||||
|
||||
func (routes *Routes) SubredditsCreateHTMX(rw http.ResponseWriter, r *http.Request) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
actual, err := routes.API.SubredditCheck(ctx, api.SubredditCheckParam{
|
||||
Subreddit: sub.Name,
|
||||
SubredditType: reddit.SubredditType(sub.Subtype),
|
||||
})
|
||||
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",
|
||||
})
|
||||
if err := renderer.Render(ctx, rw); err != nil {
|
||||
log.New(ctx).Err(err).Error("failed to render error")
|
||||
}
|
||||
return
|
||||
}
|
||||
sub.Name = actual
|
||||
|
||||
_, err = routes.API.SubredditsCreate(ctx, sub)
|
||||
if err != nil {
|
||||
log.New(ctx).Err(err).Error("failed to create subreddit")
|
||||
code, message := errs.HTTPMessage(err)
|
||||
rw.Header().Set("HX-Retarget", components.NotificationContainerID)
|
||||
rw.WriteHeader(code)
|
||||
if err := components.ErrorNotication(message).Render(ctx, rw); err != nil {
|
||||
log.New(ctx).Err(err).Error("failed to render error")
|
||||
}
|
||||
return
|
||||
}
|
||||
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) {
|
||||
sub = &models.Subreddit{}
|
||||
|
||||
var t reddit.SubredditType
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
enableSchedule, _ := strconv.Atoi(r.FormValue("enable_schedule"))
|
||||
sub.EnableSchedule = int32(enableSchedule)
|
||||
if sub.EnableSchedule > 1 {
|
||||
sub.EnableSchedule = 1
|
||||
} else if sub.EnableSchedule < 0 {
|
||||
sub.EnableSchedule = 0
|
||||
}
|
||||
if sub.EnableSchedule == 0 {
|
||||
sub.Schedule = "@daily"
|
||||
}
|
||||
|
||||
if sub.EnableSchedule == 1 {
|
||||
sub.Schedule = r.FormValue("schedule")
|
||||
_, err = cronParser.Parse(sub.Schedule)
|
||||
if err != nil {
|
||||
errs = append(errs, addview.ScheduleInput(addview.ScheduleInputData{
|
||||
Value: sub.Schedule,
|
||||
Error: fmt.Sprintf("invalid cron schedule: %s", err),
|
||||
HXSwapOOB: "true",
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
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 sub, errs
|
||||
}
|
||||
|
||||
var cronParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
|
||||
|
||||
func validateSubredditsCreate(body *models.Subreddit) error {
|
||||
|
|
|
@ -45,7 +45,7 @@ func (routes *Routes) SubredditValidateScheduleHTMX(rw http.ResponseWriter, r *h
|
|||
|
||||
next := scheduler.Next(time.Now())
|
||||
|
||||
data.Valid = fmt.Sprintf("Schedule is valid. Next run at: %s", next.Format("Monday, _2 January 2006 15:04:05 MST"))
|
||||
data.Valid = fmt.Sprintf("Syntax is valid. Next run at: %s", next.Format("Monday, _2 January 2006 15:04 MST"))
|
||||
|
||||
if err := addview.ScheduleInput(data).Render(ctx, rw); err != nil {
|
||||
log.New(ctx).Err(err).Error("failed to render schedule input")
|
||||
|
|
|
@ -19,19 +19,24 @@ templ AddviewContent(c *views.Context) {
|
|||
<h1>Add Subreddit</h1>
|
||||
<div class="divider"></div>
|
||||
<form
|
||||
action="/htmx/subreddits/add"
|
||||
method="POST"
|
||||
onkeydown="return event.key !== 'Enter'"
|
||||
hx-post="/htmx/subreddit/add"
|
||||
hx-post="/htmx/subreddits/add"
|
||||
>
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 gap-4"
|
||||
>
|
||||
<label id="subreddit-input" class="form-control w-full">
|
||||
@SubredditInputForm(SubredditInputData{})
|
||||
</label>
|
||||
<label id="subreddit-type-input" class="form-control w-full">
|
||||
@SubredditTypeInput(SubredditTypeData{})
|
||||
</label>
|
||||
<div class="flex gap-4 content-center">
|
||||
<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>
|
||||
|
|
73
views/subredditsview/addview/countback.templ
Normal file
73
views/subredditsview/addview/countback.templ
Normal file
|
@ -0,0 +1,73 @@
|
|||
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,6 +1,5 @@
|
|||
package addview
|
||||
|
||||
import "github.com/tigorlazuardi/redmage/views/components"
|
||||
import "github.com/tigorlazuardi/redmage/views/utils"
|
||||
import "github.com/tigorlazuardi/redmage/api/reddit"
|
||||
|
||||
|
@ -9,9 +8,20 @@ type SubredditInputData struct {
|
|||
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{
|
||||
|
@ -22,7 +32,24 @@ templ SubredditInputForm(data SubredditInputData) {
|
|||
}) }
|
||||
>Subreddit Name</span>
|
||||
</div>
|
||||
@subredditInputField("/htmx/subreddits/check", data)
|
||||
<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{
|
||||
|
@ -43,31 +70,5 @@ templ SubredditInputForm(data SubredditInputData) {
|
|||
}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ subredditInputField(target string, data SubredditInputData) {
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
hx-post={ target }
|
||||
hx-target="#subreddit-input"
|
||||
hx-target-4xx="#subreddit-input"
|
||||
hx-trigger="keyup changed delay:1s, on-demand"
|
||||
hx-include="[name='type']"
|
||||
hx-target-5x={ components.NotificationContainerID }
|
||||
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'))"
|
||||
/>
|
||||
</label>
|
||||
}
|
|
@ -1,16 +1,37 @@
|
|||
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>
|
||||
|
@ -19,14 +40,14 @@ templ scheduleInputContainer() {
|
|||
templ ScheduleInput(data ScheduleInputData) {
|
||||
<div
|
||||
id="schedule-input-group"
|
||||
class="form-control w-full my-auto"
|
||||
class="form-control w-full"
|
||||
hx-get="/htmx/subreddits/validate/schedule"
|
||||
hx-trigger="change, input delay:1s"
|
||||
hx-trigger="change"
|
||||
hx-include="this"
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML"
|
||||
hx-swap-oob={ data.HXSwapOOB }
|
||||
>
|
||||
<label class="label">
|
||||
<label for="schedule" class="label">
|
||||
<span
|
||||
class={ utils.CX(map[string]bool{
|
||||
"label-text": true,
|
||||
|
@ -35,18 +56,26 @@ templ ScheduleInput(data ScheduleInputData) {
|
|||
"text-base": true,
|
||||
}) }
|
||||
>Schedule</span>
|
||||
<div class="tooltip" data-tip="Whether to enable scheduler or not">
|
||||
<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"/>
|
||||
<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" checked/>
|
||||
<input type="checkbox" name="enable_schedule" value="1" class="toggle toggle-primary my-auto" checked/>
|
||||
}
|
||||
</div>
|
||||
</label>
|
||||
if data.Disabled {
|
||||
<input name="schedule" type="text" placeholder="e.g. '@daily' or '0 0 * * MON'" class="input input-bordered" 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'"
|
||||
|
@ -59,6 +88,10 @@ templ ScheduleInput(data ScheduleInputData) {
|
|||
"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">
|
||||
|
@ -72,8 +105,10 @@ templ ScheduleInput(data ScheduleInputData) {
|
|||
>
|
||||
if data.Valid != "" {
|
||||
{ data.Valid }
|
||||
} else {
|
||||
} 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>
|
|
@ -1,27 +0,0 @@
|
|||
package addview
|
||||
|
||||
type SubredditTypeData struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
templ SubredditTypeInput(data SubredditTypeData) {
|
||||
<div class="label">
|
||||
<span class="label-text text-base">Subreddit Type</span>
|
||||
</div>
|
||||
<select
|
||||
onchange="htmx.trigger('#name', 'on-demand')"
|
||||
name="type"
|
||||
value={ data.Value }
|
||||
class="select select-bordered"
|
||||
>
|
||||
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>
|
||||
<div class="min-h-4"></div>
|
||||
<script>document.addEventListener('DOMContentLoaded', () => htmx.trigger('#name', 'on-demand')) </script>
|
||||
}
|
52
views/subredditsview/addview/type.templ
Normal file
52
views/subredditsview/addview/type.templ
Normal file
|
@ -0,0 +1,52 @@
|
|||
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>
|
||||
}
|
Loading…
Reference in a new issue