frontend-foundation: GOAT front-end Phase 1 — auth, tag management, public tag page (22/22 spec scenarios pass)

This commit is contained in:
2026-08-05 13:46:50 +10:00
parent b846c2c58e
commit 133e375b6b
31 changed files with 1591 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
{{define "account-panel"}}
<div id="account-panel">
{{if .AddError}}<p class="mt-4 rounded border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-700">{{.AddError}}</p>{{end}}
<div class="mt-4 rounded-xl border border-stone-200 bg-white p-4 shadow-sm">
<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>
</form>
</div>
<div id="tag-list">
{{template "tag-list" .Tags}}
</div>
</div>
{{end}}

View File

@@ -0,0 +1,7 @@
{{define "content"}}
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold">My Tags</h1>
<a href="/" class="text-sm text-stone-600 hover:underline">← Home</a>
</div>
{{template "account-panel" .Data}}
{{end}}

View File

@@ -0,0 +1,35 @@
{{define "base"}}
<!doctype html>
<html lang="en" class="h-full">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}} · WhereWoof</title>
<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>
</head>
<body class="min-h-full bg-stone-100 text-stone-900 antialiased">
<nav class="bg-stone-900 text-white">
<div class="mx-auto flex max-w-4xl items-center justify-between px-4 py-3">
<a href="/" class="text-lg font-bold tracking-tight">🐕 WhereWoof</a>
<div class="flex items-center gap-4 text-sm">
{{if .CurrentUser}}
<span class="text-stone-300">Hi, {{.CurrentUser.Name}}</span>
<a href="/account" class="hover:underline">My Tags</a>
<form method="post" action="/logout" class="inline">
<button class="hover:underline">Log out</button>
</form>
{{else}}
<a href="/login" class="hover:underline">Log in</a>
<a href="/register" class="rounded bg-amber-500 px-3 py-1 font-semibold text-stone-900 hover:bg-amber-400">Register</a>
{{end}}
</div>
</div>
</nav>
<main class="mx-auto max-w-4xl px-4 py-8">
{{template "content" .}}
</main>
</body>
</html>
{{end}}

View File

@@ -0,0 +1,24 @@
{{define "content"}}
<div class="mx-auto max-w-2xl text-center">
<h1 class="text-4xl font-extrabold tracking-tight">Lost something?<br><span class="text-amber-600">The tag brings it home.</span></h1>
<p class="mt-4 text-lg text-stone-600">
WhereWoof tags carry the return details for your dog, baggage, skis — anything you care about.
A finder scans the QR code or taps the NFC tag and sees exactly how to get it back to you.
</p>
<div class="mt-8 grid gap-4 sm:grid-cols-2">
<div class="rounded-xl border border-stone-200 bg-white p-6 text-left shadow-sm">
<div class="text-2xl">🏷️</div>
<h2 class="mt-2 font-bold">I have a tag</h2>
<p class="mt-1 text-sm text-stone-600">Claim it and set up the return details for your item.</p>
<a href="/register" class="mt-4 inline-block rounded bg-stone-900 px-4 py-2 text-sm font-semibold text-white hover:bg-stone-700">Set up my tag</a>
</div>
<div class="rounded-xl border border-stone-200 bg-white p-6 text-left shadow-sm">
<div class="text-2xl">🔍</div>
<h2 class="mt-2 font-bold">I found something</h2>
<p class="mt-1 text-sm text-stone-600">Scan the tag you found — the page shows the owner's return details.</p>
<a href="/account" class="mt-4 inline-block rounded bg-amber-500 px-4 py-2 text-sm font-semibold text-stone-900 hover:bg-amber-400">My account</a>
</div>
</div>
</div>
{{end}}

View File

@@ -0,0 +1,22 @@
{{define "content"}}
<div class="mx-auto max-w-md">
<h1 class="text-2xl font-bold">Log in</h1>
<p class="mt-1 text-sm text-stone-600">Welcome back to WhereWoof.</p>
{{if .Error}}<p class="mt-4 rounded bg-red-50 px-4 py-2 text-sm text-red-700 border border-red-200">{{.Error}}</p>{{end}}
<form method="post" action="/login" class="mt-6 space-y-4 rounded-xl border border-stone-200 bg-white p-6 shadow-sm">
<div>
<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>
<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>
<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>
<p class="mt-4 text-center text-sm text-stone-600">New to WhereWoof? <a href="/register" class="text-amber-600 hover:underline">Create an account</a></p>
</div>
{{end}}

