devices: filled the add device page

This commit is contained in:
Tigor Hutasuhut 2024-05-07 20:51:00 +07:00
parent 19ffbdf98c
commit 494a4bc956
14 changed files with 458 additions and 22 deletions

View file

@ -23,9 +23,14 @@ type DevicesListParams struct {
} }
func (dlp *DevicesListParams) FillFromQuery(query Queryable) { func (dlp *DevicesListParams) FillFromQuery(query Queryable) {
dlp.Status, _ = strconv.Atoi(query.Get("status")) statusStr := query.Get("status")
if dlp.Status > 2 { switch statusStr {
dlp.Status = 2 case "0":
dlp.Status = 0
case "1":
dlp.Status = 1
default:
dlp.Status = -1
} }
dlp.Q = query.Get("q") dlp.Q = query.Get("q")
@ -68,8 +73,8 @@ func (dlp DevicesListParams) Query() (expr []bob.Mod[*dialect.SelectQuery]) {
} }
func (dlp DevicesListParams) CountQuery() (expr []bob.Mod[*dialect.SelectQuery]) { func (dlp DevicesListParams) CountQuery() (expr []bob.Mod[*dialect.SelectQuery]) {
if dlp.Status > 0 { if dlp.Status >= 0 {
expr = append(expr, models.SelectWhere.Devices.Enable.EQ(int32(dlp.Status-1))) expr = append(expr, models.SelectWhere.Devices.Enable.EQ(int32(dlp.Status)))
} }
if dlp.Q != "" { if dlp.Q != "" {

View file

@ -58,6 +58,7 @@ var DefaultConfig = map[string]any{
"web.dependencies.htmx.version": "1.9.12", "web.dependencies.htmx.version": "1.9.12",
"web.dependencies.dayjs.version": "1.11.10", "web.dependencies.dayjs.version": "1.11.10",
"web.dependencies.alpinejs.version": "3.13.10",
"runtime.version": "0.0.1", "runtime.version": "0.0.1",
"runtime.environment": "development", "runtime.environment": "development",

2
go.mod
View file

@ -40,6 +40,8 @@ require (
github.com/boreq/errors v0.1.0 // indirect github.com/boreq/errors v0.1.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gosimple/slug v1.14.0 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
github.com/oklog/ulid v1.3.1 // indirect github.com/oklog/ulid v1.3.1 // indirect

4
go.sum
View file

@ -184,6 +184,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gosimple/slug v1.14.0 h1:RtTL/71mJNDfpUbCOmnf/XFkzKRtD6wL6Uy+3akm4Es=
github.com/gosimple/slug v1.14.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=

View file

@ -0,0 +1,59 @@
package routes
import (
"net/http"
"github.com/gosimple/slug"
"github.com/tigorlazuardi/redmage/pkg/errs"
"github.com/tigorlazuardi/redmage/pkg/log"
"github.com/tigorlazuardi/redmage/views/devicesview/adddevice"
)
func (routes *Routes) DevicesValidateNameHTMX(rw http.ResponseWriter, req *http.Request) {
ctx, span := tracer.Start(req.Context(), "*Routes.ValidateName")
defer span.End()
var nameData adddevice.NameInputData
nameData.Value = req.FormValue("name")
nameComponent := adddevice.NameInput(nameData)
s := req.FormValue("slug")
if s != "" || nameData.Value == "" {
if err := nameComponent.Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render name input")
}
return
}
s = slug.Make(nameData.Value)
slugData := adddevice.SlugInputData{
Value: s,
HXSwapOOB: true,
}
exist, err := routes.API.DevicesValidateSlug(ctx, s)
if err != nil {
log.New(ctx).Err(err).Error("failed to validate slug")
_, message := errs.HTTPMessage(err)
slugData.Error = message
_ = nameComponent.Render(ctx, rw)
if err := adddevice.SlugInput(slugData).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render name input")
}
}
if exist {
slugData.Error = "Device with this identifier already exist. Please change the value manually."
// rw.WriteHeader(http.StatusConflict)
_ = nameComponent.Render(ctx, rw)
if err := adddevice.SlugInput(slugData).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render name input")
}
return
}
slugData.Valid = "Identifier is available"
_ = nameComponent.Render(ctx, rw)
if err := adddevice.SlugInput(slugData).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render name input")
}
}

View file

@ -3,6 +3,7 @@ package routes
import ( import (
"net/http" "net/http"
"github.com/gosimple/slug"
"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/devicesview/adddevice" "github.com/tigorlazuardi/redmage/views/devicesview/adddevice"
@ -13,7 +14,7 @@ func (routes *Routes) DevicesValidateSlugHTMX(rw http.ResponseWriter, req *http.
defer span.End() defer span.End()
var data adddevice.SlugInputData var data adddevice.SlugInputData
data.Value = req.FormValue("slug") data.Value = slug.Make(req.FormValue("slug"))
if data.Value == "" { if data.Value == "" {
if err := adddevice.SlugInput(data).Render(ctx, rw); err != nil { if err := adddevice.SlugInput(data).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render slug input") log.New(ctx).Err(err).Error("failed to render slug input")

View file

@ -64,6 +64,7 @@ func (routes *Routes) registerHTMXRoutes(router chi.Router) {
router.Get("/subreddits/validate/schedule", routes.SubredditValidateScheduleHTMX) router.Get("/subreddits/validate/schedule", routes.SubredditValidateScheduleHTMX)
router.Post("/devices/add/validate/slug", routes.DevicesValidateSlugHTMX) router.Post("/devices/add/validate/slug", routes.DevicesValidateSlugHTMX)
router.Post("/devices/add/validate/name", routes.DevicesValidateNameHTMX)
} }
func (routes *Routes) registerWWWRoutes(router chi.Router) { func (routes *Routes) registerWWWRoutes(router chi.Router) {

View file

@ -0,0 +1,39 @@
package adddevice
import "fmt"
type AspectRatioToleranceData struct {
Value float64
}
templ AspectRatioToleranceInput(data AspectRatioToleranceData) {
<label
id="aspect-ratio-tolerance-form"
class="form-control sm:col-span-2"
>
<div class="label">
<span class="label-text">Aspect Ratio Tolerance</span>
<span class="label-text-alt hidden sm:inline">NOTE: This is NOT a filter for image sizes, but just the shape of it.</span>
</div>
<input
id="aspect-ratio-tolerance-field"
name="aspect_ratio_tolerance"
type="number"
min="0"
@change="$el.setCustomValidity(''); this.error = false"
class="input input-bordered"
:class="{'text-error': error, 'input-error': error}"
value={ fmt.Sprintf("%.2f", data.Value) }
placeholder="0.20"
step="0.01"
required
/>
<div class="label">
<span class="label-text">
Aspect Ratio Tolerance is a filter on how close the shape of the download images to the device shape is.
The bigger the value, the more images will be accepted and vice versa. '0.20' is the default value because
it accept quite a bit of images to download while still looking good even when the image is stretched for the device's wallpaper.
</span>
</div>
</label>
}

View file

@ -0,0 +1,99 @@
package adddevice
import "fmt"
import "github.com/tigorlazuardi/redmage/views/utils"
import "strconv"
templ MaxImageResolutionXInput(data ResolutionData) {
<label
x-data={ fmt.Sprintf(`{error: %t}`, data.Error != "") }
id="max-image-width-form"
class="form-control"
if data.HXSwapOOB {
hx-swap-oob="true"
}
>
<div class="label">
<span
class={ utils.CXX("label-text", true, "text-error", data.Error != "") }
:class="{'text-error': error}"
>Maximum Image Width</span>
</div>
<input
id="max-image-width-field"
x-data={ fmt.Sprintf(`{ init() { $el.setCustomValidity(%q) } }`, data.Error) }
name="max_x"
type="number"
min="0"
@change="$el.setCustomValidity(''); this.error = false"
class={ utils.CXX(
"input input-bordered", true,
"text-error", data.Error != "",
"input-error", data.Error != "",
) }
:class="{'text-error': error, 'input-error': error}"
value={ strconv.Itoa(data.Value) }
placeholder="0"
required
/>
<div class="label">
<span
class={ utils.CXX("label-text", true, "text-error", data.Error != "") }
:class="{'text-error': error}"
>
if data.Error != "" {
{ data.Error }
} else {
Maximum image width resolution to download for this device. Set to '0' to disable this filter. Set this value to avoid image sizes that are too big.
}
</span>
</div>
</label>
}
templ MaxImageResolutionYInput(data ResolutionData) {
<label
x-data={ fmt.Sprintf(`{error: %t}`, data.Error != "") }
id="max-image-height-form"
class="form-control"
if data.HXSwapOOB {
hx-swap-oob="true"
}
>
<div class="label">
<span
class={ utils.CXX("label-text", true, "text-error", data.Error != "") }
:class="{'text-error': error}"
>Maximum Image Height</span>
</div>
<input
id="max-image-height-field"
x-data={ fmt.Sprintf(`{ init() { $el.setCustomValidity(%q) } }`, data.Error) }
name="min_y"
type="number"
min="0"
@change="$el.setCustomValidity(''); this.error = false"
class={ utils.CXX(
"input input-bordered", true,
"text-error", data.Error != "",
"input-error", data.Error != "",
) }
:class="{'text-error': error, 'input-error': error}"
value={ strconv.Itoa(data.Value) }
placeholder="0"
required
/>
<div class="label">
<span
class={ utils.CXX("label-text", true, "text-error", data.Error != "") }
:class="{'text-error': error}"
>
if data.Error != "" {
{ data.Error }
} else {
Maximum image height resolution to download for this device. Set to '0' to disable this filter. Set this value to avoid image sizes that are too big.
}
</span>
</div>
</label>
}

View file

@ -0,0 +1,101 @@
package adddevice
import "fmt"
import "github.com/tigorlazuardi/redmage/views/utils"
import "strconv"
templ MinImageResolutionXInput(data ResolutionData) {
<label
x-data={ fmt.Sprintf(`{error: %t}`, data.Error != "") }
id="min-image-width-form"
class="form-control"
if data.HXSwapOOB {
hx-swap-oob="true"
}
>
<div class="label">
<span
class={ utils.CXX("label-text", true, "text-error", data.Error != "") }
:class="{'text-error': error}"
>Minimum Image Width</span>
</div>
<input
id="min-image-width-field"
x-data={ fmt.Sprintf(`{ init() { $el.setCustomValidity(%q) } }`, data.Error) }
name="min_x"
type="number"
min="0"
@change="$el.setCustomValidity(''); this.error = false"
class={ utils.CXX(
"input input-bordered", true,
"text-error", data.Error != "",
"input-error", data.Error != "",
) }
:class="{'text-error': error, 'input-error': error}"
value={ strconv.Itoa(data.Value) }
placeholder="0"
required
/>
<div class="label">
<span
class={ utils.CXX("label-text", true, "text-error", data.Error != "") }
:class="{'text-error': error}"
>
if data.Error != "" {
{ data.Error }
} else {
Minimum image width resolution to download for this device. Set to '0' to disable this filter. Recommended to set this value same as your device's width resolution
so you will get non-blurry images when used as wallpaper.
}
</span>
</div>
</label>
}
templ MinImageResolutionYInput(data ResolutionData) {
<label
x-data={ fmt.Sprintf(`{error: %t}`, data.Error != "") }
id="min-image-height-form"
class="form-control"
if data.HXSwapOOB {
hx-swap-oob="true"
}
>
<div class="label">
<span
class={ utils.CXX("label-text", true, "text-error", data.Error != "") }
:class="{'text-error': error}"
>Minimum Image Height</span>
</div>
<input
id="min-image-height-field"
x-data={ fmt.Sprintf(`{ init() { $el.setCustomValidity(%q) } }`, data.Error) }
name="min_y"
type="number"
min="0"
@change="$el.setCustomValidity(''); this.error = false"
class={ utils.CXX(
"input input-bordered", true,
"text-error", data.Error != "",
"input-error", data.Error != "",
) }
:class="{'text-error': error, 'input-error': error}"
value={ strconv.Itoa(data.Value) }
placeholder="0"
required
/>
<div class="label">
<span
class={ utils.CXX("label-text", true, "text-error", data.Error != "") }
:class="{'text-error': error}"
>
if data.Error != "" {
{ data.Error }
} else {
Minimum image height resolution to download for this device. Set to '0' to disable this filter. Recommended to set this value same as your device's height resolution
so you will get non-blurry images when used as wallpaper.
}
</span>
</div>
</label>
}

View file

@ -0,0 +1,109 @@
package adddevice
import "github.com/tigorlazuardi/redmage/views/utils"
import "fmt"
import "strconv"
type ResolutionData struct {
Error string
Value int
HXSwapOOB bool
}
templ ResolutionXInput(data ResolutionData) {
<label
x-data={ fmt.Sprintf(`{error: %t}`, data.Error != "") }
id="resolution-x-form"
class="form-control"
if data.HXSwapOOB {
hx-swap-oob="true"
}
>
<div class="label">
<span
class={ utils.CXX("label-text", true, "text-error", data.Error != "") }
:class="{'text-error': error}"
>Width</span>
</div>
<input
id="resolution-x-field"
x-data={ fmt.Sprintf(`{ init() { $el.setCustomValidity(%q) } }`, data.Error) }
name="resolution_x"
type="number"
min="1"
@change="$el.setCustomValidity(''); this.error = false"
class={ utils.CXX(
"input input-bordered", true,
"text-error", data.Error != "",
"input-error", data.Error != "",
) }
:class="{'text-error': error, 'input-error': error}"
if data.Value > 0 {
value={ strconv.Itoa(data.Value) }
}
placeholder="1920"
required
/>
<div class="label">
<span
class={ utils.CXX("label-text", true, "text-error", data.Error != "") }
:class="{'text-error': error}"
>
if data.Error != "" {
{ data.Error }
} else {
Your intended device width resolution.
}
</span>
</div>
</label>
}
templ ResolutionYInput(data ResolutionData) {
<label
x-data={ fmt.Sprintf(`{error: %t}`, data.Error != "") }
id="resolution-y-form"
class="form-control"
if data.HXSwapOOB {
hx-swap-oob="true"
}
>
<div class="label">
<span
class={ utils.CXX("label-text", true, "text-error", data.Error != "") }
:class="{'text-error': error}"
>Height</span>
</div>
<input
id="resolution-y-field"
x-data={ fmt.Sprintf(`{ init() { $el.setCustomValidity(%q) } }`, data.Error) }
name="resolution_y"
type="number"
min="1"
@change="$el.setCustomValidity(''); this.error = false"
class={ utils.CXX(
"input input-bordered", true,
"text-error", data.Error != "",
"input-error", data.Error != "",
) }
:class="{'text-error': error, 'input-error': error}"
if data.Value > 0 {
value={ strconv.Itoa(data.Value) }
}
placeholder="1080"
required
/>
<div class="label">
<span
class={ utils.CXX("label-text", true, "text-error", data.Error != "") }
:class="{'text-error': error}"
>
if data.Error != "" {
{ data.Error }
} else {
Your intended device height resolution.
}
</span>
</div>
</label>
}

View file

@ -11,7 +11,13 @@ type SlugInputData struct {
} }
templ SlugInput(data SlugInputData) { templ SlugInput(data SlugInputData) {
<label id="slug-input-form" class="form-control"> <label
id="slug-input-form"
class="form-control"
if data.HXSwapOOB {
hx-swap-oob="true"
}
>
<div class="label"> <div class="label">
<span <span
class={ utils.CXX( class={ utils.CXX(
@ -35,12 +41,15 @@ templ SlugInput(data SlugInputData) {
"input-success", data.Valid != "", "input-success", data.Valid != "",
) } ) }
hx-post="/htmx/devices/add/validate/slug" hx-post="/htmx/devices/add/validate/slug"
hx-trigger="change, input delay:200ms" hx-trigger="change, input delay:2s"
hx-target="#slug-input-form" hx-target="#slug-input-form"
hx-target-409="#slug-input-form" hx-target-409="#slug-input-form"
hx-swap="outerHTML" hx-swap="outerHTML"
value={ data.Value } value={ data.Value }
placeholder="my-awesome-device" placeholder="my-awesome-device"
title="Url Friendly Characters Only"
pattern="^[a-z0-9-_]+$"
required
/> />
<div class="label"> <div class="label">
<span class={ utils.CXX("label-text", true, "text-error", data.Error != "", "text-success", data.Valid != "") }> <span class={ utils.CXX("label-text", true, "text-error", data.Error != "", "text-success", data.Valid != "") }>
@ -50,7 +59,7 @@ templ SlugInput(data SlugInputData) {
{ data.Error } { data.Error }
} else { } else {
Unique identifier for the device. Unique identifier for the device.
Value must be lowercase english alphabet and supported separator is only 'dash' (-). Value must be lowercase english alphabet and supported separator is only 'dash' (-) and 'underscores' (_).
} }
</span> </span>
</div> </div>

View file

@ -17,9 +17,23 @@ templ Content(c *views.Context) {
@components.Container() { @components.Container() {
<h1>Add Device</h1> <h1>Add Device</h1>
<div class="divider"></div> <div class="divider"></div>
<form class="grid sm:grid-cols-2 gap-4"> <form
method="post"
hx-post="/htmx/devices/add"
action="/htmx/devices/add"
class="grid sm:grid-cols-2 gap-4"
>
@NameInput(NameInputData{}) @NameInput(NameInputData{})
@SlugInput(SlugInputData{}) @SlugInput(SlugInputData{})
@ResolutionXInput(ResolutionData{})
@ResolutionYInput(ResolutionData{})
<div class="divider my-auto sm:col-span-2"><h3 class="m-0 p-0">Filter</h3></div>
@AspectRatioToleranceInput(AspectRatioToleranceData{Value: 0.2})
@MinImageResolutionXInput(ResolutionData{})
@MinImageResolutionYInput(ResolutionData{})
@MaxImageResolutionXInput(ResolutionData{})
@MaxImageResolutionYInput(ResolutionData{})
<button type="submit" class="btn btn-primary sm:col-span-2">Add</button>
</form> </form>
} }
</main> </main>

View file

@ -36,16 +36,8 @@ templ filter(data Data) {
<label for="status">Status</label> <label for="status">Status</label>
<select id="status" name="status" class="select select-bordered w-full"> <select id="status" name="status" class="select select-bordered w-full">
<option value="">*No Filter</option> <option value="">*No Filter</option>
if data.Params.Status == 2 { <option value="2" selected?={ data.Params.Status == 2 }>Enabled</option>
<option selected value="2">Enabled</option> <option value="1" selected?={ data.Params.Status == 1 }>Disabled</option>
} else {
<option value="2">Enabled</option>
}
if data.Params.Status == 1 {
<option selected value="1">Disabled</option>
} else {
<option value="1">Disabled</option>
}
</select> </select>
<label for="limit">Limit</label> <label for="limit">Limit</label>
<select id="limit" name="limit" class="select select-bordered w-full"> <select id="limit" name="limit" class="select select-bordered w-full">