Files
ghglance/internal/card/top_starred_repos.go
T
tiennm99 ad775adffa feat(web): add web UI and Coolify compose deployment
`ghglance -serve :8080` runs a web UI from the same binary: submit a
username, a background job renders every card in every theme, and
/u/<user> shows them again with a theme picker and embed URLs.

- Cards are stored on disk with atomic symlink publishing and deleted
  after -retention (default 24h); -cooldown limits token-less regeneration.
- Token-less jobs use a public-only server token with private and org
  scope forced off; a submitter's own token is used for that job only and
  never stored or logged. The form links to GitHub's new-token page with
  the needed scopes pre-ticked.
- The CLI and web share one fetch helper; CLI output is unchanged.
- compose.yml deploys to Coolify with a data volume and /healthz check.
2026-10-07 11:08:07 +07:00

102 lines
2.8 KiB
Go

package card
import (
"fmt"
"strings"
"github.com/tiennm99/ghglance/internal/github"
"github.com/tiennm99/ghglance/internal/theme"
)
type topStarredReposCard struct{}
func (topStarredReposCard) Filename() string { return "top-starred-repos.svg" }
// maxTopRepoRows is how many repos we show. Matches the row count the stats
// and records cards fit in the shared 340x200 frame.
const maxTopRepoRows = 7
func (topStarredReposCard) SVG(p *github.Profile, t theme.Theme) ([]byte, error) {
const (
width = 340
height = 200
rowX = 20
rowY0 = 55
rowDY = 20
barX = 150
barW = 120 // max bar width; the top repo fills this
barH = 10
valueX = 334 // right-aligned anchor for the star count
nameMax = 17 // truncate long repo names at this many characters
)
repos := ownedNonForkRepos(p.TopRepos)
if len(repos) > maxTopRepoRows {
repos = repos[:maxTopRepoRows]
}
var b strings.Builder
b.WriteString(header(width, height, t.Background, t.Stroke, t.StrokeOpacity, t.Title, "Top Starred Repos"))
if len(repos) == 0 {
fmt.Fprintf(&b, `
<text x="25" y="100" font-size="13" fill="%s">No public repos with stars.</text>`, t.Muted)
b.WriteString(footer)
return []byte(b.String()), nil
}
maxStars := repos[0].Stars
if maxStars <= 0 {
maxStars = 1
}
// Row layout: language swatch + name on the left, a proportional bar in
// the middle, star count right-anchored at valueX. The card title already
// says "Top Starred Repos", so the per-row star icon would just be noise.
for i, r := range repos {
y := rowY0 + i*rowDY
langColor := r.PrimaryColor
if langColor == "" {
langColor = t.Accent
}
name := truncate(r.Name, nameMax)
fmt.Fprintf(&b, `
<circle cx="%d" cy="%d" r="4" fill="%s"/>
<text x="%d" y="%d" font-size="12" fill="%s">%s</text>`,
rowX+4, y-4, langColor,
rowX+14, y, t.Text, escapeXML(name))
bw := float64(barW) * float64(r.Stars) / float64(maxStars)
fmt.Fprintf(&b, `
<rect x="%d" y="%d" width="%d" height="%d" rx="3" fill="%s" fill-opacity="0.15"/>
<rect x="%d" y="%d" width="%.2f" height="%d" rx="3" fill="%s"/>`,
barX, y-barH+2, barW, barH, t.Accent,
barX, y-barH+2, bw, barH, t.Accent)
fmt.Fprintf(&b, `
<text x="%d" y="%d" font-size="12" font-weight="600" fill="%s" text-anchor="end">%s ★</text>`,
valueX, y, t.Accent, escapeXML(formatInt(r.Stars)))
}
b.WriteString(footer)
return []byte(b.String()), nil
}
// ownedNonForkRepos filters out forks so the card highlights the user's own
// work. TopRepos is already sorted by stargazer count desc at fetch time, so
// we just skim the prefix.
func ownedNonForkRepos(repos []github.RepoInfo) []github.RepoInfo {
out := make([]github.RepoInfo, 0, len(repos))
for _, r := range repos {
if r.IsFork {
continue
}
if r.Stars <= 0 {
continue
}
out = append(out, r)
}
return out
}