diff --git a/package.json b/package.json index 516dcad..af481f1 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "scripts": { "start": "rm -rf public && gulp", "build": "rm -rf public && gulp build", + "dev": "SERVER=http://localhost:9000/ ENV=development npm start", + "prod": "ENV=production npm start", "lint": "eslint src", "test": "mocha src/**/__tests__/*.js --compilers js:babel-core/register --require test/test-helper" }, diff --git a/src/assets/images/hp3_bw.jpg b/src/assets/images/hp3_bw.jpg new file mode 100644 index 0000000..c8ba56b Binary files /dev/null and b/src/assets/images/hp3_bw.jpg differ diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..43576a3 --- /dev/null +++ b/src/config.js @@ -0,0 +1,16 @@ + +var development = { + stripe: { + publicKey: "pk_test_OmNve9H1OuORlmD4rblpjgzh" + } +} + +var production = { + stripe: { + publicKey: "pk_live_xrnwdLNXbt20LMxpIDffJnnC" + } +} + +module.exports = function() { + return process.env.ENV == 'development' ? development : production; +} diff --git a/src/index.jade b/src/index.jade index 665b339..bcba216 100644 --- a/src/index.jade +++ b/src/index.jade @@ -33,6 +33,7 @@ html meta(name='msapplication-TileColor', content='#10558c') meta(name='msapplication-TileImage', content='icons/mstile-144x144.png') meta(property='og:image', content='images/logo.png') + script(src='https://js.stripe.com/v3/') script(src='//use.typekit.net/scb5xny.js') script. try{Typekit.load();}catch(e){}; @@ -44,15 +45,18 @@ html video(autoplay, loop, poster='images/poster.jpg', class='header__video-bg') source(src='videos/jumbo.mp4', type='video/mp4') .header__container + .header__nav + a(href='/') etusivu + a(href='/yhdistys.html') yhdistys .header__headline .header__logo h1.header__title Slack-yhteisö kaikille ohjelmoinnista ja ohjelmistoalasta kiinnostuneille harrastajille ja ammattilaisille. - .content + .content.with-feed section .row h3 Tule mukaan - #invite-form + #invite-form.form section .row @@ -144,7 +148,6 @@ html i.fa.fa-facebook div#email - #fader // inject:js // endinject diff --git a/src/js/components/inviteForm.js b/src/js/components/inviteForm.js index 357419c..768b235 100644 --- a/src/js/components/inviteForm.js +++ b/src/js/components/inviteForm.js @@ -48,6 +48,7 @@ module.exports = React.createClass({ }, render() { var formClasses = classSet({ + 'form': true, 'invite-form': true, 'has-success': this.state.submitted, 'has-error': this.state.error, @@ -76,7 +77,7 @@ module.exports = React.createClass({ } feedbackMessage = ( -
+
{messageText}
); diff --git a/src/js/components/members.js b/src/js/components/members.js index e0e4013..a907016 100644 --- a/src/js/components/members.js +++ b/src/js/components/members.js @@ -7,7 +7,7 @@ var _ = require('lodash'); var api = require('../api'); module.exports = React.createClass({ - getInitialState() { + getInitialState() { return { members: [] }; diff --git a/src/js/components/membershipForm.js b/src/js/components/membershipForm.js new file mode 100644 index 0000000..d40287c --- /dev/null +++ b/src/js/components/membershipForm.js @@ -0,0 +1,50 @@ +'use strict'; + +var classSet = require('classnames'); +var React = require('react'); +var request = require('axios'); + +var MembershipInfoForm = require('./membershipInfoForm.js'); +var StripeCheckout = require('./stripeCheckout.js'); + +module.exports = React.createClass({ + getInitialState() { + return { + infoFormSuccess: false, + paymentSuccess: false, + userInfo: null + }; + }, + + handlePaymentSuccess() { + this.setState({ paymentSuccess: true }); + }, + + handleInfoFormSuccess(userInfo) { + this.setState({ + infoFormSuccess: true, + userInfo: userInfo + }); + }, + + render() { + if(!this.state.infoFormSuccess) { + return + + } else if (!this.state.paymentSuccess) { + return ( + + ) + + } else { + return ( +
+

Maksu ja rekisteröityminen onnistui.

+

Tervetuloa Koodiklinikka ry:n jäseneksi!

+
+ ) + } + } +}); diff --git a/src/js/components/membershipInfoForm.js b/src/js/components/membershipInfoForm.js new file mode 100644 index 0000000..ec43371 --- /dev/null +++ b/src/js/components/membershipInfoForm.js @@ -0,0 +1,166 @@ +'use strict'; + +var _ = require('lodash'); +var request = require('axios'); +var React = require('react'); +var classSet = require('classnames'); + +var api = require('../api'); +var StripeCheckout = require('./stripeCheckout.js'); + +var fieldNameTranslations = { + email: { fi: "Sähköpostiosoite" }, + name: { fi: "Koko nimi "}, + handle: { fi: "Slack-käyttäjätunnus "}, + residence: { fi: "Paikkakunta" } +} + +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,}))$/; + return re.test(email); +} + +module.exports = React.createClass({ + + getInitialState() { + return { + email: '', + name: '', + handle: '', + residence: '', + sending: false, + errors: [] + }; + }, + onSubmit(e) { + e.preventDefault(); + + this.setState({ + sending: true, + errors: [] + }); + + + var userInfo = { + email: this.state.email, + name: this.state.name, + handle: this.state.handle, + residence: this.state.residence, + } + + var userInfoErrors = this.getDataErrors(); + + if(userInfoErrors.length){ + this.setState({ + sending: false, + errors: userInfoErrors + }); + } else { + this.props.onSuccess(userInfo); + } + }, + handleError(err) { + this.setState({ error: err, sending: false }); + }, + onChange(e) { + if(e.target.value === this.state[e.target.name]) { + return; + } + + this.setState({ + [e.target.name]: e.target.value, + errors: [] + }); + }, + + getDataErrors() { + var foundErrors = []; + + if (!this.state.name) + foundErrors.push({ field: 'name', type: 'missing' }); + + if (!this.state.email) + foundErrors.push({ field: 'email', type: 'missing' }); + else if(!validateEmail(this.state.email)) + foundErrors.push({ field: 'email', type: 'invalid' }); + + if (!this.state.handle) + foundErrors.push({ field: 'handle', type: 'missing' }); + + if (!this.state.residence) + foundErrors.push({ field: 'residence', type: 'missing' }); + + + return foundErrors; + }, + + render() { + var formClasses = classSet({ + 'form': true, + 'membership-form': true, + 'has-error': this.state.errors.length, + 'sending': this.state.sending + }); + + + /* generate error messages */ + var feedbackMessages = []; + var fieldsWithErrors = []; + + this.state.errors.forEach((err, i) => { + 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.` + } + + feedbackMessages.push((
{ feedbackText }
)) + }); + + + /* generate input fields */ + var fieldNames = ['name', 'email', 'handle', 'residence']; + var inputFields = []; + + fieldNames.forEach((fieldName) => { + var inputClasses = classSet({ + 'input': true, + 'has-error': _.includes(fieldsWithErrors, fieldName) + }); + + inputFields.push(( + + )) + }) + + return ( +
+
+ { feedbackMessages } + { inputFields } + + + +
+
+ ) + } +}); diff --git a/src/js/components/stripeCheckout.js b/src/js/components/stripeCheckout.js new file mode 100644 index 0000000..78c4ad2 --- /dev/null +++ b/src/js/components/stripeCheckout.js @@ -0,0 +1,66 @@ +'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'), { + email: this.props.payerEmail, + 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

Virhe maksaessa! Ota yhteyttä info@koodiklinikka.fi

+ } else if (this.state.sending) { + return Odota hetki... + } else { + return ( + + ) + } + } +}); diff --git a/src/js/main.js b/src/js/main.js index d5b35bc..64f7d72 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -3,31 +3,49 @@ require('./ga'); let ReactDOM = require('react-dom'); var React = require('react'); - var inviteForm = React.createFactory(require('./components/inviteForm')); var fader = React.createFactory(require('./components/fader')); var members = React.createFactory(require('./components/members')); var feed = React.createFactory(require('./components/feed')); var email = React.createFactory(require('./components/email')); +var membershipForm = React.createFactory(require('./components/membershipForm')); -ReactDOM.render( - inviteForm(), - document.getElementById('invite-form')); +const pathName = window.location.pathname; -ReactDOM.render( - fader(), - document.getElementById('fader')); +if(pathName == '/') { + ReactDOM.render( + inviteForm(), + document.getElementById('invite-form')); + ReactDOM.render( + fader(), + document.getElementById('fader')); -ReactDOM.render( - members(), - document.getElementById('members')); + ReactDOM.render( + members(), + document.getElementById('members')); + ReactDOM.render( + feed(), + document.getElementById('feed')); -ReactDOM.render( - feed(), - document.getElementById('feed')); + ReactDOM.render( + email(), + document.getElementById('email')); +} else if (pathName == '/yhdistys.html') { + ReactDOM.render( + membershipForm(), + document.getElementById('membership-form')); -ReactDOM.render( - email(), - document.getElementById('email')); + ReactDOM.render( + fader(), + document.getElementById('fader')); + + ReactDOM.render( + email(), + document.getElementById('email')); + + ReactDOM.render( + members(), + document.getElementById('members')); +} diff --git a/src/styles/_header.styl b/src/styles/_header.styl index 0bffafa..ca3ee77 100644 --- a/src/styles/_header.styl +++ b/src/styles/_header.styl @@ -23,6 +23,23 @@ vertical-align middle width 100% +.header__nav + position absolute + right 60px + top 32px + + @media screen and (max-width: 1030px) + right 40px + top 20px + a + color white + font-size 18px + margin-left 16px + text-shadow 0 2px 0 rgba(0,0,0,0.1) + @media screen and (max-width: 1030px) + display block + margin-top 5px + .header__headline display table-cell padding 0 1em @@ -85,3 +102,21 @@ @media (min-aspect-ratio: 1/2) width 100% height auto + +.header__members + z-index -1 + position absolute + top 0 + left 0 + height 100% + .member + margin 0 + border-radius 0 + width (100/18)% + + @media screen and (max-width: 1200px) + width (100/15)% + @media screen and (max-width: 810px) + width 10% + @media screen and (min-width: 2000px) + width 5% diff --git a/src/styles/style.styl b/src/styles/style.styl index c1a6b38..fe3b597 100644 --- a/src/styles/style.styl +++ b/src/styles/style.styl @@ -25,6 +25,9 @@ h1, h2, h3 h2 margin-bottom 1em +h3 + font-size 1.25em + p margin-top 1em line-height 1.5em @@ -51,13 +54,16 @@ section .content z-index 2 position relative - padding-right feedWidth min-height 50vh box-sizing border-box @media screen and (max-width: 700px) h3 margin-top 0 + + &.with-feed + @media screen and (min-width: 1100px) + padding-right feedWidth section:first-child box-shadow -1px -1px 1px rgba(0, 0, 0, 0.05) border-bottom 1px solid #EEEEEE @@ -104,18 +110,38 @@ section:first-child padding 0 &: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 + display block + margin-left auto + margin-right auto + width 28px + height 28px + .invite-form position relative .btn width 40px height 32px - background linkColor padding 0 position absolute right 8px top 7px - - border-bottom 2px solid #117280 color rgba(255, 255, 255, 0.5) &:active border-bottom 0 @@ -123,17 +149,26 @@ section:first-child position absolute right 9px top 9px - width 0px - height 0px - background transparent url('../images/ajax-loader.gif') no-repeat center center - display block &.sending - .btn - display none .loader + display block width 28px height 28px +.membership-form + .input + margin 8px 0px + .btn + margin-top 12px + +.stripe-form + margin 20px 0px + .name + margin-top 20px + text-align left + display block + color rgba(0, 0, 0, 0.4) + @keyframes drop 0% transform rotateX(90deg) @@ -146,7 +181,7 @@ section:first-child 100% transform rotateX(0deg) -.invite-form--message +.form--message background linkColor color #fff line-height 40px @@ -156,8 +191,8 @@ section:first-child transform-origin 100% 0 animation drop 0.6s linear -.invite-form.has-error - .invite-form--message +.form.has-error + .form--message background rgb(226, 33, 112) .members @@ -314,3 +349,15 @@ footer display block .column display block + +.bread-img + background url('../images/hp3_bw.jpg') + background-size 120% + background-position-y 80% + border-radius 0px + opacity 0.85 + height 320px + +@media screen and (max-width: 700px) + .bread-img + display none \ No newline at end of file diff --git a/src/yhdistys.jade b/src/yhdistys.jade new file mode 100644 index 0000000..fbc0b46 --- /dev/null +++ b/src/yhdistys.jade @@ -0,0 +1,120 @@ +doctype html +html + head + title Yhdistys + // inject:css + // endinject + meta(name='description', content='Koodiklinikka on suomalainen Slack-yhteisö ohjelmistoalan harrastajille ja ammattilaisille.') + meta(name='keywords', content='ohjelmointi,frontend,open source,devaus,suomi,javascript,clojure,go,java,node.js,io.js,angular.js,web') + meta(charset='utf-8') + meta(name='viewport', content='width=device-width, initial-scale=1') + meta(name='apple-mobile-web-app-capable', content='yes') + + script. + if(location.hostname === 'koodiklinikka.fi' && location.protocol !== 'https:') { + location.protocol = 'https'; + } + link(rel='apple-touch-icon', sizes='57x57', href='icons/apple-touch-icon-57x57.png') + link(rel='apple-touch-icon', sizes='114x114', href='icons/apple-touch-icon-114x114.png') + link(rel='apple-touch-icon', sizes='72x72', href='icons/apple-touch-icon-72x72.png') + link(rel='apple-touch-icon', sizes='144x144', href='icons/apple-touch-icon-144x144.png') + link(rel='apple-touch-icon', sizes='60x60', href='icons/apple-touch-icon-60x60.png') + link(rel='apple-touch-icon', sizes='120x120', href='icons/apple-touch-icon-120x120.png') + link(rel='apple-touch-icon', sizes='76x76', href='icons/apple-touch-icon-76x76.png') + link(rel='apple-touch-icon', sizes='152x152', href='icons/apple-touch-icon-152x152.png') + link(rel='apple-touch-icon', sizes='180x180', href='icons/apple-touch-icon-180x180.png') + link(rel='icon', type='image/png', href='icons/favicon-192x192.png', sizes='192x192') + link(rel='icon', type='image/png', href='icons/favicon-160x160.png', sizes='160x160') + link(rel='icon', type='image/png', href='icons/favicon-96x96.png', sizes='96x96') + link(rel='icon', type='image/png', href='icons/favicon-16x16.png', sizes='16x16') + link(rel='icon', type='image/png', href='icons/favicon-32x32.png', sizes='32x32') + link(rel='shortcut icon', href='icons/favicon.ico') + link(rel='icon', href='icons/favicon.ico') + meta(name='msapplication-TileColor', content='#10558c') + meta(name='msapplication-TileImage', content='icons/mstile-144x144.png') + meta(property='og:image', content='images/logo.png') + script(src='https://js.stripe.com/v3/') + script(src='//use.typekit.net/scb5xny.js') + script. + try{Typekit.load();}catch(e){}; + + body + .site + .container + .header + .header__container + .header__nav + a(href='/') etusivu + a(href='/yhdistys.html') yhdistys + .header__headline + .header__logo + h1.header__title Koodiklinikka ry + .header__members + #members + + .content + section + .row + .bread + .column.column1-2 + h3 Rekisteröity yhdistys + p. + Koodiklinikka on nyt rekisteröity yhdistys! + p. + Lähes kolmivuotisen taipaleensa aikana Koodiklinikka on kasvanut lähes tuhannen rekisteröityneen jäsenen yhteisöiksi ja näin saavuttanut aseman Suomen suurimpana ohjelmointiaiheisena yhteisönä! Liittymällä Koodiklinikka ry:n jäseneksi tuet toimintaamme, ja tulevaisuudessa yhdityksen jäsenyys oikeuttaa etuihin Koodiklinikan tapahtumissa. + p. + Yhdistyksen jäsenyys ei ole, eikä tule olemaan, pakollinen Koodiklinikan käyttäjille tai meetuppeihin osallistujille. + .column.column1-2 + .bread-img + .row + .bread + .column.column1-2 + h3 Liity jäseneksi + #membership-form.form + + .column.column1-2 + p. + Kuka tahansa Slack:iin rekisteröitynyt käyttäjä voi liittyä yhdistyksen jäseneksi maksamalla kertaluontoisen 10€ jäsenmaksun. + p. + Nimellisellä jäsenmaksulla tulemme tekemään tapahtumistamme vieläkin mielenkiintoisempia ja kattamaan kustannuksia jäsenmäärämme kasvaessa. + p. + Jos tarvitset Slack-kutsun, siirry etusivulle. + p. + Jäsenrekisterin rekisteriseloste luettavissa Google Drivessä. + + + footer + .sponsors + .sponsors__label Yhteistyössä + a(href='http://futurice.com/', target='_blank') + img.sponsor.sponsor__futurice(src='images/futurice.svg') + a(href='http://www.metosin.fi/', target='_blank') + img.sponsor.sponsor__metosin(src='images/metosin.svg') + a(href='https://www.solita.fi/', target='_blank') + img.sponsor(src='images/solita.svg') + a(href='http://leonidasoy.fi/', target='_blank') + img.sponsor.sponsor__leonidas(src='images/leonidas.png') + a(href='https://www.nordea.com/', target='_blank') + img.sponsor.sponsor__nordea(src='images/nordea.png') + + .contacts + div + a(href='https://koodiklinikka.slack.com') + i.fa.fa-slack + + a(href='https://github.com/koodiklinikka/koodiklinikka.fi') + i.fa.fa-github + + a(href='https://twitter.com/koodiklinikka') + i.fa.fa-twitter + + a(href='https://www.linkedin.com/groups/12025476') + i.fa.fa-linkedin + + a(href='https://www.facebook.com/koodiklinikka') + i.fa.fa-facebook + div#email + + #fader + // inject:js + // endinject