PageSourceSearch

https://medjugorje.hr/strapi-donation-logic.js

js medjugorje.hr collected 2026-10-02 06:29:47 UTC 13,138 bytes, 399 lines download raw bytes

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.