frontend-foundation: GOAT front-end Phase 1 — auth, tag management, public tag page (22/22 spec scenarios pass)
This commit is contained in:
16
frontend/templates/account-panel.html
Normal file
16
frontend/templates/account-panel.html
Normal 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}}
|
||||
7
frontend/templates/account.html
Normal file
7
frontend/templates/account.html
Normal 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}}
|
||||
35
frontend/templates/base.html
Normal file
35
frontend/templates/base.html
Normal 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}}
|
||||
24
frontend/templates/index.html
Normal file
24
frontend/templates/index.html
Normal 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}}
|
||||
22
frontend/templates/login.html
Normal file
22
frontend/templates/login.html
Normal 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}}
|
||||
8
frontend/templates/not-found.html
Normal file
8
frontend/templates/not-found.html
Normal 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}}
|
||||
26
frontend/templates/register.html
Normal file
26
frontend/templates/register.html
Normal 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}}
|
||||
49
frontend/templates/tag-edit.html
Normal file
49
frontend/templates/tag-edit.html
Normal 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}}
|
||||
37
frontend/templates/tag-list.html
Normal file
37
frontend/templates/tag-list.html
Normal 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}}
|
||||
64
frontend/templates/tag-public.html
Normal file
64
frontend/templates/tag-public.html
Normal 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}}
|
||||
Reference in New Issue
Block a user