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

appview: user/repos: introduce "all repos" page

Changed files
+252 -131
appview
+22 -9
appview/pages/pages.go
···
type ProfilePageParams struct {
LoggedInUser *auth.User
-
UserDid string
-
UserHandle string
Repos []db.Repo
CollaboratingRepos []db.Repo
-
ProfileStats ProfileStats
-
FollowStatus db.FollowStatus
-
Profile *db.Profile
-
AvatarUri string
ProfileTimeline *db.ProfileTimeline
+
Card ProfileCard
DidHandleMap map[string]string
}
-
type ProfileStats struct {
-
Followers int
-
Following int
+
type ProfileCard struct {
+
UserDid string
+
UserHandle string
+
FollowStatus db.FollowStatus
+
AvatarUri string
+
Followers int
+
Following int
+
+
Profile *db.Profile
}
func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error {
return p.execute("user/profile", w, params)
+
}
+
+
type ReposPageParams struct {
+
LoggedInUser *auth.User
+
Repos []db.Repo
+
Card ProfileCard
+
+
DidHandleMap map[string]string
+
}
+
+
func (p *Pages) ReposPage(w io.Writer, params ReposPageParams) error {
+
return p.execute("user/repos", w, params)
}
type FollowFragmentParams struct {
+1 -1
appview/pages/templates/user/fragments/editBio.html
···
{{ define "user/fragments/editBio" }}
<form
-
hx-post="/{{ .LoggedInUser.Did }}/profile/bio"
+
hx-post="/profile/bio"
class="flex flex-col gap-4 my-2 max-w-full"
hx-disabled-elt="#save-btn,#cancel-btn"
hx-swap="none">
+1 -1
appview/pages/templates/user/fragments/editPins.html
···
{{ define "user/fragments/editPins" }}
{{ $profile := .Profile }}
<form
-
hx-post="/{{ .LoggedInUser.Did }}/profile/pins"
+
hx-post="/profile/pins"
hx-disabled-elt="#save-btn,#cancel-btn"
hx-swap="none">
<div class="flex items-center justify-between mb-2">
+98
appview/pages/templates/user/fragments/profileCard.html
···
+
{{ define "user/fragments/profileCard" }}
+
<div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm max-h-fit">
+
<div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center">
+
<div id="avatar" class="col-span-1 flex justify-center items-center">
+
{{ if .AvatarUri }}
+
<img class="w-3/4 rounded-full p-2" src="{{ .AvatarUri }}" />
+
{{ end }}
+
</div>
+
<div class="col-span-2">
+
<p title="{{ didOrHandle .UserDid .UserHandle }}"
+
class="text-lg font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
+
{{ didOrHandle .UserDid .UserHandle }}
+
</p>
+
+
<div class="md:hidden">
+
{{ block "followerFollowing" (list .Followers .Following) }} {{ end }}
+
</div>
+
</div>
+
<div class="col-span-3 md:col-span-full">
+
<div id="profile-bio" class="text-sm">
+
{{ $profile := .Profile }}
+
{{ with .Profile }}
+
+
{{ if .Description }}
+
<p class="text-base pb-4 md:pb-2">{{ .Description }}</p>
+
{{ end }}
+
+
<div class="hidden md:block">
+
{{ block "followerFollowing" (list $.Followers $.Following) }} {{ end }}
+
</div>
+
+
<div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
+
{{ if .Location }}
+
<div class="flex items-center gap-2">
+
<span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span>
+
<span>{{ .Location }}</span>
+
</div>
+
{{ end }}
+
{{ if .IncludeBluesky }}
+
<div class="flex items-center gap-2">
+
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
+
<a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">
+
bluesky/{{ didOrHandle $.UserDid $.UserHandle }}
+
</a>
+
</div>
+
{{ end }}
+
{{ range $link := .Links }}
+
{{ if $link }}
+
<div class="flex items-center gap-2">
+
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
+
<a href="{{ $link }}">{{ $link }}</a>
+
</div>
+
{{ end }}
+
{{ end }}
+
{{ if not $profile.IsStatsEmpty }}
+
<div class="flex items-center justify-evenly gap-2 py-2">
+
{{ range $stat := .Stats }}
+
{{ if $stat.Kind }}
+
<div class="flex flex-col items-center gap-2">
+
<span class="text-xl font-bold">{{ $stat.Value }}</span>
+
<span>{{ $stat.Kind.String }}</span>
+
</div>
+
{{ end }}
+
{{ end }}
+
</div>
+
{{ end }}
+
</div>
+
{{ end }}
+
{{ if ne .FollowStatus.String "IsSelf" }}
+
{{ template "user/fragments/follow" . }}
+
{{ else }}
+
<button id="editBtn"
+
class="btn mt-2 w-full flex items-center gap-2"
+
hx-target="#profile-bio"
+
hx-get="/profile/edit-bio"
+
hx-swap="innerHTML">
+
{{ i "pencil" "w-4 h-4" }}
+
edit
+
</button>
+
{{ end }}
+
</div>
+
<div id="update-profile" class="text-red-400 dark:text-red-500"></div>
+
</div>
+
</div>
+
</div>
+
{{ end }}
+
+
{{ define "followerFollowing" }}
+
{{ $followers := index . 0 }}
+
{{ $following := index . 1 }}
+
<div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm">
+
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
+
<span id="followers">{{ $followers }} followers</span>
+
<span class="select-none after:content-['·']"></span>
+
<span id="following">{{ $following }} following</span>
+
</div>
+
{{ end }}
+
+12 -102
appview/pages/templates/user/profile.html
···
-
{{ define "title" }}{{ or .UserHandle .UserDid }}{{ end }}
+
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }}{{ end }}
{{ define "content" }}
<div class="grid grid-cols-1 md:grid-cols-8 gap-6">
<div class="md:col-span-2 order-1 md:order-1">
-
{{ block "profileCard" . }}{{ end }}
+
{{ template "user/fragments/profileCard" .Card }}
</div>
<div id="all-repos" class="md:col-span-3 order-2 md:order-2">
{{ block "ownRepos" . }}{{ end }}
···
{{ end }}
{{ end }}
-
{{ define "profileCard" }}
-
<div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm max-h-fit">
-
<div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center">
-
<div id="avatar" class="col-span-1 flex justify-center items-center">
-
{{ if .AvatarUri }}
-
<img class="w-3/4 rounded-full p-2" src="{{ .AvatarUri }}" />
-
{{ end }}
-
</div>
-
<div class="col-span-2">
-
<p title="{{ didOrHandle .UserDid .UserHandle }}"
-
class="text-lg font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
-
{{ didOrHandle .UserDid .UserHandle }}
-
</p>
-
-
<div class="md:hidden">
-
{{ block "followerFollowing" .ProfileStats }} {{ end }}
-
</div>
-
</div>
-
<div class="col-span-3 md:col-span-full">
-
<div id="profile-bio" class="text-sm">
-
{{ $profile := .Profile }}
-
{{ with .Profile }}
-
-
{{ if .Description }}
-
<p class="text-base pb-4 md:pb-2">{{ .Description }}</p>
-
{{ end }}
-
-
<div class="hidden md:block">
-
{{ block "followerFollowing" $.ProfileStats }} {{ end }}
-
</div>
-
-
<div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
-
{{ if .Location }}
-
<div class="flex items-center gap-2">
-
<span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span>
-
<span>{{ .Location }}</span>
-
</div>
-
{{ end }}
-
{{ if .IncludeBluesky }}
-
<div class="flex items-center gap-2">
-
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
-
<a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">
-
bluesky/{{ didOrHandle $.UserDid $.UserHandle }}
-
</a>
-
</div>
-
{{ end }}
-
{{ range $link := .Links }}
-
{{ if $link }}
-
<div class="flex items-center gap-2">
-
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
-
<a href="{{ $link }}">{{ $link }}</a>
-
</div>
-
{{ end }}
-
{{ end }}
-
{{ if not $profile.IsStatsEmpty }}
-
<div class="flex items-center justify-evenly gap-2 py-2">
-
{{ range $stat := .Stats }}
-
{{ if $stat.Kind }}
-
<div class="flex flex-col items-center gap-2">
-
<span class="text-xl font-bold">{{ $stat.Value }}</span>
-
<span>{{ $stat.Kind.String }}</span>
-
</div>
-
{{ end }}
-
{{ end }}
-
</div>
-
{{ end }}
-
</div>
-
{{ end }}
-
{{ if ne .FollowStatus.String "IsSelf" }}
-
{{ template "user/fragments/follow" . }}
-
{{ else }}
-
<button id="editBtn"
-
class="btn mt-2 w-full flex items-center gap-2"
-
hx-target="#profile-bio"
-
hx-get="/{{ $.UserDid }}/profile/edit-bio"
-
hx-swap="innerHTML">
-
{{ i "pencil" "w-4 h-4" }}
-
edit
-
</button>
-
{{ end }}
-
</div>
-
<div id="update-profile" class="text-red-400 dark:text-red-500"></div>
-
</div>
-
</div>
-
</div>
-
{{ end }}
-
-
{{ define "followerFollowing" }}
-
<div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm">
-
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
-
<span id="followers">{{ .Followers }} followers</span>
-
<span class="select-none after:content-['·']"></span>
-
<span id="following">{{ .Following }} following</span>
-
</div>
-
{{ end }}
-
{{ define "ownRepos" }}
<div class="text-sm font-bold p-2 pr-0 dark:text-white flex items-center justify-between gap-2">
-
<span>PINNED REPOS</span>
-
{{ if and .LoggedInUser (eq .LoggedInUser.Did .UserDid) }}
-
<button hx-get="/{{ $.UserDid }}/profile/edit-pins" hx-target="#all-repos" class="btn font-normal text-sm flex gap-2 items-center">
+
<a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}?tab=repos"
+
class="flex text-black dark:text-white items-center gap-4 no-underline hover:no-underline group">
+
<span>PINNED REPOS</span>
+
<span class="flex md:hidden group-hover:flex gap-2 items-center font-normal text-sm text-gray-500 dark:text-gray-400 ">
+
view all {{ i "chevron-right" "w-4 h-4" }}
+
</span>
+
</a>
+
{{ if and .LoggedInUser (eq .LoggedInUser.Did .Card.UserDid) }}
+
<button hx-get="profile/edit-pins" hx-target="#all-repos" class="btn font-normal text-sm flex gap-2 items-center">
{{ i "pencil" "w-3 h-3" }}
edit
</button>
···
id="repo-card"
class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800">
<div id="repo-card-name" class="font-medium">
-
<a href="/@{{ or $.UserHandle $.UserDid }}/{{ .Name }}"
+
<a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}/{{ .Name }}"
>{{ .Name }}</a
>
</div>
+44
appview/pages/templates/user/repos.html
···
+
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} · repos {{ end }}
+
+
{{ define "content" }}
+
<div class="grid grid-cols-1 md:grid-cols-8 gap-6">
+
<div class="md:col-span-2 order-1 md:order-1">
+
{{ template "user/fragments/profileCard" .Card }}
+
</div>
+
<div id="all-repos" class="md:col-span-6 order-2 md:order-2">
+
{{ block "ownRepos" . }}{{ end }}
+
</div>
+
</div>
+
{{ end }}
+
+
{{ define "ownRepos" }}
+
<p class="text-sm font-bold p-2 dark:text-white">ALL REPOSITORIES</p>
+
<div id="repos" class="grid grid-cols-1 gap-4 mb-6">
+
{{ range .Repos }}
+
<div
+
id="repo-card"
+
class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800">
+
<div id="repo-card-name" class="font-medium">
+
<a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}/{{ .Name }}"
+
>{{ .Name }}</a
+
>
+
</div>
+
{{ if .Description }}
+
<div class="text-gray-600 dark:text-gray-300 text-sm">
+
{{ .Description }}
+
</div>
+
{{ end }}
+
<div class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto">
+
{{ if .RepoStats.StarCount }}
+
<div class="flex gap-1 items-center text-sm">
+
{{ i "star" "w-3 h-3 fill-current" }}
+
<span>{{ .RepoStats.StarCount }}</span>
+
</div>
+
{{ end }}
+
</div>
+
</div>
+
{{ else }}
+
<p class="px-6 dark:text-white">This user does not have any repos yet.</p>
+
{{ end }}
+
</div>
+
{{ end }}
+65 -10
appview/state/profile.go
···
"tangled.sh/tangled.sh/core/appview/pages"
)
-
func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
+
func (s *State) Profile(w http.ResponseWriter, r *http.Request) {
+
tabVal := r.URL.Query().Get("tab")
+
switch tabVal {
+
case "":
+
s.profilePage(w, r)
+
case "repos":
+
s.reposPage(w, r)
+
}
+
}
+
+
func (s *State) profilePage(w http.ResponseWriter, r *http.Request) {
didOrHandle := chi.URLParam(r, "user")
if didOrHandle == "" {
http.Error(w, "Bad request", http.StatusBadRequest)
···
profileAvatarUri := s.GetAvatarUri(ident.Handle.String())
s.pages.ProfilePage(w, pages.ProfilePageParams{
LoggedInUser: loggedInUser,
-
UserDid: ident.DID.String(),
-
UserHandle: ident.Handle.String(),
Repos: pinnedRepos,
CollaboratingRepos: pinnedCollaboratingRepos,
-
ProfileStats: pages.ProfileStats{
-
Followers: followers,
-
Following: following,
+
DidHandleMap: didHandleMap,
+
Card: pages.ProfileCard{
+
UserDid: ident.DID.String(),
+
UserHandle: ident.Handle.String(),
+
AvatarUri: profileAvatarUri,
+
Profile: profile,
+
FollowStatus: followStatus,
+
Followers: followers,
+
Following: following,
},
-
Profile: profile,
-
FollowStatus: db.FollowStatus(followStatus),
-
DidHandleMap: didHandleMap,
-
AvatarUri: profileAvatarUri,
ProfileTimeline: timeline,
+
})
+
}
+
+
func (s *State) reposPage(w http.ResponseWriter, r *http.Request) {
+
ident, ok := r.Context().Value("resolvedId").(identity.Identity)
+
if !ok {
+
s.pages.Error404(w)
+
return
+
}
+
+
profile, err := db.GetProfile(s.db, ident.DID.String())
+
if err != nil {
+
log.Printf("getting profile data for %s: %s", ident.DID.String(), err)
+
}
+
+
repos, err := db.GetAllReposByDid(s.db, ident.DID.String())
+
if err != nil {
+
log.Printf("getting repos for %s: %s", ident.DID.String(), err)
+
}
+
+
loggedInUser := s.auth.GetUser(r)
+
followStatus := db.IsNotFollowing
+
if loggedInUser != nil {
+
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String())
+
}
+
+
followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String())
+
if err != nil {
+
log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
+
}
+
+
profileAvatarUri := s.GetAvatarUri(ident.Handle.String())
+
+
s.pages.ReposPage(w, pages.ReposPageParams{
+
LoggedInUser: loggedInUser,
+
Repos: repos,
+
Card: pages.ProfileCard{
+
UserDid: ident.DID.String(),
+
UserHandle: ident.Handle.String(),
+
AvatarUri: profileAvatarUri,
+
Profile: profile,
+
FollowStatus: followStatus,
+
Followers: followers,
+
Following: following,
+
},
})
}
+9 -8
appview/state/router.go
···
r.Use(StripLeadingAt)
r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
-
r.Get("/", s.ProfilePage)
-
r.Route("/profile", func(r chi.Router) {
-
r.Use(middleware.AuthMiddleware(s.auth))
-
r.Get("/edit-bio", s.EditBioFragment)
-
r.Get("/edit-pins", s.EditPinsFragment)
-
r.Post("/bio", s.UpdateProfileBio)
-
r.Post("/pins", s.UpdateProfilePins)
-
})
+
r.Get("/", s.Profile)
r.With(ResolveRepo(s)).Route("/{repo}", func(r chi.Router) {
r.Get("/", s.RepoIndex)
···
r.With(middleware.AuthMiddleware(s.auth)).Route("/star", func(r chi.Router) {
r.Post("/", s.Star)
r.Delete("/", s.Star)
+
})
+
+
r.Route("/profile", func(r chi.Router) {
+
r.Use(middleware.AuthMiddleware(s.auth))
+
r.Get("/edit-bio", s.EditBioFragment)
+
r.Get("/edit-pins", s.EditPinsFragment)
+
r.Post("/bio", s.UpdateProfileBio)
+
r.Post("/pins", s.UpdateProfilePins)
})
r.Mount("/settings", s.SettingsRouter())