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 3
AT URI
at://did:plc:hm5f3dnm6jdhrc55qp2npdja/sh.tangled.repo.pull/3mg7bpskm4n22
+133 -19
Diff #3
+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
··· 688 688 IsStarred bool 689 689 SubjectAt syntax.ATURI 690 690 StarCount int 691 + RepoName string 691 692 HxSwapOob bool 692 693 } 693 694 ··· 1416 1417 1417 1418 func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1418 1419 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1420 + } 1421 + 1422 + type RepoStarsParams struct { 1423 + LoggedInUser *oauth.MultiAccountUser 1424 + RepoInfo repoinfo.RepoInfo 1425 + Active string 1426 + Starrers []models.Star 1427 + } 1428 + 1429 + func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error { 1430 + params.Active = "overview" 1431 + return p.executeRepo("repo/stars", w, params) 1419 1432 } 1420 1433 1421 1434 type PipelinesParams struct {
+33 -18
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" }} 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 }}&repoName={{ .RepoName }}" 13 + {{ else }} 14 + hx-post="/star?subject={{ .SubjectAt }}&countHint={{ .StarCount }}&repoName={{ .RepoName }}" 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 md:hidden group-[.htmx-request]:hidden">{{ if .IsStarred }}unstar{{ else }}star{{ end }}</span> 26 + </button> 27 + {{ if .RepoName }} 28 + {{ $did := .SubjectAt.Authority | string }} 29 + <a 30 + href="/{{ resolve $did }}/{{ .RepoName }}/stars" 31 + class="flex items-center px-2 text-sm no-underline hover:no-underline border-l border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700" 32 + title="Starred by" 33 + > 34 + {{ .StarCount }} 35 + </a> 19 36 {{ else }} 20 - {{ i "star" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 37 + <span class="flex items-center px-2 text-sm border-l border-gray-200 dark:border-gray-700"> 38 + {{ .StarCount }} 39 + </span> 21 40 {{ 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> 41 + </div> 27 42 {{ 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 + "RepoName" .RepoInfo.Name) }} 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
··· 1201 1201 } 1202 1202 } 1203 1203 1204 + func (rp *Repo) Stars(w http.ResponseWriter, r *http.Request) { 1205 + l := rp.logger.With("handler", "Stars") 1206 + 1207 + user := rp.oauth.GetMultiAccountUser(r) 1208 + f, err := rp.repoResolver.Resolve(r) 1209 + if err != nil { 1210 + l.Error("failed to resolve source repo", "err", err) 1211 + return 1212 + } 1213 + 1214 + starrers, err := db.GetStars(rp.db, f.RepoAt()) 1215 + if err != nil { 1216 + l.Error("failed to fetch starrers", "err", err, "repoAt", f.RepoAt()) 1217 + return 1218 + } 1219 + 1220 + rp.pages.RepoStars(w, pages.RepoStarsParams{ 1221 + LoggedInUser: user, 1222 + RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 1223 + Starrers: starrers, 1224 + }) 1225 + } 1226 + 1204 1227 // this is used to rollback changes made to the PDS 1205 1228 // 1206 1229 // 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)
+5
appview/state/star.go
··· 36 36 return 37 37 } 38 38 39 + repoName := r.URL.Query().Get("repoName") 40 + 39 41 switch r.Method { 40 42 case http.MethodPost: 41 43 createdAt := time.Now().Format(time.RFC3339) ··· 79 81 IsStarred: true, 80 82 SubjectAt: subjectUri, 81 83 StarCount: starCount, 84 + RepoName: repoName, 82 85 }) 83 86 84 87 return ··· 119 122 IsStarred: false, 120 123 SubjectAt: subjectUri, 121 124 StarCount: starCount, 125 + RepoName: repoName, 122 126 }) 123 127 124 128 return 125 129 } 126 130 127 131 } 132 +

History

4 rounds 14 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 6 comments
  • here we should use overview, since there is no stars tab
  • why do we need to pass StarsHref around? not saying we shouldn't, just curious!
  • i would like for this button to be styled more like the codeberg/github buttons, [icon "star" | count], where clicking on the count goes to the starred-by page, and clicking on the left portion performs the star creation/deletion action.

I think the difficulty with trying to style like codeberg is that it has distinctly different buttons for fork and view fork, and watch and view watchers, in addition to just stars and starrers. With how the buttons are set up currently on tangled, I'm not sure if it would make sense from a user's perspective if clicking on the number shows the starrers, since it visually looks like the same button as the star itself. (although maybe there is room to make a change, if you want to also add a fork count and "view forks" page.

GitHub also works a bit differently (although more similar to how this MR is set up). It treats the star button on a repo page as a star/unstar button, and requires clicking a different spot on the page to view who starred the repo (in the sidebar where it shows license, star and fork count etc.). Tangled doesn't exactly have an analog for this sidebar menu (but maybe there is room for a starrers section below where commits/branches are shown? -- I don't think it would make sense to need to scroll down to see this info though).

As for StarsHref, I made those functions to get the URL for the template, but I may have missed something so I'll take another pass to see if I can pull in the repo name and DID in a simpler way. (The lack of tooling for HTML templates does make this a bit tricky to poke at, so its quite possible I just missed something)

Dug in a bit more, I think it is necessary to get the repo name in order to populate the star page URL on the button. Also, I had an idea for the star button that I'll try later -- move the star count to where I have the new icon, and show "star" or "unstar" next to the star icon (basically just what codeberg does). If it looks good, but weird next to the other buttons I'll make a separate PR to make a similar change to the other buttons.

Okay, I was wrong, we can just pass .RepoName into the template, which removes the need for the StarsHref pattern. Latest version is a bit simpler, using a query param instead.

This isn't necessarily better since we need to add RepoName in a few places in the code, so I can revert this change if you'd rather keep the functions around instead.

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.