Monorepo for Tangled tangled.org

appview/pages: show trending repos in the timeline

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

Changed files
+155 -10
appview
db
pages
templates
timeline
user
fragments
state
+4
appview/db/db.go
··· 470 id integer primary key autoincrement, 471 name text unique 472 ); 473 `) 474 if err != nil { 475 return nil, err
··· 470 id integer primary key autoincrement, 471 name text unique 472 ); 473 + 474 + -- indexes for better star query performance 475 + create index if not exists idx_stars_created on stars(created); 476 + create index if not exists idx_stars_repo_at_created on stars(repo_at, created); 477 `) 478 if err != nil { 479 return nil, err
+72 -3
appview/db/star.go
··· 47 // Get a star record 48 func GetStar(e Execer, starredByDid string, repoAt syntax.ATURI) (*Star, error) { 49 query := ` 50 - select starred_by_did, repo_at, created, rkey 51 from stars 52 where starred_by_did = ? and repo_at = ?` 53 row := e.QueryRow(query, starredByDid, repoAt) ··· 119 } 120 121 repoQuery := fmt.Sprintf( 122 - `select starred_by_did, repo_at, created, rkey 123 from stars 124 %s 125 order by created desc ··· 187 var stars []Star 188 189 rows, err := e.Query(` 190 - select 191 s.starred_by_did, 192 s.repo_at, 193 s.rkey, ··· 244 245 return stars, nil 246 }
··· 47 // Get a star record 48 func GetStar(e Execer, starredByDid string, repoAt syntax.ATURI) (*Star, error) { 49 query := ` 50 + select starred_by_did, repo_at, created, rkey 51 from stars 52 where starred_by_did = ? and repo_at = ?` 53 row := e.QueryRow(query, starredByDid, repoAt) ··· 119 } 120 121 repoQuery := fmt.Sprintf( 122 + `select starred_by_did, repo_at, created, rkey 123 from stars 124 %s 125 order by created desc ··· 187 var stars []Star 188 189 rows, err := e.Query(` 190 + select 191 s.starred_by_did, 192 s.repo_at, 193 s.rkey, ··· 244 245 return stars, nil 246 } 247 + 248 + // GetTopStarredReposLastWeek returns the top 8 most starred repositories from the last week 249 + func GetTopStarredReposLastWeek(e Execer) ([]Repo, error) { 250 + // first, get the top repo URIs by star count from the last week 251 + query := ` 252 + with recent_starred_repos as ( 253 + select distinct repo_at 254 + from stars 255 + where created >= datetime('now', '-7 days') 256 + ), 257 + repo_star_counts as ( 258 + select 259 + s.repo_at, 260 + count(*) as star_count 261 + from stars s 262 + join recent_starred_repos rsr on s.repo_at = rsr.repo_at 263 + group by s.repo_at 264 + ) 265 + select rsc.repo_at 266 + from repo_star_counts rsc 267 + order by rsc.star_count desc 268 + limit 8 269 + ` 270 + 271 + rows, err := e.Query(query) 272 + if err != nil { 273 + return nil, err 274 + } 275 + defer rows.Close() 276 + 277 + var repoUris []string 278 + for rows.Next() { 279 + var repoUri string 280 + err := rows.Scan(&repoUri) 281 + if err != nil { 282 + return nil, err 283 + } 284 + repoUris = append(repoUris, repoUri) 285 + } 286 + 287 + if err := rows.Err(); err != nil { 288 + return nil, err 289 + } 290 + 291 + if len(repoUris) == 0 { 292 + return []Repo{}, nil 293 + } 294 + 295 + // get full repo data 296 + repos, err := GetRepos(e, 0, FilterIn("at_uri", repoUris)) 297 + if err != nil { 298 + return nil, err 299 + } 300 + 301 + // sort repos by the original trending order 302 + repoMap := make(map[string]Repo) 303 + for _, repo := range repos { 304 + repoMap[repo.RepoAt().String()] = repo 305 + } 306 + 307 + orderedRepos := make([]Repo, 0, len(repoUris)) 308 + for _, uri := range repoUris { 309 + if repo, exists := repoMap[uri]; exists { 310 + orderedRepos = append(orderedRepos, repo) 311 + } 312 + } 313 + 314 + return orderedRepos, nil 315 + }
+10 -1
appview/pages/pages.go
··· 302 } 303 304 func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 305 - return p.execute("timeline", w, params) 306 } 307 308 type SettingsParams struct {
··· 302 } 303 304 func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 305 + return p.execute("timeline/timeline", w, params) 306 + } 307 + 308 + type TopStarredReposLastWeekParams struct { 309 + LoggedInUser *oauth.User 310 + Repos []db.Repo 311 + } 312 + 313 + func (p *Pages) TopStarredReposLastWeek(w io.Writer, params TopStarredReposLastWeekParams) error { 314 + return p.executePlain("timeline/fragments/topStarredRepos", w, params) 315 } 316 317 type SettingsParams struct {
+28 -4
appview/pages/templates/timeline.html appview/pages/templates/timeline/timeline.html
··· 4 <meta property="og:title" content="timeline · tangled" /> 5 <meta property="og:type" content="object" /> 6 <meta property="og:url" content="https://tangled.sh" /> 7 - <meta property="og:description" content="see what's tangling" /> 8 {{ end }} 9 10 - {{ define "topbar" }} 11 - {{ template "layouts/topbar" $ }} 12 - {{ end }} 13 14 {{ define "content" }} 15 {{ with .LoggedInUser }} 16 {{ block "timeline" $ }}{{ end }} 17 {{ else }} 18 {{ block "hero" $ }}{{ end }} 19 {{ block "timeline" $ }}{{ end }} 20 {{ end }} 21 {{ end }} 22 23 {{ define "hero" }}
··· 4 <meta property="og:title" content="timeline · tangled" /> 5 <meta property="og:type" content="object" /> 6 <meta property="og:url" content="https://tangled.sh" /> 7 + <meta property="og:description" content="tightly-knit social coding" /> 8 {{ end }} 9 10 + 11 + 12 13 {{ define "content" }} 14 {{ with .LoggedInUser }} 15 + {{ block "trending" . }}{{ end }} 16 {{ block "timeline" $ }}{{ end }} 17 {{ else }} 18 {{ block "hero" $ }}{{ end }} 19 + {{ block "trending" . }}{{ end }} 20 {{ block "timeline" $ }}{{ end }} 21 {{ end }} 22 + {{ end }} 23 + 24 + {{ define "trending" }} 25 + 26 + <div 27 + hx-get="/timeline/trending" 28 + hx-trigger="load" 29 + hx-indicator="#starred-loading" 30 + > 31 + <!-- Loading spinner --> 32 + <div id="starred-loading" class="htmx-indicator"> 33 + <div class="p-6"> 34 + <h3 class="font-bold text-xl text-gray-900 dark:text-white flex items-center gap-2 mb-3"> 35 + Trending 36 + {{ i "trending-up" "size-4 flex-shrink-0" }} 37 + </h3> 38 + </div> 39 + <div class="flex items-center justify-center py-8"> 40 + <div class="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900 dark:border-white"></div> 41 + <span class="ml-2 text-sm text-gray-500 dark:text-gray-400">Loading...</span> 42 + </div> 43 + </div> 44 + </div> 45 {{ end }} 46 47 {{ define "hero" }}
+25
appview/pages/templates/timeline/fragments/topStarredRepos.html
···
··· 1 + {{ define "timeline/fragments/topStarredRepos" }} 2 + <div class="w-full md:mx-0"> 3 + <div class="p-6"> 4 + <h3 class="font-bold text-xl text-gray-900 dark:text-white flex items-center gap-2 mb-3"> 5 + Trending 6 + {{ i "trending-up" "size-4 flex-shrink-0" }} 7 + </h3> 8 + </div> 9 + <div class="flex gap-3 overflow-x-auto pb-4 px-4 scrollbar-hide"> 10 + {{ range $index, $repo := .Repos }} 11 + <div class="flex-none w-72 relative h-full"> 12 + <div class="relative h-full items-stretch border border-gray-200 dark:border-gray-700 rounded-sm"> 13 + {{ template "user/fragments/repoCard" (list $ $repo true) }} 14 + </div> 15 + </div> 16 + {{ else }} 17 + <div class="py-8 px-6 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-sm"> 18 + <div class="text-sm text-gray-500 dark:text-gray-400 text-center"> 19 + No trending repositories this week 20 + </div> 21 + </div> 22 + {{ end }} 23 + </div> 24 + </div> 25 + {{ end }}
+2 -2
appview/pages/templates/user/fragments/repoCard.html
··· 14 15 {{ $repoOwner := resolve .Did }} 16 {{- if $fullName -}} 17 - <a href="/{{ $repoOwner }}/{{ .Name }}">{{ $repoOwner }}/{{ .Name }}</a> 18 {{- else -}} 19 - <a href="/{{ $repoOwner }}/{{ .Name }}">{{ .Name }}</a> 20 {{- end -}} 21 </div> 22 {{ with .Description }}
··· 14 15 {{ $repoOwner := resolve .Did }} 16 {{- if $fullName -}} 17 + <a href="/{{ $repoOwner }}/{{ .Name }}" class="truncate">{{ $repoOwner }}/{{ .Name }}</a> 18 {{- else -}} 19 + <a href="/{{ $repoOwner }}/{{ .Name }}" class="truncate">{{ .Name }}</a> 20 {{- end -}} 21 </div> 22 {{ with .Description }}
+1
appview/state/router.go
··· 112 r.Handle("/static/*", s.pages.Static()) 113 114 r.Get("/", s.Timeline) 115 116 r.Route("/repo", func(r chi.Router) { 117 r.Route("/new", func(r chi.Router) {
··· 112 r.Handle("/static/*", s.pages.Static()) 113 114 r.Get("/", s.Timeline) 115 + r.Get("/timeline/trending", s.TopStarredReposLastWeek) 116 117 r.Route("/repo", func(r chi.Router) { 118 r.Route("/new", func(r chi.Router) {
+13
appview/state/state.go
··· 199 }) 200 } 201 202 func (s *State) Keys(w http.ResponseWriter, r *http.Request) { 203 user := chi.URLParam(r, "user") 204 user = strings.TrimPrefix(user, "@")
··· 199 }) 200 } 201 202 + func (s *State) TopStarredReposLastWeek(w http.ResponseWriter, r *http.Request) { 203 + repos, err := db.GetTopStarredReposLastWeek(s.db) 204 + if err != nil { 205 + log.Println(err) 206 + s.pages.Notice(w, "topstarredrepos", "Unable to load.") 207 + return 208 + } 209 + 210 + s.pages.TopStarredReposLastWeek(w, pages.TopStarredReposLastWeekParams{ 211 + Repos: repos, 212 + }) 213 + } 214 + 215 func (s *State) Keys(w http.ResponseWriter, r *http.Request) { 216 user := chi.URLParam(r, "user") 217 user = strings.TrimPrefix(user, "@")