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
+157 -226
Diff #1
+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)
+3 -38
appview/oauth/handler.go
··· 6 "encoding/json" 7 "errors" 8 "fmt" 9 - "log/slog" 10 "net/http" 11 "slices" 12 "time" ··· 131 } 132 133 l.Debug("adding to default spindle") 134 - session, err := o.getAppPasswordSession() 135 if err != nil { 136 l.Error("failed to create session", "err", err) 137 return ··· 145 } 146 147 if err := session.putRecord(record, tangled.SpindleMemberNSID); err != nil { 148 - o.invalidateAppPasswordSession() 149 l.Error("failed to add to default spindle", "err", err) 150 return 151 } ··· 171 } 172 173 l.Debug("adding to default knot") 174 - session, err := o.getAppPasswordSession() 175 if err != nil { 176 l.Error("failed to create session", "err", err) 177 return ··· 185 } 186 187 if err := session.putRecord(record, tangled.KnotMemberNSID); err != nil { 188 - o.invalidateAppPasswordSession() 189 l.Error("failed to add to default knot", "err", err) 190 return 191 } ··· 251 PdsEndpoint string 252 Did string 253 RateLimitBypass string 254 - Logger *slog.Logger 255 } 256 257 - func CreateAppPasswordSession(res *idresolver.Resolver, appPassword, did, rateLimitBypass string, logger *slog.Logger) (*AppPasswordSession, error) { 258 if appPassword == "" { 259 return nil, fmt.Errorf("no app password configured") 260 } ··· 288 sessionReq.Header.Set("x-ratelimit-bypass", rateLimitBypass) 289 } 290 291 - logger.Debug("creating app password session", "url", sessionURL, "headers", sessionReq.Header) 292 - 293 client := &http.Client{Timeout: 30 * time.Second} 294 sessionResp, err := client.Do(sessionReq) 295 if err != nil { ··· 309 session.PdsEndpoint = pdsEndpoint 310 session.Did = did 311 session.RateLimitBypass = rateLimitBypass 312 - session.Logger = logger 313 314 return &session, nil 315 } ··· 344 req.Header.Set("x-ratelimit-bypass", s.RateLimitBypass) 345 } 346 347 - s.Logger.Debug("putting record", "url", url, "collection", collection, "headers", req.Header) 348 - 349 client := &http.Client{Timeout: 30 * time.Second} 350 resp, err := client.Do(req) 351 if err != nil { ··· 359 360 return nil 361 } 362 - 363 - // getAppPasswordSession returns a cached AppPasswordSession, creating one if needed. 364 - func (o *OAuth) getAppPasswordSession() (*AppPasswordSession, error) { 365 - o.appPasswordSessionMu.Lock() 366 - defer o.appPasswordSessionMu.Unlock() 367 - 368 - if o.appPasswordSession != nil { 369 - return o.appPasswordSession, nil 370 - } 371 - 372 - session, err := CreateAppPasswordSession(o.IdResolver, o.Config.Core.AppPassword, consts.TangledDid, o.Config.Core.RateLimitBypass, o.Logger) 373 - if err != nil { 374 - return nil, err 375 - } 376 - 377 - o.appPasswordSession = session 378 - return session, nil 379 - } 380 - 381 - // invalidateAppPasswordSession clears the cached session so the next call to 382 - // getAppPasswordSession will create a fresh one. 383 - func (o *OAuth) invalidateAppPasswordSession() { 384 - o.appPasswordSessionMu.Lock() 385 - defer o.appPasswordSessionMu.Unlock() 386 - o.appPasswordSession = nil 387 - }
··· 6 "encoding/json" 7 "errors" 8 "fmt" 9 "net/http" 10 "slices" 11 "time" ··· 130 } 131 132 l.Debug("adding to default spindle") 133 + session, err := CreateAppPasswordSession(o.IdResolver, o.Config.Core.AppPassword, consts.TangledDid, o.Config.Core.RateLimitBypass) 134 if err != nil { 135 l.Error("failed to create session", "err", err) 136 return ··· 144 } 145 146 if err := session.putRecord(record, tangled.SpindleMemberNSID); err != nil { 147 l.Error("failed to add to default spindle", "err", err) 148 return 149 } ··· 169 } 170 171 l.Debug("adding to default knot") 172 + session, err := CreateAppPasswordSession(o.IdResolver, o.Config.Core.AppPassword, consts.TangledDid, o.Config.Core.RateLimitBypass) 173 if err != nil { 174 l.Error("failed to create session", "err", err) 175 return ··· 183 } 184 185 if err := session.putRecord(record, tangled.KnotMemberNSID); err != nil { 186 l.Error("failed to add to default knot", "err", err) 187 return 188 } ··· 248 PdsEndpoint string 249 Did string 250 RateLimitBypass string 251 } 252 253 + func CreateAppPasswordSession(res *idresolver.Resolver, appPassword, did, rateLimitBypass string) (*AppPasswordSession, error) { 254 if appPassword == "" { 255 return nil, fmt.Errorf("no app password configured") 256 } ··· 284 sessionReq.Header.Set("x-ratelimit-bypass", rateLimitBypass) 285 } 286 287 client := &http.Client{Timeout: 30 * time.Second} 288 sessionResp, err := client.Do(sessionReq) 289 if err != nil { ··· 303 session.PdsEndpoint = pdsEndpoint 304 session.Did = did 305 session.RateLimitBypass = rateLimitBypass 306 307 return &session, nil 308 } ··· 337 req.Header.Set("x-ratelimit-bypass", s.RateLimitBypass) 338 } 339 340 client := &http.Client{Timeout: 30 * time.Second} 341 resp, err := client.Do(req) 342 if err != nil { ··· 350 351 return nil 352 }
-4
appview/oauth/oauth.go
··· 5 "fmt" 6 "log/slog" 7 "net/http" 8 - "sync" 9 "time" 10 11 comatproto "github.com/bluesky-social/indigo/api/atproto" ··· 34 Enforcer *rbac.Enforcer 35 IdResolver *idresolver.Resolver 36 Logger *slog.Logger 37 - 38 - appPasswordSession *AppPasswordSession 39 - appPasswordSessionMu sync.Mutex 40 } 41 42 func New(config *config.Config, ph posthog.Client, db *db.DB, enforcer *rbac.Enforcer, res *idresolver.Resolver, logger *slog.Logger) (*OAuth, error) {
··· 5 "fmt" 6 "log/slog" 7 "net/http" 8 "time" 9 10 comatproto "github.com/bluesky-social/indigo/api/atproto" ··· 33 Enforcer *rbac.Enforcer 34 IdResolver *idresolver.Resolver 35 Logger *slog.Logger 36 } 37 38 func New(config *config.Config, ph posthog.Client, db *db.DB, enforcer *rbac.Enforcer, res *idresolver.Resolver, logger *slog.Logger) (*OAuth, error) {
-5
appview/pages/markup/markdown.go
··· 22 "github.com/yuin/goldmark/text" 23 "github.com/yuin/goldmark/util" 24 callout "gitlab.com/staticnoise/goldmark-callout" 25 - "go.abhg.dev/goldmark/mermaid" 26 htmlparse "golang.org/x/net/html" 27 28 "tangled.org/core/api/tangled" ··· 57 md := goldmark.New( 58 goldmark.WithExtensions( 59 extension.GFM, 60 - &mermaid.Extender{ 61 - RenderMode: mermaid.RenderModeClient, 62 - NoScript: true, 63 - }, 64 highlighting.NewHighlighting( 65 highlighting.WithFormatOptions( 66 chromahtml.Standalone(false),
··· 22 "github.com/yuin/goldmark/text" 23 "github.com/yuin/goldmark/util" 24 callout "gitlab.com/staticnoise/goldmark-callout" 25 htmlparse "golang.org/x/net/html" 26 27 "tangled.org/core/api/tangled" ··· 56 md := goldmark.New( 57 goldmark.WithExtensions( 58 extension.GFM, 59 highlighting.NewHighlighting( 60 highlighting.WithFormatOptions( 61 chromahtml.Standalone(false),
-46
appview/pages/markup/markdown_test.go
··· 2 3 import ( 4 "bytes" 5 - "strings" 6 "testing" 7 ) 8 - 9 - func TestMermaidExtension(t *testing.T) { 10 - tests := []struct { 11 - name string 12 - markdown string 13 - contains string 14 - notContains string 15 - }{ 16 - { 17 - name: "mermaid block produces pre.mermaid", 18 - markdown: "```mermaid\ngraph TD\n A-->B\n```", 19 - contains: `<pre class="mermaid">`, 20 - notContains: `<code class="language-mermaid"`, 21 - }, 22 - { 23 - name: "mermaid block contains diagram source", 24 - markdown: "```mermaid\ngraph TD\n A-->B\n```", 25 - contains: "graph TD", 26 - }, 27 - { 28 - name: "non-mermaid code block is not affected", 29 - markdown: "```go\nfunc main() {}\n```", 30 - contains: `<pre class="chroma">`, 31 - }, 32 - } 33 - 34 - for _, tt := range tests { 35 - t.Run(tt.name, func(t *testing.T) { 36 - md := NewMarkdown("tangled.org") 37 - 38 - var buf bytes.Buffer 39 - if err := md.Convert([]byte(tt.markdown), &buf); err != nil { 40 - t.Fatalf("failed to convert markdown: %v", err) 41 - } 42 - 43 - result := buf.String() 44 - if !strings.Contains(result, tt.contains) { 45 - t.Errorf("expected output to contain:\n%s\ngot:\n%s", tt.contains, result) 46 - } 47 - if tt.notContains != "" && strings.Contains(result, tt.notContains) { 48 - t.Errorf("expected output NOT to contain:\n%s\ngot:\n%s", tt.notContains, result) 49 - } 50 - }) 51 - } 52 - } 53 54 func TestAtExtension_Rendering(t *testing.T) { 55 tests := []struct {
··· 2 3 import ( 4 "bytes" 5 "testing" 6 ) 7 8 func TestAtExtension_Rendering(t *testing.T) { 9 tests := []struct {
+1 -1
appview/pages/markup/sanitizer.go
··· 72 policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input") 73 74 // for code blocks 75 - policy.AllowAttrs("class").Matching(regexp.MustCompile(`chroma|mermaid`)).OnElements("pre") 76 policy.AllowAttrs("class").Matching(regexp.MustCompile(`anchor|footnote-ref|footnote-backref`)).OnElements("a") 77 policy.AllowAttrs("class").Matching(regexp.MustCompile(`heading`)).OnElements("h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8") 78 policy.AllowAttrs("class").Matching(regexp.MustCompile(strings.Join(slices.Collect(maps.Values(chroma.StandardTypes)), "|"))).OnElements("span")
··· 72 policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input") 73 74 // for code blocks 75 + policy.AllowAttrs("class").Matching(regexp.MustCompile(`chroma`)).OnElements("pre") 76 policy.AllowAttrs("class").Matching(regexp.MustCompile(`anchor|footnote-ref|footnote-backref`)).OnElements("a") 77 policy.AllowAttrs("class").Matching(regexp.MustCompile(`heading`)).OnElements("h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8") 78 policy.AllowAttrs("class").Matching(regexp.MustCompile(strings.Join(slices.Collect(maps.Values(chroma.StandardTypes)), "|"))).OnElements("span")
+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 }}
-17
appview/pages/templates/layouts/base.html
··· 39 <link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin /> 40 41 <link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" /> 42 - 43 - <script> 44 - document.addEventListener('DOMContentLoaded', () => { 45 - const nodes = document.querySelectorAll('pre.mermaid'); 46 - if (!nodes.length) return; 47 - const script = document.createElement('script'); 48 - script.src = '/static/mermaid.min.js'; 49 - script.onload = async () => { 50 - mermaid.initialize({ 51 - startOnLoad: true, 52 - theme: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default', 53 - }); 54 - await mermaid.run({ nodes }); 55 - }; 56 - document.head.appendChild(script); 57 - }); 58 - </script> 59 <title>{{ block "title" . }}{{ end }}</title> 60 {{ block "extrameta" . }}{{ end }} 61 </head>
··· 39 <link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin /> 40 41 <link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" /> 42 <title>{{ block "title" . }}{{ end }}</title> 43 {{ block "extrameta" . }}{{ end }} 44 </head>
+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"
-46
appview/pages/templates/repo/fragments/diff.html
··· 11 {{ template "diffTopbar" . }} 12 {{ block "diffLayout" . }} {{ end }} 13 {{ template "fragments/resizable" }} 14 - {{ template "activeFileHighlight" }} 15 {{ end }} 16 17 {{ define "diffTopbar" }} ··· 230 }); 231 </script> 232 {{ end }} 233 - 234 - {{ define "activeFileHighlight" }} 235 - <script> 236 - document.addEventListener('DOMContentLoaded', function() { 237 - const diffFiles = document.querySelectorAll('details[id^="file-"]'); 238 - const filetreeLinks = document.querySelectorAll('.filetree-link'); 239 - if (diffFiles.length === 0 || filetreeLinks.length === 0) return; 240 - 241 - const linkMap = new Map(); 242 - filetreeLinks.forEach(link => { 243 - const path = link.getAttribute('data-path'); 244 - if (path) linkMap.set('file-' + path, link); 245 - }); 246 - 247 - let currentActive = null; 248 - function setActive(link) { 249 - if (link && link !== currentActive) { 250 - if (currentActive) currentActive.classList.remove('font-bold'); 251 - link.classList.add('font-bold'); 252 - currentActive = link; 253 - } 254 - } 255 - 256 - filetreeLinks.forEach(link => { 257 - link.addEventListener('click', () => setActive(link)); 258 - }); 259 - 260 - const topbar = document.querySelector('.sticky.top-0.z-30'); 261 - const headerHeight = topbar ? topbar.offsetHeight : 0; 262 - 263 - function updateActiveFile() { 264 - for (const file of diffFiles) { 265 - const rect = file.getBoundingClientRect(); 266 - if (rect.top <= headerHeight && rect.bottom > headerHeight) { 267 - setActive(linkMap.get(file.id)); 268 - return; 269 - } 270 - } 271 - } 272 - 273 - document.addEventListener('scroll', updateActiveFile); 274 - updateActiveFile(); 275 - }); 276 - </script> 277 - {{ end }}
··· 11 {{ template "diffTopbar" . }} 12 {{ block "diffLayout" . }} {{ end }} 13 {{ template "fragments/resizable" }} 14 {{ end }} 15 16 {{ define "diffTopbar" }} ··· 229 }); 230 </script> 231 {{ end }}
+1 -1
appview/pages/templates/repo/fragments/fileTree.html
··· 16 {{ else if .Name }} 17 <div class="tree-file flex items-center gap-2 pt-1"> 18 {{ i "file" "flex-shrink-0 size-4" }} 19 - <a href="#file-{{ .Path }}" data-path="{{ .Path }}" class="filetree-link filename truncate text-black dark:text-white no-underline hover:underline">{{ .Name }}</a> 20 </div> 21 {{ else }} 22 {{ range $child := .Children }}
··· 16 {{ else if .Name }} 17 <div class="tree-file flex items-center gap-2 pt-1"> 18 {{ i "file" "flex-shrink-0 size-4" }} 19 + <a href="#file-{{ .Path }}" class="filename truncate text-black dark:text-white no-underline hover:underline">{{ .Name }}</a> 20 </div> 21 {{ else }} 22 {{ range $child := .Children }}
+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 + }
+1 -1
appview/state/state.go
··· 622 return 623 } 624 625 - session, err := oauth.CreateAppPasswordSession(res, config.Core.AppPassword, consts.TangledDid, config.Core.RateLimitBypass, logger) 626 if err != nil { 627 logger.Error("failed to create appassword session... skipping fetch", "err", err) 628 return
··· 622 return 623 } 624 625 + session, err := oauth.CreateAppPasswordSession(res, config.Core.AppPassword, consts.TangledDid, config.Core.RateLimitBypass) 626 if err != nil { 627 logger.Error("failed to create appassword session... skipping fetch", "err", err) 628 return
-13
flake.lock
··· 148 "url": "https://github.com/lucide-icons/lucide/releases/download/0.536.0/lucide-icons-0.536.0.zip" 149 } 150 }, 151 - "mermaid-src": { 152 - "flake": false, 153 - "locked": { 154 - "narHash": "sha256-/YOdECG2V5c3kJ1QfGvhziTT6K/Dx/4mOk2mr3Fs/do=", 155 - "type": "file", 156 - "url": "https://cdn.jsdelivr.net/npm/mermaid@11.12.3/dist/mermaid.min.js" 157 - }, 158 - "original": { 159 - "type": "file", 160 - "url": "https://cdn.jsdelivr.net/npm/mermaid@11.12.3/dist/mermaid.min.js" 161 - } 162 - }, 163 "nixpkgs": { 164 "locked": { 165 "lastModified": 1766070988, ··· 187 "indigo": "indigo", 188 "inter-fonts-src": "inter-fonts-src", 189 "lucide-src": "lucide-src", 190 - "mermaid-src": "mermaid-src", 191 "nixpkgs": "nixpkgs", 192 "sqlite-lib-src": "sqlite-lib-src" 193 }
··· 148 "url": "https://github.com/lucide-icons/lucide/releases/download/0.536.0/lucide-icons-0.536.0.zip" 149 } 150 }, 151 "nixpkgs": { 152 "locked": { 153 "lastModified": 1766070988, ··· 175 "indigo": "indigo", 176 "inter-fonts-src": "inter-fonts-src", 177 "lucide-src": "lucide-src", 178 "nixpkgs": "nixpkgs", 179 "sqlite-lib-src": "sqlite-lib-src" 180 }
+1 -7
flake.nix
··· 37 url = "git+https://tangled.org/@jakelazaroff.com/actor-typeahead"; 38 flake = false; 39 }; 40 - mermaid-src = { 41 - url = "https://cdn.jsdelivr.net/npm/mermaid@11.12.3/dist/mermaid.min.js"; 42 - flake = false; 43 - }; 44 ibm-plex-mono-src = { 45 url = "https://github.com/IBM/plex/releases/download/%40ibm%2Fplex-mono%401.1.0/ibm-plex-mono.zip"; 46 flake = false; ··· 63 sqlite-lib-src, 64 ibm-plex-mono-src, 65 actor-typeahead-src, 66 - mermaid-src, 67 ... 68 }: let 69 supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin"]; ··· 90 lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;}; 91 goat = self.callPackage ./nix/pkgs/goat.nix {inherit indigo;}; 92 appview-static-files = self.callPackage ./nix/pkgs/appview-static-files.nix { 93 - inherit htmx-src htmx-ws-src lucide-src inter-fonts-src ibm-plex-mono-src actor-typeahead-src mermaid-src; 94 }; 95 appview = self.callPackage ./nix/pkgs/appview.nix {}; 96 docs = self.callPackage ./nix/pkgs/docs.nix { ··· 223 type = "app"; 224 program = toString (pkgs.writeShellScript "watch-appview" '' 225 echo "copying static files to appview/pages/static..." 226 - mkdir -p appview/pages/static 227 ${pkgs.coreutils}/bin/cp -fr --no-preserve=ownership ${packages'.appview-static-files}/* appview/pages/static 228 ${air-watcher "appview" ""}/bin/run 229 '');
··· 37 url = "git+https://tangled.org/@jakelazaroff.com/actor-typeahead"; 38 flake = false; 39 }; 40 ibm-plex-mono-src = { 41 url = "https://github.com/IBM/plex/releases/download/%40ibm%2Fplex-mono%401.1.0/ibm-plex-mono.zip"; 42 flake = false; ··· 59 sqlite-lib-src, 60 ibm-plex-mono-src, 61 actor-typeahead-src, 62 ... 63 }: let 64 supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin"]; ··· 85 lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;}; 86 goat = self.callPackage ./nix/pkgs/goat.nix {inherit indigo;}; 87 appview-static-files = self.callPackage ./nix/pkgs/appview-static-files.nix { 88 + inherit htmx-src htmx-ws-src lucide-src inter-fonts-src ibm-plex-mono-src actor-typeahead-src; 89 }; 90 appview = self.callPackage ./nix/pkgs/appview.nix {}; 91 docs = self.callPackage ./nix/pkgs/docs.nix { ··· 218 type = "app"; 219 program = toString (pkgs.writeShellScript "watch-appview" '' 220 echo "copying static files to appview/pages/static..." 221 ${pkgs.coreutils}/bin/cp -fr --no-preserve=ownership ${packages'.appview-static-files}/* appview/pages/static 222 ${air-watcher "appview" ""}/bin/run 223 '');
-1
go.mod
··· 48 github.com/yuin/goldmark-emoji v1.0.6 49 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc 50 gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab 51 - go.abhg.dev/goldmark/mermaid v0.6.0 52 golang.org/x/crypto v0.40.0 53 golang.org/x/image v0.31.0 54 golang.org/x/net v0.42.0
··· 48 github.com/yuin/goldmark-emoji v1.0.6 49 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc 50 gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab 51 golang.org/x/crypto v0.40.0 52 golang.org/x/image v0.31.0 53 golang.org/x/net v0.42.0
-16
go.sum
··· 105 github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 106 github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 107 github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 108 - github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU= 109 - github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= 110 - github.com/chromedp/chromedp v0.14.0 h1:/xE5m6wEBwivhalHwlCOyYfBcAJNwg4nLw96QiCfYr0= 111 - github.com/chromedp/chromedp v0.14.0/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo= 112 - github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= 113 - github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= 114 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 115 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 116 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= ··· 181 github.com/go-git/go-git-fixtures/v5 v5.0.0-20241203230421-0753e18f8f03/go.mod h1:hMKrMnUE4W0SJ7bFyM00dyz/HoknZoptGWzrj6M+dEM= 182 github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= 183 github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= 184 - github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs= 185 - github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= 186 github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 187 github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 188 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= ··· 197 github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= 198 github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 199 github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 200 - github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 201 - github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 202 - github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 203 - github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 204 - github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= 205 - github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= 206 github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 207 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 208 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= ··· 530 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 531 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= 532 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 533 - go.abhg.dev/goldmark/mermaid v0.6.0 h1:VvkYFWuOjD6cmSBVJpLAtzpVCGM1h0B7/DQ9IzERwzY= 534 - go.abhg.dev/goldmark/mermaid v0.6.0/go.mod h1:uMc+PcnIH2NVL7zjH10Q1wr7hL3+4n4jUMifhyBYB9I= 535 go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= 536 go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= 537 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
··· 105 github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 106 github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 107 github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 108 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 109 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 110 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= ··· 175 github.com/go-git/go-git-fixtures/v5 v5.0.0-20241203230421-0753e18f8f03/go.mod h1:hMKrMnUE4W0SJ7bFyM00dyz/HoknZoptGWzrj6M+dEM= 176 github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= 177 github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= 178 github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 179 github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 180 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= ··· 189 github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= 190 github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 191 github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 192 github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 193 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 194 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= ··· 516 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 517 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= 518 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 519 go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= 520 go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= 521 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-5
input.css
··· 215 @apply disabled:accent-blue-500 checked:accent-blue-500 disabled:checked:accent-blue-500; 216 } 217 218 - /* Mermaid diagrams */ 219 - .prose pre.mermaid { 220 - @apply flex justify-center my-4 overflow-x-auto bg-transparent border-0; 221 - } 222 - 223 /* Base callout */ 224 details[data-callout] { 225 @apply border-l-4 pl-3 py-2 text-gray-800 dark:text-gray-200 my-4;
··· 215 @apply disabled:accent-blue-500 checked:accent-blue-500 disabled:checked:accent-blue-500; 216 } 217 218 /* Base callout */ 219 details[data-callout] { 220 @apply border-l-4 pl-3 py-2 text-gray-800 dark:text-gray-200 my-4;
-3
nix/gomod2nix.toml
··· 545 [mod."gitlab.com/yawning/tuplehash"] 546 version = "v0.0.0-20230713102510-df83abbf9a02" 547 hash = "sha256-pehQduoaJRLchebhgvMYacVvbuNIBA++XkiqCuqdato=" 548 - [mod."go.abhg.dev/goldmark/mermaid"] 549 - version = "v0.6.0" 550 - hash = "sha256-JmjaCfzJU/M/R0TnXSzNwBaHmoLLooiXwQJeVRbZ3AQ=" 551 [mod."go.etcd.io/bbolt"] 552 version = "v1.4.0" 553 hash = "sha256-nR/YGQjwz6ue99IFbgw/01Pl8PhoOjpKiwVy5sJxlps="
··· 545 [mod."gitlab.com/yawning/tuplehash"] 546 version = "v0.0.0-20230713102510-df83abbf9a02" 547 hash = "sha256-pehQduoaJRLchebhgvMYacVvbuNIBA++XkiqCuqdato=" 548 [mod."go.etcd.io/bbolt"] 549 version = "v1.4.0" 550 hash = "sha256-nR/YGQjwz6ue99IFbgw/01Pl8PhoOjpKiwVy5sJxlps="
-2
nix/pkgs/appview-static-files.nix
··· 6 inter-fonts-src, 7 ibm-plex-mono-src, 8 actor-typeahead-src, 9 - mermaid-src, 10 sqlite-lib, 11 tailwindcss, 12 dolly, ··· 22 mkdir -p $out/{fonts,icons,logos} && cd $out 23 cp -f ${htmx-src} htmx.min.js 24 cp -f ${htmx-ws-src} htmx-ext-ws.min.js 25 - cp -f ${mermaid-src} mermaid.min.js 26 cp -rf ${lucide-src}/*.svg icons/ 27 cp -f ${inter-fonts-src}/web/InterVariable*.woff2 fonts/ 28 cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 fonts/
··· 6 inter-fonts-src, 7 ibm-plex-mono-src, 8 actor-typeahead-src, 9 sqlite-lib, 10 tailwindcss, 11 dolly, ··· 21 mkdir -p $out/{fonts,icons,logos} && cd $out 22 cp -f ${htmx-src} htmx.min.js 23 cp -f ${htmx-ws-src} htmx-ext-ws.min.js 24 cp -rf ${lucide-src}/*.svg icons/ 25 cp -f ${inter-fonts-src}/web/InterVariable*.woff2 fonts/ 26 cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 fonts/

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.