Promo: order page + red banner + contact form; fix admin login rehash

- /order page: single tag $5.50 / 10-pack $22.00 (GST-incl), qty via
  Stripe Payment Links (single 1-25, pack 1-2), thanks state
- Red 'Protect & recover your valuables — ORDER NOW' banner in base layout
  (every page incl. found-dog tag page) + footer promo links
- Contact page: form -> Gmail SMTP (env SMTP_*), hello@where-woof.com
- Fix admin login 500: User.getAuthPasswordName() = password_hash so
  Laravel 13 rehash-on-login writes the correct column (was trying
  UPDATE users SET password = ...)
This commit is contained in:
2026-09-01 12:53:53 +10:00
parent aabaa750f4
commit 2a101e9a62
9 changed files with 223 additions and 3 deletions

View File

@@ -15,6 +15,8 @@ class User extends Authenticatable implements FilamentUser
use HasFactory, Notifiable, Billable; use HasFactory, Notifiable, Billable;
protected $table = 'users'; protected $table = 'users';
// Shared schema uses password_hash, not Laravel's default 'password'.
protected $authPasswordName = 'password_hash';
public $timestamps = false; // shared schema: created_at only, set by DB default public $timestamps = false; // shared schema: created_at only, set by DB default
protected $fillable = ['email', 'password_hash', 'name', 'phone', 'is_admin', 'stripe_id', 'sms_credits']; protected $fillable = ['email', 'password_hash', 'name', 'phone', 'is_admin', 'stripe_id', 'sms_credits'];
protected $hidden = ['password_hash']; protected $hidden = ['password_hash'];

View File

