fix batch: admin login rehash->password_hash mapping (500); geolocation timeout 20s + hide button on approval + 'location shared' notice; sms: link includes typed number (Alpine); location page = found-dog + map + finder number + tag details; nicer closed page; password visibility toggles; product photos; templates use x-text (no {{}} in Alpine)

This commit is contained in:
2026-08-10 12:27:20 +10:00
parent b5db858ef8
commit 9158681001
6 changed files with 70 additions and 26 deletions

View File

@@ -1,11 +1,8 @@
package handlers
import (
"errors"
"net/http"
"github.com/jackc/pgx/v5"
"wherewoof/frontend/internal/db"
)
@@ -16,10 +13,16 @@ type locationData struct {
FinderPhone string
TagCode string
ScannedAt string
ItemType string
Name string
PhotoURL string
Phone string
Address string
}
// ServeShort resolves a short code to its scan and renders the owner's
// location page (map embed + finder details).
// location page: "your item was found here" — map embed + finder details +
// the tag's return details.
func (a *App) ServeShort(w http.ResponseWriter, r *http.Request) {
code := r.PathValue("code")
sc, err := a.Queries.GetShortCode(r.Context(), code)
@@ -36,11 +39,7 @@ func (a *App) ServeShort(w http.ResponseWriter, r *http.Request) {
tag, err := a.Queries.GetTagByID(r.Context(), scan.TagID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
http.NotFound(w, r)
return
}
http.Error(w, "internal error", http.StatusInternalServerError)
http.NotFound(w, r)
return
}
@@ -50,8 +49,15 @@ func (a *App) ServeShort(w http.ResponseWriter, r *http.Request) {
FinderPhone: scan.ScannerPhone.String,
TagCode: tag.TagCode,
ScannedAt: scan.ScannedAt.Time.Local().Format("Mon 2 Jan, 3:04 pm"),
Name: tag.Description.String,
PhotoURL: tag.PhotoUrl.String,
Phone: tag.Phone.String,
Address: tag.Address.String,
}
a.render(w, r, "location", "Location found", data, "")
if tag.ItemType.Valid {
data.ItemType = titleCase(tag.ItemType.String)
}
a.render(w, r, "location", "Found", data, "")
}
// keep the db import referenced (Tag lookup above uses a.Queries only; this

View File

@@ -2,8 +2,8 @@
<div class="mx-auto max-w-2xl">
<div class="overflow-hidden rounded-xl border border-stone-200 bg-white shadow-sm">
<div class="bg-stone-900 px-6 py-5 text-white">
<h1 class="text-2xl font-bold">📍 Location found</h1>
<p class="mt-1 text-sm text-stone-300">Your Where Woof tag was scanned at {{.Data.ScannedAt}}. This is where it was reported:</p>
<h1 class="text-2xl font-bold">🎉 Your {{if .Data.ItemType}}{{.Data.ItemType}}{{else}}item{{end}} was found!</h1>
<p class="mt-1 text-sm text-stone-300">It was reported at {{.Data.ScannedAt}}. Here's where it was seen:</p>
</div>
<div class="p-6">
<div class="overflow-hidden rounded-lg border border-stone-200">
@@ -16,9 +16,23 @@
</div>
{{if .Data.FinderPhone}}
<div class="mt-4 flex items-center justify-between rounded bg-green-50 px-4 py-3 border border-green-200">
<dt class="font-medium text-sm">The finder left their number</dt>
<dd><a href="tel:{{.Data.FinderPhone}}" class="font-semibold text-green-700 hover:underline">{{.Data.FinderPhone}}</a></dd>
<div class="mt-4 flex items-center justify-between rounded border border-green-200 bg-green-50 px-4 py-3">
<span class="text-sm font-medium">The finder left their number</span>
<a href="tel:{{.Data.FinderPhone}}" class="font-semibold text-green-700 hover:underline">{{.Data.FinderPhone}}</a>
</div>
{{end}}
{{if or .Data.PhotoURL .Data.Name .Data.Address .Data.Phone}}
<div class="mt-4 rounded-xl border border-stone-200 p-4">
<h2 class="font-bold">About your {{if .Data.ItemType}}{{.Data.ItemType}}{{else}}item{{end}}</h2>
<div class="mt-2 flex gap-4">
{{if .Data.PhotoURL}}<img src="{{.Data.PhotoURL}}" alt="Your item" class="h-28 w-28 rounded-lg object-cover">{{end}}
<div class="text-sm">
{{if .Data.Name}}<p class="font-medium">{{.Data.Name}}</p>{{end}}
{{if .Data.Address}}<p class="text-stone-600">{{.Data.Address}}</p>{{end}}
{{if .Data.Phone}}<p class="text-stone-600">Contact: <a href="tel:{{.Data.Phone}}" class="text-amber-600 hover:underline">{{.Data.Phone}}</a></p>{{end}}
</div>
</div>
</div>
{{end}}

View File

@@ -10,9 +10,12 @@
<label class="block text-sm font-medium" for="email">Email</label>
<input id="email" name="email" type="email" class="mt-1 w-full rounded border border-stone-300 px-3 py-2" required>
</div>
<div>
<div x-data="{ show: false }">
<label class="block text-sm font-medium" for="password">Password</label>
<input id="password" name="password" type="password" class="mt-1 w-full rounded border border-stone-300 px-3 py-2" required>
<div class="relative mt-1">
<input id="password" name="password" type="password" :type="show ? 'text' : 'password'" class="w-full rounded border border-stone-300 px-3 py-2 pr-10" required>
<button type="button" @click="show = !show" class="absolute inset-y-0 right-0 flex items-center px-3 text-stone-500 hover:text-stone-700" aria-label="Toggle password visibility" x-text="show ? '👁️' : '🙈'"></button>
</div>
</div>
<button type="submit" class="w-full rounded bg-stone-900 px-4 py-2 font-semibold text-white hover:bg-stone-700">Log in</button>
</form>

