summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--static/response-targets.js130
-rw-r--r--static/style.css3
-rw-r--r--timer.go85
-rw-r--r--view/main.templ3
-rw-r--r--view/main_templ.go2
-rw-r--r--view/timers_list.templ39
-rw-r--r--view/timers_list_templ.go99
7 files changed, 331 insertions, 30 deletions
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) {
<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>
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("<!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
}
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) {
- <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) {
- <h1>Timers</h1>
- for _, t := range timers {
- @timer(t)
- }
+ <div class="timers-list">
+ <h1>Timers</h1>
+ for _, t := range timers {
+ @timer(t)
+ }
+ <h4>Create timer</h4>
+ @TimerCreateForm("", "")
+ </div>
}
+
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("<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)
}