diff --git a/frontend/internal/handlers/auth.go b/frontend/internal/handlers/auth.go index 130e2ae..c5ce95a 100644 --- a/frontend/internal/handlers/auth.go +++ b/frontend/internal/handlers/auth.go @@ -26,6 +26,10 @@ func (a *App) Register(w http.ResponseWriter, r *http.Request) { a.render(w, r, "register", "Create account", nil, "Email and password are required.") return } + if confirm := strings.ToLower(strings.TrimSpace(r.FormValue("confirm_email"))); confirm != "" && confirm != email { + a.render(w, r, "register", "Create account", nil, "Emails do not match.") + return + } if len(password) < 8 { a.render(w, r, "register", "Create account", nil, "Password must be at least 8 characters.") return diff --git a/frontend/internal/handlers/tags.go b/frontend/internal/handlers/tags.go index ca24809..351772e 100644 --- a/frontend/internal/handlers/tags.go +++ b/frontend/internal/handlers/tags.go @@ -2,7 +2,11 @@ package handlers import ( "errors" + "fmt" + "io" "net/http" + "os" + "path/filepath" "strconv" "strings" @@ -169,6 +173,73 @@ func (a *App) loadOwnedTag(w http.ResponseWriter, r *http.Request, uid int64) (d return tag, true } +// UploadPhoto saves an uploaded image for a tag and sets its photo_url (HTMX). +func (a *App) UploadPhoto(w http.ResponseWriter, r *http.Request) { + uid, _ := auth.GetUserID(r) + tag, ok := a.loadOwnedTag(w, r, uid) + if !ok { + return + } + + if err := r.ParseMultipartForm(5 << 20); err != nil { // 5 MB + writeUploadMsg(w, "Upload too large (max 5 MB).") + return + } + file, header, err := r.FormFile("photo") + if err != nil { + writeUploadMsg(w, "Please choose an image file.") + return + } + defer file.Close() + + if header.Size > 5<<20 { + writeUploadMsg(w, "Upload too large (max 5 MB).") + return + } + if !strings.HasPrefix(header.Header.Get("Content-Type"), "image/") { + writeUploadMsg(w, "Only image files are allowed.") + return + } + + // Sanitise the filename and store under uploads/. + ext := strings.ToLower(filepath.Ext(header.Filename)) + if ext == "" { + ext = ".jpg" + } + dst := filepath.Join("static", "uploads", fmt.Sprintf("tag%d%s", tag.ID, ext)) + out, err := os.Create(dst) + if err != nil { + writeUploadMsg(w, "Could not save the photo.") + return + } + defer out.Close() + if _, err := io.Copy(out, file); err != nil { + writeUploadMsg(w, "Could not save the photo.") + return + } + + photoURL := "/static/uploads/" + filepath.Base(dst) + if _, err := a.Queries.UpdateTagDetails(r.Context(), db.UpdateTagDetailsParams{ + ID: tag.ID, + PhotoUrl: pgtype.Text{String: photoURL, Valid: true}, + // Preserve existing values: re-send them via the form is not possible here, + // so use the stored tag fields for everything else. + ItemType: tag.ItemType, Description: tag.Description, Phone: tag.Phone, + Address: tag.Address, Notes: tag.Notes, SmsEnabled: tag.SmsEnabled, + }); err != nil { + writeUploadMsg(w, "Could not update the tag.") + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprintf(w, `

Photo uploaded ✓ view — refresh to see it on the tag page.

`, photoURL) +} + +func writeUploadMsg(w http.ResponseWriter, msg string) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprintf(w, `

%s

`, msg) +} + func textOrNil(s string) pgtype.Text { return pgtype.Text{String: s, Valid: s != ""} } diff --git a/frontend/main.go b/frontend/main.go index 2c58315..9ad5b38 100644 --- a/frontend/main.go +++ b/frontend/main.go @@ -73,6 +73,7 @@ func main() { 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)) + mux.Handle("POST /account/tags/{id}/photo", app.RequireAuth(app.UploadPhoto)) srv := &http.Server{ Addr: addr, diff --git a/frontend/static/favicon.svg b/frontend/static/favicon.svg new file mode 100644 index 0000000..4bc9011 --- /dev/null +++ b/frontend/static/favicon.svg @@ -0,0 +1,4 @@ + + + 🐕 + diff --git a/frontend/static/uploads/.gitkeep b/frontend/static/uploads/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/static/uploads/tag1.png b/frontend/static/uploads/tag1.png new file mode 100644 index 0000000..0dd1608 --- /dev/null +++ b/frontend/static/uploads/tag1.png @@ -0,0 +1,2 @@ +PNG + diff --git a/frontend/templates/base.html b/frontend/templates/base.html index 254edfe..089f41d 100644 --- a/frontend/templates/base.html +++ b/frontend/templates/base.html @@ -4,6 +4,7 @@ + {{.Title}} · Where Woof ! @@ -24,7 +25,10 @@ 🐕 Where Woof
{{if .CurrentUser}} - Hi, {{.CurrentUser.Name}} +
+ Hi, {{.CurrentUser.Name}} + {{.CurrentUser.Email}} +
My Tags
diff --git a/frontend/templates/register.html b/frontend/templates/register.html index 2c8114d..ab9dc49 100644 --- a/frontend/templates/register.html +++ b/frontend/templates/register.html @@ -11,9 +11,13 @@
- +
+
+ + +
diff --git a/frontend/templates/tag-edit.html b/frontend/templates/tag-edit.html index 4691031..951f644 100644 --- a/frontend/templates/tag-edit.html +++ b/frontend/templates/tag-edit.html @@ -25,8 +25,15 @@
- - + + {{if .Data.PhotoUrl.Valid}}Current photo{{end}} + + + + + Uploading… + +

Upload a photo, or paste a URL above.

diff --git a/todo.txt b/todo.txt index 33e164b..b0ccc57 100644 --- a/todo.txt +++ b/todo.txt @@ -9,3 +9,7 @@ x 2026-08-05 Set up Gitea for where_woof +project:where-woof +agent:where_woof 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 +(A) Photo +project:where-woof +issues:htmx +(A) Confirm Username and See username +project:where-woof +issues:htmx +(A) Favicon +project:where-woof +issues:htmx +(A) Edit this tags details allowed me to edit +project:where-woof +issues:htmx