View File

@@ -0,0 +1,8 @@
{{define "content"}}
<div class="mx-auto max-w-md text-center">
<div class="text-5xl">🔎</div>
<h1 class="mt-4 text-2xl font-bold">Tag not found</h1>
<p class="mt-2 text-stone-600">We couldn't find a WhereWoof tag with that code. Check the code on the tag and try again.</p>
<a href="/" class="mt-6 inline-block rounded bg-stone-900 px-4 py-2 text-sm font-semibold text-white hover:bg-stone-700">Go home</a>
</div>
{{end}}

View File

@@ -0,0 +1,26 @@
{{define "content"}}
<div class="mx-auto max-w-md">
<h1 class="text-2xl font-bold">Create your account</h1>
<p class="mt-1 text-sm text-stone-600">Register to claim your WhereWoof tag.</p>
{{if .Error}}<p class="mt-4 rounded bg-red-50 px-4 py-2 text-sm text-red-700 border border-red-200">{{.Error}}</p>{{end}}
<form method="post" action="/register" class="mt-6 space-y-4 rounded-xl border border-stone-200 bg-white p-6 shadow-sm">
<div>
<label class="block text-sm font-medium" for="name">Name</label>
<input id="name" name="name" type="text" class="mt-1 w-full rounded border border-stone-300 px-3 py-2" required>
</div>
<div>
<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>
<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>
<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>
<p class="mt-4 text-center text-sm text-stone-600">Already have an account? <a href="/login" class="text-amber-600 hover:underline">Log in</a></p>
</div>
{{end}}

View File

@@ -0,0 +1,49 @@
{{define "content"}}
<div class="mx-auto max-w-xl">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold">Edit tag</h1>
<a href="/account" class="text-sm text-stone-600 hover:underline">← My Tags</a>
</div>
<p class="mt-1 font-mono text-xs text-stone-500">Tag {{.Data.TagCode}}</p>
{{if .Error}}<p class="mt-4 rounded bg-red-50 px-4 py-2 text-sm text-red-700 border border-red-200">{{.Error}}</p>{{end}}
<form method="post" action="/account/tags/{{.Data.ID}}/edit" class="mt-6 space-y-4 rounded-xl border border-stone-200 bg-white p-6 shadow-sm">
<div>
<label class="block text-sm font-medium">Item type</label>
<select name="item_type" class="mt-1 w-full rounded border border-stone-300 px-3 py-2">
<option value="">Select…</option>
<option value="dog" {{if eq .Data.ItemType.String "dog"}}selected{{end}}>Dog</option>
<option value="cat" {{if eq .Data.ItemType.String "cat"}}selected{{end}}>Cat</option>
<option value="baggage" {{if eq .Data.ItemType.String "baggage"}}selected{{end}}>Baggage</option>
<option value="skis" {{if eq .Data.ItemType.String "skis"}}selected{{end}}>Skis</option>
<option value="other" {{if eq .Data.ItemType.String "other"}}selected{{end}}>Other</option>
</select>
</div>
<div>
<label class="block text-sm font-medium">Description</label>
<textarea name="description" rows="3" class="mt-1 w-full rounded border border-stone-300 px-3 py-2">{{if .Data.Description.Valid}}{{.Data.Description.String}}{{end}}</textarea>
</div>
<div>
<label class="block text-sm font-medium">Photo URL</label>
<input name="photo_url" type="url" value="{{if .Data.PhotoUrl.Valid}}{{.Data.PhotoUrl.String}}{{end}}" class="mt-1 w-full rounded border border-stone-300 px-3 py-2" placeholder="https://…">
</div>
<div>
<label class="block text-sm font-medium">Phone number (shown to finders)</label>
<input name="phone" type="tel" value="{{if .Data.Phone.Valid}}{{.Data.Phone.String}}{{end}}" class="mt-1 w-full rounded border border-stone-300 px-3 py-2" placeholder="+61…">
</div>
<div>
<label class="block text-sm font-medium">Address</label>
<input name="address" type="text" value="{{if .Data.Address.Valid}}{{.Data.Address.String}}{{end}}" class="mt-1 w-full rounded border border-stone-300 px-3 py-2">
</div>
<div>
<label class="block text-sm font-medium">Notes</label>
<textarea name="notes" rows="2" class="mt-1 w-full rounded border border-stone-300 px-3 py-2">{{if .Data.Notes.Valid}}{{.Data.Notes.String}}{{end}}</textarea>
</div>
<div class="flex gap-3">
<button type="submit" class="rounded bg-stone-900 px-4 py-2 font-semibold text-white hover:bg-stone-700">Save details</button>
<a href="/account" class="rounded border border-stone-300 px-4 py-2 text-sm font-medium hover:bg-stone-50">Cancel</a>
</div>
</form>
</div>
{{end}}

