diff --git a/internal/handlers/cv_helpers.go b/internal/handlers/cv_helpers.go
index 00427ae..e6f9359 100644
--- a/internal/handlers/cv_helpers.go
+++ b/internal/handlers/cv_helpers.go
@@ -1,11 +1,14 @@
package handlers
import (
+ "encoding/json"
"fmt"
"log"
+ "net/http"
"os"
"path/filepath"
"strings"
+ "sync"
"time"
"github.com/go-git/go-git/v5"
@@ -327,9 +330,12 @@ func (h *CVHandler) prepareTemplateData(lang string) (map[string]interface{}, er
)
}
- // Process projects for dynamic dates
+ // Process projects for dynamic dates and fetch GitHub stars
for i := range cv.Projects {
processProjectDates(&cv.Projects[i], lang)
+ if cv.Projects[i].OpenSource && cv.Projects[i].GitRepoUrl != "" {
+ cv.Projects[i].Stars = getGitHubStars(cv.Projects[i].GitRepoUrl)
+ }
}
// Split skills between left and right sidebars
@@ -383,6 +389,65 @@ func (h *CVHandler) prepareTemplateData(lang string) (map[string]interface{}, er
return data, nil
}
+// ==============================================================================
+// GITHUB STARS
+// ==============================================================================
+
+var (
+ starsCache = make(map[string]starsCacheEntry)
+ starsCacheMu sync.RWMutex
+)
+
+type starsCacheEntry struct {
+ count int
+ fetched time.Time
+}
+
+// getGitHubStars fetches the star count for a GitHub repo URL, cached for 30 minutes.
+func getGitHubStars(repoURL string) int {
+ // Extract "owner/repo" from URL
+ repoURL = strings.TrimSuffix(repoURL, "/")
+ parts := strings.SplitN(repoURL, "github.com/", 2)
+ if len(parts) != 2 {
+ return 0
+ }
+ repo := parts[1]
+
+ // Check cache
+ starsCacheMu.RLock()
+ if entry, ok := starsCache[repo]; ok && time.Since(entry.fetched) < 30*time.Minute {
+ starsCacheMu.RUnlock()
+ return entry.count
+ }
+ starsCacheMu.RUnlock()
+
+ // Fetch from GitHub API
+ client := &http.Client{Timeout: 3 * time.Second}
+ resp, err := client.Get(fmt.Sprintf("https://api.github.com/repos/%s", repo))
+ if err != nil {
+ return 0
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != 200 {
+ return 0
+ }
+
+ var result struct {
+ Stars int `json:"stargazers_count"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return 0
+ }
+
+ // Cache the result
+ starsCacheMu.Lock()
+ starsCache[repo] = starsCacheEntry{count: result.Stars, fetched: time.Now()}
+ starsCacheMu.Unlock()
+
+ return result.Stars
+}
+
// ==============================================================================
// COOKIE HELPERS
// ==============================================================================
diff --git a/internal/models/cv.go b/internal/models/cv.go
index 78729e5..94b9cc8 100644
--- a/internal/models/cv.go
+++ b/internal/models/cv.go
@@ -112,6 +112,7 @@ type Project struct {
// Computed fields (not stored in JSON)
ComputedStartDate string `json:"-"` // Dynamically calculated from git repo or system
DynamicDate string `json:"-"` // Current date for ongoing projects
+ Stars int `json:"-"` // GitHub star count (fetched at runtime)
}
type Award struct {
diff --git a/internal/models/cv/cv.go b/internal/models/cv/cv.go
index 572f446..69fd25a 100644
--- a/internal/models/cv/cv.go
+++ b/internal/models/cv/cv.go
@@ -117,6 +117,7 @@ type Project struct {
// Computed fields (not stored in JSON)
ComputedStartDate string `json:"-"` // Dynamically calculated from git repo or system
DynamicDate string `json:"-"` // Current date for ongoing projects
+ Stars int `json:"-"` // GitHub star count (fetched at runtime)
}
type Award struct {
diff --git a/templates/partials/sections/projects.html b/templates/partials/sections/projects.html
index 11c2144..e1de6fd 100644
--- a/templates/partials/sections/projects.html
+++ b/templates/partials/sections/projects.html
@@ -32,7 +32,7 @@
{{if .Current}}LIVE{{end}}
{{if .GitRepoUrl}}GitHub{{end}}
- {{if and .OpenSource .GitRepoUrl}}stars {{end}}
+ {{if and .OpenSource .GitRepoUrl}}stars{{if .Stars}} {{.Stars}}{{end}}{{end}}
{{if .MaintainedBy}}{{$.UI.Sections.MaintainedBy}} {{.MaintainedBy}}{{end}}
{{if .StartDate}}{{.StartDate}}{{if .Current}}{{if .DynamicDate}} / {{.DynamicDate}}{{else}} / {{$.UI.Sections.Present}}{{end}}{{end}}{{end}} - ({{.Location}})
@@ -102,21 +102,4 @@
{{end}}
-
{{end}}