Skip to content

Commit 48360bd

Browse files
authored
Merge pull request #1 from StupidHackTH/feat/add-project-permalinks
feat: Add permalinks to projects
2 parents 5b5b5e7 + 8691372 commit 48360bd

File tree

1 file changed

+130
-43
lines changed

1 file changed

+130
-43
lines changed

src/components/ProjectsSection.astro

Lines changed: 130 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@ function extractImageUrl(imageMarkdown: string): string {
88
}
99
1010
// Function to parse links from markdown format [Label](url)
11-
function parseLinks(linksMarkdown: string): Array<{label: string, url: string, icon: string}> {
11+
function parseLinks(
12+
linksMarkdown: string
13+
): Array<{ label: string; url: string; icon: string }> {
1214
if (!linksMarkdown.trim()) return [];
13-
15+
1416
const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
15-
const links: Array<{label: string, url: string, icon: string}> = [];
17+
const links: Array<{ label: string; url: string; icon: string }> = [];
1618
let match;
17-
19+
1820
while ((match = linkPattern.exec(linksMarkdown)) !== null) {
1921
const label = match[1];
2022
const url = match[2];
21-
23+
2224
// Map link types to icons
2325
let icon = "mdi:link"; // default
2426
if (label.toLowerCase().includes("github")) {
@@ -30,44 +32,49 @@ function parseLinks(linksMarkdown: string): Array<{label: string, url: string, i
3032
} else if (label.toLowerCase().includes("slides")) {
3133
icon = "mdi:presentation";
3234
}
33-
35+
3436
links.push({ label, url, icon });
3537
}
36-
38+
3739
return links;
3840
}
3941
4042
// Function to parse team members from mixed markdown/plain text format
41-
function parseMembers(membersString: string): Array<{name: string, url?: string, icon: string}> {
43+
function parseMembers(
44+
membersString: string
45+
): Array<{ name: string; url?: string; icon: string }> {
4246
if (!membersString.trim()) return [];
43-
44-
const memberLines = membersString.split('\n').map(line => line.trim()).filter(line => line);
45-
const members: Array<{name: string, url?: string, icon: string}> = [];
46-
47-
memberLines.forEach(line => {
47+
48+
const memberLines = membersString
49+
.split("\n")
50+
.map((line) => line.trim())
51+
.filter((line) => line);
52+
const members: Array<{ name: string; url?: string; icon: string }> = [];
53+
54+
memberLines.forEach((line) => {
4855
// Check if line contains markdown link [Name](URL)
4956
const linkMatch = line.match(/\[([^\]]+)\]\(([^)]+)\)/);
5057
if (linkMatch) {
5158
const name = linkMatch[1];
5259
const url = linkMatch[2];
53-
60+
5461
// Map URL types to icons
5562
let icon = "mdi:account-circle"; // default for linked members
56-
if (url.includes('github.com')) {
63+
if (url.includes("github.com")) {
5764
icon = "mdi:github";
58-
} else if (url.includes('linkedin.com')) {
65+
} else if (url.includes("linkedin.com")) {
5966
icon = "mdi:linkedin";
60-
} else if (url.includes('twitter.com') || url.includes('x.com')) {
67+
} else if (url.includes("twitter.com") || url.includes("x.com")) {
6168
icon = "mdi:twitter";
6269
}
63-
70+
6471
members.push({ name, url, icon });
6572
} else {
6673
// Plain text member name
6774
members.push({ name: line, icon: "mdi:account" });
6875
}
6976
});
70-
77+
7178
return members;
7279
}
7380
---
@@ -81,7 +88,7 @@ function parseMembers(membersString: string): Array<{name: string, url?: string,
8188
</h2>
8289

8390
<div
84-
class="inline-block text-white px-6 py-3 rounded-lg mb-12 text-xl md:text-2xl font-medium transform -rotate-2 bg-[#19806f] scroll-animate bounce-up stagger-1 mx-auto block text-center"
91+
class="text-white px-6 py-3 rounded-lg mb-12 text-xl md:text-2xl font-medium transform -rotate-2 bg-[#19806f] scroll-animate bounce-up stagger-1 mx-auto block text-center"
8592
style="font-family: 'K2D', sans-serif;"
8693
>
8794
ผลงานแห่งความเพี้ยน 🎨
@@ -93,9 +100,10 @@ function parseMembers(membersString: string): Array<{name: string, url?: string,
93100
const imageUrl = extractImageUrl(team.project.image);
94101
const links = parseLinks(team.project.links);
95102
const members = parseMembers(team.members);
96-
103+
97104
return (
98105
<div
106+
id={team.slug}
99107
class={`bg-gray-900 rounded-xl border border-gray-800 hover:border-[#19806f] transition-all duration-300 overflow-hidden scroll-animate fade-up stagger-${index + 2}`}
100108
>
101109
{/* Project Image */}
@@ -108,52 +116,62 @@ function parseMembers(membersString: string): Array<{name: string, url?: string,
108116
/>
109117
</div>
110118
)}
111-
119+
112120
{/* Content */}
113121
<div class="p-6">
114122
{/* Project Name & Team */}
115123
<div class="mb-4">
116-
<h3
117-
class="text-xl font-bold text-white mb-2"
118-
style="font-family: 'K2D', sans-serif;"
119-
>
120-
{team.project.name}
121-
</h3>
124+
<div class="flex items-center gap-2 mb-2 group">
125+
<h3
126+
class="text-xl font-bold text-white"
127+
style="font-family: 'K2D', sans-serif;"
128+
>
129+
{team.project.name}
130+
</h3>
131+
<a
132+
href={`#${team.slug}`}
133+
title="Copy permalink"
134+
aria-label="Copy permalink"
135+
class="copy-permalink-button text-gray-500 hover:text-white transition-all opacity-0 group-hover:opacity-100"
136+
data-slug={team.slug}
137+
>
138+
<iconify-icon icon="mdi:link-variant" class="text-lg" />
139+
</a>
140+
</div>
122141
<p class="text-[#19806f] font-medium text-sm mb-3">
123142
by {team.name}
124143
</p>
125-
144+
126145
{/* Team Members */}
127146
{members.length > 0 && (
128147
<div class="flex flex-wrap gap-2 mb-3">
129-
{members.map((member) => (
148+
{members.map((member) =>
130149
member.url ? (
131150
<a
132151
href={member.url}
133152
target="_blank"
134153
rel="noopener noreferrer"
135154
class="inline-flex items-center gap-1.5 px-2 py-1 bg-gray-800 hover:bg-gray-700 text-gray-300 hover:text-white text-xs rounded-md transition-colors"
136155
>
137-
<iconify-icon icon={member.icon} class="text-sm"></iconify-icon>
156+
<iconify-icon icon={member.icon} class="text-sm" />
138157
{member.name}
139158
</a>
140159
) : (
141160
<span class="inline-flex items-center gap-1.5 px-2 py-1 bg-gray-800 text-gray-300 text-xs rounded-md">
142-
<iconify-icon icon={member.icon} class="text-sm"></iconify-icon>
161+
<iconify-icon icon={member.icon} class="text-sm" />
143162
{member.name}
144163
</span>
145164
)
146-
))}
165+
)}
147166
</div>
148167
)}
149168
</div>
150169

151170
{/* Description */}
152171
<p class="text-gray-300 leading-relaxed mb-4 text-sm">
153-
{team.project.description.length > 200
154-
? team.project.description.substring(0, 200) + "..."
155-
: team.project.description
156-
}
172+
{team.project.description.length > 200
173+
? team.project.description.substring(0, 200) + "..."
174+
: team.project.description}
157175
</p>
158176

159177
{/* Links */}
@@ -166,7 +184,7 @@ function parseMembers(membersString: string): Array<{name: string, url?: string,
166184
rel="noopener noreferrer"
167185
class="inline-flex items-center gap-2 px-3 py-2 bg-gray-800 hover:bg-[#19806f] text-white text-sm rounded-lg transition-colors"
168186
>
169-
<iconify-icon icon={link.icon} class="text-lg"></iconify-icon>
187+
<iconify-icon icon={link.icon} class="text-lg" />
170188
{link.label}
171189
</a>
172190
))}
@@ -176,12 +194,15 @@ function parseMembers(membersString: string): Array<{name: string, url?: string,
176194
{/* Manorah Commentary */}
177195
<div class="border-t border-gray-800 pt-4 mt-4">
178196
<div class="flex items-start gap-3">
179-
<iconify-icon
180-
icon="mdi:drama-masks"
197+
<iconify-icon
198+
icon="mdi:drama-masks"
181199
class="text-[#19806f] text-xl mt-1 flex-shrink-0"
182-
></iconify-icon>
200+
/>
183201
<div>
184-
<h4 class="text-[#19806f] font-semibold text-sm mb-2" style="font-family: 'K2D', sans-serif;">
202+
<h4
203+
class="text-[#19806f] font-semibold text-sm mb-2"
204+
style="font-family: 'K2D', sans-serif;"
205+
>
185206
มโนราห์ว่าไง
186207
</h4>
187208
<p class="text-gray-300 text-sm leading-relaxed italic">
@@ -197,4 +218,70 @@ function parseMembers(membersString: string): Array<{name: string, url?: string,
197218
}
198219
</div>
199220
</div>
200-
</section>
221+
</section>
222+
223+
<script>
224+
document.addEventListener("DOMContentLoaded", () => {
225+
const copyButtons = document.querySelectorAll(".copy-permalink-button");
226+
227+
copyButtons.forEach((button) => {
228+
button.addEventListener("click", async (e) => {
229+
e.preventDefault();
230+
const button = e.currentTarget;
231+
if (!(button instanceof HTMLElement)) return;
232+
233+
const slug = button.dataset.slug;
234+
if (!slug) return;
235+
236+
const url = `${window.location.href.split("#")[0]}#${slug}`;
237+
238+
try {
239+
if (navigator.clipboard && window.isSecureContext) {
240+
await navigator.clipboard.writeText(url);
241+
} else {
242+
const textarea = document.createElement("textarea");
243+
textarea.value = url;
244+
textarea.setAttribute("readonly", "");
245+
textarea.style.position = "absolute";
246+
textarea.style.left = "-9999px";
247+
document.body.appendChild(textarea);
248+
const selection = document.getSelection();
249+
const selected =
250+
selection && selection.rangeCount > 0
251+
? selection.getRangeAt(0)
252+
: null;
253+
textarea.select();
254+
try {
255+
document.execCommand("copy");
256+
} finally {
257+
document.body.removeChild(textarea);
258+
if (selected && selection) {
259+
selection.removeAllRanges();
260+
selection.addRange(selected);
261+
}
262+
}
263+
}
264+
265+
// Provide visual feedback
266+
const icon = button.querySelector("iconify-icon");
267+
if (icon) {
268+
const originalIcon = icon.getAttribute("icon");
269+
icon.setAttribute("icon", "mdi:check");
270+
button.setAttribute("title", "Copied!");
271+
button.setAttribute("aria-label", "Copied!");
272+
273+
setTimeout(() => {
274+
icon.setAttribute("icon", originalIcon || "mdi:link-variant");
275+
button.setAttribute("title", "Copy permalink");
276+
button.setAttribute("aria-label", "Copy permalink");
277+
}, 1500);
278+
}
279+
} catch (err) {
280+
console.error("Failed to copy: ", err);
281+
button.setAttribute("title", "Failed to copy");
282+
button.setAttribute("aria-label", "Failed to copy");
283+
}
284+
});
285+
});
286+
});
287+
</script>

0 commit comments

Comments
 (0)