View File

@@ -0,0 +1,37 @@
{{define "tag-list"}}
{{if .}}
<div class="mt-4 overflow-hidden rounded-xl border border-stone-200 bg-white shadow-sm">
<table class="w-full text-left text-sm">
<thead class="bg-stone-50 text-stone-600">
<tr>
<th class="px-4 py-2 font-medium">Tag</th>
<th class="px-4 py-2 font-medium">Status</th>
<th class="px-4 py-2 font-medium">Item</th>
<th class="px-4 py-2 font-medium"></th>
</tr>
</thead>
<tbody>
{{range .}}
<tr class="border-t border-stone-100">
<td class="px-4 py-2 font-mono text-xs">{{.TagCode}}</td>
<td class="px-4 py-2">
{{if eq .Status "active"}}<span class="rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">Active</span>
{{else if eq .Status "suspended"}}<span class="rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800">Suspended</span>
{{else}}<span class="rounded-full bg-stone-200 px-2 py-0.5 text-xs font-medium text-stone-700">Unset</span>{{end}}
</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">
<button class="ml-3 text-red-600 hover:underline">Remove</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p class="mt-6 text-center text-stone-500">No tags yet. Enter your tag code above to claim it.</p>
{{end}}
{{end}}

View File

@@ -0,0 +1,64 @@
{{define "content"}}
<div class="mx-auto max-w-2xl">
{{if eq .Data.Status "unset"}}
<div class="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 isn't set up yet</h1>
<p class="mt-2 text-stone-600">This WhereWoof tag hasn't been claimed. If this is your tag, log in and add it to your account to set up the return details.</p>
<div class="mt-6 flex justify-center gap-3">
<a href="/login" class="rounded bg-stone-900 px-4 py-2 text-sm font-semibold text-white hover:bg-stone-700">Log in</a>
<a href="/register" class="rounded bg-amber-500 px-4 py-2 text-sm font-semibold text-stone-900 hover:bg-amber-400">Create account</a>
</div>
</div>
{{else if eq .Data.Status "suspended"}}
<div class="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 is unavailable</h1>
<p class="mt-2 text-stone-600">The owner has disabled this tag. Please try another way to return the item.</p>
</div>
{{else}}
<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">
{{if .Data.ItemType.Valid}}{{.Data.ItemType.String | title}}{{else}}Item{{end}} — found!
</h1>
<p class="mt-1 text-sm text-stone-300">This item has a WhereWoof tag. Here's how to return it:</p>
</div>
<div class="p-6">
{{if .Data.PhotoUrl.Valid}}
<img src="{{.Data.PhotoUrl.String}}" alt="Item photo" class="mb-4 h-56 w-full rounded-lg object-cover">
{{end}}
{{if .Data.Description.Valid}}
<p class="text-stone-700">{{.Data.Description.String}}</p>
{{end}}
<dl class="mt-4 space-y-2 text-sm">
{{if .Data.Phone.Valid}}
<div class="flex items-center justify-between rounded bg-stone-50 px-4 py-3">
<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>
{{end}}
{{if .Data.Address.Valid}}
<div class="flex items-center justify-between rounded bg-stone-50 px-4 py-3">
<dt class="font-medium">Return to</dt>
<dd class="text-right">{{.Data.Address.String}}</dd>
</div>
{{end}}
{{if .Data.Notes.Valid}}
<div class="rounded bg-stone-50 px-4 py-3">
<dt class="font-medium">Notes</dt>
<dd class="mt-1 text-stone-600">{{.Data.Notes.String}}</dd>
</div>
{{end}}
</dl>
<p class="mt-6 rounded bg-amber-50 px-4 py-3 text-sm text-amber-800 border border-amber-200">
Found this item? Please contact the owner to arrange the return. Thank you for helping!
</p>
{{if .Data.IsOwner}}
<p class="mt-4 text-center text-sm"><a href="/account/tags/{{.Data.ID}}/edit" class="text-amber-600 hover:underline">✏️ Edit this tag's details</a></p>
{{end}}
</div>
</div>
{{end}}
</div>
{{end}}