Commit 0f8a976f authored by Aral Balkan's avatar Aral Balkan

Rewrite for better state handling

parent d768e033
......@@ -58,7 +58,7 @@ input[name="donationAmount"]
height: 0;
}
#donationForm, #progressIndicator, #thankYouMessage, #errorMessage
#patronageForm, #progressIndicator, #thankYouMessage, #errorMessage
{
margin-left: auto;
margin-right: auto;
......@@ -77,7 +77,7 @@ input[name="donationAmount"]
.donationForm
.patronageForm
{
padding: 10px;
width: 300px;
......@@ -93,7 +93,7 @@ input[name="donationAmount"]
}
@media only screen and (min-width: 380px) {
.donationForm {
.patronageForm {
padding: 10px;
width: 358px;
}
......@@ -248,6 +248,7 @@ input[name="donationAmount"] + label
line-height: 30px;
padding: 0;
-webkit-appearance: none;
opacity: 0.5;
}
@media only screen and (min-width: 380px) {
......@@ -259,7 +260,7 @@ input[name="donationAmount"] + label
}
}
#donateButton
#submitButton
{
font-size: 16px;
margin-top: 4px;
......@@ -274,13 +275,13 @@ input[name="donationAmount"] + label
background-color: #E3F9A8;
}
#donateButton:hover
#submitButton:hover
{
background-color: #CBE89B;
/* border: 2px solid #A4C776; */
}
#donateButton:disabled
#submitButton:disabled
{
color: #ccc;
background-color: #eee;
......@@ -288,7 +289,7 @@ input[name="donationAmount"] + label
}
@media only screen and (min-width: 380px) {
#donateButton
#submitButton
{
font-size: 20px;
}
......
......@@ -30,7 +30,7 @@
</div>
<!-- Donation form -->
<div id='donationForm' class='donationForm'>
<form id='patronageForm' class='patronageForm'>
<fieldset>
<legend class='hidden'>Type of donation</legend>
......@@ -51,12 +51,12 @@
<input type='radio' name='donationAmount' value='50' class='hidden selectButton' id='tier6'><label class='donationTierButtonLabel unselectable' for='tier6'>€50</label>
<input type='radio' name='donationAmount' value='100' class='hidden selectButton' id='tier7'><label class='donationTierButtonLabel unselectable' for='tier7'>€100</label>
<!-- Unfortunately can’t use an input type='number' here because the events are not fired consistently across major browsers. -->
<input type='radio' name='donationAmount' value='0' class='hidden' id='tier8'><label class='donationTierButtonLabel unselectable' id='otherDonationLabel' for='tier8'>Other<input id='otherDonationAmount' type='text' title='Custom donation amount'></input></label>
<input type='radio' name='donationAmount' value='-1' class='hidden selectButton' id='tier8'><label class='donationTierButtonLabel unselectable' id='otherDonationLabel' for='tier8'>Other<input id='otherDonationAmount' type='string' pattern='[0-9]{1,6}' title='Custom donation amount'></input></label>
</fieldset>
<button id='donateButton'>Become a patron</button>
<button type='submit' id='submitButton'>Become a patron</button>
<small class='donation-currency'>Donations are in <a href='https://transferwise.com/gb/currency-converter/currencies/eur-euro'>Euros</a></small>
</div> <!-- end of donation form -->
</form> <!-- end of donation form -->
<script src='https://js.stripe.com/v3/'></script>
<script src='js/index.js'></script>
......
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Patronage form.
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Globals
//
// Convenience method for iterating NodeList instances like Arrays.
NodeList.prototype.forEach = Array.prototype.forEach
function isOtherDonationAmountValid () {
var isValid = Number(document.getElementById('otherDonationAmount').value) > 0
return isValid
}
// Shorthand references for DOM lookup.
const $ = document.querySelector.bind(document)
const $$ = document.querySelectorAll.bind(document)
class PatronageForm {
static get CUSTOM_DONATION_AMOUNT () { return -1 }
// Submit form on Enter.
document.addEventListener('keypress', function(e){
if (e.keyCode == 13){
// Enter pressed, submit.
document.getElementById('donateButton').click()
constructor () {
window.addEventListener('load', this.setInitialInterfaceState.bind(this))
this.stripe = Stripe('pk_test_mLQRpGuO7qq3XMfSgwmt4n8U00FSZOIY1h')
}
})
// When the donation type changes, update the text on the button to reflect
// whether this is a donation or a patronship.
document.querySelectorAll('input[name="donationType"]').forEach(function(element){
element.addEventListener('change', function(e){
const donateButton = document.querySelector('#donateButton')
if (e.target.id === 'monthlyDonation') {
donateButton.innerHTML = 'Become a patron'
} else {
donateButton.innerHTML = 'Donate'
setInitialInterfaceState () {
// Store references to the interface elements.
this.interface = {
patronageForm: $('#patronageForm'),
submitButton: $('#submitButton'),
otherDonationAmountTextInput: $('#otherDonationAmount'),
otherDonationAmountRadioButton: $('#tier8'),
serverError: $('#serverError'),
errorMessage: $('#errorMessage'),
donationTypeRadioButtons: $$('input[name="donationType"]'),
donationAmountRadioButtons: $$('input[name="donationAmount"]'),
selectedDonationTypeButton: _ => document.querySelector('input[name="donationType"]:checked'),
selectedDonationAmountButton: _ => document.querySelector('input[name="donationAmount"]:checked'),
}
})
})
document.querySelector('#otherDonationAmount').addEventListener('focus', e => {
document.querySelector('#tier8').click()
})
function handleCustomDonationTier() {
// Focus the other donation amount text field to save the person another click.
const otherDonationAmountTextInput = document.getElementById('otherDonationAmount')
otherDonationAmountTextInput.style.opacity = 1
otherDonationAmountTextInput.focus()
// If it’s a custom amount, we’ll enable the donation button
// once the person has entered a valid amount in the other donation amount text field.
donateButtonDisabled = !isOtherDonationAmountValid()
document.getElementById('donateButton').disabled = donateButtonDisabled
}
// When a valid custom donation amount value is entered, enable the donation button.
document.querySelectorAll('input[name="donationAmount"]').forEach(function(element){
element.addEventListener('change', function(e){
if (e.target.getAttribute('id') == 'tier8'){
handleCustomDonationTier()
//
// Handle all interface events as a state update.
//
const updateFormState = this.updateFormState.bind(this)
this.interface.donationTypeRadioButtons.forEach(element => {
element.addEventListener('change', updateFormState)
})
this.interface.donationAmountRadioButtons.forEach(element => {
element.addEventListener('change', updateFormState)
})
this.interface.otherDonationAmountTextInput.addEventListener('focus', updateFormState)
this.interface.otherDonationAmountTextInput.addEventListener('input', updateFormState)
//
// Handle form submit requests.
//
const handleSubmitRequest = event => {
event.preventDefault()
if (this.formIsValid) {
this.redirectToCheckout()
}
}
patronageForm.addEventListener('submit', event => { handleSubmitRequest(event) })
document.addEventListener('keypress', event => { if (event.keyCode == 13) { handleSubmitRequest(event) } })
// Update the form state for the first time.
this.updateFormState()
}
// Updates form state in response to interface events.
updateFormState () {
// Handle the label of the submit button based on the type of donation.
this.interface.submitButton.innerHTML = this.isPatronage ? 'Become a patron' : 'Donate'
const otherDonationAmountTextInput = this.interface.otherDonationAmountTextInput
// If the donation amount text input has focus, ensure that the corresponding
// radio button is also selected.
if (otherDonationAmountTextInput === document.activeElement) {
this.interface.otherDonationAmountRadioButton.checked = true
}
// Handle the state of the custom donation amount control.
if (this.interface.selectedDonationAmountButton().id === 'tier8') {
// Custom donation is active
otherDonationAmountTextInput.style.opacity = 1
otherDonationAmountTextInput.focus()
let otherDonationAmountString = otherDonationAmountTextInput.value
otherDonationAmountString = otherDonationAmountString.slice(0,6) // Limit to six digits.
let otherDonationAmountInteger = parseInt(otherDonationAmountString) // Ensure it is a valid integer.
otherDonationAmountTextInput.value = isNaN(otherDonationAmountInteger) ? '' : otherDonationAmountInteger
} else {
if (!isOtherDonationAmountValid()) {
// Clear the other donation amount entry if it is not valid
// Clear the other donation amount entry if it is not valid and
// it isn’t selected.
if (!this.otherDonationAmountIsValid) {
document.getElementById('otherDonationAmount').value = ''
} else {
// Just disable it.
document.getElementById('otherDonationAmount').style.opacity = 0.5
}
document.getElementById('donateButton').disabled = false
// De-emphasize the text input when the control is not in focus.
document.getElementById('otherDonationAmount').style.opacity = 0.5
}
})
})
// Enable/disable the submit button based on whether the form is valid.
this.interface.submitButton.disabled = !this.formIsValid
}
// Validation: other donation amount.
document.getElementById('otherDonationAmount').addEventListener('focus', function(e){
// Make sure that the other donation amount radio button is selected.
// (e.g., if the person moved to the text field via a screenreader, it may not have been.)
document.getElementById('tier8').checked = true
})
// Redirects to Stripe checkout.
async redirectToCheckout () {
const host = `https://${window.location.hostname}`
const stripeRedirect = this.isPatronage ? this.stripe.redirectToCheckout({
//
// Patronage.
//
items: [
{plan: 'plan_FSsO2vwva5oEOP', quantity: this.donationAmount}
],
successUrl: `${host}/patronage/?id={CHECKOUT_SESSION_ID}`,
cancelUrl: `${host}/fund-us/cancel`,
}) : this.stripe.redirectToCheckout({
//
// Donation.
//
items: [
{sku: 'sku_FVm0elVvrMW0sX', quantity: this.donationAmount}
],
successUrl: `${host}/fund-us/thank-you`,
cancelUrl: `${host}/fund-us/cancel`,
})
const stripeResult = await stripeRedirect
if (stripeResult.error) {
showError(result.error.message)
}
}
document.getElementById('otherDonationAmount').addEventListener('input', function(e){
// Set the state of the donation button based on whether a valid amount has been entered.
document.getElementById('donateButton').disabled = !isOtherDonationAmountValid()
})
get formIsValid () {
try { this.donationType } catch (error) { return false }
try { this.donationAmount } catch (error) { return false }
return true
}
// Returns the donation amount from the form.
function getDonationAmount () {
var selectedDonationAmount = Number(document.querySelector('input[name="donationAmount"]:checked').value)
// If the person has not chosen a donation amount, check if they entered a custom amount.
if (selectedDonationAmount == 0)
{
selectedDonationAmount = Number(document.getElementById('otherDonationAmount').value)
get isPatronage () {
return this.donationType === 'monthly'
}
return selectedDonationAmount
}
get donationType () {
const selectedDonationTypeButton = this.interface.selectedDonationTypeButton()
if (selectedDonationTypeButton !== null) {
return selectedDonationTypeButton.value
} else {
throw new Error('donation type is invalid')
}
}
// There’s been an error.
function showError (error) {
var serverErrorDetails = document.getElementById('serverError');
serverErrorDetails.innerHTML = response.body.error;
serverErrorDetails.classList.remove('displayNone');
var errorMessage = document.getElementById('errorMessage');
errorMessage.classList.remove('displayNone');
}
// Donate button: submits the form.
document.getElementById('donateButton').addEventListener('click', function(e) {
e.preventDefault()
// Returns the donation amount from the form.
get donationAmount () {
const selectedDonationAmountButton = this.interface.selectedDonationAmountButton()
const host = `https://${window.location.hostname}`
if (selectedDonationAmountButton !== null) {
const amount = Number(selectedDonationAmountButton.value)
return (amount === PatronageForm.CUSTOM_DONATION_AMOUNT) ? this.customDonationAmount : amount
} else {
throw new Error('donation amount is invalid')
}
}
// Get the details we need for the server call.
var selectedDonationType = document.querySelector('input[name="donationType"]:checked').value
var isDonationRecurring = (selectedDonationType == 'monthly')
var donationAmount = getDonationAmount()
const stripeRedirect = isDonationRecurring ? stripe.redirectToCheckout({
//
// Patronage.
//
items: [
{plan: 'plan_FSsO2vwva5oEOP', quantity: donationAmount}
],
successUrl: `${host}/patronage/?id={CHECKOUT_SESSION_ID}`,
cancelUrl: `${host}/fund-us/cancel`,
}) : stripe.redirectToCheckout({
//
// Donation.
//
items: [
{sku: 'sku_FVm0elVvrMW0sX', quantity: donationAmount}
],
successUrl: `${host}/fund-us/thank-you`,
cancelUrl: `${host}/fund-us/cancel`,
})
stripeRedirect.then(function (result) {
if (result.error) {
showError(result.error.message)
get customDonationAmount () {
const amount = Number(this.interface.otherDonationAmountTextInput.value)
if (amount > 0) {
return amount
} else {
throw new Error('invalid custom donation amount')
}
})
}
get otherDonationAmountIsValid () {
return Number(this.interface.otherDonationAmountTextInput.value) > 0
}
})
const stripe = Stripe('pk_test_mLQRpGuO7qq3XMfSgwmt4n8U00FSZOIY1h')
showError (error) {
var serverErrorDetails = document.getElementById('serverError');
serverErrorDetails.innerHTML = response.body.error;
serverErrorDetails.classList.remove('displayNone');
var errorMessage = document.getElementById('errorMessage');
errorMessage.classList.remove('displayNone');
}
}
new PatronageForm()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment