device: added add page
This commit is contained in:
parent
b8fcb37db8
commit
19ffbdf98c
6
Makefile
6
Makefile
|
@ -9,6 +9,7 @@ export GOOSE_MIGRATION_DIR=db/migrations
|
||||||
|
|
||||||
export REDMAGE_WEB_DEPENDENCIES_HTMX_VERSION=$(shell echo "$${REDMAGE_WEB_DEPENDENCIES_HTMX_VERSION:-1.9.12}")
|
export REDMAGE_WEB_DEPENDENCIES_HTMX_VERSION=$(shell echo "$${REDMAGE_WEB_DEPENDENCIES_HTMX_VERSION:-1.9.12}")
|
||||||
export REDMAGE_WEB_DEPENDENCIES_DAYJS_VERSION=$(shell echo "$${REDMAGE_WEB_DEPENDENCIES_DAYJS_VERSION:-1.11.10}")
|
export REDMAGE_WEB_DEPENDENCIES_DAYJS_VERSION=$(shell echo "$${REDMAGE_WEB_DEPENDENCIES_DAYJS_VERSION:-1.11.10}")
|
||||||
|
export REDMAGE_WEB_DEPENDENCIES_ALPINEJS_VERSION=$(shell echo "$${REDMAGE_WEB_DEPENDENCIES_ALPINEJS_VERSION:-3.13.10}")
|
||||||
|
|
||||||
start: dev-dependencies
|
start: dev-dependencies
|
||||||
@tailwindcss -i views/style.css -o public/style.css --watch &
|
@tailwindcss -i views/style.css -o public/style.css --watch &
|
||||||
|
@ -62,6 +63,11 @@ build-dependencies:
|
||||||
echo "Dayjs Timezone ${REDMAGE_WEB_DEPENDENCIES_DAYJS_VERSION} plugin not found, installing it"
|
echo "Dayjs Timezone ${REDMAGE_WEB_DEPENDENCIES_DAYJS_VERSION} plugin not found, installing it"
|
||||||
curl -o public/dayjs-timezone-${REDMAGE_WEB_DEPENDENCIES_DAYJS_VERSION}.min.js https://cdnjs.cloudflare.com/ajax/libs/dayjs/${REDMAGE_WEB_DEPENDENCIES_DAYJS_VERSION}/plugin/timezone.min.js
|
curl -o public/dayjs-timezone-${REDMAGE_WEB_DEPENDENCIES_DAYJS_VERSION}.min.js https://cdnjs.cloudflare.com/ajax/libs/dayjs/${REDMAGE_WEB_DEPENDENCIES_DAYJS_VERSION}/plugin/timezone.min.js
|
||||||
fi
|
fi
|
||||||
|
@if [ ! -f "public/alpinejs-${REDMAGE_WEB_DEPENDENCIES_ALPINEJS_VERSION}.min.js" ]; then
|
||||||
|
mkdir -p public
|
||||||
|
echo "Alpinejs ${REDMAGE_WEB_DEPENDENCIES_ALPINEJS_VERSION} not found, installing it"
|
||||||
|
curl -o public/alpinejs-${REDMAGE_WEB_DEPENDENCIES_ALPINEJS_VERSION}.min.js https://cdn.jsdelivr.net/npm/alpinejs@${REDMAGE_WEB_DEPENDENCIES_ALPINEJS_VERSION}/dist/cdn.min.js
|
||||||
|
fi
|
||||||
|
|
||||||
build: build-dependencies prepare
|
build: build-dependencies prepare
|
||||||
go build -o redmage
|
go build -o redmage
|
||||||
|
|
19
api/devices_validate_slug.go
Normal file
19
api/devices_validate_slug.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tigorlazuardi/redmage/models"
|
||||||
|
"github.com/tigorlazuardi/redmage/pkg/errs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (api *API) DevicesValidateSlug(ctx context.Context, slug string) (exist bool, err error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "*API.DevicesValidateSlug")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
exist, err = models.Devices.Query(ctx, api.db, models.SelectWhere.Devices.Slug.EQ(slug)).Exists()
|
||||||
|
if err != nil {
|
||||||
|
return exist, errs.Wrapw(err, "failed to check device slug existence", "slug", slug)
|
||||||
|
}
|
||||||
|
return exist, err
|
||||||
|
}
|
49
server/routes/device_validate_slug.go
Normal file
49
server/routes/device_validate_slug.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/tigorlazuardi/redmage/pkg/errs"
|
||||||
|
"github.com/tigorlazuardi/redmage/pkg/log"
|
||||||
|
"github.com/tigorlazuardi/redmage/views/devicesview/adddevice"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (routes *Routes) DevicesValidateSlugHTMX(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
ctx, span := tracer.Start(req.Context(), "*Routes.ValidateSlugHTMX")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
var data adddevice.SlugInputData
|
||||||
|
data.Value = req.FormValue("slug")
|
||||||
|
if data.Value == "" {
|
||||||
|
if err := adddevice.SlugInput(data).Render(ctx, rw); err != nil {
|
||||||
|
log.New(ctx).Err(err).Error("failed to render slug input")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exist, err := routes.API.DevicesValidateSlug(ctx, data.Value)
|
||||||
|
if err != nil {
|
||||||
|
log.New(ctx).Err(err).Error("failed to validate slug")
|
||||||
|
code, message := errs.HTTPMessage(err)
|
||||||
|
rw.WriteHeader(code)
|
||||||
|
data.Error = message
|
||||||
|
if err := adddevice.SlugInput(data).Render(ctx, rw); err != nil {
|
||||||
|
log.New(ctx).Err(err).Error("failed to render slug input")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exist {
|
||||||
|
data.Error = "Device with this identifier already exist"
|
||||||
|
rw.WriteHeader(http.StatusConflict)
|
||||||
|
if err := adddevice.SlugInput(data).Render(ctx, rw); err != nil {
|
||||||
|
log.New(ctx).Err(err).Error("failed to render slug input")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.Valid = "Identifier is available"
|
||||||
|
|
||||||
|
if err := adddevice.SlugInput(data).Render(ctx, rw); err != nil {
|
||||||
|
log.New(ctx).Err(err).Error("failed to render slug input")
|
||||||
|
}
|
||||||
|
}
|
|
@ -62,6 +62,8 @@ func (routes *Routes) registerHTMXRoutes(router chi.Router) {
|
||||||
router.Post("/subreddits/start", routes.SubredditStartDownloadHTMX)
|
router.Post("/subreddits/start", routes.SubredditStartDownloadHTMX)
|
||||||
router.Post("/subreddits/check", routes.SubredditCheckHTMX)
|
router.Post("/subreddits/check", routes.SubredditCheckHTMX)
|
||||||
router.Get("/subreddits/validate/schedule", routes.SubredditValidateScheduleHTMX)
|
router.Get("/subreddits/validate/schedule", routes.SubredditValidateScheduleHTMX)
|
||||||
|
|
||||||
|
router.Post("/devices/add/validate/slug", routes.DevicesValidateSlugHTMX)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (routes *Routes) registerWWWRoutes(router chi.Router) {
|
func (routes *Routes) registerWWWRoutes(router chi.Router) {
|
||||||
|
|
8
views/components/alpine.templ
Normal file
8
views/components/alpine.templ
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package components
|
||||||
|
|
||||||
|
import "github.com/tigorlazuardi/redmage/views"
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
templ AlpineJS(c *views.Context) {
|
||||||
|
<script defer src={ fmt.Sprintf("/public/alpinejs-%s.min.js", c.Config.String("web.dependencies.alpinejs.version")) }></script>
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ templ Head(vc *views.Context, extras ...templ.Component) {
|
||||||
<link rel="icon" href="/public/favicon.svg"/>
|
<link rel="icon" href="/public/favicon.svg"/>
|
||||||
@Dayjs(vc)
|
@Dayjs(vc)
|
||||||
@HTMX(vc)
|
@HTMX(vc)
|
||||||
|
@AlpineJS(vc)
|
||||||
if vc.Config.Bool("http.hotreload") {
|
if vc.Config.Bool("http.hotreload") {
|
||||||
<script src="/public/hot_reload.js"></script>
|
<script src="/public/hot_reload.js"></script>
|
||||||
}
|
}
|
||||||
|
|
39
views/devicesview/adddevice/name_input.templ
Normal file
39
views/devicesview/adddevice/name_input.templ
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package adddevice
|
||||||
|
|
||||||
|
import "github.com/tigorlazuardi/redmage/views/utils"
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type NameInputData struct {
|
||||||
|
Error string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
templ NameInput(data NameInputData) {
|
||||||
|
<label class="form-control">
|
||||||
|
<div class="label">
|
||||||
|
<span
|
||||||
|
class={ utils.CXX("label-text", true, "text-error", data.Error != "") }
|
||||||
|
>Name</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="name-input-field"
|
||||||
|
x-data={ fmt.Sprintf(`{ init() { $el.setCustomValidity(%q) } }`, data.Error) }
|
||||||
|
hx-post="/htmx/devices/add/validate/name"
|
||||||
|
hx-include="[name='slug']"
|
||||||
|
hx-trigger="change"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
class={ utils.CXX("input input-bordered", true, "input-error", data.Error != "") }
|
||||||
|
value={ data.Value }
|
||||||
|
placeholder="My Awesome Device"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div class="label">
|
||||||
|
if data.Error != "" {
|
||||||
|
<span class="label-text text-error">{ data.Error }</span>
|
||||||
|
} else {
|
||||||
|
<span class="label-text">The display name for the device. Use distinguishable value for easy lookup.</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
}
|
58
views/devicesview/adddevice/slug_input.templ
Normal file
58
views/devicesview/adddevice/slug_input.templ
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package adddevice
|
||||||
|
|
||||||
|
import "github.com/tigorlazuardi/redmage/views/utils"
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type SlugInputData struct {
|
||||||
|
Error string
|
||||||
|
Value string
|
||||||
|
Valid string
|
||||||
|
HXSwapOOB bool
|
||||||
|
}
|
||||||
|
|
||||||
|
templ SlugInput(data SlugInputData) {
|
||||||
|
<label id="slug-input-form" class="form-control">
|
||||||
|
<div class="label">
|
||||||
|
<span
|
||||||
|
class={ utils.CXX(
|
||||||
|
"label-text", true,
|
||||||
|
"text-error", data.Error != "",
|
||||||
|
"text-success", data.Valid != "",
|
||||||
|
) }
|
||||||
|
>Slug</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="slug-input-field"
|
||||||
|
x-data={ fmt.Sprintf(`{ init() { $el.setCustomValidity(%q) } }`, data.Error) }
|
||||||
|
name="slug"
|
||||||
|
type="text"
|
||||||
|
@change="$el.setCustomValidity('')"
|
||||||
|
class={ utils.CXX(
|
||||||
|
"input input-bordered", true,
|
||||||
|
"text-error", data.Error != "",
|
||||||
|
"text-success", data.Valid != "",
|
||||||
|
"input-error", data.Error != "",
|
||||||
|
"input-success", data.Valid != "",
|
||||||
|
) }
|
||||||
|
hx-post="/htmx/devices/add/validate/slug"
|
||||||
|
hx-trigger="change, input delay:200ms"
|
||||||
|
hx-target="#slug-input-form"
|
||||||
|
hx-target-409="#slug-input-form"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
value={ data.Value }
|
||||||
|
placeholder="my-awesome-device"
|
||||||
|
/>
|
||||||
|
<div class="label">
|
||||||
|
<span class={ utils.CXX("label-text", true, "text-error", data.Error != "", "text-success", data.Valid != "") }>
|
||||||
|
if data.Valid != "" {
|
||||||
|
{ data.Valid }
|
||||||
|
} else if data.Error != "" {
|
||||||
|
{ data.Error }
|
||||||
|
} else {
|
||||||
|
Unique identifier for the device.
|
||||||
|
Value must be lowercase english alphabet and supported separator is only 'dash' (-).
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
}
|
|
@ -17,6 +17,10 @@ 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">
|
||||||
|
@NameInput(NameInputData{})
|
||||||
|
@SlugInput(SlugInputData{})
|
||||||
|
</form>
|
||||||
}
|
}
|
||||||
</main>
|
</main>
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue