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:
@@ -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
216
static/js/subscriptions.js
Normal 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();
|
||||
})();
|
||||
Reference in New Issue
Block a user