diff --git a/README.md b/README.md
index 88f59e14..c7d6ed3e 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/internal/card/icons.go b/internal/card/icons.go
new file mode 100644
index 00000000..0da35c3e
--- /dev/null
+++ b/internal/card/icons.go
@@ -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 = ``
+ iconCompany = ``
+ iconLocation = ``
+ iconClock = ``
+ iconLink = ``
+ iconPeople = ``
+)
diff --git a/internal/card/profile.go b/internal/card/profile.go
index 64c70af3..f8923c97 100644
--- a/internal/card/profile.go
+++ b/internal/card/profile.go
@@ -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, `
- %s
- %s`,
- y, t.Muted, escapeXML(r.label),
- y, t.Text, escapeXML(r.value))
- y += 22
+ %s
+ %s`,
+ 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"
+}
diff --git a/internal/card/stats.go b/internal/card/stats.go
index b2ff6fde..2e9a3db7 100644
--- a/internal/card/stats.go
+++ b/internal/card/stats.go
@@ -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)},