@@ -5,9 +5,10 @@ export interface Props {
55 name: string ;
66 description: string ;
77 tags? : string [];
8+ loadIssues? : boolean ;
89}
910
10- const { projectLink, logoLink, name, description, tags = [] } = Astro .props ;
11+ const { projectLink, logoLink, name, description, tags = [], loadIssues = false } = Astro .props ;
1112---
1213
1314<div class =" Card-Container" >
@@ -34,6 +35,15 @@ const { projectLink, logoLink, name, description, tags = [] } = Astro.props;
3435 <div class =" Card-Description" >
3536 <p >{ description } </p >
3637 </div >
38+ { loadIssues && (
39+ <div class = " Card-Issues" id = { ` issues-${name .replace (/ \s + / g , ' -' ).toLowerCase ()} ` } >
40+ <div class = " Issues-Skeleton" >
41+ <div class = " skeleton-line" ></div >
42+ <div class = " skeleton-line" ></div >
43+ <div class = " skeleton-line" ></div >
44+ </div >
45+ </div >
46+ )}
3747 </div >
3848 <div class =" Card-Link" >Go to Project</div >
3949 </a >
@@ -47,7 +57,8 @@ const { projectLink, logoLink, name, description, tags = [] } = Astro.props;
4757 border: 1px solid rgba(255, 255, 255, 0.1);
4858 overflow: hidden;
4959 transition: all 0.3s ease;
50- height: 100%;
60+ height: auto;
61+ min-height: 200px;
5162 }
5263
5364 .Card-Container:hover {
@@ -123,6 +134,102 @@ const { projectLink, logoLink, name, description, tags = [] } = Astro.props;
123134 margin: 0;
124135 }
125136
137+ .Card-Issues {
138+ margin-top: 1rem;
139+ padding-top: 1rem;
140+ border-top: 1px solid rgba(255, 255, 255, 0.1);
141+ }
142+
143+ .Issues-Skeleton {
144+ display: flex;
145+ flex-direction: column;
146+ gap: 0.5rem;
147+ }
148+
149+ .skeleton-line {
150+ height: 12px;
151+ background: linear-gradient(90deg, rgba(255, 255, 255, 0.1) 25%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.1) 75%);
152+ background-size: 200% 100%;
153+ animation: skeleton-loading 1.5s infinite;
154+ border-radius: 6px;
155+ }
156+
157+ .skeleton-line:nth-child(1) {
158+ width: 100%;
159+ }
160+
161+ .skeleton-line:nth-child(2) {
162+ width: 80%;
163+ }
164+
165+ .skeleton-line:nth-child(3) {
166+ width: 60%;
167+ }
168+
169+ @keyframes skeleton-loading {
170+ 0% {
171+ background-position: 200% 0;
172+ }
173+ 100% {
174+ background-position: -200% 0;
175+ }
176+ }
177+
178+ .Issue-Item {
179+ padding: 0.5rem 0;
180+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
181+ }
182+
183+ .Issue-Item:last-child {
184+ border-bottom: none;
185+ }
186+
187+ .Issue-Title {
188+ font-size: 0.85rem;
189+ color: rgba(255, 255, 255, 0.9);
190+ margin: 0 0 0.25rem 0;
191+ line-height: 1.3;
192+ display: -webkit-box;
193+ -webkit-line-clamp: 2;
194+ -webkit-box-orient: vertical;
195+ overflow: hidden;
196+ }
197+
198+ .Issue-Link {
199+ color: inherit;
200+ text-decoration: none;
201+ }
202+
203+ .Issue-Link:hover {
204+ color: #a5b4fc;
205+ }
206+
207+ .Issue-Labels {
208+ display: flex;
209+ gap: 0.25rem;
210+ flex-wrap: wrap;
211+ margin-top: 0.25rem;
212+ }
213+
214+ .Issue-Label {
215+ font-size: 0.7rem;
216+ padding: 0.125rem 0.375rem;
217+ border-radius: 8px;
218+ font-weight: 500;
219+ }
220+
221+ .Issue-Label.good-first-issue {
222+ background: rgba(34, 197, 94, 0.2);
223+ color: #4ade80;
224+ border: 1px solid rgba(34, 197, 94, 0.3);
225+ }
226+
227+ .Issue-Label.help-wanted {
228+ background: rgba(59, 130, 246, 0.2);
229+ color: #60a5fa;
230+ border: 1px solid rgba(59, 130, 246, 0.3);
231+ }
232+
126233 .Card-Link {
127234 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
128235 color: white;
@@ -151,3 +258,94 @@ const { projectLink, logoLink, name, description, tags = [] } = Astro.props;
151258 }
152259 }
153260</style >
261+
262+ { loadIssues && (
263+ <script >
264+ // Extract repository info from project link
265+ const projectLink = '{ projectLink } ';
266+ const projectName = '{ name } ';
267+ const issuesContainerId = `issues-${ projectName .replace (/ \s + / g , ' -' ).toLowerCase ()} `;
268+
269+ // Extract owner/repo from GitHub URL
270+ function extractRepoInfo(url) {
271+ const match = url .match (/ github\. com\/ ([^ \/ ] + )\/ ([^ \/ ] + )/ );
272+ if (match ) {
273+ return {
274+ owner : match [1 ],
275+ repo : match [2 ].replace (/ \/ $ / , ' ' ) // Remove trailing slash
276+ };
277+ }
278+ return null ;
279+ }
280+
281+ // Fetch issues from GitHub API
282+ async function fetchIssues(owner, repo) {
283+ try {
284+ const labels = [' good-first-issue' , ' help-wanted' ];
285+ const promises = labels .map (label =>
286+ fetch (` https://api.github.com/repos/${owner }/${repo }/issues?labels=${label }&state=open&per_page=10 ` )
287+ .then (res => res .json ())
288+ .catch (() => [])
289+ );
290+
291+ const [goodFirstIssues , helpWantedIssues ] = await Promise.all(promises);
292+
293+ // Combine and prioritize good-first-issue
294+ const allIssues = [
295+ ... goodFirstIssues .map (issue => ({ ... issue , priority: ' good-first-issue' })),
296+ ... helpWantedIssues .map (issue => ({ ... issue , priority: ' help-wanted' }))
297+ ];
298+
299+ // Remove duplicates and sort by priority
300+ const uniqueIssues = allIssues.filter((issue, index, self) =>
301+ index === self.findIndex(i => i.id === issue.id)
302+ );
303+
304+ // Sort: good-first-issue first, then help-wanted
305+ uniqueIssues.sort((a, b) => {
306+ if (a.priority === 'good-first-issue' && b.priority !== 'good-first-issue') return -1;
307+ if (a.priority !== 'good-first-issue' && b.priority === 'good-first-issue') return 1;
308+ return 0;
309+ });
310+
311+ return uniqueIssues.slice(0, 3); // Show only 3 issues
312+ } catch (error) {
313+ console.error('Error fetching issues:' , error);
314+ return [];
315+ }
316+ }
317+
318+ // Render issues in the container
319+ function renderIssues(issues, containerId) {
320+ const container = document .getElementById (containerId );
321+ if (! container ) return ;
322+
323+ if (issues .length === 0 ) {
324+ container.innerHTML = ' <div class="no-issues">No issues found</div>' ;
325+ return;
326+ }
327+
328+ const issuesHTML = issues .map (issue => `
329+ <div class="Issue-Item">
330+ <a href="${issue .html_url }" target="_blank" class="Issue-Link">
331+ <div class="Issue-Title">${issue .title }</div>
332+ <div class="Issue-Labels">
333+ <span class="Issue-Label ${issue .priority }">${issue .priority }</span>
334+ </div>
335+ </a>
336+ </div>
337+ ` ).join (' ' );
338+
339+ container .innerHTML = issuesHTML ;
340+ }
341+
342+ // Initialize issue loading
343+ document.addEventListener('DOMContentLoaded', async () => {
344+ const repoInfo = extractRepoInfo (projectLink );
345+ if (repoInfo ) {
346+ const issues = await fetchIssues (repoInfo .owner , repoInfo .repo );
347+ renderIssues(issues , issuesContainerId );
348+ }
349+ } );
350+ </script >
351+ )}
0 commit comments