Discover books, shows, and movies at your level. Track your progress by filling your Shelf with what you find, and share with other language learners. *No dusting required. shlf.space

feat(views/login): add simple login page #3

merged opened by brookjeynes.dev targeting master from push-zkslkkkwosml
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:4mj54vc4ha3lh32ksxwunnbh/sh.tangled.repo.pull/3mftfzcu6ar22
+230 -54
Diff #0
+1 -1
docs/contributing.md
··· 46 46 47 47 Format templates and code before submitting for final review. 48 48 ``` 49 - go tool templ fmt ./internal/server/views/ && go fmt ./... 49 + go tool templ fmt ./internal/views/ && go fmt ./... 50 50 ``` 51 51 52 52 ## Proposals for bigger changes
+1 -1
docs/hacking.md
··· 33 33 If you modified the views, you will need to regenerate them: 34 34 ```bash 35 35 go tool templ generate 36 - go tool templ fmt ./internal/server/views/ 36 + go tool templ fmt ./internal/views/ 37 37 ``` 38 38 39 39 If you modified the tailwind styles, you will need to regenerate the css:
+7 -1
input.css
··· 1 - @import "tailwindcss"; 1 + @import "tailwindcss"; 2 + 3 + @utility container { 4 + margin-inline: auto; 5 + padding: 2rem 4rem; 6 + max-width: 42rem; 7 + }
+5
internal/layouts/base/base.go
··· 1 + package layouts 2 + 3 + type BaseParams struct { 4 + Title string 5 + }
+26
internal/layouts/base/base.templ
··· 1 + package layouts 2 + 3 + templ Base(params BaseParams) { 4 + <!DOCTYPE html> 5 + <html lang="en"> 6 + <head> 7 + <meta charset="UTF-8"/> 8 + <meta name="viewport" content="width=device-width, initial-scale=1.0"/> 9 + <title>shelf - { params.Title }</title> 10 + <script src="/static/htmx.min.js" defer></script> 11 + <script src="/static/lucide.min.js"></script> 12 + <script src="/static/alpinejs.min.js" defer></script> 13 + <script src="/static/oat.min.js" defer></script> 14 + <link rel="stylesheet" href="/static/style.css" type="text/css"/> 15 + <link rel="stylesheet" href="/static/oat.min.css" type="text/css"/> 16 + </head> 17 + <body class="min-h-screen"> 18 + <main> 19 + { children... } 20 + </main> 21 + </body> 22 + <script type="module" defer> 23 + lucide.createIcons(); 24 + </script> 25 + </html> 26 + }
+23
internal/server/htmx/htmx.go
··· 1 + package htmx 2 + 3 + import ( 4 + "fmt" 5 + "net/http" 6 + ) 7 + 8 + func HxNotice(w http.ResponseWriter, id, msg string) { 9 + html := fmt.Sprintf(`<span id="%s" hx-swap-oob="innerHTML">%s</span>`, id, msg) 10 + w.Header().Set("Content-Type", "text/html") 11 + w.WriteHeader(http.StatusOK) 12 + w.Write([]byte(html)) 13 + } 14 + 15 + func HxError(w http.ResponseWriter, status int, msg string) { 16 + w.WriteHeader(status) 17 + w.Write([]byte(msg)) 18 + } 19 + 20 + func HxRedirect(w http.ResponseWriter, status int, location string) { 21 + w.Header().Set("HX-Redirect", location) 22 + w.WriteHeader(status) 23 + }
+3 -3
internal/server/index.go
··· 3 3 import ( 4 4 "net/http" 5 5 6 - "shelf.app/internal/server/views" 6 + "shelf.app/internal/views/index" 7 7 ) 8 8 9 - func (s *Server) HandleIndexPage(w http.ResponseWriter, r *http.Request) { 10 - views.IndexPage(views.IndexPageParams{}).Render(r.Context(), w) 9 + func (s *Server) Index(w http.ResponseWriter, r *http.Request) { 10 + index.IndexPage(index.IndexPageParams{}).Render(r.Context(), w) 11 11 }
+50
internal/server/login.go
··· 1 + package server 2 + 3 + import ( 4 + "fmt" 5 + "net/http" 6 + "strings" 7 + 8 + "shelf.app/internal/server/htmx" 9 + "shelf.app/internal/views/login" 10 + ) 11 + 12 + func (s *Server) Login(w http.ResponseWriter, r *http.Request) { 13 + switch r.Method { 14 + case http.MethodGet: 15 + returnURL := r.URL.Query().Get("return_url") 16 + errorCode := r.URL.Query().Get("error") 17 + login.LoginPage(login.LoginPageParams{ 18 + ReturnUrl: returnURL, 19 + ErrorCode: errorCode, 20 + }).Render(r.Context(), w) 21 + case http.MethodPost: 22 + handle := r.FormValue("handle") 23 + returnURL := r.FormValue("return_url") 24 + 25 + // When users copy their handle from bsky.app, it tends to have these 26 + // characters around it: 27 + // 28 + // @nelind.dk: 29 + // \u202a ensures that the handle is always rendered left to right and 30 + // \u202c reverts that so the rest of the page renders however it should 31 + handle = strings.TrimPrefix(handle, "\u202a") 32 + handle = strings.TrimSuffix(handle, "\u202c") 33 + 34 + // `@` is harmless 35 + handle = strings.TrimPrefix(handle, "@") 36 + 37 + // Basic handle validation 38 + if !strings.Contains(handle, ".") { 39 + w.Header().Set("Content-Type", "text/html") 40 + login.LoginFormContent(login.LoginFormParams{ 41 + ReturnUrl: returnURL, 42 + Handle: handle, 43 + ErrorMessage: fmt.Sprintf("'%s' is an invalid handle. Did you mean %s.bsky.social?", handle, handle), 44 + }).Render(r.Context(), w) 45 + return 46 + } 47 + 48 + htmx.HxRedirect(w, http.StatusOK, "/") 49 + } 50 + }
+5 -1
internal/server/router.go
··· 9 9 func (s *Server) Router() http.Handler { 10 10 router := chi.NewRouter() 11 11 12 - router.Get("/", s.HandleIndexPage) 13 12 router.Handle("/static/*", s.HandleStatic()) 13 + 14 + router.Get("/", s.Index) 15 + 16 + router.Get("/login", s.Login) 17 + router.Post("/login", s.Login) 14 18 15 19 return router 16 20 }
+5 -5
internal/server/static.go
··· 8 8 "shelf.app/static" 9 9 ) 10 10 11 - func (s *Server) HandleStatic () http.Handler { 12 - var staticHandler http.Handler; 11 + func (s *Server) HandleStatic() http.Handler { 12 + var staticHandler http.Handler 13 13 14 14 if s.config.Core.Dev { 15 15 fileSystem := http.Dir("./static/files") ··· 31 31 32 32 func Cache(h http.Handler) http.Handler { 33 33 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 34 - path:=strings.Split(r.URL.Path, "?")[0] 34 + path := strings.Split(r.URL.Path, "?")[0] 35 35 36 - if strings.HasSuffix(path, ".js"){ 36 + if strings.HasSuffix(path, ".js") { 37 37 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 38 - }else{ 38 + } else { 39 39 w.Header().Set("Cache-Control", "public, max-age=3600") 40 40 } 41 41
-10
internal/server/views/index.templ
··· 1 - package views 2 - 3 - import "shelf.app/internal/server/views/layouts" 4 - 5 - templ IndexPage(params IndexPageParams) { 6 - @layouts.Base(layouts.BaseParams{Title: "home"}) 7 - <div> 8 - <h1>Hello world</h1> 9 - </div> 10 - }
-24
internal/server/views/layouts/base.templ
··· 1 - package layouts 2 - 3 - templ Base(params BaseParams) { 4 - <!DOCTYPE html> 5 - <html lang="en"> 6 - <head> 7 - <meta charset="UTF-8"/> 8 - <meta name="viewport" content="width=device-width, initial-scale=1.0"/> 9 - <title>shelf - { params.Title }</title> 10 - <script src="/static/htmx.min.js" defer></script> 11 - <script src="/static/lucide.min.js"></script> 12 - <script src="/static/alpinejs.min.js" defer></script> 13 - <link rel="stylesheet" href="/static/style.css" type="text/css"/> 14 - </head> 15 - <body class="flex flex-col min-h-screen bg-bg"> 16 - <main class="flex-1 pb-8 text-text"> 17 - { children... } 18 - </main> 19 - </body> 20 - <script type="module" defer> 21 - lucide.createIcons(); 22 - </script> 23 - </html> 24 - }
-5
internal/server/views/layouts/layouts.go
··· 1 - package layouts 2 - 3 - type BaseParams struct { 4 - Title string 5 - }
-3
internal/server/views/views.go
··· 1 - package views 2 - 3 - type IndexPageParams struct{}
+3
internal/views/index/index.go
··· 1 + package index 2 + 3 + type IndexPageParams struct{}
+10
internal/views/index/index.templ
··· 1 + package index 2 + 3 + import "shelf.app/internal/layouts/base" 4 + 5 + templ IndexPage(params IndexPageParams) { 6 + @layouts.Base(layouts.BaseParams{Title: "home"}) 7 + <div> 8 + <h1>Hello world</h1> 9 + </div> 10 + }
+40
internal/views/login/login-form.templ
··· 1 + package login 2 + 3 + templ LoginFormContent(params LoginFormParams) { 4 + <label 5 + x-init="lucide.createIcons()" 6 + if params.ErrorMessage != "" { 7 + data-field="error" 8 + } else { 9 + data-field 10 + } 11 + > 12 + Handle 13 + <input 14 + id="handle" 15 + name="handle" 16 + type="text" 17 + placeholder="username.bsky.social" 18 + autocapitalize="none" 19 + autocorrect="off" 20 + autocomplete="username" 21 + required 22 + tabindex="1" 23 + value={ params.Handle } 24 + if params.ErrorMessage != "" { 25 + aria-invalid="true" 26 + aria-errormessage="error-message" 27 + } 28 + /> 29 + if params.ErrorMessage != "" { 30 + <div id="error-message" class="error" role="status"> 31 + { params.ErrorMessage } 32 + </div> 33 + } 34 + </label> 35 + <input type="hidden" name="return_url" value={ params.ReturnUrl }/> 36 + <button type="submit" id="login-button" tabindex="2"> 37 + <i class="w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" data-lucide="loader-circle"></i> 38 + <span>Login</span> 39 + </button> 40 + }
+12
internal/views/login/login.go
··· 1 + package login 2 + 3 + type LoginPageParams struct { 4 + ReturnUrl string 5 + ErrorCode string 6 + } 7 + 8 + type LoginFormParams struct { 9 + ReturnUrl string 10 + Handle string 11 + ErrorMessage string 12 + }
+39
internal/views/login/login.templ
··· 1 + package login 2 + 3 + import "shelf.app/internal/layouts/base" 4 + 5 + templ LoginPage(params LoginPageParams) { 6 + @layouts.Base(layouts.BaseParams{Title: "login"}) { 7 + <div class="container"> 8 + <form 9 + class="group" 10 + hx-post="/login" 11 + hx-swap="innerHTML" 12 + hx-disabled-elt="#login-button" 13 + > 14 + @LoginFormContent(LoginFormParams{ 15 + ReturnUrl: params.ReturnUrl, 16 + Handle: "", 17 + ErrorMessage: "", 18 + }) 19 + </form> 20 + <div data-field="error"> 21 + if params.ErrorCode != "" { 22 + <p class="error"> 23 + switch (params.ErrorCode) { 24 + case "access_denied": 25 + You have not authorized the app. 26 + case "session": 27 + Server failed to create user session. 28 + case "handle": 29 + Server failed to validate your handle. 30 + default: 31 + Internal Server error. 32 + } 33 + Please try again. 34 + </p> 35 + } 36 + </div> 37 + </div> 38 + } 39 + }

History

1 round 0 comments
sign up or login to add to the discussion
brookjeynes.dev submitted #0
4 commits
expand
refactor: organise html/templ files to be less flat-structure like
feat(views/login): add login templates
feat(server/login): add login router
*:fmt
expand 0 comments
pull request successfully merged