Switched eslint to use prettier (#2796)
* switched eslint to use prettier
This commit is contained in:
Родитель
642ccb2387
Коммит
b9adc34955
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"extends": [
|
||||
"prettier"
|
||||
],
|
||||
"plugins": [
|
||||
"prettier",
|
||||
"react"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"es6": true
|
||||
},
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"react/jsx-uses-vars": "error"
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
plugins: ["react"]
|
||||
extends: "./node_modules/mofo-style/.eslintrc.yaml"
|
||||
|
||||
env:
|
||||
es6: true
|
||||
node: true
|
||||
browser: true
|
||||
|
||||
parserOptions:
|
||||
ecmaFeatures:
|
||||
jsx: true
|
||||
sourceType: "module"
|
||||
|
||||
rules:
|
||||
react/jsx-uses-vars: [2]
|
||||
react/jsx-uses-react: [2]
|
|
@ -4782,6 +4782,29 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"eslint-config-prettier": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-4.1.0.tgz",
|
||||
"integrity": "sha512-zILwX9/Ocz4SV2vX7ox85AsrAgXV3f2o2gpIicdMIOra48WYqgUnWNH/cR/iHtmD2Vb3dLSC3LiEJnS05Gkw7w==",
|
||||
"requires": {
|
||||
"get-stdin": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"get-stdin": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
|
||||
"integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint-plugin-prettier": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.0.1.tgz",
|
||||
"integrity": "sha512-/PMttrarPAY78PLvV3xfWibMOdMDl57hmlQ2XqFeA37wd+CJ7WSxV7txqjVPHi/AAFKd2lX0ZqfsOc/i5yFCSQ==",
|
||||
"requires": {
|
||||
"prettier-linter-helpers": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"eslint-plugin-react": {
|
||||
"version": "7.12.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.12.4.tgz",
|
||||
|
@ -5245,6 +5268,11 @@
|
|||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
|
||||
},
|
||||
"fast-diff": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
|
||||
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w=="
|
||||
},
|
||||
"fast-glob": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.6.tgz",
|
||||
|
@ -9728,6 +9756,19 @@
|
|||
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
|
||||
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw="
|
||||
},
|
||||
"prettier": {
|
||||
"version": "1.16.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.16.4.tgz",
|
||||
"integrity": "sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g=="
|
||||
},
|
||||
"prettier-linter-helpers": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
|
||||
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
|
||||
"requires": {
|
||||
"fast-diff": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"pretty-hrtime": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "http://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz",
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"start": "run-p build-uncompressed server watch:**",
|
||||
"snyk": "snyk test --file=package.json",
|
||||
"test:procfile": "node test/test-procfile.js",
|
||||
"test:eslint": "eslint --config ./.eslintrc.yaml \"source/js/**/*.js\" \"source/js/**/*.jsx\" webpack.config.js",
|
||||
"test:eslint": "eslint --config ./.eslintrc.json \"source/js/**/*.js\" \"source/js/**/*.jsx\" webpack.config.js",
|
||||
"test:scss": "stylelint \"source/sass/**/*.scss\" \"source/js/**/*.scss\" --syntax scss",
|
||||
"test": "run-s test:** build",
|
||||
"watch:images": "chokidar \"source/images/**/*\" -c \"npm run build:images\"",
|
||||
|
@ -52,6 +52,8 @@
|
|||
"classnames": "2.2.6",
|
||||
"dotenv": "^6.2.0",
|
||||
"eslint": "^5.15.1",
|
||||
"eslint-config-prettier": "^4.1.0",
|
||||
"eslint-plugin-prettier": "^3.0.1",
|
||||
"eslint-plugin-react": "^7.12.4",
|
||||
"event-stream": "3.3.4",
|
||||
"html-entities": "^1.2.1",
|
||||
|
@ -64,6 +66,7 @@
|
|||
"optipng-bin": "^5.1.0",
|
||||
"postcss": "^7.0.14",
|
||||
"postcss-cli": "^6.1.2",
|
||||
"prettier": "^1.16.4",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "16.8.4",
|
||||
"react-dom": "16.8.4",
|
||||
|
|
|
@ -13,13 +13,21 @@ var basketSignup = function(transaction, onSuccessCallback, onFailCallback) {
|
|||
var errorArray = [];
|
||||
var newsletterErrors = [];
|
||||
|
||||
while (newsletterErrors.firstChild) { newsletterErrors.removeChild(newsletterErrors.firstChild); }
|
||||
while (newsletterErrors.firstChild) {
|
||||
newsletterErrors.removeChild(newsletterErrors.firstChild);
|
||||
}
|
||||
|
||||
var params = `email=` + encodeURIComponent(payload.email) +
|
||||
`&newsletters=` + payload.newsletter +
|
||||
`&privacy=`+ payload.privacy +
|
||||
`&fmt=` + payload.format +
|
||||
`&source_url=` + encodeURIComponent(document.location.href);
|
||||
var params =
|
||||
`email=` +
|
||||
encodeURIComponent(payload.email) +
|
||||
`&newsletters=` +
|
||||
payload.newsletter +
|
||||
`&privacy=` +
|
||||
payload.privacy +
|
||||
`&fmt=` +
|
||||
payload.format +
|
||||
`&source_url=` +
|
||||
encodeURIComponent(document.location.href);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
|
@ -27,7 +35,7 @@ var basketSignup = function(transaction, onSuccessCallback, onFailCallback) {
|
|||
if (r.target.status >= 200 && r.target.status < 300) {
|
||||
var response = r.target.response;
|
||||
|
||||
if(response === null ) {
|
||||
if (response === null) {
|
||||
onFailCallback(new Error());
|
||||
return;
|
||||
}
|
||||
|
@ -35,7 +43,7 @@ var basketSignup = function(transaction, onSuccessCallback, onFailCallback) {
|
|||
if (response.success === true) {
|
||||
onSuccessCallback();
|
||||
} else {
|
||||
if(response.errors) {
|
||||
if (response.errors) {
|
||||
for (var i = 0; i < response.errors.length; i++) {
|
||||
errorArray.push(response.errors[i]);
|
||||
}
|
||||
|
@ -53,14 +61,13 @@ var basketSignup = function(transaction, onSuccessCallback, onFailCallback) {
|
|||
|
||||
xhr.open(`POST`, url, true);
|
||||
xhr.setRequestHeader(`Content-type`, `application/x-www-form-urlencoded`);
|
||||
xhr.setRequestHeader(`X-Requested-With`,`XMLHttpRequest`);
|
||||
xhr.setRequestHeader(`X-Requested-With`, `XMLHttpRequest`);
|
||||
xhr.timeout = 5000;
|
||||
xhr.ontimeout = onFailCallback;
|
||||
xhr.responseType = `json`;
|
||||
xhr.send(params);
|
||||
|
||||
return false;
|
||||
|
||||
};
|
||||
|
||||
module.exports = basketSignup;
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import ReactGA from '../react-ga-proxy.js';
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import ReactGA from "../react-ga-proxy.js";
|
||||
|
||||
import primaryNav from './components/primary-nav/primary-nav.js';
|
||||
import CreepVote from './components/creep-vote/creep-vote.jsx';
|
||||
import Creepometer from './components/creepometer/creepometer.jsx';
|
||||
import Filter from './components/filter/filter.jsx';
|
||||
import primaryNav from "./components/primary-nav/primary-nav.js";
|
||||
import CreepVote from "./components/creep-vote/creep-vote.jsx";
|
||||
import Creepometer from "./components/creepometer/creepometer.jsx";
|
||||
import Filter from "./components/filter/filter.jsx";
|
||||
|
||||
import copyToClipboard from './copy-to-clipboard.js';
|
||||
import HomepageSlider from './homepage-c-slider.js';
|
||||
import ProductGA from './product-analytics.js';
|
||||
import copyToClipboard from "./copy-to-clipboard.js";
|
||||
import HomepageSlider from "./homepage-c-slider.js";
|
||||
import ProductGA from "./product-analytics.js";
|
||||
|
||||
// Track all ReactDOM.render calls so we can use a Promise.all()
|
||||
// all the way at the end to make sure we don't report "we are done"
|
||||
|
@ -32,9 +32,11 @@ let main = {
|
|||
let filter = document.querySelector(`#product-filter`);
|
||||
|
||||
if (filter) {
|
||||
apps.push(new Promise(resolve => {
|
||||
ReactDOM.render(<Filter whenLoaded={() => resolve()}/>, filter);
|
||||
}));
|
||||
apps.push(
|
||||
new Promise(resolve => {
|
||||
ReactDOM.render(<Filter whenLoaded={() => resolve()} />, filter);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,7 +46,9 @@ let main = {
|
|||
// Set up help text accordions where necessary:
|
||||
let productBox = document.querySelector(`.product-detail .h1-heading`);
|
||||
let productName = productBox ? productBox.textContent : `unknown product`;
|
||||
let criteriaWithHelp = document.querySelectorAll(`.criterion button.toggle`);
|
||||
let criteriaWithHelp = document.querySelectorAll(
|
||||
`.criterion button.toggle`
|
||||
);
|
||||
|
||||
if (criteriaWithHelp.length > 0) {
|
||||
Array.from(criteriaWithHelp).forEach(button => {
|
||||
|
@ -75,11 +79,15 @@ let main = {
|
|||
enableCopyLinks() {
|
||||
if (document.querySelectorAll(`.copy-link`)) {
|
||||
Array.from(document.querySelectorAll(`.copy-link`)).forEach(element => {
|
||||
element.addEventListener(`click`, (event) => {
|
||||
element.addEventListener(`click`, event => {
|
||||
event.preventDefault();
|
||||
|
||||
let productBox = document.querySelector(`.product-detail .h1-heading`);
|
||||
let productTitle = productBox ? productBox.textContent : `unknown product`;
|
||||
let productBox = document.querySelector(
|
||||
`.product-detail .h1-heading`
|
||||
);
|
||||
let productTitle = productBox
|
||||
? productBox.textContent
|
||||
: `unknown product`;
|
||||
|
||||
ReactGA.event({
|
||||
category: `product`,
|
||||
|
@ -105,20 +113,31 @@ let main = {
|
|||
let votes = element.querySelector(`input[name=votes]`).value;
|
||||
|
||||
try {
|
||||
votes = JSON.parse(votes.replace(/'/g,`"`));
|
||||
votes = JSON.parse(votes.replace(/'/g, `"`));
|
||||
} catch (e) {
|
||||
votes = {
|
||||
creepiness: {
|
||||
average: 50,
|
||||
'vote_breakdown': {'0': 0, '1': 0, '2': 0, '3': 0, '4': 0}
|
||||
vote_breakdown: { "0": 0, "1": 0, "2": 0, "3": 0, "4": 0 }
|
||||
},
|
||||
confidence: {'0': 0, '1': 0}
|
||||
confidence: { "0": 0, "1": 0 }
|
||||
};
|
||||
}
|
||||
|
||||
apps.push(new Promise(resolve => {
|
||||
ReactDOM.render(<CreepVote csrf={csrf.value} productName={productName} productID={parseInt(productID,10)} votes={votes} whenLoaded={() => resolve()}/>, element);
|
||||
}));
|
||||
apps.push(
|
||||
new Promise(resolve => {
|
||||
ReactDOM.render(
|
||||
<CreepVote
|
||||
csrf={csrf.value}
|
||||
productName={productName}
|
||||
productID={parseInt(productID, 10)}
|
||||
votes={votes}
|
||||
whenLoaded={() => resolve()}
|
||||
/>,
|
||||
element
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -128,9 +147,17 @@ let main = {
|
|||
Array.from(creepometerTargets).forEach(element => {
|
||||
let initialValue = element.dataset.initialValue;
|
||||
|
||||
apps.push(new Promise(resolve => {
|
||||
ReactDOM.render(<Creepometer initialValue={initialValue} whenLoaded={() => resolve()}/>, element);
|
||||
}));
|
||||
apps.push(
|
||||
new Promise(resolve => {
|
||||
ReactDOM.render(
|
||||
<Creepometer
|
||||
initialValue={initialValue}
|
||||
whenLoaded={() => resolve()}
|
||||
/>,
|
||||
element
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import Creepometer from '../creepometer/creepometer.jsx';
|
||||
import CreepChart from '../creepiness-chart/creepiness-chart.jsx';
|
||||
import LikelyhoodChart from '../likelyhood-chart/likelyhood-chart.jsx';
|
||||
import SocialShare from '../social-share/social-share.jsx';
|
||||
import React from "react";
|
||||
import Creepometer from "../creepometer/creepometer.jsx";
|
||||
import CreepChart from "../creepiness-chart/creepiness-chart.jsx";
|
||||
import LikelyhoodChart from "../likelyhood-chart/likelyhood-chart.jsx";
|
||||
import SocialShare from "../social-share/social-share.jsx";
|
||||
|
||||
import CREEPINESS_LABELS from "../creepiness-labels.js";
|
||||
|
||||
|
@ -116,7 +116,7 @@ export default class CreepVote extends React.Component {
|
|||
this.sendVoteFor({
|
||||
attribute: `confidence`,
|
||||
productID,
|
||||
value: confidence,
|
||||
value: confidence
|
||||
});
|
||||
|
||||
this.sendVoteFor({
|
||||
|
@ -138,60 +138,119 @@ export default class CreepVote extends React.Component {
|
|||
* @returns {jsx} What users see when they haven't voted on this product yet.
|
||||
*/
|
||||
renderVoteAsk() {
|
||||
|
||||
return (<form method="post" id="creep-vote" className={this.state.submitAttempted ? `submit-attempted` : ``} onSubmit={evt => this.submitVote(evt)}>
|
||||
<div className="row mb-5">
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="mb-4 text-center">
|
||||
<h3 className="h5-heading mb-2">How creepy do you think this is?</h3>
|
||||
return (
|
||||
<form
|
||||
method="post"
|
||||
id="creep-vote"
|
||||
className={this.state.submitAttempted ? `submit-attempted` : ``}
|
||||
onSubmit={evt => this.submitVote(evt)}
|
||||
>
|
||||
<div className="row mb-5">
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="mb-4 text-center">
|
||||
<h3 className="h5-heading mb-2">
|
||||
How creepy do you think this is?
|
||||
</h3>
|
||||
</div>
|
||||
<Creepometer
|
||||
initialValue={this.state.creepiness}
|
||||
onChange={value => this.setCreepiness(value)}
|
||||
/>
|
||||
</div>
|
||||
<Creepometer initialValue={this.state.creepiness} onChange={value => this.setCreepiness(value)}></Creepometer>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 mt-5 mt-md-0">
|
||||
<div className="mb-4 text-center">
|
||||
<h3 className="h5-heading mb-2">How likely are you to buy it?</h3>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div class="btn-group btn-group-toggle mt-3 mt-md-5" data-toggle="buttons" onAnimationEnd={evt => this.handleAnimationEnd(evt)}>
|
||||
<label for="likely">
|
||||
<input type="radio" name="wouldbuy" id="likely" autocomplete="off" required/>
|
||||
<span class="likely btn" onClick={() => this.setConfidence(true)}><img alt="thumb up" src="/_images/buyers-guide/icon-thumb-up-black.svg" /> Likely</span>
|
||||
</label>
|
||||
<label for="unlikely">
|
||||
<input type="radio" name="wouldbuy" id="unlikely" autocomplete="off" required/>
|
||||
<span class="unlikely btn" onClick={() => this.setConfidence(false)}><img alt="thumb down" src="/_images/buyers-guide/icon-thumb-down-black.svg" /> Not likely</span>
|
||||
</label>
|
||||
<div className="col-12 col-md-6 mt-5 mt-md-0">
|
||||
<div className="mb-4 text-center">
|
||||
<h3 className="h5-heading mb-2">How likely are you to buy it?</h3>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div
|
||||
class="btn-group btn-group-toggle mt-3 mt-md-5"
|
||||
data-toggle="buttons"
|
||||
onAnimationEnd={evt => this.handleAnimationEnd(evt)}
|
||||
>
|
||||
<label for="likely">
|
||||
<input
|
||||
type="radio"
|
||||
name="wouldbuy"
|
||||
id="likely"
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
<span
|
||||
class="likely btn"
|
||||
onClick={() => this.setConfidence(true)}
|
||||
>
|
||||
<img
|
||||
alt="thumb up"
|
||||
src="/_images/buyers-guide/icon-thumb-up-black.svg"
|
||||
/>{" "}
|
||||
Likely
|
||||
</span>
|
||||
</label>
|
||||
<label for="unlikely">
|
||||
<input
|
||||
type="radio"
|
||||
name="wouldbuy"
|
||||
id="unlikely"
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
<span
|
||||
class="unlikely btn"
|
||||
onClick={() => this.setConfidence(false)}
|
||||
>
|
||||
<img
|
||||
alt="thumb down"
|
||||
src="/_images/buyers-guide/icon-thumb-down-black.svg"
|
||||
/>{" "}
|
||||
Not likely
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12 text-center">
|
||||
<button id="creep-vote-btn" type="submit" className="btn btn-ghost mb-2" onClick={() => this.handleSubmitBtnClick()}>Vote & See Results</button>
|
||||
<p class="h6-heading-uppercase mb-0">{this.state.totalVotes} votes</p>
|
||||
<div className="row">
|
||||
<div className="col-12 text-center">
|
||||
<button
|
||||
id="creep-vote-btn"
|
||||
type="submit"
|
||||
className="btn btn-ghost mb-2"
|
||||
onClick={() => this.handleSubmitBtnClick()}
|
||||
>
|
||||
Vote & See Results
|
||||
</button>
|
||||
<p class="h6-heading-uppercase mb-0">
|
||||
{this.state.totalVotes} votes
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>);
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {jsx} What users see when they have voted on this product.
|
||||
*/
|
||||
renderDidVote(){
|
||||
renderDidVote() {
|
||||
let bins = CREEPINESS_LABELS.length;
|
||||
let userVoteGroup = Math.floor(bins * (this.state.creepiness-1)/100);
|
||||
let userVoteGroup = Math.floor((bins * (this.state.creepiness - 1)) / 100);
|
||||
let creepType = CREEPINESS_LABELS[userVoteGroup];
|
||||
|
||||
return(
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<div className="col-12 text-center">
|
||||
<h3 className="h3-heading mb-1">{this.state.totalVotes + 1} Votes — invite your friends!</h3>
|
||||
<div className="h6-heading-uppercase text-muted"></div>
|
||||
<h3 className="h3-heading mb-1">
|
||||
{this.state.totalVotes + 1} Votes — invite your friends!
|
||||
</h3>
|
||||
<div className="h6-heading-uppercase text-muted" />
|
||||
</div>
|
||||
<div className="row mt-3">
|
||||
<div className="col">
|
||||
<CreepChart userVoteGroup={userVoteGroup} values={this.props.votes.creepiness.vote_breakdown} />
|
||||
<CreepChart
|
||||
userVoteGroup={userVoteGroup}
|
||||
values={this.props.votes.creepiness.vote_breakdown}
|
||||
/>
|
||||
</div>
|
||||
<div className="col likelyhood-chart p-5">
|
||||
<LikelyhoodChart values={this.props.votes.confidence} />
|
||||
|
@ -199,7 +258,10 @@ export default class CreepVote extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<SocialShare productName={this.props.productName} creepType={creepType} />
|
||||
<SocialShare
|
||||
productName={this.props.productName}
|
||||
creepType={creepType}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -208,10 +270,14 @@ export default class CreepVote extends React.Component {
|
|||
handleReadResearchClick() {
|
||||
let research = document.getElementById(`product-research`);
|
||||
|
||||
if (!research) { return; }
|
||||
if (!research) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.scrollBy({
|
||||
top: research.getBoundingClientRect().top - parseInt(window.getComputedStyle(research).marginTop, 10),
|
||||
top:
|
||||
research.getBoundingClientRect().top -
|
||||
parseInt(window.getComputedStyle(research).marginTop, 10),
|
||||
left: 0,
|
||||
behavior: `smooth`
|
||||
});
|
||||
|
@ -220,7 +286,7 @@ export default class CreepVote extends React.Component {
|
|||
render() {
|
||||
let voteContent;
|
||||
|
||||
if(this.state.didVote) {
|
||||
if (this.state.didVote) {
|
||||
voteContent = this.renderDidVote();
|
||||
} else {
|
||||
voteContent = this.renderVoteAsk();
|
||||
|
@ -228,9 +294,17 @@ export default class CreepVote extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="creep-vote mt-4 mb-5">
|
||||
<div class="what-you-think-label h5-heading d-inline-block">Tell us what you think</div>
|
||||
<button id="btn-read-search" className="btn btn-link info-help mb-4 mt-2" onClick={() => this.handleReadResearchClick()}>Read our research first</button>
|
||||
{ voteContent }
|
||||
<div class="what-you-think-label h5-heading d-inline-block">
|
||||
Tell us what you think
|
||||
</div>
|
||||
<button
|
||||
id="btn-read-search"
|
||||
className="btn btn-link info-help mb-4 mt-2"
|
||||
onClick={() => this.handleReadResearchClick()}
|
||||
>
|
||||
Read our research first
|
||||
</button>
|
||||
{voteContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
import CREEPINESS_LABELS from "../creepiness-labels.js";
|
||||
|
||||
export default class CreepChart extends React.Component {
|
||||
|
@ -10,11 +10,36 @@ export default class CreepChart extends React.Component {
|
|||
getInitialState() {
|
||||
let values = this.props.values;
|
||||
let data = [
|
||||
{c: `no-creep`, label: CREEPINESS_LABELS[0], value: values[0], offset: 0},
|
||||
{c: `little-creep`, label: CREEPINESS_LABELS[1], value: values[1], offset: 225},
|
||||
{c: `somewhat-creep`, label: CREEPINESS_LABELS[2], value: values[2], offset: 475},
|
||||
{c: `very-creep`, label: CREEPINESS_LABELS[3], value: values[3], offset: 725},
|
||||
{c: `super-creep`, label: CREEPINESS_LABELS[4], value: values[4], offset: 975}
|
||||
{
|
||||
c: `no-creep`,
|
||||
label: CREEPINESS_LABELS[0],
|
||||
value: values[0],
|
||||
offset: 0
|
||||
},
|
||||
{
|
||||
c: `little-creep`,
|
||||
label: CREEPINESS_LABELS[1],
|
||||
value: values[1],
|
||||
offset: 225
|
||||
},
|
||||
{
|
||||
c: `somewhat-creep`,
|
||||
label: CREEPINESS_LABELS[2],
|
||||
value: values[2],
|
||||
offset: 475
|
||||
},
|
||||
{
|
||||
c: `very-creep`,
|
||||
label: CREEPINESS_LABELS[3],
|
||||
value: values[3],
|
||||
offset: 725
|
||||
},
|
||||
{
|
||||
c: `super-creep`,
|
||||
label: CREEPINESS_LABELS[4],
|
||||
value: values[4],
|
||||
offset: 975
|
||||
}
|
||||
];
|
||||
let sum = data.reduce((tally, v) => tally + v.value, 0);
|
||||
|
||||
|
@ -29,23 +54,24 @@ export default class CreepChart extends React.Component {
|
|||
<div>
|
||||
<table id="creepiness-score">
|
||||
<tbody>
|
||||
{
|
||||
this.state.creepinessData.map((data,index) => {
|
||||
let percent = Math.round(100 * data.value / this.state.totalCreepiness);
|
||||
let voteColumn = this.props.userVoteGroup === index ? `your-vote` : ``;
|
||||
{this.state.creepinessData.map((data, index) => {
|
||||
let percent = Math.round(
|
||||
(100 * data.value) / this.state.totalCreepiness
|
||||
);
|
||||
let voteColumn =
|
||||
this.props.userVoteGroup === index ? `your-vote` : ``;
|
||||
|
||||
return (
|
||||
<tr key={data.c} className={`${voteColumn} ${data.c}`}>
|
||||
<th>
|
||||
<div className="bar" style={{height: `${percent}px`,}}></div>
|
||||
<span className="creep-label">{data.label}</span>
|
||||
<span className="creep-face"></span>
|
||||
</th>
|
||||
<td className="creepiness">{percent}%</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
}
|
||||
return (
|
||||
<tr key={data.c} className={`${voteColumn} ${data.c}`}>
|
||||
<th>
|
||||
<div className="bar" style={{ height: `${percent}px` }} />
|
||||
<span className="creep-label">{data.label}</span>
|
||||
<span className="creep-face" />
|
||||
</th>
|
||||
<td className="creepiness">{percent}%</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="row">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
export default class Creepometer extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -85,9 +85,10 @@ export default class Creepometer extends React.Component {
|
|||
|
||||
slideMove(e) {
|
||||
if (this.state.dragging) {
|
||||
let x = e.clientX, bbox = this.state.parentBBox;
|
||||
let x = e.clientX,
|
||||
bbox = this.state.parentBBox;
|
||||
|
||||
if (e.touches){
|
||||
if (e.touches) {
|
||||
x = e.touches[0].clientX;
|
||||
}
|
||||
|
||||
|
@ -104,51 +105,62 @@ export default class Creepometer extends React.Component {
|
|||
|
||||
repositionTrackHead(x, bbox) {
|
||||
// compute the handle offset
|
||||
let percentage = Math.round(100 * (x - bbox.left) / bbox.width);
|
||||
let percentage = Math.round((100 * (x - bbox.left)) / bbox.width);
|
||||
let value = percentage ? percentage : 1;
|
||||
|
||||
this.setState({
|
||||
percentage,
|
||||
value
|
||||
}, () => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(value);
|
||||
this.setState(
|
||||
{
|
||||
percentage,
|
||||
value
|
||||
},
|
||||
() => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let frameOffset = Math.round(this.state.percentage * (this.faceCount-1)/100);
|
||||
let frameOffset = Math.round(
|
||||
(this.state.percentage * (this.faceCount - 1)) / 100
|
||||
);
|
||||
|
||||
let trackheadOpts = {
|
||||
style: {
|
||||
left: `${this.state.value}%`
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
let faceOpts = {
|
||||
style: {
|
||||
background: `url("${this.framePath}sprite-resized-64-colors.png"), #f2b946`,
|
||||
background: `url("${
|
||||
this.framePath
|
||||
}sprite-resized-64-colors.png"), #f2b946`,
|
||||
backgroundSize: `70px`,
|
||||
backgroundPositionX: 0,
|
||||
backgroundPositionY: `-${frameOffset * this.faceHeight}px`,
|
||||
backgroundRepeat: `no-repeat`,
|
||||
},
|
||||
backgroundRepeat: `no-repeat`
|
||||
}
|
||||
};
|
||||
|
||||
let mouseOpts = {
|
||||
onMouseDown: evt => this.slideStart(evt),
|
||||
onTouchStart: evt => this.slideStart(evt),
|
||||
onTouchStart: evt => this.slideStart(evt)
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="creepometer">
|
||||
<div className="slider-container p-2">
|
||||
<div className="slider" ref={e => (this.sliderElement=e)} onClick={evt => this.slideClick(evt)}>
|
||||
<div
|
||||
className="slider"
|
||||
ref={e => (this.sliderElement = e)}
|
||||
onClick={evt => this.slideClick(evt)}
|
||||
>
|
||||
<div className="h6-heading copy copy-left">Not creepy</div>
|
||||
<div className="trackhead" {...trackheadOpts}>
|
||||
<div className="face" {...faceOpts} {...mouseOpts}/>
|
||||
<div className="pip" {...mouseOpts}/>
|
||||
<div className="face" {...faceOpts} {...mouseOpts} />
|
||||
<div className="pip" {...mouseOpts} />
|
||||
</div>
|
||||
<div className="h6-heading copy copy-right">Super creepy</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import ReactGA from '../../../react-ga-proxy.js';
|
||||
import React from "react";
|
||||
import ReactGA from "../../../react-ga-proxy.js";
|
||||
|
||||
const CLASS_HIDDEN = `d-none`;
|
||||
const CLASS_PRODUCT_BOX = `product-box`;
|
||||
|
@ -20,8 +20,20 @@ class SelectableOption extends React.Component {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<div onClick={e => this.forward(e)} className={`radio-group-entry ` + (this.props.type || `radio-button`)}>
|
||||
<span className={(this.props.square ? `square` : ``) + ` dot ` + (this.props.selected? `selected` : ``)}/> <span data-label={this.props.label} className="label">{this.props.label}</span>
|
||||
<div
|
||||
onClick={e => this.forward(e)}
|
||||
className={`radio-group-entry ` + (this.props.type || `radio-button`)}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
(this.props.square ? `square` : ``) +
|
||||
` dot ` +
|
||||
(this.props.selected ? `selected` : ``)
|
||||
}
|
||||
/>{" "}
|
||||
<span data-label={this.props.label} className="label">
|
||||
{this.props.label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -119,28 +131,34 @@ export default class Filter extends React.Component {
|
|||
}
|
||||
|
||||
toggleSealOfApproval() {
|
||||
this.setState({
|
||||
sealOfApproval: !this.state.sealOfApproval
|
||||
}, () => {
|
||||
// Only fire once (per app lifecycle).
|
||||
if (!this.toggledSealOfApproval) {
|
||||
this.toggledSealOfApproval = true;
|
||||
this.setState(
|
||||
{
|
||||
sealOfApproval: !this.state.sealOfApproval
|
||||
},
|
||||
() => {
|
||||
// Only fire once (per app lifecycle).
|
||||
if (!this.toggledSealOfApproval) {
|
||||
this.toggledSealOfApproval = true;
|
||||
|
||||
ReactGA.event({
|
||||
category: `buyersguide`,
|
||||
action: `filter by standards`,
|
||||
label: `filter by standards on buyersguide homepage`
|
||||
});
|
||||
ReactGA.event({
|
||||
category: `buyersguide`,
|
||||
action: `filter by standards`,
|
||||
label: `filter by standards on buyersguide homepage`
|
||||
});
|
||||
}
|
||||
|
||||
this.setVisibilities();
|
||||
}
|
||||
|
||||
this.setVisibilities();
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
handleLikelihood(label) {
|
||||
this.setState({
|
||||
likelihood: label
|
||||
}, () => this.setVisibilities());
|
||||
this.setState(
|
||||
{
|
||||
likelihood: label
|
||||
},
|
||||
() => this.setVisibilities()
|
||||
);
|
||||
}
|
||||
|
||||
slideStart(e) {
|
||||
|
@ -170,9 +188,10 @@ export default class Filter extends React.Component {
|
|||
|
||||
slideMove(e) {
|
||||
if (this.state.dragging) {
|
||||
let x = e.clientX, bbox = this.state.parentBBox;
|
||||
let x = e.clientX,
|
||||
bbox = this.state.parentBBox;
|
||||
|
||||
if (e.touches){
|
||||
if (e.touches) {
|
||||
x = e.touches[0].clientX;
|
||||
}
|
||||
|
||||
|
@ -183,8 +202,8 @@ export default class Filter extends React.Component {
|
|||
x = bbox.left;
|
||||
}
|
||||
|
||||
let fraction = (x-bbox.left)/bbox.width;
|
||||
let percentage = (fraction*100) | 0;
|
||||
let fraction = (x - bbox.left) / bbox.width;
|
||||
let percentage = (fraction * 100) | 0;
|
||||
let value = percentage ? percentage : 1;
|
||||
|
||||
let update = {};
|
||||
|
@ -195,7 +214,7 @@ export default class Filter extends React.Component {
|
|||
if (update.creepinessMin > this.state.creepinessMax - creepInterval) {
|
||||
update.creepinessMax = update.creepinessMin + creepInterval;
|
||||
}
|
||||
} else if(this.activeSlider === this.maxHead) {
|
||||
} else if (this.activeSlider === this.maxHead) {
|
||||
update.creepinessMax = Math.max(value, 1 + creepInterval);
|
||||
if (update.creepinessMax < this.state.creepinessMin + creepInterval) {
|
||||
update.creepinessMin = update.creepinessMax - creepInterval;
|
||||
|
@ -236,12 +255,12 @@ export default class Filter extends React.Component {
|
|||
//
|
||||
// So: we need to non-linear-maths it up, to fix that!
|
||||
|
||||
let interval = (max - min),
|
||||
left = min,
|
||||
pivot = (100 - interval) / 100,
|
||||
naiveScaledLeft = left * pivot,
|
||||
difference = left - naiveScaledLeft,
|
||||
leftMatchingPercentage = left + difference/pivot;
|
||||
let interval = max - min,
|
||||
left = min,
|
||||
pivot = (100 - interval) / 100,
|
||||
naiveScaledLeft = left * pivot,
|
||||
difference = left - naiveScaledLeft,
|
||||
leftMatchingPercentage = left + difference / pivot;
|
||||
|
||||
update.trackStyle = {
|
||||
backgroundSize: `${interval}% 100%`,
|
||||
|
@ -251,9 +270,9 @@ export default class Filter extends React.Component {
|
|||
|
||||
setVisibilities() {
|
||||
let minC = this.state.creepinessMin,
|
||||
maxC = this.state.creepinessMax,
|
||||
like = this.state.likelihood,
|
||||
all = Array.from(document.querySelectorAll(`.${CLASS_PRODUCT_BOX}`));
|
||||
maxC = this.state.creepinessMax,
|
||||
like = this.state.likelihood,
|
||||
all = Array.from(document.querySelectorAll(`.${CLASS_PRODUCT_BOX}`));
|
||||
|
||||
all.forEach(productBox => {
|
||||
let c = parseInt(productBox.dataset.creepiness, 10);
|
||||
|
@ -270,7 +289,9 @@ export default class Filter extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
if (hidden) { return; }
|
||||
if (hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter out for creepiness
|
||||
if (c < minC || c > maxC) {
|
||||
|
@ -280,7 +301,9 @@ export default class Filter extends React.Component {
|
|||
classes.remove(CLASS_HIDDEN);
|
||||
}
|
||||
|
||||
if (hidden) { return; }
|
||||
if (hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
// not hidden by creepiness: do we need to hide it due to buyers likelihood?
|
||||
let recommendation = productBox.querySelector(`.recommendation`);
|
||||
|
@ -293,12 +316,20 @@ export default class Filter extends React.Component {
|
|||
recommendation.classList.remove(CLASS_HIDDEN);
|
||||
}
|
||||
|
||||
if (hidden) { return; }
|
||||
if (hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (like === `Likely` && recommendation.classList.contains(`negative`)) {
|
||||
if (
|
||||
like === `Likely` &&
|
||||
recommendation.classList.contains(`negative`)
|
||||
) {
|
||||
classes.add(CLASS_HIDDEN);
|
||||
hidden = true;
|
||||
} else if (like === `Not likely` && recommendation.classList.contains(`positive`)) {
|
||||
} else if (
|
||||
like === `Not likely` &&
|
||||
recommendation.classList.contains(`positive`)
|
||||
) {
|
||||
classes.add(CLASS_HIDDEN);
|
||||
hidden = true;
|
||||
}
|
||||
|
@ -306,7 +337,9 @@ export default class Filter extends React.Component {
|
|||
});
|
||||
|
||||
let noMatchingNote = document.querySelector(`.no-matching-products-note`);
|
||||
let numHidden = document.querySelectorAll(`.${CLASS_PRODUCT_BOX}.${CLASS_HIDDEN}`).length;
|
||||
let numHidden = document.querySelectorAll(
|
||||
`.${CLASS_PRODUCT_BOX}.${CLASS_HIDDEN}`
|
||||
).length;
|
||||
|
||||
if (numHidden === all.length) {
|
||||
noMatchingNote.classList.remove(CLASS_HIDDEN);
|
||||
|
@ -318,14 +351,14 @@ export default class Filter extends React.Component {
|
|||
getFilterContent() {
|
||||
let mouseOpts = {
|
||||
onMouseDown: evt => this.slideStart(evt),
|
||||
onTouchStart: evt => this.slideStart(evt),
|
||||
onTouchStart: evt => this.slideStart(evt)
|
||||
};
|
||||
|
||||
let likelihoods = [`Likely`, `Not likely`, `Both`].map(label => {
|
||||
return {
|
||||
label,
|
||||
onClick: () => this.handleLikelihood(label),
|
||||
selected: (this.state.likelihood === label)
|
||||
selected: this.state.likelihood === label
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -336,11 +369,14 @@ export default class Filter extends React.Component {
|
|||
<h2 className="filter-caption">Filter by</h2>
|
||||
|
||||
<div className="seal-of-approval">
|
||||
<h3 className="h6-heading-uppercase">minimum security standards <img
|
||||
src="/_images/buyers-guide/mini-badge.svg"
|
||||
width="30px"
|
||||
height="30px"
|
||||
/></h3>
|
||||
<h3 className="h6-heading-uppercase">
|
||||
minimum security standards{" "}
|
||||
<img
|
||||
src="/_images/buyers-guide/mini-badge.svg"
|
||||
width="30px"
|
||||
height="30px"
|
||||
/>
|
||||
</h3>
|
||||
<SelectableOption
|
||||
type="checkbox"
|
||||
label="Meets standards"
|
||||
|
@ -354,9 +390,23 @@ export default class Filter extends React.Component {
|
|||
<h3 className="h6-heading-uppercase">creepiness</h3>
|
||||
<div className="slider">
|
||||
<label>nice</label>
|
||||
<div className="track" ref={e => (this.track=e)} style={this.state.trackStyle}>
|
||||
<span className="min track-head" style={{ left: this.state.offsetMin }} ref={e => (this.minHead=e)} {...mouseOpts} />
|
||||
<span className="max track-head" style={{ left: this.state.offsetMax }} ref={e => (this.maxHead=e)} {...mouseOpts} />
|
||||
<div
|
||||
className="track"
|
||||
ref={e => (this.track = e)}
|
||||
style={this.state.trackStyle}
|
||||
>
|
||||
<span
|
||||
className="min track-head"
|
||||
style={{ left: this.state.offsetMin }}
|
||||
ref={e => (this.minHead = e)}
|
||||
{...mouseOpts}
|
||||
/>
|
||||
<span
|
||||
className="max track-head"
|
||||
style={{ left: this.state.offsetMax }}
|
||||
ref={e => (this.maxHead = e)}
|
||||
{...mouseOpts}
|
||||
/>
|
||||
</div>
|
||||
<label className="creepy">creepy</label>
|
||||
</div>
|
||||
|
@ -364,7 +414,9 @@ export default class Filter extends React.Component {
|
|||
|
||||
<div className="likelihood">
|
||||
<h3 className="h6-heading-uppercase">likelihood to buy</h3>
|
||||
{ likelihoods.map(opts => <SelectableOption {...opts}/>) }
|
||||
{likelihoods.map(opts => (
|
||||
<SelectableOption {...opts} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -375,8 +427,13 @@ export default class Filter extends React.Component {
|
|||
|
||||
return [
|
||||
<div className="filter-label">Filter</div>,
|
||||
<div className={`filter-content` + (this.state.collapsed ? ` collapsed` : ``)} onClick={() => this.open()}>
|
||||
{ content }
|
||||
<div
|
||||
className={
|
||||
`filter-content` + (this.state.collapsed ? ` collapsed` : ``)
|
||||
}
|
||||
onClick={() => this.open()}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
// TODO: Inject likely % in .bar and .likelyhood-words
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
export default class LikelyhoodChart extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render(){
|
||||
render() {
|
||||
let values = this.props.values;
|
||||
let total = values[0] + values[1];
|
||||
let perc = Math.round(100 * values[0]/total, 10);
|
||||
let perc = Math.round((100 * values[0]) / total, 10);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -21,8 +21,10 @@ export default class LikelyhoodChart extends React.Component {
|
|||
<span className="likely-label">likely</span>
|
||||
</th>
|
||||
<td className="likelyhood">
|
||||
<span className="bar" style={{width: `${100 - perc}%`,}}></span>
|
||||
<span className="likelyhood-words">{100 - perc}% likely to buy it</span>
|
||||
<span className="bar" style={{ width: `${100 - perc}%` }} />
|
||||
<span className="likelyhood-words">
|
||||
{100 - perc}% likely to buy it
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="unlikely">
|
||||
|
@ -30,8 +32,10 @@ export default class LikelyhoodChart extends React.Component {
|
|||
<span className="likely-label">not likely</span>
|
||||
</th>
|
||||
<td className="likelyhood">
|
||||
<span className="bar" style={{width: `${perc}%`,}}></span>
|
||||
<span className="likelyhood-words">{perc}% not likely to buy it</span>
|
||||
<span className="bar" style={{ width: `${perc}%` }} />
|
||||
<span className="likelyhood-words">
|
||||
{perc}% not likely to buy it
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* This is a modified version of source/js/primary-nav.js
|
||||
*/
|
||||
* This is a modified version of source/js/primary-nav.js
|
||||
*/
|
||||
|
||||
let primaryNav = {
|
||||
init: function() {
|
||||
|
@ -39,7 +39,7 @@ let primaryNav = {
|
|||
setContentUnderneathState(openMenu);
|
||||
}
|
||||
|
||||
document.addEventListener(`keyup`, (e) => {
|
||||
document.addEventListener(`keyup`, e => {
|
||||
if (e.keyCode === 27) {
|
||||
menuOpen = false;
|
||||
setMenuState(menuOpen);
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import React from 'react';
|
||||
import ReactGA from '../../../react-ga-proxy';
|
||||
import copyToClipboard from '../../copy-to-clipboard.js';
|
||||
import React from "react";
|
||||
import ReactGA from "../../../react-ga-proxy";
|
||||
import copyToClipboard from "../../copy-to-clipboard.js";
|
||||
|
||||
const SocialShareLink = (props) => {
|
||||
const SocialShareLink = props => {
|
||||
let classes = `social-icon`;
|
||||
let srLabel = ``;
|
||||
let link = `PrivacyNotIncluded.org`;
|
||||
let shareText = `I think this ${props.productName} is ${props.creepType.toUpperCase()}. What do you think? Check out the Creep-O-Meter over on @mozilla’s ${link} buyer’s guide.`;
|
||||
let shareText = `I think this ${
|
||||
props.productName
|
||||
} is ${props.creepType.toUpperCase()}. What do you think? Check out the Creep-O-Meter over on @mozilla’s ${link} buyer’s guide.`;
|
||||
let shareEvent = {
|
||||
category: `product`,
|
||||
action: `share tap`,
|
||||
|
@ -25,7 +27,9 @@ const SocialShareLink = (props) => {
|
|||
classes += ` social-button-twitter`;
|
||||
srLabel = `Twitter`;
|
||||
shareEvent.label += `to twitter`;
|
||||
link = `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}`;
|
||||
link = `https://twitter.com/intent/tweet?text=${encodeURIComponent(
|
||||
shareText
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (props.type === `email`) {
|
||||
|
@ -51,7 +55,7 @@ const SocialShareLink = (props) => {
|
|||
if (props.type === `link`) {
|
||||
let _trackShareAction = trackShareAction;
|
||||
|
||||
trackShareAction = (evt) => {
|
||||
trackShareAction = evt => {
|
||||
evt.preventDefault();
|
||||
copyToClipboard(evt.target, window.location.href);
|
||||
evt.target.innerHTML = evt.target.innerHTML.replace(srLabel, `Copied`);
|
||||
|
@ -59,10 +63,19 @@ const SocialShareLink = (props) => {
|
|||
};
|
||||
}
|
||||
|
||||
return <a target="_blank" className="pni-s-link" href={link} onClick={trackShareAction}><span className={classes}/> {srLabel}</a>;
|
||||
return (
|
||||
<a
|
||||
target="_blank"
|
||||
className="pni-s-link"
|
||||
href={link}
|
||||
onClick={trackShareAction}
|
||||
>
|
||||
<span className={classes} /> {srLabel}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
const SocialShare = (props) => {
|
||||
const SocialShare = props => {
|
||||
return (
|
||||
<div class="social pni-share-buttons d-flex justify-content-center flex-wrap flex-md-nowrap mt-3">
|
||||
<SocialShareLink type="facebook" {...props} />
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
const copyToClipboard = (linkElement, textToCopy) => {
|
||||
|
||||
let textArea = document.createElement(`textarea`);
|
||||
|
||||
textArea.setAttribute(`contenteditable`, true);
|
||||
|
|
|
@ -29,14 +29,15 @@ function isElementInViewport(element) {
|
|||
return (
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.bottom <=
|
||||
(window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
}
|
||||
|
||||
// map a value from one range to another
|
||||
function map(v, s1,e1, s2,e2) {
|
||||
return s2 + (v-s1) * (e2-s2) / (e1-s1);
|
||||
function map(v, s1, e1, s2, e2) {
|
||||
return s2 + ((v - s1) * (e2 - s2)) / (e1 - s1);
|
||||
}
|
||||
|
||||
// cap a value to a range
|
||||
|
@ -53,48 +54,65 @@ export default {
|
|||
let bubbleText = bubble.querySelector(`.text`);
|
||||
let products = document.querySelectorAll(`.product-box`);
|
||||
|
||||
window.addEventListener(`scroll`, () => {
|
||||
window.addEventListener(
|
||||
`scroll`,
|
||||
() => {
|
||||
// Figure out which face to show while scrolling:
|
||||
let visible = Array.from(products).filter(v => {
|
||||
return isElementInViewport(v) && !v.classList.contains(`d-none`);
|
||||
});
|
||||
|
||||
// Figure out which face to show while scrolling:
|
||||
let visible = Array.from(products).filter(v => {
|
||||
return isElementInViewport(v) && !v.classList.contains(`d-none`);
|
||||
});
|
||||
let n = visible.length;
|
||||
|
||||
let n = visible.length;
|
||||
// Shortcut this scroll update if there are no products
|
||||
if (n === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Shortcut this scroll update if there are no products
|
||||
if (n===0) { return; }
|
||||
// Reduce the creepiness, treating any product without votes
|
||||
// as a "neutral" product with creepiness = 50
|
||||
let averageCreepiness = visible.reduce((tally, v) => {
|
||||
let value = parseFloat(v.dataset.creepiness) || 50;
|
||||
|
||||
// Reduce the creepiness, treating any product without votes
|
||||
// as a "neutral" product with creepiness = 50
|
||||
let averageCreepiness = visible.reduce( (tally, v) => {
|
||||
let value = parseFloat(v.dataset.creepiness) || 50;
|
||||
return tally + value / n;
|
||||
}, 0);
|
||||
|
||||
return tally + value/n;
|
||||
}, 0);
|
||||
// Compress the value so that we show a smiley face even for products with a lowish creepiness score.
|
||||
let mappedAverageCreepiness = cap(
|
||||
map(
|
||||
averageCreepiness,
|
||||
MINIMUM_HAPPINESS_RATING,
|
||||
MAXIMUM_CREEPINESS_RATING,
|
||||
0,
|
||||
100
|
||||
),
|
||||
1,
|
||||
100
|
||||
);
|
||||
|
||||
// Compress the value so that we show a smiley face even for products with a lowish creepiness score.
|
||||
let mappedAverageCreepiness = cap(map(averageCreepiness, MINIMUM_HAPPINESS_RATING, MAXIMUM_CREEPINESS_RATING, 0, 100), 1, 100);
|
||||
// The averageCreepiness will be in range [1,100] so we can dec1 the
|
||||
// valueto make sure we're in frame range [0,frames.length-1]:
|
||||
let frame = Math.round(
|
||||
((SPRITE_FRAME_COUNT - 1) * (mappedAverageCreepiness - 1)) / 100
|
||||
);
|
||||
|
||||
// The averageCreepiness will be in range [1,100] so we can dec1 the
|
||||
// valueto make sure we're in frame range [0,frames.length-1]:
|
||||
let frame = Math.round((SPRITE_FRAME_COUNT-1) * (mappedAverageCreepiness-1)/100);
|
||||
face.style.backgroundPositionY = `${-frame * EMOJI_FRAME_HEIGHT}px`;
|
||||
|
||||
face.style.backgroundPositionY = `${-frame * EMOJI_FRAME_HEIGHT}px`;
|
||||
// Figure out what the corresponding creepiness label should be:
|
||||
let len = CREEPINESS_LABELS.length;
|
||||
let bin = Math.floor((len * (mappedAverageCreepiness - 1)) / 100);
|
||||
|
||||
// Figure out what the corresponding creepiness label should be:
|
||||
let len = CREEPINESS_LABELS.length;
|
||||
let bin = Math.floor(len * (mappedAverageCreepiness-1)/100);
|
||||
|
||||
if (bin === -1) {
|
||||
bubbleText.textContent = ``;
|
||||
bubble.classList.add(`d-none`);
|
||||
} else {
|
||||
bubbleText.textContent = `${CREEPINESS_LABELS[bin]}!`;
|
||||
bubble.classList.remove(`d-none`);
|
||||
if (bin === -1) {
|
||||
bubbleText.textContent = ``;
|
||||
bubble.classList.add(`d-none`);
|
||||
} else {
|
||||
bubbleText.textContent = `${CREEPINESS_LABELS[bin]}!`;
|
||||
bubble.classList.remove(`d-none`);
|
||||
}
|
||||
},
|
||||
{
|
||||
passive: true // remember not to bog down the UI thread.
|
||||
}
|
||||
}, {
|
||||
passive: true // remember not to bog down the UI thread.
|
||||
});
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,55 +1,55 @@
|
|||
import ReactGA from '../react-ga-proxy';
|
||||
import DNT from '../dnt.js';
|
||||
import ReactGA from "../react-ga-proxy";
|
||||
import DNT from "../dnt.js";
|
||||
|
||||
function getQuerySelectorEvents(pageTitle, productName) {
|
||||
return {
|
||||
// "site-wide" events
|
||||
'#donate-button': {
|
||||
"#donate-button": {
|
||||
category: `buyersguide`,
|
||||
action: `donate tap`,
|
||||
label: `${pageTitle} donate header`
|
||||
},
|
||||
'#donate-button-main': {
|
||||
"#donate-button-main": {
|
||||
category: `buyersguide`,
|
||||
action: `donate tap`,
|
||||
label: `${pageTitle} donate header`
|
||||
},
|
||||
'#donate-button-footer': {
|
||||
"#donate-button-footer": {
|
||||
category: `buyersguide`,
|
||||
action: `donate tap`,
|
||||
label: `${pageTitle} donate footer`
|
||||
},
|
||||
'#nav-social-button-fb': {
|
||||
"#nav-social-button-fb": {
|
||||
category: `buyersguide`,
|
||||
action: `share tap`,
|
||||
label: `share site to Facebook`,
|
||||
transport: `beacon`
|
||||
},
|
||||
'#nav-social-button-twitter': {
|
||||
"#nav-social-button-twitter": {
|
||||
category: `buyersguide`,
|
||||
action: `share tap`,
|
||||
label: `share site to Twitter`,
|
||||
transport: `beacon`
|
||||
},
|
||||
'#nav-social-button-email': {
|
||||
"#nav-social-button-email": {
|
||||
category: `buyersguide`,
|
||||
action: `share tap`,
|
||||
label: `share site to Email`,
|
||||
transport: `beacon`
|
||||
},
|
||||
'#nav-social-button-fb-mb': {
|
||||
"#nav-social-button-fb-mb": {
|
||||
category: `buyersguide`,
|
||||
action: `share tap`,
|
||||
label: `share site to Facebook`,
|
||||
transport: `beacon`
|
||||
},
|
||||
'#nav-social-button-twitter-mb': {
|
||||
"#nav-social-button-twitter-mb": {
|
||||
category: `buyersguide`,
|
||||
action: `share tap`,
|
||||
label: `share site to Twitter`,
|
||||
transport: `beacon`
|
||||
},
|
||||
'#nav-social-button-email-mb': {
|
||||
"#nav-social-button-email-mb": {
|
||||
category: `buyersguide`,
|
||||
action: `share tap`,
|
||||
label: `share site to Email`,
|
||||
|
@ -57,52 +57,52 @@ function getQuerySelectorEvents(pageTitle, productName) {
|
|||
},
|
||||
|
||||
// product events
|
||||
'#product-company-url': {
|
||||
"#product-company-url": {
|
||||
category: `product`,
|
||||
action: `company link tap`,
|
||||
label: `company link for ${productName}`,
|
||||
transport: `beacon`
|
||||
},
|
||||
'#product-copy-link-button': {
|
||||
"#product-copy-link-button": {
|
||||
category: `product`,
|
||||
action: `copy link tap`,
|
||||
label: `copy link ${productName}`
|
||||
},
|
||||
'#product-live-chat': {
|
||||
"#product-live-chat": {
|
||||
category: `product`,
|
||||
action: `customer support link tap`,
|
||||
label: `support link for ${productName}`
|
||||
},
|
||||
'#creep-vote-btn': {
|
||||
"#creep-vote-btn": {
|
||||
category: `product`,
|
||||
action: `opinion submitted`,
|
||||
label: `opinion on ${productName}`
|
||||
},
|
||||
'#product-social-button-fb': {
|
||||
"#product-social-button-fb": {
|
||||
category: `product`,
|
||||
action: `share tap`,
|
||||
label: `share ${productName} to Facebook`,
|
||||
transport: `beacon`
|
||||
},
|
||||
'#product-social-button-twitter': {
|
||||
"#product-social-button-twitter": {
|
||||
category: `product`,
|
||||
action: `share tap`,
|
||||
label: `share ${productName} to Twitter`,
|
||||
transport: `beacon`
|
||||
},
|
||||
'#product-social-button-email': {
|
||||
"#product-social-button-email": {
|
||||
category: `product`,
|
||||
action: `share tap`,
|
||||
label: `share ${productName} to Email`,
|
||||
transport: `beacon`
|
||||
},
|
||||
'#privacy-policy-link': {
|
||||
"#privacy-policy-link": {
|
||||
category: `product`,
|
||||
action: `privacy policy link tap`,
|
||||
label: `policy link for ${productName}`,
|
||||
transport: `beacon`
|
||||
},
|
||||
'#reading-level-link': {
|
||||
"#reading-level-link": {
|
||||
category: `product`,
|
||||
action: `carnegie mellon reading level links`,
|
||||
label: `reading level link for ${productName}`,
|
||||
|
@ -110,7 +110,7 @@ function getQuerySelectorEvents(pageTitle, productName) {
|
|||
},
|
||||
|
||||
// product updates
|
||||
'.product-update-link': {
|
||||
".product-update-link": {
|
||||
category: `product`,
|
||||
action: `update article link tap`,
|
||||
label: `update article link for ${productName}`,
|
||||
|
@ -135,10 +135,12 @@ const ProductGA = {
|
|||
|
||||
let productBox = document.querySelector(`.product-detail .h1-heading`);
|
||||
let productName = productBox ? productBox.textContent : `unknown product`;
|
||||
let pageTitle = document.querySelector(`meta[property='og:title']`).getAttribute(`content`);
|
||||
let pageTitle = document
|
||||
.querySelector(`meta[property='og:title']`)
|
||||
.getAttribute(`content`);
|
||||
let querySelectorEvents = getQuerySelectorEvents(pageTitle, productName);
|
||||
|
||||
Object.keys(querySelectorEvents).forEach( querySelector => {
|
||||
Object.keys(querySelectorEvents).forEach(querySelector => {
|
||||
let elements = document.querySelectorAll(querySelector);
|
||||
|
||||
if (elements.length > 0) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Person from '../../components/people/person.jsx';
|
||||
import LoadingIndicator from '../../components/loading-indicator/loading-indicator.jsx';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Person from "../../components/people/person.jsx";
|
||||
import LoadingIndicator from "../../components/loading-indicator/loading-indicator.jsx";
|
||||
|
||||
export default class FellowList extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -14,11 +14,13 @@ export default class FellowList extends React.Component {
|
|||
|
||||
fetchFellows(query) {
|
||||
Object.assign(query, {
|
||||
'profile_type': `fellow`,
|
||||
'ordering': `custom_name`
|
||||
profile_type: `fellow`,
|
||||
ordering: `custom_name`
|
||||
});
|
||||
|
||||
let queryString = Object.entries(query).map(pair => pair.map(encodeURIComponent).join(`=`)).join(`&`);
|
||||
let queryString = Object.entries(query)
|
||||
.map(pair => pair.map(encodeURIComponent).join(`=`))
|
||||
.join(`&`);
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.addEventListener(`load`, () => {
|
||||
|
@ -27,7 +29,10 @@ export default class FellowList extends React.Component {
|
|||
});
|
||||
});
|
||||
|
||||
xhr.open(`GET`, `${this.props.env.PULSE_API_DOMAIN}/api/pulse/v2/profiles/?${queryString}`);
|
||||
xhr.open(
|
||||
`GET`,
|
||||
`${this.props.env.PULSE_API_DOMAIN}/api/pulse/v2/profiles/?${queryString}`
|
||||
);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
|
@ -57,7 +62,8 @@ export default class FellowList extends React.Component {
|
|||
newQKeys.splice(newQKeys.indexOf(key), 1);
|
||||
});
|
||||
|
||||
if (newQKeys.length) { // newQuery has more key-value pairs than oldQuery
|
||||
if (newQKeys.length) {
|
||||
// newQuery has more key-value pairs than oldQuery
|
||||
hasChanged = true;
|
||||
}
|
||||
|
||||
|
@ -68,28 +74,37 @@ export default class FellowList extends React.Component {
|
|||
// massage fellow's Pulse profile data and pass it to <Person> to render
|
||||
|
||||
let links = {};
|
||||
let programType = fellow.program_type && fellow.program_type.toLowerCase() === `in residence` ? `Fellow in Residence` : fellow.program_type;
|
||||
let programType =
|
||||
fellow.program_type &&
|
||||
fellow.program_type.toLowerCase() === `in residence`
|
||||
? `Fellow in Residence`
|
||||
: fellow.program_type;
|
||||
let issues = fellow.issues;
|
||||
|
||||
issues.unshift(programType);
|
||||
|
||||
if ( fellow.twitter ) {
|
||||
if (fellow.twitter) {
|
||||
links.twitter = fellow.twitter;
|
||||
}
|
||||
|
||||
if ( fellow.linkedin ) {
|
||||
if (fellow.linkedin) {
|
||||
links.linkedIn = fellow.linkedin;
|
||||
}
|
||||
|
||||
let metadata = {
|
||||
'internet_health_issues': issues,
|
||||
internet_health_issues: issues,
|
||||
links: links,
|
||||
name: fellow.name,
|
||||
role: `${fellow.profile_type}, ${fellow.program_year}`,
|
||||
image: fellow.thumbnail,
|
||||
location: fellow.location,
|
||||
affiliations: [], // don't show affiliations meta for now
|
||||
'custom_link': { text: `See work`, link: `https://${this.props.env.PULSE_DOMAIN}/profile/${fellow.profile_id}` }
|
||||
custom_link: {
|
||||
text: `See work`,
|
||||
link: `https://${this.props.env.PULSE_DOMAIN}/profile/${
|
||||
fellow.profile_id
|
||||
}`
|
||||
}
|
||||
};
|
||||
|
||||
return <Person metadata={metadata} key={fellow.name} />;
|
||||
|
@ -97,10 +112,18 @@ export default class FellowList extends React.Component {
|
|||
|
||||
renderFellows() {
|
||||
if (!this.state.fellows) {
|
||||
return <div className="col-12 mx-auto my-5 text-center"><LoadingIndicator /></div>;
|
||||
return (
|
||||
<div className="col-12 mx-auto my-5 text-center">
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.state.fellows.length ? this.state.fellows.map(fellow => this.renderFellowCard(fellow)) : <div className="col-12 mb-5">No fellow profile found.</div>;
|
||||
return this.state.fellows.length ? (
|
||||
this.state.fellows.map(fellow => this.renderFellowCard(fellow))
|
||||
) : (
|
||||
<div className="col-12 mb-5">No fellow profile found.</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import FellowList from './fellow-list.jsx';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import FellowList from "./fellow-list.jsx";
|
||||
|
||||
export default class SingleFilterFellowList extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -28,23 +28,30 @@ export default class SingleFilterFellowList extends React.Component {
|
|||
}
|
||||
|
||||
renderFilters() {
|
||||
return <div className="fellowships-directory-filter px-2">
|
||||
{ this.props.filterOptions.map(option => {
|
||||
let id = `fellow-list-${this.props.filterType}-${option.replace(` `, `-`)}`;
|
||||
return (
|
||||
<div className="fellowships-directory-filter px-2">
|
||||
{this.props.filterOptions.map(option => {
|
||||
let id = `fellow-list-${this.props.filterType}-${option.replace(
|
||||
` `,
|
||||
`-`
|
||||
)}`;
|
||||
|
||||
return <div key={option} className="filter-option">
|
||||
<input type="radio"
|
||||
name={this.props.filterType}
|
||||
value={option}
|
||||
id={id}
|
||||
onClick={() => this.handleFilterClick(option)}
|
||||
defaultChecked={this.state.selectedOption === option}
|
||||
/>
|
||||
<label htmlFor={id}>{option}</label>
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
return (
|
||||
<div key={option} className="filter-option">
|
||||
<input
|
||||
type="radio"
|
||||
name={this.props.filterType}
|
||||
value={option}
|
||||
id={id}
|
||||
onClick={() => this.handleFilterClick(option)}
|
||||
defaultChecked={this.state.selectedOption === option}
|
||||
/>
|
||||
<label htmlFor={id}>{option}</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -57,9 +64,7 @@ export default class SingleFilterFellowList extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col-12 mb-5">
|
||||
{this.renderFilters()}
|
||||
</div>
|
||||
<div className="col-12 mb-5">{this.renderFilters()}</div>
|
||||
</div>
|
||||
<FellowList env={this.props.env} query={this.generateQuery()} />
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
export default class Highlights extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -13,13 +13,13 @@ export default class Highlights extends React.Component {
|
|||
return (
|
||||
<div className="col-md-6" key={`super-highlight-${index}`}>
|
||||
<div className={`${item.image ? `pb-5` : `py-5`}`}>
|
||||
{ item.image &&
|
||||
<img src={item.image} />
|
||||
}
|
||||
{item.image && <img src={item.image} />}
|
||||
<div className="key-item mx-2 mx-md-4 p-3">
|
||||
<h5 className="h4-heading mb-2">{item.title}</h5>
|
||||
<p>{item.description}</p>
|
||||
<a className="cta-link mb-2" href={item.link_url}>{item.link_label}</a>
|
||||
<a className="cta-link mb-2" href={item.link_url}>
|
||||
{item.link_label}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -30,11 +30,17 @@ export default class Highlights extends React.Component {
|
|||
return (
|
||||
<div className="row my-3" key={`highlight-${index}`}>
|
||||
<div className="col-sm-12 col-md-4 pt-3 hidden-sm-down">
|
||||
{ item.image ? <img src={item.image}/> : <div className="placeholder"></div> }
|
||||
{item.image ? (
|
||||
<img src={item.image} />
|
||||
) : (
|
||||
<div className="placeholder" />
|
||||
)}
|
||||
</div>
|
||||
<div className="col-sm-12 col-md-8 pt-3">
|
||||
{ index !== 0 && <hr className="mt-0 mb-4" /> }
|
||||
<h5 className="h5-heading mb-3"><a href={item.link_url}>{item.title}</a></h5>
|
||||
{index !== 0 && <hr className="mt-0 mb-4" />}
|
||||
<h5 className="h5-heading mb-3">
|
||||
<a href={item.link_url}>{item.title}</a>
|
||||
</h5>
|
||||
<p>{item.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -43,9 +49,7 @@ export default class Highlights extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="highlights mb-5">
|
||||
<div className="row">
|
||||
{superHighlights}
|
||||
</div>
|
||||
<div className="row">{superHighlights}</div>
|
||||
{highlights}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import moment from "moment";
|
||||
|
||||
export default class HomeNews extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -10,22 +10,29 @@ export default class HomeNews extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
let newsItem = (item, featured=false, index=null, hr=false) => {
|
||||
let newsItem = (item, featured = false, index = null, hr = false) => {
|
||||
return (
|
||||
<div className="news-item" key={index}>
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
{ item.glyph && <img src={item.glyph} className="mr-2 glyph"/> }
|
||||
{item.glyph && <img src={item.glyph} className="mr-2 glyph" />}
|
||||
<p className="h6-heading-uppercase mb-0">{item.outlet}</p>
|
||||
</div>
|
||||
<h5 className="mb-2">
|
||||
<a href={item.link} className={featured ? `h4-heading` : `h5-heading`}>{item.headline}</a>
|
||||
<a
|
||||
href={item.link}
|
||||
className={featured ? `h4-heading` : `h5-heading`}
|
||||
>
|
||||
{item.headline}
|
||||
</a>
|
||||
</h5>
|
||||
<p className="h6-heading mb-2">{ item.author && `by ${item.author} on ` }{ moment(item.date, `YYYY-MM-DD`).format(`MMMM YYYY`) }</p>
|
||||
{ item.excerpt && <p>{item.excerpt}</p> }
|
||||
{ hr && <hr/> }
|
||||
<p className="h6-heading mb-2">
|
||||
{item.author && `by ${item.author} on `}
|
||||
{moment(item.date, `YYYY-MM-DD`).format(`MMMM YYYY`)}
|
||||
</p>
|
||||
{item.excerpt && <p>{item.excerpt}</p>}
|
||||
{hr && <hr />}
|
||||
</div>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
let unfeaturedNews = this.props.data.slice(1).map((item, index, array) => {
|
||||
|
@ -38,18 +45,20 @@ export default class HomeNews extends React.Component {
|
|||
<div className="row mb-3">
|
||||
<div className="col-md-6 mb-0 pb-4">
|
||||
<div className="play-button-wrapper">
|
||||
<img src={featuredNews.thumbnail}/>
|
||||
{ featuredNews.is_video && <a href={featuredNews.link} className="play-button-overlay"></a> }
|
||||
<img src={featuredNews.thumbnail} />
|
||||
{featuredNews.is_video && (
|
||||
<a href={featuredNews.link} className="play-button-overlay" />
|
||||
)}
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="key-item mx-2 mx-md-4 p-3">
|
||||
{ newsItem(featuredNews, true) }
|
||||
{newsItem(featuredNews, true)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 mb-0 pb-4">{ unfeaturedNews }</div>
|
||||
<div className="col-md-6 mb-0 pb-4">{unfeaturedNews}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import ReactGA from 'react-ga';
|
||||
import classNames from 'classnames';
|
||||
import basketSignup from '../../basket-signup.js';
|
||||
import React from "react";
|
||||
import ReactGA from "react-ga";
|
||||
import classNames from "classnames";
|
||||
import basketSignup from "../../basket-signup.js";
|
||||
|
||||
export default class JoinUs extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -20,16 +20,20 @@ export default class JoinUs extends React.Component {
|
|||
}
|
||||
|
||||
submitForm(event) {
|
||||
this.setState({userSubmitted: true});
|
||||
this.setState({ userSubmitted: true });
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if(this.refs.email.value && this.refs.privacy.checked){
|
||||
basketSignup({
|
||||
email: this.refs.email.value,
|
||||
privacy: this.refs.privacy.checked,
|
||||
newsletter: this.props.newsletter
|
||||
}, this.formSubmissionSuccessful, this.formSubmissionFailure);
|
||||
if (this.refs.email.value && this.refs.privacy.checked) {
|
||||
basketSignup(
|
||||
{
|
||||
email: this.refs.email.value,
|
||||
privacy: this.refs.privacy.checked,
|
||||
newsletter: this.props.newsletter
|
||||
},
|
||||
this.formSubmissionSuccessful,
|
||||
this.formSubmissionFailure
|
||||
);
|
||||
}
|
||||
|
||||
ReactGA.event({
|
||||
|
@ -48,12 +52,12 @@ export default class JoinUs extends React.Component {
|
|||
}
|
||||
|
||||
formSubmissionSuccessful() {
|
||||
this.setState({signupSuccess: true});
|
||||
this.setState({ signupSuccess: true });
|
||||
}
|
||||
|
||||
formSubmissionFailure(e) {
|
||||
console.error(e);
|
||||
this.setState({signupFailed: true});
|
||||
this.setState({ signupFailed: true });
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -64,59 +68,115 @@ export default class JoinUs extends React.Component {
|
|||
|
||||
render() {
|
||||
let inputGroupClass = classNames({
|
||||
'has-danger': !this.state.signupSuccess && this.state.userSubmitted && !this.refs.email.value || this.state.signupFailed
|
||||
"has-danger":
|
||||
(!this.state.signupSuccess &&
|
||||
this.state.userSubmitted &&
|
||||
!this.refs.email.value) ||
|
||||
this.state.signupFailed
|
||||
});
|
||||
|
||||
let privacyClass = classNames({
|
||||
'form-check': true,
|
||||
'has-danger': !this.state.signupSuccess && this.state.userSubmitted && !this.refs.privacy.checked
|
||||
"form-check": true,
|
||||
"has-danger":
|
||||
!this.state.signupSuccess &&
|
||||
this.state.userSubmitted &&
|
||||
!this.refs.privacy.checked
|
||||
});
|
||||
|
||||
let signupState = classNames({
|
||||
'py-5': true,
|
||||
'signup-success': this.state.signupSuccess && this.state.userSubmitted,
|
||||
'signup-fail': !this.state.signupSuccess && this.state.userSubmitted
|
||||
"py-5": true,
|
||||
"signup-success": this.state.signupSuccess && this.state.userSubmitted,
|
||||
"signup-fail": !this.state.signupSuccess && this.state.userSubmitted
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={ `container my-default ${signupState}` }>
|
||||
<div className={`container my-default ${signupState}`}>
|
||||
<div className="col join-main-content">
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6 d-flex justify-content-center flex-column join-content">
|
||||
<div className="mb-5 join-page-title">
|
||||
<h2 className="h1-heading">{!this.state.signupSuccess ? `${this.props.ctaHeader}` : `Thanks!`}</h2>
|
||||
<h2 className="h1-heading">
|
||||
{!this.state.signupSuccess
|
||||
? `${this.props.ctaHeader}`
|
||||
: `Thanks!`}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="join-heading">
|
||||
{ this.state.signupSuccess && <h3 className="h3-black">Thanks!</h3> }
|
||||
{this.state.signupSuccess && (
|
||||
<h3 className="h3-black">Thanks!</h3>
|
||||
)}
|
||||
</div>
|
||||
{!this.state.signupSuccess ?
|
||||
<p className="body-large" dangerouslySetInnerHTML={{__html:this.props.ctaDescription}}></p>
|
||||
: <p dangerouslySetInnerHTML={{__html:this.props.thankYouMessage}}></p>
|
||||
}
|
||||
{!this.state.signupSuccess ? (
|
||||
<p
|
||||
className="body-large"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: this.props.ctaDescription
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<p
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: this.props.thankYouMessage
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{ !this.state.signupSuccess &&
|
||||
<div className="col-12 col-md-6 join-form">
|
||||
<form onSubmit={this.submitForm}>
|
||||
<div className={inputGroupClass}>
|
||||
<div className="mb-2">
|
||||
<input type="email" className="form-control" placeholder="EMAIL ADDRESS" ref="email" onFocus={this.onInputFocus}/>
|
||||
{!this.state.signupSuccess && (
|
||||
<div className="col-12 col-md-6 join-form">
|
||||
<form onSubmit={this.submitForm}>
|
||||
<div className={inputGroupClass}>
|
||||
<div className="mb-2">
|
||||
<input
|
||||
type="email"
|
||||
className="form-control"
|
||||
placeholder="EMAIL ADDRESS"
|
||||
ref="email"
|
||||
onFocus={this.onInputFocus}
|
||||
/>
|
||||
</div>
|
||||
{this.state.userSubmitted && !this.refs.email.value && (
|
||||
<small className="form-check form-control-feedback">
|
||||
Please enter your email
|
||||
</small>
|
||||
)}
|
||||
{this.state.signupFailed && (
|
||||
<small className="form-check form-control-feedback">
|
||||
Something went wrong. Please check your email address
|
||||
and try again
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
{this.state.userSubmitted && !this.refs.email.value && <small className="form-check form-control-feedback">Please enter your email</small>}
|
||||
{this.state.signupFailed && <small className="form-check form-control-feedback">Something went wrong. Please check your email address and try again</small>}
|
||||
</div>
|
||||
<div className={privacyClass}>
|
||||
<label className="form-check-label mb-4">
|
||||
<input type="checkbox" className="form-check-input" id="PrivacyCheckbox" ref="privacy" />
|
||||
<span className="form-text">I'm okay with Mozilla handling my info as explained in this <a href="https://www.mozilla.org/privacy/websites/">Privacy Notice</a></span>
|
||||
{this.state.userSubmitted && !this.refs.privacy.checked && <small className="has-danger">Please check this box if you want to proceed</small>}
|
||||
</label>
|
||||
<div>
|
||||
<button className="btn btn-normal join-btn">Sign Up</button>
|
||||
<div className={privacyClass}>
|
||||
<label className="form-check-label mb-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
id="PrivacyCheckbox"
|
||||
ref="privacy"
|
||||
/>
|
||||
<span className="form-text">
|
||||
I'm okay with Mozilla handling my info as explained in
|
||||
this{" "}
|
||||
<a href="https://www.mozilla.org/privacy/websites/">
|
||||
Privacy Notice
|
||||
</a>
|
||||
</span>
|
||||
{this.state.userSubmitted &&
|
||||
!this.refs.privacy.checked && (
|
||||
<small className="has-danger">
|
||||
Please check this box if you want to proceed
|
||||
</small>
|
||||
)}
|
||||
</label>
|
||||
<div>
|
||||
<button className="btn btn-normal join-btn">
|
||||
Sign Up
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
export default class Leaders extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -12,23 +12,32 @@ export default class Leaders extends React.Component {
|
|||
// Limit to 4 leaders
|
||||
let leaders = this.props.data.slice(0, 4).map((item, index) => {
|
||||
return (
|
||||
<div className="featured-person col-6 col-sm-4 col-md-3 mb-4 mb-sm-0" key={index}>
|
||||
<img className="img-fluid d-block" src={item.image} alt="Headshot"/>
|
||||
<div
|
||||
className="featured-person col-6 col-sm-4 col-md-3 mb-4 mb-sm-0"
|
||||
key={index}
|
||||
>
|
||||
<img className="img-fluid d-block" src={item.image} alt="Headshot" />
|
||||
<h2 className="h5-heading my-2">{item.name}</h2>
|
||||
<p className="h6-heading">{item.affiliations.join(`, `)}</p>
|
||||
<div className="person-social-links mt-3">
|
||||
{ item.links.twitter && <a href={item.links.twitter} className="twitter gray small mr-4"></a> }
|
||||
{ item.links.linkedIn && <a href={item.links.linkedIn} className="linkedIn gray small mr-4"></a> }
|
||||
{item.links.twitter && (
|
||||
<a
|
||||
href={item.links.twitter}
|
||||
className="twitter gray small mr-4"
|
||||
/>
|
||||
)}
|
||||
{item.links.linkedIn && (
|
||||
<a
|
||||
href={item.links.linkedIn}
|
||||
className="linkedIn gray small mr-4"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
{ leaders }
|
||||
</div>
|
||||
);
|
||||
return <div className="row">{leaders}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
export default () => {
|
||||
return <div className="loading-indicator d-inline-block">
|
||||
<div className="dot"></div>
|
||||
<div className="dot"></div>
|
||||
<div className="dot"></div>
|
||||
</div>;
|
||||
return (
|
||||
<div className="loading-indicator d-inline-block">
|
||||
<div className="dot" />
|
||||
<div className="dot" />
|
||||
<div className="dot" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
export default class MemberNotice extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -52,10 +52,14 @@ export default class MemberNotice extends React.Component {
|
|||
this.refs.pane2.style.width = `${wrapperWidth}px`;
|
||||
|
||||
// Set "start" height (which is actually just the current height, but not explicitly set)
|
||||
this.refs.wrapper.style.height = this.state.isExpanded ? `${this.refs.pane2.clientHeight}px` : `${this.refs.pane1.clientHeight}px`;
|
||||
this.refs.wrapper.style.height = this.state.isExpanded
|
||||
? `${this.refs.pane2.clientHeight}px`
|
||||
: `${this.refs.pane1.clientHeight}px`;
|
||||
|
||||
// Set "end" height (this triggers the transition to start)
|
||||
this.refs.wrapper.style.height = this.state.isExpanded ? `${this.refs.pane1.clientHeight}px` : `${this.refs.pane2.clientHeight}px`;
|
||||
this.refs.wrapper.style.height = this.state.isExpanded
|
||||
? `${this.refs.pane1.clientHeight}px`
|
||||
: `${this.refs.pane2.clientHeight}px`;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -69,23 +73,52 @@ export default class MemberNotice extends React.Component {
|
|||
<div className="container d-flex py-3 justify-content-between align-items-top">
|
||||
<div ref="wrapper" className="wrapper align-self-center mr-3">
|
||||
<div ref="pane1" className="pane">
|
||||
<p className="mb-0">Welcome to our member preview. It’s a work in progress and we invite your <a href="https://mzl.la/2ohCL8O">feedback</a>.</p>
|
||||
<p className="mb-0">
|
||||
Welcome to our member preview. It’s a work in progress and we
|
||||
invite your <a href="https://mzl.la/2ohCL8O">feedback</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div ref="pane2" className="pane pane-hidden">
|
||||
<div className="hidden-sm-down">
|
||||
<h3 className="h5-heading">This is a Barn Raising</h3>
|
||||
<p>There is a movement to keep the Internet healthy taking root around the world. Mozilla is a part of this movement and wants to help it grow.</p>
|
||||
<p>We're building a home for people who care about the health of the Internet, hand in hand with the community that emerged from MozFest and events around the world. It’s a space for us to learn, find resources, and connect to new people and ideas.</p>
|
||||
<p className="mb-0">If you are a part of this movement for Internet Health then we want your <a href="https://mzl.la/2ohCL8O">feedback</a> to help it grow. This is a barn raising.</p>
|
||||
<p>
|
||||
There is a movement to keep the Internet healthy taking root
|
||||
around the world. Mozilla is a part of this movement and wants
|
||||
to help it grow.
|
||||
</p>
|
||||
<p>
|
||||
We're building a home for people who care about the health of
|
||||
the Internet, hand in hand with the community that emerged from
|
||||
MozFest and events around the world. It’s a space for us to
|
||||
learn, find resources, and connect to new people and ideas.
|
||||
</p>
|
||||
<p className="mb-0">
|
||||
If you are a part of this movement for Internet Health then we
|
||||
want your <a href="https://mzl.la/2ohCL8O">feedback</a> to help
|
||||
it grow. This is a barn raising.
|
||||
</p>
|
||||
</div>
|
||||
<div className="hidden-md-up">
|
||||
<h3 className="h5-heading">This is a Movement</h3>
|
||||
<p className="mb-0">We're building a home for people who care about the health of the Internet. This is a space for us to learn, find resources, and connect. If you're a part of the movement for Internet Health, we want your <a href="https://mzl.la/2ohCL8O">feedback</a> and help.</p>
|
||||
<p className="mb-0">
|
||||
We're building a home for people who care about the health of
|
||||
the Internet. This is a space for us to learn, find resources,
|
||||
and connect. If you're a part of the movement for Internet
|
||||
Health, we want your{" "}
|
||||
<a href="https://mzl.la/2ohCL8O">feedback</a> and help.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button className={`btn btn-${this.state.isExpanded ? `expanded` : `collapsed`}`} onClick={this.toggle}>+</button>
|
||||
<button
|
||||
className={`btn btn-${
|
||||
this.state.isExpanded ? `expanded` : `collapsed`
|
||||
}`}
|
||||
onClick={this.toggle}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
export default class MultipageNavMobile extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -26,34 +26,48 @@ export default class MultipageNavMobile extends React.Component {
|
|||
render() {
|
||||
let activeLinkLabel;
|
||||
let links = this.props.links.map((link, index) => {
|
||||
let className = `multipage-link${ link.isActive ? ` active` : `` }`;
|
||||
let className = `multipage-link${link.isActive ? ` active` : ``}`;
|
||||
|
||||
if (link.isActive) {
|
||||
activeLinkLabel = <a className={`active-link-label ${className}`}>{link.label}</a>;
|
||||
activeLinkLabel = (
|
||||
<a className={`active-link-label ${className}`}>{link.label}</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (link.isActive) {
|
||||
activeLinkLabel = <a className={`active-link-label d-inline-block ${className}`}>{link.label}</a>;
|
||||
activeLinkLabel = (
|
||||
<a className={`active-link-label d-inline-block ${className}`}>
|
||||
{link.label}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={`link-${index}`}>
|
||||
<a href={link.href} className={className}>{link.label}</a>
|
||||
<a href={link.href} className={className}>
|
||||
{link.label}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
links.unshift(<div key="current-active-link">
|
||||
<button className="expander" onClick={this.toggle}>
|
||||
<div className="d-flex justify-content-between">
|
||||
<div>{activeLinkLabel}</div>
|
||||
<div className="d-inline-block align-self-center control"></div>
|
||||
</div>
|
||||
</button>
|
||||
</div>);
|
||||
links.unshift(
|
||||
<div key="current-active-link">
|
||||
<button className="expander" onClick={this.toggle}>
|
||||
<div className="d-flex justify-content-between">
|
||||
<div>{activeLinkLabel}</div>
|
||||
<div className="d-inline-block align-self-center control" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`dropdown-nav${this.state.isOpen ? ` dropdown-nav-open` : ``}`}>
|
||||
<div
|
||||
className={`dropdown-nav${
|
||||
this.state.isOpen ? ` dropdown-nav-open` : ``
|
||||
}`}
|
||||
>
|
||||
{links}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import moment from "moment";
|
||||
|
||||
export default class News extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -16,47 +16,63 @@ export default class News extends React.Component {
|
|||
|
||||
// HEROKU_APP_DOMAIN is used by review apps
|
||||
if (!networkSiteUrl && this.props.env.HEROKU_APP_NAME) {
|
||||
networkSiteUrl = `https://${this.props.env.HEROKU_APP_NAME}.herokuapp.com`;
|
||||
networkSiteUrl = `https://${
|
||||
this.props.env.HEROKU_APP_NAME
|
||||
}.herokuapp.com`;
|
||||
}
|
||||
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.addEventListener(`load`, () => {
|
||||
this.setState({
|
||||
news: JSON.parse(xhr.response)
|
||||
}, () => {
|
||||
if (this.props.whenLoaded) {
|
||||
this.props.whenLoaded();
|
||||
this.setState(
|
||||
{
|
||||
news: JSON.parse(xhr.response)
|
||||
},
|
||||
() => {
|
||||
if (this.props.whenLoaded) {
|
||||
this.props.whenLoaded();
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
|
||||
xhr.open(`GET`, `${networkSiteUrl}/api/news/?format=json&featured=True&page=1`);
|
||||
xhr.open(
|
||||
`GET`,
|
||||
`${networkSiteUrl}/api/news/?format=json&featured=True&page=1`
|
||||
);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
render() {
|
||||
let blurb = (newsItem, hasHR=true) => {
|
||||
let blurb = (newsItem, hasHR = true) => {
|
||||
return (
|
||||
<div key={newsItem.headline}>
|
||||
<div className="mb-3 news-item">
|
||||
<div className="d-flex align-items-center mb-3">
|
||||
{ newsItem.glyph && <img src={newsItem.glyph} className="mr-2 glyph"/> }
|
||||
{newsItem.glyph && (
|
||||
<img src={newsItem.glyph} className="mr-2 glyph" />
|
||||
)}
|
||||
<p className="h6-heading-uppercase mb-0">{newsItem.outlet}</p>
|
||||
</div>
|
||||
<h3 className="h4-heading mb-2">
|
||||
<a href={newsItem.link} className="newsItem headline">{newsItem.headline}</a>
|
||||
<a href={newsItem.link} className="newsItem headline">
|
||||
{newsItem.headline}
|
||||
</a>
|
||||
</h3>
|
||||
{ newsItem.author && <p className="h6-heading">by {newsItem.author}</p> }
|
||||
<p className="h6-heading">{moment(newsItem.date, `YYYY-MM-DD`).format(`MMMM YYYY`)}</p>
|
||||
{newsItem.author && (
|
||||
<p className="h6-heading">by {newsItem.author}</p>
|
||||
)}
|
||||
<p className="h6-heading">
|
||||
{moment(newsItem.date, `YYYY-MM-DD`).format(`MMMM YYYY`)}
|
||||
</p>
|
||||
</div>
|
||||
{ hasHR && <hr/> }
|
||||
{hasHR && <hr />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
let newsForYear = (year) => {
|
||||
let filteredNews = this.state.news.filter((item) => {
|
||||
let newsForYear = year => {
|
||||
let filteredNews = this.state.news.filter(item => {
|
||||
return item.date.match(year);
|
||||
});
|
||||
|
||||
|
@ -66,7 +82,9 @@ export default class News extends React.Component {
|
|||
<h2 className="h2-typeaccent">{year}</h2>
|
||||
</div>
|
||||
<div className="col-md-8 col-lg-7">
|
||||
{ filteredNews.map((item, index, array) => { return blurb(item, index < array.length - 1); }) }
|
||||
{filteredNews.map((item, index, array) => {
|
||||
return blurb(item, index < array.length - 1);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -83,11 +101,7 @@ export default class News extends React.Component {
|
|||
year++;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container py-5">
|
||||
{ newsByYear }
|
||||
</div>
|
||||
);
|
||||
return <div className="container py-5">{newsByYear}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import Person from './person.jsx';
|
||||
import React from "react";
|
||||
import Person from "./person.jsx";
|
||||
|
||||
export default class People extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -15,36 +15,38 @@ export default class People extends React.Component {
|
|||
|
||||
// HEROKU_APP_DOMAIN is used by review apps
|
||||
if (!networkSiteUrl && this.props.env.HEROKU_APP_NAME) {
|
||||
networkSiteUrl = `https://${this.props.env.HEROKU_APP_NAME}.herokuapp.com`;
|
||||
networkSiteUrl = `https://${
|
||||
this.props.env.HEROKU_APP_NAME
|
||||
}.herokuapp.com`;
|
||||
}
|
||||
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.addEventListener(`load`, () => {
|
||||
this.setState({
|
||||
people: JSON.parse(xhr.response)
|
||||
}, () => {
|
||||
if (this.props.whenLoaded) {
|
||||
this.props.whenLoaded();
|
||||
this.setState(
|
||||
{
|
||||
people: JSON.parse(xhr.response)
|
||||
},
|
||||
() => {
|
||||
if (this.props.whenLoaded) {
|
||||
this.props.whenLoaded();
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
|
||||
xhr.open(`GET`, `${networkSiteUrl}/api/people/?format=json&featured=True&page=1`);
|
||||
xhr.open(
|
||||
`GET`,
|
||||
`${networkSiteUrl}/api/people/?format=json&featured=True&page=1`
|
||||
);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
render() {
|
||||
let people = this.state.people.map((person, index) => {
|
||||
return (
|
||||
<Person key={index} metadata={person}/>
|
||||
);
|
||||
return <Person key={index} metadata={person} />;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
{people}
|
||||
</div>
|
||||
);
|
||||
return <div className="row">{people}</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
class Person extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -14,17 +14,23 @@ class Person extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
let issues = this.props.metadata.internet_health_issues.map((issue, index) => {
|
||||
return (
|
||||
<span key={index} className="issue-link small d-inline-block mr-1">{issue}</span>
|
||||
);
|
||||
});
|
||||
let issues = this.props.metadata.internet_health_issues.map(
|
||||
(issue, index) => {
|
||||
return (
|
||||
<span key={index} className="issue-link small d-inline-block mr-1">
|
||||
{issue}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
let filteredLinks = {};
|
||||
|
||||
// Django returns empty strings instead of null for empty links, so let's filter empty links out:
|
||||
for(let key in this.props.metadata.links) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.props.metadata.links, key)) {
|
||||
for (let key in this.props.metadata.links) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(this.props.metadata.links, key)
|
||||
) {
|
||||
if (this.props.metadata.links[key] !== `` && key !== `interview`) {
|
||||
filteredLinks[key] = this.props.metadata.links[key];
|
||||
}
|
||||
|
@ -35,15 +41,23 @@ class Person extends React.Component {
|
|||
let classes = `${linkKey} gray mr-4`;
|
||||
|
||||
return (
|
||||
<a href={this.props.metadata.links[linkKey]} className={classes} key={index}></a>
|
||||
<a
|
||||
href={this.props.metadata.links[linkKey]}
|
||||
className={classes}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
let metaBlock = (
|
||||
<div className="meta-block mb-2">
|
||||
<div className="h5-heading mb-1">{this.props.metadata.name}</div>
|
||||
<div className="meta-block-item meta-block-item-role">{this.props.metadata.role}</div>
|
||||
<div className="meta-block-item meta-block-item-location">{this.props.metadata.location}</div>
|
||||
<div className="meta-block-item meta-block-item-role">
|
||||
{this.props.metadata.role}
|
||||
</div>
|
||||
<div className="meta-block-item meta-block-item-location">
|
||||
{this.props.metadata.location}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
@ -54,43 +68,66 @@ class Person extends React.Component {
|
|||
<div className="col-md-4 col-12 mr-3">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<img src={this.props.metadata.image} className="headshot" alt="Headshot" />
|
||||
<img
|
||||
src={this.props.metadata.image}
|
||||
className="headshot"
|
||||
alt="Headshot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="row">
|
||||
<div ref="quoteContent" className="col-12 quote-content d-flex pt-3">
|
||||
<div
|
||||
ref="quoteContent"
|
||||
className="col-12 quote-content d-flex pt-3"
|
||||
>
|
||||
<div className="col">
|
||||
<div className="row my-5">
|
||||
<div className="person-quote quote-small">{this.props.metadata.quote}</div>
|
||||
<div className="person-quote quote-small">
|
||||
{this.props.metadata.quote}
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="quote-attribution">
|
||||
{metaBlock}
|
||||
<div className="person-issues mb-2">{issues}</div>
|
||||
<div className="h6-heading">{this.props.metadata.affiliations[0]}</div>
|
||||
{this.props.metadata.partnership_logo &&
|
||||
<div className="h6-heading">
|
||||
{this.props.metadata.affiliations[0]}
|
||||
</div>
|
||||
{this.props.metadata.partnership_logo && (
|
||||
<div className="my-1">
|
||||
<img className="partnership_logo" src={this.props.metadata.partnership_logo} alt="Logo of partnered organization"/>
|
||||
<img
|
||||
className="partnership_logo"
|
||||
src={this.props.metadata.partnership_logo}
|
||||
alt="Logo of partnered organization"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="row person-social-links mt-3 justify-content-between">
|
||||
{socialLinks.length > 0 &&
|
||||
<div>{socialLinks}</div>
|
||||
}
|
||||
{this.props.metadata.links.interview &&
|
||||
<div>
|
||||
<a className="cta-link" href={this.props.metadata.links.interview}>Read Interview</a>
|
||||
</div>
|
||||
}
|
||||
{this.props.metadata.custom_link &&
|
||||
<div>
|
||||
<a href={this.props.metadata.custom_link.link} className="cta-link">{this.props.metadata.custom_link.text}</a>
|
||||
</div>
|
||||
}
|
||||
{socialLinks.length > 0 && <div>{socialLinks}</div>}
|
||||
{this.props.metadata.links.interview && (
|
||||
<div>
|
||||
<a
|
||||
className="cta-link"
|
||||
href={this.props.metadata.links.interview}
|
||||
>
|
||||
Read Interview
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{this.props.metadata.custom_link && (
|
||||
<div>
|
||||
<a
|
||||
href={this.props.metadata.custom_link.link}
|
||||
className="cta-link"
|
||||
>
|
||||
{this.props.metadata.custom_link.text}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -106,36 +143,56 @@ class Person extends React.Component {
|
|||
<div className="col-4 mr-3">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<img src={this.props.metadata.image} className="headshot" alt="Headshot" />
|
||||
<img
|
||||
src={this.props.metadata.image}
|
||||
className="headshot"
|
||||
alt="Headshot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col bio-content pt-3">
|
||||
{metaBlock}
|
||||
<div className="person-issues">{issues}</div>
|
||||
<div className="person-affiliations h6-heading mt-2">{this.props.metadata.affiliations.join(`; `)}</div>
|
||||
{this.props.metadata.partnership_logo &&
|
||||
<div className="person-affiliations h6-heading mt-2">
|
||||
{this.props.metadata.affiliations.join(`; `)}
|
||||
</div>
|
||||
{this.props.metadata.partnership_logo && (
|
||||
<div className="row mt-3">
|
||||
<div className="col">
|
||||
<img className="partnership_logo" src={this.props.metadata.partnership_logo} alt="Logo of partnered organization"/>
|
||||
<img
|
||||
className="partnership_logo"
|
||||
src={this.props.metadata.partnership_logo}
|
||||
alt="Logo of partnered organization"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
<div className="person-social-links mt-3">
|
||||
<div className="row flex-column-reverse flex-md-row justify-content-between">
|
||||
{socialLinks.length > 0 &&
|
||||
<div className="col-12 col-md my-1 my-0">{socialLinks}</div>
|
||||
}
|
||||
{this.props.metadata.links.interview &&
|
||||
<div className="col-12 col-md my-1 my-0">
|
||||
<a className="cta-link" href={this.props.metadata.links.interview}>Read Interview</a>
|
||||
</div>
|
||||
}
|
||||
{this.props.metadata.custom_link &&
|
||||
<div className="col-12 col-md my-1 my-0 text-md-right">
|
||||
<a href={this.props.metadata.custom_link.link} className="cta-link">{this.props.metadata.custom_link.text}</a>
|
||||
</div>
|
||||
}
|
||||
{socialLinks.length > 0 && (
|
||||
<div className="col-12 col-md my-1 my-0">{socialLinks}</div>
|
||||
)}
|
||||
{this.props.metadata.links.interview && (
|
||||
<div className="col-12 col-md my-1 my-0">
|
||||
<a
|
||||
className="cta-link"
|
||||
href={this.props.metadata.links.interview}
|
||||
>
|
||||
Read Interview
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{this.props.metadata.custom_link && (
|
||||
<div className="col-12 col-md my-1 my-0 text-md-right">
|
||||
<a
|
||||
href={this.props.metadata.custom_link.link}
|
||||
className="cta-link"
|
||||
>
|
||||
{this.props.metadata.custom_link.text}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -155,12 +212,12 @@ Person.propTypes = {
|
|||
quote: PropTypes.string,
|
||||
featured: PropTypes.bool,
|
||||
affiliations: PropTypes.array,
|
||||
'internet_health_issues': PropTypes.array,
|
||||
internet_health_issues: PropTypes.array,
|
||||
image: PropTypes.string,
|
||||
'partnership_logo': PropTypes.string,
|
||||
partnership_logo: PropTypes.string,
|
||||
link: PropTypes.object,
|
||||
'custom_link': PropTypes.object,
|
||||
}).isRequired,
|
||||
custom_link: PropTypes.object
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default Person;
|
||||
|
|
|
@ -1,25 +1,36 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import SALESFORCE_COUNTRY_LIST from './salesforce-country-list.js';
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import SALESFORCE_COUNTRY_LIST from "./salesforce-country-list.js";
|
||||
|
||||
export default class CountrySelect extends React.Component {
|
||||
render() {
|
||||
let className = classNames(`form-label-group`, `country-picker`, this.props.className);
|
||||
let className = classNames(
|
||||
`form-label-group`,
|
||||
`country-picker`,
|
||||
this.props.className
|
||||
);
|
||||
let codes = Object.keys(SALESFORCE_COUNTRY_LIST);
|
||||
let options = codes.map( code => {
|
||||
return <option key={code} value={code}>{SALESFORCE_COUNTRY_LIST[code]}</option>;
|
||||
let options = codes.map(code => {
|
||||
return (
|
||||
<option key={code} value={code}>
|
||||
{SALESFORCE_COUNTRY_LIST[code]}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<select className="form-control"
|
||||
<select
|
||||
className="form-control"
|
||||
disabled={this.props.disabled}
|
||||
ref={(element) => { this.element = element; }}
|
||||
ref={element => {
|
||||
this.element = element;
|
||||
}}
|
||||
onFocus={this.props.onFocus}
|
||||
defaultValue={``}
|
||||
>
|
||||
<option value="">{this.props.label}</option>
|
||||
{ options }
|
||||
{options}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -23,8 +23,8 @@ class DonationModal extends React.Component {
|
|||
// CSS position property, we need to relocate the DOM
|
||||
// node so that we get the full-viewport effect.
|
||||
let body = document.body,
|
||||
c1 = body.children[0],
|
||||
n = this.fragment;
|
||||
c1 = body.children[0],
|
||||
n = this.fragment;
|
||||
|
||||
if (!c1) {
|
||||
body.appendChild(n);
|
||||
|
@ -56,12 +56,12 @@ class DonationModal extends React.Component {
|
|||
getModalContent() {
|
||||
if (!this.donateURL) {
|
||||
let base = `https://donate.mozilla.org/?`,
|
||||
query = [
|
||||
`utm_source=foundation.mozilla.org`,
|
||||
`utm_medium=petitionmodal`,
|
||||
`utm_campaign=${this.props.slug}`,
|
||||
`utm_content=${this.props.name}`
|
||||
].join(`&`);
|
||||
query = [
|
||||
`utm_source=foundation.mozilla.org`,
|
||||
`utm_medium=petitionmodal`,
|
||||
`utm_campaign=${this.props.slug}`,
|
||||
`utm_content=${this.props.name}`
|
||||
].join(`&`);
|
||||
|
||||
this.donateURL = `${base}${query}`;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
export default class FloatingLabelInput extends React.Component {
|
||||
render() {
|
||||
|
@ -7,9 +7,12 @@ export default class FloatingLabelInput extends React.Component {
|
|||
|
||||
return (
|
||||
<div className={className}>
|
||||
<input className="form-control"
|
||||
<input
|
||||
className="form-control"
|
||||
disabled={this.props.disabled}
|
||||
ref={(element) => { this.element = element; }}
|
||||
ref={element => {
|
||||
this.element = element;
|
||||
}}
|
||||
id={this.props.id}
|
||||
type={this.props.type}
|
||||
placeholder={this.props.label}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
export default class FloatingLabelInput extends React.Component {
|
||||
render() {
|
||||
|
@ -7,9 +7,12 @@ export default class FloatingLabelInput extends React.Component {
|
|||
|
||||
return (
|
||||
<div className={className}>
|
||||
<textarea className="form-control"
|
||||
<textarea
|
||||
className="form-control"
|
||||
disabled={this.props.disabled}
|
||||
ref={(element) => { this.element = element; }}
|
||||
ref={element => {
|
||||
this.element = element;
|
||||
}}
|
||||
id={this.props.id}
|
||||
type={this.props.type}
|
||||
placeholder={this.props.label}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* In order to move fast and not break things, we're doing petition localisation
|
||||
|
@ -8,9 +8,8 @@ import React from 'react';
|
|||
* a link element.
|
||||
*/
|
||||
export default {
|
||||
|
||||
// English
|
||||
"en": {
|
||||
en: {
|
||||
"First name": `First name`,
|
||||
"Please enter your given name(s)": `Please enter your given name(s)`,
|
||||
"Last name": `Last name`,
|
||||
|
@ -21,17 +20,22 @@ export default {
|
|||
"Please enter your country": `Please enter your country`,
|
||||
"Postal code": `Postal code`,
|
||||
"Please enter your postal code": `Please enter your postal code`,
|
||||
"Comment": `Comment`,
|
||||
Comment: `Comment`,
|
||||
"Please include a comment": `Please include a comment`,
|
||||
"Something went wrong. Please check your email address and try again": `Something went wrong. Please check your email address and try again`,
|
||||
"I'm okay with Mozilla handling my info as explained in this Privacy Notice": <span>I'm okay with Mozilla handling my info as explained in this <a href="https://www.mozilla.org/privacy/websites/">Privacy Notice</a></span>,
|
||||
"I'm okay with Mozilla handling my info as explained in this Privacy Notice": (
|
||||
<span>
|
||||
I'm okay with Mozilla handling my info as explained in this{" "}
|
||||
<a href="https://www.mozilla.org/privacy/websites/">Privacy Notice</a>
|
||||
</span>
|
||||
),
|
||||
"Please check this box if you want to proceed": `Please check this box if you want to proceed`,
|
||||
"Yes, I want to receive email updates about Mozilla's campaigns.": `Yes, I want to receive email updates about Mozilla’s campaigns.`,
|
||||
"Add my name": `Add my name`
|
||||
},
|
||||
|
||||
// German
|
||||
"de": {
|
||||
de: {
|
||||
"First name": `Vorname`,
|
||||
"Please enter your given name(s)": `Bitte geben Sie Ihre(n) Vornamen ein`,
|
||||
"Last name": `Nachname`,
|
||||
|
@ -42,17 +46,26 @@ export default {
|
|||
"Please enter your country": `Bitte geben Sie Ihr Land ein`,
|
||||
"Postal code": `Postleitzahl`,
|
||||
"Please enter your postal code": `Bitte geben Sie Ihre Postleitzahl ein`,
|
||||
"Comment": `Kommentar`,
|
||||
Comment: `Kommentar`,
|
||||
"Please include a comment": `Bitte geben Sie einen Kommentar ein`,
|
||||
"Something went wrong. Please check your email address and try again": `Etwas ist schiefgegangen. Bitte überprüfen Sie Ihre E-Mail-Adresse und versuchen Sie es erneut`,
|
||||
"I'm okay with Mozilla handling my info as explained in this Privacy Notice": <span>Ich bin einverstanden mit dem Umgang Mozillas mit meinen Informationen wie im <a href="https://www.mozilla.org/privacy/websites/">Datenschutzhinweis</a> beschrieben</span>,
|
||||
"I'm okay with Mozilla handling my info as explained in this Privacy Notice": (
|
||||
<span>
|
||||
Ich bin einverstanden mit dem Umgang Mozillas mit meinen Informationen
|
||||
wie im{" "}
|
||||
<a href="https://www.mozilla.org/privacy/websites/">
|
||||
Datenschutzhinweis
|
||||
</a>{" "}
|
||||
beschrieben
|
||||
</span>
|
||||
),
|
||||
"Please check this box if you want to proceed": `Aktivieren Sie bitte dieses Kontrollkästchen, um fortzufahren`,
|
||||
"Yes, I want to receive email updates about Mozilla's campaigns.": `Ja, ich möchte per E-Mail Neuigkeiten über Mozillas Kampagnen erfahren.`,
|
||||
"Add my name": `Meinen Namen hinzufügen`
|
||||
},
|
||||
|
||||
// Spanish
|
||||
"es": {
|
||||
es: {
|
||||
"First name": `Nombre`,
|
||||
"Please enter your given name(s)": `Introduce tu nombre`,
|
||||
"Last name": `Apellido`,
|
||||
|
@ -63,17 +76,25 @@ export default {
|
|||
"Please enter your country": `Introduce tu país`,
|
||||
"Postal code": `Código postal`,
|
||||
"Please enter your postal code": `Introduce tu código postal`,
|
||||
"Comment": `Comentario`,
|
||||
Comment: `Comentario`,
|
||||
"Please include a comment": `Por favor, escribe un comentario`,
|
||||
"Something went wrong. Please check your email address and try again": `Algo fue mal. Comprueba tu dirección de correo y vuelve a intentarlo`,
|
||||
"I'm okay with Mozilla handling my info as explained in this Privacy Notice": <span>Estoy de acuerdo con la gestión de mi información, tal y como se explica en este <a href="https://www.mozilla.org/privacy/websites/">Aviso de privacidad</a></span>,
|
||||
"I'm okay with Mozilla handling my info as explained in this Privacy Notice": (
|
||||
<span>
|
||||
Estoy de acuerdo con la gestión de mi información, tal y como se explica
|
||||
en este{" "}
|
||||
<a href="https://www.mozilla.org/privacy/websites/">
|
||||
Aviso de privacidad
|
||||
</a>
|
||||
</span>
|
||||
),
|
||||
"Please check this box if you want to proceed": `Marca esta casilla si quieres continuar`,
|
||||
"Yes, I want to receive email updates about Mozilla's campaigns.": `Sí, quiero recibir actualizaciones sobre las campañas de Mozilla.`,
|
||||
"Add my name": `Añadir mi nombre`
|
||||
},
|
||||
|
||||
// French
|
||||
"fr": {
|
||||
fr: {
|
||||
"First name": `Prénom`,
|
||||
"Please enter your given name(s)": `Veuillez saisir votre prénom`,
|
||||
"Last name": `Nom`,
|
||||
|
@ -84,17 +105,24 @@ export default {
|
|||
"Please enter your country": `Veuillez sélectionner votre pays`,
|
||||
"Postal code": `Code postal`,
|
||||
"Please enter your postal code": `Veuillez saisir votre code postal`,
|
||||
"Comment": `Commentaire`,
|
||||
Comment: `Commentaire`,
|
||||
"Please include a comment": `Veuillez ajouter un commentaire`,
|
||||
"Something went wrong. Please check your email address and try again": `Une erreur s’est produite. Veuillez vérifier votre adresse électronique et réessayer`,
|
||||
"I'm okay with Mozilla handling my info as explained in this Privacy Notice": <span>J’accepte que Mozilla utilise mes informations conformément à <a href="https://www.mozilla.org/privacy/websites/">cette politique de confidentialité</a></span>,
|
||||
"I'm okay with Mozilla handling my info as explained in this Privacy Notice": (
|
||||
<span>
|
||||
J’accepte que Mozilla utilise mes informations conformément à{" "}
|
||||
<a href="https://www.mozilla.org/privacy/websites/">
|
||||
cette politique de confidentialité
|
||||
</a>
|
||||
</span>
|
||||
),
|
||||
"Please check this box if you want to proceed": `Veuillez cocher cette case si vous désirez poursuivre`,
|
||||
"Yes, I want to receive email updates about Mozilla's campaigns.": `J’accepte de recevoir des informations par courriel au sujet des campagnes de Mozilla.`,
|
||||
"Add my name": `Ajouter mon nom`
|
||||
},
|
||||
|
||||
// Polish
|
||||
"pl": {
|
||||
pl: {
|
||||
"First name": `Imię`,
|
||||
"Please enter your given name(s)": `Proszę podać imię (lub imiona)`,
|
||||
"Last name": `Nazwisko`,
|
||||
|
@ -105,17 +133,24 @@ export default {
|
|||
"Please enter your country": `Proszę podać kraj`,
|
||||
"Postal code": `Kod pocztowy`,
|
||||
"Please enter your postal code": `Proszę podać kod pocztowy`,
|
||||
"Comment": `Komentarz`,
|
||||
Comment: `Komentarz`,
|
||||
"Please include a comment": `Proszę dołączyć komentarz`,
|
||||
"Something went wrong. Please check your email address and try again": `Coś się nie powiodło. Proszę sprawdzić poprawność adresu e-mail i spróbować ponownie`,
|
||||
"I'm okay with Mozilla handling my info as explained in this Privacy Notice": <span>Pozwalam Mozilli wykorzystywać te informacje w sposób opisany w <a href="https://www.mozilla.org/privacy/websites/">zasadach ochrony prywatności</a></span>,
|
||||
"I'm okay with Mozilla handling my info as explained in this Privacy Notice": (
|
||||
<span>
|
||||
Pozwalam Mozilli wykorzystywać te informacje w sposób opisany w{" "}
|
||||
<a href="https://www.mozilla.org/privacy/websites/">
|
||||
zasadach ochrony prywatności
|
||||
</a>
|
||||
</span>
|
||||
),
|
||||
"Please check this box if you want to proceed": `Proszę zaznaczyć to pole, aby kontynuować`,
|
||||
"Yes, I want to receive email updates about Mozilla's campaigns.": `Tak, chcę otrzymywać wiadomości o kampaniach Mozilli.`,
|
||||
"Add my name": `Podpisz`
|
||||
},
|
||||
|
||||
// Portuguese
|
||||
"pt": {
|
||||
pt: {
|
||||
"First name": `Nome`,
|
||||
"Please enter your given name(s)": `Insira seu nome`,
|
||||
"Last name": `Sobrenome`,
|
||||
|
@ -126,13 +161,20 @@ export default {
|
|||
"Please enter your country": `Insira seu país`,
|
||||
"Postal code": `CEP`,
|
||||
"Please enter your postal code": `Insira seu CEP`,
|
||||
"Comment": `Comentário`,
|
||||
Comment: `Comentário`,
|
||||
"Please include a comment": `Inclua um comentário`,
|
||||
"Something went wrong. Please check your email address and try again": `Algo deu errado. Verifique seu endereço de e-mail e tente novamente`,
|
||||
"I'm okay with Mozilla handling my info as explained in this Privacy Notice": <span>Concordo com a Mozilla lidar com minhas informações, como explicado neste <a href="https://www.mozilla.org/privacy/websites/">Aviso de Privacidade</a></span>,
|
||||
"I'm okay with Mozilla handling my info as explained in this Privacy Notice": (
|
||||
<span>
|
||||
Concordo com a Mozilla lidar com minhas informações, como explicado
|
||||
neste{" "}
|
||||
<a href="https://www.mozilla.org/privacy/websites/">
|
||||
Aviso de Privacidade
|
||||
</a>
|
||||
</span>
|
||||
),
|
||||
"Please check this box if you want to proceed": `Marque esta opção se deseja continuar`,
|
||||
"Yes, I want to receive email updates about Mozilla's campaigns.": `Quero receber atualizações por e-mail sobre campanhas da Mozilla.`,
|
||||
"Add my name": `Adicionar meu nome`
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import data from './locale-strings.jsx';
|
||||
import data from "./locale-strings.jsx";
|
||||
|
||||
const DEFAULT_LOCALE = `en`;
|
||||
let currentLocale = false;
|
||||
|
@ -17,7 +17,7 @@ function getCurrentLocale() {
|
|||
if (!currentLocale) {
|
||||
var pathsegments = window.location.pathname.split(`/`).filter(v => v);
|
||||
|
||||
currentLocale = (pathsegments.length > 0) ? pathsegments[0] : DEFAULT_LOCALE;
|
||||
currentLocale = pathsegments.length > 0 ? pathsegments[0] : DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
return currentLocale || DEFAULT_LOCALE;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import React from 'react';
|
||||
import ReactGA from 'react-ga';
|
||||
import classNames from 'classnames';
|
||||
import DonationModal from './donation-modal.jsx';
|
||||
import FloatingLabelInput from './floating-label-input.jsx';
|
||||
import FloatingLabelTextarea from './floating-label-textarea.jsx';
|
||||
import CountrySelect from './country-select.jsx';
|
||||
import basketSignup from '../../basket-signup.js';
|
||||
import get from './locales';
|
||||
import React from "react";
|
||||
import ReactGA from "react-ga";
|
||||
import classNames from "classnames";
|
||||
import DonationModal from "./donation-modal.jsx";
|
||||
import FloatingLabelInput from "./floating-label-input.jsx";
|
||||
import FloatingLabelTextarea from "./floating-label-textarea.jsx";
|
||||
import CountrySelect from "./country-select.jsx";
|
||||
import basketSignup from "../../basket-signup.js";
|
||||
import get from "./locales";
|
||||
|
||||
export default class Petition extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -54,7 +54,7 @@ export default class Petition extends React.Component {
|
|||
basketSuccess: false,
|
||||
basketFailed: false,
|
||||
userTriedSubmitting: false,
|
||||
showDonationModal: false,
|
||||
showDonationModal: false
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -69,19 +69,31 @@ export default class Petition extends React.Component {
|
|||
|
||||
// helper function for auto-generating checkboxes off of the passed props.
|
||||
generateCheckboxes(disabled) {
|
||||
return [`checkbox1`, `checkbox2`].map(name => {
|
||||
let label = this[name];
|
||||
return [`checkbox1`, `checkbox2`]
|
||||
.map(name => {
|
||||
let label = this[name];
|
||||
|
||||
if (!label) { return null; }
|
||||
return (
|
||||
<div key={name}>
|
||||
<label className="form-check-label">
|
||||
<input className="form-check-input" disabled={disabled} type="checkbox" ref={name} />
|
||||
<span className="h6-heading form-text" dangerouslySetInnerHTML={{__html: label}}/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}).filter(v => v);
|
||||
if (!label) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div key={name}>
|
||||
<label className="form-check-label">
|
||||
<input
|
||||
className="form-check-input"
|
||||
disabled={disabled}
|
||||
type="checkbox"
|
||||
ref={name}
|
||||
/>
|
||||
<span
|
||||
className="h6-heading form-text"
|
||||
dangerouslySetInnerHTML={{ __html: label }}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.filter(v => v);
|
||||
}
|
||||
|
||||
// state update function
|
||||
|
@ -90,7 +102,7 @@ export default class Petition extends React.Component {
|
|||
apiSuccess: true
|
||||
};
|
||||
|
||||
if (this.props.modals && this.props.modals.length>0) {
|
||||
if (this.props.modals && this.props.modals.length > 0) {
|
||||
update.showDonationModal = true;
|
||||
}
|
||||
|
||||
|
@ -99,7 +111,7 @@ export default class Petition extends React.Component {
|
|||
|
||||
// state update function
|
||||
apiSubmissionFailure(e) {
|
||||
if(e && e instanceof Error) {
|
||||
if (e && e instanceof Error) {
|
||||
this.setState({ apiFailed: true });
|
||||
}
|
||||
}
|
||||
|
@ -111,19 +123,24 @@ export default class Petition extends React.Component {
|
|||
|
||||
// state update function
|
||||
basketSubmissionFailure(e) {
|
||||
if(e && e instanceof Error) {
|
||||
if (e && e instanceof Error) {
|
||||
this.setState({ basketFailed: true });
|
||||
}
|
||||
}
|
||||
|
||||
// state check function
|
||||
apiIsDone() {
|
||||
return this.state.apiSubmitted && (this.state.apiSuccess || this.state.apiFailed);
|
||||
return (
|
||||
this.state.apiSubmitted && (this.state.apiSuccess || this.state.apiFailed)
|
||||
);
|
||||
}
|
||||
|
||||
// state check function
|
||||
basketIsDone() {
|
||||
return this.state.basketSubmitted && (this.state.basketSuccess || this.state.basketFailed);
|
||||
return (
|
||||
this.state.basketSubmitted &&
|
||||
(this.state.basketSuccess || this.state.basketFailed)
|
||||
);
|
||||
}
|
||||
|
||||
// state check function
|
||||
|
@ -171,47 +188,47 @@ export default class Petition extends React.Component {
|
|||
|
||||
// These should not be possible due to the fact that we validate
|
||||
// their content prior to submission. TODO: remove these rejections?
|
||||
if(!givenNames || !surname) {
|
||||
if (!givenNames || !surname) {
|
||||
return reject(new Error(`missing name/surname`));
|
||||
}
|
||||
|
||||
if(this.props.requiresCountryCode === `True` && !country) {
|
||||
if (this.props.requiresCountryCode === `True` && !country) {
|
||||
return reject(new Error(`missing country`));
|
||||
}
|
||||
|
||||
if(this.props.requiresPostalCode === `True` && !postalCode) {
|
||||
if (this.props.requiresPostalCode === `True` && !postalCode) {
|
||||
return reject(new Error(`missing postal code`));
|
||||
}
|
||||
|
||||
if(this.props.commentRequirements === `required` && !comment) {
|
||||
if (this.props.commentRequirements === `required` && !comment) {
|
||||
return reject(new Error(`missing required comment`));
|
||||
}
|
||||
|
||||
if (this.refs.newsletterSignup) {
|
||||
newsletterSignup = !!(this.refs.newsletterSignup.checked);
|
||||
newsletterSignup = !!this.refs.newsletterSignup.checked;
|
||||
}
|
||||
|
||||
let payload = {
|
||||
givenNames,
|
||||
surname,
|
||||
email: this.email.element.value,
|
||||
checkbox1: this.props.checkbox1 ? !!(this.refs.checkbox1.checked) : null,
|
||||
checkbox2: this.props.checkbox2 ? !!(this.refs.checkbox2.checked) : null,
|
||||
checkbox1: this.props.checkbox1 ? !!this.refs.checkbox1.checked : null,
|
||||
checkbox2: this.props.checkbox2 ? !!this.refs.checkbox2.checked : null,
|
||||
newsletterSignup,
|
||||
country,
|
||||
postalCode,
|
||||
comment,
|
||||
source: window.location.toString(),
|
||||
source: window.location.toString()
|
||||
};
|
||||
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if(xhr.readyState !== XMLHttpRequest.DONE) {
|
||||
if (xhr.readyState !== XMLHttpRequest.DONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(xhr.status !== 201) {
|
||||
if (xhr.status !== 201) {
|
||||
reject(new Error(xhr.responseText));
|
||||
}
|
||||
|
||||
|
@ -220,7 +237,7 @@ export default class Petition extends React.Component {
|
|||
|
||||
xhr.open(`POST`, this.props.apiUrl, true);
|
||||
xhr.setRequestHeader(`Content-Type`, `application/json`);
|
||||
xhr.setRequestHeader(`X-Requested-With`,`XMLHttpRequest`);
|
||||
xhr.setRequestHeader(`X-Requested-With`, `XMLHttpRequest`);
|
||||
xhr.setRequestHeader(`X-CSRFToken`, this.props.csrfToken);
|
||||
xhr.timeout = 5000;
|
||||
xhr.ontimeout = () => reject(new Error(`xhr timed out`));
|
||||
|
@ -247,13 +264,17 @@ export default class Petition extends React.Component {
|
|||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if(this.email.element.value && this.refs.privacy.checked) {
|
||||
if(this.refs.newsletterSignup.checked) {
|
||||
basketSignup({
|
||||
email: this.email.element.value,
|
||||
privacy: this.refs.privacy.checked,
|
||||
newsletter: this.props.newsletter
|
||||
}, resolve, reject);
|
||||
if (this.email.element.value && this.refs.privacy.checked) {
|
||||
if (this.refs.newsletterSignup.checked) {
|
||||
basketSignup(
|
||||
{
|
||||
email: this.email.element.value,
|
||||
privacy: this.refs.privacy.checked,
|
||||
newsletter: this.props.newsletter
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
|
@ -291,7 +312,6 @@ export default class Petition extends React.Component {
|
|||
postalCode = !!this.postalCode.element.value;
|
||||
}
|
||||
|
||||
|
||||
if (this.props.commentRequirements === `required`) {
|
||||
comment = !!this.comment.element.value;
|
||||
}
|
||||
|
@ -374,12 +394,14 @@ export default class Petition extends React.Component {
|
|||
let success = signingIsDone && this.state.apiSuccess; // we don't actually care about basket succeeding, see (5)
|
||||
|
||||
let signupState = classNames({
|
||||
'row': true,
|
||||
'sign-success': success,
|
||||
'sign-failure': signingIsDone && (this.state.apiFailed || this.state.basketFailed)
|
||||
row: true,
|
||||
"sign-success": success,
|
||||
"sign-failure":
|
||||
signingIsDone && (this.state.apiFailed || this.state.basketFailed)
|
||||
});
|
||||
|
||||
let unrecoverableError = this.state.apiSubmitted && !this.state.apiSuccess && this.state.apiFailed;
|
||||
let unrecoverableError =
|
||||
this.state.apiSubmitted && !this.state.apiSuccess && this.state.apiFailed;
|
||||
|
||||
let petitionContent, formContent;
|
||||
|
||||
|
@ -389,18 +411,21 @@ export default class Petition extends React.Component {
|
|||
petitionContent = this.renderUnrecoverableError();
|
||||
} else {
|
||||
petitionContent = this.renderStandardHeading();
|
||||
formContent = !unrecoverableError && !this.state.basketSuccess && this.renderSubmissionForm();
|
||||
formContent =
|
||||
!unrecoverableError &&
|
||||
!this.state.basketSuccess &&
|
||||
this.renderSubmissionForm();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={signupState}>
|
||||
<div className="col">
|
||||
<div className="row">
|
||||
<div className="col-12 petition-content">{ petitionContent }</div>
|
||||
{ formContent }
|
||||
<div className="col-12 petition-content">{petitionContent}</div>
|
||||
{formContent}
|
||||
</div>
|
||||
</div>
|
||||
{ this.state.showDonationModal ? this.renderDonationModal() : null }
|
||||
{this.state.showDonationModal ? this.renderDonationModal() : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -414,23 +439,25 @@ export default class Petition extends React.Component {
|
|||
// This is where can do client-side A/B testing
|
||||
let modals = this.modals;
|
||||
|
||||
if (modals.length===0) {
|
||||
if (modals.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let modal = modals[0];
|
||||
|
||||
return <DonationModal
|
||||
slug={this.props.ctaSlug}
|
||||
name={modal.name}
|
||||
heading={modal.header}
|
||||
bodyText={modal.body}
|
||||
donateText={modal.donate_text}
|
||||
shareText={modal.dismiss_text}
|
||||
onDonate={() => this.userElectedToDonate()}
|
||||
onShare={() => this.userElectedToShare()}
|
||||
onClose={() => this.setState({ showDonationModal: false })}
|
||||
/>;
|
||||
return (
|
||||
<DonationModal
|
||||
slug={this.props.ctaSlug}
|
||||
name={modal.name}
|
||||
heading={modal.header}
|
||||
bodyText={modal.body}
|
||||
donateText={modal.donate_text}
|
||||
shareText={modal.dismiss_text}
|
||||
onDonate={() => this.userElectedToDonate()}
|
||||
onShare={() => this.userElectedToShare()}
|
||||
onClose={() => this.setState({ showDonationModal: false })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -441,7 +468,9 @@ export default class Petition extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
<p>{this.props.thankYou}</p>
|
||||
<a href={this.props.shareLink} className="btn btn-info">{this.props.shareText}</a>
|
||||
<a href={this.props.shareLink} className="btn btn-info">
|
||||
{this.props.shareText}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
@ -449,21 +478,51 @@ export default class Petition extends React.Component {
|
|||
<div>
|
||||
<p>{this.props.thankYou}</p>
|
||||
<div className="share-btn-container">
|
||||
<button className="share-progress-btn share-progress-fb" onClick={e => this.fbButtonClicked(e)}>
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 264 512">
|
||||
<path fill="currentColor" d="M76.7 512V283H0v-91h76.7v-71.7C76.7 42.4 124.3 0 193.8 0c33.3 0 61.9 2.5 70.2 3.6V85h-48.2c-37.8 0-45.1 18-45.1 44.3V192H256l-11.7 91h-73.6v229"></path>
|
||||
<button
|
||||
className="share-progress-btn share-progress-fb"
|
||||
onClick={e => this.fbButtonClicked(e)}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 264 512"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M76.7 512V283H0v-91h76.7v-71.7C76.7 42.4 124.3 0 193.8 0c33.3 0 61.9 2.5 70.2 3.6V85h-48.2c-37.8 0-45.1 18-45.1 44.3V192H256l-11.7 91h-73.6v229"
|
||||
/>
|
||||
</svg>
|
||||
SHARE
|
||||
</button>
|
||||
<button className="share-progress-btn share-progress-tw" onClick={e => this.twButtonClicked(e)}>
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path fill="currentColor" d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path>
|
||||
<button
|
||||
className="share-progress-btn share-progress-tw"
|
||||
onClick={e => this.twButtonClicked(e)}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"
|
||||
/>
|
||||
</svg>
|
||||
TWEET
|
||||
</button>
|
||||
<button className="share-progress-btn share-progress-em" onClick={e => this.emButtonClicked(e)}>
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path fill="currentColor" d="M502.3 190.8c3.9-3.1 9.7-.2 9.7 4.7V400c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V195.6c0-5 5.7-7.8 9.7-4.7 22.4 17.4 52.1 39.5 154.1 113.6 21.1 15.4 56.7 47.8 92.2 47.6 35.7.3 72-32.8 92.3-47.6 102-74.1 131.6-96.3 154-113.7zM256 320c23.2.4 56.6-29.2 73.4-41.4 132.7-96.3 142.8-104.7 173.4-128.7 5.8-4.5 9.2-11.5 9.2-18.9v-19c0-26.5-21.5-48-48-48H48C21.5 64 0 85.5 0 112v19c0 7.4 3.4 14.3 9.2 18.9 30.6 23.9 40.7 32.4 173.4 128.7 16.8 12.2 50.2 41.8 73.4 41.4z"></path>
|
||||
<button
|
||||
className="share-progress-btn share-progress-em"
|
||||
onClick={e => this.emButtonClicked(e)}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M502.3 190.8c3.9-3.1 9.7-.2 9.7 4.7V400c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V195.6c0-5 5.7-7.8 9.7-4.7 22.4 17.4 52.1 39.5 154.1 113.6 21.1 15.4 56.7 47.8 92.2 47.6 35.7.3 72-32.8 92.3-47.6 102-74.1 131.6-96.3 154-113.7zM256 320c23.2.4 56.6-29.2 73.4-41.4 132.7-96.3 142.8-104.7 173.4-128.7 5.8-4.5 9.2-11.5 9.2-18.9v-19c0-26.5-21.5-48-48-48H48C21.5 64 0 85.5 0 112v19c0 7.4 3.4 14.3 9.2 18.9 30.6 23.9 40.7 32.4 173.4 128.7 16.8 12.2 50.2 41.8 73.4 41.4z"
|
||||
/>
|
||||
</svg>
|
||||
EMAIL
|
||||
</button>
|
||||
|
@ -479,7 +538,10 @@ export default class Petition extends React.Component {
|
|||
renderUnrecoverableError() {
|
||||
return (
|
||||
<div>
|
||||
<p>Something went wrong while trying to sign the petition. Please try again later.</p>
|
||||
<p>
|
||||
Something went wrong while trying to sign the petition. Please try
|
||||
again later.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -496,14 +558,13 @@ export default class Petition extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @returns {jsx} Petition cta description if one exists.
|
||||
*/
|
||||
renderCtaDescription() {
|
||||
if (this.props.ctaDescription) {
|
||||
return (
|
||||
<p dangerouslySetInnerHTML={{__html:this.props.ctaDescription}}></p>
|
||||
<p dangerouslySetInnerHTML={{ __html: this.props.ctaDescription }} />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
@ -517,35 +578,52 @@ export default class Petition extends React.Component {
|
|||
* @returns {jsx} the petition form
|
||||
*/
|
||||
renderSubmissionForm() {
|
||||
let disableFields = (this.state.userTriedSubmitting && this.state.apiSubmitted) ? `disabled` : null;
|
||||
let disableFields =
|
||||
this.state.userTriedSubmitting && this.state.apiSubmitted
|
||||
? `disabled`
|
||||
: null;
|
||||
|
||||
let givenGroupClass = classNames({
|
||||
'has-danger': this.state.userTriedSubmitting && !this.givenNames.element.value
|
||||
"has-danger":
|
||||
this.state.userTriedSubmitting && !this.givenNames.element.value
|
||||
});
|
||||
|
||||
let surGroupClass = classNames({
|
||||
'has-danger': this.state.userTriedSubmitting && !this.surname.element.value
|
||||
"has-danger":
|
||||
this.state.userTriedSubmitting && !this.surname.element.value
|
||||
});
|
||||
|
||||
let emailGroupClass = classNames({
|
||||
'has-danger': this.state.userTriedSubmitting && (!this.email.element.value || !this.validatesAsEmail(this.email.element.value))
|
||||
"has-danger":
|
||||
this.state.userTriedSubmitting &&
|
||||
(!this.email.element.value ||
|
||||
!this.validatesAsEmail(this.email.element.value))
|
||||
});
|
||||
|
||||
let countryGroupClass = classNames({
|
||||
'has-danger': this.props.requiresCountryCode === `True` && this.state.userTriedSubmitting && !this.country.element.value
|
||||
"has-danger":
|
||||
this.props.requiresCountryCode === `True` &&
|
||||
this.state.userTriedSubmitting &&
|
||||
!this.country.element.value
|
||||
});
|
||||
|
||||
let postalCodeGroupClass = classNames({
|
||||
'has-danger': this.props.requiresPostalCode === `True` && this.state.userTriedSubmitting && !this.postalCode.element.value
|
||||
"has-danger":
|
||||
this.props.requiresPostalCode === `True` &&
|
||||
this.state.userTriedSubmitting &&
|
||||
!this.postalCode.element.value
|
||||
});
|
||||
|
||||
let commentGroupClass = classNames({
|
||||
'has-danger': this.props.commentRequirements === `required` && this.state.userTriedSubmitting && !this.comment.element.value
|
||||
"has-danger":
|
||||
this.props.commentRequirements === `required` &&
|
||||
this.state.userTriedSubmitting &&
|
||||
!this.comment.element.value
|
||||
});
|
||||
|
||||
let privacyClass = classNames(`my-3`, {
|
||||
'form-check': true,
|
||||
'has-danger': this.state.userTriedSubmitting && !this.refs.privacy.checked
|
||||
"form-check": true,
|
||||
"has-danger": this.state.userTriedSubmitting && !this.refs.privacy.checked
|
||||
});
|
||||
|
||||
let checkboxes = this.generateCheckboxes(disableFields);
|
||||
|
@ -557,97 +635,188 @@ export default class Petition extends React.Component {
|
|||
<div className={givenGroupClass}>
|
||||
<FloatingLabelInput
|
||||
className="mb-1 w-100"
|
||||
ref={(element) => { this.givenNames = element; }} id="givenNames"
|
||||
type="text" label={ get(`First name`) }
|
||||
ref={element => {
|
||||
this.givenNames = element;
|
||||
}}
|
||||
id="givenNames"
|
||||
type="text"
|
||||
label={get(`First name`)}
|
||||
disabled={disableFields}
|
||||
onFocus={this.onInputFocus}
|
||||
/>
|
||||
{this.state.userTriedSubmitting && !this.givenNames.element.value && <small className="form-check form-control-feedback">{ get(`Please enter your given name(s)`) }</small>}
|
||||
{this.state.userTriedSubmitting &&
|
||||
!this.givenNames.element.value && (
|
||||
<small className="form-check form-control-feedback">
|
||||
{get(`Please enter your given name(s)`)}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={surGroupClass}>
|
||||
<FloatingLabelInput
|
||||
className="mb-1 w-100"
|
||||
ref={(element) => { this.surname = element; }} id="surname"
|
||||
type="text" label={ get(`Last name`) }
|
||||
ref={element => {
|
||||
this.surname = element;
|
||||
}}
|
||||
id="surname"
|
||||
type="text"
|
||||
label={get(`Last name`)}
|
||||
disabled={disableFields}
|
||||
onFocus={this.onInputFocus}
|
||||
/>
|
||||
{this.state.userTriedSubmitting && !this.surname.element.value && <small className="form-check form-control-feedback">{ get(`Please enter your surname`) }</small>}
|
||||
{this.state.userTriedSubmitting &&
|
||||
!this.surname.element.value && (
|
||||
<small className="form-check form-control-feedback">
|
||||
{get(`Please enter your surname`)}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={emailGroupClass}>
|
||||
<FloatingLabelInput
|
||||
className="mb-1 w-100"
|
||||
ref={(element) => { this.email = element; }} id="emailInput"
|
||||
type="email" label={ get(`Email address`) }
|
||||
ref={element => {
|
||||
this.email = element;
|
||||
}}
|
||||
id="emailInput"
|
||||
type="email"
|
||||
label={get(`Email address`)}
|
||||
disabled={disableFields}
|
||||
onFocus={this.onInputFocus}
|
||||
/>
|
||||
{this.state.userTriedSubmitting && (!this.email.element.value || !this.validatesAsEmail(this.email.element.value)) && <small className="form-check form-control-feedback">{ get(`Please enter your email`) }</small>}
|
||||
{this.state.userTriedSubmitting &&
|
||||
(!this.email.element.value ||
|
||||
!this.validatesAsEmail(this.email.element.value)) && (
|
||||
<small className="form-check form-control-feedback">
|
||||
{get(`Please enter your email`)}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{ this.legacy === `True` ? null :
|
||||
{this.legacy === `True` ? null : (
|
||||
<div className={countryGroupClass}>
|
||||
<CountrySelect
|
||||
className="mb-1 w-100"
|
||||
ref={(element) => { this.country = element; }}
|
||||
label={ get(`Your country`) }
|
||||
ref={element => {
|
||||
this.country = element;
|
||||
}}
|
||||
label={get(`Your country`)}
|
||||
disabled={disableFields}
|
||||
onFocus={this.onInputFocus}
|
||||
/>
|
||||
{ this.props.requiresCountryCode === `True` && this.state.userTriedSubmitting && !this.country.element.value && <small className="form-check form-control-feedback">{ get(`Please enter your country`) }</small>}
|
||||
{this.props.requiresCountryCode === `True` &&
|
||||
this.state.userTriedSubmitting &&
|
||||
!this.country.element.value && (
|
||||
<small className="form-check form-control-feedback">
|
||||
{get(`Please enter your country`)}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
|
||||
|
||||
{ this.props.requiresPostalCode === `False` ? null :
|
||||
{this.props.requiresPostalCode === `False` ? null : (
|
||||
<div className={postalCodeGroupClass}>
|
||||
<FloatingLabelInput
|
||||
className="mb-1 w-100"
|
||||
ref={(element) => { this.postalCode = element; }} id="postalCodeInput"
|
||||
type="text" label={ get(`Postal code`) }
|
||||
ref={element => {
|
||||
this.postalCode = element;
|
||||
}}
|
||||
id="postalCodeInput"
|
||||
type="text"
|
||||
label={get(`Postal code`)}
|
||||
disabled={disableFields}
|
||||
onFocus={this.onInputFocus}
|
||||
/>
|
||||
{this.state.userTriedSubmitting && !this.postalCode.element.value && <small className="form-check form-control-feedback">{ get(`Please enter your postal code`) }</small>}
|
||||
{this.state.userTriedSubmitting &&
|
||||
!this.postalCode.element.value && (
|
||||
<small className="form-check form-control-feedback">
|
||||
{get(`Please enter your postal code`)}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
|
||||
{ this.props.commentRequirements === `none` ? null :
|
||||
{this.props.commentRequirements === `none` ? null : (
|
||||
<div className={commentGroupClass}>
|
||||
<FloatingLabelTextarea
|
||||
className="mb-1 w-100"
|
||||
ref={(element) => { this.comment = element; }} id="commentInput"
|
||||
type="text" label="Comment"
|
||||
ref={element => {
|
||||
this.comment = element;
|
||||
}}
|
||||
id="commentInput"
|
||||
type="text"
|
||||
label="Comment"
|
||||
disabled={disableFields}
|
||||
onFocus={this.onInputFocus}
|
||||
/>
|
||||
{this.props.commentRequirements === `required` && this.state.userTriedSubmitting && !this.comment.element.value && <small className="form-check form-control-feedback">Please include a comment</small>}
|
||||
{this.props.commentRequirements === `required` &&
|
||||
this.state.userTriedSubmitting &&
|
||||
!this.comment.element.value && (
|
||||
<small className="form-check form-control-feedback">
|
||||
Please include a comment
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
)}
|
||||
</div>
|
||||
{this.state.basketFailed && <small className="form-check form-control-feedback">{ get(`Something went wrong. Please check your email address and try again`) }</small>}
|
||||
{this.state.basketFailed && (
|
||||
<small className="form-check form-control-feedback">
|
||||
{get(
|
||||
`Something went wrong. Please check your email address and try again`
|
||||
)}
|
||||
</small>
|
||||
)}
|
||||
<div className={privacyClass}>
|
||||
<div className="my-2">
|
||||
<label className="form-check-label">
|
||||
<input disabled={disableFields} type="checkbox" className="form-check-input" id="PrivacyCheckbox" ref="privacy" />
|
||||
<span className="h6-heading form-text">{ get(`I'm okay with Mozilla handling my info as explained in this Privacy Notice`) }</span>
|
||||
{this.state.userTriedSubmitting && !this.refs.privacy.checked && <small className="has-danger">{ get(`Please check this box if you want to proceed`) }</small>}
|
||||
<input
|
||||
disabled={disableFields}
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
id="PrivacyCheckbox"
|
||||
ref="privacy"
|
||||
/>
|
||||
<span className="h6-heading form-text">
|
||||
{get(
|
||||
`I'm okay with Mozilla handling my info as explained in this Privacy Notice`
|
||||
)}
|
||||
</span>
|
||||
{this.state.userTriedSubmitting &&
|
||||
!this.refs.privacy.checked && (
|
||||
<small className="has-danger">
|
||||
{get(`Please check this box if you want to proceed`)}
|
||||
</small>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
{ this.props.subscribed ? null :
|
||||
{this.props.subscribed ? null : (
|
||||
<div className="my-2">
|
||||
<label className="form-check-label">
|
||||
<input disabled={disableFields} type="checkbox" className="form-check-input" id="NewsletterSignup" ref="newsletterSignup" />
|
||||
<span className="h6-heading form-text">{ get(`Yes, I want to receive email updates about Mozilla's campaigns.`) }</span>
|
||||
<input
|
||||
disabled={disableFields}
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
id="NewsletterSignup"
|
||||
ref="newsletterSignup"
|
||||
/>
|
||||
<span className="h6-heading form-text">
|
||||
{get(
|
||||
`Yes, I want to receive email updates about Mozilla's campaigns.`
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
{ checkboxes.length > 0 ? (<div className="my-2">{checkboxes}</div>) : null }
|
||||
)}
|
||||
{checkboxes.length > 0 ? (
|
||||
<div className="my-2">{checkboxes}</div>
|
||||
) : null}
|
||||
<div className="mt-3">
|
||||
<button disabled={disableFields} className="col-12 btn btn-normal petition-btn">{ get(`Add my name`) }</button>
|
||||
<button
|
||||
disabled={disableFields}
|
||||
className="col-12 btn btn-normal petition-btn"
|
||||
>
|
||||
{get(`Add my name`)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -1,254 +1,254 @@
|
|||
const SALESFORCE_COUNTRY_LIST = {
|
||||
"AF": `Afghanistan`,
|
||||
"AX": `Aland Islands`,
|
||||
"AL": `Albania`,
|
||||
"DZ": `Algeria`,
|
||||
"AS": `American Samoa`,
|
||||
"AD": `Andorra`,
|
||||
"AO": `Angola`,
|
||||
"AI": `Anguilla`,
|
||||
"AQ": `Antarctica`,
|
||||
"AG": `Antigua and Barbuda`,
|
||||
"AR": `Argentina`,
|
||||
"AM": `Armenia`,
|
||||
"AW": `Aruba`,
|
||||
"AU": `Australia`,
|
||||
"AT": `Austria`,
|
||||
"AZ": `Azerbaijan`,
|
||||
"BS": `Bahamas`,
|
||||
"BH": `Bahrain`,
|
||||
"BD": `Bangladesh`,
|
||||
"BB": `Barbados`,
|
||||
"BY": `Belarus`,
|
||||
"BE": `Belgium`,
|
||||
"BZ": `Belize`,
|
||||
"BJ": `Benin`,
|
||||
"BM": `Bermuda`,
|
||||
"BT": `Bhutan`,
|
||||
"BO": `Bolivia, Plurinational State of`,
|
||||
"BQ": `Bonaire, Sint Eustatius and Saba`,
|
||||
"BA": `Bosnia and Herzegovina`,
|
||||
"BW": `Botswana`,
|
||||
"BV": `Bouvet Island`,
|
||||
"BR": `Brazil`,
|
||||
"IO": `British Indian Ocean Territory`,
|
||||
"BN": `Brunei Darussalam`,
|
||||
"BG": `Bulgaria`,
|
||||
"BF": `Burkina Faso`,
|
||||
"BI": `Burundi`,
|
||||
"KH": `Cambodia`,
|
||||
"CM": `Cameroon`,
|
||||
"CA": `Canada`,
|
||||
"CV": `Cape Verde`,
|
||||
"KY": `Cayman Islands`,
|
||||
"CF": `Central African Republic`,
|
||||
"TD": `Chad`,
|
||||
"CL": `Chile`,
|
||||
"CN": `China`,
|
||||
"CX": `Christmas Island`,
|
||||
"CC": `Cocos (Keeling) Islands`,
|
||||
"CO": `Colombia`,
|
||||
"KM": `Comoros`,
|
||||
"CG": `Congo`,
|
||||
"CD": `Congo, the Democratic Republic of the`,
|
||||
"CK": `Cook Islands`,
|
||||
"CR": `Costa Rica`,
|
||||
"CI": `Cote d'Ivoire`,
|
||||
"HR": `Croatia`,
|
||||
"CU": `Cuba`,
|
||||
"CW": `Curaçao`,
|
||||
"CY": `Cyprus`,
|
||||
"CZ": `Czech Republic`,
|
||||
"DK": `Denmark`,
|
||||
"DJ": `Djibouti`,
|
||||
"DM": `Dominica`,
|
||||
"DO": `Dominican Republic`,
|
||||
"EC": `Ecuador`,
|
||||
"EG": `Egypt`,
|
||||
"SV": `El Salvador`,
|
||||
"GQ": `Equatorial Guinea`,
|
||||
"ER": `Eritrea`,
|
||||
"EE": `Estonia`,
|
||||
"ET": `Ethiopia`,
|
||||
"FK": `Falkland Islands (Malvinas)`,
|
||||
"FO": `Faroe Islands`,
|
||||
"FJ": `Fiji`,
|
||||
"FI": `Finland`,
|
||||
"FR": `France`,
|
||||
"GF": `French Guiana`,
|
||||
"PF": `French Polynesia`,
|
||||
"TF": `French Southern Territories`,
|
||||
"GA": `Gabon`,
|
||||
"GM": `Gambia`,
|
||||
"GE": `Georgia`,
|
||||
"DE": `Germany`,
|
||||
"GH": `Ghana`,
|
||||
"GI": `Gibraltar`,
|
||||
"GR": `Greece`,
|
||||
"GL": `Greenland`,
|
||||
"GD": `Grenada`,
|
||||
"GP": `Guadeloupe`,
|
||||
"GU": `Guam`,
|
||||
"GT": `Guatemala`,
|
||||
"GG": `Guernsey`,
|
||||
"GN": `Guinea`,
|
||||
"GW": `Guinea-Bissau`,
|
||||
"GY": `Guyana`,
|
||||
"HT": `Haiti`,
|
||||
"HM": `Heard Island and McDonald Islands`,
|
||||
"VA": `Holy See (Vatican City State)`,
|
||||
"HN": `Honduras`,
|
||||
"HK": `Hong Kong`,
|
||||
"HU": `Hungary`,
|
||||
"IS": `Iceland`,
|
||||
"IN": `India`,
|
||||
"ID": `Indonesia`,
|
||||
"IR": `Iran, Islamic Republic of`,
|
||||
"IQ": `Iraq`,
|
||||
"IE": `Ireland`,
|
||||
"IM": `Isle of Man`,
|
||||
"IL": `Israel`,
|
||||
"IT": `Italy`,
|
||||
"JM": `Jamaica`,
|
||||
"JP": `Japan`,
|
||||
"JE": `Jersey`,
|
||||
"JO": `Jordan`,
|
||||
"KZ": `Kazakhstan`,
|
||||
"KE": `Kenya`,
|
||||
"KI": `Kiribati`,
|
||||
"KP": `Korea, Democratic People's Republic of`,
|
||||
"KR": `Korea, Republic of`,
|
||||
"KW": `Kuwait`,
|
||||
"KG": `Kyrgyzstan`,
|
||||
"LA": `Lao People's Democratic Republic`,
|
||||
"LV": `Latvia`,
|
||||
"LB": `Lebanon`,
|
||||
"LS": `Lesotho`,
|
||||
"LR": `Liberia`,
|
||||
"LY": `Libya`,
|
||||
"LI": `Liechtenstein`,
|
||||
"LT": `Lithuania`,
|
||||
"LU": `Luxembourg`,
|
||||
"MO": `Macao`,
|
||||
"MK": `Macedonia, the former Yugoslav Republic of`,
|
||||
"MG": `Madagascar`,
|
||||
"MW": `Malawi`,
|
||||
"MY": `Malaysia`,
|
||||
"MV": `Maldives`,
|
||||
"ML": `Mali`,
|
||||
"MT": `Malta`,
|
||||
"MH": `Marshall Islands`,
|
||||
"MQ": `Martinique`,
|
||||
"MR": `Mauritania`,
|
||||
"MU": `Mauritius`,
|
||||
"YT": `Mayotte`,
|
||||
"MX": `Mexico`,
|
||||
"FM": `Micronesia`,
|
||||
"MD": `Moldova, Republic of`,
|
||||
"MC": `Monaco`,
|
||||
"MN": `Mongolia`,
|
||||
"ME": `Montenegro`,
|
||||
"MS": `Montserrat`,
|
||||
"MA": `Morocco`,
|
||||
"MZ": `Mozambique`,
|
||||
"MM": `Myanmar`,
|
||||
"NA": `Namibia`,
|
||||
"NR": `Nauru`,
|
||||
"NP": `Nepal`,
|
||||
"NL": `Netherlands`,
|
||||
"AN": `Netherlands Antilles`,
|
||||
"NC": `New Caledonia`,
|
||||
"NZ": `New Zealand`,
|
||||
"NI": `Nicaragua`,
|
||||
"NE": `Niger`,
|
||||
"NG": `Nigeria`,
|
||||
"NU": `Niue`,
|
||||
"NF": `Norfolk Island`,
|
||||
"MP": `Northern Mariana Islands`,
|
||||
"NO": `Norway`,
|
||||
"OM": `Oman`,
|
||||
"PK": `Pakistan`,
|
||||
"PW": `Palau`,
|
||||
"PS": `Palestine`,
|
||||
"PA": `Panama`,
|
||||
"PG": `Papua New Guinea`,
|
||||
"PY": `Paraguay`,
|
||||
"PE": `Peru`,
|
||||
"PH": `Philippines`,
|
||||
"PN": `Pitcairn`,
|
||||
"PL": `Poland`,
|
||||
"PT": `Portugal`,
|
||||
"PR": `Puerto Rico`,
|
||||
"QA": `Qatar`,
|
||||
"RE": `Reunion`,
|
||||
"RO": `Romania`,
|
||||
"RU": `Russian Federation`,
|
||||
"RW": `Rwanda`,
|
||||
"BL": `Saint Barthélemy`,
|
||||
"SH": `Saint Helena, Ascension and Tristan da Cunha`,
|
||||
"KN": `Saint Kitts and Nevis`,
|
||||
"LC": `Saint Lucia`,
|
||||
"MF": `Saint Martin (French part)`,
|
||||
"PM": `Saint Pierre and Miquelon`,
|
||||
"VC": `Saint Vincent and the Grenadines`,
|
||||
"WS": `Samoa`,
|
||||
"SM": `San Marino`,
|
||||
"ST": `Sao Tome and Principe`,
|
||||
"SA": `Saudi Arabia`,
|
||||
"SN": `Senegal`,
|
||||
"RS": `Serbia`,
|
||||
"SC": `Seychelles`,
|
||||
"SL": `Sierra Leone`,
|
||||
"SG": `Singapore`,
|
||||
"SX": `Sint Maarten (Dutch part)`,
|
||||
"SK": `Slovakia`,
|
||||
"SI": `Slovenia`,
|
||||
"SB": `Solomon Islands`,
|
||||
"SO": `Somalia`,
|
||||
"ZA": `South Africa`,
|
||||
"GS": `South Georgia and the South Sandwich Islands`,
|
||||
"SS": `South Sudan`,
|
||||
"ES": `Spain`,
|
||||
"LK": `Sri Lanka`,
|
||||
"SD": `Sudan`,
|
||||
"SR": `Suriname`,
|
||||
"SJ": `Svalbard and Jan Mayen`,
|
||||
"SZ": `Swaziland`,
|
||||
"SE": `Sweden`,
|
||||
"CH": `Switzerland`,
|
||||
"SY": `Syrian Arab Republic`,
|
||||
"TW": `Taiwan`,
|
||||
"TJ": `Tajikistan`,
|
||||
"TZ": `Tanzania, United Republic of`,
|
||||
"TH": `Thailand`,
|
||||
"TL": `Timor-Leste`,
|
||||
"TG": `Togo`,
|
||||
"TK": `Tokelau`,
|
||||
"TO": `Tonga`,
|
||||
"TT": `Trinidad and Tobago`,
|
||||
"TN": `Tunisia`,
|
||||
"TR": `Turkey`,
|
||||
"TM": `Turkmenistan`,
|
||||
"TC": `Turks and Caicos Islands`,
|
||||
"TV": `Tuvalu`,
|
||||
"VI": `U.S. Virgin Islands`,
|
||||
"UG": `Uganda`,
|
||||
"UA": `Ukraine`,
|
||||
"AE": `United Arab Emirates`,
|
||||
"GB": `United Kingdom`,
|
||||
"US": `United States`,
|
||||
"UM": `United States Minor Outlying Islands`,
|
||||
"UY": `Uruguay`,
|
||||
"UZ": `Uzbekistan`,
|
||||
"VU": `Vanuatu`,
|
||||
"VE": `Venezuela, Bolivarian Republic of`,
|
||||
"VN": `Viet Nam`,
|
||||
"VG": `Virgin Islands, British`,
|
||||
"WF": `Wallis and Futuna`,
|
||||
"EH": `Western Sahara`,
|
||||
"YE": `Yemen`,
|
||||
"ZM": `Zambia`,
|
||||
"ZW": `Zimbabwe`
|
||||
AF: `Afghanistan`,
|
||||
AX: `Aland Islands`,
|
||||
AL: `Albania`,
|
||||
DZ: `Algeria`,
|
||||
AS: `American Samoa`,
|
||||
AD: `Andorra`,
|
||||
AO: `Angola`,
|
||||
AI: `Anguilla`,
|
||||
AQ: `Antarctica`,
|
||||
AG: `Antigua and Barbuda`,
|
||||
AR: `Argentina`,
|
||||
AM: `Armenia`,
|
||||
AW: `Aruba`,
|
||||
AU: `Australia`,
|
||||
AT: `Austria`,
|
||||
AZ: `Azerbaijan`,
|
||||
BS: `Bahamas`,
|
||||
BH: `Bahrain`,
|
||||
BD: `Bangladesh`,
|
||||
BB: `Barbados`,
|
||||
BY: `Belarus`,
|
||||
BE: `Belgium`,
|
||||
BZ: `Belize`,
|
||||
BJ: `Benin`,
|
||||
BM: `Bermuda`,
|
||||
BT: `Bhutan`,
|
||||
BO: `Bolivia, Plurinational State of`,
|
||||
BQ: `Bonaire, Sint Eustatius and Saba`,
|
||||
BA: `Bosnia and Herzegovina`,
|
||||
BW: `Botswana`,
|
||||
BV: `Bouvet Island`,
|
||||
BR: `Brazil`,
|
||||
IO: `British Indian Ocean Territory`,
|
||||
BN: `Brunei Darussalam`,
|
||||
BG: `Bulgaria`,
|
||||
BF: `Burkina Faso`,
|
||||
BI: `Burundi`,
|
||||
KH: `Cambodia`,
|
||||
CM: `Cameroon`,
|
||||
CA: `Canada`,
|
||||
CV: `Cape Verde`,
|
||||
KY: `Cayman Islands`,
|
||||
CF: `Central African Republic`,
|
||||
TD: `Chad`,
|
||||
CL: `Chile`,
|
||||
CN: `China`,
|
||||
CX: `Christmas Island`,
|
||||
CC: `Cocos (Keeling) Islands`,
|
||||
CO: `Colombia`,
|
||||
KM: `Comoros`,
|
||||
CG: `Congo`,
|
||||
CD: `Congo, the Democratic Republic of the`,
|
||||
CK: `Cook Islands`,
|
||||
CR: `Costa Rica`,
|
||||
CI: `Cote d'Ivoire`,
|
||||
HR: `Croatia`,
|
||||
CU: `Cuba`,
|
||||
CW: `Curaçao`,
|
||||
CY: `Cyprus`,
|
||||
CZ: `Czech Republic`,
|
||||
DK: `Denmark`,
|
||||
DJ: `Djibouti`,
|
||||
DM: `Dominica`,
|
||||
DO: `Dominican Republic`,
|
||||
EC: `Ecuador`,
|
||||
EG: `Egypt`,
|
||||
SV: `El Salvador`,
|
||||
GQ: `Equatorial Guinea`,
|
||||
ER: `Eritrea`,
|
||||
EE: `Estonia`,
|
||||
ET: `Ethiopia`,
|
||||
FK: `Falkland Islands (Malvinas)`,
|
||||
FO: `Faroe Islands`,
|
||||
FJ: `Fiji`,
|
||||
FI: `Finland`,
|
||||
FR: `France`,
|
||||
GF: `French Guiana`,
|
||||
PF: `French Polynesia`,
|
||||
TF: `French Southern Territories`,
|
||||
GA: `Gabon`,
|
||||
GM: `Gambia`,
|
||||
GE: `Georgia`,
|
||||
DE: `Germany`,
|
||||
GH: `Ghana`,
|
||||
GI: `Gibraltar`,
|
||||
GR: `Greece`,
|
||||
GL: `Greenland`,
|
||||
GD: `Grenada`,
|
||||
GP: `Guadeloupe`,
|
||||
GU: `Guam`,
|
||||
GT: `Guatemala`,
|
||||
GG: `Guernsey`,
|
||||
GN: `Guinea`,
|
||||
GW: `Guinea-Bissau`,
|
||||
GY: `Guyana`,
|
||||
HT: `Haiti`,
|
||||
HM: `Heard Island and McDonald Islands`,
|
||||
VA: `Holy See (Vatican City State)`,
|
||||
HN: `Honduras`,
|
||||
HK: `Hong Kong`,
|
||||
HU: `Hungary`,
|
||||
IS: `Iceland`,
|
||||
IN: `India`,
|
||||
ID: `Indonesia`,
|
||||
IR: `Iran, Islamic Republic of`,
|
||||
IQ: `Iraq`,
|
||||
IE: `Ireland`,
|
||||
IM: `Isle of Man`,
|
||||
IL: `Israel`,
|
||||
IT: `Italy`,
|
||||
JM: `Jamaica`,
|
||||
JP: `Japan`,
|
||||
JE: `Jersey`,
|
||||
JO: `Jordan`,
|
||||
KZ: `Kazakhstan`,
|
||||
KE: `Kenya`,
|
||||
KI: `Kiribati`,
|
||||
KP: `Korea, Democratic People's Republic of`,
|
||||
KR: `Korea, Republic of`,
|
||||
KW: `Kuwait`,
|
||||
KG: `Kyrgyzstan`,
|
||||
LA: `Lao People's Democratic Republic`,
|
||||
LV: `Latvia`,
|
||||
LB: `Lebanon`,
|
||||
LS: `Lesotho`,
|
||||
LR: `Liberia`,
|
||||
LY: `Libya`,
|
||||
LI: `Liechtenstein`,
|
||||
LT: `Lithuania`,
|
||||
LU: `Luxembourg`,
|
||||
MO: `Macao`,
|
||||
MK: `Macedonia, the former Yugoslav Republic of`,
|
||||
MG: `Madagascar`,
|
||||
MW: `Malawi`,
|
||||
MY: `Malaysia`,
|
||||
MV: `Maldives`,
|
||||
ML: `Mali`,
|
||||
MT: `Malta`,
|
||||
MH: `Marshall Islands`,
|
||||
MQ: `Martinique`,
|
||||
MR: `Mauritania`,
|
||||
MU: `Mauritius`,
|
||||
YT: `Mayotte`,
|
||||
MX: `Mexico`,
|
||||
FM: `Micronesia`,
|
||||
MD: `Moldova, Republic of`,
|
||||
MC: `Monaco`,
|
||||
MN: `Mongolia`,
|
||||
ME: `Montenegro`,
|
||||
MS: `Montserrat`,
|
||||
MA: `Morocco`,
|
||||
MZ: `Mozambique`,
|
||||
MM: `Myanmar`,
|
||||
NA: `Namibia`,
|
||||
NR: `Nauru`,
|
||||
NP: `Nepal`,
|
||||
NL: `Netherlands`,
|
||||
AN: `Netherlands Antilles`,
|
||||
NC: `New Caledonia`,
|
||||
NZ: `New Zealand`,
|
||||
NI: `Nicaragua`,
|
||||
NE: `Niger`,
|
||||
NG: `Nigeria`,
|
||||
NU: `Niue`,
|
||||
NF: `Norfolk Island`,
|
||||
MP: `Northern Mariana Islands`,
|
||||
NO: `Norway`,
|
||||
OM: `Oman`,
|
||||
PK: `Pakistan`,
|
||||
PW: `Palau`,
|
||||
PS: `Palestine`,
|
||||
PA: `Panama`,
|
||||
PG: `Papua New Guinea`,
|
||||
PY: `Paraguay`,
|
||||
PE: `Peru`,
|
||||
PH: `Philippines`,
|
||||
PN: `Pitcairn`,
|
||||
PL: `Poland`,
|
||||
PT: `Portugal`,
|
||||
PR: `Puerto Rico`,
|
||||
QA: `Qatar`,
|
||||
RE: `Reunion`,
|
||||
RO: `Romania`,
|
||||
RU: `Russian Federation`,
|
||||
RW: `Rwanda`,
|
||||
BL: `Saint Barthélemy`,
|
||||
SH: `Saint Helena, Ascension and Tristan da Cunha`,
|
||||
KN: `Saint Kitts and Nevis`,
|
||||
LC: `Saint Lucia`,
|
||||
MF: `Saint Martin (French part)`,
|
||||
PM: `Saint Pierre and Miquelon`,
|
||||
VC: `Saint Vincent and the Grenadines`,
|
||||
WS: `Samoa`,
|
||||
SM: `San Marino`,
|
||||
ST: `Sao Tome and Principe`,
|
||||
SA: `Saudi Arabia`,
|
||||
SN: `Senegal`,
|
||||
RS: `Serbia`,
|
||||
SC: `Seychelles`,
|
||||
SL: `Sierra Leone`,
|
||||
SG: `Singapore`,
|
||||
SX: `Sint Maarten (Dutch part)`,
|
||||
SK: `Slovakia`,
|
||||
SI: `Slovenia`,
|
||||
SB: `Solomon Islands`,
|
||||
SO: `Somalia`,
|
||||
ZA: `South Africa`,
|
||||
GS: `South Georgia and the South Sandwich Islands`,
|
||||
SS: `South Sudan`,
|
||||
ES: `Spain`,
|
||||
LK: `Sri Lanka`,
|
||||
SD: `Sudan`,
|
||||
SR: `Suriname`,
|
||||
SJ: `Svalbard and Jan Mayen`,
|
||||
SZ: `Swaziland`,
|
||||
SE: `Sweden`,
|
||||
CH: `Switzerland`,
|
||||
SY: `Syrian Arab Republic`,
|
||||
TW: `Taiwan`,
|
||||
TJ: `Tajikistan`,
|
||||
TZ: `Tanzania, United Republic of`,
|
||||
TH: `Thailand`,
|
||||
TL: `Timor-Leste`,
|
||||
TG: `Togo`,
|
||||
TK: `Tokelau`,
|
||||
TO: `Tonga`,
|
||||
TT: `Trinidad and Tobago`,
|
||||
TN: `Tunisia`,
|
||||
TR: `Turkey`,
|
||||
TM: `Turkmenistan`,
|
||||
TC: `Turks and Caicos Islands`,
|
||||
TV: `Tuvalu`,
|
||||
VI: `U.S. Virgin Islands`,
|
||||
UG: `Uganda`,
|
||||
UA: `Ukraine`,
|
||||
AE: `United Arab Emirates`,
|
||||
GB: `United Kingdom`,
|
||||
US: `United States`,
|
||||
UM: `United States Minor Outlying Islands`,
|
||||
UY: `Uruguay`,
|
||||
UZ: `Uzbekistan`,
|
||||
VU: `Vanuatu`,
|
||||
VE: `Venezuela, Bolivarian Republic of`,
|
||||
VN: `Viet Nam`,
|
||||
VG: `Virgin Islands, British`,
|
||||
WF: `Wallis and Futuna`,
|
||||
EH: `Western Sahara`,
|
||||
YE: `Yemen`,
|
||||
ZM: `Zambia`,
|
||||
ZW: `Zimbabwe`
|
||||
};
|
||||
|
||||
export default SALESFORCE_COUNTRY_LIST;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
export default class PulseProjectList extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -16,7 +16,7 @@ export default class PulseProjectList extends React.Component {
|
|||
projectXHR.addEventListener(`load`, () => {
|
||||
let projects = JSON.parse(projectXHR.response);
|
||||
|
||||
projects.results = projects.results.sort((a,b) => {
|
||||
projects.results = projects.results.sort((a, b) => {
|
||||
if (this.props.reverseChronological) {
|
||||
return Date.parse(a.created) < Date.parse(b.created) ? 1 : -1;
|
||||
} else {
|
||||
|
@ -24,30 +24,41 @@ export default class PulseProjectList extends React.Component {
|
|||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
projects: this.props.max ? projects.results.slice(0, this.props.max) : projects.results
|
||||
}, () => {
|
||||
if (this.props.whenLoaded) {
|
||||
this.props.whenLoaded();
|
||||
this.setState(
|
||||
{
|
||||
projects: this.props.max
|
||||
? projects.results.slice(0, this.props.max)
|
||||
: projects.results
|
||||
},
|
||||
() => {
|
||||
if (this.props.whenLoaded) {
|
||||
this.props.whenLoaded();
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
|
||||
const apiURL = `${this.props.env.PULSE_API_DOMAIN}/api/pulse/v2/entries/`;
|
||||
|
||||
const params = {
|
||||
"format": `json`,
|
||||
"help_type": this.props.help && this.props.help !== `all` ? this.props.help : null,
|
||||
"issue": this.props.issues && this.props.issues !== `all` ? this.props.issues : null,
|
||||
"page_size": this.props.max ? this.props.max : 12,
|
||||
"search": this.props.query,
|
||||
"featured": this.props.featured && `True`
|
||||
format: `json`,
|
||||
help_type:
|
||||
this.props.help && this.props.help !== `all` ? this.props.help : null,
|
||||
issue:
|
||||
this.props.issues && this.props.issues !== `all`
|
||||
? this.props.issues
|
||||
: null,
|
||||
page_size: this.props.max ? this.props.max : 12,
|
||||
search: this.props.query,
|
||||
featured: this.props.featured && `True`
|
||||
};
|
||||
|
||||
// Serialize parameters into a query string
|
||||
const serializedParams = Object.keys(params).filter((key) => params[key]).map((key) => {
|
||||
return `${key}=${encodeURIComponent(params[key])}`;
|
||||
});
|
||||
const serializedParams = Object.keys(params)
|
||||
.filter(key => params[key])
|
||||
.map(key => {
|
||||
return `${key}=${encodeURIComponent(params[key])}`;
|
||||
});
|
||||
|
||||
const apiURLwithQuery = `${apiURL}?${serializedParams.join(`&`)}`;
|
||||
|
||||
|
@ -68,7 +79,7 @@ export default class PulseProjectList extends React.Component {
|
|||
byline = `By ${project.related_creators.map(rc => rc.name).join(`, `)}`;
|
||||
}
|
||||
|
||||
if (this.props.directLink){
|
||||
if (this.props.directLink) {
|
||||
url = project.content_url;
|
||||
} else {
|
||||
url = `https://${this.props.env.PULSE_DOMAIN}/entry/${project.id}`;
|
||||
|
@ -76,22 +87,34 @@ export default class PulseProjectList extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="col-6 col-md-4 my-4" key={`pulse-project-${index}`}>
|
||||
<a className="pulse-project" href={url} target="_blank" rel="noopener noreferrer">
|
||||
<a
|
||||
className="pulse-project"
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="thumbnail">
|
||||
<div className="img-container">
|
||||
<img className={`project-image${ project.thumbnail ? `` : ` placeholder` }`} src={ project.thumbnail ? project.thumbnail : `/_images/proportional-spacer.png` }/>
|
||||
<img
|
||||
className={`project-image${
|
||||
project.thumbnail ? `` : ` placeholder`
|
||||
}`}
|
||||
src={
|
||||
project.thumbnail
|
||||
? project.thumbnail
|
||||
: `/_images/proportional-spacer.png`
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h5 className="project-title h5-heading my-2">{project.title}</h5>
|
||||
</a>
|
||||
{ byline && <p className="h6-heading my-1">{byline}</p> }
|
||||
{byline && <p className="h6-heading my-1">{byline}</p>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="row">{projectList}</div>
|
||||
);
|
||||
return <div className="row">{projectList}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
let _dntStatus = navigator.doNotTrack || navigator.msDoNotTrack,
|
||||
fxMatch = navigator.userAgent.match(/Firefox\/(\d+)/),
|
||||
ie10Match = navigator.userAgent.match(/MSIE 10/i),
|
||||
w8Match = navigator.appVersion.match(/Windows NT 6.2/);
|
||||
fxMatch = navigator.userAgent.match(/Firefox\/(\d+)/),
|
||||
ie10Match = navigator.userAgent.match(/MSIE 10/i),
|
||||
w8Match = navigator.appVersion.match(/Windows NT 6.2/);
|
||||
|
||||
if (fxMatch && Number(fxMatch[1]) < 32) {
|
||||
_dntStatus = `Unspecified`;
|
||||
} else if (ie10Match && w8Match) {
|
||||
_dntStatus = `Unspecified`;
|
||||
} else {
|
||||
_dntStatus = { '0': `Disabled`, '1': `Enabled` }[_dntStatus] || `Unspecified`;
|
||||
_dntStatus = { "0": `Disabled`, "1": `Enabled` }[_dntStatus] || `Unspecified`;
|
||||
}
|
||||
|
||||
const DNT = {
|
||||
allowTracking: (_dntStatus !== `Enabled`)
|
||||
allowTracking: _dntStatus !== `Enabled`
|
||||
};
|
||||
|
||||
export default DNT;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import ReactGA from '../react-ga-proxy.js';
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import ReactGA from "../react-ga-proxy.js";
|
||||
|
||||
const KEY_STATE = `donate modal state`;
|
||||
const KEY_TIMER = `donate modal timer`;
|
||||
|
@ -39,7 +39,10 @@ class DonateModal extends React.Component {
|
|||
this.startTimer();
|
||||
|
||||
// show modal after delay. If delay is a negative value, show modal immediately
|
||||
setTimeout(() => this.setState({ visible: true }), Math.max(this.state.delay, 0));
|
||||
setTimeout(
|
||||
() => this.setState({ visible: true }),
|
||||
Math.max(this.state.delay, 0)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,11 +66,14 @@ class DonateModal extends React.Component {
|
|||
sessionStorage.setItem(KEY_STATE, `dismissed`);
|
||||
sessionStorage.removeItem(KEY_TIMER);
|
||||
|
||||
this.setState({
|
||||
dismissed: true
|
||||
}, () => {
|
||||
clearInterval(this.runTimer);
|
||||
});
|
||||
this.setState(
|
||||
{
|
||||
dismissed: true
|
||||
},
|
||||
() => {
|
||||
clearInterval(this.runTimer);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -76,9 +82,9 @@ class DonateModal extends React.Component {
|
|||
}
|
||||
|
||||
let title = this.props.title,
|
||||
subheading = this.props.subheading,
|
||||
cta = this.props.cta,
|
||||
utm = this.props.utm;
|
||||
subheading = this.props.subheading,
|
||||
cta = this.props.cta,
|
||||
utm = this.props.utm;
|
||||
|
||||
return (
|
||||
<div className={`donate-modal ${this.state.visible ? `show` : ``}`}>
|
||||
|
@ -88,18 +94,22 @@ class DonateModal extends React.Component {
|
|||
<div className="container">
|
||||
<div className="row align-items-center text-center text-md-left">
|
||||
<div className="col-md-6">
|
||||
<h1 className="h3-heading">{ title }</h1>
|
||||
<p className="normal">{ subheading } </p>
|
||||
<h1 className="h3-heading">{title}</h1>
|
||||
<p className="normal">{subheading} </p>
|
||||
</div>
|
||||
<div className="col-md-4 offset-md-2">
|
||||
<h2 className="h5-heading">{ cta.title }</h2>
|
||||
<h2 className="h5-heading">{cta.title}</h2>
|
||||
<div>
|
||||
<a
|
||||
className="d-block d-md-inline-block text-center btn btn-donate ml-0"
|
||||
onClick={evt => this.handleBtnClick(evt)}
|
||||
href={`https://donate.mozilla.org/?utm_source=foundation.mozilla.org&utm_medium=${utm.medium}&utm_campaign=${utm.campaign}&utm_content=${utm.content}`}
|
||||
href={`https://donate.mozilla.org/?utm_source=foundation.mozilla.org&utm_medium=${
|
||||
utm.medium
|
||||
}&utm_campaign=${utm.campaign}&utm_content=${utm.content}`}
|
||||
target="_blank"
|
||||
>{ cta.text }</a>
|
||||
>
|
||||
{cta.text}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -111,6 +121,6 @@ class DonateModal extends React.Component {
|
|||
|
||||
// Export a manual injection function.
|
||||
|
||||
export default function injectDonateModal(element, props={}) {
|
||||
export default function injectDonateModal(element, props = {}) {
|
||||
ReactDOM.render(<DonateModal {...props} />, element);
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "React" }] */
|
||||
|
||||
import React from 'react';
|
||||
import ReactGA from 'react-ga';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Analytics from './analytics.js';
|
||||
import React from "react";
|
||||
import ReactGA from "react-ga";
|
||||
import ReactDOM from "react-dom";
|
||||
import Analytics from "./analytics.js";
|
||||
|
||||
import JoinUs from './components/join/join.jsx';
|
||||
import Petition from './components/petition/petition.jsx';
|
||||
import People from './components/people/people.jsx';
|
||||
import MemberNotice from './components/member-notice/member-notice.jsx';
|
||||
import MultipageNavMobile from './components/multipage-nav-mobile/multipage-nav-mobile.jsx';
|
||||
import News from './components/news/news.jsx';
|
||||
import SingleFilterFellowList from './components/fellow-list/single-filter-fellow-list.jsx';
|
||||
import PulseProjectList from './components/pulse-project-list/pulse-project-list.jsx';
|
||||
import JoinUs from "./components/join/join.jsx";
|
||||
import Petition from "./components/petition/petition.jsx";
|
||||
import People from "./components/people/people.jsx";
|
||||
import MemberNotice from "./components/member-notice/member-notice.jsx";
|
||||
import MultipageNavMobile from "./components/multipage-nav-mobile/multipage-nav-mobile.jsx";
|
||||
import News from "./components/news/news.jsx";
|
||||
import SingleFilterFellowList from "./components/fellow-list/single-filter-fellow-list.jsx";
|
||||
import PulseProjectList from "./components/pulse-project-list/pulse-project-list.jsx";
|
||||
|
||||
import primaryNav from './primary-nav.js';
|
||||
import primaryNav from "./primary-nav.js";
|
||||
|
||||
const SHOW_MEMBER_NOTICE = false;
|
||||
|
||||
|
@ -28,7 +28,7 @@ const apps = [];
|
|||
|
||||
let main = {
|
||||
init() {
|
||||
this.fetchEnv((envData) => {
|
||||
this.fetchEnv(envData => {
|
||||
env = envData;
|
||||
networkSiteURL = env.NETWORK_SITE_URL;
|
||||
|
||||
|
@ -52,11 +52,15 @@ let main = {
|
|||
},
|
||||
|
||||
decorateExternalLinks() {
|
||||
Array.from( document.querySelectorAll(`a`) ).forEach((link) => {
|
||||
Array.from(document.querySelectorAll(`a`)).forEach(link => {
|
||||
let href = link.getAttribute(`href`);
|
||||
|
||||
// Define an external link as any URL with `//` in it
|
||||
if (href && href.match(/\/\//) && env.TARGET_DOMAINS.some(domain => !href.match(`//${domain}`))) {
|
||||
if (
|
||||
href &&
|
||||
href.match(/\/\//) &&
|
||||
env.TARGET_DOMAINS.some(domain => !href.match(`//${domain}`))
|
||||
) {
|
||||
link.setAttribute(`target`, `_blank`);
|
||||
|
||||
// https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/
|
||||
|
@ -99,7 +103,7 @@ let main = {
|
|||
let ticking = false;
|
||||
let elBurgerWrapper = document.querySelector(`.wrapper-burger`);
|
||||
|
||||
let adjustNavbar = (scrollPosition) => {
|
||||
let adjustNavbar = scrollPosition => {
|
||||
if (scrollPosition > 0) {
|
||||
elBurgerWrapper.classList.add(`scrolled`);
|
||||
} else {
|
||||
|
@ -107,15 +111,20 @@ let main = {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
let elCtaAnchor = document.querySelector(`#cta-anchor`);
|
||||
let elStickyButton = document.querySelector(`.narrow-sticky-button-container`);
|
||||
let elStickyButton = document.querySelector(
|
||||
`.narrow-sticky-button-container`
|
||||
);
|
||||
let noopCtaButton = () => {};
|
||||
let adjustCtaButton = noopCtaButton;
|
||||
|
||||
if (elCtaAnchor && elStickyButton) {
|
||||
let getAnchorPosition = () => {
|
||||
return elCtaAnchor.getBoundingClientRect().top + window.scrollY - window.innerHeight;
|
||||
return (
|
||||
elCtaAnchor.getBoundingClientRect().top +
|
||||
window.scrollY -
|
||||
window.innerHeight
|
||||
);
|
||||
};
|
||||
|
||||
let ctaAnchorPosition = getAnchorPosition();
|
||||
|
@ -124,14 +133,14 @@ let main = {
|
|||
ctaAnchorPosition = getAnchorPosition();
|
||||
});
|
||||
|
||||
let scrollCtaButton = (scrollPosition) => {
|
||||
let scrollCtaButton = scrollPosition => {
|
||||
if (scrollPosition > ctaAnchorPosition) {
|
||||
elStickyButton.classList.add(`hidden`);
|
||||
adjustCtaButton = noopCtaButton;
|
||||
}
|
||||
};
|
||||
|
||||
let initCtaButton = (scrollPosition) => {
|
||||
let initCtaButton = scrollPosition => {
|
||||
if (scrollPosition <= ctaAnchorPosition) {
|
||||
elStickyButton.classList.remove(`hidden`);
|
||||
adjustCtaButton = scrollCtaButton;
|
||||
|
@ -164,21 +173,25 @@ let main = {
|
|||
|
||||
// Extra tracking
|
||||
|
||||
document.getElementById(`donate-header-btn`).addEventListener(`click`, () => {
|
||||
ReactGA.event({
|
||||
category: `donate`,
|
||||
action: `donate button tap`,
|
||||
label: `${document.title} header`
|
||||
document
|
||||
.getElementById(`donate-header-btn`)
|
||||
.addEventListener(`click`, () => {
|
||||
ReactGA.event({
|
||||
category: `donate`,
|
||||
action: `donate button tap`,
|
||||
label: `${document.title} header`
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById(`donate-footer-btn`).addEventListener(`click`, () => {
|
||||
ReactGA.event({
|
||||
category: `donate`,
|
||||
action: `donate button tap`,
|
||||
label: `${document.title} footer`
|
||||
document
|
||||
.getElementById(`donate-footer-btn`)
|
||||
.addEventListener(`click`, () => {
|
||||
ReactGA.event({
|
||||
category: `donate`,
|
||||
action: `donate button tap`,
|
||||
label: `${document.title} footer`
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
bindGAEventTrackers() {
|
||||
|
@ -197,7 +210,9 @@ let main = {
|
|||
});
|
||||
}
|
||||
|
||||
let participateDonateBtn = document.querySelector(`#view-participate .card-cta .btn[href*="donate.mozilla.org"]`);
|
||||
let participateDonateBtn = document.querySelector(
|
||||
`#view-participate .card-cta .btn[href*="donate.mozilla.org"]`
|
||||
);
|
||||
|
||||
if (participateDonateBtn) {
|
||||
participateDonateBtn.addEventListener(`click`, () => {
|
||||
|
@ -213,28 +228,46 @@ let main = {
|
|||
// Embed various React components based on the existence of containers within the current page
|
||||
injectReactComponents() {
|
||||
if (SHOW_MEMBER_NOTICE && document.getElementById(`member-notice`)) {
|
||||
apps.push(new Promise(resolve => {
|
||||
ReactDOM.render(<MemberNotice whenLoaded={() => resolve()}/>, document.getElementById(`member-notice`));
|
||||
}));
|
||||
apps.push(
|
||||
new Promise(resolve => {
|
||||
ReactDOM.render(
|
||||
<MemberNotice whenLoaded={() => resolve()} />,
|
||||
document.getElementById(`member-notice`)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Embed additional instances of the Join Us box that don't need an API exposed (eg: Homepage)
|
||||
if (document.querySelectorAll(`.join-us:not(#join-us)`)) {
|
||||
var elements = Array.from(document.querySelectorAll(`.join-us:not(#join-us)`));
|
||||
var elements = Array.from(
|
||||
document.querySelectorAll(`.join-us:not(#join-us)`)
|
||||
);
|
||||
|
||||
if (elements.length) {
|
||||
elements.forEach(element => {
|
||||
var props = element.dataset;
|
||||
|
||||
apps.push(new Promise(resolve => {
|
||||
ReactDOM.render(<JoinUs {...props} isHidden={false} whenLoaded={() => resolve()} />, element);
|
||||
}));
|
||||
apps.push(
|
||||
new Promise(resolve => {
|
||||
ReactDOM.render(
|
||||
<JoinUs
|
||||
{...props}
|
||||
isHidden={false}
|
||||
whenLoaded={() => resolve()}
|
||||
/>,
|
||||
element
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// petition elements
|
||||
var petitionElements = Array.from(document.querySelectorAll(`.sign-petition`));
|
||||
var petitionElements = Array.from(
|
||||
document.querySelectorAll(`.sign-petition`)
|
||||
);
|
||||
var subscribed = false;
|
||||
|
||||
if (window.location.search.indexOf(`subscribed=1`) !== -1) {
|
||||
|
@ -244,41 +277,73 @@ let main = {
|
|||
petitionElements.forEach(element => {
|
||||
var props = element.dataset;
|
||||
|
||||
props.apiUrl = `${networkSiteURL}/api/campaign/petitions/${props.petitionId}/`;
|
||||
props.apiUrl = `${networkSiteURL}/api/campaign/petitions/${
|
||||
props.petitionId
|
||||
}/`;
|
||||
|
||||
apps.push(new Promise(resolve => {
|
||||
ReactDOM.render(<Petition {...props} isHidden={false} subscribed={subscribed} whenLoaded={() => resolve()}/>, element);
|
||||
}));
|
||||
apps.push(
|
||||
new Promise(resolve => {
|
||||
ReactDOM.render(
|
||||
<Petition
|
||||
{...props}
|
||||
isHidden={false}
|
||||
subscribed={subscribed}
|
||||
whenLoaded={() => resolve()}
|
||||
/>,
|
||||
element
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (document.getElementById(`people`)) {
|
||||
apps.push(new Promise(resolve => {
|
||||
ReactDOM.render(<People env={env} whenLoaded={() => resolve()} />, document.getElementById(`people`));
|
||||
}));
|
||||
apps.push(
|
||||
new Promise(resolve => {
|
||||
ReactDOM.render(
|
||||
<People env={env} whenLoaded={() => resolve()} />,
|
||||
document.getElementById(`people`)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Multipage nav used in landing pages
|
||||
if (document.querySelector(`#multipage-nav`)) {
|
||||
let links = [];
|
||||
|
||||
links = [].map.call(document.querySelectorAll(`#multipage-nav a`), (child) => {
|
||||
return {
|
||||
label: child.textContent.trim(),
|
||||
href: child.getAttribute(`href`),
|
||||
isActive: !!child.getAttribute(`class`).match(/active/)
|
||||
};
|
||||
});
|
||||
links = [].map.call(
|
||||
document.querySelectorAll(`#multipage-nav a`),
|
||||
child => {
|
||||
return {
|
||||
label: child.textContent.trim(),
|
||||
href: child.getAttribute(`href`),
|
||||
isActive: !!child.getAttribute(`class`).match(/active/)
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
apps.push(new Promise(resolve => {
|
||||
ReactDOM.render(<MultipageNavMobile links={links} whenLoaded={() => resolve()} />, document.querySelector(`#multipage-nav-mobile .container .row .col-12`));
|
||||
}));
|
||||
apps.push(
|
||||
new Promise(resolve => {
|
||||
ReactDOM.render(
|
||||
<MultipageNavMobile links={links} whenLoaded={() => resolve()} />,
|
||||
document.querySelector(
|
||||
`#multipage-nav-mobile .container .row .col-12`
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// News
|
||||
if (document.querySelector(`#news`)) {
|
||||
apps.push(new Promise(resolve => {
|
||||
ReactDOM.render(<News env={env} whenLoaded={() => resolve()} />, document.querySelector(`#news`));
|
||||
}));
|
||||
apps.push(
|
||||
new Promise(resolve => {
|
||||
ReactDOM.render(
|
||||
<News env={env} whenLoaded={() => resolve()} />,
|
||||
document.querySelector(`#news`)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Fellowships single filter fellow list
|
||||
|
@ -287,17 +352,20 @@ let main = {
|
|||
);
|
||||
|
||||
singleFilterFellowList.forEach(target => {
|
||||
apps.push(new Promise(resolve => {
|
||||
ReactDOM.render(
|
||||
<SingleFilterFellowList
|
||||
env={env}
|
||||
filterType={target.dataset.filterType}
|
||||
filterOptions={target.dataset.filterOptions.split(`,`)}
|
||||
selectedOption={target.dataset.selectedOption}
|
||||
whenLoaded={() => resolve()}
|
||||
/>, target
|
||||
);
|
||||
}));
|
||||
apps.push(
|
||||
new Promise(resolve => {
|
||||
ReactDOM.render(
|
||||
<SingleFilterFellowList
|
||||
env={env}
|
||||
filterType={target.dataset.filterType}
|
||||
filterOptions={target.dataset.filterOptions.split(`,`)}
|
||||
selectedOption={target.dataset.selectedOption}
|
||||
whenLoaded={() => resolve()}
|
||||
/>,
|
||||
target
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Pulse project lists
|
||||
|
@ -306,21 +374,24 @@ let main = {
|
|||
);
|
||||
|
||||
pulseProjectList.forEach(target => {
|
||||
apps.push(new Promise(resolve => {
|
||||
ReactDOM.render(
|
||||
<PulseProjectList
|
||||
env={ env }
|
||||
featured={ target.dataset.featured === `True` }
|
||||
help={ target.dataset.help }
|
||||
issues={ target.dataset.issues }
|
||||
max={ parseInt(target.dataset.max, 10) }
|
||||
query={ target.dataset.query || `` }
|
||||
reverseChronological={ target.dataset.reversed === `True` }
|
||||
whenLoaded={() => resolve()}
|
||||
directLink={ target.dataset.directLink === `True` }
|
||||
/>, target
|
||||
);
|
||||
}));
|
||||
apps.push(
|
||||
new Promise(resolve => {
|
||||
ReactDOM.render(
|
||||
<PulseProjectList
|
||||
env={env}
|
||||
featured={target.dataset.featured === `True`}
|
||||
help={target.dataset.help}
|
||||
issues={target.dataset.issues}
|
||||
max={parseInt(target.dataset.max, 10)}
|
||||
query={target.dataset.query || ``}
|
||||
reverseChronological={target.dataset.reversed === `True`}
|
||||
whenLoaded={() => resolve()}
|
||||
directLink={target.dataset.directLink === `True`}
|
||||
/>,
|
||||
target
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
|
|
|
@ -1 +1 @@
|
|||
import 'whatwg-fetch';
|
||||
import "whatwg-fetch";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import ReactGA from 'react-ga';
|
||||
import ReactGA from "react-ga";
|
||||
|
||||
let primaryNav = {
|
||||
init: function() {
|
||||
|
@ -58,7 +58,7 @@ let primaryNav = {
|
|||
trackMenuState(openMenu);
|
||||
}
|
||||
|
||||
document.addEventListener(`keyup`, (e) => {
|
||||
document.addEventListener(`keyup`, e => {
|
||||
if (e.keyCode === 27) {
|
||||
menuOpen = false;
|
||||
setMenuState(menuOpen);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import DNT from './dnt.js';
|
||||
import ReactGA from 'react-ga';
|
||||
import DNT from "./dnt.js";
|
||||
import ReactGA from "react-ga";
|
||||
|
||||
// A no-operation object with the same API surface
|
||||
// as ReactGA, for when tracking is not appreciated:
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
let path = require(`path`);
|
||||
let frontendPath = path.resolve(__dirname, `network-api`,`networkapi`,`frontend`,`_js`);
|
||||
let frontendPath = path.resolve(
|
||||
__dirname,
|
||||
`network-api`,
|
||||
`networkapi`,
|
||||
`frontend`,
|
||||
`_js`
|
||||
);
|
||||
|
||||
let rules = [
|
||||
{
|
||||
|
@ -15,7 +21,6 @@ let rules = [
|
|||
}
|
||||
];
|
||||
|
||||
|
||||
let main = {
|
||||
entry: `./source/js/main.js`,
|
||||
output: {
|
||||
|
|
Загрузка…
Ссылка в новой задаче