+22
-9
appview/pages/pages.go
+22
-9
appview/pages/pages.go
···
311
311
312
312
type ProfilePageParams struct {
313
313
LoggedInUser *auth.User
314
-
UserDid string
315
-
UserHandle string
316
314
Repos []db.Repo
317
315
CollaboratingRepos []db.Repo
318
-
ProfileStats ProfileStats
319
-
FollowStatus db.FollowStatus
320
-
Profile *db.Profile
321
-
AvatarUri string
322
316
ProfileTimeline *db.ProfileTimeline
317
+
Card ProfileCard
323
318
324
319
DidHandleMap map[string]string
325
320
}
326
321
327
-
type ProfileStats struct {
328
-
Followers int
329
-
Following int
322
+
type ProfileCard struct {
323
+
UserDid string
324
+
UserHandle string
325
+
FollowStatus db.FollowStatus
326
+
AvatarUri string
327
+
Followers int
328
+
Following int
329
+
330
+
Profile *db.Profile
330
331
}
331
332
332
333
func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error {
333
334
return p.execute("user/profile", w, params)
335
+
}
336
+
337
+
type ReposPageParams struct {
338
+
LoggedInUser *auth.User
339
+
Repos []db.Repo
340
+
Card ProfileCard
341
+
342
+
DidHandleMap map[string]string
343
+
}
344
+
345
+
func (p *Pages) ReposPage(w io.Writer, params ReposPageParams) error {
346
+
return p.execute("user/repos", w, params)
334
347
}
335
348
336
349
type FollowFragmentParams struct {
+1
-1
appview/pages/templates/user/fragments/editBio.html
+1
-1
appview/pages/templates/user/fragments/editBio.html
+1
-1
appview/pages/templates/user/fragments/editPins.html
+1
-1
appview/pages/templates/user/fragments/editPins.html
+98
appview/pages/templates/user/fragments/profileCard.html
+98
appview/pages/templates/user/fragments/profileCard.html
···
1
+
{{ define "user/fragments/profileCard" }}
2
+
<div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm max-h-fit">
3
+
<div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center">
4
+
<div id="avatar" class="col-span-1 flex justify-center items-center">
5
+
{{ if .AvatarUri }}
6
+
<img class="w-3/4 rounded-full p-2" src="{{ .AvatarUri }}" />
7
+
{{ end }}
8
+
</div>
9
+
<div class="col-span-2">
10
+
<p title="{{ didOrHandle .UserDid .UserHandle }}"
11
+
class="text-lg font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
12
+
{{ didOrHandle .UserDid .UserHandle }}
13
+
</p>
14
+
15
+
<div class="md:hidden">
16
+
{{ block "followerFollowing" (list .Followers .Following) }} {{ end }}
17
+
</div>
18
+
</div>
19
+
<div class="col-span-3 md:col-span-full">
20
+
<div id="profile-bio" class="text-sm">
21
+
{{ $profile := .Profile }}
22
+
{{ with .Profile }}
23
+
24
+
{{ if .Description }}
25
+
<p class="text-base pb-4 md:pb-2">{{ .Description }}</p>
26
+
{{ end }}
27
+
28
+
<div class="hidden md:block">
29
+
{{ block "followerFollowing" (list $.Followers $.Following) }} {{ end }}
30
+
</div>
31
+
32
+
<div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
33
+
{{ if .Location }}
34
+
<div class="flex items-center gap-2">
35
+
<span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span>
36
+
<span>{{ .Location }}</span>
37
+
</div>
38
+
{{ end }}
39
+
{{ if .IncludeBluesky }}
40
+
<div class="flex items-center gap-2">
41
+
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
42
+
<a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">
43
+
bluesky/{{ didOrHandle $.UserDid $.UserHandle }}
44
+
</a>
45
+
</div>
46
+
{{ end }}
47
+
{{ range $link := .Links }}
48
+
{{ if $link }}
49
+
<div class="flex items-center gap-2">
50
+
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
51
+
<a href="{{ $link }}">{{ $link }}</a>
52
+
</div>
53
+
{{ end }}
54
+
{{ end }}
55
+
{{ if not $profile.IsStatsEmpty }}
56
+
<div class="flex items-center justify-evenly gap-2 py-2">
57
+
{{ range $stat := .Stats }}
58
+
{{ if $stat.Kind }}
59
+
<div class="flex flex-col items-center gap-2">
60
+
<span class="text-xl font-bold">{{ $stat.Value }}</span>
61
+
<span>{{ $stat.Kind.String }}</span>
62
+
</div>
63
+
{{ end }}
64
+
{{ end }}
65
+
</div>
66
+
{{ end }}
67
+
</div>
68
+
{{ end }}
69
+
{{ if ne .FollowStatus.String "IsSelf" }}
70
+
{{ template "user/fragments/follow" . }}
71
+
{{ else }}
72
+
<button id="editBtn"
73
+
class="btn mt-2 w-full flex items-center gap-2"
74
+
hx-target="#profile-bio"
75
+
hx-get="/profile/edit-bio"
76
+
hx-swap="innerHTML">
77
+
{{ i "pencil" "w-4 h-4" }}
78
+
edit
79
+
</button>
80
+
{{ end }}
81
+
</div>
82
+
<div id="update-profile" class="text-red-400 dark:text-red-500"></div>
83
+
</div>
84
+
</div>
85
+
</div>
86
+
{{ end }}
87
+
88
+
{{ define "followerFollowing" }}
89
+
{{ $followers := index . 0 }}
90
+
{{ $following := index . 1 }}
91
+
<div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm">
92
+
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
93
+
<span id="followers">{{ $followers }} followers</span>
94
+
<span class="select-none after:content-['·']"></span>
95
+
<span id="following">{{ $following }} following</span>
96
+
</div>
97
+
{{ end }}
98
+
+12
-102
appview/pages/templates/user/profile.html
+12
-102
appview/pages/templates/user/profile.html
···
1
-
{{ define "title" }}{{ or .UserHandle .UserDid }}{{ end }}
1
+
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }}{{ end }}
2
2
3
3
{{ define "content" }}
4
4
<div class="grid grid-cols-1 md:grid-cols-8 gap-6">
5
5
<div class="md:col-span-2 order-1 md:order-1">
6
-
{{ block "profileCard" . }}{{ end }}
6
+
{{ template "user/fragments/profileCard" .Card }}
7
7
</div>
8
8
<div id="all-repos" class="md:col-span-3 order-2 md:order-2">
9
9
{{ block "ownRepos" . }}{{ end }}
···
225
225
{{ end }}
226
226
{{ end }}
227
227
228
-
{{ define "profileCard" }}
229
-
<div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm max-h-fit">
230
-
<div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center">
231
-
<div id="avatar" class="col-span-1 flex justify-center items-center">
232
-
{{ if .AvatarUri }}
233
-
<img class="w-3/4 rounded-full p-2" src="{{ .AvatarUri }}" />
234
-
{{ end }}
235
-
</div>
236
-
<div class="col-span-2">
237
-
<p title="{{ didOrHandle .UserDid .UserHandle }}"
238
-
class="text-lg font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
239
-
{{ didOrHandle .UserDid .UserHandle }}
240
-
</p>
241
-
242
-
<div class="md:hidden">
243
-
{{ block "followerFollowing" .ProfileStats }} {{ end }}
244
-
</div>
245
-
</div>
246
-
<div class="col-span-3 md:col-span-full">
247
-
<div id="profile-bio" class="text-sm">
248
-
{{ $profile := .Profile }}
249
-
{{ with .Profile }}
250
-
251
-
{{ if .Description }}
252
-
<p class="text-base pb-4 md:pb-2">{{ .Description }}</p>
253
-
{{ end }}
254
-
255
-
<div class="hidden md:block">
256
-
{{ block "followerFollowing" $.ProfileStats }} {{ end }}
257
-
</div>
258
-
259
-
<div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
260
-
{{ if .Location }}
261
-
<div class="flex items-center gap-2">
262
-
<span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span>
263
-
<span>{{ .Location }}</span>
264
-
</div>
265
-
{{ end }}
266
-
{{ if .IncludeBluesky }}
267
-
<div class="flex items-center gap-2">
268
-
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
269
-
<a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">
270
-
bluesky/{{ didOrHandle $.UserDid $.UserHandle }}
271
-
</a>
272
-
</div>
273
-
{{ end }}
274
-
{{ range $link := .Links }}
275
-
{{ if $link }}
276
-
<div class="flex items-center gap-2">
277
-
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
278
-
<a href="{{ $link }}">{{ $link }}</a>
279
-
</div>
280
-
{{ end }}
281
-
{{ end }}
282
-
{{ if not $profile.IsStatsEmpty }}
283
-
<div class="flex items-center justify-evenly gap-2 py-2">
284
-
{{ range $stat := .Stats }}
285
-
{{ if $stat.Kind }}
286
-
<div class="flex flex-col items-center gap-2">
287
-
<span class="text-xl font-bold">{{ $stat.Value }}</span>
288
-
<span>{{ $stat.Kind.String }}</span>
289
-
</div>
290
-
{{ end }}
291
-
{{ end }}
292
-
</div>
293
-
{{ end }}
294
-
</div>
295
-
{{ end }}
296
-
{{ if ne .FollowStatus.String "IsSelf" }}
297
-
{{ template "user/fragments/follow" . }}
298
-
{{ else }}
299
-
<button id="editBtn"
300
-
class="btn mt-2 w-full flex items-center gap-2"
301
-
hx-target="#profile-bio"
302
-
hx-get="/{{ $.UserDid }}/profile/edit-bio"
303
-
hx-swap="innerHTML">
304
-
{{ i "pencil" "w-4 h-4" }}
305
-
edit
306
-
</button>
307
-
{{ end }}
308
-
</div>
309
-
<div id="update-profile" class="text-red-400 dark:text-red-500"></div>
310
-
</div>
311
-
</div>
312
-
</div>
313
-
{{ end }}
314
-
315
-
{{ define "followerFollowing" }}
316
-
<div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm">
317
-
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
318
-
<span id="followers">{{ .Followers }} followers</span>
319
-
<span class="select-none after:content-['·']"></span>
320
-
<span id="following">{{ .Following }} following</span>
321
-
</div>
322
-
{{ end }}
323
-
324
228
{{ define "ownRepos" }}
325
229
<div class="text-sm font-bold p-2 pr-0 dark:text-white flex items-center justify-between gap-2">
326
-
<span>PINNED REPOS</span>
327
-
{{ if and .LoggedInUser (eq .LoggedInUser.Did .UserDid) }}
328
-
<button hx-get="/{{ $.UserDid }}/profile/edit-pins" hx-target="#all-repos" class="btn font-normal text-sm flex gap-2 items-center">
230
+
<a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}?tab=repos"
231
+
class="flex text-black dark:text-white items-center gap-4 no-underline hover:no-underline group">
232
+
<span>PINNED REPOS</span>
233
+
<span class="flex md:hidden group-hover:flex gap-2 items-center font-normal text-sm text-gray-500 dark:text-gray-400 ">
234
+
view all {{ i "chevron-right" "w-4 h-4" }}
235
+
</span>
236
+
</a>
237
+
{{ if and .LoggedInUser (eq .LoggedInUser.Did .Card.UserDid) }}
238
+
<button hx-get="profile/edit-pins" hx-target="#all-repos" class="btn font-normal text-sm flex gap-2 items-center">
329
239
{{ i "pencil" "w-3 h-3" }}
330
240
edit
331
241
</button>
···
337
247
id="repo-card"
338
248
class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800">
339
249
<div id="repo-card-name" class="font-medium">
340
-
<a href="/@{{ or $.UserHandle $.UserDid }}/{{ .Name }}"
250
+
<a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}/{{ .Name }}"
341
251
>{{ .Name }}</a
342
252
>
343
253
</div>
+44
appview/pages/templates/user/repos.html
+44
appview/pages/templates/user/repos.html
···
1
+
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} · repos {{ end }}
2
+
3
+
{{ define "content" }}
4
+
<div class="grid grid-cols-1 md:grid-cols-8 gap-6">
5
+
<div class="md:col-span-2 order-1 md:order-1">
6
+
{{ template "user/fragments/profileCard" .Card }}
7
+
</div>
8
+
<div id="all-repos" class="md:col-span-6 order-2 md:order-2">
9
+
{{ block "ownRepos" . }}{{ end }}
10
+
</div>
11
+
</div>
12
+
{{ end }}
13
+
14
+
{{ define "ownRepos" }}
15
+
<p class="text-sm font-bold p-2 dark:text-white">ALL REPOSITORIES</p>
16
+
<div id="repos" class="grid grid-cols-1 gap-4 mb-6">
17
+
{{ range .Repos }}
18
+
<div
19
+
id="repo-card"
20
+
class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800">
21
+
<div id="repo-card-name" class="font-medium">
22
+
<a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}/{{ .Name }}"
23
+
>{{ .Name }}</a
24
+
>
25
+
</div>
26
+
{{ if .Description }}
27
+
<div class="text-gray-600 dark:text-gray-300 text-sm">
28
+
{{ .Description }}
29
+
</div>
30
+
{{ end }}
31
+
<div class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto">
32
+
{{ if .RepoStats.StarCount }}
33
+
<div class="flex gap-1 items-center text-sm">
34
+
{{ i "star" "w-3 h-3 fill-current" }}
35
+
<span>{{ .RepoStats.StarCount }}</span>
36
+
</div>
37
+
{{ end }}
38
+
</div>
39
+
</div>
40
+
{{ else }}
41
+
<p class="px-6 dark:text-white">This user does not have any repos yet.</p>
42
+
{{ end }}
43
+
</div>
44
+
{{ end }}
+65
-10
appview/state/profile.go
+65
-10
appview/state/profile.go
···
20
20
"tangled.sh/tangled.sh/core/appview/pages"
21
21
)
22
22
23
-
func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
23
+
func (s *State) Profile(w http.ResponseWriter, r *http.Request) {
24
+
tabVal := r.URL.Query().Get("tab")
25
+
switch tabVal {
26
+
case "":
27
+
s.profilePage(w, r)
28
+
case "repos":
29
+
s.reposPage(w, r)
30
+
}
31
+
}
32
+
33
+
func (s *State) profilePage(w http.ResponseWriter, r *http.Request) {
24
34
didOrHandle := chi.URLParam(r, "user")
25
35
if didOrHandle == "" {
26
36
http.Error(w, "Bad request", http.StatusBadRequest)
···
118
128
profileAvatarUri := s.GetAvatarUri(ident.Handle.String())
119
129
s.pages.ProfilePage(w, pages.ProfilePageParams{
120
130
LoggedInUser: loggedInUser,
121
-
UserDid: ident.DID.String(),
122
-
UserHandle: ident.Handle.String(),
123
131
Repos: pinnedRepos,
124
132
CollaboratingRepos: pinnedCollaboratingRepos,
125
-
ProfileStats: pages.ProfileStats{
126
-
Followers: followers,
127
-
Following: following,
133
+
DidHandleMap: didHandleMap,
134
+
Card: pages.ProfileCard{
135
+
UserDid: ident.DID.String(),
136
+
UserHandle: ident.Handle.String(),
137
+
AvatarUri: profileAvatarUri,
138
+
Profile: profile,
139
+
FollowStatus: followStatus,
140
+
Followers: followers,
141
+
Following: following,
128
142
},
129
-
Profile: profile,
130
-
FollowStatus: db.FollowStatus(followStatus),
131
-
DidHandleMap: didHandleMap,
132
-
AvatarUri: profileAvatarUri,
133
143
ProfileTimeline: timeline,
144
+
})
145
+
}
146
+
147
+
func (s *State) reposPage(w http.ResponseWriter, r *http.Request) {
148
+
ident, ok := r.Context().Value("resolvedId").(identity.Identity)
149
+
if !ok {
150
+
s.pages.Error404(w)
151
+
return
152
+
}
153
+
154
+
profile, err := db.GetProfile(s.db, ident.DID.String())
155
+
if err != nil {
156
+
log.Printf("getting profile data for %s: %s", ident.DID.String(), err)
157
+
}
158
+
159
+
repos, err := db.GetAllReposByDid(s.db, ident.DID.String())
160
+
if err != nil {
161
+
log.Printf("getting repos for %s: %s", ident.DID.String(), err)
162
+
}
163
+
164
+
loggedInUser := s.auth.GetUser(r)
165
+
followStatus := db.IsNotFollowing
166
+
if loggedInUser != nil {
167
+
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String())
168
+
}
169
+
170
+
followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String())
171
+
if err != nil {
172
+
log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
173
+
}
174
+
175
+
profileAvatarUri := s.GetAvatarUri(ident.Handle.String())
176
+
177
+
s.pages.ReposPage(w, pages.ReposPageParams{
178
+
LoggedInUser: loggedInUser,
179
+
Repos: repos,
180
+
Card: pages.ProfileCard{
181
+
UserDid: ident.DID.String(),
182
+
UserHandle: ident.Handle.String(),
183
+
AvatarUri: profileAvatarUri,
184
+
Profile: profile,
185
+
FollowStatus: followStatus,
186
+
Followers: followers,
187
+
Following: following,
188
+
},
134
189
})
135
190
}
136
191
+9
-8
appview/state/router.go
+9
-8
appview/state/router.go
···
53
53
r.Use(StripLeadingAt)
54
54
55
55
r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
56
-
r.Get("/", s.ProfilePage)
57
-
r.Route("/profile", func(r chi.Router) {
58
-
r.Use(middleware.AuthMiddleware(s.auth))
59
-
r.Get("/edit-bio", s.EditBioFragment)
60
-
r.Get("/edit-pins", s.EditPinsFragment)
61
-
r.Post("/bio", s.UpdateProfileBio)
62
-
r.Post("/pins", s.UpdateProfilePins)
63
-
})
56
+
r.Get("/", s.Profile)
64
57
65
58
r.With(ResolveRepo(s)).Route("/{repo}", func(r chi.Router) {
66
59
r.Get("/", s.RepoIndex)
···
244
237
r.With(middleware.AuthMiddleware(s.auth)).Route("/star", func(r chi.Router) {
245
238
r.Post("/", s.Star)
246
239
r.Delete("/", s.Star)
240
+
})
241
+
242
+
r.Route("/profile", func(r chi.Router) {
243
+
r.Use(middleware.AuthMiddleware(s.auth))
244
+
r.Get("/edit-bio", s.EditBioFragment)
245
+
r.Get("/edit-pins", s.EditPinsFragment)
246
+
r.Post("/bio", s.UpdateProfileBio)
247
+
r.Post("/pins", s.UpdateProfilePins)
247
248
})
248
249
249
250
r.Mount("/settings", s.SettingsRouter())