@@ -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