@@ -58,6 +58,7 @@ func LoadTemplates() (Templates, error) {
"about": {dir + "/about.html"}, "about": {dir + "/about.html"},
"what": {dir + "/what.html"}, "what": {dir + "/what.html"},
"contact": {dir + "/contact.html"}, "contact": {dir + "/contact.html"},
"order": {dir + "/order.html"},
"register": {dir + "/register.html"}, "register": {dir + "/register.html"},
"login": {dir + "/login.html"}, "login": {dir + "/login.html"},
"account": {dir + "/account.html", dir + "/account-panel.html", dir + "/tag-list.html", dir + "/tag-edit-inline.html"}, "account": {dir + "/account.html", dir + "/account-panel.html", dir + "/tag-list.html", dir + "/tag-edit-inline.html"},

View File

@@ -14,5 +14,11 @@ func (a *App) WhatIsThis(w http.ResponseWriter, r *http.Request) {
// Contact renders the contact page. // Contact renders the contact page.
func (a *App) Contact(w http.ResponseWriter, r *http.Request) { func (a *App) Contact(w http.ResponseWriter, r *http.Request) {
a.render(w, r, "contact", "Contact", nil, "") a.render(w, r, "contact", "Contact", contactData{}, "")
}
// contactData passed to the contact page (form submission state).
type contactData struct {
Sent bool
Error string
} }

View File

@@ -0,0 +1,92 @@
package handlers
import (
"fmt"
"net/http"
"net/smtp"
"os"
"strings"
)
// orderData passes the Stripe Payment Links (env-configured) to the order page.
// Links are bought via Stripe-hosted checkout; the buyer chooses quantity there
// (single 1-25, 10-pack 1-2). Prices: $5.00 + GST, $20.00 + GST (fee handled
// by Stripe Managed Payments).
type orderData struct {
SingleLink string
PackLink string
Thanks bool
}
// Order renders the public ordering page.
func (a *App) Order(w http.ResponseWriter, r *http.Request) {
a.render(w, r, "order", "Order tags",
orderData{
SingleLink: os.Getenv("ORDER_SINGLE_LINK"),
PackLink: os.Getenv("ORDER_PACK_LINK"),
Thanks: r.URL.Query().Get("thanks") == "1",
}, "")
}
// ContactSubmit receives the contact form and emails the team via SMTP
// (env: SMTP_HOST/SMTP_PORT/SMTP_USER/SMTP_PASS, CONTACT_TO).
// Returns the contact page with a success/error notice.
func (a *App) ContactSubmit(w http.ResponseWriter, r *http.Request) {
name := strings.TrimSpace(r.FormValue("name"))
email := strings.TrimSpace(r.FormValue("email"))
subject := strings.TrimSpace(r.FormValue("subject"))
message := strings.TrimSpace(r.FormValue("message"))
errMsg := ""
if name == "" || email == "" || message == "" {
errMsg = "Please fill in your name, email and message."
} else if !strings.Contains(email, "@") {
errMsg = "Please enter a valid email address."
}
if errMsg == "" {
if err := sendContactEmail(name, email, subject, message); err != nil {
fmt.Println("contact email failed:", err)
errMsg = "Sorry — we couldn't send your message right now. Please email us directly at hello@where-woof.com."
} else {
// Success: show a confirmation instead of re-rendering the form.
a.render(w, r, "contact", "Get in touch", contactData{Sent: true}, "")
return
}
}
a.render(w, r, "contact", "Get in touch", contactData{Error: errMsg}, errMsg)
}
// sendContactEmail sends the form contents to CONTACT_TO via Gmail SMTP
// (ENV: SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, CONTACT_TO).
func sendContactEmail(name, email, subject, message string) error {
host := os.Getenv("SMTP_HOST")
port := os.Getenv("SMTP_PORT")
user := os.Getenv("SMTP_USER")
pass := os.Getenv("SMTP_PASS")
to := os.Getenv("CONTACT_TO")
if host == "" || user == "" || pass == "" || to == "" {
return fmt.Errorf("SMTP not configured")
}
if port == "" {
port = "587"
}
subj := subject
if subj == "" {
subj = "Where Woof contact form"
}
body := fmt.Sprintf("From: %s <%s>\nSubject: %s\n\n%s", name, email, subj, message)
msg := []byte("To: " + to + "\r\n" +
"From: " + user + "\r\n" +
"Subject: " + subj + "\r\n" +
"MIME-Version: 1.0\r\n" +
"Content-Type: text/plain; charset=utf-8\r\n" +
"\r\n" + body + "\r\n")
addr := host + ":" + port
auth := smtp.PlainAuth("", user, pass, host)
return smtp.SendMail(addr, auth, user, []string{to}, msg)
}

View File

@@ -70,6 +70,8 @@ func main() {
mux.HandleFunc("GET /about", app.About) mux.HandleFunc("GET /about", app.About)
mux.HandleFunc("GET /what-is-this", app.WhatIsThis) mux.HandleFunc("GET /what-is-this", app.WhatIsThis)
mux.HandleFunc("GET /contact", app.Contact) mux.HandleFunc("GET /contact", app.Contact)
mux.HandleFunc("POST /contact", app.ContactSubmit)
mux.HandleFunc("GET /order", app.Order)
mux.HandleFunc("/{path...}", app.LegacyNotFound) mux.HandleFunc("/{path...}", app.LegacyNotFound)
mux.HandleFunc("GET /t/{tag_code}", app.PublicTag) mux.HandleFunc("GET /t/{tag_code}", app.PublicTag)
mux.HandleFunc("GET /s/{code}", app.ServeShort) mux.HandleFunc("GET /s/{code}", app.ServeShort)

View File

@@ -40,9 +40,25 @@
</div> </div>
</div> </div>
</nav> </nav>
<!-- Promo banner: consistent across every page -> order page -->
<div class="bg-red-600 text-white">
<a href="/order" class="mx-auto flex max-w-4xl items-center justify-center gap-2 px-4 py-2.5 text-center text-sm font-bold tracking-wide hover:bg-red-500">
<span>🛡️ Protect &amp; recover your valuables</span>
<span class="rounded bg-white px-2 py-0.5 text-xs font-extrabold text-red-600">ORDER NOW — tags from $5</span>
</a>
</div>
<main class="mx-auto max-w-4xl px-4 py-8"> <main class="mx-auto max-w-4xl px-4 py-8">
{{template "content" .}} {{template "content" .}}
</main> </main>
<footer class="border-t border-stone-200 py-6">
<div class="mx-auto flex max-w-4xl flex-wrap items-center justify-center gap-x-6 gap-y-2 px-4 text-sm text-stone-500">
<a href="/order" class="font-semibold text-red-600 hover:underline">🛡️ Order tags — from $5</a>
<a href="/about" class="hover:underline">About</a>
<a href="/what-is-this" class="hover:underline">What is this?</a>
<a href="/contact" class="hover:underline">Get in touch</a>
<span>🐕 Where Woof — every item deserves a way home</span>
</div>
</footer>
</body> </body>
</html> </html>
{{end}} {{end}}

View File

@@ -2,9 +2,46 @@
<div class="mx-auto max-w-2xl"> <div class="mx-auto max-w-2xl">
<h1 class="text-3xl font-extrabold tracking-tight">Get in touch</h1> <h1 class="text-3xl font-extrabold tracking-tight">Get in touch</h1>
<p class="mt-4 text-stone-700">Questions, lost-tag help, or bulk orders? We'd love to hear from you.</p> <p class="mt-4 text-stone-700">Questions, lost-tag help, or bulk orders? We'd love to hear from you.</p>
{{if .Data.Sent}}
<div class="mt-6 rounded-xl border border-green-200 bg-green-50 p-6 text-center shadow-sm">
<div class="text-4xl">📨</div>
<h2 class="mt-2 text-lg font-bold text-green-800">Message sent!</h2>
<p class="mt-1 text-green-700">Thanks for reaching out — we'll get back to you shortly (usually within a day).</p>
</div>
{{else}}
<div class="mt-6 rounded-xl border border-stone-200 bg-white p-6 shadow-sm">
{{if .Data.Error}}
<p class="mb-4 rounded border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-700">{{.Data.Error}}</p>
{{end}}
<form method="post" action="/contact" class="space-y-4">
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label class="block text-sm font-semibold text-stone-700" for="name">Your name</label>
<input id="name" name="name" required class="mt-1 w-full rounded-lg border border-stone-300 px-3 py-2 text-sm focus:border-amber-500 focus:outline-none" placeholder="Jane Smith">
</div>
<div>
<label class="block text-sm font-semibold text-stone-700" for="email">Your email</label>
<input id="email" name="email" type="email" required class="mt-1 w-full rounded-lg border border-stone-300 px-3 py-2 text-sm focus:border-amber-500 focus:outline-none" placeholder="you@example.com">
</div>
</div>
<div>
<label class="block text-sm font-semibold text-stone-700" for="subject">Subject</label>
<input id="subject" name="subject" class="mt-1 w-full rounded-lg border border-stone-300 px-3 py-2 text-sm focus:border-amber-500 focus:outline-none" placeholder="Bulk order for a park / trail">
</div>
<div>
<label class="block text-sm font-semibold text-stone-700" for="message">Message</label>
<textarea id="message" name="message" required rows="5" class="mt-1 w-full rounded-lg border border-stone-300 px-3 py-2 text-sm focus:border-amber-500 focus:outline-none" placeholder="Tell us what you need…"></textarea>
</div>
<button type="submit" class="rounded-full bg-stone-900 px-6 py-2.5 text-sm font-bold text-white hover:bg-stone-700">Send message</button>
</form>
</div>
{{end}}
<div class="mt-6 space-y-3 rounded-xl border border-stone-200 bg-white p-6 text-sm shadow-sm"> <div class="mt-6 space-y-3 rounded-xl border border-stone-200 bg-white p-6 text-sm shadow-sm">
<p><strong>Email:</strong> <a href="mailto:info@where-woof.com" class="text-amber-600 hover:underline">info@where-woof.com</a></p> <p><strong>Email:</strong> <a href="mailto:hello@where-woof.com" class="text-amber-600 hover:underline">hello@where-woof.com</a></p>
<p><strong>Website:</strong> <a href="/" class="text-amber-600 hover:underline">where-woof.com</a></p> <p><strong>Website:</strong> <a href="/" class="text-amber-600 hover:underline">where-woof.com</a></p>
<p><strong>Order tags:</strong> <a href="/order" class="text-amber-600 hover:underline">yeah, take my money</a> — from $5 + GST.</p>
<p class="text-stone-500">For bulk tags (parks, trails, campgrounds) mention your use case and volume.</p> <p class="text-stone-500">For bulk tags (parks, trails, campgrounds) mention your use case and volume.</p>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,60 @@
{{define "content"}}
<div class="mx-auto max-w-3xl">
{{if .Data.Thanks}}
<div class="mb-6 rounded-xl border border-green-200 bg-green-50 p-6 text-center shadow-sm">
<div class="text-4xl">✅</div>
<h2 class="mt-2 text-xl font-bold text-green-800">Thanks for your order!</h2>
<p class="mt-1 text-green-700">We've received your payment and will ship your tags soon. Watch your inbox for the email from <strong>hello@where-woof.com</strong> with activation details.</p>
</div>
{{end}}
<h1 class="text-3xl font-extrabold tracking-tight">Order tags</h1>
<p class="mt-2 text-stone-700">Stick a Where Woof tag on anything you don't want to lose — a dog collar, a school bag, ski gear, a toolbox. If it's found, whoever finds it scans the tag and you get an SMS with its location, instantly.</p>
<div class="mt-8 grid gap-6 sm:grid-cols-2">
<!-- Single tag -->
<div class="flex flex-col rounded-2xl border border-stone-200 bg-white p-6 shadow-sm">
<div class="text-4xl">🏷️</div>
<h2 class="mt-3 text-xl font-bold">Single tag</h2>
<p class="mt-1 text-sm text-stone-600">One tag for one item. Plastic laminate, QR + NFC.</p>
<p class="mt-4 text-2xl font-extrabold">$5.00 <span class="text-sm font-normal text-stone-500">+ GST ($5.50)</span></p>
<p class="mt-1 text-xs text-stone-500">+ $10/yr subscription (50 SMS alerts included)</p>
<div class="mt-auto pt-5">
<a href="{{.Data.SingleLink}}" class="block rounded-full bg-stone-900 px-5 py-3 text-center font-bold text-white hover:bg-stone-700">
Order now
</a>
<p class="mt-2 text-center text-xs text-stone-500">Need 2, 3 … 7? Pick the quantity at checkout (up to 25).</p>
</div>
</div>
<!-- 10-pack -->
<div class="flex flex-col rounded-2xl border-2 border-red-600 bg-white p-6 shadow-sm">
<div class="flex items-center justify-between">
<div class="text-4xl">🎒🐾</div>
<span class="rounded-full bg-red-600 px-2.5 py-1 text-xs font-extrabold text-white">BEST VALUE</span>
</div>
<h2 class="mt-3 text-xl font-bold">10-pack</h2>
<p class="mt-1 text-sm text-stone-600">Tag the whole family — collars, bags, keys, skis.</p>
<p class="mt-4 text-2xl font-extrabold">$20.00 <span class="text-sm font-normal text-stone-500">+ GST ($22.00)</span></p>
<p class="mt-1 text-xs text-stone-500">$2 per tag · + $10/yr subscription (50 SMS alerts included)</p>
<div class="mt-auto pt-5">
<a href="{{.Data.PackLink}}" class="block rounded-full bg-red-600 px-5 py-3 text-center font-bold text-white hover:bg-red-500">
Order the 10-pack
</a>
<p class="mt-2 text-center text-xs text-stone-500">Up to 2 packs per account (20 tags, fits the 25-tag limit).</p>
</div>
</div>
</div>
<div class="mt-8 rounded-xl border border-stone-200 bg-white p-6 text-sm shadow-sm">
<h3 class="font-bold">How it works</h3>
<ol class="mt-2 list-decimal space-y-1 pl-5 text-stone-700">
<li>Order your tags — they arrive ready to scan (QR + NFC).</li>
<li>When they arrive, create a free account and add each tag.</li>
<li>Set the item's name and your phone number.</li>
<li>If it's ever lost and found, you get an SMS with a map link — instantly.</li>
</ol>
<p class="mt-3 text-stone-500">Questions? <a href="/contact" class="text-amber-600 hover:underline">Get in touch</a> — hello@where-woof.com</p>
</div>
</div>
{{end}}

View File

@@ -26,3 +26,7 @@ x 2026-08-08 2026-08-08 Billing phase +
(A) Do we tie this all into an established Open Source inventory payment system - keep qick dash for now? +area:CONCEPT (A) Do we tie this all into an established Open Source inventory payment system - keep qick dash for now? +area:CONCEPT
(A) Order-tags. Tie in the inventory system? +area:Where-woof (A) Order-tags. Tie in the inventory system? +area:Where-woof
(A) General webiste pages - what is this? Get in touch? About Us. +area:Where-woof (A) General webiste pages - what is this? Get in touch? About Us. +area:Where-woof
(A) Order tags page + promotion banner (Protect & Recover Valuables — Order now) +project:where-woof +agent:where_woof
(A) Red promo banner on tag/about/contact/what-is-this pages +project:where-woof +agent:where_woof
(A) Get in touch page: hello@where-woof.com + contact form +project:where-woof +agent:where_woof
(C) Verify coordinates in scan/location flow +project:where-woof +agent:where_woof