Phase 3: HTMX inline tag editing, swap animations, loading indicators, Alpine phone validation
This commit is contained in:
@@ -52,7 +52,7 @@ func LoadTemplates() (Templates, error) {
|
||||
"index": {dir + "/index.html"},
|
||||
"register": {dir + "/register.html"},
|
||||
"login": {dir + "/login.html"},
|
||||
"account": {dir + "/account.html", dir + "/account-panel.html", dir + "/tag-list.html"},
|
||||
"account": {dir + "/account.html", dir + "/account-panel.html", dir + "/tag-list.html", dir + "/tag-edit-inline.html"},
|
||||
"edit": {dir + "/tag-edit.html"},
|
||||
"public": {dir + "/tag-public.html"},
|
||||
"notfound": {dir + "/not-found.html"},
|
||||
|
||||
@@ -87,6 +87,16 @@ func (a *App) EditTagPage(w http.ResponseWriter, r *http.Request) {
|
||||
a.render(w, r, "edit", "Edit tag", tag, "")
|
||||
}
|
||||
|
||||
// EditInlinePage returns the compact inline edit form for HTMX row editing.
|
||||
func (a *App) EditInlinePage(w http.ResponseWriter, r *http.Request) {
|
||||
uid, _ := auth.GetUserID(r)
|
||||
tag, ok := a.loadOwnedTag(w, r, uid)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
a.renderPartial(w, "account", "tag-edit-inline", tag)
|
||||
}
|
||||
|
||||
func (a *App) EditTag(w http.ResponseWriter, r *http.Request) {
|
||||
uid, _ := auth.GetUserID(r)
|
||||
tag, ok := a.loadOwnedTag(w, r, uid)
|
||||
@@ -112,6 +122,16 @@ func (a *App) EditTag(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// If this was an HTMX inline edit, return the refreshed account panel.
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
tags, err := a.Queries.ListTagsByOwner(r.Context(), ownerID(uid))
|
||||
if err != nil {
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
a.renderPartial(w, "account", "account-panel", accountData{Tags: tags})
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/account", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ func main() {
|
||||
mux.Handle("GET /account", app.RequireAuth(app.Account))
|
||||
mux.Handle("POST /account/tags", app.RequireAuth(app.AddTag))
|
||||
mux.Handle("GET /account/tags/{id}/edit", app.RequireAuth(app.EditTagPage))
|
||||
mux.Handle("GET /account/tags/{id}/edit/inline", app.RequireAuth(app.EditInlinePage))
|
||||
mux.Handle("POST /account/tags/{id}/edit", app.RequireAuth(app.EditTag))
|
||||
mux.Handle("POST /account/tags/{id}/delete", app.RequireAuth(app.DeleteTag))
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<form hx-post="/account/tags" hx-target="#account-panel" hx-swap="outerHTML" class="flex gap-2">
|
||||
<input name="tag_code" type="text" placeholder="Enter tag code (e.g. TEST000001)" class="flex-1 rounded border border-stone-300 px-3 py-2 text-sm" required>
|
||||
<button type="submit" class="rounded bg-stone-900 px-4 py-2 text-sm font-semibold text-white hover:bg-stone-700">Add tag</button>
|
||||
<span class="htmx-indicator self-center text-xs text-stone-400">Adding…</span>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,6 +8,15 @@
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<style>
|
||||
/* HTMX swap/settle animations */
|
||||
.htmx-swapping { opacity: 0; transition: opacity 150ms ease-out; }
|
||||
.htmx-settling { opacity: 0; transition: opacity 200ms ease-in; }
|
||||
.htmx-settling.show { opacity: 1; }
|
||||
.fade-row { transition: opacity 200ms ease-out; }
|
||||
.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { opacity: 1; }
|
||||
.htmx-indicator { opacity: 0; transition: opacity 150ms ease-in; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-full bg-stone-100 text-stone-900 antialiased">
|
||||
<nav class="bg-stone-900 text-white">
|
||||
|
||||
45
frontend/templates/tag-edit-inline.html
Normal file
45
frontend/templates/tag-edit-inline.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{{define "tag-edit-inline"}}
|
||||
<tr class="border-t border-stone-100 bg-amber-50/50">
|
||||
<td colspan="4" class="px-4 py-3">
|
||||
<form hx-post="/account/tags/{{.ID}}/edit" hx-target="#account-panel" hx-swap="outerHTML" class="grid grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-stone-600">Item type</label>
|
||||
<select name="item_type" class="mt-0.5 w-full rounded border border-stone-300 px-2 py-1">
|
||||
<option value="">Select…</option>
|
||||
<option value="dog" {{if eq .ItemType.String "dog"}}selected{{end}}>Dog</option>
|
||||
<option value="cat" {{if eq .ItemType.String "cat"}}selected{{end}}>Cat</option>
|
||||
<option value="baggage" {{if eq .ItemType.String "baggage"}}selected{{end}}>Baggage</option>
|
||||
<option value="skis" {{if eq .ItemType.String "skis"}}selected{{end}}>Skis</option>
|
||||
<option value="other" {{if eq .ItemType.String "other"}}selected{{end}}>Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-stone-600">Description</label>
|
||||
<input name="description" type="text" value="{{if .Description.Valid}}{{.Description.String}}{{end}}" class="mt-0.5 w-full rounded border border-stone-300 px-2 py-1" placeholder="e.g. Shadow">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-stone-600">Phone</label>
|
||||
<input name="phone" type="tel" value="{{if .Phone.Valid}}{{.Phone.String}}{{end}}" class="mt-0.5 w-full rounded border border-stone-300 px-2 py-1" placeholder="+61…">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-stone-600">Address</label>
|
||||
<input name="address" type="text" value="{{if .Address.Valid}}{{.Address.String}}{{end}}" class="mt-0.5 w-full rounded border border-stone-300 px-2 py-1">
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs font-medium text-stone-600">Notes</label>
|
||||
<textarea name="notes" rows="1" class="mt-0.5 w-full rounded border border-stone-300 px-2 py-1">{{if .Notes.Valid}}{{.Notes.String}}{{end}}</textarea>
|
||||
</div>
|
||||
<div class="col-span-2 flex items-center gap-3">
|
||||
<label class="flex items-center gap-1.5 text-xs">
|
||||
<input type="checkbox" name="sms_enabled" value="on" {{if .SmsEnabled}}checked{{end}} class="h-3.5 w-3.5">
|
||||
SMS alerts
|
||||
</label>
|
||||
<div class="flex-1"></div>
|
||||
<span class="htmx-indicator text-xs text-stone-400">Saving…</span>
|
||||
<button type="submit" class="rounded bg-stone-900 px-3 py-1 text-xs font-semibold text-white hover:bg-stone-700">Save</button>
|
||||
<button type="button" hx-get="/account" hx-target="#account-panel" hx-swap="outerHTML" class="rounded border border-stone-300 px-3 py-1 text-xs hover:bg-stone-50">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
@@ -21,8 +21,8 @@
|
||||
</td>
|
||||
<td class="px-4 py-2">{{if .ItemType.Valid}}{{.ItemType.String}}{{else}}—{{end}}</td>
|
||||
<td class="px-4 py-2 text-right">
|
||||
<a href="/account/tags/{{.ID}}/edit" class="text-amber-600 hover:underline">Edit</a>
|
||||
<form hx-post="/account/tags/{{.ID}}/delete" hx-target="#account-panel" hx-swap="outerHTML" hx-confirm="Remove this tag from your account?" class="inline">
|
||||
<a href="/account/tags/{{.ID}}/edit" hx-get="/account/tags/{{.ID}}/edit/inline" hx-target="closest tr" hx-swap="outerHTML" class="text-amber-600 hover:underline">Edit</a>
|
||||
<form hx-post="/account/tags/{{.ID}}/delete" hx-target="#account-panel" hx-swap="outerHTML" hx-confirm="Remove this tag from your account?" class="inline fade-row">
|
||||
<button class="ml-3 text-red-600 hover:underline">Remove</button>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
@@ -61,12 +61,14 @@
|
||||
<button id="recheck-btn" type="button" class="mt-2 rounded bg-amber-500 px-3 py-1.5 text-xs font-semibold text-stone-900 hover:bg-amber-400">📍 Re-check my location</button>
|
||||
</div>
|
||||
|
||||
<div id="contact-area" class="mt-4">
|
||||
<form hx-post="/t/{{.Data.TagCode}}/contact" hx-target="#contact-area" hx-swap="innerHTML" class="flex gap-2">
|
||||
<div id="contact-area" class="mt-4" x-data="{ phone: '', get valid() { return /^[0-9+ ]{8,16}$/.test(this.phone.trim()); } }">
|
||||
<form hx-post="/t/{{.Data.TagCode}}/contact" hx-target="#contact-area" hx-swap="innerHTML" class="flex gap-2" @submit.prevent="if(!valid) return; $el.requestSubmit()">
|
||||
<input type="hidden" name="fingerprint" value="">
|
||||
<input name="phone" type="tel" placeholder="Leave your number for the owner (optional)" class="flex-1 rounded border border-stone-300 px-3 py-2 text-sm" pattern="[0-9+ ]{8,16}">
|
||||
<button type="submit" class="rounded bg-stone-900 px-4 py-2 text-sm font-semibold text-white hover:bg-stone-700">Send number</button>
|
||||
<input name="phone" type="tel" placeholder="Leave your number for the owner (optional)" class="flex-1 rounded border px-3 py-2 text-sm" :class="phone && !valid ? 'border-red-400' : 'border-stone-300'" pattern="[0-9+ ]{8,16}" x-model="phone">
|
||||
<button type="submit" class="rounded bg-stone-900 px-4 py-2 text-sm font-semibold text-white hover:bg-stone-700" :disabled="phone && !valid">Send number</button>
|
||||
<span class="htmx-indicator self-center text-xs text-stone-400">Sending…</span>
|
||||
</form>
|
||||
<p x-show="phone && !valid" class="mt-1 text-xs text-red-600">Enter a valid phone number (8–16 digits, e.g. 0423 274 487).</p>
|
||||
</div>
|
||||
|
||||
{{if .Data.IsOwner}}
|
||||
|
||||
4
todo.txt
4
todo.txt
@@ -6,6 +6,6 @@
|
||||
(F) Billing phase (Stripe AU, plans, scan limits) +project:where-woof +agent:where_woof
|
||||
(G) Deploy front-end to .13 + Caddy + DNS + InMotion redirect +project:where-woof +agent:where_woof
|
||||
x 2026-08-05 Set up Gitea for where_woof +project:where-woof +agent:where_woof
|
||||
(B) Scan-flow: re-alert when different finder phone within 250 m window +project:where-woof +agent:right-monitor-pi-coding
|
||||
(B) Scan-flow: browser fingerprint + 24 h block, store in DB +project:where-woof +agent:right-monitor-pi-coding
|
||||
x 2026-08-07 (B) Scan-flow: re-alert when different finder phone within 250 m window +project:where-woof +agent:right-monitor-pi-coding
|
||||
x 2026-08-07 (B) Scan-flow: browser fingerprint + 24 h block, store in DB +project:where-woof +agent:right-monitor-pi-coding
|
||||
x 2026-08-07 (B) Preset tag-ID registry: seed real manufactured IDs, enforce registry-only (anti-scam) +project:where-woof +agent:right-monitor-pi-coding
|
||||
|
||||
Reference in New Issue
Block a user