this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(web): additional filters

filter by forks/no forks/any
make language a dropdown (bad ux, but less typing)

+49 -10
+26 -7
store/store.go
··· 437 437 return out, nil 438 438 } 439 439 440 - // RecentRepos returns repos created since `since`, optionally filtered by 441 - // primary language. Pass limit <= 0 for no limit. Uses raw SQL for the 442 - // scalar star-count subquery and handle JOIN — both awkward in ent. 443 - func (s *Store) RecentRepos(ctx context.Context, language string, since time.Time, limit int) ([]Repo, error) { 440 + // ReposFilter narrows the result of RecentRepos. Zero values mean "no 441 + // filter" so callers can mix and match. 442 + type ReposFilter struct { 443 + Language string // primary language match (post-filter in Go over JSON map) 444 + Since time.Time // repos created at or after this time (zero = no time floor) 445 + ForksOnly bool // only rows with non-empty source 446 + NoForks bool // exclude rows with non-empty source 447 + } 448 + 449 + // RecentRepos returns repos matching the filter, sorted newest first. Pass 450 + // limit <= 0 for no limit. Uses raw SQL because the scalar star-count 451 + // subquery and handle LEFT JOIN are awkward to express via ent's query API. 452 + func (s *Store) RecentRepos(ctx context.Context, f ReposFilter, limit int) ([]Repo, error) { 444 453 candidateLimit := -1 445 454 if limit > 0 { 446 455 candidateLimit = limit 447 - if language != "" { 456 + if f.Language != "" { 448 457 candidateLimit = limit * 5 449 458 } 450 459 } 460 + forksOnly := 0 461 + if f.ForksOnly { 462 + forksOnly = 1 463 + } 464 + noForks := 0 465 + if f.NoForks { 466 + noForks = 1 467 + } 451 468 rows, err := s.db.QueryContext(ctx, ` 452 469 SELECT r.at_uri, r.did, r.rkey, r.name, r.knot, r.description, r.topics, 453 470 r.website, r.source, r.spindle, r.repo_did, r.created_at, r.seen_at, ··· 457 474 LEFT JOIN handles h ON h.did = r.did 458 475 WHERE r.created_at >= ? 459 476 AND (? = '' OR r.languages IS NOT NULL) 477 + AND (? = 0 OR (r.source IS NOT NULL AND r.source != '')) 478 + AND (? = 0 OR r.source IS NULL OR r.source = '') 460 479 ORDER BY r.created_at DESC 461 480 LIMIT ? 462 - `, since.UnixMilli(), language, candidateLimit) 481 + `, f.Since.UnixMilli(), f.Language, forksOnly, noForks, candidateLimit) 463 482 if err != nil { 464 483 return nil, err 465 484 } ··· 471 490 if err != nil { 472 491 return nil, err 473 492 } 474 - if language != "" && r.Primary() != language { 493 + if f.Language != "" && r.Primary() != f.Language { 475 494 continue 476 495 } 477 496 out = append(out, r)
+13 -2
web/server.go
··· 30 30 Language string 31 31 SinceRaw string // empty = no time filter 32 32 Threshold time.Time // zero = no time filter 33 + Forks string // "" or "only" 33 34 } 34 35 35 36 func parseQuery(r *http.Request) query { 36 37 q := query{ 37 38 Language: r.URL.Query().Get("language"), 38 39 SinceRaw: r.URL.Query().Get("since"), 40 + Forks: r.URL.Query().Get("forks"), 39 41 } 40 42 if q.SinceRaw != "" { 41 43 if d, err := time.ParseDuration(q.SinceRaw); err == nil { ··· 45 47 return q 46 48 } 47 49 50 + func (q query) filter() store.ReposFilter { 51 + return store.ReposFilter{ 52 + Language: q.Language, 53 + Since: q.Threshold, 54 + ForksOnly: q.Forks == "only", 55 + NoForks: q.Forks == "no", 56 + } 57 + } 58 + 48 59 func reposJSON(s *store.Store) http.HandlerFunc { 49 60 return func(w http.ResponseWriter, r *http.Request) { 50 61 q := parseQuery(r) 51 - repos, err := s.RecentRepos(r.Context(), q.Language, q.Threshold, 0) 62 + repos, err := s.RecentRepos(r.Context(), q.filter(), 0) 52 63 if err != nil { 53 64 http.Error(w, err.Error(), http.StatusInternalServerError) 54 65 return ··· 61 72 func reposHTML(s *store.Store) http.HandlerFunc { 62 73 return func(w http.ResponseWriter, r *http.Request) { 63 74 q := parseQuery(r) 64 - repos, err := s.RecentRepos(r.Context(), q.Language, q.Threshold, 0) 75 + repos, err := s.RecentRepos(r.Context(), q.filter(), 0) 65 76 if err != nil { 66 77 http.Error(w, err.Error(), http.StatusInternalServerError) 67 78 return
+1
web/templates/layout.html
··· 22 22 td.handle { font-weight: 600; } 23 23 td.handle a, td.knot a, .link a { color: inherit; text-decoration: none; border-bottom: 1px dotted currentColor; } 24 24 td.handle a:hover, td.knot a:hover, .link a:hover { border-bottom-style: solid; } 25 + .fork { color: #888; cursor: help; margin-left: .25em; } 25 26 td.knot, td.did { color: #888; } 26 27 td.lang { color: #6a6; } 27 28 td.num { text-align: right; font-variant-numeric: tabular-nums; color: #888; }
+9 -1
web/templates/repos.html
··· 9 9 </select> 10 10 </label> 11 11 <label>since<input name="since" value="{{.Query.SinceRaw}}" placeholder="e.g. 24h"></label> 12 + <label>forks 13 + <select name="forks"> 14 + <option value=""{{if eq .Query.Forks ""}} selected{{end}}>any</option> 15 + <option value="only"{{if eq .Query.Forks "only"}} selected{{end}}>only forks</option> 16 + <option value="no"{{if eq .Query.Forks "no"}} selected{{end}}>no forks</option> 17 + </select> 18 + </label> 12 19 <button>filter</button> 13 20 </form> 14 - <p class="summary">{{len .Repos}} repos · <a href="/repos.json?language={{.Query.Language}}&since={{.Query.SinceRaw}}">json</a></p> 21 + <p class="summary">{{len .Repos}} repos · <a href="/repos.json?language={{.Query.Language}}&since={{.Query.SinceRaw}}&forks={{.Query.Forks}}">json</a></p> 15 22 {{if .Repos}} 16 23 <table> 17 24 <thead><tr><th>created</th><th>repo</th><th>knot</th><th>language</th><th>★</th><th>description</th></tr></thead> ··· 25 32 {{- else -}} 26 33 {{.DID}}/{{.Name}} 27 34 {{- end -}} 35 + {{if .Source}} <span class="fork" title="forked from {{.Source}}">↗</span>{{end}} 28 36 </td> 29 37 <td class="knot">{{.Knot}}</td> 30 38 <td class="lang">{{.Primary}}</td>