feat(14-02): migrate auth_login.templ to AuthLayout with ui.FormField inputs
- Replace Layout+Card pattern with AuthLayout("Sign in to Xtablo", csrfToken)
- Wire GoogleButton and AuthDivider into LoginPage body
- Replace raw <input> elements with @ui.FormField/@ui.Input design system components
- Add signup-copy nav link ("Don't have an account? Sign up")
- Preserve HTMX swap: hx-post="/login" hx-target="#login-form" hx-swap="outerHTML"
- Remove loginCardBody, AuthProviderButtonsBlock, AuthProviderButtonControl helpers
- Update test assertions for new GoogleButton labels ("Sign in with Google" / disabled attr)
This commit is contained in:
parent
51cb9bd895
commit
808eaecc85
2 changed files with 54 additions and 66 deletions
|
|
@ -167,7 +167,7 @@ func TestSignupProviderButtonsConfigured(t *testing.T) {
|
||||||
|
|
||||||
body := rec.Body.String()
|
body := rec.Body.String()
|
||||||
for _, want := range []string{
|
for _, want := range []string{
|
||||||
"Continue with Google",
|
"Sign in with Google",
|
||||||
`href="/auth/google/start"`,
|
`href="/auth/google/start"`,
|
||||||
">or<",
|
">or<",
|
||||||
`name="email"`,
|
`name="email"`,
|
||||||
|
|
@ -192,9 +192,10 @@ func TestSignupProviderButtonsDisabledWhenConfigMissing(t *testing.T) {
|
||||||
router.ServeHTTP(rec, req)
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
body := rec.Body.String()
|
body := rec.Body.String()
|
||||||
for _, want := range []string{"Google sign-in not configured"} {
|
// GoogleButton renders a disabled <button> when not configured; check for the disabled attribute
|
||||||
|
for _, want := range []string{`class="gsi-material-button" disabled`} {
|
||||||
if !strings.Contains(body, want) {
|
if !strings.Contains(body, want) {
|
||||||
t.Fatalf("signup page missing disabled copy %q; body: %s", want, body)
|
t.Fatalf("signup page missing disabled Google button %q; body: %s", want, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, notWant := range []string{"Apple sign-in", `href="/auth/apple/start"`} {
|
for _, notWant := range []string{"Apple sign-in", `href="/auth/apple/start"`} {
|
||||||
|
|
@ -557,7 +558,7 @@ func TestLoginProviderButtonsConfigured(t *testing.T) {
|
||||||
|
|
||||||
body := rec.Body.String()
|
body := rec.Body.String()
|
||||||
for _, want := range []string{
|
for _, want := range []string{
|
||||||
"Continue with Google",
|
"Sign in with Google",
|
||||||
`href="/auth/google/start"`,
|
`href="/auth/google/start"`,
|
||||||
">or<",
|
">or<",
|
||||||
`name="email"`,
|
`name="email"`,
|
||||||
|
|
@ -582,9 +583,10 @@ func TestLoginProviderButtonsDisabledWhenConfigMissing(t *testing.T) {
|
||||||
router.ServeHTTP(rec, req)
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
body := rec.Body.String()
|
body := rec.Body.String()
|
||||||
for _, want := range []string{"Google sign-in not configured"} {
|
// GoogleButton renders a disabled <button> when not configured; check for the disabled attribute
|
||||||
|
for _, want := range []string{`class="gsi-material-button" disabled`} {
|
||||||
if !strings.Contains(body, want) {
|
if !strings.Contains(body, want) {
|
||||||
t.Fatalf("login page missing disabled copy %q; body: %s", want, body)
|
t.Fatalf("login page missing disabled Google button %q; body: %s", want, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, notWant := range []string{"Apple sign-in", `href="/auth/apple/start"`} {
|
for _, notWant := range []string{"Apple sign-in", `href="/auth/apple/start"`} {
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,26 @@ package templates
|
||||||
|
|
||||||
import "backend/internal/web/ui"
|
import "backend/internal/web/ui"
|
||||||
|
|
||||||
// LoginPage renders the full /login page wrapped in the base Layout.
|
// LoginPage renders the full /login page wrapped in AuthLayout.
|
||||||
// It delegates the form section to LoginFormFragment so HTMX can swap just the
|
// It delegates the form section to LoginFormFragment so HTMX can swap just the
|
||||||
// form on validation errors without re-rendering the surrounding shell.
|
// form on validation errors without re-rendering the surrounding shell.
|
||||||
templ LoginPage(form LoginForm, errs LoginErrors, csrfToken string, providers AuthProviderButtons) {
|
templ LoginPage(form LoginForm, errs LoginErrors, csrfToken string, providers AuthProviderButtons) {
|
||||||
@Layout("Sign in", nil, csrfToken) {
|
@AuthLayout("Sign in to Xtablo", csrfToken) {
|
||||||
<div class="flex min-h-[60vh] items-start justify-center pt-16">
|
<div class="auth-card-topbar"></div>
|
||||||
@ui.Card(ui.CardProps{Body: loginCardBody(form, errs, csrfToken, providers)})
|
<div class="brand-header">
|
||||||
|
<img class="brand-logo" src="/static/logo_dark.png" alt="Xtablo"/>
|
||||||
|
</div>
|
||||||
|
<div class="title-group">
|
||||||
|
<h1>Sign in to Xtablo</h1>
|
||||||
|
</div>
|
||||||
|
<div class="auth-body">
|
||||||
|
@GoogleButton(providers.Google.StartURL, providers.Google.Configured)
|
||||||
|
@AuthDivider()
|
||||||
|
@LoginFormFragment(form, errs, csrfToken)
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
templ loginCardBody(form LoginForm, errs LoginErrors, csrfToken string, providers AuthProviderButtons) {
|
|
||||||
<div class="w-full max-w-sm px-6 py-8">
|
|
||||||
<h1 class="mb-6 text-2xl font-semibold">Sign in to your account</h1>
|
|
||||||
@AuthProviderButtonsBlock(providers)
|
|
||||||
@LoginFormFragment(form, errs, csrfToken)
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoginFormFragment is the bare form used for HTMX swaps.
|
// LoginFormFragment is the bare form used for HTMX swaps.
|
||||||
// hx-post targets this component itself so the form can be replaced inline
|
// hx-post targets this component itself so the form can be replaced inline
|
||||||
// on validation failure (D-19, D-20).
|
// on validation failure (D-19, D-20).
|
||||||
|
|
@ -33,62 +34,47 @@ templ LoginFormFragment(form LoginForm, errs LoginErrors, csrfToken string) {
|
||||||
hx-post="/login"
|
hx-post="/login"
|
||||||
hx-target="#login-form"
|
hx-target="#login-form"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
class="space-y-5"
|
class="login-form"
|
||||||
>
|
>
|
||||||
@ui.CSRFField(csrfToken)
|
@ui.CSRFField(csrfToken)
|
||||||
@GeneralError(errs.General)
|
@GeneralError(errs.General)
|
||||||
<div>
|
@ui.FormField(ui.FormFieldProps{
|
||||||
<label for="email" class="block text-sm font-medium text-slate-700">Email address</label>
|
Label: "Email address",
|
||||||
<input
|
For: "email",
|
||||||
id="email"
|
Field: ui.Input(ui.InputProps{
|
||||||
type="email"
|
ID: "email",
|
||||||
name="email"
|
Name: "email",
|
||||||
value={ form.Email }
|
Type: "email",
|
||||||
required
|
Placeholder: "you@example.com",
|
||||||
autocomplete="email"
|
Value: form.Email,
|
||||||
class="mt-1 block w-full rounded border border-slate-300 px-3 py-2 text-sm placeholder-slate-400 focus:border-slate-500 focus:outline-none"
|
Required: true,
|
||||||
placeholder="you@example.com"
|
Attrs: templ.Attributes{"autocomplete": "email"},
|
||||||
/>
|
}),
|
||||||
@FieldError(errs.Email)
|
Error: errs.Email,
|
||||||
</div>
|
})
|
||||||
<div>
|
@ui.FormField(ui.FormFieldProps{
|
||||||
<label for="password" class="block text-sm font-medium text-slate-700">Password</label>
|
Label: "Password",
|
||||||
<input
|
For: "password",
|
||||||
id="password"
|
Field: ui.Input(ui.InputProps{
|
||||||
type="password"
|
ID: "password",
|
||||||
name="password"
|
Name: "password",
|
||||||
required
|
Type: "password",
|
||||||
autocomplete="current-password"
|
Placeholder: "Your password",
|
||||||
class="mt-1 block w-full rounded border border-slate-300 px-3 py-2 text-sm placeholder-slate-400 focus:border-slate-500 focus:outline-none"
|
Required: true,
|
||||||
placeholder="Your password"
|
Attrs: templ.Attributes{"autocomplete": "current-password"},
|
||||||
/>
|
}),
|
||||||
@FieldError(errs.Password)
|
Error: errs.Password,
|
||||||
</div>
|
})
|
||||||
@ui.Button(ui.ButtonProps{
|
@ui.Button(ui.ButtonProps{
|
||||||
Label: "Sign in",
|
Label: "Sign in to Xtablo",
|
||||||
Variant: ui.ButtonVariantDefault,
|
Variant: ui.ButtonVariantDefault,
|
||||||
Tone: ui.ButtonToneSolid,
|
Tone: ui.ButtonToneSolid,
|
||||||
Size: ui.SizeMD,
|
Size: ui.SizeMD,
|
||||||
Type: "submit",
|
Type: "submit",
|
||||||
})
|
})
|
||||||
|
<p class="signup-copy">
|
||||||
|
Don't have an account?
|
||||||
|
<a class="signup-link" href="/signup">Sign up</a>
|
||||||
|
</p>
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ AuthProviderButtonsBlock(providers AuthProviderButtons) {
|
|
||||||
<div class="auth-provider-stack">
|
|
||||||
@AuthProviderButtonControl(providers.Google)
|
|
||||||
</div>
|
|
||||||
<div class="auth-provider-separator" aria-hidden="true">
|
|
||||||
<span></span>
|
|
||||||
<em>or</em>
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
templ AuthProviderButtonControl(provider AuthProviderButton) {
|
|
||||||
if provider.Configured {
|
|
||||||
<a class="auth-provider-button" href={ templ.SafeURL(provider.StartURL) }>{ provider.Label }</a>
|
|
||||||
} else {
|
|
||||||
<button type="button" class="auth-provider-button auth-provider-button-disabled" disabled aria-disabled="true">{ provider.DisabledLabel }</button>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue