render auth and password pages
This commit is contained in:
parent
557dd4cb0c
commit
86fd4b73b6
4 changed files with 156 additions and 3 deletions
|
|
@ -1,6 +1,11 @@
|
||||||
package handler
|
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 {
|
type authHandler struct {
|
||||||
}
|
}
|
||||||
|
|
@ -10,6 +15,9 @@ func NewAuthHandler() *authHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *authHandler) AuthPage(w http.ResponseWriter, r *http.Request) {
|
func (h *authHandler) AuthPage(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
ui.Render(w, r, pages.Auth(""))
|
||||||
w.Write([]byte("200 OK"))
|
}
|
||||||
|
|
||||||
|
func (h *authHandler) PasswordPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ui.Render(w, r, pages.AuthPassword(""))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ func SetupRoutes(a *app.App) http.Handler {
|
||||||
|
|
||||||
// Auth pages
|
// Auth pages
|
||||||
mux.HandleFunc("GET /auth", middleware.RequireGuest(auth.AuthPage))
|
mux.HandleFunc("GET /auth", middleware.RequireGuest(auth.AuthPage))
|
||||||
|
mux.HandleFunc("GET /auth/password", middleware.RequireGuest(auth.PasswordPage))
|
||||||
|
|
||||||
// 404
|
// 404
|
||||||
mux.HandleFunc("/{path...}", home.NotFoundPage)
|
mux.HandleFunc("/{path...}", home.NotFoundPage)
|
||||||
|
|
|
||||||
96
internal/ui/pages/auth_password.templ
Normal file
96
internal/ui/pages/auth_password.templ
Normal 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
48
internal/ui/render.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue