mirror of
https://github.com/tiennm99/ghstats.git
synced 2026-09-01 02:32:01 +00:00
refactor(card): redesign profile details with octicon labels
- Title becomes "login (Name)" (or just "login") instead of "Name's Profile Details". - Row labels replaced with Octicon glyphs (repos, company, location, link, clock, people) rendered in the theme's muted color. - Joined and account-age rows merge into a single "YYYY-MM-DD (N years ago)" line; years round down to whole years with month/day fallbacks. - Drop profile-summary-for-github from README credits — no code from that project is used. github-profile-summary-cards is the only direct reference.
This commit is contained in:
@@ -130,8 +130,7 @@ access token with `read:user` and `repo`, save it as a repo secret (e.g.
|
||||
|
||||
## Credits & inspiration
|
||||
|
||||
- [**github-profile-summary-cards**](https://github.com/vn7n24fzkq/github-profile-summary-cards) by [@vn7n24fzkq](https://github.com/vn7n24fzkq) — card layout, chart styles, theme palette, and output structure.
|
||||
- [**profile-summary-for-github**](https://github.com/tipsy/profile-summary-for-github) by [@tipsy](https://github.com/tipsy) — the original profile-summary generator.
|
||||
- [**github-profile-summary-cards**](https://github.com/vn7n24fzkq/github-profile-summary-cards) by [@vn7n24fzkq](https://github.com/vn7n24fzkq) — card layout, chart styles, theme palette, Octicon selection, and output structure.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package card
|
||||
|
||||
// Octicon path data (16x16 viewBox) from https://primer.style/octicons.
|
||||
// Mirrors the icon set used by github-profile-summary-cards so rendered
|
||||
// profile cards look consistent with that project.
|
||||
const (
|
||||
iconRepos = `<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>`
|
||||
iconCompany = `<path fill-rule="evenodd" d="M1.5 14.25c0 .138.112.25.25.25H4v-1.25a.75.75 0 01.75-.75h2.5a.75.75 0 01.75.75v1.25h2.25a.25.25 0 00.25-.25V1.75a.25.25 0 00-.25-.25h-8.5a.25.25 0 00-.25.25v12.5zM1.75 16A1.75 1.75 0 010 14.25V1.75C0 .784.784 0 1.75 0h8.5C11.216 0 12 .784 12 1.75v12.5c0 .085-.006.168-.018.25h2.268a.25.25 0 00.25-.25V8.285a.25.25 0 00-.111-.208l-1.055-.703a.75.75 0 11.832-1.248l1.055.703c.487.325.779.871.779 1.456v5.965A1.75 1.75 0 0114.25 16h-3.5a.75.75 0 01-.197-.026c-.099.017-.2.026-.303.026h-3a.75.75 0 01-.75-.75V14h-1v1.25a.75.75 0 01-.75.75h-3zM3 3.75A.75.75 0 013.75 3h.5a.75.75 0 010 1.5h-.5A.75.75 0 013 3.75zM3.75 6a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5zM3 9.75A.75.75 0 013.75 9h.5a.75.75 0 010 1.5h-.5A.75.75 0 013 9.75zM7.75 9a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5zM7 6.75A.75.75 0 017.75 6h.5a.75.75 0 010 1.5h-.5A.75.75 0 017 6.75zM7.75 3a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5z"/>`
|
||||
iconLocation = `<path fill-rule="evenodd" d="M11.536 3.464a5 5 0 010 7.072L8 14.07l-3.536-3.535a5 5 0 117.072-7.072v.001zm1.06 8.132a6.5 6.5 0 10-9.192 0l3.535 3.536a1.5 1.5 0 002.122 0l3.535-3.536zM8 9a2 2 0 100-4 2 2 0 000 4z"/>`
|
||||
iconClock = `<path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm.5 4.75a.75.75 0 00-1.5 0v3.5a.75.75 0 00.471.696l2.5 1a.75.75 0 00.557-1.392L8.5 7.742V4.75z"/>`
|
||||
iconLink = `<path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"/>`
|
||||
iconPeople = `<path fill-rule="evenodd" d="M5.5 3.5a2 2 0 100 4 2 2 0 000-4zM2 5.5a3.5 3.5 0 115.898 2.549 5.507 5.507 0 013.034 4.084.75.75 0 11-1.482.235 4.001 4.001 0 00-7.9 0 .75.75 0 01-1.482-.236A5.507 5.507 0 013.102 8.05 3.49 3.49 0 012 5.5zM11 4a.75.75 0 100 1.5 1.5 1.5 0 01.666 2.844.75.75 0 00-.416.672v.352a.75.75 0 00.574.73c1.2.289 2.162 1.2 2.522 2.372a.75.75 0 101.434-.44 5.01 5.01 0 00-2.56-3.012A3 3 0 0011 4z"/>`
|
||||
)
|
||||
+87
-36
@@ -13,66 +13,117 @@ type profileCard struct{}
|
||||
|
||||
func (profileCard) Filename() string { return "0-profile-details.svg" }
|
||||
|
||||
// profileRow is one labeled-by-icon line in the profile card.
|
||||
type profileRow struct {
|
||||
icon string // raw SVG path(s) from icons.go
|
||||
value string
|
||||
}
|
||||
|
||||
func (profileCard) SVG(p *github.Profile, t theme.Theme) ([]byte, error) {
|
||||
const (
|
||||
width = 500
|
||||
height = 220
|
||||
width = 500
|
||||
height = 220
|
||||
rowX = 25
|
||||
rowY0 = 70
|
||||
rowDY = 24
|
||||
iconSize = 14
|
||||
maxRows = 7
|
||||
)
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(header(width, height, t.Background, t.Stroke, t.StrokeOpacity, t.Title, title(p)))
|
||||
b.WriteString(header(width, height, t.Background, t.Stroke, t.StrokeOpacity, t.Title, cardTitle(p)))
|
||||
|
||||
// Key-value lines; skip empty fields to avoid blank rows.
|
||||
y := 75
|
||||
rows := profileRows(p)
|
||||
for _, r := range rows {
|
||||
rows := buildProfileRows(p)
|
||||
if len(rows) > maxRows {
|
||||
rows = rows[:maxRows]
|
||||
}
|
||||
|
||||
// scale factor to fit 16x16 octicon into iconSize box
|
||||
scale := float64(iconSize) / 16.0
|
||||
|
||||
for i, r := range rows {
|
||||
y := rowY0 + i*rowDY
|
||||
// icon glyph: translate to row position, scale down, fill with muted.
|
||||
fmt.Fprintf(&b, `
|
||||
<text x="25" y="%d" font-size="13" fill="%s">%s</text>
|
||||
<text x="140" y="%d" font-size="13" fill="%s">%s</text>`,
|
||||
y, t.Muted, escapeXML(r.label),
|
||||
y, t.Text, escapeXML(r.value))
|
||||
y += 22
|
||||
<g transform="translate(%d,%.2f) scale(%.3f)" fill="%s">%s</g>
|
||||
<text x="%d" y="%d" font-size="13" fill="%s">%s</text>`,
|
||||
rowX, float64(y-iconSize+2), scale, t.Muted, r.icon,
|
||||
rowX+iconSize+10, y, t.Text, escapeXML(r.value))
|
||||
}
|
||||
|
||||
b.WriteString(footer)
|
||||
return []byte(b.String()), nil
|
||||
}
|
||||
|
||||
func title(p *github.Profile) string {
|
||||
// cardTitle mirrors github-profile-summary-cards: "login (Name)" when name is
|
||||
// set, "login" otherwise.
|
||||
func cardTitle(p *github.Profile) string {
|
||||
if p.Name != "" {
|
||||
return p.Name + "'s Profile Details"
|
||||
return p.Login + " (" + p.Name + ")"
|
||||
}
|
||||
return p.Login + "'s Profile Details"
|
||||
return p.Login
|
||||
}
|
||||
|
||||
type kv struct{ label, value string }
|
||||
|
||||
func profileRows(p *github.Profile) []kv {
|
||||
rows := []kv{{"Username", "@" + p.Login}}
|
||||
if p.Name != "" {
|
||||
rows = append(rows, kv{"Name", p.Name})
|
||||
}
|
||||
func buildProfileRows(p *github.Profile) []profileRow {
|
||||
var rows []profileRow
|
||||
if p.Company != "" {
|
||||
rows = append(rows, kv{"Company", p.Company})
|
||||
rows = append(rows, profileRow{icon: iconCompany, value: p.Company})
|
||||
}
|
||||
if p.Location != "" {
|
||||
rows = append(rows, kv{"Location", p.Location})
|
||||
rows = append(rows, profileRow{icon: iconLocation, value: p.Location})
|
||||
}
|
||||
if p.Website != "" {
|
||||
rows = append(rows, kv{"Website", p.Website})
|
||||
rows = append(rows, profileRow{icon: iconLink, value: p.Website})
|
||||
}
|
||||
if !p.CreatedAt.IsZero() {
|
||||
rows = append(rows, kv{"Joined", p.CreatedAt.Format("2006-01-02")})
|
||||
years := time.Since(p.CreatedAt).Hours() / 24 / 365
|
||||
rows = append(rows, kv{"Account age", fmt.Sprintf("%.1f years", years)})
|
||||
}
|
||||
rows = append(rows,
|
||||
kv{"Followers", formatInt(p.Followers)},
|
||||
kv{"Following", formatInt(p.Following)},
|
||||
kv{"Public repos", formatInt(p.PublicRepos)},
|
||||
)
|
||||
if len(rows) > 7 {
|
||||
rows = rows[:7]
|
||||
rows = append(rows, profileRow{
|
||||
icon: iconClock,
|
||||
value: fmt.Sprintf("%s (%s)", p.CreatedAt.Format("2006-01-02"), ageAgo(p.CreatedAt)),
|
||||
})
|
||||
}
|
||||
rows = append(rows, profileRow{
|
||||
icon: iconPeople,
|
||||
value: fmt.Sprintf("%s followers · %s following", formatInt(p.Followers), formatInt(p.Following)),
|
||||
})
|
||||
rows = append(rows, profileRow{
|
||||
icon: iconRepos,
|
||||
value: fmt.Sprintf("%s public repos", formatInt(p.PublicRepos)),
|
||||
})
|
||||
return rows
|
||||
}
|
||||
|
||||
// ageAgo returns "N years ago" (rounded down to whole years). Falls back to
|
||||
// months or days for accounts under a year old. Matches
|
||||
// github-profile-summary-cards' getProfileDateJoined.
|
||||
func ageAgo(since time.Time) string {
|
||||
now := time.Now()
|
||||
y := now.Year() - since.Year()
|
||||
m := int(now.Month()) - int(since.Month())
|
||||
if m < 0 || (m == 0 && now.Day() < since.Day()) {
|
||||
y--
|
||||
}
|
||||
if y > 0 {
|
||||
return fmt.Sprintf("%d %s ago", y, plural(y, "year"))
|
||||
}
|
||||
|
||||
months := (now.Year()-since.Year())*12 + int(now.Month()) - int(since.Month())
|
||||
if now.Day() < since.Day() {
|
||||
months--
|
||||
}
|
||||
if months > 0 {
|
||||
return fmt.Sprintf("%d %s ago", months, plural(months, "month"))
|
||||
}
|
||||
|
||||
days := int(now.Sub(since).Hours() / 24)
|
||||
if days < 0 {
|
||||
days = 0
|
||||
}
|
||||
return fmt.Sprintf("%d %s ago", days, plural(days, "day"))
|
||||
}
|
||||
|
||||
func plural(n int, word string) string {
|
||||
if n == 1 {
|
||||
return word
|
||||
}
|
||||
return word + "s"
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ func (statsCard) SVG(p *github.Profile, t theme.Theme) ([]byte, error) {
|
||||
height = 220
|
||||
)
|
||||
|
||||
items := []kv{
|
||||
items := []struct{ label, value string }{
|
||||
{"Total Stars", formatInt(p.TotalStars)},
|
||||
{"Total Commits (last year)", formatInt(p.TotalCommits)},
|
||||
{"Total PRs", formatInt(p.TotalPRs)},
|
||||
|
||||
Reference in New Issue
Block a user