Merge pull request #2 from koodiklinikka/foobar

New loader animation & success icon for successful registration
This commit is contained in:
Riku Rouvila
2017-07-08 21:04:52 +01:00
committed by GitHub
11 changed files with 266 additions and 236 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -6,11 +6,11 @@ var development = {
}
var production = {
stripe: {
publicKey: "pk_live_xrnwdLNXbt20LMxpIDffJnnC"
}
stripe: {
publicKey: "pk_live_xrnwdLNXbt20LMxpIDffJnnC"
}
}
module.exports = function() {
module.exports = function () {
return process.env.ENV == 'development' ? development : production;
}

View File

@@ -5,7 +5,7 @@ var React = require('react');
var classSet = require('classnames');
var api = require('../api');
var Loader = require('./loader');
module.exports = React.createClass({
getInitialState() {
return {
@@ -99,11 +99,11 @@ module.exports = React.createClass({
disabled={this.state.error || this.state.submitted}>
</button>
<span
className='loader'>
</span>
<div className='invite-form__loader'>
<Loader />
</div>
{feedbackMessage}
</form>
)
);
}
});

View File

@@ -0,0 +1,12 @@
const React = require('react');
module.exports = function Loader() {
return (
<div className='sk-folding-cube'>
<div className='sk-cube1 sk-cube'></div>
<div className='sk-cube2 sk-cube'></div>
<div className='sk-cube4 sk-cube'></div>
<div className='sk-cube3 sk-cube'></div>
</div>
);
};

View File

@@ -1,50 +1,34 @@
'use strict';
var classSet = require('classnames');
var React = require('react');
var request = require('axios');
var MembershipInfoForm = require('./membershipInfoForm.js');
var StripeCheckout = require('./stripeCheckout.js');
import React from 'react';
import MembershipInfoForm from './membershipInfoForm';
module.exports = React.createClass({
getInitialState() {
return {
infoFormSuccess: false,
paymentSuccess: false,
userInfo: null
paymentSuccess: false
};
},
handlePaymentSuccess() {
this.setState({ paymentSuccess: true });
},
handleInfoFormSuccess(userInfo) {
this.setState({
infoFormSuccess: true,
userInfo: userInfo
});
},
render() {
if(!this.state.infoFormSuccess) {
return <MembershipInfoForm onSuccess={ this.handleInfoFormSuccess }></MembershipInfoForm>
} else if (!this.state.paymentSuccess) {
if(!this.state.paymentSuccess) {
return (
<StripeCheckout
userInfo = { this.state.userInfo }
onPaymentSuccess = { this.handlePaymentSuccess }>
</StripeCheckout>)
} else {
return (
<div>
<p> Maksu ja rekisteröityminen onnistui.</p>
<p> Tervetuloa Koodiklinikka ry:n jäseneksi!</p>
</div>
)
<MembershipInfoForm onPaymentSuccess={this.handlePaymentSuccess} />
);
}
return (
<div>
<svg height='50' width='50' viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'>
<path
fill='#349c4a'
d='M256 6.998c-137.533 0-249 111.467-249 249 0 137.534 111.467 249 249 249s249-111.467 249-249c0-137.534-111.467-249-249-249zm0 478.08c-126.31 0-229.08-102.77-229.08-229.08 0-126.31 102.77-229.08 229.08-229.08 126.31 0 229.08 102.77 229.08 229.08 0 126.31-102.77 229.08-229.08 229.08z' />
<path
fill='#349c4a'
d='M384.235 158.192L216.92 325.518 127.86 236.48l-14.142 14.144 103.2 103.18 181.36-181.47' />
</svg>
<p> Maksu ja rekisteröityminen onnistui.</p>
<p> Tervetuloa Koodiklinikka ry:n jäseneksi!</p>
</div>
);
}
});

View File

@@ -1,21 +1,23 @@
'use strict';
var _ = require('lodash');
var request = require('axios');
var React = require('react');
var _ = require('lodash');
var request = require('axios');
var React = require('react');
var classSet = require('classnames');
var StripeCheckout = require('react-stripe-checkout').default;
var api = require('../api');
var StripeCheckout = require('./stripeCheckout.js');
var api = require('../api');
var Loader = require('./loader');
var config = require('../../config.js')();
var fieldNameTranslations = {
address: { fi: "Osoite" },
city: { fi: "Paikkakunta" },
email: { fi: "Sähköpostiosoite" },
handle: { fi: "Slack-käyttäjätunnus "},
name: { fi: "Koko nimi "},
postcode: { fi: "Postinumero" }
}
address: { fi: 'Osoite' },
city: { fi: 'Paikkakunta' },
email: { fi: 'Sähköpostiosoite' },
handle: { fi: 'Slack-käyttäjätunnus ' },
name: { fi: 'Koko nimi ' },
postcode: { fi: 'Postinumero' }
};
function validateEmail(email) {
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
@@ -24,54 +26,51 @@ function validateEmail(email) {
const fieldNames = ['name', 'email', 'handle', 'address', 'city', 'postcode'];
function getUserInfo(state) {
return _.pick(state, fieldNames);
}
module.exports = React.createClass({
getInitialState() {
return {
address: '',
city: '',
email: '',
errors: [],
handle: '',
name: '',
postcode: '',
sending: false
address: '',
city: '',
email: '',
handle: '',
name: '',
postcode: '',
sending: false,
pristineFields: fieldNames
};
},
onSubmit(e) {
e.preventDefault();
onSubmit(token) {
this.setState({
sending: true,
errors: []
error: null
});
if (this.getDataErrors().length) {
this.setState({
sending: false,
errors: userInfoErrors
request.post(api('membership'), {
userInfo: getUserInfo(this.state),
stripeToken: token.id
})
.then(() => {
this.setState({ sending: false });
this.props.onPaymentSuccess();
})
.catch((err) => {
this.setState({ error: err, sending: false });
});
} else {
this.props.onSuccess({
email: this.state.email,
name: this.state.name,
handle: this.state.handle,
address: this.state.address,
postcode: this.state.postcode,
city: this.state.city,
});
}
},
handleError(err) {
this.setState({ error: err, sending: false });
},
onChange(e) {
if (e.target.value === this.state[e.target.name]) {
var name = e.target.name;
if (e.target.value === this.state[name]) {
return;
}
this.setState({
[e.target.name]: e.target.value,
pristineFields: this.state.pristineFields.filter((fieldName) => fieldName !== name),
errors: []
});
},
@@ -80,84 +79,117 @@ module.exports = React.createClass({
var foundErrors = [];
fieldNames.forEach((fieldName) => {
if(!this.state[fieldName])
foundErrors.push({ field: fieldName, type: 'missing' })
})
if (!this.state[fieldName]) {
foundErrors.push({ field: fieldName, type: 'missing' });
}
});
if(this.state.email && !validateEmail(this.state.email))
if (this.state.email && !validateEmail(this.state.email)) {
foundErrors.push({ field: 'email', type: 'invalid' });
}
return foundErrors;
},
render() {
const inputErrors = this.getDataErrors();
var formClasses = classSet({
'form': true,
'form': true,
'membership-form': true,
'has-error': this.state.errors.length,
'sending': this.state.sending
'has-error': inputErrors.length !== 0 || this.state.error,
'sending': this.state.sending
});
/* generate error messages */
var feedbackMessages = [];
var fieldsWithErrors = [];
this.state.errors.forEach((err, i) => {
function getErrorMessage(err) {
var feedbackText;
fieldsWithErrors.push(err.field);
if(err.type == 'missing') {
feedbackText = `${ fieldNameTranslations[err.field].fi } on pakollinen.`
} else if (err.type == 'invalid') {
feedbackText = `${ fieldNameTranslations[err.field].fi } on virheellinen.`
if (err.type === 'missing') {
feedbackText = `${fieldNameTranslations[err.field].fi} on pakollinen.`;
} else if (err.type === 'invalid') {
feedbackText = `${fieldNameTranslations[err.field].fi} on virheellinen.`;
}
feedbackMessages.push((<div key={i} className='form--message'>{ feedbackText }</div>))
});
return <div key={err.field} className='form--message'>{feedbackText}</div>;
}
/* generate error messages */
var visibleErrors = inputErrors
.filter((error) => this.state.pristineFields.indexOf(error.field) === -1);
/* generate input fields */
var inputFields = [];
var fieldsWithErrors = visibleErrors.map(({ field }) => field);
fieldNames.forEach((fieldName) => {
var inputFields = fieldNames.map((fieldName) => {
var inputClasses = classSet({
'input': true,
'has-error': _.includes(fieldsWithErrors, fieldName),
'half': fieldName == 'city' || fieldName == 'postcode',
'left': fieldName == 'city'
'half': fieldName === 'city' || fieldName === 'postcode',
'left': fieldName === 'city'
});
inputFields.push((
<input
key = { fieldName }
className = { inputClasses }
type = { fieldName == 'email' ? 'email' : 'text' }
name = { fieldName }
placeholder = { fieldNameTranslations[fieldName].fi }
value = { this.state[fieldName] }
onChange = { this.onChange } />
))
})
function showsErrorFor(field) {
if (fieldName === 'city') {
return false;
}
return field === fieldName || fieldName === 'postcode' && field === 'city';
}
return (
<span key={fieldName}>
<input
className={inputClasses}
type={fieldName === 'email' ? 'email' : 'text'}
name={fieldName}
placeholder={fieldNameTranslations[fieldName].fi}
value={this.state[fieldName]}
onChange={this.onChange} />
{
visibleErrors
.filter(({ field }) => showsErrorFor(field))
.map(getErrorMessage)
}
</span>
);
});
if (this.state.sending) {
return (
<div className='membership-form__loader'>
<Loader />
</div>
);
}
return (
<div>
<form className={ formClasses } onSubmit={ this.onSubmit }>
{ feedbackMessages }
{ inputFields }
<button
className = 'btn btn__submit'
type = 'submit'
title = 'Lähetä'
disabled = { this.state.errors.length || this.state.submitted }>
Siirry maksamaan
</button>
<span
className='loader'>
</span>
<h3>Liity jäseneksi</h3>
<form className={formClasses}>
{inputFields}
{this.state.error && (
<div className='form--message'>
Jotain meni pieleen! Ota yhteyttä info@koodiklinikka.fi
</div>
)}
<StripeCheckout
amount={1000}
currency='EUR'
description='Jäsenmaksu'
email={this.state.email}
image='https://avatars3.githubusercontent.com/u/10520119?v=3&s=200'
locale='fi'
name='Koodiklinikka ry'
stripeKey={config.stripe.publicKey}
token={this.onSubmit}
>
<button
type='button'
disabled={inputErrors.length !== 0}
className='btn btn__submit'>
Siirry maksamaan
</button>
</StripeCheckout>
</form>
</div>
)
);
}
});

View File

@@ -1,66 +0,0 @@
'use strict';
var request = require('axios');
var React = require('react');
var classSet = require('classnames');
import StripeCheckout from 'react-stripe-checkout';
var api = require('../api');
var config = require('../../config.js')();
module.exports = React.createClass({
getInitialState() {
return {
error: null,
sending: false
};
},
onSubmit(token) {
this.setState({
error: null,
sending: true
});
request.post(api('membership'), {
userInfo: this.props.userInfo,
stripeToken: token.id
})
.then(() => {
this.setState({
sending: false
});
this.props.onPaymentSuccess();
})
.catch((e) => {
this.setState({
error: e,
sending: false
});
});
},
render() {
if (this.state.error) {
return <p>Virhe maksaessa! Ota yhteyttä info@koodiklinikka.fi</p>
} else if (this.state.sending) {
return <img src="../images/ajax-loader.gif" alt="Odota hetki..." height="42" width="42"></img>
} else {
return (<StripeCheckout
amount = { 1000 }
currency = 'EUR'
description = 'Jäsenmaksu'
email = { this.props.userInfo.email }
image = 'https://avatars3.githubusercontent.com/u/10520119?v=3&s=200'
locale = "en"
name = 'Koodiklinikka ry'
stripeKey = { config.stripe.publicKey }
token = { this.onSubmit }
>
<button className="btn btn-primary">
Maksa kortilla
</button>
</StripeCheckout>)
}
}
});

View File

@@ -35,13 +35,33 @@ headerHeight = 400px
top 20px
a
color white
font-size 18px
font-size 16px
font-weight bold
margin-left 16px
text-shadow 0 2px 0 rgba(0,0,0,0.1)
@media screen and (max-width: 1030px)
position relative
@media screen and (max-width: 420px)
display block
margin-top 5px
&:last-child
margin-left 1.5em
&:before
content '!'
font-size 11px
line-height 15px
width 14px
height 14px
text-align center
background #ec3d3d
display inline-block
margin-right 5px
top -5px
right -15px
border-radius 100%
position absolute
text-shadow none
.header__headline
display table-cell
padding 0 1em

55
src/styles/_loader.styl Normal file
View File

@@ -0,0 +1,55 @@
.sk-folding-cube
margin auto
width 100%
height 100%
position relative
transform rotateZ(45deg)
.sk-folding-cube .sk-cube
float left
width 50%
height 50%
position relative
transform scale(1.1)
.sk-folding-cube .sk-cube:before
content ''
position absolute
top 0
left 0
width 100%
height 100%
background-color linkColor
animation sk-foldCubeAngle 2.4s infinite linear both
transform-origin 100% 100%
.sk-folding-cube .sk-cube2
transform scale(1.1) rotateZ(90deg)
.sk-folding-cube .sk-cube3
transform scale(1.1) rotateZ(180deg)
.sk-folding-cube .sk-cube4
transform scale(1.1) rotateZ(270deg)
.sk-folding-cube .sk-cube2:before
animation-delay 0.3s
.sk-folding-cube .sk-cube3:before
animation-delay 0.6s
.sk-folding-cube .sk-cube4:before
animation-delay 0.9s
@keyframes sk-foldCubeAngle
0%, 10%
transform perspective(140px) rotateX(-180deg)
opacity 0
25%, 75%
transform perspective(140px) rotateX(0deg)
opacity 1
90%, 100%
transform perspective(140px) rotateY(180deg)
opacity 0

View File

@@ -6,6 +6,7 @@ footerHeight = 50px
@require '_input'
@require '_button'
@require '_header'
@require '_loader'
body, html
margin 0
@@ -112,28 +113,16 @@ section:first-child
&:first-child
margin-top 0
.loader
background transparent url('../images/ajax-loader.gif') no-repeat center center
width 28px
height 28px
.form
.btn
background linkColor
border-bottom 2px solid #117280
color rgba(255, 255, 255, 0.9)
.loader
display none
&.sending
.btn
display none
.loader
.invite-form__loader
display block
margin-left auto
margin-right auto
width 28px
height 28px
.invite-form
position relative
.btn
@@ -146,15 +135,13 @@ section:first-child
color rgba(255, 255, 255, 0.5)
&:active
border-bottom 0
.loader
.invite-form__loader
display none
width 20px
height 20px
position absolute
right 9px
top 9px
&.sending
.loader
display block
width 28px
height 28px
right 14px
top 14px
.membership-form
.input
@@ -162,6 +149,11 @@ section:first-child
.btn
margin-top 12px
.membership-form__loader
width 70px
height 70px
margin auto
.stripe-form
margin 20px 0px
.name
@@ -218,7 +210,7 @@ footer
justify-content space-between
flex-wrap wrap
text-align center
@media screen and (max-width: 760px)
@media screen and (max-width: 940px)
display block
i
margin 0 0.30em
@@ -230,7 +222,7 @@ footer
display flex
flex-direction column
justify-content center
@media screen and (max-width: 760px)
@media screen and (max-width: 940px)
margin-top 1em
.sponsors
@@ -251,16 +243,17 @@ footer
height 40px
margin-right 1em
vertical-align middle
@media screen and (max-width: 760px)
@media screen and (max-width: 940px)
margin-top 1em
.sponsor__futurice, .sponsor__metosin, .sponsor__leonidas
height 30px
.sponsor__nordea
height 25px
margin-top: -3px
margin-top -3px
@media screen and (max-width: 940px)
margin-top 1em
.feed
width feedWidth
@@ -353,11 +346,12 @@ footer
.bread-img
background url('../images/hp3_bw.jpg')
background-size 120%
background-position-y 80%
border-radius 0px
background-size cover
border-radius 160px
opacity 0.85
width 320px
height 320px
margin auto
@media screen and (max-width: 700px)
.bread-img

View File

@@ -32,7 +32,6 @@ block content
.row
.bread
.column.column1-2
h3 Liity jäseneksi
#membership-form.form
.column.column1-2