feat: unify auth templates

This commit is contained in:
Ruidy 2025-09-20 16:08:24 +02:00
parent abd0642edd
commit c02501329a
No known key found for this signature in database
GPG key ID: 705C24D202990805
7 changed files with 490 additions and 172 deletions

View file

@ -32,6 +32,7 @@ type Server struct {
func New(cfg config.Config, logger *slog.Logger) (*Server, error) {
tmpl, err := template.ParseFS(
web.Templates,
"templates/auth_base.html",
"templates/login.html",
"templates/dashboard.html",
"templates/signup.html",

View file

@ -17,6 +17,8 @@ func (s *Server) render(w http.ResponseWriter, name string, data any) {
// PageData contains fields shared by the templates for now.
type PageData struct {
Title string
View string
Email string
Error string
Info string
@ -26,17 +28,29 @@ type PageData struct {
}
func newLoginData(email, errMsg, token string) PageData {
return PageData{Email: email, Error: errMsg, CSRFToken: token}
return PageData{Title: "Sign in · Auth Demo", View: "login", Email: email, Error: errMsg, CSRFToken: token}
}
func newUnauthorizedData(errMsg, token string) PageData {
return PageData{Error: errMsg, CSRFToken: token}
return PageData{
Title: "Access denied · Auth Demo",
View: "unauthorized",
Error: errMsg,
CSRFToken: token,
}
}
func newDashboardData(email, token, createdAt, createdAtISO string) PageData {
return PageData{Email: email, CSRFToken: token, CreatedAt: createdAt, CreatedAtISO: createdAtISO}
return PageData{
Title: "Dashboard · Auth Demo",
View: "dashboard",
Email: email,
CSRFToken: token,
CreatedAt: createdAt,
CreatedAtISO: createdAtISO,
}
}
func newSignupData(email, errMsg, token string) PageData {
return PageData{Email: email, Error: errMsg, CSRFToken: token}
return PageData{Title: "Create account · Auth Demo", View: "signup", Email: email, Error: errMsg, CSRFToken: token}
}

View file

@ -0,0 +1,272 @@
{{define "auth_base"}}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{if .Title}}{{.Title}}{{else}}Auth Demo{{end}}</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
/>
<style>
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
:root {
--pico-font-family: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
--pico-background-color: #f0f1f6;
--pico-color: #1f2033;
--pico-muted-color: #6c6f7f;
--pico-card-background-color: #ffffff;
--pico-border-radius: 1.75rem;
--pico-form-element-border-radius: 0.85rem;
--pico-primary: #6b3df0;
--pico-primary-hover: #5630c7;
--pico-primary-focus: rgba(107, 61, 240, 0.25);
--pico-primary-inverse: #ffffff;
--pico-form-element-active-border-color: #6b3df0;
--pico-shadow: 0 25px 45px rgba(15, 23, 42, 0.12);
}
body {
background: var(--pico-background-color);
}
.auth-wrapper {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: clamp(1.5rem, 4vw, 3rem);
}
.auth-card {
width: min(960px, 100%);
background: var(--pico-card-background-color);
border-radius: var(--pico-border-radius);
box-shadow: var(--pico-shadow);
overflow: hidden;
display: grid;
grid-template-columns: minmax(240px, 1fr) minmax(320px, 1.2fr);
}
.auth-visual {
color: var(--pico-primary-inverse);
background: linear-gradient(
to bottom,
rgba(19, 27, 52, 0.35),
rgba(19, 27, 52, 0.75)
),
url("https://images.unsplash.com/photo-1545239351-1141bd82e8a6?auto=format&fit=crop&w=900&q=80")
center/cover no-repeat;
padding: clamp(2rem, 4vw, 3rem);
display: flex;
flex-direction: column;
gap: 2rem;
}
.auth-logo {
display: inline-flex;
align-items: center;
gap: 0.75rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
font-size: 0.9rem;
}
.auth-logo-mark {
width: 2.25rem;
height: 2.25rem;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.45);
display: grid;
place-items: center;
}
.auth-quote blockquote {
margin: 0;
font-size: 1.1rem;
font-weight: 500;
line-height: 1.6;
}
.auth-quote cite {
display: block;
font-style: normal;
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.85);
margin-top: 0.85rem;
}
.auth-content {
padding: clamp(2.25rem, 5vw, 3.25rem);
display: flex;
flex-direction: column;
gap: 1.75rem;
}
.auth-heading h1 {
margin-bottom: 0.5rem;
font-size: clamp(1.9rem, 3vw, 2.35rem);
}
.auth-heading p {
margin: 0;
color: var(--pico-muted-color);
}
.auth-note {
border-radius: var(--pico-form-element-border-radius);
padding: 1rem 1.1rem;
background: color-mix(in srgb, var(--pico-primary) 12%, #ffffff);
border: 1px dashed color-mix(in srgb, var(--pico-primary) 45%, transparent);
font-size: 0.95rem;
color: var(--pico-muted-color);
}
.auth-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.auth-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
flex-wrap: wrap;
font-size: 0.9rem;
}
.auth-toggle {
display: inline-flex;
align-items: center;
gap: 0.45rem;
color: var(--pico-muted-color);
}
.auth-toggle input[type="checkbox"] {
accent-color: var(--pico-primary);
}
.auth-actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.auth-divider {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.9rem;
color: var(--pico-muted-color);
}
.auth-divider::before,
.auth-divider::after {
content: "";
flex: 1;
height: 1px;
background: color-mix(in srgb, var(--pico-muted-color) 25%, transparent);
}
.auth-google {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.6rem;
}
.auth-google svg {
flex-shrink: 0;
}
.auth-footer {
text-align: center;
font-size: 0.9rem;
color: var(--pico-muted-color);
}
@media (max-width: 960px) {
.auth-card {
grid-template-columns: 1fr;
}
.auth-visual {
min-height: 260px;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
}
</style>
</head>
<body>
<main class="auth-wrapper">
<article class="auth-card">
<section class="auth-visual" aria-label="Product testimonial">
<header class="auth-logo">
<span class="auth-logo-mark">N</span>
Nucleus
</header>
<div class="auth-quote">
<blockquote>“Simply all the tools that my team and I need.”</blockquote>
<cite>Karen Yue · Director of Digital Marketing Technology</cite>
</div>
</section>
<section class="auth-content">
{{if eq .View "login"}}
{{template "login_content" .}}
{{else if eq .View "signup"}}
{{template "signup_content" .}}
{{else if eq .View "dashboard"}}
{{template "dashboard_content" .}}
{{else if eq .View "unauthorized"}}
{{template "unauthorized_content" .}}
{{else}}
{{template "auth_default_content" .}}
{{end}}
</section>
</article>
</main>
</body>
</html>
{{end}}
{{define "auth_default_content"}}
<div class="auth-heading">
<h1>Authentication</h1>
<p>Pick an auth flow to continue.</p>
</div>
{{end}}
{{define "dashboard_content"}}
<div class="auth-heading">
<h1>Welcome back</h1>
<p>You're signed in as <strong>{{.Email}}</strong>.</p>
</div>
<article>
{{if .CreatedAt}}
<p>
Member since
<time datetime="{{.CreatedAtISO}}">{{.CreatedAt}}</time>.
</p>
{{end}}
<p>This dashboard will grow alongside the authentication features.</p>
</article>
<form method="post" action="/logout" class="auth-actions">
<input type="hidden" name="_csrf" value="{{.CSRFToken}}" />
<button type="submit" class="secondary">Sign out</button>
</form>
{{end}}
{{define "unauthorized_content"}}
<div class="auth-heading">
<h1>Access denied</h1>
<p>{{if .Error}}{{.Error}}{{else}}You do not have permission to view that page.{{end}}</p>
</div>
<p class="auth-footer">
<a href="/" role="button">Back to safety</a>
</p>
{{end}}

View file

@ -1,35 +1,3 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Dashboard</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
/>
</head>
<body>
<main class="container">
<h1>Welcome</h1>
<article>
<p>You are signed in as <strong>{{.Email}}</strong>.</p>
{{if .CreatedAt}}
<p>
Member since
<time datetime="{{.CreatedAtISO}}">{{.CreatedAt}}</time>.
</p>
{{define "dashboard.html"}}
{{template "auth_base" .}}
{{end}}
<p>
This placeholder dashboard will evolve as we flesh out the auth flow.
</p>
<footer>
<form method="post" action="/logout">
<input type="hidden" name="_csrf" value="{{.CSRFToken}}" />
<button type="submit" class="secondary">Sign out</button>
</form>
</footer>
</article>
</main>
</body>
</html>

View file

@ -1,35 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Auth Demo</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
/>
</head>
<body>
<main class="container">
<h1>Sign in</h1>
<p>Authenticate with the demo credentials below to view the dashboard.</p>
<article>
<header>Demo account</header>
<p>
<strong>Email:</strong> user@example.com<br /><strong>
Password:
</strong>
Password123
</p>
</article>
{{define "login.html"}}
{{template "auth_base" .}}
{{end}}
{{define "login_content"}}
<div class="auth-heading">
<h1>Welcome back to Nucleus</h1>
<p>Build your design system effortlessly with our powerful component library.</p>
</div>
<div class="auth-note" role="note">
<strong>Demo account access</strong><br />
Email: user@example.com · Password: Password123
</div>
{{if .Error}}
<article class="secondary">
<strong>Unable to sign in:</strong> {{.Error}}
<article class="contrast" role="alert">
<header>Unable to sign in</header>
<p>{{.Error}}</p>
</article>
{{end}}
<form method="post" action="/login">
<form method="post" action="/login" class="auth-form">
<input type="hidden" name="_csrf" value="{{.CSRFToken}}" />
<label for="email">Email</label>
<label for="email">
Email
<input
type="email"
id="email"
@ -39,22 +30,60 @@
autofocus
value="{{.Email}}"
/>
<label for="password">Password</label>
</label>
<label for="password">
Password
<input
type="password"
id="password"
name="password"
placeholder="Password"
placeholder="Your password"
required
pattern="(?=.*[A-Z])(?=.*\d).{8,}"
title="At least 8 characters including one uppercase letter and one number"
/>
<button type="submit">Login</button>
</label>
<div class="auth-meta">
<a href="#">Forgot password?</a>
<label class="auth-toggle">
<input type="checkbox" name="remember" />
Remember me
</label>
</div>
<div class="auth-actions">
<button type="submit" class="primary">Log in</button>
<div class="auth-divider">or</div>
<button type="button" class="secondary outline auth-google">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M21.6 12.23c0-.74-.06-1.28-.19-1.84H12v3.34h5.52c-.11.83-.72 2.09-2.08 2.94l-.02.11 3.02 2.34.21.02c1.95-1.8 3.05-4.45 3.05-7.25z"
fill="#4285F4"
/>
<path
d="M12 22c2.7 0 4.97-.89 6.63-2.41l-3.16-2.45c-.84.56-1.96.95-3.47.95-2.66 0-4.92-1.8-5.72-4.29H3.07v2.52C4.71 19.98 8.08 22 12 22z"
fill="#34A853"
/>
<path
d="M6.28 13.8a5.95 5.95 0 010-3.6V7.68H3.07a9.96 9.96 0 000 8.64l3.21-2.52z"
fill="#FBBC05"
/>
<path
d="M12 5.91c1.87 0 3.13.81 3.85 1.49l2.81-2.74C16.96 3.13 14.7 2 12 2 8.08 2 4.71 4.02 3.07 7.32l3.21 2.52C7.08 7.71 9.34 5.91 12 5.91z"
fill="#EA4335"
/>
</svg>
Continue with Google
</button>
</div>
</form>
<p>
Need an account?
<a href="/signup">Create one now</a>.
<p class="auth-footer">
Don't have an account? <a href="/signup">Sign up</a>
</p>
</main>
</body>
</html>
{{end}}

View file

@ -1,36 +1,50 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Create Account</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
/>
</head>
<body>
<main class="container">
<h1>Sign up</h1>
<p>Provide your email and a strong password to create an account.</p>
{{if .Error}}
<article class="secondary">
<strong>Unable to sign up:</strong> {{.Error}}
{{define "signup.html"}}
{{template "auth_base" .}}
{{end}}
{{define "signup_content"}}
<div class="auth-heading">
<h1>Create your account</h1>
<p>Join Nucleus UI and start designing with ease.</p>
</div>
{{if .Info}}
<article class="secondary" role="status">
<header>Heads-up</header>
<p>{{.Info}}</p>
</article>
{{end}}
<form method="post" action="/signup">
{{if .Error}}
<article class="contrast" role="alert">
<header>Unable to sign up</header>
<p>{{.Error}}</p>
</article>
{{end}}
<form method="post" action="/signup" class="auth-form">
<input type="hidden" name="_csrf" value="{{.CSRFToken}}" />
<label for="email">Email</label>
<label for="name">
Name
<input
type="text"
id="name"
name="name"
placeholder="Your name"
autocomplete="name"
/>
</label>
<label for="email">
Email
<input
type="email"
id="email"
name="email"
placeholder="Enter your email"
required
autofocus
autocomplete="email"
value="{{.Email}}"
/>
<label for="password">Password</label>
</label>
<label for="password">
Password
<input
type="password"
id="password"
@ -40,12 +54,51 @@
pattern="(?=.*[A-Z])(?=.*\d).{8,}"
title="At least 8 characters including one uppercase letter and one number"
/>
<button type="submit">Create account</button>
</label>
<label for="password_confirm">
Confirm password
<input
type="password"
id="password_confirm"
name="password_confirm"
placeholder="Re-type your password"
required
/>
</label>
<div class="auth-actions">
<button type="submit" class="primary">Create account</button>
<div class="auth-divider">or</div>
<button type="button" class="secondary outline auth-google">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M21.6 12.23c0-.74-.06-1.28-.19-1.84H12v3.34h5.52c-.11.83-.72 2.09-2.08 2.94l-.02.11 3.02 2.34.21.02c1.95-1.8 3.05-4.45 3.05-7.25z"
fill="#4285F4"
/>
<path
d="M12 22c2.7 0 4.97-.89 6.63-2.41l-3.16-2.45c-.84.56-1.96.95-3.47.95-2.66 0-4.92-1.8-5.72-4.29H3.07v2.52C4.71 19.98 8.08 22 12 22z"
fill="#34A853"
/>
<path
d="M6.28 13.8a5.95 5.95 0 010-3.6V7.68H3.07a9.96 9.96 0 000 8.64l3.21-2.52z"
fill="#FBBC05"
/>
<path
d="M12 5.91c1.87 0 3.13.81 3.85 1.49l2.81-2.74C16.96 3.13 14.7 2 12 2 8.08 2 4.71 4.02 3.07 7.32l3.21 2.52C7.08 7.71 9.34 5.91 12 5.91z"
fill="#EA4335"
/>
</svg>
Sign up with Google
</button>
</div>
</form>
<p>
Already registered?
<a href="/">Return to sign in</a>.
<p class="auth-footer">
Have an account? <a href="/">Log in</a>
</p>
</main>
</body>
</html>
{{end}}

View file

@ -1,22 +1,3 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Unauthorized</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
/>
</head>
<body>
<main class="container">
<h1>Unauthorized</h1>
<p>
{{if .Error}}{{.Error}}{{else}}You do not have permission to view that
page.{{end}}
</p>
<a href="/" role="button">Back to safety</a>
</main>
</body>
</html>
{{define "unauthorized.html"}}
{{template "auth_base" .}}
{{end}}