render auth and password pages

This commit is contained in:
juancwu 2025-12-16 16:28:11 -05:00
commit 86fd4b73b6
4 changed files with 156 additions and 3 deletions

View file

@ -1,6 +1,11 @@
package handler
import "net/http"
import (
"net/http"
"git.juancwu.dev/juancwu/budgit/internal/ui"
"git.juancwu.dev/juancwu/budgit/internal/ui/pages"
)
type authHandler struct {
}
@ -10,6 +15,9 @@ func NewAuthHandler() *authHandler {
}
func (h *authHandler) AuthPage(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("200 OK"))
ui.Render(w, r, pages.Auth(""))
}
func (h *authHandler) PasswordPage(w http.ResponseWriter, r *http.Request) {
ui.Render(w, r, pages.AuthPassword(""))
}

View file

@ -26,6 +26,7 @@ func SetupRoutes(a *app.App) http.Handler {
// Auth pages
mux.HandleFunc("GET /auth", middleware.RequireGuest(auth.AuthPage))
mux.HandleFunc("GET /auth/password", middleware.RequireGuest(auth.PasswordPage))
// 404
mux.HandleFunc("/{path...}", home.NotFoundPage)

View file

@ -0,0 +1,96 @@
package pages
import (
"git.juancwu.dev/juancwu/budgit/internal/ctxkeys"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/csrf"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/form"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/label"
"git.juancwu.dev/juancwu/budgit/internal/ui/layouts"
)
templ AuthPassword(errorMsg string) {
{{ cfg := ctxkeys.Config(ctx) }}
@layouts.Auth(layouts.SEOProps{
Title: "Sign In",
Description: "Sign in with your password",
Path: ctxkeys.URLPath(ctx),
}) {
<div class="min-h-screen flex items-center justify-center p-4">
<div class="w-full max-w-sm">
<div class="text-center mb-8">
<div class="mb-8">
@button.Button(button.Props{
Variant: button.VariantSecondary,
Size: button.SizeLg,
Href: "/",
}) {
@icon.Layers()
{ cfg.AppName }
}
</div>
<h2 class="text-3xl font-bold">Sign In</h2>
<p class="text-muted-foreground mt-2">Sign in with your password</p>
</div>
<form action="/auth/password" method="POST" class="space-y-4">
@csrf.Token()
@form.Item() {
@label.Label(label.Props{
For: "email",
Class: "block mb-2",
}) {
Email
}
@input.Input(input.Props{
ID: "email",
Name: "email",
Type: input.TypeEmail,
Placeholder: "name@example.com",
HasError: errorMsg != "",
Attributes: templ.Attributes{"autofocus": ""},
})
if errorMsg != "" {
@form.Message(form.MessageProps{Variant: form.MessageVariantError}) {
{ errorMsg }
}
}
}
@form.Item() {
@label.Label(label.Props{
For: "password",
Class: "block mb-2",
}) {
Password
}
@input.Input(input.Props{
ID: "password",
Name: "password",
Type: input.TypePassword,
Placeholder: "••••••••",
})
<div class="text-right mt-1">
<a href="/auth/forgot-password" class="text-sm text-muted-foreground hover:text-foreground transition-colors">
Forgot password?
</a>
</div>
}
@button.Button(button.Props{
Type: button.TypeSubmit,
FullWidth: true,
}) {
Sign In
}
</form>
<!-- Magic Link option -->
<p class="mt-6 text-center text-sm text-muted-foreground">
Don't have an account?
<a href="/auth" class="text-primary hover:underline ml-1">
Sign up with magic link
</a>
</p>
</div>
</div>
}
}

48
internal/ui/render.go Normal file
View file

@ -0,0 +1,48 @@
package ui
import (
"fmt"
"log/slog"
"net/http"
"github.com/a-h/templ"
)
func Render(w http.ResponseWriter, r *http.Request, c templ.Component) {
err := c.Render(r.Context(), w)
if err != nil {
slog.Error("render failed", "error", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}
func RenderFragment(w http.ResponseWriter, r *http.Request, c templ.Component, fragmentIDs ...any) {
err := templ.RenderFragments(r.Context(), w, c, fragmentIDs...)
if err != nil {
slog.Error("render fragment failed", "error", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}
func RenderOOB(w http.ResponseWriter, r *http.Request, c templ.Component, target string) {
// Write OOB wrapper start
_, err := fmt.Fprintf(w, `<div hx-swap-oob="%s">`, target)
if err != nil {
slog.Error("render oob write wrapper start failed", "error", err)
return
}
// Render component
err = c.Render(r.Context(), w)
if err != nil {
slog.Error("render oob component render failed", "error", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
// Write OOB wrapper end
_, err = w.Write([]byte(`</div>`))
if err != nil {
slog.Error("render oob write wrapper end failed", "error", err)
}
}