Timer create & delete

This commit is contained in:
2024-04-11 00:19:44 +02:00
parent 49f7be2a35
commit 5e12dabced
7 changed files with 331 additions and 30 deletions

130
static/response-targets.js Normal file
View 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;
}
}
});
})();

View File

@ -6,3 +6,6 @@ body {
font-family: sans-serif; font-family: sans-serif;
} }
.error {
color: red;
}

View File

@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"database/sql" "database/sql"
"context" "context"
"strings"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
@ -12,6 +13,14 @@ import (
"stevenlr.com/timer/model" "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 { func initializeDatabase(db *sql.DB) error {
tx, err := db.Begin() tx, err := db.Begin()
if err != nil { return err } if err != nil { return err }
@ -28,16 +37,10 @@ func initializeDatabase(db *sql.DB) error {
`) `)
if err != nil { return err } if err != nil { return err }
now := model.MakeTimeNow() err = insertTimer(tx, "My timer")
id := model.MakeUUID()
_, err = tx.Exec(`
INSERT INTO Timer VALUES ($1, $2, $3, $4)`, id, "My timer", now, now);
if err != nil { return err } if err != nil { return err }
id = model.MakeUUID() err = insertTimer(tx, "My timer2")
_, err = tx.Exec(`
INSERT INTO Timer VALUES ($1, $2, $3, $4)`, id, "My timer 2", now, now);
if err != nil { return err } if err != nil { return err }
return tx.Commit() return tx.Commit()
@ -72,16 +75,27 @@ func queryTimer(db *sql.DB, idStr string) *model.Timer {
return nil 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 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) w.WriteHeader(http.StatusNotFound)
view.Main(view.Error404()).Render(context.Background(), w) 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 == "/" { if r.URL.Path == "/" {
timers := queryAllTimers(server.db) timers := queryAllTimers(server.db)
view.Main(view.TimersList(timers)).Render(context.Background(), w) 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")) timer := queryTimer(server.db, r.PathValue("timerId"))
if timer != nil { if timer != nil {
view.Main(view.TimerView(*timer)).Render(context.Background(), w) 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() { func main() {
log.Println("Starting...") log.Println("Starting...")
@ -112,13 +163,15 @@ func main() {
log.Fatalln(err) log.Fatalln(err)
} }
myServer := myServer{ db: db } myServer := MyServer{ db: db }
fs := http.FileServer(http.Dir("static/")) 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("GET /timer/{timerId}", myServer.handleTimer)
http.HandleFunc("/", myServer.handleMain) http.HandleFunc("DELETE /timer/{timerId}", myServer.handleDeleteTimer)
http.HandleFunc("PUT /timer", myServer.handlePutTimer)
http.HandleFunc("GET /", myServer.handleMain)
log.Println("Started!") log.Println("Started!")
http.ListenAndServe("0.0.0.0:80", nil) http.ListenAndServe("0.0.0.0:80", nil)

View File

@ -7,8 +7,9 @@ templ Main(contents templ.Component) {
<title>Cool timer app</title> <title>Cool timer app</title>
<link rel="stylesheet" href="/static/style.css" /> <link rel="stylesheet" href="/static/style.css" />
<script src="/static/htmx.min.js"></script> <script src="/static/htmx.min.js"></script>
<script src="/static/response-targets.js"></script>
</head> </head>
<body hx-boost="true"> <body hx-boost="true" hx-ext="response-targets">
@contents @contents
</body> </body>
</html> </html>

View File

@ -23,7 +23,7 @@ func Main(contents templ.Component) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@ -6,12 +6,41 @@ import (
) )
templ timer(t model.Timer) { 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) { templ TimersList(timers []model.Timer) {
<div class="timers-list">
<h1>Timers</h1> <h1>Timers</h1>
for _, t := range timers { for _, t := range timers {
@timer(t) @timer(t)
} }
<h4>Create timer</h4>
@TimerCreateForm("", "")
</div>
} }

View File

@ -28,7 +28,7 @@ func timer(t model.Timer) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -44,13 +44,86 @@ func timer(t model.Timer) templ.Component {
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(t.Name) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(t.Name)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -69,12 +142,12 @@ func TimersList(timers []model.Timer) templ.Component {
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx) templ_7745c5c3_Var8 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil { if templ_7745c5c3_Var8 == nil {
templ_7745c5c3_Var4 = templ.NopComponent templ_7745c5c3_Var8 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -84,6 +157,18 @@ func TimersList(timers []model.Timer) templ.Component {
return templ_7745c5c3_Err 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 { if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
} }