Signed-off-by: oppiliappan me@oppi.li
+27
-2
appview/pages/pages.go
+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
-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
-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
+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
+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
+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
+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
+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
+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
+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
}