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