Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Small Technology Foundation web site
spikes
patronage
Commits
0f8a976f
Verified
Commit
0f8a976f
authored
Aug 01, 2019
by
Aral Balkan
Browse files
Rewrite for better state handling
parent
d768e033
Changes
3
Hide whitespace changes
Inline
Side-by-side
fund-us/css/index.css
View file @
0f8a976f
...
...
@@ -58,7 +58,7 @@ input[name="donationAmount"]
height
:
0
;
}
#
don
at
i
onForm
,
#progressIndicator
,
#thankYouMessage
,
#errorMessage
#
p
at
r
on
age
Form
,
#progressIndicator
,
#thankYouMessage
,
#errorMessage
{
margin-left
:
auto
;
margin-right
:
auto
;
...
...
@@ -77,7 +77,7 @@ input[name="donationAmount"]
.
don
at
i
onForm
.
p
at
r
on
age
Form
{
padding
:
10px
;
width
:
300px
;
...
...
@@ -93,7 +93,7 @@ input[name="donationAmount"]
}
@media
only
screen
and
(
min-width
:
380px
)
{
.
don
at
i
onForm
{
.
p
at
r
on
age
Form
{
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
}
}
#
donate
Button
#
submit
Button
{
font-size
:
16px
;
margin-top
:
4px
;
...
...
@@ -274,13 +275,13 @@ input[name="donationAmount"] + label
background-color
:
#E3F9A8
;
}
#
donate
Button
:hover
#
submit
Button
:hover
{
background-color
:
#CBE89B
;
/* border: 2px solid #A4C776; */
}
#
donate
Button
:disabled
#
submit
Button
:disabled
{
color
:
#ccc
;
background-color
:
#eee
;
...
...
@@ -288,7 +289,7 @@ input[name="donationAmount"] + label
}
@media
only
screen
and
(
min-width
:
380px
)
{
#
donate
Button
#
submit
Button
{
font-size
:
20px
;
}
...
...
fund-us/index.html
View file @
0f8a976f
...
...
@@ -30,7 +30,7 @@
</div>
<!-- Donation form -->
<
div
id=
'
don
at
i
onForm'
class=
'
don
at
i
onForm'
>
<
form
id=
'
p
at
r
on
age
Form'
class=
'
p
at
r
on
age
Form'
>
<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=
'donate
Button'
>
Become a patron
</button>
<button
type=
'submit'
id=
'submit
Button'
>
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>
...
...
fund-us/js/index.js
View file @
0f8a976f
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// 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
()
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment