|
21 | 21 | if (navCloseBtn) {
|
22 | 22 | navCloseBtn.addEventListener("click", toggleNav);
|
23 | 23 | }
|
24 |
| - |
| 24 | + |
25 | 25 | // Close on Escape key
|
26 | 26 | document.addEventListener('keydown', (e) => {
|
27 | 27 | if (e.key === 'Escape' && body.classList.contains('is-mobile-nav-open')) {
|
|
44 | 44 |
|
45 | 45 | let bubblePool = [];
|
46 | 46 | for (let i = 0; i < MAX_BUBBLES; i++) {
|
47 |
| - let bubble = document.createElement('div'); |
48 |
| - bubble.className = 'contribution-bubble'; |
49 |
| - bubble.dataset.busy = 'false'; |
50 |
| - bubbleContainer.appendChild(bubble); |
51 |
| - bubblePool.push(bubble); |
| 47 | + let bubble = document.createElement('div'); |
| 48 | + bubble.className = 'contribution-bubble'; |
| 49 | + bubble.dataset.busy = 'false'; |
| 50 | + bubbleContainer.appendChild(bubble); |
| 51 | + bubblePool.push(bubble); |
52 | 52 | }
|
53 |
| - |
| 53 | + |
54 | 54 | let activeBubbles = new Map();
|
55 | 55 |
|
56 | 56 | const updateBubblePositions = () => {
|
57 | 57 | activeBubbles.forEach((bubble, partner) => {
|
58 | 58 | const partnerRect = partner.getBoundingClientRect();
|
59 | 59 | const containerRect = bubbleContainer.getBoundingClientRect();
|
60 |
| - |
| 60 | + |
61 | 61 | const left = partnerRect.left - containerRect.left + (partnerRect.width / 2) - (bubble.offsetWidth / 2);
|
62 | 62 | const top = partnerRect.top - containerRect.top - bubble.offsetHeight - 10;
|
63 | 63 |
|
64 | 64 | bubble.style.transform = `translate(${left}px, ${top}px)`;
|
65 | 65 | });
|
66 | 66 | requestAnimationFrame(updateBubblePositions);
|
67 | 67 | };
|
68 |
| - |
| 68 | + |
69 | 69 | requestAnimationFrame(updateBubblePositions);
|
70 | 70 |
|
71 | 71 | const showBubble = (partner) => {
|
72 |
| - const bubble = bubblePool.find(b => b.dataset.busy === 'false'); |
73 |
| - if (!bubble) return; |
74 |
| - |
75 |
| - bubble.dataset.busy = 'true'; |
76 |
| - activeBubbles.set(partner, bubble); |
77 |
| - |
78 |
| - bubble.innerHTML = partner.dataset.contributions; |
79 |
| - bubble.classList.add('visible'); |
80 |
| - |
81 |
| - setTimeout(() => { |
82 |
| - hideBubble(partner, bubble); |
83 |
| - }, BUBBLE_LIFETIME); |
| 72 | + const bubble = bubblePool.find(b => b.dataset.busy === 'false'); |
| 73 | + if (!bubble) return; |
| 74 | + |
| 75 | + bubble.dataset.busy = 'true'; |
| 76 | + activeBubbles.set(partner, bubble); |
| 77 | + |
| 78 | + bubble.innerHTML = partner.dataset.contributions; |
| 79 | + bubble.classList.add('visible'); |
| 80 | + |
| 81 | + setTimeout(() => { |
| 82 | + hideBubble(partner, bubble); |
| 83 | + }, BUBBLE_LIFETIME); |
84 | 84 | };
|
85 | 85 |
|
86 | 86 | const hideBubble = (partner, bubble) => {
|
87 |
| - bubble.classList.remove('visible'); |
88 |
| - activeBubbles.delete(partner); |
89 |
| - |
90 |
| - setTimeout(() => { |
91 |
| - bubble.dataset.busy = 'false'; |
92 |
| - }, 300); |
| 87 | + bubble.classList.remove('visible'); |
| 88 | + activeBubbles.delete(partner); |
| 89 | + |
| 90 | + setTimeout(() => { |
| 91 | + bubble.dataset.busy = 'false'; |
| 92 | + }, 300); |
93 | 93 | };
|
94 | 94 |
|
95 | 95 | setInterval(() => {
|
96 |
| - if (activeBubbles.size >= MAX_BUBBLES) return; |
| 96 | + if (activeBubbles.size >= MAX_BUBBLES) return; |
97 | 97 |
|
98 |
| - const rect = section.getBoundingClientRect(); |
99 |
| - const isInView = rect.top < window.innerHeight && rect.bottom >= 0; |
| 98 | + const rect = section.getBoundingClientRect(); |
| 99 | + const isInView = rect.top < window.innerHeight && rect.bottom >= 0; |
100 | 100 |
|
101 |
| - if (isInView) { |
102 |
| - const availablePartners = partnersWithContributions.filter(p => { |
103 |
| - if (activeBubbles.has(p)) return false; |
104 |
| - const r = p.getBoundingClientRect(); |
105 |
| - return r.left > 50 && r.right < window.innerWidth - 50; |
106 |
| - }); |
| 101 | + if (isInView) { |
| 102 | + const availablePartners = partnersWithContributions.filter(p => { |
| 103 | + if (activeBubbles.has(p)) return false; |
| 104 | + const r = p.getBoundingClientRect(); |
| 105 | + return r.left > 50 && r.right < window.innerWidth - 50; |
| 106 | + }); |
107 | 107 |
|
108 |
| - if (availablePartners.length > 0) { |
109 |
| - const randomIndex = Math.floor(Math.random() * availablePartners.length); |
110 |
| - showBubble(availablePartners[randomIndex]); |
111 |
| - } |
| 108 | + if (availablePartners.length > 0) { |
| 109 | + const randomIndex = Math.floor(Math.random() * availablePartners.length); |
| 110 | + showBubble(availablePartners[randomIndex]); |
112 | 111 | }
|
113 |
| - }, BUBBLE_INTERVAL); |
| 112 | + } |
| 113 | + }, BUBBLE_INTERVAL); |
114 | 114 | };
|
115 |
| - |
| 115 | + |
116 | 116 | // --- Copy Code Block ---
|
117 | 117 | const initCopyCodeButtons = () => {
|
118 | 118 | const codeBlocks = document.querySelectorAll('.code-block-wrapper');
|
|
148 | 148 | const modalContent = document.getElementById('og-modal-content');
|
149 | 149 | const closeBtn = document.getElementById('og-modal-close');
|
150 | 150 | if (!modal || !modalContent || !closeBtn) return;
|
151 |
| - |
| 151 | + |
152 | 152 | const showModal = () => {
|
153 | 153 | modal.classList.remove('hidden');
|
154 | 154 | modal.classList.add('flex');
|
155 | 155 | };
|
156 |
| - |
| 156 | + |
157 | 157 | const hideModal = () => {
|
158 | 158 | modal.classList.add('hidden');
|
159 | 159 | modal.classList.remove('flex');
|
160 | 160 | };
|
161 |
| - |
| 161 | + |
162 | 162 | closeBtn.addEventListener('click', hideModal);
|
163 | 163 | modal.addEventListener('click', (e) => {
|
164 | 164 | if (e.target === modal) {
|
|
170 | 170 | hideModal();
|
171 | 171 | }
|
172 | 172 | });
|
173 |
| - |
| 173 | + |
174 | 174 | window.openOgModal = (imageUrls) => {
|
175 | 175 | modalContent.innerHTML = ''; // Clear previous content
|
176 |
| - |
| 176 | + |
177 | 177 | if (!imageUrls || imageUrls.length === 0) {
|
178 | 178 | modalContent.innerHTML = '<p class="text-center col-span-full">No social media assets found for this page.</p>';
|
179 | 179 | } else {
|
|
185 | 185 | <img src="${img.url}" alt="OG Image Preview for ${img.label}" class="w-full rounded-md border border-border dark:border-darkmode-border shadow-md hover:shadow-lg transition-shadow">
|
186 | 186 | </a>
|
187 | 187 | <div class="mt-3">
|
188 |
| - <button class="copy-og-url-btn btn btn-sm btn-outline-primary" data-url="${img.url}"> |
189 |
| - <span class="copy-text">Copy URL</span> |
| 188 | + <button class="copy-og-image-btn btn btn-sm btn-outline-primary" data-url="${img.url}"> |
| 189 | + <span class="copy-text">Copy Image</span> |
190 | 190 | <span class="copied-text hidden">Copied!</span>
|
191 | 191 | </button>
|
192 | 192 | </div>
|
|
195 | 195 | modalContent.insertAdjacentHTML('beforeend', cardHTML);
|
196 | 196 | });
|
197 | 197 | }
|
198 |
| - |
199 |
| - modalContent.querySelectorAll('.copy-og-url-btn').forEach(button => { |
| 198 | + |
| 199 | + modalContent.querySelectorAll('.copy-og-image-btn').forEach(button => { |
200 | 200 | button.addEventListener('click', () => {
|
201 | 201 | const urlToCopy = button.dataset.url;
|
202 |
| - navigator.clipboard.writeText(urlToCopy).then(() => { |
203 |
| - button.classList.add('copied'); |
204 |
| - button.querySelector('.copy-text').classList.add('hidden'); |
205 |
| - button.querySelector('.copied-text').classList.remove('hidden'); |
206 |
| - |
207 |
| - setTimeout(() => { |
208 |
| - button.classList.remove('copied'); |
209 |
| - button.querySelector('.copy-text').classList.remove('hidden'); |
210 |
| - button.querySelector('.copied-text').classList.add('hidden'); |
211 |
| - }, 2000); |
212 |
| - }).catch(err => { |
213 |
| - console.error('Failed to copy URL: ', err); |
214 |
| - alert('Failed to copy URL to clipboard.'); |
215 |
| - }); |
| 202 | + fetch(urlToCopy) |
| 203 | + .then(response => response.blob()) |
| 204 | + .then(blob => { |
| 205 | + const item = new ClipboardItem({ [blob.type]: blob }); |
| 206 | + return navigator.clipboard.write([item]); |
| 207 | + }) |
| 208 | + .then(() => { |
| 209 | + button.classList.add('copied'); |
| 210 | + button.querySelector('.copy-text').classList.add('hidden'); |
| 211 | + button.querySelector('.copied-text').classList.remove('hidden'); |
| 212 | + setTimeout(() => { |
| 213 | + button.classList.remove('copied'); |
| 214 | + button.querySelector('.copy-text').classList.remove('hidden'); |
| 215 | + button.querySelector('.copied-text').classList.add('hidden'); |
| 216 | + }, 2000); |
| 217 | + }) |
| 218 | + .catch(err => { |
| 219 | + console.error('Failed to copy image: ', err); |
| 220 | + alert('Failed to copy image to clipboard. This feature requires a secure connection (HTTPS) and may need browser permissions.'); |
| 221 | + }); |
216 | 222 | });
|
217 | 223 | });
|
218 |
| - |
| 224 | + |
219 | 225 | showModal();
|
220 | 226 | };
|
221 |
| - |
| 227 | + |
222 | 228 | document.querySelectorAll('[data-og-assets]').forEach(trigger => {
|
223 |
| - trigger.addEventListener('click', (e) => { |
224 |
| - e.preventDefault(); |
225 |
| - const assetsJson = trigger.dataset.ogAssets; |
226 |
| - try { |
227 |
| - const assets = JSON.parse(assetsJson); |
228 |
| - window.openOgModal(assets); |
229 |
| - } catch (error) { |
230 |
| - console.error("Could not parse OG assets JSON:", error); |
231 |
| - window.openOgModal([]); |
232 |
| - } |
233 |
| - }); |
| 229 | + trigger.addEventListener('click', (e) => { |
| 230 | + e.preventDefault(); |
| 231 | + const assetsJson = trigger.dataset.ogAssets; |
| 232 | + try { |
| 233 | + const assets = JSON.parse(assetsJson); |
| 234 | + window.openOgModal(assets); |
| 235 | + } catch (error) { |
| 236 | + console.error("Could not parse OG assets JSON:", error); |
| 237 | + window.openOgModal([]); |
| 238 | + } |
| 239 | + }); |
234 | 240 | });
|
235 | 241 | };
|
236 | 242 |
|
237 | 243 | // Wait for the DOM to be fully loaded to initialize
|
238 | 244 | if (document.readyState === 'loading') {
|
239 | 245 | document.addEventListener('DOMContentLoaded', () => {
|
240 |
| - initCopyCodeButtons(); |
241 |
| - initOgImageModal(); |
242 |
| - if (window.innerWidth > 768) { // Only run on larger screens |
243 |
| - initContributionBubbles(); |
244 |
| - } |
| 246 | + initCopyCodeButtons(); |
| 247 | + initOgImageModal(); |
| 248 | + if (window.innerWidth > 768) { // Only run on larger screens |
| 249 | + initContributionBubbles(); |
| 250 | + } |
245 | 251 | });
|
246 | 252 | } else {
|
247 | 253 | initCopyCodeButtons();
|
248 | 254 | initOgImageModal();
|
249 | 255 | if (window.innerWidth > 768) { // Only run on larger screens
|
250 |
| - initContributionBubbles(); |
| 256 | + initContributionBubbles(); |
251 | 257 | }
|
252 | 258 | }
|
253 | 259 |
|
|
0 commit comments