Multi-Currency Implementation: - Add currency configuration with 10 supported currencies (NZD, USD, EUR, GBP, AUD, CAD, JPY, CHF, SGD, HKD) - Create client-side and server-side currency utilities for conversion and formatting - Implement currency selector UI component with auto-detection and localStorage persistence - Update Donation model to store multi-currency transactions with NZD equivalents - Update Koha service to handle currency conversion and exchange rate tracking - Update donation form UI to display prices in selected currency - Update transparency dashboard to show donations with currency indicators - Update Stripe setup documentation with currency_options configuration guide Privacy Policy: - Create comprehensive privacy policy page (GDPR compliant) - Add shared footer component with privacy policy link - Update all Koha pages with footer component Technical Details: - Exchange rates stored at donation time for historical accuracy - All donations tracked in both original currency and NZD for transparency - Base currency: NZD (New Zealand Dollar) - Uses Stripe currency_options for monthly subscriptions - Dynamic currency for one-time donations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
502 lines
21 KiB
HTML
502 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Support Tractatus | Koha Donation System</title>
|
|
<meta name="description" content="Support the Tractatus AI Safety Framework. Help fund hosting, development, research, and community building for architectural AI safety.">
|
|
<link rel="stylesheet" href="/css/tailwind.css?v=1759833751">
|
|
<style>
|
|
.gradient-text { background: linear-gradient(120deg, #3b82f6 0%, #8b5cf6 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
.skip-link { position: absolute; left: -9999px; }
|
|
.skip-link:focus { left: 0; z-index: 100; background: white; padding: 1rem; }
|
|
|
|
/* Accessibility: Focus indicators (WCAG 2.4.7) */
|
|
a:focus, button:focus, input:focus, select:focus, textarea:focus {
|
|
outline: 3px solid #3b82f6;
|
|
outline-offset: 2px;
|
|
}
|
|
a:focus:not(:focus-visible) { outline: none; }
|
|
a:focus-visible { outline: 3px solid #3b82f6; outline-offset: 2px; }
|
|
|
|
/* Tier card selection */
|
|
.tier-card {
|
|
transition: all 0.3s;
|
|
cursor: pointer;
|
|
border: 3px solid transparent;
|
|
}
|
|
.tier-card:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow: 0 12px 24px rgba(0,0,0,0.15);
|
|
}
|
|
.tier-card.selected {
|
|
border-color: #3b82f6;
|
|
background-color: #eff6ff;
|
|
}
|
|
.tier-badge {
|
|
display: inline-block;
|
|
background: #3b82f6;
|
|
color: white;
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 9999px;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-50">
|
|
|
|
<!-- Skip Link for Keyboard Navigation -->
|
|
<a href="#main-content" class="skip-link">Skip to main content</a>
|
|
|
|
<!-- Navigation (injected by navbar.js) -->
|
|
<script src="/js/components/navbar.js?v=1759875690"></script>
|
|
|
|
<!-- Main Content -->
|
|
<main id="main-content" class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
|
|
<!-- Header -->
|
|
<div class="text-center mb-12">
|
|
<h1 class="text-4xl md:text-5xl font-bold text-gray-900 mb-4">
|
|
Support Tractatus
|
|
</h1>
|
|
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
|
|
Your donation helps fund the development, hosting, and research behind the world's first production implementation of architectural AI safety constraints.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Koha Explanation -->
|
|
<div class="bg-blue-50 border-l-4 border-blue-500 p-6 mb-12 rounded">
|
|
<h2 class="text-lg font-semibold text-blue-900 mb-2">What is Koha?</h2>
|
|
<p class="text-blue-800">
|
|
<strong>Koha</strong> (koh-hah) is a Māori word meaning "gift" or "donation." In the spirit of Te Tiriti partnership and reciprocity, we use this term to honor the indigenous wisdom that informs our approach to technology governance and human agency.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Currency Selector Container -->
|
|
<div id="currency-selector-container"></div>
|
|
|
|
<!-- Donation Form -->
|
|
<div class="bg-white shadow-lg rounded-lg p-8 mb-12">
|
|
<form id="donation-form">
|
|
|
|
<!-- Frequency Selection -->
|
|
<div class="mb-8">
|
|
<label class="block text-lg font-semibold text-gray-900 mb-4">Donation Type</label>
|
|
<div class="flex gap-4">
|
|
<button type="button" id="freq-monthly" class="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" onclick="selectFrequency('monthly')">
|
|
Monthly Support
|
|
</button>
|
|
<button type="button" id="freq-onetime" class="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" onclick="selectFrequency('one_time')">
|
|
One-Time Gift
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tier Selection (Monthly) -->
|
|
<div id="tier-selection" class="mb-8">
|
|
<label class="block text-lg font-semibold text-gray-900 mb-4">Choose Your Monthly Support Level</label>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
|
|
<!-- Tier: $5 NZD -->
|
|
<div class="tier-card bg-white border-2 border-gray-200 rounded-lg p-6" onclick="selectTier('5', 500)">
|
|
<div class="text-center">
|
|
<div class="tier-badge mb-3">Foundation</div>
|
|
<div class="text-4xl font-bold text-gray-900 mb-2">$5</div>
|
|
<div class="text-sm text-gray-500 mb-4">NZD / month</div>
|
|
<p class="text-gray-600 text-sm">
|
|
Essential support for hosting and infrastructure. Every contribution matters.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tier: $15 NZD -->
|
|
<div class="tier-card bg-white border-2 border-gray-200 rounded-lg p-6 selected" onclick="selectTier('15', 1500)">
|
|
<div class="text-center">
|
|
<div class="tier-badge mb-3">Advocate</div>
|
|
<div class="text-4xl font-bold text-gray-900 mb-2">$15</div>
|
|
<div class="text-sm text-gray-500 mb-4">NZD / month</div>
|
|
<p class="text-gray-600 text-sm">
|
|
Support development and research. Help expand the framework's capabilities.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tier: $50 NZD -->
|
|
<div class="tier-card bg-white border-2 border-gray-200 rounded-lg p-6" onclick="selectTier('50', 5000)">
|
|
<div class="text-center">
|
|
<div class="tier-badge mb-3">Champion</div>
|
|
<div class="text-4xl font-bold text-gray-900 mb-2">$50</div>
|
|
<div class="text-sm text-gray-500 mb-4">NZD / month</div>
|
|
<p class="text-gray-600 text-sm">
|
|
Sustaining support for community building and advanced features.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom Amount (One-Time) -->
|
|
<div id="custom-amount" class="mb-8" style="display: none;">
|
|
<label for="amount-input" class="block text-lg font-semibold text-gray-900 mb-2">Donation Amount (NZD)</label>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-2xl text-gray-700">$</span>
|
|
<input
|
|
type="number"
|
|
id="amount-input"
|
|
min="1"
|
|
step="0.01"
|
|
placeholder="Enter amount"
|
|
class="flex-1 px-4 py-3 border-2 border-gray-300 rounded-lg text-lg focus:border-blue-600 focus:outline-none"
|
|
aria-label="Donation amount in NZD"
|
|
aria-describedby="amount-help"
|
|
>
|
|
<span class="text-lg text-gray-600">NZD</span>
|
|
</div>
|
|
<p id="amount-help" class="text-sm text-gray-500 mt-2">Minimum donation: $1.00 NZD</p>
|
|
</div>
|
|
|
|
<!-- Donor Information -->
|
|
<div class="mb-8">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Your Information</h3>
|
|
|
|
<div class="mb-4">
|
|
<label for="donor-name" class="block text-sm font-medium text-gray-700 mb-1">Name (optional)</label>
|
|
<input
|
|
type="text"
|
|
id="donor-name"
|
|
placeholder="Leave blank to donate anonymously"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:border-blue-600 focus:outline-none"
|
|
aria-describedby="name-help"
|
|
>
|
|
<p id="name-help" class="text-xs text-gray-500 mt-1">Your name will only be used if you opt-in for public acknowledgement below</p>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label for="donor-email" class="block text-sm font-medium text-gray-700 mb-1">
|
|
Email <span class="text-red-500" aria-label="required">*</span>
|
|
</label>
|
|
<input
|
|
type="email"
|
|
id="donor-email"
|
|
required
|
|
placeholder="your@email.com"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:border-blue-600 focus:outline-none"
|
|
aria-required="true"
|
|
aria-describedby="email-help"
|
|
>
|
|
<p id="email-help" class="text-xs text-gray-500 mt-1">Required for payment receipt. We never share your email.</p>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label for="donor-country" class="block text-sm font-medium text-gray-700 mb-1">Country (optional)</label>
|
|
<input
|
|
type="text"
|
|
id="donor-country"
|
|
placeholder="New Zealand"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:border-blue-600 focus:outline-none"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Public Acknowledgement -->
|
|
<div class="mb-8 bg-gray-50 p-6 rounded-lg">
|
|
<div class="flex items-start gap-3">
|
|
<input
|
|
type="checkbox"
|
|
id="public-acknowledgement"
|
|
class="mt-1 h-5 w-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
|
onchange="togglePublicName()"
|
|
>
|
|
<div class="flex-1">
|
|
<label for="public-acknowledgement" class="font-medium text-gray-900 cursor-pointer">
|
|
Include me in the public supporters list
|
|
</label>
|
|
<p class="text-sm text-gray-600 mt-1">
|
|
We value privacy. By default, all donations are anonymous. Check this box to be listed as a supporter on our transparency dashboard.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="public-name-field" class="mt-4" style="display: none;">
|
|
<label for="public-name" class="block text-sm font-medium text-gray-700 mb-1">Name to display publicly</label>
|
|
<input
|
|
type="text"
|
|
id="public-name"
|
|
placeholder="How you'd like to be acknowledged"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:border-blue-600 focus:outline-none"
|
|
aria-describedby="public-name-help"
|
|
>
|
|
<p id="public-name-help" class="text-xs text-gray-500 mt-1">Can be your real name, organization, or pseudonym</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Allocation Transparency -->
|
|
<div class="mb-8 bg-blue-50 p-6 rounded-lg">
|
|
<h3 class="font-semibold text-gray-900 mb-3">How Your Donation is Used</h3>
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
<div>
|
|
<div class="font-bold text-blue-900">40%</div>
|
|
<div class="text-gray-700">Development</div>
|
|
</div>
|
|
<div>
|
|
<div class="font-bold text-blue-900">30%</div>
|
|
<div class="text-gray-700">Hosting</div>
|
|
</div>
|
|
<div>
|
|
<div class="font-bold text-blue-900">20%</div>
|
|
<div class="text-gray-700">Research</div>
|
|
</div>
|
|
<div>
|
|
<div class="font-bold text-blue-900">10%</div>
|
|
<div class="text-gray-700">Community</div>
|
|
</div>
|
|
</div>
|
|
<p class="text-xs text-gray-600 mt-3">
|
|
<a href="/koha/transparency" class="text-blue-600 hover:underline">View full transparency dashboard →</a>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Submit Button -->
|
|
<div class="text-center">
|
|
<button
|
|
type="submit"
|
|
class="bg-blue-600 text-white px-12 py-4 rounded-lg text-lg font-semibold hover:bg-blue-700 transition shadow-lg hover:shadow-xl"
|
|
>
|
|
Proceed to Secure Payment
|
|
</button>
|
|
<p class="text-sm text-gray-500 mt-3">
|
|
🔒 Secure payment via Stripe. We don't store your card details.
|
|
</p>
|
|
</div>
|
|
|
|
</form>
|
|
</div>
|
|
|
|
<!-- FAQ Section -->
|
|
<div class="bg-white shadow rounded-lg p-8">
|
|
<h2 class="text-2xl font-bold text-gray-900 mb-6">Frequently Asked Questions</h2>
|
|
|
|
<div class="space-y-6">
|
|
<div>
|
|
<h3 class="font-semibold text-gray-900 mb-2">Why do you use the term "Koha"?</h3>
|
|
<p class="text-gray-700">
|
|
Koha is a Māori concept of gift-giving and reciprocity. We use it to honor the indigenous wisdom of Aotearoa New Zealand, where our lead developer is based, and to acknowledge that technology governance should be rooted in values of reciprocity and collective responsibility.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 class="font-semibold text-gray-900 mb-2">Is this tax-deductible?</h3>
|
|
<p class="text-gray-700">
|
|
We are currently operating as an open-source project, not a registered charity. Donations are not tax-deductible at this time. You will receive a receipt for your records.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 class="font-semibold text-gray-900 mb-2">How do I cancel my monthly donation?</h3>
|
|
<p class="text-gray-700">
|
|
You can cancel anytime by emailing <a href="mailto:support@agenticgovernance.digital" class="text-blue-600 hover:underline">support@agenticgovernance.digital</a> with your email address. We'll process your cancellation within 24 hours.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 class="font-semibold text-gray-900 mb-2">What currencies do you accept?</h3>
|
|
<p class="text-gray-700">
|
|
We accept 10 major currencies: NZD, USD, EUR, GBP, AUD, CAD, JPY, CHF, SGD, and HKD. Prices are automatically converted from our base currency (NZD) using current exchange rates. All donations are tracked in both your chosen currency and NZD for transparency reporting.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 class="font-semibold text-gray-900 mb-2">Can I make a one-time donation instead?</h3>
|
|
<p class="text-gray-700">
|
|
Yes! Click the "One-Time Gift" button at the top of the form to make a single donation of any amount.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<!-- Footer -->
|
|
<script src="/js/components/footer.js"></script>
|
|
|
|
<!-- Currency utilities and selector -->
|
|
<script src="/js/utils/currency.js"></script>
|
|
<script src="/js/components/currency-selector.js"></script>
|
|
|
|
<script>
|
|
// Form state
|
|
let selectedFrequency = 'monthly';
|
|
let selectedTier = '15';
|
|
let selectedAmount = 1500; // in cents (NZD)
|
|
let currentCurrency = detectUserCurrency(); // From currency.js
|
|
|
|
// Update prices when currency changes
|
|
window.updatePricesForCurrency = function(currency) {
|
|
currentCurrency = currency;
|
|
const prices = getTierPrices(currency);
|
|
|
|
// Update tier card prices
|
|
const tierCards = document.querySelectorAll('.tier-card');
|
|
tierCards[0].querySelector('.text-4xl').textContent = formatCurrency(prices.tier_5, currency).replace(/\.\d+$/, ''); // Remove cents for display
|
|
tierCards[0].querySelector('.text-sm.text-gray-500').textContent = `${currency} / month`;
|
|
|
|
tierCards[1].querySelector('.text-4xl').textContent = formatCurrency(prices.tier_15, currency).replace(/\.\d+$/, '');
|
|
tierCards[1].querySelector('.text-sm.text-gray-500').textContent = `${currency} / month`;
|
|
|
|
tierCards[2].querySelector('.text-4xl').textContent = formatCurrency(prices.tier_50, currency).replace(/\.\d+$/, '');
|
|
tierCards[2].querySelector('.text-sm.text-gray-500').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) {
|
|
amountHelp.textContent = `Minimum donation: ${formatCurrency(100, currency)}`;
|
|
}
|
|
};
|
|
|
|
// Select frequency
|
|
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
|
|
document.getElementById('tier-selection').style.display = 'block';
|
|
document.getElementById('custom-amount').style.display = 'none';
|
|
} 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
|
|
document.getElementById('tier-selection').style.display = 'none';
|
|
document.getElementById('custom-amount').style.display = 'block';
|
|
document.getElementById('amount-input').focus();
|
|
}
|
|
}
|
|
|
|
// Select tier
|
|
function selectTier(tier, amountNZDCents) {
|
|
selectedTier = tier;
|
|
|
|
// Calculate amount in current currency
|
|
const prices = getTierPrices(currentCurrency);
|
|
selectedAmount = prices[`tier_${tier}`];
|
|
|
|
// Update card styles
|
|
const cards = document.querySelectorAll('.tier-card');
|
|
cards.forEach(card => {
|
|
card.classList.remove('selected');
|
|
});
|
|
event.currentTarget.classList.add('selected');
|
|
}
|
|
|
|
// Toggle public name field
|
|
function togglePublicName() {
|
|
const checkbox = document.getElementById('public-acknowledgement');
|
|
const nameField = document.getElementById('public-name-field');
|
|
|
|
if (checkbox.checked) {
|
|
nameField.style.display = 'block';
|
|
document.getElementById('public-name').focus();
|
|
} else {
|
|
nameField.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Form submission
|
|
document.getElementById('donation-form').addEventListener('submit', async (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) {
|
|
alert(`Please enter a donation amount of at least ${formatCurrency(100, currentCurrency)}.`);
|
|
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';
|
|
}
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|