From 9c81926c06d1172f9110ce63f00a928da6231202 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sat, 18 Apr 2026 19:43:32 +0700 Subject: [PATCH] refactor(card): redesign profile details with octicon labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- README.md | 3 +- internal/card/icons.go | 13 +++++ internal/card/profile.go | 123 +++++++++++++++++++++++++++------------ internal/card/stats.go | 2 +- 4 files changed, 102 insertions(+), 39 deletions(-) create mode 100644 internal/card/icons.go 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)},