appview/pages: htmx-ify knots page #307

merged
opened by oppi.li targeting master from push-nwslswprzvmx
+27 -2
appview/pages/pages.go
··· 278 278 } 279 279 280 280 func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 281 - return p.execute("knots", w, params) 281 + return p.execute("knots/index", w, params) 282 282 } 283 283 284 284 type KnotParams struct { ··· 286 286 DidHandleMap map[string]string 287 287 Registration *db.Registration 288 288 Members []string 289 + Repos map[string][]db.Repo 289 290 IsOwner bool 290 291 } 291 292 292 293 func (p *Pages) Knot(w io.Writer, params KnotParams) error { 293 - return p.execute("knot", w, params) 294 + return p.execute("knots/dashboard", w, params) 295 + } 296 + 297 + type KnotListingParams struct { 298 + db.Registration 299 + } 300 + 301 + func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { 302 + return p.executePlain("knots/fragments/knotListing", w, params) 303 + } 304 + 305 + type KnotListingFullParams struct { 306 + Registrations []db.Registration 307 + } 308 + 309 + func (p *Pages) KnotListingFull(w io.Writer, params KnotListingFullParams) error { 310 + return p.executePlain("knots/fragments/knotListingFull", w, params) 311 + } 312 + 313 + type KnotSecretParams struct { 314 + Secret string 315 + } 316 + 317 + func (p *Pages) KnotSecret(w io.Writer, params KnotSecretParams) error { 318 + return p.executePlain("knots/fragments/secret", w, params) 294 319 } 295 320 296 321 type SpindlesParams struct {
-98
appview/pages/templates/knot.html
··· 1 - {{ define "title" }}{{ .Registration.Domain }}{{ end }} 2 - 3 - {{ define "content" }} 4 - <div class="p-6"> 5 - <p class="text-xl font-bold dark:text-white">{{ .Registration.Domain }}</p> 6 - </div> 7 - 8 - <div class="flex flex-col"> 9 - {{ block "registration-info" . }} {{ end }} 10 - {{ block "members" . }} {{ end }} 11 - {{ block "add-member" . }} {{ end }} 12 - </div> 13 - {{ end }} 14 - 15 - {{ define "registration-info" }} 16 - <section class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit"> 17 - <dl class="grid grid-cols-[auto_1fr] gap-x-4 dark:text-gray-200"> 18 - <dt class="font-bold">opened by</dt> 19 - <dd> 20 - <span> 21 - {{ index $.DidHandleMap .Registration.ByDid }} <span class="text-gray-500 dark:text-gray-400 font-mono">{{ .Registration.ByDid }}</span> 22 - </span> 23 - {{ if eq $.LoggedInUser.Did $.Registration.ByDid }} 24 - <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> 25 - {{ end }} 26 - </dd> 27 - 28 - <dt class="font-bold">opened</dt> 29 - <dd>{{ .Registration.Created | timeFmt }}</dd> 30 - 31 - {{ if .Registration.Registered }} 32 - <dt class="font-bold">registered</dt> 33 - <dd>{{ .Registration.Registered | timeFmt }}</dd> 34 - {{ else }} 35 - <dt class="font-bold">status</dt> 36 - <dd class="text-yellow-800 dark:text-yellow-200 bg-yellow-100 dark:bg-yellow-900 rounded px-2 py-1 inline-block"> 37 - Pending Registration 38 - </dd> 39 - {{ end }} 40 - </dl> 41 - 42 - {{ if not .Registration.Registered }} 43 - <div class="mt-4"> 44 - <button 45 - class="btn dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600" 46 - hx-post="/knots/{{.Domain}}/init" 47 - hx-swap="none"> 48 - Initialize Registration 49 - </button> 50 - </div> 51 - {{ end }} 52 - </section> 53 - {{ end }} 54 - 55 - {{ define "members" }} 56 - <h2 class="text-sm font-bold py-2 px-6 uppercase dark:text-gray-300">members</h2> 57 - <section class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit"> 58 - {{ if .Registration.Registered }} 59 - <div id="member-list" class="flex flex-col gap-4"> 60 - {{ range $.Members }} 61 - <div class="inline-flex items-center gap-4"> 62 - {{ i "user" "w-4 h-4 dark:text-gray-300" }} 63 - <a href="/{{index $.DidHandleMap .}}" class="text-gray-900 dark:text-white">{{index $.DidHandleMap .}} 64 - <span class="text-gray-500 dark:text-gray-400 font-mono">{{.}}</span> 65 - </a> 66 - </div> 67 - {{ else }} 68 - <p class="text-gray-500 dark:text-gray-400">No members have been added yet.</p> 69 - {{ end }} 70 - </div> 71 - {{ else }} 72 - <p class="text-gray-500 dark:text-gray-400">Members can be added after registration is complete.</p> 73 - {{ end }} 74 - </section> 75 - {{ end }} 76 - 77 - {{ define "add-member" }} 78 - {{ if $.IsOwner }} 79 - <h2 class="text-sm font-bold py-2 px-6 uppercase dark:text-gray-300">add member</h2> 80 - <section class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit"> 81 - <form 82 - hx-put="/knots/{{.Registration.Domain}}/member" 83 - class="max-w-2xl space-y-4"> 84 - <input 85 - type="text" 86 - id="subject" 87 - name="subject" 88 - placeholder="did or handle" 89 - required 90 - class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400"/> 91 - 92 - <button class="btn dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600" type="submit">add member</button> 93 - 94 - <div id="add-member-error" class="error dark:text-red-400"></div> 95 - </form> 96 - </section> 97 - {{ end }} 98 - {{ end }}
-93
appview/pages/templates/knots.html
··· 1 - {{ define "title" }}knots{{ end }} 2 - {{ define "content" }} 3 - <div class="p-6"> 4 - <p class="text-xl font-bold dark:text-white">Knots</p> 5 - </div> 6 - <div class="flex flex-col"> 7 - <h2 class="text-sm font-bold py-2 px-6 uppercase dark:text-gray-300">register a knot</h2> 8 - <section class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit"> 9 - <p class="mb-8 dark:text-gray-300">Generate a key to initialize your knot server.</p> 10 - <form 11 - hx-post="/knots/key" 12 - class="max-w-2xl mb-8 space-y-4" 13 - hx-indicator="#generate-knot-key-spinner" 14 - > 15 - <input 16 - type="text" 17 - id="domain" 18 - name="domain" 19 - placeholder="knot.example.com" 20 - required 21 - class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400" 22 - > 23 - <button class="btn dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 flex items-center" type="submit"> 24 - <span>generate key</span> 25 - <span id="generate-knot-key-spinner" class="group"> 26 - {{ i "loader-circle" "pl-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 27 - </span> 28 - </button> 29 - <div id="settings-knots-error" class="error dark:text-red-400"></div> 30 - </form> 31 - </section> 32 - 33 - <h2 class="text-sm font-bold py-2 px-6 uppercase dark:text-gray-300">my knots</h2> 34 - <section class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit"> 35 - <div id="knots-list" class="flex flex-col gap-6 mb-8"> 36 - {{ range .Registrations }} 37 - {{ if .Registered }} 38 - <div class="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4"> 39 - <div class="flex flex-col gap-1"> 40 - <div class="inline-flex items-center gap-4"> 41 - {{ i "git-branch" "w-3 h-3 dark:text-gray-300" }} 42 - <a href="/knots/{{ .Domain }}"> 43 - <p class="font-bold dark:text-white">{{ .Domain }}</p> 44 - </a> 45 - </div> 46 - <p class="text-sm text-gray-500 dark:text-gray-400">owned by {{ .ByDid }}</p> 47 - <p class="text-sm text-gray-500 dark:text-gray-400">registered {{ .Registered | timeFmt }}</p> 48 - </div> 49 - </div> 50 - {{ end }} 51 - {{ else }} 52 - <p class="text-sm text-gray-500 dark:text-gray-400">No knots registered</p> 53 - {{ end }} 54 - </div> 55 - </section> 56 - 57 - <h2 class="text-sm font-bold py-2 px-6 uppercase dark:text-gray-300">pending registrations</h2> 58 - <section class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit"> 59 - <div id="pending-knots-list" class="flex flex-col gap-6 mb-8"> 60 - {{ range .Registrations }} 61 - {{ if not .Registered }} 62 - <div class="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4"> 63 - <div class="flex flex-col gap-1"> 64 - <div class="inline-flex items-center gap-4"> 65 - <p class="font-bold dark:text-white">{{ .Domain }}</p> 66 - <div class="inline-flex items-center gap-1"> 67 - <span class="text-xs bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 px-2 py-1 rounded"> 68 - pending 69 - </span> 70 - </div> 71 - </div> 72 - <p class="text-sm text-gray-500 dark:text-gray-400">opened by {{ .ByDid }}</p> 73 - <p class="text-sm text-gray-500 dark:text-gray-400">created {{ .Created | timeFmt }}</p> 74 - </div> 75 - <div class="flex gap-2 items-center"> 76 - <button 77 - class="btn dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 flex gap-2 items-center group" 78 - hx-post="/knots/{{ .Domain }}/init" 79 - > 80 - {{ i "square-play" "w-5 h-5" }} 81 - <span class="hidden md:inline">initialize</span> 82 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 83 - </button> 84 - </div> 85 - </div> 86 - {{ end }} 87 - {{ else }} 88 - <p class="text-sm text-gray-500 dark:text-gray-400">No pending registrations</p> 89 - {{ end }} 90 - </div> 91 - </section> 92 - </div> 93 - {{ end }}
+63
appview/pages/templates/knots/dashboard.html
··· 1 + {{ define "title" }}{{ .Registration.Domain }}{{ end }} 2 + 3 + {{ define "content" }} 4 + <div class="px-6 py-4"> 5 + <div class="flex justify-between items-center"> 6 + <div id="left-side" class="flex gap-2 items-center"> 7 + <h1 class="text-xl font-bold dark:text-white"> 8 + {{ .Registration.Domain }} 9 + </h1> 10 + <span class="text-gray-500 text-base"> 11 + {{ .Registration.Created | shortTimeFmt }} ago 12 + </span> 13 + </div> 14 + <div id="right-side" class="flex gap-2"> 15 + {{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2" }} 16 + {{ if .Registration.Registered }} 17 + <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> 18 + {{ template "knots/fragments/addMemberModal" .Registration }} 19 + {{ else }} 20 + <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> 21 + {{ end }} 22 + </div> 23 + </div> 24 + <div id="operation-error" class="dark:text-red-400"></div> 25 + </div> 26 + 27 + {{ if .Members }} 28 + <section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white"> 29 + <div class="flex flex-col gap-2"> 30 + {{ block "knotMember" . }} {{ end }} 31 + </div> 32 + </section> 33 + {{ end }} 34 + {{ end }} 35 + 36 + {{ define "knotMember" }} 37 + {{ range .Members }} 38 + <div> 39 + <div class="flex justify-between items-center"> 40 + <div class="flex items-center gap-2"> 41 + {{ i "user" "size-4" }} 42 + {{ $user := index $.DidHandleMap . }} 43 + <a href="/{{ $user }}">{{ $user }} <span class="ml-2 font-mono text-gray-500">{{.}}</span></a> 44 + </div> 45 + </div> 46 + <div class="ml-2 pl-2 pt-2 border-l border-gray-200 dark:border-gray-700"> 47 + {{ $repos := index $.Repos . }} 48 + {{ range $repos }} 49 + <div class="flex gap-2 items-center"> 50 + {{ i "book-marked" "size-4" }} 51 + <a href="/{{ .Did }}/{{ .Name }}"> 52 + {{ .Name }} 53 + </a> 54 + </div> 55 + {{ else }} 56 + <div class="text-gray-500 dark:text-gray-400"> 57 + No repositories created yet. 58 + </div> 59 + {{ end }} 60 + </div> 61 + </div> 62 + {{ end }} 63 + {{ end }}
+58
appview/pages/templates/knots/fragments/addMemberModal.html
··· 1 + {{ define "knots/fragments/addMemberModal" }} 2 + <button 3 + class="btn gap-2 group" 4 + title="Add member to this spindle" 5 + popovertarget="add-member-{{ .Id }}" 6 + popovertargetaction="toggle" 7 + > 8 + {{ i "user-plus" "w-5 h-5" }} 9 + <span class="hidden md:inline">add member</span> 10 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 11 + </button> 12 + 13 + <div 14 + id="add-member-{{ .Id }}" 15 + popover 16 + class="bg-white w-full md:w-96 dark:bg-gray-800 p-4 rounded drop-shadow dark:text-white"> 17 + {{ block "addKnotMemberPopover" . }} {{ end }} 18 + </div> 19 + {{ end }} 20 + 21 + {{ define "addKnotMemberPopover" }} 22 + <form 23 + hx-put="/knots/{{ .Domain }}/member" 24 + hx-indicator="#spinner" 25 + hx-swap="none" 26 + class="flex flex-col gap-2" 27 + > 28 + <label for="member-did-{{ .Id }}" class="uppercase p-0"> 29 + ADD MEMBER 30 + </label> 31 + <p class="text-sm text-gray-500 dark:text-gray-400">Members can create repositories on this knot.</p> 32 + <input 33 + type="text" 34 + id="member-did-{{ .Id }}" 35 + name="subject" 36 + required 37 + placeholder="@foo.bsky.social" 38 + /> 39 + <div class="flex gap-2 pt-2"> 40 + <button 41 + type="button" 42 + popovertarget="add-member-{{ .Id }}" 43 + popovertargetaction="hide" 44 + 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" 45 + > 46 + {{ i "x" "size-4" }} cancel 47 + </button> 48 + <button type="submit" class="btn w-1/2 flex items-center"> 49 + <span class="inline-flex gap-2 items-center">{{ i "user-plus" "size-4" }} add</span> 50 + <span id="spinner" class="group"> 51 + {{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 52 + </span> 53 + </button> 54 + </div> 55 + <div id="add-member-error-{{ .Id }}" class="text-red-500 dark:text-red-400"></div> 56 + </form> 57 + {{ end }} 58 +
+51
appview/pages/templates/knots/fragments/knotListing.html
··· 1 + {{ define "knots/fragments/knotListing" }} 2 + <div 3 + id="knot-{{.Id}}" 4 + hx-swap-oob="true" 5 + class="flex items-center justify-between p-2 border-b border-gray-200 dark:border-gray-700"> 6 + {{ block "listLeftSide" . }} {{ end }} 7 + {{ block "listRightSide" . }} {{ end }} 8 + </div> 9 + {{ end }} 10 + 11 + {{ define "listLeftSide" }} 12 + <div class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]"> 13 + {{ i "hard-drive" "w-4 h-4" }} 14 + {{ if .Registered }} 15 + <a href="/knots/{{ .Domain }}"> 16 + {{ .Domain }} 17 + </a> 18 + {{ else }} 19 + {{ .Domain }} 20 + {{ end }} 21 + <span class="text-gray-500"> 22 + {{ .Created | shortTimeFmt }} ago 23 + </span> 24 + </div> 25 + {{ end }} 26 + 27 + {{ define "listRightSide" }} 28 + <div id="right-side" class="flex gap-2"> 29 + {{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2 text-sm" }} 30 + {{ if .Registered }} 31 + <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> 32 + {{ template "knots/fragments/addMemberModal" . }} 33 + {{ else }} 34 + <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> 35 + {{ block "initializeButton" . }} {{ end }} 36 + {{ end }} 37 + </div> 38 + {{ end }} 39 + 40 + {{ define "initializeButton" }} 41 + <button 42 + class="btn dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 flex gap-2 items-center group" 43 + hx-post="/knots/{{ .Domain }}/init" 44 + hx-swap="none" 45 + > 46 + {{ i "square-play" "w-5 h-5" }} 47 + <span class="hidden md:inline">initialize</span> 48 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 49 + </button> 50 + {{ end }} 51 +
+18
appview/pages/templates/knots/fragments/knotListingFull.html
··· 1 + {{ define "knots/fragments/knotListingFull" }} 2 + <section 3 + id="knot-listing-full" 4 + hx-swap-oob="true" 5 + class="rounded w-full flex flex-col gap-2"> 6 + <h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">your knots</h2> 7 + <div class="flex flex-col rounded border border-gray-200 dark:border-gray-700 w-full"> 8 + {{ range $knot := .Registrations }} 9 + {{ template "knots/fragments/knotListing" . }} 10 + {{ else }} 11 + <div class="flex items-center justify-center p-2 border-b border-gray-200 dark:border-gray-700 text-gray-500"> 12 + no knots registered yet 13 + </div> 14 + {{ end }} 15 + </div> 16 + <div id="operation-error" class="text-red-500 dark:text-red-400"></div> 17 + </section> 18 + {{ end }}
+10
appview/pages/templates/knots/fragments/secret.html
··· 1 + {{ define "knots/fragments/secret" }} 2 + <div 3 + id="secret" 4 + hx-swap-oob="true" 5 + 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"> 6 + <h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">generated secret</h2> 7 + <p class="pb-2">Configure your knot to use this secret, and then hit initialize.</p> 8 + <span class="font-mono overflow-x">{{ .Secret }}</span> 9 + </div> 10 + {{ end }}
+55
appview/pages/templates/knots/index.html
··· 1 + {{ define "title" }}knots{{ end }} 2 + 3 + {{ define "content" }} 4 + <div class="px-6 py-4"> 5 + <h1 class="text-xl font-bold dark:text-white">Knots</h1> 6 + </div> 7 + 8 + <section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white"> 9 + <div class="flex flex-col gap-6"> 10 + {{ template "knots/fragments/knotListingFull" . }} 11 + {{ block "register" . }} {{ end }} 12 + </div> 13 + </section> 14 + {{ end }} 15 + 16 + {{ define "register" }} 17 + <section class="rounded max-w-2xl flex flex-col gap-2"> 18 + <h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">register a knot</h2> 19 + <p class="mb-2 dark:text-gray-300">Enter the hostname of your knot to generate a key.</p> 20 + <form 21 + hx-post="/knots/key" 22 + class="space-y-4" 23 + hx-indicator="#register-button" 24 + hx-swap="none" 25 + > 26 + <div class="flex gap-2"> 27 + <input 28 + type="text" 29 + id="domain" 30 + name="domain" 31 + placeholder="knot.example.com" 32 + required 33 + 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" 34 + > 35 + <button 36 + type="submit" 37 + id="register-button" 38 + class="btn rounded flex items-center py-2 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 group" 39 + > 40 + <span class="inline-flex items-center gap-2"> 41 + {{ i "plus" "w-4 h-4" }} 42 + generate 43 + </span> 44 + <span class="pl-2 hidden group-[.htmx-request]:inline"> 45 + {{ i "loader-circle" "w-4 h-4 animate-spin" }} 46 + </span> 47 + </button> 48 + </div> 49 + 50 + <div id="registration-error" class="error dark:text-red-400"></div> 51 + </form> 52 + 53 + <div id="secret"></div> 54 + </section> 55 + {{ end }}
+1 -1
appview/spindles/spindles.go
··· 582 582 l := s.Logger.With("handler", "removeMember") 583 583 584 584 noticeId := "operation-error" 585 - defaultErr := "Failed to add member. Try again later." 585 + defaultErr := "Failed to remove member. Try again later." 586 586 fail := func() { 587 587 s.Pages.Notice(w, noticeId, defaultErr) 588 588 }