Improve subscription UI with large tier buttons

- Replace dropdown tier selection with attractive visual buttons
- Add tier-button CSS with hover effects and selection states
- Remove 'or pay by card' divider from subscription form for cleaner UI
- Update JavaScript to handle tier button selection events
- Fix Stripe module import conflict by renaming stripe directory to stripe_config
- Add responsive grid layout for tier buttons on mobile devices
This commit is contained in:
2025-10-07 17:22:51 +01:00
parent bfdcee8602
commit 3ddbc40bb5
2655 changed files with 516264 additions and 2 deletions

View File

@@ -842,3 +842,169 @@ button {
cursor: pointer;
}
#result { margin-top: 12px; min-height: 20px; }
/* Payment Type Tabs */
.payment-type-tabs {
display: flex;
margin-bottom: 24px;
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--border-color);
}
.tab-button {
flex: 1;
padding: 12px 16px;
border: none;
background: var(--bg-secondary);
color: var(--text-secondary);
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.tab-button.active {
background: var(--accent);
color: white;
}
.tab-button:hover:not(.active) {
background: var(--border-color);
color: var(--text-primary);
}
/* Payment Forms */
.payment-form {
display: none;
}
.payment-form.active {
display: block;
}
/* Subscription specific styles */
#subscription-form select {
margin-bottom: 16px;
}
#subscription-form input[type="email"] {
margin-bottom: 16px;
}
/* Subscription result styling */
#subscription-result {
margin-top: 16px;
padding: 12px;
border-radius: 8px;
font-weight: 500;
}
/* Tier Button Styling */
.tier-buttons {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin: 20px 0;
}
.tier-button {
background: var(--bg-secondary);
border: 2px solid var(--border-color);
border-radius: 16px;
padding: 24px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.tier-button:hover {
border-color: var(--accent);
background: var(--bg-input);
transform: translateY(-2px);
box-shadow: 0 8px 25px var(--shadow);
}
.tier-button.selected {
border-color: var(--accent);
background: var(--accent);
color: white;
transform: translateY(-2px);
box-shadow: 0 8px 25px var(--shadow);
}
.tier-button.selected .tier-description {
color: rgba(255, 255, 255, 0.9);
}
.tier-icon {
font-size: 32px;
margin-bottom: 8px;
}
.tier-name {
font-size: 20px;
font-weight: 600;
margin-bottom: 4px;
color: var(--text-primary);
}
.tier-button.selected .tier-name {
color: white;
}
.tier-price {
font-size: 24px;
font-weight: 700;
margin-bottom: 8px;
color: var(--accent);
}
.tier-button.selected .tier-price {
color: white;
}
.tier-period {
font-size: 14px;
font-weight: 400;
opacity: 0.8;
}
.tier-description {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.4;
margin-top: 8px;
}
/* Mobile responsive */
@media (max-width: 768px) {
.tier-buttons {
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.tier-button {
padding: 20px 16px;
}
.tier-icon {
font-size: 28px;
}
.tier-name {
font-size: 18px;
}
.tier-price {
font-size: 20px;
}
}
@media (max-width: 480px) {
.tier-buttons {
grid-template-columns: 1fr;
}
}

216
static/js/subscriptions.js Normal file
View File

@@ -0,0 +1,216 @@
(async () => {
const cfg = await fetch("/config").then(r => r.json());
const stripe = Stripe(cfg.publishableKey);
const subscriptionForm = document.getElementById("subscription-form");
const subscriptionResult = document.getElementById("subscription-result");
const tierSelect = document.getElementById("subscription-tier");
const emailInput = document.getElementById("subscriber-email");
const nameInput = document.getElementById("subscriber-name");
const submitButton = document.getElementById("subscription-submit-button");
let subscriptionElements, subscriptionCardElement;
// Initialize tier button selection
function initializeTierButtons() {
const tierButtons = document.querySelectorAll('.tier-button');
const tierInput = document.getElementById('subscription-tier');
tierButtons.forEach(button => {
button.addEventListener('click', function() {
// Remove selected class from all buttons
tierButtons.forEach(btn => btn.classList.remove('selected'));
// Add selected class to clicked button
this.classList.add('selected');
// Update the tier input value
const selectedTier = this.getAttribute('data-tier');
if (tierInput) {
tierInput.value = selectedTier;
}
});
});
}
// Initialize basic card element (display only, no payment processing)
function initializeBasicCardElement() {
// Clean up existing elements
if (subscriptionElements) {
subscriptionElements.destroy();
}
// Create basic elements for card display
subscriptionElements = stripe.elements({
appearance: {
theme: document.documentElement.getAttribute('data-theme') === 'dark' ? 'night' : 'stripe'
}
});
// Create and mount card element
subscriptionCardElement = subscriptionElements.create("card", {
style: {
base: {
fontSize: '16px',
color: getComputedStyle(document.documentElement).getPropertyValue('--text-primary'),
'::placeholder': {
color: getComputedStyle(document.documentElement).getPropertyValue('--text-secondary'),
},
},
}
});
subscriptionCardElement.mount("#subscription-card-element");
}
// Handle subscription payment
async function handleSubscriptionPayment(e) {
e.preventDefault();
const tier_id = tierSelect.value;
const email = emailInput.value;
const name = nameInput.value;
if (!tier_id || !email) {
subscriptionResult.textContent = "❌ Please fill in all required fields.";
return;
}
subscriptionResult.textContent = "Creating subscription...";
submitButton.disabled = true;
try {
// Create subscription
const res = await fetch("/create-subscription", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ tier_id, email, name })
});
const data = await res.json();
if (data.error) {
subscriptionResult.textContent = "❌ " + data.error;
submitButton.disabled = false;
return;
}
const clientSecret = data.clientSecret;
subscriptionResult.textContent = "Processing payment...";
// Confirm payment with the card element
const { error } = await stripe.confirmPayment({
elements: subscriptionElements,
confirmParams: {
return_url: window.location.href,
},
redirect: "if_required"
});
if (error) {
subscriptionResult.textContent = "❌ " + error.message;
} else {
subscriptionResult.innerHTML = "✅ Thank you! Your subscription is now active.<br>You'll receive an email confirmation shortly.";
showSubscriptionSuccessAnimation();
// Add to supporter wall
const name = nameInput.value.trim();
if (name && window.supporterWall) {
const tierData = await getTierData(tier_id);
await window.supporterWall.addSupporter(name, tierData.amount * 100, tierData.currency);
}
}
} catch (err) {
subscriptionResult.textContent = "❌ Subscription failed. Please try again.";
console.error(err);
} finally {
submitButton.disabled = false;
}
}
// Get tier data
async function getTierData(tierId) {
const tiers = await fetch("/subscription-tiers").then(r => r.json());
return tiers.tiers[tierId];
}
// Success animation for subscriptions
function showSubscriptionSuccessAnimation() {
subscriptionResult.style.background = 'linear-gradient(135deg, #28a745, #20c997)';
subscriptionResult.style.color = 'white';
subscriptionResult.style.padding = '12px';
subscriptionResult.style.borderRadius = '8px';
subscriptionResult.style.animation = 'bounce 0.6s ease-out';
}
// Update submit button text based on tier
async function updateSubmitButtonText() {
const tier_id = tierSelect.value;
if (tier_id) {
try {
const tierData = await getTierData(tier_id);
if (tierData.interval === 'month') {
submitButton.textContent = `Start Monthly Support - ${tierData.currency} ${tierData.amount}/${tierData.interval}`;
} else {
submitButton.textContent = `Start Yearly Support - ${tierData.currency} ${tierData.amount}/${tierData.interval}`;
}
} catch (err) {
submitButton.textContent = "Start Support";
}
} else {
submitButton.textContent = "Start Support";
}
}
// Event listeners
subscriptionForm.addEventListener("submit", handleSubscriptionPayment);
tierSelect.addEventListener("change", updateSubmitButtonText);
// Tab switching functionality
const oneTimeTab = document.getElementById("one-time-tab");
const recurringTab = document.getElementById("recurring-tab");
const donationForm = document.getElementById("donation-form");
function switchToOneTime() {
oneTimeTab.classList.add("active");
recurringTab.classList.remove("active");
donationForm.classList.add("active");
subscriptionForm.classList.remove("active");
}
function switchToRecurring() {
recurringTab.classList.add("active");
oneTimeTab.classList.remove("active");
subscriptionForm.classList.add("active");
donationForm.classList.remove("active");
// Initialize card element when switching to subscription tab
if (!subscriptionElements) {
initializeBasicCardElement();
}
}
oneTimeTab.addEventListener("click", switchToOneTime);
recurringTab.addEventListener("click", switchToRecurring);
// Update elements theme when theme changes
if (window.themeManager) {
const originalSetTheme = window.themeManager.setTheme;
window.themeManager.setTheme = function(theme) {
originalSetTheme.call(this, theme);
// Reinitialize card element with new theme
setTimeout(() => {
if (subscriptionForm.classList.contains("active")) {
initializeBasicCardElement();
}
}, 100);
};
}
// Initialize card element on page load if subscription form is visible
if (subscriptionForm.classList.contains("active")) {
initializeBasicCardElement();
}
// Initialize tier button selection
initializeTierButtons();
})();