- 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>
415 lines
12 KiB
JavaScript
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');
|
|
}
|
|
}
|