Case Submission Portal (Admin Moderation Queue): - Add statistics endpoint (GET /api/cases/submissions/stats) - Enhance filtering: status, failure_mode, AI relevance score - Add sorting options: date, relevance, completeness - Create admin moderation interface (case-moderation.html) - Implement CSP-compliant admin UI (no inline event handlers) - Deploy moderation actions: approve, reject, request-info - Fix API parameter mapping for different action types Internationalization (i18n): - Implement lightweight i18n system (i18n-simple.js, ~5KB) - Add language selector component with flag emojis - Create German and French translations for homepage - Document Te Reo Māori translation requirements - Add i18n attributes to homepage - Integrate language selector into navbar Bug Fixes: - Fix search button modal display on docs.html (remove conflicting flex class) Page Enhancements: - Add dedicated JS modules for researcher, leader, koha pages - Improve page-specific functionality and interactions Documentation: - Add I18N_IMPLEMENTATION_SUMMARY.md (implementation guide) - Add TE_REO_MAORI_TRANSLATION_REQUIREMENTS.md (cultural sensitivity guide) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
289 lines
9.1 KiB
JavaScript
289 lines
9.1 KiB
JavaScript
/**
|
|
* Koha Donation System
|
|
* Handles donation form functionality with CSP compliance
|
|
*/
|
|
|
|
// Form state
|
|
let selectedFrequency = 'monthly';
|
|
let selectedTier = '15';
|
|
let selectedAmount = 1500; // in cents (NZD)
|
|
let currentCurrency = typeof detectUserCurrency === 'function' ? detectUserCurrency() : 'NZD';
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize event listeners
|
|
initializeFrequencyButtons();
|
|
initializeTierCards();
|
|
initializePublicAcknowledgement();
|
|
initializeDonationForm();
|
|
});
|
|
|
|
/**
|
|
* Initialize frequency selection buttons
|
|
*/
|
|
function initializeFrequencyButtons() {
|
|
const monthlyBtn = document.getElementById('freq-monthly');
|
|
const onetimeBtn = document.getElementById('freq-onetime');
|
|
|
|
if (monthlyBtn) {
|
|
monthlyBtn.addEventListener('click', function() {
|
|
selectFrequency('monthly');
|
|
});
|
|
}
|
|
|
|
if (onetimeBtn) {
|
|
onetimeBtn.addEventListener('click', function() {
|
|
selectFrequency('one_time');
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize tier card click handlers
|
|
*/
|
|
function initializeTierCards() {
|
|
const tierCards = document.querySelectorAll('[data-tier]');
|
|
|
|
tierCards.forEach(card => {
|
|
card.addEventListener('click', function() {
|
|
const tier = this.dataset.tier;
|
|
const amount = parseInt(this.dataset.amount);
|
|
selectTier(tier, amount);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize public acknowledgement checkbox
|
|
*/
|
|
function initializePublicAcknowledgement() {
|
|
const checkbox = document.getElementById('public-acknowledgement');
|
|
|
|
if (checkbox) {
|
|
checkbox.addEventListener('change', togglePublicName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize donation form submission
|
|
*/
|
|
function initializeDonationForm() {
|
|
const form = document.getElementById('donation-form');
|
|
|
|
if (form) {
|
|
form.addEventListener('submit', handleFormSubmit);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update prices when currency changes
|
|
*/
|
|
window.updatePricesForCurrency = function(currency) {
|
|
currentCurrency = currency;
|
|
|
|
if (typeof getTierPrices !== 'function' || typeof formatCurrency !== 'function') {
|
|
console.warn('Currency utilities not loaded');
|
|
return;
|
|
}
|
|
|
|
const prices = getTierPrices(currency);
|
|
|
|
// Update tier card prices
|
|
const tierCards = document.querySelectorAll('.tier-card');
|
|
if (tierCards[0]) {
|
|
const priceEl = tierCards[0].querySelector('.text-4xl');
|
|
const currencyEl = tierCards[0].querySelector('.text-sm.text-gray-500');
|
|
if (priceEl) priceEl.textContent = formatCurrency(prices.tier_5, currency).replace(/\.\d+$/, '');
|
|
if (currencyEl) currencyEl.textContent = `${currency} / month`;
|
|
}
|
|
|
|
if (tierCards[1]) {
|
|
const priceEl = tierCards[1].querySelector('.text-4xl');
|
|
const currencyEl = tierCards[1].querySelector('.text-sm.text-gray-500');
|
|
if (priceEl) priceEl.textContent = formatCurrency(prices.tier_15, currency).replace(/\.\d+$/, '');
|
|
if (currencyEl) currencyEl.textContent = `${currency} / month`;
|
|
}
|
|
|
|
if (tierCards[2]) {
|
|
const priceEl = tierCards[2].querySelector('.text-4xl');
|
|
const currencyEl = tierCards[2].querySelector('.text-sm.text-gray-500');
|
|
if (priceEl) priceEl.textContent = formatCurrency(prices.tier_50, currency).replace(/\.\d+$/, '');
|
|
if (currencyEl) currencyEl.textContent = `${currency} / month`;
|
|
}
|
|
|
|
// Update custom amount placeholder
|
|
const amountInput = document.getElementById('amount-input');
|
|
if (amountInput) {
|
|
amountInput.placeholder = 'Enter amount';
|
|
const amountCurrencyLabel = amountInput.nextElementSibling;
|
|
if (amountCurrencyLabel) {
|
|
amountCurrencyLabel.textContent = currency;
|
|
}
|
|
}
|
|
|
|
// Update help text
|
|
const amountHelp = document.getElementById('amount-help');
|
|
if (amountHelp && typeof formatCurrency === 'function') {
|
|
amountHelp.textContent = `Minimum donation: ${formatCurrency(100, currency)}`;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Select donation frequency (monthly or one-time)
|
|
*/
|
|
function selectFrequency(freq) {
|
|
selectedFrequency = freq;
|
|
|
|
// Update button styles
|
|
const monthlyBtn = document.getElementById('freq-monthly');
|
|
const onetimeBtn = document.getElementById('freq-onetime');
|
|
|
|
if (freq === 'monthly') {
|
|
monthlyBtn.className = 'flex-1 py-3 px-6 border-2 border-blue-600 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition';
|
|
onetimeBtn.className = 'flex-1 py-3 px-6 border-2 border-gray-300 bg-white text-gray-700 rounded-lg font-semibold hover:border-blue-600 transition';
|
|
|
|
// Show tier selection, hide custom amount
|
|
const tierSelection = document.getElementById('tier-selection');
|
|
const customAmount = document.getElementById('custom-amount');
|
|
if (tierSelection) tierSelection.classList.remove('hidden');
|
|
if (customAmount) customAmount.classList.add('hidden');
|
|
} else {
|
|
monthlyBtn.className = 'flex-1 py-3 px-6 border-2 border-gray-300 bg-white text-gray-700 rounded-lg font-semibold hover:border-blue-600 transition';
|
|
onetimeBtn.className = 'flex-1 py-3 px-6 border-2 border-blue-600 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition';
|
|
|
|
// Hide tier selection, show custom amount
|
|
const tierSelection = document.getElementById('tier-selection');
|
|
const customAmount = document.getElementById('custom-amount');
|
|
if (tierSelection) tierSelection.classList.add('hidden');
|
|
if (customAmount) customAmount.classList.remove('hidden');
|
|
|
|
const amountInput = document.getElementById('amount-input');
|
|
if (amountInput) amountInput.focus();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Select tier amount
|
|
*/
|
|
function selectTier(tier, amountNZDCents) {
|
|
selectedTier = tier;
|
|
|
|
// Calculate amount in current currency
|
|
if (typeof getTierPrices === 'function') {
|
|
const prices = getTierPrices(currentCurrency);
|
|
selectedAmount = prices[`tier_${tier}`];
|
|
} else {
|
|
selectedAmount = amountNZDCents;
|
|
}
|
|
|
|
// Update card styles
|
|
const cards = document.querySelectorAll('.tier-card');
|
|
cards.forEach(card => {
|
|
card.classList.remove('selected');
|
|
});
|
|
|
|
// Add selected class to clicked card
|
|
const selectedCard = document.querySelector(`[data-tier="${tier}"]`);
|
|
if (selectedCard) {
|
|
selectedCard.classList.add('selected');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle public name field visibility
|
|
*/
|
|
function togglePublicName() {
|
|
const checkbox = document.getElementById('public-acknowledgement');
|
|
const nameField = document.getElementById('public-name-field');
|
|
|
|
if (checkbox && nameField) {
|
|
if (checkbox.checked) {
|
|
nameField.classList.remove('hidden');
|
|
const publicNameInput = document.getElementById('public-name');
|
|
if (publicNameInput) publicNameInput.focus();
|
|
} else {
|
|
nameField.classList.add('hidden');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle form submission
|
|
*/
|
|
async function handleFormSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
const submitBtn = e.target.querySelector('button[type="submit"]');
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = 'Processing...';
|
|
|
|
try {
|
|
// Get form data
|
|
const donorName = document.getElementById('donor-name').value.trim() || 'Anonymous';
|
|
const donorEmail = document.getElementById('donor-email').value.trim();
|
|
const donorCountry = document.getElementById('donor-country').value.trim();
|
|
const publicAck = document.getElementById('public-acknowledgement').checked;
|
|
const publicName = document.getElementById('public-name').value.trim();
|
|
|
|
// Validate email
|
|
if (!donorEmail) {
|
|
alert('Please enter your email address.');
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = 'Proceed to Secure Payment';
|
|
return;
|
|
}
|
|
|
|
// Get amount
|
|
let amount;
|
|
if (selectedFrequency === 'monthly') {
|
|
amount = selectedAmount;
|
|
} else {
|
|
const customAmount = parseFloat(document.getElementById('amount-input').value);
|
|
if (!customAmount || customAmount < 1) {
|
|
const minAmount = typeof formatCurrency === 'function'
|
|
? formatCurrency(100, currentCurrency)
|
|
: `${currentCurrency} 1.00`;
|
|
alert(`Please enter a donation amount of at least ${minAmount}.`);
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = 'Proceed to Secure Payment';
|
|
return;
|
|
}
|
|
amount = Math.round(customAmount * 100); // Convert to cents
|
|
}
|
|
|
|
// Create checkout session
|
|
const response = await fetch('/api/koha/checkout', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
amount: amount,
|
|
currency: currentCurrency,
|
|
frequency: selectedFrequency,
|
|
tier: selectedFrequency === 'monthly' ? selectedTier : 'custom',
|
|
donor: {
|
|
name: donorName,
|
|
email: donorEmail,
|
|
country: donorCountry
|
|
},
|
|
public_acknowledgement: publicAck,
|
|
public_name: publicAck ? (publicName || donorName) : null
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.data.checkoutUrl) {
|
|
// Redirect to Stripe Checkout
|
|
window.location.href = data.data.checkoutUrl;
|
|
} else {
|
|
throw new Error(data.error || 'Failed to create checkout session');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Donation error:', error);
|
|
alert('An error occurred while processing your donation. Please try again or contact support.');
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = 'Proceed to Secure Payment';
|
|
}
|
|
}
|