tractatus/public/js/koha-donation.js
TheFlow 2298d36bed fix(submissions): restructure Economist package and fix article display
- Create Economist SubmissionTracking package correctly:
  * mainArticle = full blog post content
  * coverLetter = 216-word SIR— letter
  * Links to blog post via blogPostId
- Archive 'Letter to The Economist' from blog posts (it's the cover letter)
- Fix date display on article cards (use published_at)
- Target publication already displaying via blue badge

Database changes:
- Make blogPostId optional in SubmissionTracking model
- Economist package ID: 68fa85ae49d4900e7f2ecd83
- Le Monde package ID: 68fa2abd2e6acd5691932150

Next: Enhanced modal with tabs, validation, export

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 08:47:42 +13:00

415 lines
12 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';
}
}
/**
* Initialize manage subscription functionality
*/
document.addEventListener('DOMContentLoaded', function() {
const manageBtn = document.getElementById('manage-subscription-btn');
const emailInput = document.getElementById('manage-email');
if (manageBtn && emailInput) {
manageBtn.addEventListener('click', handleManageSubscription);
emailInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
handleManageSubscription();
}
});
}
});
/**
* Handle manage subscription button click
*/
async function handleManageSubscription() {
const emailInput = document.getElementById('manage-email');
const manageBtn = document.getElementById('manage-subscription-btn');
const errorDiv = document.getElementById('manage-error');
const loadingDiv = document.getElementById('manage-loading');
const email = emailInput.value.trim();
// Validate email
if (!email) {
showManageError('Please enter your email address.');
emailInput.focus();
return;
}
if (!isValidEmail(email)) {
showManageError('Please enter a valid email address.');
emailInput.focus();
return;
}
// Hide error, show loading
hideManageError();
showManageLoading();
manageBtn.disabled = true;
try {
const response = await fetch('/api/koha/portal', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email })
});
const data = await response.json();
if (data.success && data.data.url) {
// Redirect to Stripe Customer Portal
window.location.href = data.data.url;
} else if (response.status === 404) {
showManageError('No subscription found for this email address. Please check your email and try again.');
manageBtn.disabled = false;
hideManageLoading();
} else {
throw new Error(data.error || 'Failed to access subscription portal');
}
} catch (error) {
console.error('Manage subscription error:', error);
showManageError('An error occurred. Please try again or contact support@agenticgovernance.digital');
manageBtn.disabled = false;
hideManageLoading();
}
}
/**
* Validate email format
*/
function isValidEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}
/**
* Show error message
*/
function showManageError(message) {
const errorDiv = document.getElementById('manage-error');
if (errorDiv) {
errorDiv.textContent = message;
errorDiv.classList.remove('hidden');
}
}
/**
* Hide error message
*/
function hideManageError() {
const errorDiv = document.getElementById('manage-error');
if (errorDiv) {
errorDiv.classList.add('hidden');
}
}
/**
* Show loading indicator
*/
function showManageLoading() {
const loadingDiv = document.getElementById('manage-loading');
if (loadingDiv) {
loadingDiv.classList.remove('hidden');
}
}
/**
* Hide loading indicator
*/
function hideManageLoading() {
const loadingDiv = document.getElementById('manage-loading');
if (loadingDiv) {
loadingDiv.classList.add('hidden');
}
}