forked from tangled.org/core
Monorepo for Tangled

appview: some initial htmxing and tailwinding

Changed files
+115 -34
.air
appview
pages
templates
layouts
repo
state
+2 -2
.air/appview.toml
··· 1 1 [build] 2 - cmd = "go build -o .bin/app ./cmd/appview/main.go" 2 + cmd = "tailwindcss -i input.css -o ./appview/pages/static/tw.css && go build -o .bin/app ./cmd/appview/main.go" 3 3 bin = ".bin/app" 4 4 root = "." 5 5 6 6 exclude_regex = [".*_templ.go"] 7 - include_ext = ["go", "templ", "html"] 7 + include_ext = ["go", "templ", "html", "css"] 8 8 exclude_dir = ["target", "atrium"]
+28
appview/pages/htmx.go
··· 1 + package pages 2 + 3 + import ( 4 + "fmt" 5 + "net/http" 6 + ) 7 + 8 + // Notice performs a hx-oob-swap to replace the content of an element with a message. 9 + // Pass the id of the element and the message to display. 10 + func (s *Pages) Notice(w http.ResponseWriter, id, msg string) { 11 + html := fmt.Sprintf(`<span id="%s" hx-swap-oob="innerHTML">%s</span>`, id, msg) 12 + 13 + w.Header().Set("Content-Type", "text/html") 14 + w.WriteHeader(http.StatusOK) 15 + w.Write([]byte(html)) 16 + } 17 + 18 + // HxRedirect is a full page reload with a new location. 19 + func (s *Pages) HxRedirect(w http.ResponseWriter, location string) { 20 + w.Header().Set("HX-Redirect", location) 21 + w.WriteHeader(http.StatusOK) 22 + } 23 + 24 + // HxLocation is an SPA-style navigation to a new location. 25 + func (s *Pages) HxLocation(w http.ResponseWriter, location string) { 26 + w.Header().Set("HX-Location", location) 27 + w.WriteHeader(http.StatusOK) 28 + }
+3 -1
appview/pages/templates/layouts/base.html
··· 7 7 name="viewport" 8 8 content="width=device-width, initial-scale=1.0" 9 9 /> 10 + <script src="/static/htmx.min.js"></script> 11 + <link href="/static/tw.css" rel="stylesheet" /> 10 12 <title>{{ block "title" . }}tangled{{ end }}</title> 11 13 </head> 12 - <body> 14 + <body class="container"> 13 15 <header class="topbar"> 14 16 {{ block "topbar" . }} 15 17 {{ template "layouts/topbar" . }}
+41 -20
appview/pages/templates/repo/new.html
··· 1 - {{define "title"}}new repo{{end}} 1 + {{ define "title" }}new repo{{ end }} 2 2 3 - {{define "content"}} 4 - <h1>new repo</h1> 5 - <form method="POST" action="/repo/new"> 6 - <label for="name">repo name</label> 7 - <input type="text" id="name" name="name" required /> 3 + {{ define "content" }} 4 + <div class="container"> 5 + <h1>new repo</h1> 6 + <form> 7 + <label for="name">repo name</label> 8 + <input 9 + type="text" 10 + id="name" 11 + name="name" 12 + class="px-1 border-2 border-blue-100" 13 + required 14 + /> 8 15 9 - <br> 16 + <fieldset class="border-blue-100 border-2"> 17 + <legend>select a knot:</legend> 18 + {{ range .Knots }} 19 + <label> 20 + <input 21 + class="px-1 border-2 border-blue-500" 22 + type="radio" 23 + name="domain" 24 + value="{{ . }}" 25 + /> 26 + {{ . }} </label 27 + ><br /> 28 + {{ else }} 29 + <p>no knots available</p> 30 + {{ end }} 31 + </fieldset> 10 32 11 - <fieldset> 12 - <legend>select a knot:</legend> 13 - {{ range .Knots }} 14 - <label> 15 - <input type="radio" name="domain" value="{{ . }}"> {{ . }} 16 - </label><br> 17 - <button type="submit">create repo</button> 18 - {{ else }} 19 - <p>no knots available</p> 20 - {{ end }} 21 - </fieldset> 33 + <button 34 + type="submit" 35 + hx-post="/repo/new" 36 + hx-swap="none" 37 + class="my-2 btn" 38 + > 39 + create repo 40 + </button> 41 + </form> 42 + </div> 22 43 23 - </form> 24 - {{end}} 44 + <div id="repo" class="error"></div> 45 + {{ end }}
+13 -11
appview/state/state.go
··· 431 431 knots, err := s.enforcer.GetDomainsForUser(user.Did) 432 432 433 433 if err != nil { 434 - log.Println("invalid user?", err) 434 + s.pages.Notice(w, "repo", "Invalid user account.") 435 435 return 436 436 } 437 437 ··· 444 444 445 445 domain := r.FormValue("domain") 446 446 if domain == "" { 447 - log.Println("invalid form") 447 + s.pages.Notice(w, "repo", "Invalid form submission&mdash;missing knot domain.") 448 448 return 449 449 } 450 450 451 451 repoName := r.FormValue("name") 452 452 if repoName == "" { 453 - log.Println("invalid form") 453 + s.pages.Notice(w, "repo", "Invalid repo name.") 454 454 return 455 455 } 456 456 457 457 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 458 458 if err != nil || !ok { 459 - w.Write([]byte("domain inaccessible to you")) 459 + s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 460 460 return 461 461 } 462 462 463 463 secret, err := s.db.GetRegistrationKey(domain) 464 464 if err != nil { 465 - log.Printf("no key found for domain %s: %s\n", domain, err) 465 + s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain)) 466 466 return 467 467 } 468 468 469 469 client, err := NewSignedClient(domain, secret) 470 470 if err != nil { 471 - log.Println("failed to create client to ", domain) 471 + s.pages.Notice(w, "repo", "Failed to connect to knot server.") 472 + return 472 473 } 473 474 474 475 resp, err := client.NewRepo(user.Did, repoName) 475 476 if err != nil { 476 - log.Println("failed to send create repo request", err) 477 + s.pages.Notice(w, "repo", "Failed to create repository on knot server.") 477 478 return 478 479 } 479 480 if resp.StatusCode != http.StatusNoContent { 480 - log.Println("server returned ", resp.StatusCode) 481 + s.pages.Notice(w, "repo", fmt.Sprintf("Server returned unexpected status: %d", resp.StatusCode)) 481 482 return 482 483 } 483 484 ··· 489 490 } 490 491 err = s.db.AddRepo(repo) 491 492 if err != nil { 492 - log.Println("failed to add repo to db", err) 493 + s.pages.Notice(w, "repo", "Failed to save repository information.") 493 494 return 494 495 } 495 496 496 497 // acls 497 498 err = s.enforcer.AddRepo(user.Did, domain, filepath.Join(user.Did, repoName)) 498 499 if err != nil { 499 - log.Println("failed to set up acls", err) 500 + s.pages.Notice(w, "repo", "Failed to set up repository permissions.") 500 501 return 501 502 } 502 503 503 - w.Write([]byte("created!")) 504 + s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName)) 505 + return 504 506 } 505 507 } 506 508
+28
input.css
··· 12 12 @apply text-2xl; 13 13 @apply font-sans; 14 14 @apply text-gray-900; 15 + @apply py-4; 15 16 } 17 + 16 18 ::selection { 17 19 @apply bg-green-400; 18 20 @apply text-gray-900; 19 21 @apply bg-opacity-30; 22 + } 23 + a { 24 + @apply underline text-blue-600 hover:text-blue-800 visited:text-purple-600; 25 + } 26 + 27 + @layer components { 28 + .btn { 29 + @apply relative z-10 inline-flex min-h-[36px] cursor-pointer items-center 30 + justify-center border-0 bg-transparent px-3 pb-[0.3rem] text-base 31 + text-gray-800 before:absolute before:inset-0 before:-z-10 32 + before:block before:rounded before:border before:border-cyan-200 33 + before:bg-white before:shadow-[0_4px_3px_0_rgba(20,20,96,0.1),inset_0_-5px_0_0_#ebebf6] 34 + before:content-[''] hover:before:border-cyan-600 35 + hover:before:bg-cyan-600 36 + hover:before:shadow-[0_4px_3px_0_rgba(20,20,96,0.1),inset_0_-5px_0_0_#c2b3ff] 37 + focus:outline-none focus-visible:before:outline 38 + focus-visible:before:outline-4 focus-visible:before:outline-[#fc440f] 39 + active:border-t-4 active:border-transparent active:py-1 40 + active:before:shadow-none; 41 + } 42 + } 43 + 44 + @layer utilities { 45 + .error { 46 + @apply py-1 border-red-400 text-red-600; 47 + } 20 48 } 21 49 }