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 (
+
+
+
+ )
+ }
+});
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

+ } 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