Monorepo for Tangled

appview/pages: templatize login, signup, complete-signup pages

these pages were full html pages instead of blocks. as a result, their
meta values were not in sync with the rest of the site. by introducing
layouts/loginbase, we can now share the boilerplate.

of note is that the signup page needs an extra line in the `<head>`: the
script to load a cloudflare turnstile. this is supplied using
`extrameta`

Signed-off-by: oppiliappan <me@oppi.li>

authored by

oppiliappan and committed by tangled.org c983de83 dc2cc2b3

+263 -294
+21 -3
appview/pages/pages.go
··· 178 return p.parse(stack...) 179 } 180 181 func (p *Pages) executePlain(name string, w io.Writer, params any) error { 182 tpl, err := p.parse(name) 183 if err != nil { ··· 185 } 186 187 return tpl.Execute(w, params) 188 } 189 190 func (p *Pages) execute(name string, w io.Writer, params any) error { ··· 237 } 238 239 func (p *Pages) Login(w io.Writer, params LoginParams) error { 240 - return p.executePlain("user/login", w, params) 241 } 242 243 type SignupParams struct { ··· 245 } 246 247 func (p *Pages) Signup(w io.Writer, params SignupParams) error { 248 - return p.executePlain("user/signup", w, params) 249 } 250 251 func (p *Pages) CompleteSignup(w io.Writer) error { 252 - return p.executePlain("user/completeSignup", w, nil) 253 } 254 255 type TermsOfServiceParams struct {
··· 178 return p.parse(stack...) 179 } 180 181 + func (p *Pages) parseLoginBase(top string) (*template.Template, error) { 182 + stack := []string{ 183 + "layouts/base", 184 + "layouts/loginbase", 185 + top, 186 + } 187 + return p.parse(stack...) 188 + } 189 + 190 func (p *Pages) executePlain(name string, w io.Writer, params any) error { 191 tpl, err := p.parse(name) 192 if err != nil { ··· 194 } 195 196 return tpl.Execute(w, params) 197 + } 198 + 199 + func (p *Pages) executeLogin(name string, w io.Writer, params any) error { 200 + tpl, err := p.parseLoginBase(name) 201 + if err != nil { 202 + return err 203 + } 204 + 205 + return tpl.ExecuteTemplate(w, "layouts/base", params) 206 } 207 208 func (p *Pages) execute(name string, w io.Writer, params any) error { ··· 255 } 256 257 func (p *Pages) Login(w io.Writer, params LoginParams) error { 258 + return p.executeLogin("user/login", w, params) 259 } 260 261 type SignupParams struct { ··· 263 } 264 265 func (p *Pages) Signup(w io.Writer, params SignupParams) error { 266 + return p.executeLogin("user/signup", w, params) 267 } 268 269 func (p *Pages) CompleteSignup(w io.Writer) error { 270 + return p.executeLogin("user/completeSignup", w, nil) 271 } 272 273 type TermsOfServiceParams struct {
+26
appview/pages/templates/layouts/loginbase.html
···
··· 1 + {{ define "mainLayout" }} 2 + <div class="w-full h-screen flex items-center justify-center bg-white dark:bg-transparent"> 3 + <main class="max-w-md px-7 mt-4"> 4 + {{ template "logo" }} 5 + {{ block "content" . }}{{ end }} 6 + </main> 7 + </div> 8 + {{ end }} 9 + 10 + {{ define "topbarLayout" }} 11 + <div class="hidden"></div> 12 + {{ end }} 13 + 14 + {{ define "footerLayout" }} 15 + <div class="hidden"></div> 16 + {{ end }} 17 + 18 + {{ define "logo" }} 19 + <h1 class="flex place-content-center text-3xl font-semibold italic dark:text-white" > 20 + {{ template "fragments/logotype" }} 21 + </h1> 22 + <h2 class="text-center text-xl italic dark:text-white"> 23 + tightly-knit social coding. 24 + </h2> 25 + {{ end }} 26 +
+62 -99
appview/pages/templates/user/completeSignup.html
··· 1 - {{ define "user/completeSignup" }} 2 - <!doctype html> 3 - <html lang="en" class="dark:bg-gray-900"> 4 - <head> 5 - <meta charset="UTF-8" /> 6 - <meta 7 - name="viewport" 8 - content="width=device-width, initial-scale=1.0" 9 - /> 10 - <meta 11 - property="og:title" 12 - content="complete signup · tangled" 13 - /> 14 - <meta 15 - property="og:url" 16 - content="https://tangled.org/complete-signup" 17 - /> 18 - <meta 19 - property="og:description" 20 - content="complete your signup for tangled" 21 - /> 22 - <script src="/static/htmx.min.js"></script> 23 - <link rel="manifest" href="/pwa-manifest.json" /> 24 - <link 25 - rel="stylesheet" 26 - href="/static/tw.css?{{ cssContentHash }}" 27 - type="text/css" 28 - /> 29 - <title>complete signup &middot; tangled</title> 30 - </head> 31 - <body class="flex items-center justify-center min-h-screen"> 32 - <main class="max-w-md px-6 -mt-4"> 33 - <h1 class="flex place-content-center text-2xl font-semibold italic dark:text-white" > 34 - {{ template "fragments/logotype" }} 35 - </h1> 36 - <h2 class="text-center text-xl italic dark:text-white"> 37 - tightly-knit social coding. 38 - </h2> 39 - <form 40 - class="mt-4 max-w-sm mx-auto flex flex-col gap-4" 41 - hx-post="/signup/complete" 42 - hx-swap="none" 43 - hx-disabled-elt="#complete-signup-button" 44 - > 45 - <div class="flex flex-col"> 46 - <label for="code">verification code</label> 47 - <input 48 - type="text" 49 - id="code" 50 - name="code" 51 - tabindex="1" 52 - required 53 - placeholder="tngl-sh-foo-bar" 54 - /> 55 - <span class="text-sm text-gray-500 mt-1"> 56 - Enter the code sent to your email. 57 - </span> 58 - </div> 59 60 - <div class="flex flex-col"> 61 - <label for="username">username</label> 62 - <input 63 - type="text" 64 - id="username" 65 - name="username" 66 - tabindex="2" 67 - required 68 - placeholder="jason" 69 - /> 70 - <span class="text-sm text-gray-500 mt-1"> 71 - Your complete handle will be of the form <code>user.tngl.sh</code>. 72 - </span> 73 - </div> 74 75 - <div class="flex flex-col"> 76 - <label for="password">password</label> 77 - <input 78 - type="password" 79 - id="password" 80 - name="password" 81 - tabindex="3" 82 - required 83 - /> 84 - <span class="text-sm text-gray-500 mt-1"> 85 - Choose a strong password for your account. 86 - </span> 87 - </div> 88 89 - <button 90 - class="btn-create w-full my-2 mt-6 text-base" 91 - type="submit" 92 - id="complete-signup-button" 93 - tabindex="4" 94 - > 95 - <span>complete signup</span> 96 - </button> 97 - </form> 98 - <p id="signup-error" class="error w-full"></p> 99 - <p id="signup-msg" class="dark:text-white w-full"></p> 100 - </main> 101 - </body> 102 - </html> 103 {{ end }}
··· 1 + {{ define "title" }} complete signup {{ end }} 2 + 3 + {{ define "content" }} 4 + <form 5 + class="mt-4 max-w-sm mx-auto flex flex-col gap-4 group" 6 + hx-post="/signup/complete" 7 + hx-swap="none" 8 + hx-disabled-elt="#complete-signup-button" 9 + > 10 + <div class="flex flex-col"> 11 + <label for="code">verification code</label> 12 + <input 13 + type="text" 14 + id="code" 15 + name="code" 16 + tabindex="1" 17 + required 18 + placeholder="tngl-sh-foo-bar" 19 + /> 20 + <span class="text-sm text-gray-500 mt-1"> 21 + Enter the code sent to your email. 22 + </span> 23 + </div> 24 25 + <div class="flex flex-col"> 26 + <label for="username">username</label> 27 + <input 28 + type="text" 29 + id="username" 30 + name="username" 31 + tabindex="2" 32 + required 33 + placeholder="jason" 34 + /> 35 + <span class="text-sm text-gray-500 mt-1"> 36 + Your complete handle will be of the form <code>user.tngl.sh</code>. 37 + </span> 38 + </div> 39 40 + <div class="flex flex-col"> 41 + <label for="password">password</label> 42 + <input 43 + type="password" 44 + id="password" 45 + name="password" 46 + tabindex="3" 47 + required 48 + /> 49 + <span class="text-sm text-gray-500 mt-1"> 50 + Choose a strong password for your account. 51 + </span> 52 + </div> 53 54 + <button 55 + class="btn-create w-full my-2 mt-6 text-base" 56 + type="submit" 57 + id="complete-signup-button" 58 + tabindex="4" 59 + > 60 + {{ i "loader-circle" "size-4 animate-spin hidden group-[.htmx-request]:inline" }} 61 + <span class="inline group-[.htmx-request]:hidden">complete signup</span> 62 + </button> 63 + </form> 64 + <p id="signup-error" class="error w-full"></p> 65 + <p id="signup-msg" class="dark:text-white w-full"></p> 66 {{ end }}
+111 -132
appview/pages/templates/user/login.html
··· 1 - {{ define "user/login" }} 2 - <!doctype html> 3 - <html lang="en" class="dark:bg-gray-900"> 4 - <head> 5 - <meta charset="UTF-8" /> 6 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 - <meta property="og:title" content="login · tangled" /> 8 - <meta property="og:url" content="https://tangled.org/login" /> 9 - <meta property="og:description" content="login to for tangled" /> 10 - <script src="/static/htmx.min.js"></script> 11 - <link rel="manifest" href="/pwa-manifest.json" /> 12 - <link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" /> 13 - <title>login &middot; tangled</title> 14 - </head> 15 - <body class="flex items-center justify-center min-h-screen"> 16 - <main class="max-w-md px-7 mt-4"> 17 - <h1 class="flex place-content-center text-3xl font-semibold italic dark:text-white" > 18 - {{ template "fragments/logotype" }} 19 - </h1> 20 - <h2 class="text-center text-xl italic dark:text-white"> 21 - tightly-knit social coding. 22 - </h2> 23 24 - {{ if .AddAccount }} 25 - <div class="flex gap-2 my-4 bg-blue-50 dark:bg-blue-900/30 border border-blue-300 dark:border-sky-800 rounded px-3 py-2 text-blue-600 dark:text-blue-300"> 26 - <span class="py-1">{{ i "user-plus" "w-4 h-4" }}</span> 27 - <div> 28 - <h5 class="font-medium">Add another account</h5> 29 - <p class="text-sm">Sign in with a different account to add it to your account list.</p> 30 - </div> 31 - </div> 32 - {{ end }} 33 34 - {{ if and .LoggedInUser .LoggedInUser.Accounts }} 35 - {{ $accounts := .LoggedInUser.Accounts }} 36 - {{ if $accounts }} 37 - <div class="my-4 border border-gray-200 dark:border-gray-700 rounded overflow-hidden"> 38 - <div class="px-3 py-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"> 39 - <span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide font-medium">Saved accounts</span> 40 - </div> 41 - <div class="divide-y divide-gray-200 dark:divide-gray-700"> 42 - {{ range $accounts }} 43 - <div class="flex items-center justify-between px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"> 44 - <button 45 - type="button" 46 - hx-post="/account/switch" 47 - hx-vals='{"did": "{{ .Did }}"}' 48 - hx-swap="none" 49 - class="flex items-center gap-2 flex-1 text-left min-w-0" 50 - > 51 - {{ template "user/fragments/pic" (list .Did "size-8") }} 52 - <div class="flex flex-col min-w-0"> 53 - <span class="text-sm font-medium dark:text-white truncate">{{ .Did | resolve | truncateAt30 }}</span> 54 - <span class="text-xs text-gray-500 dark:text-gray-400">Click to switch</span> 55 - </div> 56 - </button> 57 - <button 58 - type="button" 59 - hx-delete="/account/{{ .Did }}" 60 - hx-swap="none" 61 - class="p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 flex-shrink-0" 62 - title="Remove account" 63 - > 64 - {{ i "x" "w-4 h-4" }} 65 - </button> 66 - </div> 67 - {{ end }} 68 - </div> 69 - </div> 70 - {{ end }} 71 - {{ end }} 72 73 - <form 74 - class="mt-4" 75 - hx-post="/login" 76 - hx-swap="none" 77 - hx-disabled-elt="#login-button" 78 - > 79 - <div class="flex flex-col"> 80 - <label for="handle">handle</label> 81 - <input 82 - autocapitalize="none" 83 - autocorrect="off" 84 - autocomplete="username" 85 - type="text" 86 - id="handle" 87 - name="handle" 88 - tabindex="1" 89 - required 90 - placeholder="akshay.tngl.sh" 91 - /> 92 - <span class="text-sm text-gray-500 mt-1"> 93 - Use your <a href="https://atproto.com">AT Protocol</a> 94 - handle to log in. If you're unsure, this is likely 95 - your Tangled (<code>.tngl.sh</code>) or <a href="https://bsky.app">Bluesky</a> (<code>.bsky.social</code>) account. 96 - </span> 97 - </div> 98 - <input type="hidden" name="return_url" value="{{ .ReturnUrl }}"> 99 - <input type="hidden" name="add_account" value="{{ if .AddAccount }}true{{ end }}"> 100 101 - <button 102 - class="btn w-full my-2 mt-6 text-base " 103 - type="submit" 104 - id="login-button" 105 - tabindex="3" 106 - > 107 - <span>login</span> 108 - </button> 109 - </form> 110 - {{ if .ErrorCode }} 111 - <div class="flex gap-2 my-2 bg-red-50 dark:bg-red-900 border border-red-500 rounded drop-shadow-sm px-3 py-2 text-red-500 dark:text-red-300"> 112 - <span class="py-1">{{ i "circle-alert" "w-4 h-4" }}</span> 113 - <div> 114 - <h5 class="font-medium">Login error</h5> 115 - <p class="text-sm"> 116 - {{ if eq .ErrorCode "access_denied" }} 117 - You have not authorized the app. 118 - {{ else if eq .ErrorCode "session" }} 119 - Server failed to create user session. 120 - {{ else if eq .ErrorCode "max_accounts" }} 121 - You have reached the maximum of 20 linked accounts. Please remove an account before adding a new one. 122 - {{ else }} 123 - Internal Server error. 124 - {{ end }} 125 - Please try again. 126 - </p> 127 - </div> 128 - </div> 129 - {{ end }} 130 - <p class="text-sm text-gray-500"> 131 - Don't have an account? <a href="/signup" class="underline">Create an account</a> on Tangled now! 132 - </p> 133 134 - <p id="login-msg" class="error w-full"></p> 135 - </main> 136 - </body> 137 - </html> 138 {{ end }}
··· 1 + {{ define "title" }} login {{ end }} 2 3 + {{ define "content" }} 4 + {{ if .AddAccount }} 5 + <div class="flex gap-2 my-4 bg-blue-50 dark:bg-blue-900/30 border border-blue-300 dark:border-sky-800 rounded px-3 py-2 text-blue-600 dark:text-blue-300"> 6 + <span class="py-1">{{ i "user-plus" "w-4 h-4" }}</span> 7 + <div> 8 + <h5 class="font-medium">Add another account</h5> 9 + <p class="text-sm">Sign in with a different account to add it to your account list.</p> 10 + </div> 11 + </div> 12 + {{ end }} 13 14 + {{ if and .LoggedInUser .LoggedInUser.Accounts }} 15 + {{ $accounts := .LoggedInUser.Accounts }} 16 + {{ if $accounts }} 17 + <div class="my-4 border border-gray-200 dark:border-gray-700 rounded overflow-hidden"> 18 + <div class="px-3 py-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"> 19 + <span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide font-medium">Saved accounts</span> 20 + </div> 21 + <div class="divide-y divide-gray-200 dark:divide-gray-700"> 22 + {{ range $accounts }} 23 + <div class="flex items-center justify-between px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"> 24 + <button 25 + type="button" 26 + hx-post="/account/switch" 27 + hx-vals='{"did": "{{ .Did }}"}' 28 + hx-swap="none" 29 + class="flex items-center gap-2 flex-1 text-left min-w-0" 30 + > 31 + {{ template "user/fragments/pic" (list .Did "size-8") }} 32 + <div class="flex flex-col min-w-0"> 33 + <span class="text-sm font-medium dark:text-white truncate">{{ .Did | resolve | truncateAt30 }}</span> 34 + <span class="text-xs text-gray-500 dark:text-gray-400">Click to switch</span> 35 + </div> 36 + </button> 37 + <button 38 + type="button" 39 + hx-delete="/account/{{ .Did }}" 40 + hx-swap="none" 41 + class="p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 flex-shrink-0" 42 + title="Remove account" 43 + > 44 + {{ i "x" "w-4 h-4" }} 45 + </button> 46 + </div> 47 + {{ end }} 48 + </div> 49 + </div> 50 + {{ end }} 51 + {{ end }} 52 53 + <form 54 + class="mt-4 group" 55 + hx-post="/login" 56 + hx-swap="none" 57 + hx-disabled-elt="#login-button" 58 + > 59 + <div class="flex flex-col"> 60 + <label for="handle">handle</label> 61 + <input 62 + autocapitalize="none" 63 + autocorrect="off" 64 + autocomplete="username" 65 + type="text" 66 + id="handle" 67 + name="handle" 68 + tabindex="1" 69 + required 70 + placeholder="akshay.tngl.sh" 71 + /> 72 + <span class="text-sm text-gray-500 mt-1"> 73 + Use your <a href="https://atproto.com">AT Protocol</a> 74 + handle to log in. If you're unsure, this is likely 75 + your Tangled (<code>.tngl.sh</code>) or <a href="https://bsky.app">Bluesky</a> (<code>.bsky.social</code>) account. 76 + </span> 77 + </div> 78 + <input type="hidden" name="return_url" value="{{ .ReturnUrl }}"> 79 + <input type="hidden" name="add_account" value="{{ if .AddAccount }}true{{ end }}"> 80 81 + <button 82 + class="btn w-full my-2 mt-6 text-base" 83 + type="submit" 84 + id="login-button" 85 + tabindex="3" 86 + > 87 + {{ i "loader-circle" "size-4 animate-spin hidden group-[.htmx-request]:inline" }} 88 + <span class="inline group-[.htmx-request]:hidden">login</span> 89 + </button> 90 + </form> 91 + {{ if .ErrorCode }} 92 + <div class="flex gap-2 my-2 bg-red-50 dark:bg-red-900 border border-red-500 rounded drop-shadow-sm px-3 py-2 text-red-500 dark:text-red-300"> 93 + <span class="py-1">{{ i "circle-alert" "w-4 h-4" }}</span> 94 + <div> 95 + <h5 class="font-medium">Login error</h5> 96 + <p class="text-sm"> 97 + {{ if eq .ErrorCode "access_denied" }} 98 + You have not authorized the app. 99 + {{ else if eq .ErrorCode "session" }} 100 + Server failed to create user session. 101 + {{ else if eq .ErrorCode "max_accounts" }} 102 + You have reached the maximum of 20 linked accounts. Please remove an account before adding a new one. 103 + {{ else }} 104 + Internal Server error. 105 + {{ end }} 106 + Please try again. 107 + </p> 108 + </div> 109 + </div> 110 + {{ end }} 111 + <p class="text-sm text-gray-500"> 112 + Don't have an account? <a href="/signup" class="underline">Create an account</a> on Tangled now! 113 + </p> 114 115 + <p id="login-msg" class="error w-full"></p> 116 {{ end }} 117 +
+43 -60
appview/pages/templates/user/signup.html
··· 1 - {{ define "user/signup" }} 2 - <!doctype html> 3 - <html lang="en" class="dark:bg-gray-900"> 4 - <head> 5 - <meta charset="UTF-8" /> 6 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 - <meta property="og:title" content="signup · tangled" /> 8 - <meta property="og:url" content="https://tangled.org/signup" /> 9 - <meta property="og:description" content="sign up for tangled" /> 10 - <script src="/static/htmx.min.js"></script> 11 - <link rel="manifest" href="/pwa-manifest.json" /> 12 - <link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" /> 13 - <title>sign up &middot; tangled</title> 14 15 - <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> 16 - </head> 17 - <body class="flex items-center justify-center min-h-screen"> 18 - <main class="max-w-md px-6 -mt-4"> 19 - <h1 class="flex place-content-center text-2xl font-semibold italic dark:text-white" > 20 - {{ template "fragments/logotype" }} 21 - </h1> 22 - <h2 class="text-center text-xl italic dark:text-white">tightly-knit social coding.</h2> 23 - <form 24 - class="mt-4 max-w-sm mx-auto" 25 - hx-post="/signup" 26 - hx-swap="none" 27 - hx-disabled-elt="#signup-button" 28 - > 29 - <div class="flex flex-col mt-2"> 30 - <label for="email">email</label> 31 - <input 32 - type="email" 33 - id="email" 34 - name="email" 35 - tabindex="4" 36 - required 37 - placeholder="jason@bourne.co" 38 - /> 39 - </div> 40 - <span class="text-sm text-gray-500 mt-1"> 41 - You will receive an email with an invite code. Enter your 42 - invite code, desired username, and password in the next 43 - page to complete your registration. 44 - </span> 45 - <div class="w-full mt-4 text-center"> 46 - <div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}" data-size="flexible"></div> 47 - </div> 48 - <button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" > 49 - <span>join now</span> 50 - </button> 51 - <p class="text-sm text-gray-500"> 52 - Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>. 53 - </p> 54 55 - <p id="signup-msg" class="error w-full"></p> 56 - <p class="text-sm text-gray-500 pt-4"> 57 - By signing up, you agree to our <a href="/terms" class="underline">Terms of Service</a> and <a href="/privacy" class="underline">Privacy Policy</a>. 58 - </p> 59 - </form> 60 - </main> 61 - </body> 62 - </html> 63 {{ end }}
··· 1 + {{ define "title" }} signup {{ end }} 2 3 + {{ define "extrameta" }} 4 + <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> 5 + {{ end }} 6 + 7 + {{ define "content" }} 8 + <form 9 + class="mt-4 max-w-sm mx-auto group" 10 + hx-post="/signup" 11 + hx-swap="none" 12 + hx-disabled-elt="#signup-button" 13 + > 14 + <div class="flex flex-col mt-2"> 15 + <label for="email">email</label> 16 + <input 17 + type="email" 18 + id="email" 19 + name="email" 20 + tabindex="4" 21 + required 22 + placeholder="jason@bourne.co" 23 + /> 24 + </div> 25 + <span class="text-sm text-gray-500 mt-1"> 26 + You will receive an email with an invite code. Enter your 27 + invite code, desired username, and password in the next 28 + page to complete your registration. 29 + </span> 30 + <div class="w-full mt-4 text-center"> 31 + <div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}" data-size="flexible"></div> 32 + </div> 33 + <button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" > 34 + {{ i "loader-circle" "size-4 animate-spin hidden group-[.htmx-request]:inline" }} 35 + <span class="inline group-[.htmx-request]:hidden">join now</span> 36 + </button> 37 + <p class="text-sm text-gray-500"> 38 + Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>. 39 + </p> 40 41 + <p id="signup-msg" class="error w-full"></p> 42 + <p class="text-sm text-gray-500 pt-4"> 43 + By signing up, you agree to our <a href="/terms" class="underline">Terms of Service</a> and <a href="/privacy" class="underline">Privacy Policy</a>. 44 + </p> 45 + </form> 46 {{ end }}