View File

@@ -18,9 +18,12 @@
<label class="block text-sm font-medium" for="confirm_email">Confirm email</label>
<input id="confirm_email" name="confirm_email" type="email" class="mt-1 w-full rounded border border-stone-300 px-3 py-2" required>
</div>
<div>
<div x-data="{ show: false }">
<label class="block text-sm font-medium" for="password">Password</label>
<input id="password" name="password" type="password" class="mt-1 w-full rounded border border-stone-300 px-3 py-2" minlength="8" required>
<div class="relative mt-1">
<input id="password" name="password" type="password" :type="show ? 'text' : 'password'" class="w-full rounded border border-stone-300 px-3 py-2 pr-10" minlength="8" required>
<button type="button" @click="show = !show" class="absolute inset-y-0 right-0 flex items-center px-3 text-stone-500 hover:text-stone-700" aria-label="Toggle password visibility" x-text="show ? '👁️' : '🙈'"></button>
</div>
</div>
<button type="submit" class="w-full rounded bg-stone-900 px-4 py-2 font-semibold text-white hover:bg-stone-700">Create account</button>
</form>

View File

@@ -17,10 +17,15 @@
<p class="mt-2 text-stone-600">The owner has disabled this tag. Please try another way to return the item.</p>
</div>
{{else if eq .Data.Status "closed"}}
<div class="rounded-xl border border-stone-200 bg-white p-8 text-center shadow-sm">
<div class="mx-auto max-w-xl rounded-xl border border-stone-200 bg-white p-8 text-center shadow-sm">
<div class="text-5xl">🔒</div>
<h1 class="mt-4 text-2xl font-bold">This tag has been closed</h1>
<p class="mt-2 text-stone-600">This tag is no longer active. If you found this item, please try to return it another way.</p>
<p class="mt-2 text-stone-600">This Where Woof tag is no longer active — its owner has retired it (maybe a broken tag, a new tag, or the item was found and the tag removed).</p>
<p class="mt-2 text-sm text-stone-500">If you found this item, please try to return it another way (ask around, check lost &amp; found, or contact local authorities).</p>
<div class="mt-6 flex justify-center gap-3">
<a href="/" class="rounded bg-stone-900 px-4 py-2 text-sm font-semibold text-white hover:bg-stone-700">Where Woof home</a>
<a href="/what-is-this" class="rounded border border-stone-300 px-4 py-2 text-sm font-medium hover:bg-stone-50">What is this?</a>
</div>
</div>
{{else}}
<div class="overflow-hidden rounded-xl border border-stone-200 bg-white shadow-sm">
@@ -43,9 +48,13 @@
<dt class="font-medium">Call the owner</dt>
<dd><a href="tel:{{.Data.Phone.String}}" class="font-semibold text-amber-600 hover:underline">{{.Data.Phone.String}}</a></dd>
</div>
<div class="flex items-center justify-between rounded bg-stone-50 px-4 py-3">
<div class="flex items-center justify-between rounded bg-stone-50 px-4 py-3" x-data="{ p: '' }">
<dt class="font-medium">Text the owner</dt>
<dd><a href="sms:{{.Data.Phone.String}}?&body=Hi! I found your {{if .Data.ItemType.Valid}}{{.Data.ItemType.String}}{{else}}item{{end}}{{if .Data.Description.Valid}}, {{.Data.Description.String}}{{end}}. My phone number is: " class="font-semibold text-amber-600 hover:underline">Send an SMS</a></dd>
<dd>
<a :href="'sms:' + '{{.Data.Phone.String}}' + '?&body=' + encodeURIComponent('Hi! I found your {{if .Data.ItemType.Valid}}{{.Data.ItemType.String}}{{else}}item{{end}}{{if .Data.Description.Valid}}, {{.Data.Description.String}}{{end}}. My number is: ') + (p ? p : '___')"
class="font-semibold text-amber-600 hover:underline">Send an SMS</a>
<input x-model="p" type="tel" placeholder="Add your number to include it" class="mt-1 w-44 rounded border border-stone-300 px-2 py-1 text-xs">
</dd>
</div>
{{end}}
{{if .Data.Address.Valid}}
@@ -65,6 +74,7 @@
<div class="mt-4 rounded border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
<p><strong>Help the owner find it:</strong> sharing your location sends them a map link. No location? No problem — the page still works.</p>
<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>
<p id="location-shared" class="hidden mt-2 text-xs font-medium text-green-700">✓ Location shared — the owner has been sent a map link.</p>
</div>
<div id="contact-area" class="mt-4" x-data="{ phone: '', get valid() { return /^[0-9+ ]{8,16}$/.test(this.phone.trim()); } }">
@@ -111,11 +121,16 @@
if (!navigator.geolocation) { sendScan(null, null); return; }
navigator.geolocation.getCurrentPosition(
function (pos) {
if (btn) btn.classList.add("hidden");
// Location approved: hide the button permanently and send coords.
if (btn) { btn.classList.add("hidden"); btn.disabled = true; }
var shared = document.getElementById("location-shared");
if (shared) shared.classList.remove("hidden");
sendScan(pos.coords.latitude, pos.coords.longitude);
},
function () { sendScan(null, null); },
{timeout: 8000, maximumAge: 60000}
function () {
sendScan(null, null); // no location — page still works, button stays
},
{timeout: 20000, maximumAge: 0}
);
}
if (btn) btn.addEventListener("click", tryLocation);