Signed-off-by: brookjeynes me@brookjeynes.dev
+230
-54
Diff
round #0
+1
-1
docs/contributing.md
+1
-1
docs/contributing.md
+1
-1
docs/hacking.md
+1
-1
docs/hacking.md
+7
-1
input.css
+7
-1
input.css
+5
internal/layouts/base/base.go
+5
internal/layouts/base/base.go
+26
internal/layouts/base/base.templ
+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
+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
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
+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
+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
+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
-10
internal/server/views/index.templ
-24
internal/server/views/layouts/base.templ
-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
-5
internal/server/views/layouts/layouts.go
+10
internal/views/index/index.templ
+10
internal/views/index/index.templ
+40
internal/views/login/login-form.templ
+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
+12
internal/views/login/login.go
+39
internal/views/login/login.templ
+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
brookjeynes.dev
submitted
#0
4 commits
expand
collapse
refactor: organise html/templ files to be less flat-structure like
Signed-off-by: brookjeynes <me@brookjeynes.dev>
feat(views/login): add login templates
Signed-off-by: brookjeynes <me@brookjeynes.dev>
feat(server/login): add login router
Signed-off-by: brookjeynes <me@brookjeynes.dev>
*:fmt
Signed-off-by: brookjeynes <me@brookjeynes.dev>
expand 0 comments
pull request successfully merged