Skip to content

Commit 0ee8fac

Browse files
committed
Improve donate page, sponsor teir monthly spec
1 parent 1a8097f commit 0ee8fac

File tree

2 files changed

+81
-54
lines changed

2 files changed

+81
-54
lines changed

assets/js/donate.js

Lines changed: 59 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,28 @@ const VALID_CHANNELS = new Map([
2828
]);
2929

3030
// Unified amount validation function
31-
function verifyDonateInput(amount, sponsor_channel, is_corporate, on_verified) {
31+
function verifyDonateInput(amount, sponsor_channel, is_corporate, trigger, on_verified) {
3232
const channelSelect = document.getElementById("channel-options");
3333
const channel = channelSelect ? channelSelect.value : "";
3434

3535
const value = parseCurrency(amount);
3636

37+
const modal_options = { triggerEl: trigger };
38+
3739
// Empty check
3840
if (!amount || amount.trim() === "") {
39-
showModal("Missing Amount", "Please input amount before confirming sponsorship.");
41+
showModal("Missing Amount", "Please input amount before confirming sponsorship.", modal_options);
4042
return;
4143
}
4244

4345
// Must be >= 1
4446
if (isNaN(value) || value < 1) {
45-
showModal("Invalid Amount", "Please enter a valid amount greater than 0.");
47+
showModal("Invalid Amount", "Please enter a valid amount greater than 0.", modal_options);
4648
return;
4749
}
4850

4951
if (!VALID_CHANNELS.has(sponsor_channel)) {
50-
showModal("Invalid Channel", `Unsupported sponsor channel: ${sponsor_channel}`);
52+
showModal("Invalid Channel", `Unsupported sponsor channel: ${sponsor_channel}`, modal_options);
5153
return;
5254
}
5355

@@ -58,13 +60,15 @@ function verifyDonateInput(amount, sponsor_channel, is_corporate, on_verified) {
5860

5961
const bodyEl = modalEl.querySelector(".modal-body");
6062
bodyEl.innerHTML = `
61-
For corporate sponsorships <strong> USD $${INDIVIDUAL_MAX_AMOUNT}</strong>,
63+
For corporate sponsorships <strong>&gt; USD$${INDIVIDUAL_MAX_AMOUNT}</strong>,
6264
we recommend using <strong>Open Source Collective</strong> for transparency and compliance.<br>
6365
PayPal/GitHub are intended for individual backers and not suitable for large payments.
6466
`;
6567

68+
modalEl.dataset.triggerId = trigger.id;
69+
6670
const modal = new bootstrap.Modal(modalEl);
67-
71+
6872
modalEl.querySelector(".btn-secondary").onclick = () => {
6973
modal.hide();
7074
modalEl.addEventListener("hidden.bs.modal", () => {
@@ -87,7 +91,8 @@ function verifyDonateInput(amount, sponsor_channel, is_corporate, on_verified) {
8791
} else {
8892
showModal(
8993
"Limit Exceeded",
90-
`Individual sponsorship cannot exceed USD $${INDIVIDUAL_MAX_AMOUNT}. Please adjust your amount.`
94+
`Individual sponsorship cannot exceed USD$${INDIVIDUAL_MAX_AMOUNT}. Please adjust your amount.`,
95+
modal_options
9196
);
9297
}
9398
return;
@@ -175,25 +180,30 @@ const sandbox = window.location.host.includes('local.') || window.location.host.
175180

176181
document.querySelector('#confirm-btn').addEventListener('click', (e) => {
177182
e.preventDefault();
183+
184+
// selected sponosr teir card
178185
const selected = document.querySelector('input[name="sponsor"]:checked');
179-
const isMonthly = document.getElementById('monthly').checked;
180186

181187
if (selected) {
182188
const is_custom = selected.id === 'custom';
183189
const amount_str = is_custom
184190
? document.querySelector('.amount-input').value
185191
: selected.value;
186192

193+
const is_monthly = is_custom
194+
? document.querySelector('input[name="custom-cycle"]:checked').value === 'monthly'
195+
: true;
196+
187197
const is_corporate = document.getElementById("btn-corporate-tiers").classList.contains("active");
188198
const channel = document.getElementById("channel-options").value;
189-
verifyDonateInput(amount_str, channel, is_corporate, (verified_channel) => {
190-
if ((window.cv_sel_amount !== amount_str) || (window.cv_is_mouthly !== isMonthly)) {
199+
verifyDonateInput(amount_str, channel, is_corporate, e.currentTarget, (verified_channel) => {
200+
if ((window.cv_sel_amount !== amount_str) || (window.cv_is_mouthly !== is_monthly)) {
191201
window.cv_sel_amount = amount_str;
192-
window.cv_is_mouthly = isMonthly;
202+
window.cv_is_mouthly = is_monthly;
193203
window.cv_orderid = genOrderId();
194204
}
195205

196-
console.log(`Amount: $${amount_str}${isMonthly ? '/month' : ''}`);
206+
console.log(`Amount: $${amount_str}${is_monthly ? '/month' : ''}`);
197207

198208
const amount = parseCurrency(amount_str);
199209
const teir_info = is_corporate ? corporateTiers[selected.id] : individualTiers[selected.id];
@@ -205,39 +215,39 @@ document.querySelector('#confirm-btn').addEventListener('click', (e) => {
205215
form.attr('action', actionUrl);
206216
form.children('#WIDprod').attr('value', teir_info.prod_id);
207217
form.children('#WIDout_trade_no').attr('value', window.cv_orderid);
208-
form.children('#WIDmonthly').attr('value', isMonthly ? '1' : '0');
218+
form.children('#WIDmonthly').attr('value', is_monthly ? '1' : '0');
209219
form.children('#WIDamount').attr('value', amount.toString());
210220
form.submit();
211221
}
212222
else if (verified_channel == 'github') {
213-
const gh_freq = isMonthly ? 'recurring' : 'one-time';
223+
const gh_freq = is_monthly ? 'recurring' : 'one-time';
214224
const actionUrl = `https://github.com/sponsors/axmolengine/sponsorships?preview=false&frequency=${gh_freq}&amount=${amount}`;
215225
window.open(actionUrl, '_blank');
216226
}
217227
else if (verified_channel == 'osc') {
218228
let actionUrl = '#';
219229
if (is_corporate) {
220-
if (isMonthly) {
230+
if (is_monthly) {
221231
const osc_teir = teir_info.osc_teir;
222232
isPresetTeir = true;
223233
actionUrl = `https://opencollective.com/axmol/contribute/${osc_teir}/checkout?interval=month&amount=${amount}&contributeAs=me`;
224234
}
225235
}
226236
else {
227-
if (isMonthly) {
237+
if (is_monthly) {
228238
isPresetTeir = true;
229239
actionUrl = `https://opencollective.com/axmol/contribute/backers-69887/checkout?interval=month&amount=${amount}&contributeAs=me`;
230240
}
231241
}
232242
if (actionUrl == '#') { // means no preset teir, use osc custom card
233-
const osc_interval = isMonthly ? 'month' : 'oneTime';
243+
const osc_interval = is_monthly ? 'month' : 'oneTime';
234244
actionUrl = `https://opencollective.com/axmol/donate?interval=${osc_interval}&amount=${amount}&contributeAs=me`;
235245
}
236246
window.open(actionUrl, '_blank');
237247
}
238248
});
239249
} else {
240-
showModal("Missing Tier", "Please select a tier before confirming sponsorship.");
250+
showModal("Missing Tier", "Please select a tier before confirming sponsorship.", { triggerEl: e.currentTarget });
241251
}
242252
});
243253

@@ -490,7 +500,7 @@ document.addEventListener("DOMContentLoaded", function () {
490500
radio.dataset.prod_id = tier.prod_id; // store sponsor id in data attribute
491501
radio.value = tier.amount;
492502
titleEl.textContent = tier.title;
493-
priceEl.textContent = `USD$${tier.amount}`;
503+
priceEl.textContent = `USD$${tier.amount}/month`;
494504
});
495505

496506
// Step 3: restore previous selection if possible, otherwise select the first radio
@@ -537,6 +547,7 @@ document.addEventListener("DOMContentLoaded", function () {
537547
// Show Bootstrap modal warning
538548
const modalEl = document.getElementById("channelWarning");
539549
const modal = new bootstrap.Modal(modalEl);
550+
modalEl.dataset.triggerId = e.currentTarget.id;
540551
const bodyEl = modalEl.querySelector(".modal-body");
541552
bodyEl.innerHTML = `
542553
For corporate sponsorship, we recommend using <strong>Open Source
@@ -548,21 +559,32 @@ document.addEventListener("DOMContentLoaded", function () {
548559
}
549560
});
550561

551-
const modalEl = document.getElementById("channelWarning");
552-
// update focus when hide
553-
modalEl.addEventListener("hide.bs.modal", () => {
554-
document.getElementById("confirm-btn").focus();
555-
});
562+
// handle focus when modal dlgs hide
563+
const modalDlgs = ['channelWarning', 'commonModal'];
564+
modalDlgs.forEach(name => {
565+
const modalEl = document.getElementById(name);
566+
if (!modalEl) return;
556567

557-
// disable interaction when hidden
558-
modalEl.addEventListener("hidden.bs.modal", () => {
559-
modalEl.setAttribute("inert", "");
560-
});
568+
// Store the trigger element when modal is shown
569+
modalEl.addEventListener("show.bs.modal", event => {
570+
// Bootstrap passes the trigger element in event.relatedTarget
571+
modalEl.removeAttribute("inert");
572+
});
561573

562-
// enable interaction when show
563-
modalEl.addEventListener("show.bs.modal", () => {
564-
modalEl.removeAttribute("inert");
574+
// Restore focus to the trigger element when modal is about to hide
575+
modalEl.addEventListener("hide.bs.modal", () => {
576+
const triggerId = modalEl.dataset.triggerId;
577+
if (triggerId) {
578+
document.getElementById(triggerId)?.focus();
579+
}
580+
});
581+
582+
// Disable interaction when modal is fully hidden
583+
modalEl.addEventListener("hidden.bs.modal", () => {
584+
modalEl.setAttribute("inert", "");
585+
});
565586
});
587+
566588
});
567589

568590
// Helper function to switch back to OSC from modal
@@ -611,20 +633,15 @@ function showToast(message, type = "primary") {
611633

612634
// Show a common modal with dynamic content
613635
function showModal(title, message, options = {}) {
614-
// Get modal elements
615636
const modalTitle = document.getElementById("commonModalTitle");
616637
const modalBody = document.getElementById("commonModalBody");
617638
const modalFooter = document.getElementById("commonModalFooter");
618639
const modalEl = document.getElementById("commonModal");
619640

620-
// Update title and body
621641
modalTitle.textContent = title;
622642
modalBody.innerHTML = message;
623-
624-
// Clear old footer buttons
625643
modalFooter.innerHTML = "";
626644

627-
// Default button if no options provided
628645
if (!options.buttons || options.buttons.length === 0) {
629646
const defaultBtn = document.createElement("button");
630647
defaultBtn.type = "button";
@@ -633,7 +650,6 @@ function showModal(title, message, options = {}) {
633650
defaultBtn.textContent = "OK";
634651
modalFooter.appendChild(defaultBtn);
635652
} else {
636-
// Create custom buttons
637653
options.buttons.forEach(btn => {
638654
const buttonEl = document.createElement("button");
639655
buttonEl.type = "button";
@@ -649,7 +665,12 @@ function showModal(title, message, options = {}) {
649665
});
650666
}
651667

652-
// Show modal
668+
// Store trigger element if provided
669+
if (options.triggerEl) {
670+
modalEl.dataset.triggerId = options.triggerEl.id || "";
671+
}
672+
653673
const modal = new bootstrap.Modal(modalEl);
654674
modal.show();
655675
}
676+

donate/index.html

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ <h1 class="fw-bold">Why Your Support Matters</h1>
176176
</h4> -->
177177
<div class="d-flex justify-content-between align-items-center mb-3">
178178
<!-- Sponsor Type Tabs -->
179-
<div class="d-flex gap-3">
179+
<div class="d-flex gap-2">
180180
<button id="btn-individual-tiers" type="button"
181181
class="btn btn-outline-primary active">
182182
Individual Tiers
@@ -203,7 +203,7 @@ <h1 class="fw-bold">Why Your Support Matters</h1>
203203
</div>
204204

205205
<!-- Sponsor Channel Dropdown -->
206-
<div class="d-flex align-items-center gap-2">
206+
<div class="d-flex align-items-center gap-1">
207207
<label for="channel-options" class="form-label mb-0">Sponsor
208208
Channel:</label>
209209
<select id="channel-options"
@@ -222,7 +222,7 @@ <h1 class="fw-bold">Why Your Support Matters</h1>
222222
value="5">
223223
<div class="sponsor-card">
224224
<h3 class="tier-title">Backer</h3>
225-
<div class="price">USD$5</div>
225+
<div class="price">USD$5/month</div>
226226
</div>
227227
</label>
228228

@@ -231,7 +231,7 @@ <h3 class="tier-title">Backer</h3>
231231
value="25">
232232
<div class="sponsor-card">
233233
<h3 class="tier-title">Bronze</h3>
234-
<div class="price">USD$25</div>
234+
<div class="price">USD$25/month</div>
235235
</div>
236236
</label>
237237

@@ -240,7 +240,7 @@ <h3 class="tier-title">Bronze</h3>
240240
value="50">
241241
<div class="sponsor-card">
242242
<h3 class="tier-title">Silver</h3>
243-
<div class="price">USD$50</div>
243+
<div class="price">USD$50/month</div>
244244
</div>
245245
</label>
246246

@@ -249,7 +249,7 @@ <h3 class="tier-title">Silver</h3>
249249
value="100">
250250
<div class="sponsor-card">
251251
<h3 class="tier-title">Gold</h3>
252-
<div class="price">USD$100</div>
252+
<div class="price">USD$100/month</div>
253253
</div>
254254
</label>
255255

@@ -258,23 +258,34 @@ <h3 class="tier-title">Gold</h3>
258258
value="250">
259259
<div class="sponsor-card">
260260
<h3 class="tier-title">Platinum</h3>
261-
<div class="price">USD$250</div>
261+
<div class="price">USD$250/month</div>
262262
</div>
263263
</label>
264264

265-
<!-- Other Amount -->
265+
<!-- Custom Teir -->
266266
<label>
267267
<input id="custom" class="sponsor-radio" type="radio" name="sponsor">
268268
<div class="sponsor-card">
269-
<h3>Other Amount</h3>
270-
<!-- <label for="currency-amount" id="amount-label" class="css-1mjr5kl-label-text_field_label_sm" data-ppui="true">输入金额</label> -->
269+
<h3>Custom</h3>
271270
<div class="amount-wrapper">
272271
<input id="amount-input" class="amount-input" inputmode="decimal"
273272
placeholder="Enter amount (USD)" min="1"
274273
step="1" required>
275274
<div id="amount-prefix" class="currency-symbol">$</div>
276275
<div id="amount-suffix" class="currency-symbol">USD</div>
277276
</div>
277+
278+
<!-- Payment cycle selector -->
279+
<div class="cycle-wrapper">
280+
<label class="cycle-option">
281+
<input type="radio" name="custom-cycle" value="monthly" checked>
282+
Monthly
283+
</label>
284+
<label class="cycle-option">
285+
<input type="radio" name="custom-cycle" value="onetime">
286+
One-time
287+
</label>
288+
</div>
278289
</div>
279290
</label>
280291
</div>
@@ -285,11 +296,6 @@ <h3>Other Amount</h3>
285296
<button type="submit" class="btn btn-primary btn-lg" id="confirm-btn">
286297
💖 Confirm Sponsorship
287298
</button>
288-
<!-- Monthly checkbox -->
289-
<label class="checkbox-label mb-0">
290-
<input type="checkbox" id="monthly">
291-
<span class="checkbox"></span>Monthly
292-
</label>
293299
</div>
294300

295301
<hr />
@@ -593,7 +599,7 @@ <h5 id="commonModalTitle" class="modal-title">Title</h5>
593599
<script src="/assets/js/bootstrap.bundle.min.js?v=5.3.8"></script>
594600
<script src="/assets/js/md5.js?v=2.2"></script>
595601
<script src="/assets/js/utils.js?v=2.1.0"></script>
596-
<script src="/assets/js/donate.js?v=3.0.3"></script>
602+
<script src="/assets/js/donate.js?v=3.1.0"></script>
597603
<script src="/assets/js/theme.js?v=2.1.0"></script>
598604
</body>
599605

0 commit comments

Comments
 (0)