diff --git a/components/EmailComponent.jsx b/components/EmailComponent.tsx similarity index 100% rename from components/EmailComponent.jsx rename to components/EmailComponent.tsx diff --git a/components/Fader.jsx b/components/Fader.tsx similarity index 89% rename from components/Fader.jsx rename to components/Fader.tsx index 60fd483..b0cb508 100644 --- a/components/Fader.jsx +++ b/components/Fader.tsx @@ -1,10 +1,14 @@ import React from "react"; +type Props = { + threshold: number; +}; + function clamp(min, max, value) { return Math.min(Math.max(value, min), max); } -export default class Fader extends React.Component { +export default class Fader extends React.Component { static defaultProps = { threshold: 100, }; diff --git a/components/Feed.jsx b/components/Feed.tsx similarity index 94% rename from components/Feed.jsx rename to components/Feed.tsx index 761be62..55eb438 100644 --- a/components/Feed.jsx +++ b/components/Feed.tsx @@ -8,7 +8,8 @@ import ReactTimeAgo from "react-time-ago"; import JavascriptTimeAgo from "javascript-time-ago"; import timeagoFi from "javascript-time-ago/locale/fi"; -JavascriptTimeAgo.locale(timeagoFi); +// TODO: Add type definitions for javascript-time-ago +(JavascriptTimeAgo as any).locale(timeagoFi); export default class Feed extends React.Component { state = { @@ -41,7 +42,7 @@ export default class Feed extends React.Component { target="_blank" href={message.imageLink} rel="noopener noreferrer" - tabIndex="-1" + tabIndex={-1} > {image} diff --git a/components/Footer.jsx b/components/Footer.tsx similarity index 94% rename from components/Footer.jsx rename to components/Footer.tsx index 456d83f..452d30b 100644 --- a/components/Footer.jsx +++ b/components/Footer.tsx @@ -1,7 +1,14 @@ import React from "react"; import EmailComponent from "./EmailComponent"; import sponsors from "../data/sponsors"; -const SponsorLink = ({ href, name }) => ( + +type Props = { + href: string; + name: string; + key: string; +}; + +const SponsorLink = ({ href, name }: Props) => ( {members} diff --git a/components/api.js b/components/api.ts similarity index 100% rename from components/api.js rename to components/api.ts diff --git a/components/feed-transformers.js b/components/feed-transformers.ts similarity index 97% rename from components/feed-transformers.js rename to components/feed-transformers.ts index 8fe34e2..2d17f2e 100644 --- a/components/feed-transformers.js +++ b/components/feed-transformers.ts @@ -16,7 +16,8 @@ export default { return items.filter(isVisibleGithubEvent).map(item => { const template = lodashTemplate( githubEvent.parse(item).text, - templateSettings + templateSettings, + false ); const repository = `https://github.com/${item.repo.name}`; diff --git a/components/membership/MembershipForm.jsx b/components/membership/MembershipForm.tsx similarity index 100% rename from components/membership/MembershipForm.jsx rename to components/membership/MembershipForm.tsx diff --git a/components/membership/MembershipInfoForm.jsx b/components/membership/MembershipInfoForm.tsx similarity index 81% rename from components/membership/MembershipInfoForm.jsx rename to components/membership/MembershipInfoForm.tsx index b66185a..0da36f8 100644 --- a/components/membership/MembershipInfoForm.jsx +++ b/components/membership/MembershipInfoForm.tsx @@ -1,10 +1,28 @@ -import pick from "lodash/pick"; import request from "axios"; import React from "react"; import classSet from "classnames"; import api from "../api"; import Loader from "../Loader"; +type Props = { + onSignupSuccess: () => void; +}; + +type State = { + error: boolean; + errors: string[]; + fields: { + name: string; + email: string; + handle: string; + address: string; + postcode: string; + city: string; + }; + sending: boolean; + pristineFields: string[]; +}; + const fieldNameTranslations = { address: { fi: "Osoite" }, city: { fi: "Paikkakunta" }, @@ -20,23 +38,26 @@ function validateEmail(email) { return mailValidateRe.test(email); } -const fieldNames = ["name", "email", "handle", "address", "postcode", "city"]; - function getUserInfo(state) { - return pick(state, fieldNames); + return state.fields; } -export default class MembershipInfoForm extends React.Component { - state = { - address: "", - city: "", - email: "", - handle: "", - name: "", - postcode: "", - sending: false, - pristineFields: fieldNames, - }; +export default class MembershipInfoForm extends React.Component { + constructor(props: Props) { + super(props); + this.setState({ + fields: { + address: "", + city: "", + email: "", + handle: "", + name: "", + postcode: "", + }, + sending: false, + pristineFields: Object.keys(this.state.fields), + }); + } onSubmit = async () => { this.setState({ @@ -62,7 +83,10 @@ export default class MembershipInfoForm extends React.Component { } this.setState({ - [e.target.name]: e.target.value, + fields: { + ...this.state.fields, + [name]: e.target.value, + }, pristineFields: this.state.pristineFields.filter( fieldName => fieldName !== name ), @@ -73,13 +97,13 @@ export default class MembershipInfoForm extends React.Component { getDataErrors = () => { const foundErrors = []; - fieldNames.forEach(fieldName => { + Object.keys(this.state.fields).forEach(fieldName => { if (!this.state[fieldName]) { foundErrors.push({ field: fieldName, type: "missing" }); } }); - if (this.state.email && !validateEmail(this.state.email)) { + if (this.state.fields.email && !validateEmail(this.state.fields.email)) { foundErrors.push({ field: "email", type: "invalid" }); } @@ -119,7 +143,7 @@ export default class MembershipInfoForm extends React.Component { const fieldsWithErrors = visibleErrors.map(({ field }) => field); - const inputFields = fieldNames.map(fieldName => { + const inputFields = Object.keys(this.state.fields).map(fieldName => { const inputClasses = classSet({ input: true, "has-error": fieldsWithErrors.includes(fieldName), diff --git a/data/projects.js b/data/projects.ts similarity index 100% rename from data/projects.js rename to data/projects.ts diff --git a/data/sponsors.js b/data/sponsors.ts similarity index 100% rename from data/sponsors.js rename to data/sponsors.ts diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..7b7aa2c --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/package-lock.json b/package-lock.json index 061a07a..ff8eaee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1089,6 +1089,28 @@ "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==" }, + "@types/node": { + "version": "12.12.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.5.tgz", + "integrity": "sha512-KEjODidV4XYUlJBF3XdjSH5FWoMCtO0utnhtdLf1AgeuZLOrRbvmU/gaRCVg7ZaQDjVf3l84egiY0mRNe5xE4A==", + "dev": true + }, + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", + "dev": true + }, + "@types/react": { + "version": "16.9.11", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.11.tgz", + "integrity": "sha512-UBT4GZ3PokTXSWmdgC/GeCGEJXE5ofWyibCcecRLUVN2ZBpXQGVgQGtG2foS7CrTKFKlQVVswLvf7Js6XA/CVQ==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, "@webassemblyjs/ast": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", @@ -2942,6 +2964,12 @@ } } }, + "csstype": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.7.tgz", + "integrity": "sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ==", + "dev": true + }, "cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", @@ -10318,6 +10346,12 @@ "is-typedarray": "^1.0.0" } }, + "typescript": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", + "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", + "dev": true + }, "ua-parser-js": { "version": "0.7.20", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.20.tgz", diff --git a/package.json b/package.json index c397171..2612e82 100644 --- a/package.json +++ b/package.json @@ -33,12 +33,15 @@ "twitter-text": "^3.0.0" }, "devDependencies": { + "@types/node": "12.12.5", + "@types/react": "16.9.11", "babel-eslint": "^10.0.3", "eslint": "^6.6.0", "eslint-config-prettier": "^6.5.0", "eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-react": "^7.16.0", - "prettier": "^1.18.2" + "prettier": "^1.18.2", + "typescript": "3.6.4" }, "prettier": { "trailingComma": "es5" diff --git a/pages/_document.jsx b/pages/_document.tsx similarity index 100% rename from pages/_document.jsx rename to pages/_document.tsx diff --git a/pages/index.jsx b/pages/index.tsx similarity index 100% rename from pages/index.jsx rename to pages/index.tsx diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9406f1f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "typeRoots": ["./node_modules/@types", "./typings"], + "exclude": ["node_modules"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "./typings/*"] +} diff --git a/typings/global.d.ts b/typings/global.d.ts new file mode 100644 index 0000000..a839b85 --- /dev/null +++ b/typings/global.d.ts @@ -0,0 +1,3 @@ +interface Window { + GA_INITIALIZED: boolean; +}