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 51 return &star, nil 52 52 } 53 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 + 54 85 // Remove a star 55 86 func DeleteStar(e Execer, did string, subjectAt syntax.ATURI) error { 56 87 _, err := e.Exec(`delete from stars where did = ? and subject_at = ?`, did, subjectAt)
+13
appview/pages/pages.go
··· 675 675 IsStarred bool 676 676 SubjectAt syntax.ATURI 677 677 StarCount int 678 + StarsHref string 678 679 HxSwapOob bool 679 680 } 680 681 ··· 1385 1386 1386 1387 func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1387 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) 1388 1401 } 1389 1402 1390 1403 type PipelinesParams struct {
+4
appview/pages/repoinfo/repoinfo.go
··· 35 35 return path.Join(r.ownerWithoutAt(), r.Name) 36 36 } 37 37 38 + func (r RepoInfo) StarsHref() string { 39 + return fmt.Sprintf("/%s/stars", r.FullName()) 40 + } 41 + 38 42 func (r RepoInfo) GetTabs() [][]string { 39 43 tabs := [][]string{ 40 44 {"overview", "/", "square-chart-gantt"},
+31 -19
appview/pages/templates/fragments/starBtn.html
··· 1 1 {{ define "fragments/starBtn" }} 2 2 {{/* NOTE: this fragment is always replaced with hx-swap-oob */}} 3 - <button 3 + <div 4 4 id="starBtn" 5 - class="btn disabled:opacity-50 disabled:cursor-not-allowed flex gap-2 items-center group" 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 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 7 {{ if .HxSwapOob }}hx-swap-oob='outerHTML:#starBtn[data-star-subject-at="{{ .SubjectAt }}"]'{{ end }} 13 - 14 - hx-trigger="click" 15 - hx-disabled-elt="#starBtn" 16 8 > 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" }} 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> 21 37 {{ 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> 38 + </div> 27 39 {{ end }}
+2 -1
appview/pages/templates/layouts/repobase.html
··· 115 115 {{ template "fragments/starBtn" 116 116 (dict "SubjectAt" .RepoInfo.RepoAt 117 117 "IsStarred" .RepoInfo.IsStarred 118 - "StarCount" .RepoInfo.Stats.StarCount) }} 118 + "StarCount" .RepoInfo.Stats.StarCount 119 + "StarsHref" .RepoInfo.StarsHref) }} 119 120 <a 120 121 class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group" 121 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 1195 } 1196 1196 } 1197 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 + 1198 1221 // this is used to rollback changes made to the PDS 1199 1222 // 1200 1223 // it is a no-op if the provided ATURI is empty
+2
appview/repo/router.go
··· 45 45 // a file path 46 46 r.Get("/archive/{ref}", rp.DownloadArchive) 47 47 48 + r.Get("/stars", rp.Stars) 49 + 48 50 r.Route("/fork", func(r chi.Router) { 49 51 r.Use(middleware.AuthMiddleware(rp.oauth)) 50 52 r.Get("/", rp.ForkRepo)
+20
appview/state/star.go
··· 1 1 package state 2 2 3 3 import ( 4 + "context" 5 + "fmt" 4 6 "log" 5 7 "net/http" 6 8 "time" ··· 35 37 log.Println("failed to authorize client", err) 36 38 return 37 39 } 40 + 41 + starsHref := s.starsHref(r.Context(), subjectUri) 38 42 39 43 switch r.Method { 40 44 case http.MethodPost: ··· 79 83 IsStarred: true, 80 84 SubjectAt: subjectUri, 81 85 StarCount: starCount, 86 + StarsHref: starsHref, 82 87 }) 83 88 84 89 return ··· 119 124 IsStarred: false, 120 125 SubjectAt: subjectUri, 121 126 StarCount: starCount, 127 + StarsHref: starsHref, 122 128 }) 123 129 124 130 return 125 131 } 126 132 127 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.