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:
@@ -97,3 +97,6 @@ ALTER TABLE orders ADD COLUMN IF NOT EXISTS renews_at TIMESTAMPTZ;
|
|||||||
-- Tag lifecycle: closed state (owner close / moved-from). Idempotent.
|
-- Tag lifecycle: closed state (owner close / moved-from). Idempotent.
|
||||||
ALTER TABLE tags DROP CONSTRAINT IF EXISTS tags_status_check;
|
ALTER TABLE tags DROP CONSTRAINT IF EXISTS tags_status_check;
|
||||||
ALTER TABLE tags ADD CONSTRAINT tags_status_check CHECK (status IN ('unset','active','suspended','closed'));
|
ALTER TABLE tags ADD CONSTRAINT tags_status_check CHECK (status IN ('unset','active','suspended','closed'));
|
||||||
|
|
||||||
|
-- Product photos (admin).
|
||||||
|
ALTER TABLE products ADD COLUMN IF NOT EXISTS photo_url TEXT;
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
|
|
||||||
"wherewoof/frontend/internal/db"
|
"wherewoof/frontend/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,10 +13,16 @@ type locationData struct {
|
|||||||
FinderPhone string
|
FinderPhone string
|
||||||
TagCode string
|
TagCode string
|
||||||
ScannedAt 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
|
// 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) {
|
func (a *App) ServeShort(w http.ResponseWriter, r *http.Request) {
|
||||||
code := r.PathValue("code")
|
code := r.PathValue("code")
|
||||||
sc, err := a.Queries.GetShortCode(r.Context(), code)
|
sc, err := a.Queries.GetShortCode(r.Context(), code)
|
||||||
@@ -36,13 +39,9 @@ func (a *App) ServeShort(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
tag, err := a.Queries.GetTagByID(r.Context(), scan.TagID)
|
tag, err := a.Queries.GetTagByID(r.Context(), scan.TagID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data := locationData{
|
data := locationData{
|
||||||
Lat: scan.Lat.Float64,
|
Lat: scan.Lat.Float64,
|
||||||
@@ -50,8 +49,15 @@ func (a *App) ServeShort(w http.ResponseWriter, r *http.Request) {
|
|||||||
FinderPhone: scan.ScannerPhone.String,
|
FinderPhone: scan.ScannerPhone.String,
|
||||||
TagCode: tag.TagCode,
|
TagCode: tag.TagCode,
|
||||||
ScannedAt: scan.ScannedAt.Time.Local().Format("Mon 2 Jan, 3:04 pm"),
|
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
|
// keep the db import referenced (Tag lookup above uses a.Queries only; this
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
<div class="mx-auto max-w-2xl">
|
<div class="mx-auto max-w-2xl">
|
||||||
<div class="overflow-hidden rounded-xl border border-stone-200 bg-white shadow-sm">
|
<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">
|
<div class="bg-stone-900 px-6 py-5 text-white">
|
||||||
<h1 class="text-2xl font-bold">📍 Location found</h1>
|
<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">Your Where Woof tag was scanned at {{.Data.ScannedAt}}. This is where it was reported:</p>
|
<p class="mt-1 text-sm text-stone-300">It was reported at {{.Data.ScannedAt}}. Here's where it was seen:</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div class="overflow-hidden rounded-lg border border-stone-200">
|
<div class="overflow-hidden rounded-lg border border-stone-200">
|
||||||
@@ -16,9 +16,23 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .Data.FinderPhone}}
|
{{if .Data.FinderPhone}}
|
||||||
<div class="mt-4 flex items-center justify-between rounded bg-green-50 px-4 py-3 border border-green-200">
|
<div class="mt-4 flex items-center justify-between rounded border border-green-200 bg-green-50 px-4 py-3">
|
||||||
<dt class="font-medium text-sm">The finder left their number</dt>
|
<span class="text-sm font-medium">The finder left their number</span>
|
||||||
<dd><a href="tel:{{.Data.FinderPhone}}" class="font-semibold text-green-700 hover:underline">{{.Data.FinderPhone}}</a></dd>
|
<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>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,12 @@
|
|||||||
<label class="block text-sm font-medium" for="email">Email</label>
|
<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>
|
<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>
|
<div x-data="{ show: false }">
|
||||||
<label class="block text-sm font-medium" for="password">Password</label>
|
<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>
|
</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>
|
<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>
|
</form>
|
||||||
|
|||||||
@@ -18,9 +18,12 @@
|
|||||||
<label class="block text-sm font-medium" for="confirm_email">Confirm email</label>
|
<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>
|
<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>
|
<div x-data="{ show: false }">
|
||||||
<label class="block text-sm font-medium" for="password">Password</label>
|
<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>
|
</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>
|
<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>
|
</form>
|
||||||
|
|||||||
@@ -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>
|
<p class="mt-2 text-stone-600">The owner has disabled this tag. Please try another way to return the item.</p>
|
||||||
</div>
|
</div>
|
||||||
{{else if eq .Data.Status "closed"}}
|
{{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>
|
<div class="text-5xl">🔒</div>
|
||||||
<h1 class="mt-4 text-2xl font-bold">This tag has been closed</h1>
|
<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 & 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>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="overflow-hidden rounded-xl border border-stone-200 bg-white shadow-sm">
|
<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>
|
<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>
|
<dd><a href="tel:{{.Data.Phone.String}}" class="font-semibold text-amber-600 hover:underline">{{.Data.Phone.String}}</a></dd>
|
||||||
</div>
|
</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>
|
<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>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .Data.Address.Valid}}
|
{{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">
|
<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>
|
<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>
|
<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>
|
||||||
|
|
||||||
<div id="contact-area" class="mt-4" x-data="{ phone: '', get valid() { return /^[0-9+ ]{8,16}$/.test(this.phone.trim()); } }">
|
<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; }
|
if (!navigator.geolocation) { sendScan(null, null); return; }
|
||||||
navigator.geolocation.getCurrentPosition(
|
navigator.geolocation.getCurrentPosition(
|
||||||
function (pos) {
|
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);
|
sendScan(pos.coords.latitude, pos.coords.longitude);
|
||||||
},
|
},
|
||||||
function () { sendScan(null, null); },
|
function () {
|
||||||
{timeout: 8000, maximumAge: 60000}
|
sendScan(null, null); // no location — page still works, button stays
|
||||||
|
},
|
||||||
|
{timeout: 20000, maximumAge: 0}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (btn) btn.addEventListener("click", tryLocation);
|
if (btn) btn.addEventListener("click", tryLocation);
|
||||||
|
|||||||
Reference in New Issue
Block a user