forked from tangled.org/core
this repo has no description

appview: some initial htmxing and tailwinding

Changed files
+115 -34
.air
appview
pages
templates
layouts
repo
state
+2 -2
.air/appview.toml
···
[build]
-
cmd = "go build -o .bin/app ./cmd/appview/main.go"
+
cmd = "tailwindcss -i input.css -o ./appview/pages/static/tw.css && go build -o .bin/app ./cmd/appview/main.go"
bin = ".bin/app"
root = "."
exclude_regex = [".*_templ.go"]
-
include_ext = ["go", "templ", "html"]
+
include_ext = ["go", "templ", "html", "css"]
exclude_dir = ["target", "atrium"]
+28
appview/pages/htmx.go
···
+
package pages
+
+
import (
+
"fmt"
+
"net/http"
+
)
+
+
// Notice performs a hx-oob-swap to replace the content of an element with a message.
+
// Pass the id of the element and the message to display.
+
func (s *Pages) Notice(w http.ResponseWriter, id, msg string) {
+
html := fmt.Sprintf(`<span id="%s" hx-swap-oob="innerHTML">%s</span>`, id, msg)
+
+
w.Header().Set("Content-Type", "text/html")
+
w.WriteHeader(http.StatusOK)
+
w.Write([]byte(html))
+
}
+
+
// HxRedirect is a full page reload with a new location.
+
func (s *Pages) HxRedirect(w http.ResponseWriter, location string) {
+
w.Header().Set("HX-Redirect", location)
+
w.WriteHeader(http.StatusOK)
+
}
+
+
// HxLocation is an SPA-style navigation to a new location.
+
func (s *Pages) HxLocation(w http.ResponseWriter, location string) {
+
w.Header().Set("HX-Location", location)
+
w.WriteHeader(http.StatusOK)
+
}
+3 -1
appview/pages/templates/layouts/base.html
···
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
+
<script src="/static/htmx.min.js"></script>
+
<link href="/static/tw.css" rel="stylesheet" />
<title>{{ block "title" . }}tangled{{ end }}</title>
</head>
-
<body>
+
<body class="container">
<header class="topbar">
{{ block "topbar" . }}
{{ template "layouts/topbar" . }}
+41 -20
appview/pages/templates/repo/new.html
···
-
{{define "title"}}new repo{{end}}
+
{{ define "title" }}new repo{{ end }}
-
{{define "content"}}
-
<h1>new repo</h1>
-
<form method="POST" action="/repo/new">
-
<label for="name">repo name</label>
-
<input type="text" id="name" name="name" required />
+
{{ define "content" }}
+
<div class="container">
+
<h1>new repo</h1>
+
<form>
+
<label for="name">repo name</label>
+
<input
+
type="text"
+
id="name"
+
name="name"
+
class="px-1 border-2 border-blue-100"
+
required
+
/>
-
<br>
+
<fieldset class="border-blue-100 border-2">
+
<legend>select a knot:</legend>
+
{{ range .Knots }}
+
<label>
+
<input
+
class="px-1 border-2 border-blue-500"
+
type="radio"
+
name="domain"
+
value="{{ . }}"
+
/>
+
{{ . }} </label
+
><br />
+
{{ else }}
+
<p>no knots available</p>
+
{{ end }}
+
</fieldset>
-
<fieldset>
-
<legend>select a knot:</legend>
-
{{ range .Knots }}
-
<label>
-
<input type="radio" name="domain" value="{{ . }}"> {{ . }}
-
</label><br>
-
<button type="submit">create repo</button>
-
{{ else }}
-
<p>no knots available</p>
-
{{ end }}
-
</fieldset>
+
<button
+
type="submit"
+
hx-post="/repo/new"
+
hx-swap="none"
+
class="my-2 btn"
+
>
+
create repo
+
</button>
+
</form>
+
</div>
-
</form>
-
{{end}}
+
<div id="repo" class="error"></div>
+
{{ end }}
+13 -11
appview/state/state.go
···
knots, err := s.enforcer.GetDomainsForUser(user.Did)
if err != nil {
-
log.Println("invalid user?", err)
+
s.pages.Notice(w, "repo", "Invalid user account.")
return
}
···
domain := r.FormValue("domain")
if domain == "" {
-
log.Println("invalid form")
+
s.pages.Notice(w, "repo", "Invalid form submission&mdash;missing knot domain.")
return
}
repoName := r.FormValue("name")
if repoName == "" {
-
log.Println("invalid form")
+
s.pages.Notice(w, "repo", "Invalid repo name.")
return
}
ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
if err != nil || !ok {
-
w.Write([]byte("domain inaccessible to you"))
+
s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
return
}
secret, err := s.db.GetRegistrationKey(domain)
if err != nil {
-
log.Printf("no key found for domain %s: %s\n", domain, err)
+
s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain))
return
}
client, err := NewSignedClient(domain, secret)
if err != nil {
-
log.Println("failed to create client to ", domain)
+
s.pages.Notice(w, "repo", "Failed to connect to knot server.")
+
return
}
resp, err := client.NewRepo(user.Did, repoName)
if err != nil {
-
log.Println("failed to send create repo request", err)
+
s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
return
}
if resp.StatusCode != http.StatusNoContent {
-
log.Println("server returned ", resp.StatusCode)
+
s.pages.Notice(w, "repo", fmt.Sprintf("Server returned unexpected status: %d", resp.StatusCode))
return
}
···
}
err = s.db.AddRepo(repo)
if err != nil {
-
log.Println("failed to add repo to db", err)
+
s.pages.Notice(w, "repo", "Failed to save repository information.")
return
}
// acls
err = s.enforcer.AddRepo(user.Did, domain, filepath.Join(user.Did, repoName))
if err != nil {
-
log.Println("failed to set up acls", err)
+
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
return
}
-
w.Write([]byte("created!"))
+
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName))
+
return
}
}
+28
input.css
···
@apply text-2xl;
@apply font-sans;
@apply text-gray-900;
+
@apply py-4;
}
+
::selection {
@apply bg-green-400;
@apply text-gray-900;
@apply bg-opacity-30;
+
}
+
a {
+
@apply underline text-blue-600 hover:text-blue-800 visited:text-purple-600;
+
}
+
+
@layer components {
+
.btn {
+
@apply relative z-10 inline-flex min-h-[36px] cursor-pointer items-center
+
justify-center border-0 bg-transparent px-3 pb-[0.3rem] text-base
+
text-gray-800 before:absolute before:inset-0 before:-z-10
+
before:block before:rounded before:border before:border-cyan-200
+
before:bg-white before:shadow-[0_4px_3px_0_rgba(20,20,96,0.1),inset_0_-5px_0_0_#ebebf6]
+
before:content-[''] hover:before:border-cyan-600
+
hover:before:bg-cyan-600
+
hover:before:shadow-[0_4px_3px_0_rgba(20,20,96,0.1),inset_0_-5px_0_0_#c2b3ff]
+
focus:outline-none focus-visible:before:outline
+
focus-visible:before:outline-4 focus-visible:before:outline-[#fc440f]
+
active:border-t-4 active:border-transparent active:py-1
+
active:before:shadow-none;
+
}
+
}
+
+
@layer utilities {
+
.error {
+
@apply py-1 border-red-400 text-red-600;
+
}
}
}