mirror of
https://github.com/koodiklinikka/koodiklinikka.fi.git
synced 2026-03-13 10:03:32 +00:00
Get rid of legacy JS
This commit is contained in:
7
components/EmailComponent.jsx
Normal file
7
components/EmailComponent.jsx
Normal file
@@ -0,0 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
import React from "react";
|
||||
|
||||
export default function EmailComponent() {
|
||||
return <a href="mailto:info@koodiklinikka.fi">info@koodiklinikka.fi</a>;
|
||||
}
|
||||
44
components/Fader.jsx
Normal file
44
components/Fader.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
"use strict";
|
||||
|
||||
import React from "react";
|
||||
|
||||
function clamp(min, max, value) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
export default class Fader extends React.Component {
|
||||
static defaultProps = {
|
||||
threshold: 100
|
||||
};
|
||||
|
||||
state = {
|
||||
opacity: 0
|
||||
};
|
||||
|
||||
onScroll = () => {
|
||||
var scrollableDistance = document.body.scrollHeight - window.innerHeight,
|
||||
scrollTop = window.pageYOffset || document.documentElement.scrollTop,
|
||||
distanceToBottom = scrollableDistance - scrollTop;
|
||||
|
||||
this.setState({
|
||||
opacity: clamp(0, 1, distanceToBottom / this.props.threshold)
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener("scroll", this.onScroll);
|
||||
this.onScroll();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("scroll", this.onScroll);
|
||||
}
|
||||
|
||||
render() {
|
||||
var style = {
|
||||
opacity: this.state.opacity
|
||||
};
|
||||
|
||||
return <div className="fader" style={style}></div>;
|
||||
}
|
||||
}
|
||||
81
components/Feed.jsx
Normal file
81
components/Feed.jsx
Normal file
@@ -0,0 +1,81 @@
|
||||
"use strict";
|
||||
|
||||
import React from "react";
|
||||
import request from "axios";
|
||||
import _ from "lodash";
|
||||
import transformers from "./feed-transformers";
|
||||
import api from "./api";
|
||||
|
||||
function throwError(err) {
|
||||
setTimeout(() => {
|
||||
console.log(err.stack);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
export default class Feed extends React.Component {
|
||||
state = {
|
||||
messages: []
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
request
|
||||
.get(api("feeds"))
|
||||
|
||||
.then(res => {
|
||||
const messages = _(res.data)
|
||||
.map((messages, type) => transformers[type](messages))
|
||||
.flatten()
|
||||
.value();
|
||||
|
||||
this.setState({
|
||||
messages: _(messages)
|
||||
.sortBy("timestamp")
|
||||
.reverse()
|
||||
.value()
|
||||
.slice(0, 40)
|
||||
});
|
||||
})
|
||||
.catch(throwError);
|
||||
}
|
||||
|
||||
render() {
|
||||
var messages = this.state.messages.map((message, i) => {
|
||||
var image = <img src={message.image} />;
|
||||
|
||||
if (message.imageLink) {
|
||||
image = (
|
||||
<a target="_blank" href={message.imageLink}>
|
||||
{image}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="message" key={i}>
|
||||
<div className="message__image">{image}</div>
|
||||
<div className="message__content">
|
||||
<div className="message__user">
|
||||
<a href={message.userLink}>{message.user}</a>
|
||||
</div>
|
||||
<div
|
||||
className="message__body"
|
||||
dangerouslySetInnerHTML={{ __html: message.body }}
|
||||
></div>
|
||||
<div className="message__icon">
|
||||
<i className={`fa fa-${message.type}`}></i>
|
||||
</div>
|
||||
<div className="message__details">
|
||||
<span className="message__timestamp">
|
||||
{require("timeago")(message.timestamp)}
|
||||
</span>
|
||||
<span className="message__meta">{message.meta}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return <div className="feed">{messages}</div>;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,36 @@
|
||||
import EmailComponent from "./EmailComponent";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer>
|
||||
<div className="sponsors">
|
||||
<div className="sponsors__label">Yhteistyössä</div>
|
||||
<a href="http://futurice.com/" target="_blank">
|
||||
<img src="/static/images/futurice.svg" className="sponsor sponsor__futurice" />
|
||||
<img
|
||||
src="/static/images/futurice.svg"
|
||||
className="sponsor sponsor__futurice"
|
||||
/>
|
||||
</a>
|
||||
<a href="http://www.metosin.fi/" target="_blank">
|
||||
<img src="/static/images/metosin.svg" className="sponsor sponsor__metosin" />
|
||||
<img
|
||||
src="/static/images/metosin.svg"
|
||||
className="sponsor sponsor__metosin"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://www.solita.fi/" target="_blank">
|
||||
<img src="/static/images/solita.svg" className="sponsor" />
|
||||
</a>
|
||||
<a href="http://wakeone.co/" target="_blank">
|
||||
<img src="/static/images/wakeone.svg" className="sponsor sponsor__wakeone" />
|
||||
<img
|
||||
src="/static/images/wakeone.svg"
|
||||
className="sponsor sponsor__wakeone"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://www.nordea.com/" target="_blank">
|
||||
<img src="/static/images/nordea.png" className="sponsor sponsor__nordea" />
|
||||
<img
|
||||
src="/static/images/nordea.png"
|
||||
className="sponsor sponsor__nordea"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className="contacts">
|
||||
@@ -36,7 +50,9 @@ export function Footer() {
|
||||
<a href="https://www.facebook.com/koodiklinikka">
|
||||
<i className="fa fa-facebook" />
|
||||
</a>
|
||||
<div id="email" className="email" />
|
||||
<div id="email">
|
||||
<EmailComponent />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
117
components/InviteForm.jsx
Normal file
117
components/InviteForm.jsx
Normal file
@@ -0,0 +1,117 @@
|
||||
"use strict";
|
||||
|
||||
import request from "axios";
|
||||
import React from "react";
|
||||
import classSet from "classnames";
|
||||
import api from "./api";
|
||||
import Loader from "./Loader";
|
||||
|
||||
export default class InviteForm extends React.Component {
|
||||
state = {
|
||||
email: "",
|
||||
submitted: false,
|
||||
sending: false,
|
||||
error: null
|
||||
};
|
||||
|
||||
onSubmit = e => {
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({
|
||||
submitted: false,
|
||||
sending: true,
|
||||
error: null
|
||||
});
|
||||
|
||||
request
|
||||
.post(api("invites"), {
|
||||
email: this.state.email.trim()
|
||||
})
|
||||
.then(this.handleSuccess)
|
||||
.catch(this.handleError);
|
||||
};
|
||||
|
||||
handleSuccess = () => {
|
||||
this.setState({ submitted: true, sending: false });
|
||||
};
|
||||
|
||||
handleError = err => {
|
||||
this.setState({ error: err, sending: false });
|
||||
};
|
||||
|
||||
onChange = e => {
|
||||
if (e.target.value === this.state.email) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
email: e.target.value,
|
||||
error: null,
|
||||
submitted: false
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
var formClasses = classSet({
|
||||
form: true,
|
||||
"invite-form": true,
|
||||
"has-success": this.state.submitted,
|
||||
"has-error": this.state.error,
|
||||
sending: this.state.sending
|
||||
});
|
||||
|
||||
var inputClasses = classSet({
|
||||
input: true,
|
||||
"has-success": this.state.submitted,
|
||||
"has-error": this.state.error
|
||||
});
|
||||
|
||||
var feedbackMessage;
|
||||
|
||||
if (this.state.error || this.state.submitted) {
|
||||
let messageText;
|
||||
|
||||
if (this.state.submitted) {
|
||||
messageText = "Kutsu lähetetty antamaasi sähköpostiosoitteeseen.";
|
||||
} else if (
|
||||
this.state.error.status === 400 &&
|
||||
this.state.error.data === "invalid_email"
|
||||
) {
|
||||
messageText = "Tarkasta syöttämäsi sähköpostiosoite";
|
||||
} else if (
|
||||
this.state.error.status === 400 &&
|
||||
this.state.error.data === "already_invited"
|
||||
) {
|
||||
messageText = "Sähköpostiosoitteeseen on jo lähetetty kutsu";
|
||||
} else {
|
||||
messageText = "Jotain meni pieleen. Yritä hetken päästä uudelleen.";
|
||||
}
|
||||
|
||||
feedbackMessage = <div className="form--message">{messageText}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<form className={formClasses} onSubmit={this.onSubmit}>
|
||||
<input
|
||||
className={inputClasses}
|
||||
type="text"
|
||||
name="email"
|
||||
placeholder="Email"
|
||||
value={this.state.email}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<button
|
||||
className="btn btn__submit"
|
||||
type="submit"
|
||||
title="Lähetä"
|
||||
disabled={this.state.error || this.state.submitted}
|
||||
>
|
||||
Lähetä
|
||||
</button>
|
||||
<div className="invite-form__loader">
|
||||
<Loader />
|
||||
</div>
|
||||
{feedbackMessage}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
12
components/Loader.jsx
Normal file
12
components/Loader.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
export default function Loader() {
|
||||
return (
|
||||
<div className="sk-folding-cube">
|
||||
<div className="sk-cube1 sk-cube" />
|
||||
<div className="sk-cube2 sk-cube" />
|
||||
<div className="sk-cube4 sk-cube" />
|
||||
<div className="sk-cube3 sk-cube" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
components/Members.jsx
Normal file
37
components/Members.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use strict";
|
||||
|
||||
import React from "react";
|
||||
import request from "axios";
|
||||
import _ from "lodash";
|
||||
import api from "./api";
|
||||
|
||||
export default class Members extends React.Component {
|
||||
state = {
|
||||
members: []
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
request.get(api("members")).then(
|
||||
function(res) {
|
||||
this.setState({
|
||||
members: _.shuffle(res.data)
|
||||
});
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
var members = this.state.members.map(function(member, i) {
|
||||
var src = `${member.avatar_url}&s=120`;
|
||||
return <img className="member" key={i} src={src} />;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="members">
|
||||
<a href="https://github.com/koodiklinikka" target="_blank">
|
||||
{members}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
5
components/api.js
Normal file
5
components/api.js
Normal file
@@ -0,0 +1,5 @@
|
||||
var host = process.env.SERVER || "https://lit-plateau-4689.herokuapp.com/";
|
||||
|
||||
export default function(path) {
|
||||
return host + path;
|
||||
}
|
||||
64
components/feed-transformers.js
Normal file
64
components/feed-transformers.js
Normal file
@@ -0,0 +1,64 @@
|
||||
"use strict";
|
||||
|
||||
import _ from "lodash";
|
||||
import githubEvent from "parse-github-event";
|
||||
import twitterText from "twitter-text";
|
||||
|
||||
const isVisibleGithubEvent = ({ type }) =>
|
||||
type !== "PushEvent" && type !== "DeleteEvent";
|
||||
|
||||
export default {
|
||||
github(items) {
|
||||
return items.filter(isVisibleGithubEvent).map(item => {
|
||||
_.templateSettings.interpolate = /{{([\s\S]+?)}}/g;
|
||||
|
||||
var template = _.template(githubEvent.parse(item).text);
|
||||
|
||||
var repository = `https://github.com/${item.repo.name}`;
|
||||
var branch;
|
||||
if (item.payload.ref) {
|
||||
branch = item.payload.ref.replace("refs/heads/", "");
|
||||
}
|
||||
|
||||
var message = template({
|
||||
repository: `<a target="_blank" href="${repository}">${item.repo.name}</a>`,
|
||||
branch: branch,
|
||||
number: item.payload.number,
|
||||
ref_type: item.payload.ref_type,
|
||||
ref: item.payload.ref
|
||||
});
|
||||
|
||||
var url = `https://github.com/${item.actor.login}`;
|
||||
|
||||
return {
|
||||
user: item.actor.login,
|
||||
userLink: url,
|
||||
image: item.actor.avatar_url,
|
||||
imageLink: url,
|
||||
body: message,
|
||||
timestamp: new Date(item.created_at),
|
||||
url: message.url,
|
||||
type: "github"
|
||||
};
|
||||
});
|
||||
},
|
||||
twitter(items) {
|
||||
return items.map(item => {
|
||||
if (item.retweeted) {
|
||||
item = item.retweeted_status;
|
||||
}
|
||||
|
||||
var url = `https://twitter.com/${item.user.screen_name}`;
|
||||
|
||||
return {
|
||||
user: `@${item.user.screen_name}`,
|
||||
userLink: url,
|
||||
image: item.user.profile_image_url_https,
|
||||
imageLink: url,
|
||||
body: twitterText.autoLink(item.text),
|
||||
timestamp: new Date(item.created_at),
|
||||
type: "twitter"
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
39
components/membership/MembershipForm.jsx
Normal file
39
components/membership/MembershipForm.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import MembershipInfoForm from "./MembershipInfoForm";
|
||||
|
||||
export default class MembershipForm extends React.Component {
|
||||
state = {
|
||||
signupSuccess: false
|
||||
};
|
||||
|
||||
handleSignupSuccess = () => {
|
||||
this.setState({ signupSuccess: true });
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.state.signupSuccess) {
|
||||
return <MembershipInfoForm onSignupSuccess={this.handleSignupSuccess} />;
|
||||
}
|
||||
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> Rekisteröityminen onnistui. Tervetuloa jäseneksi!</p>
|
||||
<p> Tervetuloa Koodiklinikka ry:n jäseneksi!</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
191
components/membership/MembershipInfoForm.jsx
Normal file
191
components/membership/MembershipInfoForm.jsx
Normal file
@@ -0,0 +1,191 @@
|
||||
"use strict";
|
||||
|
||||
import _ from "lodash";
|
||||
import request from "axios";
|
||||
import React from "react";
|
||||
import classSet from "classnames";
|
||||
import api from "../api";
|
||||
import Loader from "../Loader";
|
||||
//import configFactory from "../../config.js";
|
||||
//const config = configFactory();
|
||||
|
||||
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" }
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const fieldNames = ["name", "email", "handle", "address", "postcode", "city"];
|
||||
|
||||
function getUserInfo(state) {
|
||||
return _.pick(state, fieldNames);
|
||||
}
|
||||
|
||||
export default class MembershipInfoForm extends React.Component {
|
||||
state = {
|
||||
address: "",
|
||||
city: "",
|
||||
email: "",
|
||||
handle: "",
|
||||
name: "",
|
||||
postcode: "",
|
||||
sending: false,
|
||||
pristineFields: fieldNames
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
this.setState({
|
||||
sending: true,
|
||||
error: null
|
||||
});
|
||||
|
||||
request
|
||||
.post(api("membership"), {
|
||||
userInfo: getUserInfo(this.state)
|
||||
})
|
||||
.then(() => {
|
||||
this.setState({ sending: false });
|
||||
this.props.onSignupSuccess();
|
||||
})
|
||||
.catch(err => {
|
||||
this.setState({ error: err, sending: false });
|
||||
});
|
||||
};
|
||||
|
||||
onChange = e => {
|
||||
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: []
|
||||
});
|
||||
};
|
||||
|
||||
getDataErrors = () => {
|
||||
var foundErrors = [];
|
||||
|
||||
fieldNames.forEach(fieldName => {
|
||||
if (!this.state[fieldName]) {
|
||||
foundErrors.push({ field: fieldName, type: "missing" });
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
"membership-form": true,
|
||||
"has-error": inputErrors.length !== 0 || this.state.error,
|
||||
sending: this.state.sending
|
||||
});
|
||||
|
||||
function getErrorMessage(err) {
|
||||
var feedbackText;
|
||||
|
||||
if (err.type === "missing") {
|
||||
feedbackText = `${fieldNameTranslations[err.field].fi} on pakollinen.`;
|
||||
} else if (err.type === "invalid") {
|
||||
feedbackText = `${fieldNameTranslations[err.field].fi} on virheellinen.`;
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
var fieldsWithErrors = visibleErrors.map(({ field }) => field);
|
||||
|
||||
var inputFields = fieldNames.map(fieldName => {
|
||||
var inputClasses = classSet({
|
||||
input: true,
|
||||
"has-error": _.includes(fieldsWithErrors, fieldName),
|
||||
half: fieldName === "city" || fieldName === "postcode",
|
||||
left: fieldName === "postcode"
|
||||
});
|
||||
|
||||
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>
|
||||
<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>
|
||||
)}
|
||||
<br />
|
||||
<button
|
||||
type="button"
|
||||
disabled={inputErrors.length !== 0}
|
||||
className="btn btn__submit"
|
||||
onClick={this.onSubmit}
|
||||
>
|
||||
Liity jäseneksi
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user