Timer create & delete
This commit is contained in:
130
static/response-targets.js
Normal file
130
static/response-targets.js
Normal file
@ -0,0 +1,130 @@
|
||||
(function(){
|
||||
|
||||
/** @type {import("../htmx").HtmxInternalApi} */
|
||||
var api;
|
||||
|
||||
var attrPrefix = 'hx-target-';
|
||||
|
||||
// IE11 doesn't support string.startsWith
|
||||
function startsWith(str, prefix) {
|
||||
return str.substring(0, prefix.length) === prefix
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elt
|
||||
* @param {number} respCode
|
||||
* @returns {HTMLElement | null}
|
||||
*/
|
||||
function getRespCodeTarget(elt, respCodeNumber) {
|
||||
if (!elt || !respCodeNumber) return null;
|
||||
|
||||
var respCode = respCodeNumber.toString();
|
||||
|
||||
// '*' is the original syntax, as the obvious character for a wildcard.
|
||||
// The 'x' alternative was added for maximum compatibility with HTML
|
||||
// templating engines, due to ambiguity around which characters are
|
||||
// supported in HTML attributes.
|
||||
//
|
||||
// Start with the most specific possible attribute and generalize from
|
||||
// there.
|
||||
var attrPossibilities = [
|
||||
respCode,
|
||||
|
||||
respCode.substr(0, 2) + '*',
|
||||
respCode.substr(0, 2) + 'x',
|
||||
|
||||
respCode.substr(0, 1) + '*',
|
||||
respCode.substr(0, 1) + 'x',
|
||||
respCode.substr(0, 1) + '**',
|
||||
respCode.substr(0, 1) + 'xx',
|
||||
|
||||
'*',
|
||||
'x',
|
||||
'***',
|
||||
'xxx',
|
||||
];
|
||||
if (startsWith(respCode, '4') || startsWith(respCode, '5')) {
|
||||
attrPossibilities.push('error');
|
||||
}
|
||||
|
||||
for (var i = 0; i < attrPossibilities.length; i++) {
|
||||
var attr = attrPrefix + attrPossibilities[i];
|
||||
var attrValue = api.getClosestAttributeValue(elt, attr);
|
||||
if (attrValue) {
|
||||
if (attrValue === "this") {
|
||||
return api.findThisElement(elt, attr);
|
||||
} else {
|
||||
return api.querySelectorExt(elt, attrValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @param {Event} evt */
|
||||
function handleErrorFlag(evt) {
|
||||
if (evt.detail.isError) {
|
||||
if (htmx.config.responseTargetUnsetsError) {
|
||||
evt.detail.isError = false;
|
||||
}
|
||||
} else if (htmx.config.responseTargetSetsError) {
|
||||
evt.detail.isError = true;
|
||||
}
|
||||
}
|
||||
|
||||
htmx.defineExtension('response-targets', {
|
||||
|
||||
/** @param {import("../htmx").HtmxInternalApi} apiRef */
|
||||
init: function (apiRef) {
|
||||
api = apiRef;
|
||||
|
||||
if (htmx.config.responseTargetUnsetsError === undefined) {
|
||||
htmx.config.responseTargetUnsetsError = true;
|
||||
}
|
||||
if (htmx.config.responseTargetSetsError === undefined) {
|
||||
htmx.config.responseTargetSetsError = false;
|
||||
}
|
||||
if (htmx.config.responseTargetPrefersExisting === undefined) {
|
||||
htmx.config.responseTargetPrefersExisting = false;
|
||||
}
|
||||
if (htmx.config.responseTargetPrefersRetargetHeader === undefined) {
|
||||
htmx.config.responseTargetPrefersRetargetHeader = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Event} evt
|
||||
*/
|
||||
onEvent: function (name, evt) {
|
||||
if (name === "htmx:beforeSwap" &&
|
||||
evt.detail.xhr &&
|
||||
evt.detail.xhr.status !== 200) {
|
||||
if (evt.detail.target) {
|
||||
if (htmx.config.responseTargetPrefersExisting) {
|
||||
evt.detail.shouldSwap = true;
|
||||
handleErrorFlag(evt);
|
||||
return true;
|
||||
}
|
||||
if (htmx.config.responseTargetPrefersRetargetHeader &&
|
||||
evt.detail.xhr.getAllResponseHeaders().match(/HX-Retarget:/i)) {
|
||||
evt.detail.shouldSwap = true;
|
||||
handleErrorFlag(evt);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!evt.detail.requestConfig) {
|
||||
return true;
|
||||
}
|
||||
var target = getRespCodeTarget(evt.detail.requestConfig.elt, evt.detail.xhr.status);
|
||||
if (target) {
|
||||
handleErrorFlag(evt);
|
||||
evt.detail.shouldSwap = true;
|
||||
evt.detail.target = target;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
@ -6,3 +6,6 @@ body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
85
timer.go
85
timer.go
@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"database/sql"
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
@ -12,6 +13,14 @@ import (
|
||||
"stevenlr.com/timer/model"
|
||||
)
|
||||
|
||||
func insertTimer(tx *sql.Tx, name string) error {
|
||||
now := model.MakeTimeNow()
|
||||
id := model.MakeUUID()
|
||||
_, err := tx.Exec(`
|
||||
INSERT INTO Timer VALUES ($1, $2, $3, $4)`, id, name, now, now);
|
||||
return err
|
||||
}
|
||||
|
||||
func initializeDatabase(db *sql.DB) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil { return err }
|
||||
@ -28,16 +37,10 @@ func initializeDatabase(db *sql.DB) error {
|
||||
`)
|
||||
if err != nil { return err }
|
||||
|
||||
now := model.MakeTimeNow()
|
||||
|
||||
id := model.MakeUUID()
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO Timer VALUES ($1, $2, $3, $4)`, id, "My timer", now, now);
|
||||
err = insertTimer(tx, "My timer")
|
||||
if err != nil { return err }
|
||||
|
||||
id = model.MakeUUID()
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO Timer VALUES ($1, $2, $3, $4)`, id, "My timer 2", now, now);
|
||||
err = insertTimer(tx, "My timer2")
|
||||
if err != nil { return err }
|
||||
|
||||
return tx.Commit()
|
||||
@ -72,16 +75,27 @@ func queryTimer(db *sql.DB, idStr string) *model.Timer {
|
||||
return nil
|
||||
}
|
||||
|
||||
type myServer struct {
|
||||
func deleteTimer(db *sql.DB, idStr string) bool {
|
||||
var id model.UUID
|
||||
if err := id.Scan(idStr); err != nil { return false }
|
||||
|
||||
res, err := db.Exec("DELETE FROM Timer WHERE Id=$1", id)
|
||||
if err != nil { return false }
|
||||
|
||||
affected, err := res.RowsAffected()
|
||||
return err == nil && affected > 0
|
||||
}
|
||||
|
||||
type MyServer struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (server *myServer) handleNotFound(w http.ResponseWriter, _ *http.Request) {
|
||||
func (server *MyServer) handleNotFound(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
view.Main(view.Error404()).Render(context.Background(), w)
|
||||
}
|
||||
|
||||
func (server *myServer) handleMain(w http.ResponseWriter, r *http.Request) {
|
||||
func (server *MyServer) handleMain(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/" {
|
||||
timers := queryAllTimers(server.db)
|
||||
view.Main(view.TimersList(timers)).Render(context.Background(), w)
|
||||
@ -90,7 +104,7 @@ func (server *myServer) handleMain(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (server *myServer) handleTimer(w http.ResponseWriter, r *http.Request) {
|
||||
func (server *MyServer) handleTimer(w http.ResponseWriter, r *http.Request) {
|
||||
timer := queryTimer(server.db, r.PathValue("timerId"))
|
||||
if timer != nil {
|
||||
view.Main(view.TimerView(*timer)).Render(context.Background(), w)
|
||||
@ -99,6 +113,43 @@ func (server *myServer) handleTimer(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (server *MyServer) handleDeleteTimer(w http.ResponseWriter, r *http.Request) {
|
||||
success := deleteTimer(server.db, r.PathValue("timerId"))
|
||||
if !success {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func (server* MyServer) handlePutTimer(w http.ResponseWriter, r *http.Request) {
|
||||
timerName := strings.TrimSpace(r.FormValue("timerName"))
|
||||
|
||||
tx, err := server.db.Begin()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
view.TimerCreateForm(timerName, "Internal server error").Render(context.Background(), w)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if timerName == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
view.TimerCreateForm("", "Timer name cannot be empty").Render(context.Background(), w)
|
||||
return
|
||||
}
|
||||
|
||||
err = insertTimer(tx, timerName)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
view.TimerCreateForm(timerName, "Internal server error").Render(context.Background(), w)
|
||||
return
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
timers := queryAllTimers(server.db)
|
||||
view.TimersList(timers).Render(context.Background(), w)
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.Println("Starting...")
|
||||
|
||||
@ -112,13 +163,15 @@ func main() {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
myServer := myServer{ db: db }
|
||||
myServer := MyServer{ db: db }
|
||||
|
||||
fs := http.FileServer(http.Dir("static/"))
|
||||
http.Handle("/static/", http.StripPrefix("/static/", fs))
|
||||
http.Handle("GET /static/", http.StripPrefix("/static/", fs))
|
||||
|
||||
http.HandleFunc("/timer/{timerId}", myServer.handleTimer)
|
||||
http.HandleFunc("/", myServer.handleMain)
|
||||
http.HandleFunc("GET /timer/{timerId}", myServer.handleTimer)
|
||||
http.HandleFunc("DELETE /timer/{timerId}", myServer.handleDeleteTimer)
|
||||
http.HandleFunc("PUT /timer", myServer.handlePutTimer)
|
||||
http.HandleFunc("GET /", myServer.handleMain)
|
||||
|
||||
log.Println("Started!")
|
||||
http.ListenAndServe("0.0.0.0:80", nil)
|
||||
|
@ -7,8 +7,9 @@ templ Main(contents templ.Component) {
|
||||
<title>Cool timer app</title>
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/response-targets.js"></script>
|
||||
</head>
|
||||
<body hx-boost="true">
|
||||
<body hx-boost="true" hx-ext="response-targets">
|
||||
@contents
|
||||
</body>
|
||||
</html>
|
||||
|
@ -23,7 +23,7 @@ func Main(contents templ.Component) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html><head><title>Cool timer app</title><link rel=\"stylesheet\" href=\"/static/style.css\"><script src=\"/static/htmx.min.js\"></script></head><body hx-boost=\"true\">")
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html><head><title>Cool timer app</title><link rel=\"stylesheet\" href=\"/static/style.css\"><script src=\"/static/htmx.min.js\"></script><script src=\"/static/response-targets.js\"></script></head><body hx-boost=\"true\" hx-ext=\"response-targets\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
@ -6,12 +6,41 @@ import (
|
||||
)
|
||||
|
||||
templ timer(t model.Timer) {
|
||||
<p><a href={ templ.URL(fmt.Sprint("/timer/", t.Id)) }>{ t.Name }</a></p>
|
||||
<p class="timer-row">
|
||||
<a href={ templ.URL(fmt.Sprint("/timer/", t.Id)) }>{ t.Name }</a>
|
||||
-
|
||||
<a
|
||||
href="javascript:void(0);"
|
||||
hx-delete={ fmt.Sprint("/timer/", t.Id) }
|
||||
hx-target="closest .timer-row"
|
||||
>Delete</a>
|
||||
</p>
|
||||
}
|
||||
|
||||
templ TimerCreateForm(timerName string, err string) {
|
||||
<form
|
||||
hx-put="/timer"
|
||||
hx-target="closest .timers-list"
|
||||
hx-target-error="this"
|
||||
>
|
||||
<p>
|
||||
<input type="text" name="timerName" value={ timerName } placeholder="Name" />
|
||||
<button type="submit">Create</button>
|
||||
</p>
|
||||
if err != "" {
|
||||
<p class="error">{ err }</p>
|
||||
}
|
||||
</form>
|
||||
}
|
||||
|
||||
templ TimersList(timers []model.Timer) {
|
||||
<div class="timers-list">
|
||||
<h1>Timers</h1>
|
||||
for _, t := range timers {
|
||||
@timer(t)
|
||||
}
|
||||
<h4>Create timer</h4>
|
||||
@TimerCreateForm("", "")
|
||||
</div>
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ func timer(t model.Timer) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p><a href=\"")
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p class=\"timer-row\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -44,13 +44,86 @@ func timer(t model.Timer) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(t.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view\timers_list.templ`, Line: 9, Col: 66}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view\timers_list.templ`, Line: 10, Col: 67}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></p>")
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a> -\r <a href=\"javascript:void(0);\" hx-delete=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint("/timer/", t.Id))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view\timers_list.templ`, Line: 14, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-target=\"closest .timer-row\">Delete</a></p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func TimerCreateForm(timerName string, err string) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var5 == nil {
|
||||
templ_7745c5c3_Var5 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form hx-put=\"/timer\" hx-target=\"closest .timers-list\" hx-target-error=\"this\"><p><input type=\"text\" name=\"timerName\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(timerName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view\timers_list.templ`, Line: 27, Col: 65}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" placeholder=\"Name\"> <button type=\"submit\">Create</button></p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if err != "" {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p class=\"error\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(err)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view\timers_list.templ`, Line: 31, Col: 34}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -69,12 +142,12 @@ func TimersList(timers []model.Timer) templ.Component {
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var4 == nil {
|
||||
templ_7745c5c3_Var4 = templ.NopComponent
|
||||
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var8 == nil {
|
||||
templ_7745c5c3_Var8 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<h1>Timers</h1>")
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"timers-list\"><h1>Timers</h1>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -84,6 +157,18 @@ func TimersList(timers []model.Timer) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<h4>Create timer</h4>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = TimerCreateForm("", "").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
|
Reference in New Issue
Block a user