| 1 |
package app
|
| 2 |
|
| 3 |
import "html/template"
|
| 4 |
|
| 5 |
const pageCSS = `
|
| 6 |
@font-face{font-family:'Iosevka';font-style:normal;font-weight:400;font-display:swap;src:url('/assets/fonts/iosevka-latin-400-normal.woff2') format('woff2')}
|
| 7 |
@font-face{font-family:'Iosevka';font-style:normal;font-weight:500;font-display:swap;src:url('/assets/fonts/iosevka-latin-500-normal.woff2') format('woff2')}
|
| 8 |
@font-face{font-family:'Iosevka';font-style:normal;font-weight:600;font-display:swap;src:url('/assets/fonts/iosevka-latin-600-normal.woff2') format('woff2')}
|
| 9 |
@font-face{font-family:'Iosevka';font-style:italic;font-weight:400;font-display:swap;src:url('/assets/fonts/iosevka-latin-400-italic.woff2') format('woff2')}
|
| 10 |
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:swap;src:url('/assets/fonts/ibm-plex-sans-latin-400-normal.woff2') format('woff2')}
|
| 11 |
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:500;font-display:swap;src:url('/assets/fonts/ibm-plex-sans-latin-500-normal.woff2') format('woff2')}
|
| 12 |
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
| 13 |
:root{--bg:#0a0a0c;--surface:#18181c;--border:#2a2a2e;--border-strong:#3a3a40;
|
| 14 |
--text:#f5f4f0;--muted:#a8a6a0;--dim:#6a6864;--green:#5dd39e;--green-bg:#11201a;--green-dim:#1f3a2c;
|
| 15 |
--red:#ff8a7a;--red-bg:#2a1410;--serif:'Iosevka',ui-monospace,monospace;--sans:'IBM Plex Sans',system-ui,sans-serif;--mono:'Iosevka',ui-monospace,monospace}
|
| 16 |
html,body{background:var(--bg);color:var(--text);font-family:var(--sans);-webkit-font-smoothing:antialiased}
|
| 17 |
a{color:inherit;text-decoration:none}
|
| 18 |
.wrap{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:2rem}
|
| 19 |
.card{width:100%;max-width:400px;background:var(--surface);border:0.5px solid var(--border);border-radius:16px;padding:2.5rem}
|
| 20 |
.brand{font-family:var(--serif);font-size:22px;font-weight:500;letter-spacing:-0.5px}
|
| 21 |
.brand span{color:var(--green);font-size:13px}
|
| 22 |
.brand em{font-style:normal;color:var(--dim);font-size:16px}
|
| 23 |
h1{font-family:var(--serif);font-size:30px;font-weight:500;letter-spacing:-1px;margin-bottom:1.25rem}
|
| 24 |
label{display:block;font-size:12px;color:var(--muted);margin-bottom:1rem;letter-spacing:0.2px}
|
| 25 |
input,select{display:block;width:100%;margin-top:6px;padding:11px 13px;background:var(--bg);color:var(--text);
|
| 26 |
border:0.5px solid var(--border-strong);border-radius:9px;font-family:var(--sans);font-size:14px}
|
| 27 |
input:focus,select:focus{outline:none;border-color:var(--green)}
|
| 28 |
input[readonly]{color:var(--muted);border-style:dashed}
|
| 29 |
button{font-family:var(--sans);font-size:14px;font-weight:500;padding:12px 22px;border:none;border-radius:100px;
|
| 30 |
background:var(--green);color:#061a12;cursor:pointer;margin-top:6px}
|
| 31 |
button.ghost,a.btn-secondary{background:transparent;color:var(--text);border:0.5px solid var(--border-strong);display:inline-block}
|
| 32 |
.btn-sm{padding:6px 14px;font-size:12px;margin-top:0}
|
| 33 |
button.link{background:none;border:none;color:var(--muted);font-size:13px;cursor:pointer;padding:0;margin:0}
|
| 34 |
button.link:hover{color:var(--text)}
|
| 35 |
.btn-danger{background:transparent;color:var(--red);border:0.5px solid #4a2018;padding:6px 12px;font-size:12px;border-radius:100px;cursor:pointer;margin:0}
|
| 36 |
.muted{color:var(--muted);font-size:14px;line-height:1.55;margin-bottom:0.75rem}
|
| 37 |
.muted b{color:var(--text);font-weight:500}.muted a{color:var(--green)}
|
| 38 |
.err{background:var(--red-bg);color:var(--red);border:0.5px solid #4a2018;border-radius:9px;padding:10px 13px;font-size:13px;margin-bottom:1.25rem}
|
| 39 |
.notice{background:var(--green-bg);color:var(--green);border:0.5px solid var(--green-dim);border-radius:9px;padding:10px 13px;font-size:13px;margin-bottom:1.25rem}
|
| 40 |
|
| 41 |
/* admin */
|
| 42 |
.topbar{display:flex;align-items:center;justify-content:space-between;padding:1rem 2rem;border-bottom:0.5px solid var(--border);position:sticky;top:0;background:rgba(10,10,12,.85);backdrop-filter:blur(10px);z-index:10}
|
| 43 |
.topnav{display:flex;align-items:center;gap:1.5rem}
|
| 44 |
.topnav a{font-size:13px;color:var(--muted)}.topnav a:hover{color:var(--text)}
|
| 45 |
.topnav form{margin:0}
|
| 46 |
.admin{max-width:900px;margin:0 auto;padding:2.5rem 2rem}
|
| 47 |
.admin h2{font-family:var(--serif);font-size:20px;font-weight:500;margin:2rem 0 1rem;letter-spacing:-.3px}
|
| 48 |
table.admin-t{width:100%;border-collapse:collapse;font-size:13px;margin-bottom:1rem}
|
| 49 |
table.admin-t th{text-align:left;padding:9px 10px;font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--dim);border-bottom:0.5px solid var(--border)}
|
| 50 |
table.admin-t td{padding:10px;border-bottom:0.5px solid var(--border);color:var(--muted);vertical-align:middle}
|
| 51 |
table.admin-t td.mono{font-family:var(--mono);color:var(--text)}
|
| 52 |
.stat-row{display:flex;gap:12px;margin:1.5rem 0;flex-wrap:wrap}
|
| 53 |
.stat{flex:1;min-width:120px;background:var(--surface);border:0.5px solid var(--border);border-radius:12px;padding:1.1rem 1.25rem}
|
| 54 |
.stat .n{font-family:var(--serif);font-size:30px;font-weight:500;letter-spacing:-1px}
|
| 55 |
.stat .l{font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:var(--dim);margin-top:4px}
|
| 56 |
.form-inline{display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end;background:var(--surface);border:0.5px solid var(--border);border-radius:12px;padding:1.25rem;margin-bottom:1rem}
|
| 57 |
.form-inline label{margin:0;flex:1;min-width:140px}
|
| 58 |
.form-inline .btnwrap{display:flex;gap:8px;align-items:center}
|
| 59 |
.form-inline button{margin-top:0}
|
| 60 |
.pill{font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:1px;padding:2px 9px;border-radius:100px;border:0.5px solid var(--border-strong);color:var(--dim);vertical-align:middle}
|
| 61 |
.pill.int{color:var(--green);border-color:var(--green-dim)}
|
| 62 |
a.editlink{color:var(--green);font-family:var(--mono);font-size:11px}
|
| 63 |
`
|
| 64 |
|
| 65 |
var tmpl = template.Must(template.New("").Parse(`
|
| 66 |
{{define "head"}}<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
|
| 67 |
<meta name="viewport" content="width=device-width,initial-scale=1"><title>{{.Title}} · Spread</title><style>` + pageCSS + `</style></head><body><div class="wrap">{{end}}
|
| 68 |
{{define "foot"}}</div></body></html>{{end}}
|
| 69 |
|
| 70 |
{{define "ahead"}}<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
|
| 71 |
<meta name="viewport" content="width=device-width,initial-scale=1"><title>{{.Title}} · Spread admin</title><style>` + pageCSS + `</style></head><body>
|
| 72 |
<div class="topbar">
|
| 73 |
<div class="brand">Spread<span>●</span> <em>· admin</em></div>
|
| 74 |
<nav class="topnav">
|
| 75 |
<a href="/admin">Overview</a>
|
| 76 |
<a href="/admin/catalog">Catalog</a>
|
| 77 |
<a href="/admin/subscribers">Subscribers</a>
|
| 78 |
<a href="/admin/boms">BOMs</a>
|
| 79 |
<form method="post" action="/logout"><input type="hidden" name="csrf_token" value="{{.CSRF}}"><button class="link">Log out</button></form>
|
| 80 |
</nav>
|
| 81 |
</div>
|
| 82 |
<main class="admin">{{end}}
|
| 83 |
{{define "afoot"}}</main></body></html>{{end}}
|
| 84 |
|
| 85 |
{{define "login"}}{{template "head" .}}
|
| 86 |
<div class="card">
|
| 87 |
<div class="brand">Spread<span>●</span></div>
|
| 88 |
<h1>Sign in</h1>
|
| 89 |
{{if .Error}}<div class="err">{{.Error}}</div>{{end}}
|
| 90 |
<form method="post" action="/login">
|
| 91 |
<input type="hidden" name="csrf_token" value="{{.CSRF}}">
|
| 92 |
<label>Email<input type="email" name="email" value="{{.Email}}" required autofocus></label>
|
| 93 |
<label>Password<input type="password" name="password" required></label>
|
| 94 |
<button type="submit">Sign in →</button>
|
| 95 |
</form>
|
| 96 |
<p class="muted" style="margin-top:1rem"><a href="/forgot">Forgot your password?</a></p>
|
| 97 |
</div>{{template "foot" .}}{{end}}
|
| 98 |
|
| 99 |
{{define "chead"}}<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
|
| 100 |
<meta name="viewport" content="width=device-width,initial-scale=1"><title>{{.Title}} · Spread</title>
|
| 101 |
<style>` + pageCSS + `</style></head><body>
|
| 102 |
<div class="topbar">
|
| 103 |
<div class="brand">Spread<span>●</span></div>
|
| 104 |
<nav class="topnav">
|
| 105 |
<a href="/app">Your BOMs</a>
|
| 106 |
<a href="/">Public demo</a>
|
| 107 |
<form method="post" action="/logout"><input type="hidden" name="csrf_token" value="{{.CSRF}}"><button class="link">Log out</button></form>
|
| 108 |
</nav>
|
| 109 |
</div>
|
| 110 |
<main class="admin">{{end}}
|
| 111 |
{{define "cfoot"}}</main></body></html>{{end}}
|
| 112 |
|
| 113 |
{{define "app"}}{{template "chead" .}}
|
| 114 |
<h1>Your BOMs</h1>
|
| 115 |
{{if .Notice}}<div class="notice">{{.Notice}}</div>{{end}}
|
| 116 |
{{if .Error}}<div class="err">{{.Error}}</div>{{end}}
|
| 117 |
<form class="form-inline" method="post" action="/app/analyze" enctype="multipart/form-data">
|
| 118 |
<input type="hidden" name="csrf_token" value="{{.CSRF}}">
|
| 119 |
<label>Upload a BOM (.csv or .xlsx)<input type="file" name="bom" accept=".csv,.tsv,.txt,.xlsx" required></label>
|
| 120 |
<div class="btnwrap"><button type="submit">Re-quote →</button></div>
|
| 121 |
</form>
|
| 122 |
<p class="muted">Signed in as <b>{{.User.Email}}</b> — <b>{{.Sub.Name}}</b>. Uploads are re-quoted against the live catalog and saved here.</p>
|
| 123 |
<table class="admin-t">
|
| 124 |
<thead><tr><th>BOM</th><th>Uploaded</th><th>Parts</th><th>Total savings</th><th>Net</th><th></th></tr></thead>
|
| 125 |
<tbody>
|
| 126 |
{{range .BOMs}}
|
| 127 |
<tr>
|
| 128 |
<td class="mono">{{.Ref}}</td>
|
| 129 |
<td>{{.CreatedAt}}</td>
|
| 130 |
<td>{{.Lines}}</td>
|
| 131 |
<td class="mono">{{printf "$%.2f" .TotalSavings}}</td>
|
| 132 |
<td class="mono" style="color:var(--green)">{{printf "$%.2f" .NetSavings}}</td>
|
| 133 |
<td style="text-align:right"><a class="editlink" href="/app/bom/{{.ID}}">view</a></td>
|
| 134 |
</tr>
|
| 135 |
{{else}}
|
| 136 |
<tr><td colspan="6" style="color:var(--dim)">No BOMs yet — upload one above to see it re-quoted.</td></tr>
|
| 137 |
{{end}}
|
| 138 |
</tbody>
|
| 139 |
</table>
|
| 140 |
{{template "cfoot" .}}{{end}}
|
| 141 |
|
| 142 |
{{define "bom"}}{{template "chead" .}}
|
| 143 |
{{with .BOM}}
|
| 144 |
<p class="muted"><a href="{{$.BackURL}}">← back</a></p>
|
| 145 |
<h1>{{.Ref}}</h1>
|
| 146 |
<p class="muted">{{.SubscriberName}} · {{.CreatedAt}} · {{.Lines}} parts{{if .Filename}} · {{.Filename}}{{end}}</p>
|
| 147 |
<div class="stat-row">
|
| 148 |
<div class="stat"><div class="n">{{printf "$%.2f" .TotalSavings}}</div><div class="l">Total savings</div></div>
|
| 149 |
<div class="stat"><div class="n">{{printf "$%.2f" .OurCut}}</div><div class="l">Our cut (20%)</div></div>
|
| 150 |
<div class="stat"><div class="n" style="color:var(--green)">{{printf "$%.2f" .NetSavings}}</div><div class="l">Net savings</div></div>
|
| 151 |
</div>
|
| 152 |
<table class="admin-t">
|
| 153 |
<thead><tr><th>Part</th><th>Qty</th><th>Market</th><th>Spread price</th><th>You save</th></tr></thead>
|
| 154 |
<tbody>
|
| 155 |
{{range .Items}}
|
| 156 |
<tr>
|
| 157 |
<td>{{.Description}}<br><span class="mono" style="font-size:11px;color:var(--dim)">{{.PartNumber}}{{if not .Matched}} · est.{{end}}</span></td>
|
| 158 |
<td class="mono">{{printf "%.0f" .Quantity}}</td>
|
| 159 |
<td class="mono">{{printf "$%.4f" .UnitPrice}}</td>
|
| 160 |
<td class="mono">{{printf "$%.4f" .SpreadPrice}}</td>
|
| 161 |
<td class="mono" style="color:var(--green)">{{printf "$%.2f" .LineSaving}}</td>
|
| 162 |
</tr>
|
| 163 |
{{end}}
|
| 164 |
</tbody>
|
| 165 |
</table>
|
| 166 |
{{end}}
|
| 167 |
{{template "cfoot" .}}{{end}}
|
| 168 |
|
| 169 |
{{define "boms"}}{{template "ahead" .}}
|
| 170 |
<h1>Uploaded BOMs</h1>
|
| 171 |
<table class="admin-t">
|
| 172 |
<thead><tr><th>BOM</th><th>Subscriber</th><th>Uploaded by</th><th>When</th><th>Parts</th><th>Savings</th><th></th></tr></thead>
|
| 173 |
<tbody>
|
| 174 |
{{range .BOMs}}
|
| 175 |
<tr>
|
| 176 |
<td class="mono">{{.Ref}}</td>
|
| 177 |
<td>{{.SubscriberName}}</td>
|
| 178 |
<td class="mono">{{.UploadedByEmail}}</td>
|
| 179 |
<td>{{.CreatedAt}}</td>
|
| 180 |
<td>{{.Lines}}</td>
|
| 181 |
<td class="mono">{{printf "$%.2f" .TotalSavings}}</td>
|
| 182 |
<td style="text-align:right"><a class="editlink" href="/app/bom/{{.ID}}">view</a></td>
|
| 183 |
</tr>
|
| 184 |
{{else}}
|
| 185 |
<tr><td colspan="7" style="color:var(--dim)">No BOMs uploaded yet.</td></tr>
|
| 186 |
{{end}}
|
| 187 |
</tbody>
|
| 188 |
</table>
|
| 189 |
{{template "afoot" .}}{{end}}
|
| 190 |
|
| 191 |
{{define "admin"}}{{template "ahead" .}}
|
| 192 |
<h1>Overview</h1>
|
| 193 |
<p class="muted">Signed in as <b>{{.User.Email}}</b> — <b>{{.Sub.Name}}</b>.</p>
|
| 194 |
<div class="stat-row">
|
| 195 |
<div class="stat"><div class="n">{{.Counts.Catalog}}</div><div class="l">Catalog parts</div></div>
|
| 196 |
<div class="stat"><div class="n">{{.Counts.Subs}}</div><div class="l">Subscribers</div></div>
|
| 197 |
<div class="stat"><div class="n">{{.Counts.Users}}</div><div class="l">Users</div></div>
|
| 198 |
<div class="stat"><div class="n">{{.Counts.Boms}}</div><div class="l">BOMs uploaded</div></div>
|
| 199 |
</div>
|
| 200 |
<p class="muted"><a href="/admin/catalog">Manage catalog →</a></p>
|
| 201 |
<p class="muted"><a href="/admin/subscribers">Manage subscribers & users →</a></p>
|
| 202 |
<p class="muted"><a href="/admin/boms">Review uploaded BOMs →</a></p>
|
| 203 |
{{template "afoot" .}}{{end}}
|
| 204 |
|
| 205 |
{{define "catalog"}}{{template "ahead" .}}
|
| 206 |
<h1>Catalog</h1>
|
| 207 |
{{if .Notice}}<div class="notice">{{.Notice}}</div>{{end}}
|
| 208 |
{{if .Error}}<div class="err">{{.Error}}</div>{{end}}
|
| 209 |
<form class="form-inline" method="post" action="/admin/catalog">
|
| 210 |
<input type="hidden" name="csrf_token" value="{{.CSRF}}">
|
| 211 |
<label>Part number<input name="part_number" value="{{with .Edit}}{{.PartNumber}}{{end}}"{{if .Edit}} readonly{{end}} required></label>
|
| 212 |
<label>Description<input name="description" value="{{with .Edit}}{{.Description}}{{end}}"></label>
|
| 213 |
<label>Spread price<input name="spread_price" type="number" step="0.0001" min="0" value="{{with .Edit}}{{printf "%.4f" .SpreadPrice}}{{end}}" required></label>
|
| 214 |
<div class="btnwrap">
|
| 215 |
<button type="submit">{{if .Edit}}Update{{else}}Add part{{end}}</button>
|
| 216 |
{{if .Edit}}<a class="btn-secondary btn-sm" href="/admin/catalog">Cancel</a>{{end}}
|
| 217 |
</div>
|
| 218 |
</form>
|
| 219 |
<table class="admin-t">
|
| 220 |
<thead><tr><th>Part number</th><th>Description</th><th>Spread price</th><th></th></tr></thead>
|
| 221 |
<tbody>
|
| 222 |
{{range .Catalog}}
|
| 223 |
<tr>
|
| 224 |
<td class="mono">{{.PartNumber}}</td>
|
| 225 |
<td>{{.Description}}</td>
|
| 226 |
<td class="mono">{{printf "$%.4f" .SpreadPrice}}</td>
|
| 227 |
<td style="text-align:right;white-space:nowrap">
|
| 228 |
<a class="editlink" href="/admin/catalog?edit={{.PartNumber}}">edit</a>
|
| 229 |
<form method="post" action="/admin/catalog/delete" style="display:inline;margin-left:12px">
|
| 230 |
<input type="hidden" name="csrf_token" value="{{$.CSRF}}">
|
| 231 |
<input type="hidden" name="part_number" value="{{.PartNumber}}">
|
| 232 |
<button class="btn-danger" type="submit">delete</button>
|
| 233 |
</form>
|
| 234 |
</td>
|
| 235 |
</tr>
|
| 236 |
{{else}}
|
| 237 |
<tr><td colspan="4" style="color:var(--dim)">No catalog entries yet.</td></tr>
|
| 238 |
{{end}}
|
| 239 |
</tbody>
|
| 240 |
</table>
|
| 241 |
<form method="post" action="/admin/catalog/seed"><input type="hidden" name="csrf_token" value="{{.CSRF}}"><button class="btn-secondary btn-sm">{{if .Catalog}}Re-seed demo parts{{else}}Seed demo catalog{{end}}</button></form>
|
| 242 |
{{template "afoot" .}}{{end}}
|
| 243 |
|
| 244 |
{{define "subscribers"}}{{template "ahead" .}}
|
| 245 |
<h1>Subscribers & users</h1>
|
| 246 |
{{if .Notice}}<div class="notice">{{.Notice}}</div>{{end}}
|
| 247 |
{{if .Error}}<div class="err">{{.Error}}</div>{{end}}
|
| 248 |
|
| 249 |
<h2>Add a subscriber</h2>
|
| 250 |
<form class="form-inline" method="post" action="/admin/subscribers">
|
| 251 |
<input type="hidden" name="csrf_token" value="{{.CSRF}}">
|
| 252 |
<label>Company / subscriber name<input name="name" required></label>
|
| 253 |
<div class="btnwrap"><button type="submit">Create subscriber</button></div>
|
| 254 |
</form>
|
| 255 |
|
| 256 |
<h2>Add a user</h2>
|
| 257 |
<form class="form-inline" method="post" action="/admin/users">
|
| 258 |
<input type="hidden" name="csrf_token" value="{{.CSRF}}">
|
| 259 |
<label>Subscriber<select name="subscriber_id" required>{{range .Subs}}<option value="{{.ID}}">{{.Name}}{{if .IsInternal}} (internal){{end}}</option>{{end}}</select></label>
|
| 260 |
<label>Email<input type="email" name="email" required></label>
|
| 261 |
<label>Name<input name="name"></label>
|
| 262 |
<label>Password <span style="color:var(--dim)">(blank = email an invite)</span><input type="password" name="password" minlength="8"></label>
|
| 263 |
<div class="btnwrap"><button type="submit">Create user</button></div>
|
| 264 |
</form>
|
| 265 |
|
| 266 |
{{range .Subs}}
|
| 267 |
<h2>{{.Name}} {{if .IsInternal}}<span class="pill int">internal</span>{{else}}<span class="pill">customer</span>{{end}}</h2>
|
| 268 |
<table class="admin-t">
|
| 269 |
<thead><tr><th>Email</th><th>Name</th><th>User ID</th></tr></thead>
|
| 270 |
<tbody>
|
| 271 |
{{range .Users}}<tr><td class="mono">{{.Email}}</td><td>{{.Name}}</td><td class="mono">{{.ID}}</td></tr>
|
| 272 |
{{else}}<tr><td colspan="3" style="color:var(--dim)">No users yet.</td></tr>{{end}}
|
| 273 |
</tbody>
|
| 274 |
</table>
|
| 275 |
{{end}}
|
| 276 |
{{template "afoot" .}}{{end}}
|
| 277 |
|
| 278 |
{{define "forgot"}}{{template "head" .}}
|
| 279 |
<div class="card">
|
| 280 |
<div class="brand">Spread<span>●</span></div>
|
| 281 |
<h1>Reset password</h1>
|
| 282 |
{{if .Notice}}<div class="notice">{{.Notice}}</div>{{end}}
|
| 283 |
<form method="post" action="/forgot">
|
| 284 |
<input type="hidden" name="csrf_token" value="{{.CSRF}}">
|
| 285 |
<label>Email<input type="email" name="email" required autofocus></label>
|
| 286 |
<button type="submit">Send reset link →</button>
|
| 287 |
</form>
|
| 288 |
<p class="muted" style="margin-top:1rem"><a href="/login">Back to sign in</a></p>
|
| 289 |
</div>{{template "foot" .}}{{end}}
|
| 290 |
|
| 291 |
{{define "setpassword"}}{{template "head" .}}
|
| 292 |
<div class="card">
|
| 293 |
<div class="brand">Spread<span>●</span></div>
|
| 294 |
<h1>Set your password</h1>
|
| 295 |
{{if .Error}}<div class="err">{{.Error}}</div>{{end}}
|
| 296 |
<form method="post" action="/set-password">
|
| 297 |
<input type="hidden" name="csrf_token" value="{{.CSRF}}">
|
| 298 |
<input type="hidden" name="token" value="{{.Token}}">
|
| 299 |
<label>New password<input type="password" name="password" required minlength="8" autofocus></label>
|
| 300 |
<label>Confirm password<input type="password" name="confirm" required minlength="8"></label>
|
| 301 |
<button type="submit">Set password →</button>
|
| 302 |
</form>
|
| 303 |
</div>{{template "foot" .}}{{end}}
|
| 304 |
|
| 305 |
{{define "tokenerror"}}{{template "head" .}}
|
| 306 |
<div class="card">
|
| 307 |
<div class="brand">Spread<span>●</span></div>
|
| 308 |
<h1>Link expired</h1>
|
| 309 |
<p class="muted">This link is invalid or has already been used. Ask an admin to re-send your invite, or <a href="/forgot">request a new reset link</a>.</p>
|
| 310 |
</div>{{template "foot" .}}{{end}}
|
| 311 |
`))
|