From 5e12dabced6ce55031f5c7f13afb15048d03edcd Mon Sep 17 00:00:00 2001
From: Steven Le Rouzic
Date: Thu, 11 Apr 2024 00:19:44 +0200
Subject: Timer create & delete
---
static/response-targets.js | 130 +++++++++++++++++++++++++++++++++++++++++++++
static/style.css | 3 ++
timer.go | 85 +++++++++++++++++++++++------
view/main.templ | 3 +-
view/main_templ.go | 2 +-
view/timers_list.templ | 39 ++++++++++++--
view/timers_list_templ.go | 99 +++++++++++++++++++++++++++++++---
7 files changed, 331 insertions(+), 30 deletions(-)
create mode 100644 static/response-targets.js
diff --git a/static/response-targets.js b/static/response-targets.js
new file mode 100644
index 0000000..dd6fd41
--- /dev/null
+++ b/static/response-targets.js
@@ -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;
+ }
+ }
+ });
+})();
diff --git a/static/style.css b/static/style.css
index 63193d7..17df2b0 100644
--- a/static/style.css
+++ b/static/style.css
@@ -6,3 +6,6 @@ body {
font-family: sans-serif;
}
+.error {
+ color: red;
+}
diff --git a/timer.go b/timer.go
index 9b2280b..d5e0a67 100644
--- a/timer.go
+++ b/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)
diff --git a/view/main.templ b/view/main.templ
index 1a4bf95..b6d94d8 100644
--- a/view/main.templ
+++ b/view/main.templ
@@ -7,8 +7,9 @@ templ Main(contents templ.Component) {
Cool timer app
+
-
+
@contents
diff --git a/view/main_templ.go b/view/main_templ.go
index 363061e..109be9c 100644
--- a/view/main_templ.go
+++ b/view/main_templ.go
@@ -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("Cool timer app")
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Cool timer app")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/view/timers_list.templ b/view/timers_list.templ
index e1731d4..2d71a52 100644
--- a/view/timers_list.templ
+++ b/view/timers_list.templ
@@ -6,12 +6,41 @@ import (
)
templ timer(t model.Timer) {
- { t.Name }
+
+ { t.Name }
+ -
+ Delete
+
+}
+
+templ TimerCreateForm(timerName string, err string) {
+
}
templ TimersList(timers []model.Timer) {
- Timers
- for _, t := range timers {
- @timer(t)
- }
+
+
Timers
+ for _, t := range timers {
+ @timer(t)
+ }
+ Create timer
+ @TimerCreateForm("", "")
+
}
+
diff --git a/view/timers_list_templ.go b/view/timers_list_templ.go
index cbfe2eb..af4922d 100644
--- a/view/timers_list_templ.go
+++ b/view/timers_list_templ.go
@@ -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("
")
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" -\r Delete")
+ 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("")
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("Timers
")
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Timers
")
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("Create timer
")
+ 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("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
--
cgit