fix: stars count rendered server-side with 30min cache, no client-side API calls
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
"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 {
|
for i := range cv.Projects {
|
||||||
processProjectDates(&cv.Projects[i], lang)
|
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
|
// Split skills between left and right sidebars
|
||||||
@@ -383,6 +389,65 @@ func (h *CVHandler) prepareTemplateData(lang string) (map[string]interface{}, er
|
|||||||
return data, nil
|
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
|
// COOKIE HELPERS
|
||||||
// ==============================================================================
|
// ==============================================================================
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ type Project struct {
|
|||||||
// Computed fields (not stored in JSON)
|
// Computed fields (not stored in JSON)
|
||||||
ComputedStartDate string `json:"-"` // Dynamically calculated from git repo or system
|
ComputedStartDate string `json:"-"` // Dynamically calculated from git repo or system
|
||||||
DynamicDate string `json:"-"` // Current date for ongoing projects
|
DynamicDate string `json:"-"` // Current date for ongoing projects
|
||||||
|
Stars int `json:"-"` // GitHub star count (fetched at runtime)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Award struct {
|
type Award struct {
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ type Project struct {
|
|||||||
// Computed fields (not stored in JSON)
|
// Computed fields (not stored in JSON)
|
||||||
ComputedStartDate string `json:"-"` // Dynamically calculated from git repo or system
|
ComputedStartDate string `json:"-"` // Dynamically calculated from git repo or system
|
||||||
DynamicDate string `json:"-"` // Current date for ongoing projects
|
DynamicDate string `json:"-"` // Current date for ongoing projects
|
||||||
|
Stars int `json:"-"` // GitHub star count (fetched at runtime)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Award struct {
|
type Award struct {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
</strong>
|
</strong>
|
||||||
{{if .Current}}<span class="live-badge"><iconify-icon icon="mdi:wifi" width="14" height="14"></iconify-icon>LIVE</span>{{end}}
|
{{if .Current}}<span class="live-badge"><iconify-icon icon="mdi:wifi" width="14" height="14"></iconify-icon>LIVE</span>{{end}}
|
||||||
{{if .GitRepoUrl}}<a href="{{.GitRepoUrl}}" target="_blank" rel="noopener noreferrer" class="github-badge"><iconify-icon icon="mdi:github" width="14" height="14"></iconify-icon>GitHub</a>{{end}}
|
{{if .GitRepoUrl}}<a href="{{.GitRepoUrl}}" target="_blank" rel="noopener noreferrer" class="github-badge"><iconify-icon icon="mdi:github" width="14" height="14"></iconify-icon>GitHub</a>{{end}}
|
||||||
{{if and .OpenSource .GitRepoUrl}}<a href="{{.GitRepoUrl}}/stargazers" target="_blank" rel="noopener noreferrer" class="stars-badge" data-repo="{{githubRepo .GitRepoUrl}}"><iconify-icon icon="mdi:star" width="14" height="14"></iconify-icon>stars <span class="stars-count"></span></a>{{end}}
|
{{if and .OpenSource .GitRepoUrl}}<a href="{{.GitRepoUrl}}/stargazers" target="_blank" rel="noopener noreferrer" class="stars-badge"><iconify-icon icon="mdi:star" width="14" height="14"></iconify-icon>stars{{if .Stars}} {{.Stars}}{{end}}</a>{{end}}
|
||||||
{{if .MaintainedBy}}<span class="maintained-badge">{{$.UI.Sections.MaintainedBy}} {{.MaintainedBy}}</span>{{end}}
|
{{if .MaintainedBy}}<span class="maintained-badge">{{$.UI.Sections.MaintainedBy}} {{.MaintainedBy}}</span>{{end}}
|
||||||
<br>
|
<br>
|
||||||
<small>{{if .StartDate}}{{.StartDate}}{{if .Current}}{{if .DynamicDate}} / {{.DynamicDate}}{{else}} / {{$.UI.Sections.Present}}{{end}}{{end}}{{end}} - ({{.Location}})</small>
|
<small>{{if .StartDate}}{{.StartDate}}{{if .Current}}{{if .DynamicDate}} / {{.DynamicDate}}{{else}} / {{$.UI.Sections.Present}}{{end}}{{end}}{{end}} - ({{.Location}})</small>
|
||||||
@@ -102,21 +102,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{{end}}
|
{{end}}
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
document.querySelectorAll('.stars-badge[data-repo]').forEach(function(badge) {
|
|
||||||
var repo = badge.getAttribute('data-repo');
|
|
||||||
if (!repo) return;
|
|
||||||
fetch('https://api.github.com/repos/' + repo, {headers: {'Accept': 'application/vnd.github.v3+json'}})
|
|
||||||
.then(function(r) { return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (data.stargazers_count !== undefined) {
|
|
||||||
var el = badge.querySelector('.stars-count');
|
|
||||||
if (el) el.textContent = data.stargazers_count;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function() {});
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user