forked from
quillmatiq.com/augment
Fork of Chiri for Astro for my blog
1<script>
2 import type { GitHubRepoData, CachedRepoData, CardElements } from '@/types'
3
4 let githubCardsObserver: IntersectionObserver | null = null
5
6 // Retrieve cached GitHub repository data from localStorage with expiration check
7 function getCachedData(repo: string): GitHubRepoData | null {
8 try {
9 const cacheKey = `github-repo-${repo}`
10 const cached = localStorage.getItem(cacheKey)
11
12 if (!cached) {
13 return null
14 }
15
16 const parsedCache: CachedRepoData = JSON.parse(cached)
17 const now = Date.now()
18 const oneHour = 60 * 60 * 1000 // 1 hour in milliseconds
19
20 // Check if cache is expired (older than 1 hour)
21 if (now - parsedCache.timestamp > oneHour) {
22 localStorage.removeItem(cacheKey)
23 return null
24 }
25
26 return parsedCache.data
27 } catch (error) {
28 console.warn('Failed to read from cache:', error)
29 return null
30 }
31 }
32
33 // Store GitHub repository data in localStorage with timestamp for cache expiration
34 function setCachedData(repo: string, data: GitHubRepoData): void {
35 try {
36 const cacheKey = `github-repo-${repo}`
37 const cacheData: CachedRepoData = {
38 data,
39 timestamp: Date.now()
40 }
41 localStorage.setItem(cacheKey, JSON.stringify(cacheData))
42 } catch (error) {
43 console.warn('Failed to save to cache:', error)
44 }
45 }
46
47 // Update the GitHub card UI elements with fetched repository data
48 function updateCardUI(el: CardElements, data: GitHubRepoData) {
49 const numberFormat = new Intl.NumberFormat('en', {
50 notation: 'compact',
51 maximumFractionDigits: 1
52 })
53
54 if (el.avatar && data.owner?.avatar_url) {
55 el.avatar.style.backgroundImage = `url(${data.owner.avatar_url})`
56 }
57
58 if (el.desc) {
59 el.desc.textContent = data.description ?? 'No description'
60 }
61
62 if (el.stars) {
63 el.stars.textContent = numberFormat.format(data.stargazers_count ?? 0)
64 }
65
66 if (el.forks) {
67 el.forks.textContent = numberFormat.format(data.forks_count ?? 0)
68 }
69
70 if (el.license) {
71 el.license.textContent = data.license?.spdx_id ?? 'No License'
72 }
73 }
74
75 // Load GitHub repository data for a card, using cache if available or fetching from API
76 async function loadCardData(card: HTMLElement) {
77 const repo = card.dataset.repo
78 if (!repo) {
79 return
80 }
81
82 const el = {
83 avatar: card.querySelector('.gc-owner-avatar') as HTMLElement,
84 desc: card.querySelector('.gc-repo-description') as HTMLElement,
85 stars: card.querySelector('.gc-stars-count') as HTMLElement,
86 forks: card.querySelector('.gc-forks-count') as HTMLElement,
87 license: card.querySelector('.gc-license-info') as HTMLElement
88 } as const
89
90 // Try to get cached data first
91 const cachedData = getCachedData(repo)
92 if (cachedData) {
93 updateCardUI(el, cachedData)
94 return
95 }
96
97 // If no cache, fetch from API
98 try {
99 const response = await fetch(`https://api.github.com/repos/${repo}`)
100
101 if (!response.ok) {
102 if (el.desc) {
103 el.desc.textContent = '--'
104 }
105 return
106 }
107
108 const data = await response.json()
109
110 setCachedData(repo, data)
111
112 updateCardUI(el, data)
113 } catch (error) {
114 console.error(`Failed to fetch ${repo}:`, error)
115 if (el.desc) {
116 el.desc.textContent = '--'
117 }
118 }
119 }
120
121 // Set up intersection observer for lazy loading GitHub cards when they enter viewport
122 function lazySetupGithubCards() {
123 githubCardsObserver?.disconnect()
124
125 const githubCards = document.getElementsByClassName('gc-container')
126 if (githubCards.length === 0) {
127 return
128 }
129
130 // Create an intersection observer to lazy load GitHub repo data when cards enter viewport
131 githubCardsObserver = new IntersectionObserver(
132 (entries) => {
133 entries.forEach((entry) => {
134 if (entry.isIntersecting) {
135 loadCardData(entry.target as HTMLElement)
136 githubCardsObserver?.unobserve(entry.target)
137 }
138 })
139 },
140 { rootMargin: '200px' }
141 )
142
143 Array.from(githubCards).forEach((card) => githubCardsObserver?.observe(card))
144 }
145
146 lazySetupGithubCards()
147 document.addEventListener('astro:page-load', lazySetupGithubCards)
148</script>
149
150<style is:inline>
151 .prose .gc-container {
152 display: block;
153 border: 0.5px solid var(--border);
154 border-radius: 8px;
155 padding: 1rem 1.25rem 0.75rem 1.25rem;
156 margin: 1.25rem 0 1.75rem 0;
157 text-decoration: none;
158 color: inherit;
159 transition: background 0.2s ease-out;
160 background: var(--astro-code-background);
161 }
162
163 .prose .gc-container:hover {
164 background: color-mix(in srgb, var(--selection) 75%, transparent);
165 text-decoration: none;
166 }
167
168 .prose .gc-title-bar {
169 display: flex;
170 align-items: center;
171 gap: 0.75rem;
172 margin-bottom: 0.75rem;
173 }
174
175 .prose .gc-owner-avatar {
176 width: 1.5rem;
177 height: 1.5rem;
178 border-radius: 50%;
179 background-color: var(--border);
180 flex-shrink: 0;
181 }
182
183 .prose .gc-repo-title {
184 font-size: var(--font-size-xl);
185 font-weight: var(--font-weight-regular);
186 color: var(--text-primary);
187 flex-grow: 1;
188 }
189
190 .prose .gc-repo-title strong {
191 font-weight: var(--font-weight-bold);
192 }
193
194 .prose .gc-slash {
195 color: var(--text-secondary);
196 margin: 0 0.375rem;
197 }
198
199 .prose .gc-github-icon {
200 width: 1.5rem;
201 height: 1.5rem;
202 color: var(--text-primary);
203 flex-shrink: 0;
204 }
205
206 .prose .gc-repo-description {
207 font-size: var(--font-size-m);
208 color: var(--text-primary);
209 opacity: 0.6;
210 margin: 0 0 0.75rem 0;
211 line-height: 1.4;
212 }
213
214 .prose .gc-info-bar {
215 display: flex;
216 align-items: center;
217 color: var(--text-primary);
218 opacity: 0.6;
219 gap: 0.35rem;
220 }
221
222 .prose .gc-info-bar .gc-stars-count,
223 .prose .gc-info-bar .gc-forks-count,
224 .prose .gc-info-bar .gc-license-info {
225 margin-right: 0.675rem;
226 font-size: var(--font-size-s);
227 }
228
229 .prose .gc-info-icon {
230 color: var(--text-primary);
231 width: 0.875rem;
232 height: 0.875rem;
233 }
234</style>