appview/pages: htmx-ify knots page #307

merged
opened by oppi.li targeting master from push-nwslswprzvmx
+31 -6
appview/pages/pages.go
···
}
func (p *Pages) Knots(w io.Writer, params KnotsParams) error {
-
return p.execute("knots", w, params)
}
type KnotParams struct {
···
DidHandleMap map[string]string
Registration *db.Registration
Members []string
IsOwner bool
}
func (p *Pages) Knot(w io.Writer, params KnotParams) error {
-
return p.execute("knot", w, params)
}
type SpindlesParams struct {
···
DidHandleMap map[string]string
OrderedReactionKinds []db.ReactionKind
-
Reactions map[db.ReactionKind]int
-
UserReacted map[db.ReactionKind]bool
State string
}
type ThreadReactionFragmentParams struct {
ThreadAt syntax.ATURI
-
Kind db.ReactionKind
-
Count int
IsReacted bool
}
···
}
func (p *Pages) Knots(w io.Writer, params KnotsParams) error {
+
return p.execute("knots/index", w, params)
}
type KnotParams struct {
···
DidHandleMap map[string]string
Registration *db.Registration
Members []string
+
Repos map[string][]db.Repo
IsOwner bool
}
func (p *Pages) Knot(w io.Writer, params KnotParams) error {
+
return p.execute("knots/dashboard", w, params)
+
}
+
+
type KnotListingParams struct {
+
db.Registration
+
}
+
+
func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error {
+
return p.executePlain("knots/fragments/knotListing", w, params)
+
}
+
+
type KnotListingFullParams struct {
+
Registrations []db.Registration
+
}
+
+
func (p *Pages) KnotListingFull(w io.Writer, params KnotListingFullParams) error {
+
return p.executePlain("knots/fragments/knotListingFull", w, params)
+
}
+
+
type KnotSecretParams struct {
+
Secret string
+
}
+
+
func (p *Pages) KnotSecret(w io.Writer, params KnotSecretParams) error {
+
return p.executePlain("knots/fragments/secret", w, params)
}
type SpindlesParams struct {
···
DidHandleMap map[string]string
OrderedReactionKinds []db.ReactionKind
+
Reactions map[db.ReactionKind]int
+
UserReacted map[db.ReactionKind]bool
State string
}
type ThreadReactionFragmentParams struct {
ThreadAt syntax.ATURI
+
Kind db.ReactionKind
+
Count int
IsReacted bool
}
-98
appview/pages/templates/knot.html
···
-
{{ define "title" }}{{ .Registration.Domain }}{{ end }}
-
-
{{ define "content" }}
-
<div class="p-6">
-
<p class="text-xl font-bold dark:text-white">{{ .Registration.Domain }}</p>
-
</div>
-
-
<div class="flex flex-col">
-
{{ block "registration-info" . }} {{ end }}
-
{{ block "members" . }} {{ end }}
-
{{ block "add-member" . }} {{ end }}
-
</div>
-
{{ end }}
-
-
{{ define "registration-info" }}
-
<section class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit">
-
<dl class="grid grid-cols-[auto_1fr] gap-x-4 dark:text-gray-200">
-
<dt class="font-bold">opened by</dt>
-
<dd>
-
<span>
-
{{ index $.DidHandleMap .Registration.ByDid }} <span class="text-gray-500 dark:text-gray-400 font-mono">{{ .Registration.ByDid }}</span>
-
</span>
-
{{ if eq $.LoggedInUser.Did $.Registration.ByDid }}
-
<span class="text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded ml-2">you</span>
-
{{ end }}
-
</dd>
-
-
<dt class="font-bold">opened</dt>
-
<dd>{{ .Registration.Created | timeFmt }}</dd>
-
-
{{ if .Registration.Registered }}
-
<dt class="font-bold">registered</dt>
-
<dd>{{ .Registration.Registered | timeFmt }}</dd>
-
{{ else }}
-
<dt class="font-bold">status</dt>
-
<dd class="text-yellow-800 dark:text-yellow-200 bg-yellow-100 dark:bg-yellow-900 rounded px-2 py-1 inline-block">
-
Pending Registration
-
</dd>
-
{{ end }}
-
</dl>
-
-
{{ if not .Registration.Registered }}
-
<div class="mt-4">
-
<button
-
class="btn dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600"
-
hx-post="/knots/{{.Domain}}/init"
-
hx-swap="none">
-
Initialize Registration
-
</button>
-
</div>
-
{{ end }}
-
</section>
-
{{ end }}
-
-
{{ define "members" }}
-
<h2 class="text-sm font-bold py-2 px-6 uppercase dark:text-gray-300">members</h2>
-
<section class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit">
-
{{ if .Registration.Registered }}
-
<div id="member-list" class="flex flex-col gap-4">
-
{{ range $.Members }}
-
<div class="inline-flex items-center gap-4">
-
{{ i "user" "w-4 h-4 dark:text-gray-300" }}
-
<a href="/{{index $.DidHandleMap .}}" class="text-gray-900 dark:text-white">{{index $.DidHandleMap .}}
-
<span class="text-gray-500 dark:text-gray-400 font-mono">{{.}}</span>
-
</a>
-
</div>
-
{{ else }}
-
<p class="text-gray-500 dark:text-gray-400">No members have been added yet.</p>
-
{{ end }}
-
</div>
-
{{ else }}
-
<p class="text-gray-500 dark:text-gray-400">Members can be added after registration is complete.</p>
-
{{ end }}
-
</section>
-
{{ end }}
-
-
{{ define "add-member" }}
-
{{ if $.IsOwner }}
-
<h2 class="text-sm font-bold py-2 px-6 uppercase dark:text-gray-300">add member</h2>
-
<section class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit">
-
<form
-
hx-put="/knots/{{.Registration.Domain}}/member"
-
class="max-w-2xl space-y-4">
-
<input
-
type="text"
-
id="subject"
-
name="subject"
-
placeholder="did or handle"
-
required
-
class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400"/>
-
-
<button class="btn dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600" type="submit">add member</button>
-
-
<div id="add-member-error" class="error dark:text-red-400"></div>
-
</form>
-
</section>
-
{{ end }}
-
{{ end }}
···
-93
appview/pages/templates/knots.html
···
-
{{ define "title" }}knots{{ end }}
-
{{ define "content" }}
-
<div class="p-6">
-
<p class="text-xl font-bold dark:text-white">Knots</p>
-
</div>
-
<div class="flex flex-col">
-
<h2 class="text-sm font-bold py-2 px-6 uppercase dark:text-gray-300">register a knot</h2>
-
<section class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit">
-
<p class="mb-8 dark:text-gray-300">Generate a key to initialize your knot server.</p>
-
<form
-
hx-post="/knots/key"
-
class="max-w-2xl mb-8 space-y-4"
-
hx-indicator="#generate-knot-key-spinner"
-
>
-
<input
-
type="text"
-
id="domain"
-
name="domain"
-
placeholder="knot.example.com"
-
required
-
class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400"
-
>
-
<button class="btn dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 flex items-center" type="submit">
-
<span>generate key</span>
-
<span id="generate-knot-key-spinner" class="group">
-
{{ i "loader-circle" "pl-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
-
</span>
-
</button>
-
<div id="settings-knots-error" class="error dark:text-red-400"></div>
-
</form>
-
</section>
-
-
<h2 class="text-sm font-bold py-2 px-6 uppercase dark:text-gray-300">my knots</h2>
-
<section class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit">
-
<div id="knots-list" class="flex flex-col gap-6 mb-8">
-
{{ range .Registrations }}
-
{{ if .Registered }}
-
<div class="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4">
-
<div class="flex flex-col gap-1">
-
<div class="inline-flex items-center gap-4">
-
{{ i "git-branch" "w-3 h-3 dark:text-gray-300" }}
-
<a href="/knots/{{ .Domain }}">
-
<p class="font-bold dark:text-white">{{ .Domain }}</p>
-
</a>
-
</div>
-
<p class="text-sm text-gray-500 dark:text-gray-400">owned by {{ .ByDid }}</p>
-
<p class="text-sm text-gray-500 dark:text-gray-400">registered {{ .Registered | timeFmt }}</p>
-
</div>
-
</div>
-
{{ end }}
-
{{ else }}
-
<p class="text-sm text-gray-500 dark:text-gray-400">No knots registered</p>
-
{{ end }}
-
</div>
-
</section>
-
-
<h2 class="text-sm font-bold py-2 px-6 uppercase dark:text-gray-300">pending registrations</h2>
-
<section class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit">
-
<div id="pending-knots-list" class="flex flex-col gap-6 mb-8">
-
{{ range .Registrations }}
-
{{ if not .Registered }}
-
<div class="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4">
-
<div class="flex flex-col gap-1">
-
<div class="inline-flex items-center gap-4">
-
<p class="font-bold dark:text-white">{{ .Domain }}</p>
-
<div class="inline-flex items-center gap-1">
-
<span class="text-xs bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 px-2 py-1 rounded">
-
pending
-
</span>
-
</div>
-
</div>
-
<p class="text-sm text-gray-500 dark:text-gray-400">opened by {{ .ByDid }}</p>
-
<p class="text-sm text-gray-500 dark:text-gray-400">created {{ .Created | timeFmt }}</p>
-
</div>
-
<div class="flex gap-2 items-center">
-
<button
-
class="btn dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 flex gap-2 items-center group"
-
hx-post="/knots/{{ .Domain }}/init"
-
>
-
{{ i "square-play" "w-5 h-5" }}
-
<span class="hidden md:inline">initialize</span>
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
-
</button>
-
</div>
-
</div>
-
{{ end }}
-
{{ else }}
-
<p class="text-sm text-gray-500 dark:text-gray-400">No pending registrations</p>
-
{{ end }}
-
</div>
-
</section>
-
</div>
-
{{ end }}
···
+63
appview/pages/templates/knots/dashboard.html
···
···
+
{{ define "title" }}{{ .Registration.Domain }}{{ end }}
+
+
{{ define "content" }}
+
<div class="px-6 py-4">
+
<div class="flex justify-between items-center">
+
<div id="left-side" class="flex gap-2 items-center">
+
<h1 class="text-xl font-bold dark:text-white">
+
{{ .Registration.Domain }}
+
</h1>
+
<span class="text-gray-500 text-base">
+
{{ .Registration.Created | shortTimeFmt }} ago
+
</span>
+
</div>
+
<div id="right-side" class="flex gap-2">
+
{{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2" }}
+
{{ if .Registration.Registered }}
+
<span class="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 {{$style}}">{{ i "shield-check" "w-4 h-4" }} verified</span>
+
{{ template "knots/fragments/addMemberModal" .Registration }}
+
{{ else }}
+
<span class="bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200 {{$style}}">{{ i "shield-off" "w-4 h-4" }} pending</span>
+
{{ end }}
+
</div>
+
</div>
+
<div id="operation-error" class="dark:text-red-400"></div>
+
</div>
+
+
{{ if .Members }}
+
<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
+
<div class="flex flex-col gap-2">
+
{{ block "knotMember" . }} {{ end }}
+
</div>
+
</section>
+
{{ end }}
+
{{ end }}
+
+
{{ define "knotMember" }}
+
{{ range .Members }}
+
<div>
+
<div class="flex justify-between items-center">
+
<div class="flex items-center gap-2">
+
{{ i "user" "size-4" }}
+
{{ $user := index $.DidHandleMap . }}
+
<a href="/{{ $user }}">{{ $user }} <span class="ml-2 font-mono text-gray-500">{{.}}</span></a>
+
</div>
+
</div>
+
<div class="ml-2 pl-2 pt-2 border-l border-gray-200 dark:border-gray-700">
+
{{ $repos := index $.Repos . }}
+
{{ range $repos }}
+
<div class="flex gap-2 items-center">
+
{{ i "book-marked" "size-4" }}
+
<a href="/{{ .Did }}/{{ .Name }}">
+
{{ .Name }}
+
</a>
+
</div>
+
{{ else }}
+
<div class="text-gray-500 dark:text-gray-400">
+
No repositories created yet.
+
</div>
+
{{ end }}
+
</div>
+
</div>
+
{{ end }}
+
{{ end }}
+58
appview/pages/templates/knots/fragments/addMemberModal.html
···
···
+
{{ define "knots/fragments/addMemberModal" }}
+
<button
+
class="btn gap-2 group"
+
title="Add member to this spindle"
+
popovertarget="add-member-{{ .Id }}"
+
popovertargetaction="toggle"
+
>
+
{{ i "user-plus" "w-5 h-5" }}
+
<span class="hidden md:inline">add member</span>
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</button>
+
+
<div
+
id="add-member-{{ .Id }}"
+
popover
+
class="bg-white w-full md:w-96 dark:bg-gray-800 p-4 rounded drop-shadow dark:text-white">
+
{{ block "addKnotMemberPopover" . }} {{ end }}
+
</div>
+
{{ end }}
+
+
{{ define "addKnotMemberPopover" }}
+
<form
+
hx-put="/knots/{{ .Domain }}/member"
+
hx-indicator="#spinner"
+
hx-swap="none"
+
class="flex flex-col gap-2"
+
>
+
<label for="member-did-{{ .Id }}" class="uppercase p-0">
+
ADD MEMBER
+
</label>
+
<p class="text-sm text-gray-500 dark:text-gray-400">Members can create repositories on this knot.</p>
+
<input
+
type="text"
+
id="member-did-{{ .Id }}"
+
name="subject"
+
required
+
placeholder="@foo.bsky.social"
+
/>
+
<div class="flex gap-2 pt-2">
+
<button
+
type="button"
+
popovertarget="add-member-{{ .Id }}"
+
popovertargetaction="hide"
+
class="btn w-1/2 flex items-center gap-2 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
+
>
+
{{ i "x" "size-4" }} cancel
+
</button>
+
<button type="submit" class="btn w-1/2 flex items-center">
+
<span class="inline-flex gap-2 items-center">{{ i "user-plus" "size-4" }} add</span>
+
<span id="spinner" class="group">
+
{{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</span>
+
</button>
+
</div>
+
<div id="add-member-error-{{ .Id }}" class="text-red-500 dark:text-red-400"></div>
+
</form>
+
{{ end }}
+
+51
appview/pages/templates/knots/fragments/knotListing.html
···
···
+
{{ define "knots/fragments/knotListing" }}
+
<div
+
id="knot-{{.Id}}"
+
hx-swap-oob="true"
+
class="flex items-center justify-between p-2 border-b border-gray-200 dark:border-gray-700">
+
{{ block "listLeftSide" . }} {{ end }}
+
{{ block "listRightSide" . }} {{ end }}
+
</div>
+
{{ end }}
+
+
{{ define "listLeftSide" }}
+
<div class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]">
+
{{ i "hard-drive" "w-4 h-4" }}
+
{{ if .Registered }}
+
<a href="/knots/{{ .Domain }}">
+
{{ .Domain }}
+
</a>
+
{{ else }}
+
{{ .Domain }}
+
{{ end }}
+
<span class="text-gray-500">
+
{{ .Created | shortTimeFmt }} ago
+
</span>
+
</div>
+
{{ end }}
+
+
{{ define "listRightSide" }}
+
<div id="right-side" class="flex gap-2">
+
{{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2 text-sm" }}
+
{{ if .Registered }}
+
<span class="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 {{$style}}">{{ i "shield-check" "w-4 h-4" }} verified</span>
+
{{ template "knots/fragments/addMemberModal" . }}
+
{{ else }}
+
<span class="bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200 {{$style}}">{{ i "shield-off" "w-4 h-4" }} pending</span>
+
{{ block "initializeButton" . }} {{ end }}
+
{{ end }}
+
</div>
+
{{ end }}
+
+
{{ define "initializeButton" }}
+
<button
+
class="btn dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 flex gap-2 items-center group"
+
hx-post="/knots/{{ .Domain }}/init"
+
hx-swap="none"
+
>
+
{{ i "square-play" "w-5 h-5" }}
+
<span class="hidden md:inline">initialize</span>
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</button>
+
{{ end }}
+
+18
appview/pages/templates/knots/fragments/knotListingFull.html
···
···
+
{{ define "knots/fragments/knotListingFull" }}
+
<section
+
id="knot-listing-full"
+
hx-swap-oob="true"
+
class="rounded w-full flex flex-col gap-2">
+
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">your knots</h2>
+
<div class="flex flex-col rounded border border-gray-200 dark:border-gray-700 w-full">
+
{{ range $knot := .Registrations }}
+
{{ template "knots/fragments/knotListing" . }}
+
{{ else }}
+
<div class="flex items-center justify-center p-2 border-b border-gray-200 dark:border-gray-700 text-gray-500">
+
no knots registered yet
+
</div>
+
{{ end }}
+
</div>
+
<div id="operation-error" class="text-red-500 dark:text-red-400"></div>
+
</section>
+
{{ end }}
+10
appview/pages/templates/knots/fragments/secret.html
···
···
+
{{ define "knots/fragments/secret" }}
+
<div
+
id="secret"
+
hx-swap-oob="true"
+
class="bg-gray-50 dark:bg-gray-700 border border-black dark:border-gray-500 rounded px-6 py-2 w-full lg:w-3xl">
+
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">generated secret</h2>
+
<p class="pb-2">Configure your knot to use this secret, and then hit initialize.</p>
+
<span class="font-mono overflow-x">{{ .Secret }}</span>
+
</div>
+
{{ end }}
+55
appview/pages/templates/knots/index.html
···
···
+
{{ define "title" }}knots{{ end }}
+
+
{{ define "content" }}
+
<div class="px-6 py-4">
+
<h1 class="text-xl font-bold dark:text-white">Knots</h1>
+
</div>
+
+
<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
+
<div class="flex flex-col gap-6">
+
{{ template "knots/fragments/knotListingFull" . }}
+
{{ block "register" . }} {{ end }}
+
</div>
+
</section>
+
{{ end }}
+
+
{{ define "register" }}
+
<section class="rounded max-w-2xl flex flex-col gap-2">
+
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">register a knot</h2>
+
<p class="mb-2 dark:text-gray-300">Enter the hostname of your knot to generate a key.</p>
+
<form
+
hx-post="/knots/key"
+
class="space-y-4"
+
hx-indicator="#register-button"
+
hx-swap="none"
+
>
+
<div class="flex gap-2">
+
<input
+
type="text"
+
id="domain"
+
name="domain"
+
placeholder="knot.example.com"
+
required
+
class="flex-1 w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400 px-3 py-2 border rounded"
+
>
+
<button
+
type="submit"
+
id="register-button"
+
class="btn rounded flex items-center py-2 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 group"
+
>
+
<span class="inline-flex items-center gap-2">
+
{{ i "plus" "w-4 h-4" }}
+
generate
+
</span>
+
<span class="pl-2 hidden group-[.htmx-request]:inline">
+
{{ i "loader-circle" "w-4 h-4 animate-spin" }}
+
</span>
+
</button>
+
</div>
+
+
<div id="registration-error" class="error dark:text-red-400"></div>
+
</form>
+
+
<div id="secret"></div>
+
</section>
+
{{ end }}
+1 -1
appview/spindles/spindles.go
···
l := s.Logger.With("handler", "removeMember")
noticeId := "operation-error"
-
defaultErr := "Failed to add member. Try again later."
fail := func() {
s.Pages.Notice(w, noticeId, defaultErr)
}
···
l := s.Logger.With("handler", "removeMember")
noticeId := "operation-error"
+
defaultErr := "Failed to remove member. Try again later."
fail := func() {
s.Pages.Notice(w, noticeId, defaultErr)
}