Skip to content

Commit 2493c3c

Browse files
committed
use masonary layout for project cards grid
1 parent a3e2e82 commit 2493c3c

File tree

4 files changed

+307
-48
lines changed

4 files changed

+307
-48
lines changed

.astro/types.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
/// <reference types="astro/client" />
2-
/// <reference path="content.d.ts" />

src/components/ProjectCard.astro

Lines changed: 200 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
)}

src/components/ProjectList.astro

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,16 @@ const allTags = [...new Set(projectList.flatMap(project => project.tags || []))]
2828

2929
<section id="project-list" class="containerLayout">
3030
{projectList.map((item) => (
31-
<ProjectCard
32-
name={item.name}
33-
logoLink={item.imageSrc}
34-
projectLink={item.projectLink}
35-
description={item.description}
36-
tags={item.tags}
37-
/>
31+
<div class="masonry-item">
32+
<ProjectCard
33+
name={item.name}
34+
logoLink={item.imageSrc}
35+
projectLink={item.projectLink}
36+
description={item.description}
37+
tags={item.tags}
38+
loadIssues={item.loadIssues}
39+
/>
40+
</div>
3841
))}
3942
</section>
4043

@@ -112,6 +115,52 @@ const allTags = [...new Set(projectList.flatMap(project => project.tags || []))]
112115

113116
// Initial filter (show all)
114117
filterProjects();
118+
119+
// Masonry layout function
120+
function initMasonry() {
121+
const container = document.getElementById('project-list');
122+
const items = Array.from(container.querySelectorAll('.masonry-item'));
123+
124+
if (items.length === 0) return;
125+
126+
// Reset positioning
127+
items.forEach(item => {
128+
item.style.position = 'absolute';
129+
item.style.top = '0';
130+
item.style.left = '0';
131+
});
132+
133+
// Calculate positions
134+
const containerWidth = container.offsetWidth;
135+
const gap = 24; // 1.5rem = 24px
136+
const itemWidth = (containerWidth - 2 * gap) / 3; // 3 columns
137+
138+
const columns = [0, 0, 0]; // Track height of each column
139+
140+
items.forEach((item, index) => {
141+
const columnIndex = index % 3; // Fill by rows (0, 1, 2, 0, 1, 2...)
142+
const x = columnIndex * (itemWidth + gap);
143+
const y = columns[columnIndex];
144+
145+
item.style.position = 'absolute';
146+
item.style.left = x + 'px';
147+
item.style.top = y + 'px';
148+
item.style.width = itemWidth + 'px';
149+
150+
// Update column height
151+
columns[columnIndex] += item.offsetHeight + gap;
152+
});
153+
154+
// Set container height
155+
container.style.height = Math.max(...columns) + 'px';
156+
}
157+
158+
// Initialize masonry on load and resize
159+
window.addEventListener('load', initMasonry);
160+
window.addEventListener('resize', initMasonry);
161+
162+
// Re-initialize masonry after issues load
163+
setTimeout(initMasonry, 2000); // Wait for issues to load
115164
</script>
116165

117166
<style>
@@ -175,12 +224,16 @@ const allTags = [...new Set(projectList.flatMap(project => project.tags || []))]
175224
}
176225

177226
.containerLayout {
178-
display: grid;
179-
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
180-
gap: 1.5rem;
227+
position: relative;
181228
padding: 0 1rem;
182229
}
183230

231+
.masonry-item {
232+
position: absolute;
233+
width: calc(33.333% - 1rem);
234+
min-width: 300px;
235+
}
236+
184237
@media (max-width: 768px) {
185238
#container {
186239
flex-direction: column;
@@ -199,9 +252,15 @@ const allTags = [...new Set(projectList.flatMap(project => project.tags || []))]
199252
}
200253

201254
.containerLayout {
202-
grid-template-columns: 1fr;
203-
gap: 1rem;
204255
padding: 0 0.5rem;
205256
}
257+
258+
.masonry-item {
259+
position: relative !important;
260+
width: 100% !important;
261+
min-width: unset !important;
262+
top: auto !important;
263+
left: auto !important;
264+
}
206265
}
207266
</style>

0 commit comments

Comments
 (0)