Monorepo for Tangled tangled.org

appview: add "starred-by" page to repos #1112

open opened by pdewey.com targeting master from pdewey.com/tangled-core: feat-starred-by-page

Adds a page and route for each repo that shows all users that have starred a given repo. This divides the star button within a repo page, adding an icon to the right side that can be clicked to open the new stars page.

Closes #427

Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:hm5f3dnm6jdhrc55qp2npdja/sh.tangled.repo.pull/3mg7bpskm4n22
+150 -20
Diff #2
+31
appview/db/star.go
··· 51 return &star, nil 52 } 53 54 // Remove a star 55 func DeleteStar(e Execer, did string, subjectAt syntax.ATURI) error { 56 _, err := e.Exec(`delete from stars where did = ? and subject_at = ?`, did, subjectAt)
··· 51 return &star, nil 52 } 53 54 + func GetStars(e Execer, subjectAt syntax.ATURI) ([]models.Star, error) { 55 + query := ` 56 + select did, subject_at, created, rkey 57 + from stars 58 + where subject_at = ? 59 + order by created desc 60 + ` 61 + rows, err := e.Query(query, subjectAt) 62 + if err != nil { 63 + return nil, err 64 + } 65 + defer rows.Close() 66 + 67 + var stars []models.Star 68 + for rows.Next() { 69 + var star models.Star 70 + var created string 71 + if err := rows.Scan(&star.Did, &star.RepoAt, &created, &star.Rkey); err != nil { 72 + return nil, err 73 + } 74 + 75 + star.Created = time.Now() 76 + if t, err := time.Parse(time.RFC3339, created); err == nil { 77 + star.Created = t 78 + } 79 + stars = append(stars, star) 80 + } 81 + 82 + return stars, rows.Err() 83 + } 84 + 85 // Remove a star 86 func DeleteStar(e Execer, did string, subjectAt syntax.ATURI) error { 87 _, err := e.Exec(`delete from stars where did = ? and subject_at = ?`, did, subjectAt)
+13
appview/pages/pages.go
··· 675 IsStarred bool 676 SubjectAt syntax.ATURI 677 StarCount int 678 HxSwapOob bool 679 } 680 ··· 1385 1386 func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1387 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1388 } 1389 1390 type PipelinesParams struct {
··· 675 IsStarred bool 676 SubjectAt syntax.ATURI 677 StarCount int 678 + StarsHref string 679 HxSwapOob bool 680 } 681 ··· 1386 1387 func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1388 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1389 + } 1390 + 1391 + type RepoStarsParams struct { 1392 + LoggedInUser *oauth.MultiAccountUser 1393 + RepoInfo repoinfo.RepoInfo 1394 + Active string 1395 + Starrers []models.Star 1396 + } 1397 + 1398 + func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error { 1399 + params.Active = "stars" 1400 + return p.executeRepo("repo/stars", w, params) 1401 } 1402 1403 type PipelinesParams struct {
+4
appview/pages/repoinfo/repoinfo.go
··· 35 return path.Join(r.ownerWithoutAt(), r.Name) 36 } 37 38 func (r RepoInfo) GetTabs() [][]string { 39 tabs := [][]string{ 40 {"overview", "/", "square-chart-gantt"},
··· 35 return path.Join(r.ownerWithoutAt(), r.Name) 36 } 37 38 + func (r RepoInfo) StarsHref() string { 39 + return fmt.Sprintf("/%s/stars", r.FullName()) 40 + } 41 + 42 func (r RepoInfo) GetTabs() [][]string { 43 tabs := [][]string{ 44 {"overview", "/", "square-chart-gantt"},
+31 -19
appview/pages/templates/fragments/starBtn.html
··· 1 {{ define "fragments/starBtn" }} 2 {{/* NOTE: this fragment is always replaced with hx-swap-oob */}} 3 - <button 4 id="starBtn" 5 - class="btn disabled:opacity-50 disabled:cursor-not-allowed flex gap-2 items-center group" 6 data-star-subject-at="{{ .SubjectAt }}" 7 - {{ if .IsStarred }} 8 - hx-delete="/star?subject={{ .SubjectAt }}&countHint={{ .StarCount }}" 9 - {{ else }} 10 - hx-post="/star?subject={{ .SubjectAt }}&countHint={{ .StarCount }}" 11 - {{ end }} 12 {{ if .HxSwapOob }}hx-swap-oob='outerHTML:#starBtn[data-star-subject-at="{{ .SubjectAt }}"]'{{ end }} 13 - 14 - hx-trigger="click" 15 - hx-disabled-elt="#starBtn" 16 > 17 - {{ if .IsStarred }} 18 - {{ i "star" "w-4 h-4 fill-current inline group-[.htmx-request]:hidden" }} 19 - {{ else }} 20 - {{ i "star" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 21 {{ end }} 22 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 23 - <span class="text-sm"> 24 - {{ .StarCount }} 25 - </span> 26 - </button> 27 {{ end }}
··· 1 {{ define "fragments/starBtn" }} 2 {{/* NOTE: this fragment is always replaced with hx-swap-oob */}} 3 + <div 4 id="starBtn" 5 + class="flex w-full min-h-[30px] items-stretch overflow-hidden rounded border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-sm" 6 data-star-subject-at="{{ .SubjectAt }}" 7 {{ if .HxSwapOob }}hx-swap-oob='outerHTML:#starBtn[data-star-subject-at="{{ .SubjectAt }}"]'{{ end }} 8 > 9 + <button 10 + class="flex flex-1 justify-center gap-2 items-center px-2 group disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700" 11 + {{ if .IsStarred }} 12 + hx-delete="/star?subject={{ .SubjectAt }}&countHint={{ .StarCount }}" 13 + {{ else }} 14 + hx-post="/star?subject={{ .SubjectAt }}&countHint={{ .StarCount }}" 15 + {{ end }} 16 + hx-trigger="click" 17 + hx-disabled-elt="this" 18 + > 19 + {{ if .IsStarred }} 20 + {{ i "star" "w-4 h-4 fill-current inline group-[.htmx-request]:hidden" }} 21 + {{ else }} 22 + {{ i "star" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 23 + {{ end }} 24 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 25 + <span class="text-sm"> 26 + {{ .StarCount }} 27 + </span> 28 + </button> 29 + {{ if .StarsHref }} 30 + <a 31 + href="{{ .StarsHref }}" 32 + class="flex items-center px-2 no-underline hover:no-underline border-l border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700" 33 + title="Starred by" 34 + > 35 + {{ i "users" "w-3 h-3" }} 36 + </a> 37 {{ end }} 38 + </div> 39 {{ end }}
+2 -1
appview/pages/templates/layouts/repobase.html
··· 115 {{ template "fragments/starBtn" 116 (dict "SubjectAt" .RepoInfo.RepoAt 117 "IsStarred" .RepoInfo.IsStarred 118 - "StarCount" .RepoInfo.Stats.StarCount) }} 119 <a 120 class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group" 121 hx-boost="true"
··· 115 {{ template "fragments/starBtn" 116 (dict "SubjectAt" .RepoInfo.RepoAt 117 "IsStarred" .RepoInfo.IsStarred 118 + "StarCount" .RepoInfo.Stats.StarCount 119 + "StarsHref" .RepoInfo.StarsHref) }} 120 <a 121 class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group" 122 hx-boost="true"
+24
appview/pages/templates/repo/stars.html
···
··· 1 + {{ define "title" }}stars 路 {{ .RepoInfo.FullName }}{{ end }} 2 + {{ define "repoContent" }} 3 + <div class="flex flex-col gap-4"> 4 + <h2 class="text-sm uppercase font-bold">Starred by</h2> 5 + <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> 6 + {{ range .Starrers }} 7 + {{ $handle := resolve .Did }} 8 + <div class="border border-gray-200 dark:border-gray-700 rounded p-4"> 9 + <div class="flex items-center gap-3"> 10 + {{ template "user/fragments/picLink" (list .Did "size-10") }} 11 + <div class="flex-1 min-w-0"> 12 + <a href="/{{ $handle }}" class="block truncate">{{ $handle }}</a> 13 + <p class="text-sm text-gray-500 dark:text-gray-400"> 14 + starred {{ .Created | relTimeFmt }} 15 + </p> 16 + </div> 17 + </div> 18 + </div> 19 + {{ else }} 20 + <p class="text-gray-500 dark:text-gray-400 col-span-3">No stars yet.</p> 21 + {{ end }} 22 + </div> 23 + </div> 24 + {{ end }}
+23
appview/repo/repo.go
··· 1195 } 1196 } 1197 1198 // this is used to rollback changes made to the PDS 1199 // 1200 // it is a no-op if the provided ATURI is empty
··· 1195 } 1196 } 1197 1198 + func (rp *Repo) Stars(w http.ResponseWriter, r *http.Request) { 1199 + l := rp.logger.With("handler", "Stars") 1200 + 1201 + user := rp.oauth.GetMultiAccountUser(r) 1202 + f, err := rp.repoResolver.Resolve(r) 1203 + if err != nil { 1204 + l.Error("failed to resolve source repo", "err", err) 1205 + return 1206 + } 1207 + 1208 + starrers, err := db.GetStars(rp.db, f.RepoAt()) 1209 + if err != nil { 1210 + l.Error("failed to fetch starrers", "err", err, "repoAt", f.RepoAt()) 1211 + return 1212 + } 1213 + 1214 + rp.pages.RepoStars(w, pages.RepoStarsParams{ 1215 + LoggedInUser: user, 1216 + RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 1217 + Starrers: starrers, 1218 + }) 1219 + } 1220 + 1221 // this is used to rollback changes made to the PDS 1222 // 1223 // it is a no-op if the provided ATURI is empty
+2
appview/repo/router.go
··· 45 // a file path 46 r.Get("/archive/{ref}", rp.DownloadArchive) 47 48 r.Route("/fork", func(r chi.Router) { 49 r.Use(middleware.AuthMiddleware(rp.oauth)) 50 r.Get("/", rp.ForkRepo)
··· 45 // a file path 46 r.Get("/archive/{ref}", rp.DownloadArchive) 47 48 + r.Get("/stars", rp.Stars) 49 + 50 r.Route("/fork", func(r chi.Router) { 51 r.Use(middleware.AuthMiddleware(rp.oauth)) 52 r.Get("/", rp.ForkRepo)
+20
appview/state/star.go
··· 1 package state 2 3 import ( 4 "log" 5 "net/http" 6 "time" ··· 35 log.Println("failed to authorize client", err) 36 return 37 } 38 39 switch r.Method { 40 case http.MethodPost: ··· 79 IsStarred: true, 80 SubjectAt: subjectUri, 81 StarCount: starCount, 82 }) 83 84 return ··· 119 IsStarred: false, 120 SubjectAt: subjectUri, 121 StarCount: starCount, 122 }) 123 124 return 125 } 126 127 }
··· 1 package state 2 3 import ( 4 + "context" 5 + "fmt" 6 "log" 7 "net/http" 8 "time" ··· 37 log.Println("failed to authorize client", err) 38 return 39 } 40 + 41 + starsHref := s.starsHref(r.Context(), subjectUri) 42 43 switch r.Method { 44 case http.MethodPost: ··· 83 IsStarred: true, 84 SubjectAt: subjectUri, 85 StarCount: starCount, 86 + StarsHref: starsHref, 87 }) 88 89 return ··· 124 IsStarred: false, 125 SubjectAt: subjectUri, 126 StarCount: starCount, 127 + StarsHref: starsHref, 128 }) 129 130 return 131 } 132 133 } 134 + 135 + func (s *State) starsHref(ctx context.Context, subjectUri syntax.ATURI) string { 136 + repo, err := db.GetRepoByAtUri(s.db, subjectUri.String()) 137 + if err != nil { 138 + return "" 139 + } 140 + 141 + id, err := s.idResolver.ResolveIdent(ctx, repo.Did) 142 + if err != nil { 143 + return fmt.Sprintf("/%s/%s/stars", repo.Did, repo.Name) 144 + } 145 + 146 + return fmt.Sprintf("/%s/%s/stars", id.Handle, repo.Name) 147 + }

History

3 rounds 8 comments
sign up or login to add to the discussion
3 commits
expand
appview/db: add GetStarrers to list stargazers for a repo
appview: add "starred-by" page at /{user}/{repo}/stars
appview/pages: split star button to include starrers link
no conflicts, ready to merge
expand 0 comments
3 commits
expand
appview/db: add GetStarrers to list stargazers for a repo
appview: add "starred-by" page at /{user}/{repo}/stars
appview/pages: split star button to include starrers link
expand 4 comments

Looks like the diff got bigger from a bunch of unrelated changes. None of those were changed in any of commits, do I just need to rebase?

*any of my commits

Yeah that's a bug from our implementation 馃槄 Rebasing to master will fix it.

3 commits
expand
appview/db: add GetStarrers to list stargazers for a repo
appview: add "starred-by" page at /{user}/{repo}/stars
appview/pages: split star button to include starrers link
expand 4 comments

Also, screenshots of what this new page and the stars button look like can be seen in the tangled discord

Thank you for the contribution! As .StarsHref is pretty constant for the repository, can we generate it from star handlers instead of receiving as a url query?

Or we can just not include the surrounding div on add Star htmx api.

I like the idea of generating it from a handler, I'll implement that.