1/** 2 * Donation UI used by localized Strapi pages. 3 * 4 * Required elements: 5 * - #paypal-button-container 6 * - .donation-option[data-amount] 7 * - #donation-custom (optional) 8 * - #donation-message (optional) 9 * - #donation-error (optional) 10 */ 11(function () { 12 'use strict'; 13 14 const MIN_CUSTOM_DONATION = 5; 15 const CONFIG_URL = '/api/donate/config'; 16 const CONVERSION_URL = '/api/donate/conversion'; 17 const CONFIRMATION_STORAGE_KEY = 'medjugorjeDonationConfirmation'; 18 const CONFIRMATION_MAX_AGE_MS = 30 * 60 * 1000; 19 const COPY = { 20 hr: { 21 invalid: 'Unesite iznos donacije od najmanje â¬{amount}.', 22 success: 'Hvala! VaÅ¡a donacija od â¬{amount} je uspjeÅ¡no zaprimljena.', 23 unavailable: 'PayPal trenutno nije dostupan. PokuÅ¡ajte ponovno.' 24 }, 25 en: { 26 invalid: 'Please enter a donation amount of at least â¬{amount}.', 27 success: 'Thank you! Your donation of â¬{amount} has been received.', 28 unavailable: 'PayPal is currently unavailable. Please try again.' 29 }, 30 de: { 31 invalid: 'Bitte geben Sie einen Spendenbetrag von mindestens â¬{amount} ein.', 32 success: 'Vielen Dank! Ihre Spende von â¬{amount} ist eingegangen.', 33 unavailable: 'PayPal ist derzeit nicht verfügbar. Bitte versuchen Sie es erneut.' 34 }, 35 it: { 36 invalid: 'Inserisci un importo di almeno â¬{amount}.', 37 success: 'Grazie! La tua donazione di â¬{amount} è stata ricevuta.', 38 unavailable: 'PayPal non è al momento disponibile. Riprova.' 39 }, 40 es: { 41 invalid: 'Introduce una donación de al menos â¬{amount}.', 42 success: '¡Gracias! Hemos recibido tu donación de â¬{amount}.', 43 unavailable: 'PayPal no está disponible en este momento. Inténtalo de nuevo.' 44 }, 45 fr: { 46 invalid: 'Veuillez saisir un don dâau moins â¬{amount}.', 47 success: 'Merci ! Votre don de â¬{amount} a bien été reçu.', 48 unavailable: 'PayPal est actuellement indisponible. Veuillez réessayer.' 49 }, 50 pl: { 51 invalid: 'Wpisz kwotÄ darowizny wynoszÄ cÄ co najmniej â¬{amount}.', 52 success: 'DziÄkujemy! OtrzymaliÅmy TwojÄ darowiznÄ w wysokoÅci â¬{amount}.', 53 unavailable: 'PayPal jest obecnie niedostÄpny. Spróbuj ponownie.' 54 }, 55 hu: { 56 invalid: 'Adjon meg legalább â¬{amount} összegű adományt.', 57 success: 'Köszönjük! A(z) â¬{amount} összegű adomány megérkezett.', 58 unavailable: 'A PayPal jelenleg nem érhetÅ el. Próbálja újra.' 59 }, 60 ar: { 61 invalid: 'ÙØ±Ø¬Ù Ø¥Ø¯Ø®Ø§Ù Ù Ø¨ÙØº تبرع ÙØ§ ÙÙ٠ع٠â¬{amount}.', 62 success: 'Ø´ÙØ±Ùا ÙÙ! ØªÙ Ø§Ø³ØªÙØ§Ù تبرع٠بÙÙÙ Ø© â¬{amount}.', 63 unavailable: 'PayPal ØºÙØ± Ù ØªØ§Ø ØØ§ÙÙÙØ§. ÙØ±Ø¬Ù اÙÙ ØØ§ÙÙØ© ٠رة أخرÙ.' 64 }, 65 zh: { 66 invalid: '请è¾å ¥è³å° â¬{amount} çææ¬¾éé¢ã', 67 success: 'è°¢è°¢ï¼æä»¬å·²æ¶å°æ¨ â¬{amount} çææ¬¾ã', 68 unavailable: 'PayPal ææ¶ä¸å¯ç¨ï¼è¯·éè¯ã' 69 } 70 }; 71 72 let paypalSdkPromise; 73 74 function getElement(id) { 75 return document.getElementById(id); 76 } 77 78 function getCopy() { 79 const locale = (document.documentElement.lang || 'en').split('-')[0].toLowerCase(); 80 return COPY[locale] || COPY.en; 81 } 82 83 function formatMessage(template, amount) { 84 return template.replace('{amount}', Number(amount).toFixed(2)); 85 } 86 87 async function getDonationConfig() { 88 const response = await fetch(CONFIG_URL, { 89 credentials: 'same-origin', 90 headers: { 91 Accept: 'application/json' 92 } 93 }); 94 95 if (!response.ok) throw new Error('Donation configuration is unavailable.'); 96 return response.json(); 97 } 98 99 function loadPaypalSdk(config) { 100 if (window.paypal) return Promise.resolve(window.paypal); 101 if (paypalSdkPromise) return paypalSdkPromise; 102 103 const params = new URLSearchParams({ 104 'client-id': config.paypalClientId, 105 currency: config.currency, 106 intent: 'capture', 107 components: 'buttons' 108 }); 109 const sdkSrc = `https://www.paypal.com/sdk/js?${params}`; 110 111 paypalSdkPromise = new Promise((resolve, reject) => { 112 const existing = document.querySelector('script[data-medjugorje-paypal-sdk]'); 113 if (existing) { 114 existing.addEventListener('load', () => resolve(window.paypal), { once: true }); 115 existing.addEventListener('error', reject, { once: true }); 116 return; 117 } 118 119 const script = document.createElement('script'); 120 script.src = sdkSrc; 121 script.async = true; 122 script.dataset.medjugorjePaypalSdk = 'true'; 123 script.onload = () => { 124 if (window.paypal) resolve(window.paypal); 125 else reject(new Error('PayPal SDK unavailable.')); 126 }; 127 script.onerror = reject; 128 document.head.appendChild(script); 129 }); 130
131 return paypalSdkPromise; 132 } 133 134 async function reportDonationConversion(orderId) { 135 if (!orderId) return; 136 137 const response = await fetch(CONVERSION_URL, { 138 method: 'POST', 139 credentials: 'same-origin', 140 keepalive: true, 141 headers: { 142 'Content-Type': 'application/json' 143 }, 144 body: JSON.stringify({ 145 order_id: orderId 146 }) 147 }); 148 149 if (!response.ok) { 150 throw new Error(`Conversion endpoint returned ${response.status}.`); 151 } 152 } 153 154 function showMessage(element, message, bringIntoView) { 155 if (!element) return false; 156 element.textContent = message; 157 element.setAttribute('role', 'status'); 158 element.setAttribute('aria-live', 'polite'); 159 element.setAttribute('aria-atomic', 'true'); 160 element.setAttribute('tabindex', '-1'); 161 element.classList.remove('hidden'); 162 163 if (bringIntoView) { 164 window.requestAnimationFrame(() => { 165 element.scrollIntoView({ 166 behavior: 'smooth', 167 block: 'center' 168 }); 169 element.focus({ preventScroll: true }); 170 }); 171 } 172 173 return true; 174 } 175 176 function storeDonationConfirmation(amount) { 177 try { 178 window.sessionStorage.setItem( 179 CONFIRMATION_STORAGE_KEY, 180 JSON.stringify({ 181 amount: Number(amount), 182 createdAt: Date.now() 183 }) 184 ); 185 } catch { 186 // Storage can be unavailable due to browser privacy settings. 187 } 188 } 189 190 function clearDonationConfirmation() { 191 try { 192 window.sessionStorage.removeItem(CONFIRMATION_STORAGE_KEY); 193 } catch { 194 // Storage can be unavailable due to browser privacy settings. 195 } 196 } 197 198 function restoreDonationConfirmation(element, copy) { 199 try { 200 const raw = window.sessionStorage.getItem(CONFIRMATION_STORAGE_KEY); 201 if (!raw) return false; 202 203 const confirmation = JSON.parse(raw); 204 const amount = Number(confirmation?.amount); 205 const createdAt = Number(confirmation?.createdAt); 206 const isRecent = Number.isFinite(createdAt) 207 && Date.now() - createdAt <= CONFIRMATION_MAX_AGE_MS; 208 209 if (!isRecent || !Number.isFinite(amount) || amount <= 0) { 210 clearDonationConfirmation(); 211 return false; 212 } 213 214 return showMessage( 215 element, 216 formatMessage(copy.success, amount), 217 false 218 ); 219 } catch { 220 clearDonationConfirmation(); 221 return false; 222 } 223 } 224 225 async function initDonationUI() { 226 const paypalContainer = getElement('paypal-button-container'); 227 if (!paypalContainer || paypalContainer.dataset.donationInitialized === 'true') return; 228 229 paypalContainer.dataset.donationInitialized = 'true'; 230 231 const presetButtons = Array.from(document.querySelectorAll('.donation-option')); 232 const customAmountInput = getElement('donation-custom'); 233 const messageElement = getElement('donation-message'); 234 const errorElement = getElement('donation-error'); 235 const copy = getCopy(); 236 restoreDonationConfirmation(messageElement, copy); 237 let selectedAmount = Number(presetButtons[0]?.dataset.amount || 5); 238 let paypalButtons; 239 let inputTimer; 240 241 function isCustomMode() { 242 return Boolean(customAmountInput?.value); 243 } 244 245 function getValidAmount() { 246 const minimum = isCustomMode() ? MIN_CUSTOM_DONATION : 1; 247 return Number.isFinite(selectedAmount) && selectedAmount >= minimum 248 ? selectedAmount 249 : 0; 250 } 251 252 function toggleError(show, message) { 253 if (!errorElement) return; 254 if (message) errorElement.textContent = message; 255 errorElement.classList.toggle('hidden', !show); 256 } 257 258 function resetFeedback() { 259 toggleError(false); 260 clearDonationConfirmation(); 261 if (messageElement) { 262 messageElement.textContent = ''; 263 messageElement.classList.add('hidden'); 264 } 265 } 266 267 function setActiveButton(target) { 268 presetButtons.forEach((button) => { 269 button.classList.remove('bg-blue-500', 'text-white', 'shadow'); 270 button.classList.add('bg-blue-100', 'text-blue-700', 'hover:bg-blue-200'); 271 }); 272 273 if (target) { 274 target.classList.add('bg-blue-500', 'text-white', 'shadow'); 275 target.classList.remove('bg-blue-100', 'text-blue-700', 'hover:bg-blue-200'); 276 } 277 } 278 279 try { 280 const config = await getDonationConfig(); 281 const paypal = await loadPaypalSdk(config); 282 283 async function renderPaypalButtons(force) { 284 if (paypalButtons && !force) return; 285 286 if (paypalButtons && force) { 287 if (typeof paypalButtons.close === 'function') await paypalButtons.close(); 288 paypalButtons = undefined; 289 paypalContainer.replaceChildren(); 290 } 291 292 paypalButtons = paypal.Buttons({ 293 style: { 294 layout: 'vertical', 295 color: 'gold', 296 shape: 'rect', 297 label: 'donate' 298 }, 299 createOrder: (_data, actions) => { 300 const amount = getValidAmount(); 301 if (!amount) { 302 const minimum = isCustomMode() ? MIN_CUSTOM_DONATION : 1; 303 const message = formatMessage(copy.invalid, minimum); 304 toggleError(true, message); 305 window.alert(message); 306 return Promise.reject(new Error('Invalid donation amount.')); 307 } 308 309 toggleError(false); 310 return actions.order.create({ 311 application_context: { 312 shipping_preference: 'NO_SHIPPING' 313 }, 314 purchase_units: [ 315 { 316 amount: { 317 currency_code: config.currency, 318 value: amount.toFixed(2) 319 } 320 } 321 ] 322 }); 323 }, 324 onApprove: async (data, actions) => { 325 const details = await actions.order.capture(); 326 const orderId = data.orderID || details?.id; 327 const verifiedAmount = Number( 328 details?.purchase_units?.[0]?.payments?.captures?.[0]?.amount?.value 329 ); 330 const amount = Number.isFinite(verifiedAmount) 331 ? verifiedAmount 332 : getValidAmount(); 333 334 storeDonationConfirmation(amount); 335 showMessage( 336 messageElement, 337 formatMessage(copy.success, amount), 338 true 339 ); 340 341 reportDonationConversion(orderId) 342 .catch(error => console.error('Failed to report donation conversion.', error)); 343 }, 344 onError: (error) => { 345 console.error('PayPal donation failed.', error); 346 toggleError(true, copy.unavailable); 347 } 348 }); 349 350 await paypalButtons.render(paypalContainer); 351 } 352 353 presetButtons.forEach((button) => { 354 button.addEventListener('click', () => { 355 selectedAmount = Number(button.dataset.amount || 0); 356 if (customAmountInput) customAmountInput.value = '';
357 setActiveButton(button); 358 resetFeedback(); 359 renderPaypalButtons(true) 360 .catch(error => console.error('Failed to refresh PayPal buttons.', error)); 361 }); 362 }); 363 364 customAmountInput?.addEventListener('input', () => { 365 const value = Number.parseFloat(customAmountInput.value); 366 const hasValue = customAmountInput.value !== ''; 367 const valid = Number.isFinite(value) && value >= MIN_CUSTOM_DONATION; 368 369 selectedAmount = valid ? value : 0; 370 setActiveButton(); 371 resetFeedback(); 372 toggleError( 373 hasValue && !valid, 374 formatMessage(copy.invalid, MIN_CUSTOM_DONATION) 375 ); 376 377 window.clearTimeout(inputTimer); 378 inputTimer = window.setTimeout(() => { 379 renderPaypalButtons(true) 380 .catch(error => console.error('Failed to refresh PayPal buttons.', error)); 381 }, 250); 382 }); 383 384 await renderPaypalButtons(false); 385 } catch (error) { 386 console.error('Failed to initialize donation UI.', error); 387 toggleError(true, copy.unavailable); 388 delete paypalContainer.dataset.donationInitialized; 389 } 390 } 391 392 window.initDonationUI = initDonationUI; 393 394 if (document.readyState === 'loading') { 395 document.addEventListener('DOMContentLoaded', initDonationUI, { once: true }); 396 } else { 397 initDonationUI(); 398 } 399})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.