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:
2026-04-18 19:43:32 +07:00
parent 1c4244722c
commit 9c81926c06
4 changed files with 102 additions and 39 deletions
+1 -2
View File
@@ -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
+13
View File
@@ -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
View File
@@ -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"
}
+1 -1
View File
@@ -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)},