Feat: Donation integration (#151)

* Clean up MapModal removal

* Add DonationModal and logic

* Remove unused challenge files

* Add email field to donation

* Fix up UI and ajustv copy
This commit is contained in:
Stuart Taylor
2018-06-07 23:13:33 +01:00
committed by Mrugesh Mohapatra
parent cc6067f065
commit f4e33e1ee6
22 changed files with 741 additions and 570 deletions

View File

@ -141,7 +141,8 @@ exports.modifyWebpackConfig = ({ config, stage }) => {
{
HOME_PATH: JSON.stringify(
process.env.HOME_PATH || 'http://localhost:3000'
)
),
STRIPE_PUBLIC_KEY: JSON.stringify(process.env.STRIPE_PULIC_KEY || '')
}
]);
config.plugin('RemoveServiceWorkerPlugin', RmServiceWorkerPlugin, [

View File

@ -15,6 +15,7 @@
"browser-cookies": "^1.2.0",
"chai": "^4.1.2",
"copy-webpack-plugin": "^4.5.1",
"date-fns": "^1.29.0",
"debug": "^3.1.0",
"dotenv": "^5.0.1",
"enzyme": "^3.3.0",
@ -48,6 +49,7 @@
"react-reflex": "^2.2.1",
"react-router-redux": "^5.0.0-alpha.9",
"react-spinkit": "^3.0.0",
"react-stripe-elements": "^2.0.0",
"react-test-renderer": "^16.3.1",
"redux": "^3.7.2",
"redux-actions": "^2.3.0",
@ -57,7 +59,7 @@
"rxjs": "^5.5.7",
"store": "^2.0.12",
"uglifyjs-webpack-plugin": "^1.2.4",
"validator": "^9.4.1",
"validator": "^10.3.0",
"webpack-remove-serviceworker-plugin": "^1.0.0"
},
"keywords": [

View File

@ -1,174 +0,0 @@
{
"name": "APIs and Microservices Projects",
"order": 4,
"time": "150 hours",
"helpRoom": "HelpBackend",
"challenges": [
{
"id": "bd7158d8c443edefaeb5bdef",
"title": "Timestamp Microservice",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://curse-arrow.glitch.me/' target='_blank'>https://curse-arrow.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-timestamp/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-timestamp/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "I can pass a string as a parameter, and it will check to see whether that string contains either a unix timestamp or a natural language date (example: January 1, 2016).",
"testString": ""
},
{
"text": "If it does, it returns both the Unix timestamp and the natural language form of that date.",
"testString": ""
},
{
"text": "If it does not contain a date or Unix timestamp, it returns null for those properties.",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "January 1, 2016",
"translations": {
"es": {
"title": "Microservicio de Marca Temporal"
}
}
},
{
"id": "bd7158d8c443edefaeb5bdff",
"title": "Request Header Parser Microservice",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://dandelion-roar.glitch.me/' target='_blank'>https://dandelion-roar.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-headerparser/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-headerparser/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "I can get the IP address, language and operating system for my browser.",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "January 1, 2016",
"translations": {
"es": {
"title": "Microservicio para analizar el encabezado de una petición"
}
}
},
{
"id": "bd7158d8c443edefaeb5bd0e",
"title": "URL Shortener Microservice",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://thread-paper.glitch.me/' target='_blank'>https://thread-paper.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-urlshortener/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-urlshortener/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "I can pass a URL as a parameter and I will receive a shortened URL in the JSON response.",
"testString": ""
},
{
"text": "If I pass an invalid URL that doesn't follow the valid http://www.example.com format, the JSON response will contain an error instead.",
"testString": ""
},
{
"text": "When I visit that shortened URL, it will redirect me to my original link.",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "January 1, 2016",
"translations": {
"es": {
"title": "Microservicio para acortar URLs"
}
}
},
{
"id": "5a8b073d06fa14fcfde687aa",
"title": "Exercise Tracker",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://fuschia-custard.glitch.me/' target='_blank'>https://fuschia-custard.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-exercisetracker/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-exercisetracker/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "I can create a user by posting form data username to /api/exercise/new-user and returned will be an object with username and <code>_id</code>.",
"testString": ""
},
{
"text": "I can get an array of all users by getting api/exercise/users with the same info as when creating a user.",
"testString": ""
},
{
"text": "I can add an exercise to any user by posting form data userId(_id), description, duration, and optionally date to /api/exercise/add. If no date supplied it will use current date. App will return the user object with the exercise fields added.",
"testString": ""
},
{
"text": "I can retrieve a full exercise log of any user by getting /api/exercise/log with a parameter of userId(_id). App will return the user object with added array log and count (total exercise count).",
"testString": ""
},
{
"text": "I can retrieve part of the log of any user by also passing along optional parameters of from & to or limit. (Date format yyyy-mm-dd, limit = int)",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "February 17, 2017",
"translations": {
"es": {
"title": "Capa de abstracción para buscar imágenes"
}
}
},
{
"id": "bd7158d8c443edefaeb5bd0f",
"title": "File Metadata Microservice",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://purple-paladin.glitch.me/' target='_blank'>https://purple-paladin.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-filemetadata/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-filemetadata/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "I can submit a FormData object that includes a file upload.",
"testString": ""
},
{
"text": "When I submit something, I will receive the file size in bytes within the JSON response.",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "January 1, 2016",
"translations": {
"es": {
"title": "Microservicio de metadatos de archivos"
}
}
}
]
}

View File

@ -1,298 +0,0 @@
{
"name": "Information Security and Quality Assurance Projects",
"order": 4,
"time": "150 hours",
"helpRoom": "HelpBackend",
"challenges": [
{
"id": "587d8249367417b2b2512c41",
"title": "Metric-Imperial Converter",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://hard-twilight.glitch.me/' target='_blank'>https://hard-twilight.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-metricimpconverter/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-metricimpconverter/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "I will prevent the client from trying to guess(sniff) the MIME type.",
"testString": ""
},
{
"text": "I will prevent cross-site scripting (XSS) attacks.",
"testString": ""
},
{
"text": "I can GET /api/convert with a single parameter containing an accepted number and unit and have it converted. (Hint: Split the input by looking for the index of the first character which will mark the start of the unit)",
"testString": ""
},
{
"text": "I can convert 'gal' to 'L' and vice versa. (1 gal to 3.78541 L)",
"testString": ""
},
{
"text": "I can convert 'lbs' to 'kg' and vice versa. (1 lbs to 0.453592 kg)",
"testString": ""
},
{
"text": "I can convert 'mi' to 'km' and vice versa. (1 mi to 1.60934 km)",
"testString": ""
},
{
"text": "If my unit of measurement is invalid, returned will be 'invalid unit'.",
"testString": ""
},
{
"text": "If my number is invalid, returned with will 'invalid number'.",
"testString": ""
},
{
"text": "If both are invalid, return will be 'invalid number and unit'.",
"testString": ""
},
{
"text": "I can use fractions, decimals or both in my parameter(ie. 5, 1/2, 2.5/6), but if nothing is provided it will default to 1.",
"testString": ""
},
{
"text": "My return will consist of the initNum, initUnit, returnNum, returnUnit, and string spelling out units in format '{initNum} {initial_Units} converts to {returnNum} {return_Units}' with the result rounded to 5 decimals in the string.",
"testString": ""
},
{
"text": "All 16 unit tests are complete and passing.",
"testString": ""
},
{
"text": "All 5 functional tests are complete and passing.",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "January 15, 2017",
"translations": {}
},
{
"id": "587d8249367417b2b2512c42",
"title": "Issue Tracker",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://protective-garage.glitch.me/' target='_blank'>https://protective-garage.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-issuetracker/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-issuetracker/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "Prevent cross site scripting (XSS) attacks.",
"testString": ""
},
{
"text": "I can POST /api/issues/{projectname} with form data containing required issue_title, issue_text, created_by, and optional assigned_to and status_text.",
"testString": ""
},
{
"text": "The object saved (and returned) will include all of those fields (blank for optional no input) and also include created_on(date/time), updated_on(date/time), open(boolean, true for open, false for closed), and _id.",
"testString": ""
},
{
"text": "I can PUT /api/issues/{projectname} with a id and any fields in the object with a value to object said object. Returned will be 'successfully updated' or 'could not update '+id. This should always update updated_on. If no fields are sent return 'no updated field sent'.",
"testString": ""
},
{
"text": "I can DELETE /api/issues/{projectname} with a id to completely delete an issue. If no _id is sent return 'id error', success: 'deleted '+id, failed: 'could not delete '+id.",
"testString": ""
},
{
"text": "I can GET /api/issues/{projectname} for an array of all issues on that specific project with all the information for each issue as was returned when posted.",
"testString": ""
},
{
"text": "I can filter my get request by also passing along any field and value in the query(ie. /api/issues/{project}?open=false). I can pass along as many fields/values as I want.",
"testString": ""
},
{
"text": "All 11 functional tests are complete and passing.",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "January 15, 2017",
"translations": {}
},
{
"id": "587d824a367417b2b2512c43",
"title": "Personal Library",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://spark-cathedral.glitch.me/' target='_blank'>https://spark-cathedral.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-library/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-library/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "Nothing from my website will be cached in my client.",
"testString": ""
},
{
"text": "The headers will say that the site is powered by 'PHP 4.2.0' even though it isn't (as a security measure).",
"testString": ""
},
{
"text": "I can post a title to /api/books to add a book and returned will be the object with the title and a unique _id.",
"testString": ""
},
{
"text": "I can get /api/books to retrieve an array of all books containing title, _id, and commentcount.",
"testString": ""
},
{
"text": "I can get /api/books/{id} to retrieve a single object of a book containing _title, _id, & an array of comments (empty array if no comments present).",
"testString": ""
},
{
"text": "I can post a comment to /api/books/{id} to add a comment to a book and returned will be the books object similar to get /api/books/{id} including the new comment.",
"testString": ""
},
{
"text": "I can delete /api/books/{_id} to delete a book from the collection. Returned will be 'delete successful' if successful.",
"testString": ""
},
{
"text": "If I try to request a book that doesn't exist I will be returned 'no book exists'.",
"testString": ""
},
{
"text": "I can send a delete request to /api/books to delete all books in the database. Returned will be 'complete delete successful' if successful.",
"testString": ""
},
{
"text": "All 6 functional tests required are complete and passing.",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "January 15, 2017",
"translations": {}
},
{
"id": "587d824a367417b2b2512c44",
"title": "Stock Price Checker",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://giant-chronometer.glitch.me/' target='_blank'>https://giant-chronometer.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-stockchecker/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-stockchecker/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "Set the content security policies to only allow loading of scripts and css from your server.",
"testString": ""
},
{
"text": "I can GET /api/stock-prices with form data containing a Nasdaq stock ticker and receive back an object stockData.",
"testString": ""
},
{
"text": "In stockData, I can see the stock(string, the ticker), price(decimal in string format), and likes(int).",
"testString": ""
},
{
"text": "I can also pass along field like as true(boolean) to have my like added to the stock(s). Only 1 like per ip should be accepted.",
"testString": ""
},
{
"text": "If I pass along 2 stocks, the return object will be an array with both stock's info. Instead of likes, it will display rel_likes(the difference between the likes on both stocks) on both.",
"testString": ""
},
{
"text": "A good way to receive current price is the following external API(replacing 'GOOG' with your stock): https://finance.google.com/finance/info?q=NASDAQ%3aGOOG",
"testString": ""
},
{
"text": "All 5 functional tests are complete and passing.",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "January 15, 2017",
"translations": {}
},
{
"id": "587d824a367417b2b2512c45",
"title": "Anonymous Message Board",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://horn-celery.glitch.me/' target='_blank'>https://horn-celery.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-messageboard/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-messageboard/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "Only allow your site to be loading in an iFrame on your own pages.",
"testString": ""
},
{
"text": "Do not allow DNS prefetching.",
"testString": ""
},
{
"text": "Only allow your site to send the referrer for your own pages.",
"testString": ""
},
{
"text": "I can POST a thread to a specific message board by passing form data text and deletepassword_ to /api/threads/{board}.(Recommend res.redirect to board page /b/{board}) Saved will be at least _id, text, createdon_(date&time), bumpedon_(date&time, starts same as created_on), reported(boolean), deletepassword_, & replies(array).",
"testString": ""
},
{
"text": "I can POST a reply to a thread on a specific board by passing form data text, deletepassword_, & threadid_ to /api/replies/{board} and it will also update the bumped_on date to the comments date.(Recommend res.redirect to thread page /b/{board}/{thread_id}) In the thread's replies array will be saved _id, text, createdon_, deletepassword_, & reported.",
"testString": ""
},
{
"text": "I can GET an array of the most recent 10 bumped threads on the board with only the most recent 3 replies each from /api/threads/{board}. The reported and deletepasswords_ fields will not be sent to the client.",
"testString": ""
},
{
"text": "I can GET an entire thread with all its replies from /api/replies/{board}?thread_id={thread_id}. Also hiding the same fields the client should be see.",
"testString": ""
},
{
"text": "I can delete a thread completely if I send a DELETE request to /api/threads/{board} and pass along the threadid_ & deletepassword_. (Text response will be 'incorrect password' or 'success')",
"testString": ""
},
{
"text": "I can delete a post(just changing the text to '[deleted]' instead of removing completely like a thread) if I send a DELETE request to /api/replies/{board} and pass along the threadid_, replyid_, & deletepassword_. (Text response will be 'incorrect password' or 'success')",
"testString": ""
},
{
"text": "I can report a thread and change its reported value to true by sending a PUT request to /api/threads/{board} and pass along the threadid_. (Text response will be 'success')",
"testString": ""
},
{
"text": "I can report a reply and change its reported value to true by sending a PUT request to /api/replies/{board} and pass along the threadid_ & replyid_. (Text response will be 'success')",
"testString": ""
},
{
"text": "Complete functional tests that wholly test routes and pass.",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "January 15, 2017",
"translations": {}
}
]
}

View File

@ -0,0 +1,131 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Modal } from 'react-bootstrap';
import { StripeProvider, Elements } from 'react-stripe-elements';
import ga from '../../analytics';
import DonateForm from './components/DonateForm';
import DonateCompletion from './components/DonateCompletion';
import {
userSelector,
closeDonationModal,
isDonationModalOpenSelector
} from '../../redux/app';
import './donation.css';
import poweredByStripe from '../../../static/img/powered_by_stripe.svg';
const mapStateToProps = createSelector(
userSelector,
isDonationModalOpenSelector,
({ email }, show) => ({ email, show })
);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
closeDonationModal
},
dispatch
);
const propTypes = {
closeDonationModal: PropTypes.func.isRequired,
email: PropTypes.string,
show: PropTypes.bool
};
const stripeKey = 'pk_test_R38mgr7bpfGqZy5vyMAJieka';
class DonationModal extends PureComponent {
constructor(...props) {
super(...props);
this.state = {
stripe: null
};
this.renderCompletion = this.renderCompletion.bind(this);
this.renderMaybe = this.renderMaybe.bind(this);
}
componentDidMount() {
if (window.Stripe) {
/* eslint-disable react/no-did-mount-set-state */
this.setState(state => ({
...state,
stripe: window.Stripe(stripeKey)
}));
} else {
document.querySelector('#stripe-js').addEventListener('load', () => {
// Create Stripe instance once Stripe.js loads
console.info('stripe has loaded');
this.setState(state => ({
...state,
stripe: window.Stripe(stripeKey)
}));
});
}
}
renderCompletion(props) {
return (
<DonateCompletion close={this.props.closeDonationModal} {...props} />
);
}
renderMaybe() {
const { closeDonationModal } = this.props;
const handleClick = e => {
e.preventDefault();
return closeDonationModal();
};
return (
<div className='maybe-later-container'>
<a onClick={handleClick}>Maybe later</a>
</div>
);
}
render() {
const { email, show } = this.props;
if (show) {
ga.modalview('/donation-modal');
}
return (
<StripeProvider stripe={this.state.stripe}>
<Elements>
<Modal bsSize='lg' className='donation-modal' show={show}>
<Modal.Header className='fcc-modal'>
<Modal.Title className='text-center'>
Support Our NonProfit
</Modal.Title>
</Modal.Header>
<Modal.Body>
<DonateForm
email={email}
maybeButton={this.renderMaybe}
renderCompletion={this.renderCompletion}
/>
</Modal.Body>
<Modal.Footer>
<img src={poweredByStripe} />
</Modal.Footer>
</Modal>
</Elements>
</StripeProvider>
);
}
}
DonationModal.displayName = 'DonationModal';
DonationModal.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps,
(state, dispatch, own) => ({ ...state, ...dispatch, ...own }),
{
pure: false
}
)(DonationModal);

View File

@ -0,0 +1,58 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Button } from 'react-bootstrap';
import StripCardForm from './StripeCardForm';
const propTypes = {
amount: PropTypes.number.isRequired,
handleSubmit: PropTypes.func.isRequired
};
class CardForm extends PureComponent {
constructor(...props) {
super(...props);
this.state = {
isFormValid: false
};
this.getValidationState = this.getValidationState.bind(this);
this.submit = this.submit.bind(this);
}
submit(e) {
e.preventDefault();
this.props.handleSubmit();
}
getValidationState(isFormValid) {
this.setState(state => ({
...state,
isFormValid
}));
}
render() {
const { amount } = this.props;
const { isFormValid } = this.state;
return (
<form className='donation-form' onSubmit={this.submit}>
<StripCardForm getValidationState={this.getValidationState} />
<Button
block={true}
bsSize='lg'
bsStyle='primary'
disabled={!isFormValid}
type='submit'
>
{`Confirm Monthly Donation of $${amount}`}
</Button>
</form>
);
}
}
CardForm.displayName = 'CardForm';
CardForm.propTypes = propTypes;
export default CardForm;

View File

@ -0,0 +1,55 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Alert, Button } from 'react-bootstrap';
import Spinner from 'react-spinkit';
const propTypes = {
close: PropTypes.func.isRequired,
error: PropTypes.string,
processing: PropTypes.bool,
reset: PropTypes.func.isRequired,
success: PropTypes.bool
};
function DonateCompletion({ close, processing, reset, success, error = null }) {
/* eslint-disable no-nested-ternary */
const style = processing ? 'info' : success ? 'success' : 'danger';
const heading = processing
? 'We are processing your donation'
: success
? 'Donation successful. Thank you for supporting the freeCodeCamp ' +
'community!'
: 'Something went wrong with your donation';
return (
<Alert bsStyle={style}>
<h4>{heading}</h4>
<div id='donation-completion-body'>
{processing && (
<Spinner
className='user-state-spinner'
color='#006400'
fadeIn='none'
name='line-scale'
/>
)}
{error && error}
</div>
<p className='donation-completion-buttons'>
{error && (
<Fragment>
<Button bsStyle='primary' onClick={reset}>
Try again
</Button>
<span />
</Fragment>
)}
{!processing && <Button onClick={close}>Close</Button>}
</p>
</Alert>
);
}
DonateCompletion.displayName = 'DonateCompletion';
DonateCompletion.propTypes = propTypes;
export default DonateCompletion;

View File

@ -0,0 +1,207 @@
import React, { PureComponent, Fragment } from 'react';
import PropTypes from 'prop-types';
import isEmail from 'validator/lib/isEmail';
import CardForm from './CardForm';
import { injectStripe } from 'react-stripe-elements';
import { postJSON$ } from '../../../templates/Challenges/utils/ajax-stream';
const propTypes = {
email: PropTypes.string,
maybeButton: PropTypes.func.isRequired,
renderCompletion: PropTypes.func.isRequired,
stripe: PropTypes.shape({
createToken: PropTypes.func.isRequired
})
};
const initialSate = {
donationAmount: 500,
donationState: {
processing: false,
success: false,
error: ''
}
};
class DonateForm extends PureComponent {
constructor(...args) {
super(...args);
const [props] = args;
this.state = {
...initialSate,
email: props.email
};
this.buttonAmounts = [500, 1000, 3500, 5000, 25000];
this.handleAmountClick = this.handleAmountClick.bind(this);
this.handleEmailChange = this.handleEmailChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.isActive = this.isActive.bind(this);
this.renderAmountButtons = this.renderAmountButtons.bind(this);
this.postDonation = this.postDonation.bind(this);
this.resetDonation = this.resetDonation.bind(this);
}
handleAmountClick(e) {
e.preventDefault();
const donationAmount = parseInt(e.target.id, 10);
return this.setState(state => ({
...state,
donationAmount
}));
}
handleEmailChange(e) {
const newValue = e.target.value;
return this.setState(state => ({
...state,
email: newValue
}));
}
handleSubmit() {
const { email } = this.state;
if (!email || !isEmail(email)) {
return this.setState(state => ({
...state,
donationState: {
...state.donationState,
error:
'We need a valid email address to send your donation tax reciept to'
}
}));
}
return this.props.stripe.createToken({ email }).then(({ error, token }) => {
if (error) {
return this.setState(state => ({
...state,
donationState: {
...state.donationState,
error:
'Something went wrong processing your donation. Your card' +
' has not been charged.'
}
}));
}
return this.postDonation(token);
});
}
isActive(amount) {
return this.state.donationAmount === amount;
}
postDonation(token) {
const { donationAmount: amount } = this.state;
this.setState(state => ({
...state,
donationState: {
...state.donationState,
processing: true
}
}));
return postJSON$('/external/donate/charge-stripe', {
token,
amount
}).subscribe(
res =>
this.setState(state => ({
...state,
donationState: {
...state.donationState,
processing: false,
success: true,
error: res.error
}
})),
err =>
this.setState(state => ({
...state,
donationState: {
...state.donationState,
processing: false,
success: false,
error: err.error
}
}))
);
}
renderAmountButtons() {
return this.buttonAmounts.map(amount => (
<li key={'amount-' + amount}>
<a
className={`amount-value ${this.isActive(amount) ? 'active' : ''}`}
href=''
id={amount}
onClick={this.handleAmountClick}
tabIndex='-1'
>{`$${amount / 100}`}</a>
</li>
));
}
renderDonateForm() {
return (
<Fragment>
<p>
freeCodeCamp is completely free. But it costs our nonprofit a lot of
money to run it. Help us pay for servers. Set up a tax-deductible
monthly donation you can afford.
</p>
<div id='donate-amount-panel'>
<ul>{this.renderAmountButtons()}</ul>
</div>
{this.renderEmailInput()}
<CardForm
amount={this.state.donationAmount / 100}
handleSubmit={this.handleSubmit}
/>
{this.props.maybeButton()}
</Fragment>
);
}
renderEmailInput() {
const { email } = this.state;
return (
<div className='donation-email-container'>
<label>
Email where we should send your donation tax reciept:
<input
onChange={this.handleEmailChange}
placeholder='email@example.com'
required={true}
type='email'
value={email}
/>
</label>
</div>
);
}
resetDonation() {
return this.setState(() => initialSate);
}
render() {
const { donationState: { processing, success, error } } = this.state;
const { renderCompletion } = this.props;
if (processing || success || error) {
return renderCompletion({
processing,
success,
error,
reset: this.resetDonation
});
}
return this.renderDonateForm();
}
}
DonateForm.displayName = 'DonateForm';
DonateForm.propTypes = propTypes;
export default injectStripe(DonateForm);

View File

@ -0,0 +1,96 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import {
CardNumberElement,
CardExpiryElement,
CardCVCElement
} from 'react-stripe-elements';
import { ControlLabel, FormGroup } from 'react-bootstrap';
const propTypes = {
getValidationState: PropTypes.func.isRequired
};
const style = {
base: {
color: '#006400'
}
};
class StripCardForm extends PureComponent {
constructor(...props) {
super(...props);
this.state = {
validation: {
cardNumber: {
complete: false,
error: null
},
cardExpiry: {
complete: false,
error: null
},
cardCvc: {
complete: false,
error: null
}
}
};
this.handleInputChange = this.handleInputChange.bind(this);
this.isValidInput = this.isValidInput.bind(this);
}
componentDidMount() {
this.props.getValidationState(this.isValidInput());
}
handleInputChange(event) {
const { elementType, error, complete } = event;
return this.setState(
state => ({
...state,
validation: {
...state.validation,
[elementType]: {
error,
complete
}
}
}),
() => this.props.getValidationState(this.isValidInput())
);
}
isValidInput() {
const { validation } = this.state;
return Object.keys(validation)
.map(key => validation[key])
.every(({ complete, error }) => complete && !error);
}
render() {
return (
<div className='donation-elements'>
<FormGroup>
<ControlLabel>Card Number:</ControlLabel>
<CardNumberElement onChange={this.handleInputChange} style={style} />
</FormGroup>
<FormGroup>
<ControlLabel>Card Expiry:</ControlLabel>
<CardExpiryElement onChange={this.handleInputChange} style={style} />
</FormGroup>
<FormGroup>
<ControlLabel>Card CVC (3-digit security number):</ControlLabel>
<CardCVCElement onChange={this.handleInputChange} style={style} />
</FormGroup>
</div>
);
}
}
StripCardForm.displayName = 'StripCardForm';
StripCardForm.propTypes = propTypes;
export default StripCardForm;

View File

@ -0,0 +1,106 @@
.donation-modal {
font-size: 1.2rem;
}
.donation-modal p {
width: 90%;
margin-left: auto;
margin-right: auto;
}
.donation-modal .modal-title {
font-size: 1.2rem;
}
.donation-form {
display: flex;
flex-direction: column;
width:80%;
justify-content: center;
margin: 0 auto;
height: 300px;
}
#donate-amount-panel ul {
list-style: none;
display: flex;
margin-left: 0px;
}
#donate-amount-panel li {
flex: 0.20;
display: flex;
justify-content: center;
align-items: center;
}
#donate-amount-panel a {
width: calc(100% - 20px);
height: 75px;
display: flex;
justify-content: center;
align-items: center;
border: 2px solid #006400;
}
#donate-amount-panel a:hover, #donate-amount-panel a:focus, #donate-amount-panel a.active {
text-decoration: none;
color: white;
background-color: #006400;
font-weight: bold;
}
.donation-elements {
display: flex;
height: 100%;
flex-direction: column;
justify-content: space-evenly;
}
#donation-completion-body {
display: flex;
justify-content: center;
align-items: center;
}
.donation-completion-buttons {
display: flex;
justify-content: flex-end;
}
.donation-completion-buttons button {
margin: 0 10px;
}
.donation-email-container {
width: 80%;
margin: 0 auto;
}
.donation-email-container label {
display: flex;
flex-direction: column;
}
.donation-email-container input {
color: #006400;
font-weight: normal;
border: none;
}
.maybe-later-container {
width: 80%;
margin: 50px auto 0;
display: flex;
justify-content: center;
}
.maybe-later-container a {
font-size: 18px;
}
.maybe-later-container a:hover {
text-decoration: none;
font-size: 18px;
cursor: pointer;
}

View File

@ -0,0 +1 @@
export { default } from './Donation';

View File

@ -7,7 +7,7 @@ import Link from 'gatsby-link';
import ga from '../../../analytics';
import { makeExpandedBlockSelector, toggleBlock } from '../redux';
import { toggleMapModal, userSelector } from '../../../redux/app';
import { userSelector } from '../../../redux/app';
import Caret from '../../icons/Caret';
/* eslint-disable max-len */
import GreenPass from '../../../templates/Challenges/components/icons/GreenPass';
@ -27,7 +27,7 @@ const mapStateToProps = (state, ownProps) => {
};
const mapDispatchToProps = dispatch =>
bindActionCreators({ toggleBlock, toggleMapModal }, dispatch);
bindActionCreators({ toggleBlock }, dispatch);
const propTypes = {
blockDashedName: PropTypes.string,
@ -41,8 +41,7 @@ const propTypes = {
})
}),
isExpanded: PropTypes.bool,
toggleBlock: PropTypes.func.isRequired,
toggleMapModal: PropTypes.func.isRequired
toggleBlock: PropTypes.func.isRequired
};
const mapIconStyle = { height: '15px', marginRight: '10px', width: '15px' };
@ -67,7 +66,6 @@ export class Block extends PureComponent {
handleChallengeClick(slug) {
return () => {
this.props.toggleMapModal();
return ga.event({
category: 'Map Challenge Click',
action: slug

View File

@ -1,63 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Modal } from 'react-bootstrap';
import { ChallengeNode } from '../../redux/propTypes';
import Map from '../Map';
import { toggleMapModal, isMapModalOpenSelector } from '../../redux/app';
import Spacer from '../util/Spacer';
import './map-modal.css';
const mapStateToProps = createSelector(isMapModalOpenSelector, show => ({
show
}));
const mapDispatchToProps = dispatch =>
bindActionCreators({ toggleMapModal }, dispatch);
const propTypes = {
introNodes: PropTypes.arrayOf(
PropTypes.shape({
fields: PropTypes.shape({ slug: PropTypes.string.isRequired }),
frontmatter: PropTypes.shape({
title: PropTypes.string.isRequired,
block: PropTypes.string.isRequired
})
})
),
nodes: PropTypes.arrayOf(ChallengeNode),
show: PropTypes.bool,
toggleMapModal: PropTypes.func.isRequired
};
function MapModal({ introNodes, nodes, show, toggleMapModal }) {
return (
<Modal
bsSize='lg'
className='map-modal'
onHide={toggleMapModal}
show={show}
>
<Modal.Header className='map-modal-header fcc-modal' closeButton={true}>
<Modal.Title className='text-center'>
A Curriculum to Learn to Code
</Modal.Title>
</Modal.Header>
<Modal.Body>
<Spacer />
<Map introNodes={introNodes} nodes={nodes} />
<Spacer />
</Modal.Body>
</Modal>
);
}
MapModal.displayName = 'MapModal';
MapModal.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(MapModal);

View File

@ -1,3 +0,0 @@
.map-modal .map-ui {
overflow-y: scroll;
}

View File

@ -55,6 +55,7 @@ function HTML(props) {
key={'body'}
/>
{postBodyComponents}
<script async={true} id='stripe-js' src='https://js.stripe.com/v3/' />
</body>
</html>
);

View File

@ -7,6 +7,7 @@ import Helmet from 'react-helmet';
import ga from '../analytics';
import Header from '../components/Header';
import DonationModal from '../components/Donation';
import { fetchUser } from '../redux/app';
import 'prismjs/themes/prism.css';
@ -93,6 +94,7 @@ class Layout extends PureComponent {
<div className='app-wrapper'>
<main>{children()}</main>
</div>
<DonationModal />
</Fragment>
);
}

View File

@ -4,7 +4,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import Link from 'gatsby-link';
import Helmet from 'react-helmet';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import {
@ -12,7 +11,6 @@ import {
AllChallengeNode,
AllMarkdownRemark
} from '../redux/propTypes';
import { toggleMapModal } from '../redux/app';
import Spacer from '../components/util/Spacer';
import Map from '../components/Map';
@ -20,16 +18,12 @@ import './index.css';
const mapStateToProps = () => ({});
const mapDispatchToProps = dispatch =>
bindActionCreators({ toggleMapModal }, dispatch);
const propTypes = {
data: PropTypes.shape({
challengeNode: ChallengeNode,
allChallengeNode: AllChallengeNode,
allMarkdownRemark: AllMarkdownRemark
}),
toggleMapModal: PropTypes.func.isRequired
})
};
const IndexPage = ({
@ -70,7 +64,7 @@ const IndexPage = ({
IndexPage.displayName = 'IndexPage';
IndexPage.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(IndexPage);
export default connect(mapStateToProps)(IndexPage);
export const query = graphql`
query FirstChallenge {

View File

@ -18,17 +18,19 @@ export const types = createTypes(
'noUserFound',
'hardGoTo',
'updateUserSignedIn',
'toggleMapModal'
'openDonationModal',
'closeDonationModal'
],
ns
);
const initialState = {
appUsername: '',
completionCount: 0,
showLoading: true,
isSignedIn: false,
user: {},
showMapModal: false
showDonationModal: false
};
export const fetchUser = createAction(types.fetchUser);
@ -38,19 +40,40 @@ export const noUserFound = createAction(types.noUserFound);
export const hardGoTo = createAction(types.hardGoTo);
export const toggleMapModal = createAction(types.toggleMapModal);
export const openDonationModal = createAction(types.openDonationModal);
export const closeDonationModal = createAction(types.closeDonationModal);
export const updateUserSignedIn = createAction(types.updateUserSignedIn);
export const isMapModalOpenSelector = state => state[ns].showMapModal;
export const completionCountSelector = state => state[ns].completionCount;
export const isDonationModalOpenSelector = state => state[ns].showDonationModal;
export const isSignedInSelector = state => state[ns].isSignedIn;
export const userSelector = state => state[ns].user || {};
export const userStateLoadingSelector = state => state[ns].showLoading;
export const completedChallengesSelector = state =>
state[ns].user.completedChallenges || [];
userSelector(state).completedChallenges || [];
export const currentChallengeIdSelector = state =>
userSelector(state).currentChallengeId || '';
export const shouldShowDonationSelector = state => {
const completedChallenges = completedChallengesSelector(state);
const completionCount = completionCountSelector(state);
const currentCompletedLength = completedChallenges.length;
// the user has not completed 9 challenges in total yet
if (currentCompletedLength < 9) {
return false;
}
// this will mean we are on the 10th submission in total for the user
if (completedChallenges.length === 9) {
return true;
}
// this will mean we are on the 3rd submission for this browser session
if (completionCount === 2) {
return true;
}
return false;
};
export const reducer = handleActions(
{
[types.fetchUserComplete]: (
@ -65,9 +88,13 @@ export const reducer = handleActions(
}),
[types.fetchUserError]: state => ({ ...state, showLoading: false }),
[types.noUserFound]: state => ({ ...state, showLoading: false }),
[types.toggleMapModal]: state => ({
[types.closeDonationModal]: state => ({
...state,
showMapModal: !state.showMapModal
showDonationModal: false
}),
[types.openDonationModal]: state => ({
...state,
showDonationModal: true
}),
[types.updateUserSignedIn]: (state, { payload }) => ({
...state,
@ -75,6 +102,7 @@ export const reducer = handleActions(
}),
[challenge.submitComplete]: (state, { payload: { id } }) => ({
...state,
completionCount: state.completionCount + 1,
user: {
...state.user,
completedChallenges: uniqBy(

View File

@ -4,8 +4,6 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { Button } from 'react-bootstrap';
// import { submittingSelector } from './redux';
import { toggleMapModal } from '../../../redux/app';
import { openModal } from '../redux';
const mapStateToProps = () => ({});
@ -13,16 +11,14 @@ const mapStateToProps = () => ({});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
openHelpModal: () => openModal('help'),
toggleMapModal
openHelpModal: () => openModal('help')
},
dispatch
);
const propTypes = {
guideUrl: PropTypes.string,
openHelpModal: PropTypes.func.isRequired,
toggleMapModal: PropTypes.func.isRequired
openHelpModal: PropTypes.func.isRequired
};
export class ToolPanel extends PureComponent {

View File

@ -22,7 +22,12 @@ import {
challengeTestsSelector,
closeModal
} from './';
import { userSelector, isSignedInSelector } from '../../../redux/app';
import {
userSelector,
isSignedInSelector,
openDonationModal,
shouldShowDonationSelector
} from '../../../redux/app';
import { postJSON$ } from '../utils/ajax-stream';
import { challengeTypes, submitTypes } from '../../../../utils/challengeTypes';
@ -117,14 +122,20 @@ const submitters = {
'project.backEnd': submitProject
};
function shouldShowDonate(state) {
return shouldShowDonationSelector(state) ? of(openDonationModal()) : empty();
}
export default function completionEpic(action$, { getState }) {
return action$.pipe(
ofType(types.submitChallenge),
switchMap(({ type }) => {
const state = getState();
const meta = challengeMetaSelector(state);
const { isDonating } = userSelector(state);
const { nextChallengePath, introPath, challengeType } = meta;
const next = of(push(introPath ? introPath : nextChallengePath));
const showDonate = isDonating ? empty() : shouldShowDonate(state);
const closeChallengeModal = of(closeModal('completion'));
let submitter = () => of({ type: 'no-user-signed-in' });
if (
@ -143,6 +154,7 @@ export default function completionEpic(action$, { getState }) {
return submitter(type, state).pipe(
concat(next),
concat(closeChallengeModal),
concat(showDonate),
filter(Boolean)
);
})

View File

@ -0,0 +1,11 @@
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="119px" height="26px">
<path fill-rule="evenodd" opacity="0.349" fill="rgb(66, 71, 112)"
d="M113.000,26.000 L6.000,26.000 C2.686,26.000 -0.000,23.314 -0.000,20.000 L-0.000,6.000 C-0.000,2.686 2.686,-0.000 6.000,-0.000 L113.000,-0.000 C116.314,-0.000 119.000,2.686 119.000,6.000 L119.000,20.000 C119.000,23.314 116.314,26.000 113.000,26.000 ZM118.000,6.000 C118.000,3.239 115.761,1.000 113.000,1.000 L6.000,1.000 C3.239,1.000 1.000,3.239 1.000,6.000 L1.000,20.000 C1.000,22.761 3.239,25.000 6.000,25.000 L113.000,25.000 C115.761,25.000 118.000,22.761 118.000,20.000 L118.000,6.000 Z"/>
<path fill-rule="evenodd" opacity="0.502" fill="rgb(66, 71, 112)"
d="M60.700,18.437 L59.395,18.437 L60.405,15.943 L58.395,10.871 L59.774,10.871 L61.037,14.323 L62.310,10.871 L63.689,10.871 L60.700,18.437 ZM55.690,16.259 C55.238,16.259 54.774,16.091 54.354,15.764 L54.354,16.133 L53.007,16.133 L53.007,8.566 L54.354,8.566 L54.354,11.229 C54.774,10.913 55.238,10.745 55.690,10.745 C57.100,10.745 58.068,11.881 58.068,13.502 C58.068,15.122 57.100,16.259 55.690,16.259 ZM55.406,11.902 C55.038,11.902 54.669,12.060 54.354,12.376 L54.354,14.628 C54.669,14.943 55.038,15.101 55.406,15.101 C56.164,15.101 56.690,14.449 56.690,13.502 C56.690,12.555 56.164,11.902 55.406,11.902 ZM47.554,15.764 C47.144,16.091 46.681,16.259 46.218,16.259 C44.818,16.259 43.840,15.122 43.840,13.502 C43.840,11.881 44.818,10.745 46.218,10.745 C46.681,10.745 47.144,10.913 47.554,11.229 L47.554,8.566 L48.912,8.566 L48.912,16.133 L47.554,16.133 L47.554,15.764 ZM47.554,12.376 C47.249,12.060 46.881,11.902 46.513,11.902 C45.744,11.902 45.218,12.555 45.218,13.502 C45.218,14.449 45.744,15.101 46.513,15.101 C46.881,15.101 47.249,14.943 47.554,14.628 L47.554,12.376 ZM39.535,13.870 C39.619,14.670 40.251,15.217 41.134,15.217 C41.619,15.217 42.155,15.038 42.702,14.722 L42.702,15.849 C42.103,16.122 41.503,16.259 40.913,16.259 C39.324,16.259 38.209,15.101 38.209,13.460 C38.209,11.871 39.303,10.745 40.808,10.745 C42.187,10.745 43.123,11.829 43.123,13.375 C43.123,13.523 43.123,13.691 43.102,13.870 L39.535,13.870 ZM40.756,11.786 C40.103,11.786 39.598,12.271 39.535,12.997 L41.829,12.997 C41.787,12.281 41.356,11.786 40.756,11.786 ZM35.988,12.618 L35.988,16.133 L34.641,16.133 L34.641,10.871 L35.988,10.871 L35.988,11.397 C36.367,10.976 36.830,10.745 37.282,10.745 C37.430,10.745 37.577,10.755 37.724,10.797 L37.724,11.997 C37.577,11.955 37.409,11.934 37.251,11.934 C36.809,11.934 36.335,12.176 35.988,12.618 ZM29.979,13.870 C30.063,14.670 30.694,15.217 31.578,15.217 C32.062,15.217 32.599,15.038 33.146,14.722 L33.146,15.849 C32.546,16.122 31.946,16.259 31.357,16.259 C29.768,16.259 28.653,15.101 28.653,13.460 C28.653,11.871 29.747,10.745 31.252,10.745 C32.630,10.745 33.567,11.829 33.567,13.375 C33.567,13.523 33.567,13.691 33.546,13.870 L29.979,13.870 ZM31.199,11.786 C30.547,11.786 30.042,12.271 29.979,12.997 L32.273,12.997 C32.231,12.281 31.799,11.786 31.199,11.786 ZM25.274,16.133 L24.200,12.555 L23.137,16.133 L21.927,16.133 L20.117,10.871 L21.464,10.871 L22.527,14.449 L23.590,10.871 L24.810,10.871 L25.873,14.449 L26.936,10.871 L28.283,10.871 L26.484,16.133 L25.274,16.133 ZM17.043,16.259 C15.454,16.259 14.328,15.112 14.328,13.502 C14.328,11.881 15.454,10.745 17.043,10.745 C18.632,10.745 19.748,11.881 19.748,13.502 C19.748,15.112 18.632,16.259 17.043,16.259 ZM17.043,11.871 C16.254,11.871 15.707,12.534 15.707,13.502 C15.707,14.470 16.254,15.133 17.043,15.133 C17.822,15.133 18.369,14.470 18.369,13.502 C18.369,12.534 17.822,11.871 17.043,11.871 ZM11.128,13.533 L9.918,13.533 L9.918,16.133 L8.571,16.133 L8.571,8.892 L11.128,8.892 C12.602,8.892 13.654,9.850 13.654,11.218 C13.654,12.586 12.602,13.533 11.128,13.533 ZM10.939,9.987 L9.918,9.987 L9.918,12.439 L10.939,12.439 C11.718,12.439 12.265,11.944 12.265,11.218 C12.265,10.482 11.718,9.987 10.939,9.987 Z"/>
<path fill-rule="evenodd" opacity="0.502" fill="rgb(66, 71, 112)"
d="M111.116,14.051 L105.557,14.051 C105.684,15.382 106.659,15.774 107.766,15.774 C108.893,15.774 109.781,15.536 110.555,15.146 L110.555,17.433 C109.784,17.861 108.765,18.169 107.408,18.169 C104.642,18.169 102.704,16.437 102.704,13.013 C102.704,10.121 104.348,7.825 107.049,7.825 C109.746,7.825 111.154,10.120 111.154,13.028 C111.154,13.303 111.129,13.898 111.116,14.051 ZM107.031,10.140 C106.321,10.140 105.532,10.676 105.532,11.955 L108.468,11.955 C108.468,10.677 107.728,10.140 107.031,10.140 ZM98.108,18.169 C97.114,18.169 96.507,17.750 96.099,17.451 L96.093,20.664 L93.254,21.268 L93.253,8.014 L95.753,8.014 L95.901,8.715 C96.293,8.349 97.012,7.825 98.125,7.825 C100.119,7.825 101.997,9.621 101.997,12.927 C101.997,16.535 100.139,18.169 98.108,18.169 ZM97.446,10.340 C96.795,10.340 96.386,10.578 96.090,10.903 L96.107,15.122 C96.383,15.421 96.780,15.661 97.446,15.661 C98.496,15.661 99.200,14.518 99.200,12.989 C99.200,11.504 98.485,10.340 97.446,10.340 ZM89.149,8.014 L91.999,8.014 L91.999,17.966 L89.149,17.966 L89.149,8.014 ZM89.149,4.836 L91.999,4.230 L91.999,6.543 L89.149,7.149 L89.149,4.836 ZM86.110,11.219 L86.110,17.966 L83.272,17.966 L83.272,8.014 L85.727,8.014 L85.905,8.853 C86.570,7.631 87.897,7.879 88.275,8.015 L88.275,10.625 C87.914,10.508 86.781,10.338 86.110,11.219 ZM80.024,14.475 C80.024,16.148 81.816,15.627 82.179,15.482 L82.179,17.793 C81.801,18.001 81.115,18.169 80.187,18.169 C78.502,18.169 77.237,16.928 77.237,15.247 L77.250,6.138 L80.022,5.548 L80.024,8.014 L82.180,8.014 L82.180,10.435 L80.024,10.435 L80.024,14.475 ZM76.485,14.959 C76.485,17.003 74.858,18.169 72.497,18.169 C71.518,18.169 70.448,17.979 69.392,17.525 L69.392,14.814 C70.345,15.332 71.559,15.721 72.500,15.721 C73.133,15.721 73.589,15.551 73.589,15.026 C73.589,13.671 69.273,14.181 69.273,11.038 C69.273,9.028 70.808,7.825 73.111,7.825 C74.052,7.825 74.992,7.969 75.933,8.344 L75.933,11.019 C75.069,10.552 73.972,10.288 73.109,10.288 C72.514,10.288 72.144,10.460 72.144,10.903 C72.144,12.181 76.485,11.573 76.485,14.959 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -2736,6 +2736,10 @@ data-urls@^1.0.0:
whatwg-mimetype "^2.0.0"
whatwg-url "^6.4.0"
date-fns@^1.29.0:
version "1.29.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6"
date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
@ -8820,6 +8824,12 @@ react-spinkit@^3.0.0:
object-assign "^4.1.0"
prop-types "^15.5.8"
react-stripe-elements@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/react-stripe-elements/-/react-stripe-elements-2.0.0.tgz#5bfd881ad13e18a3255eff49f63ababf3861d216"
dependencies:
prop-types "^15.5.10"
react-test-renderer@^16.0.0-0, react-test-renderer@^16.3.1:
version "16.3.1"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.3.1.tgz#d9257936d8535bd40f57f3d5a84e7b0452fb17f2"
@ -11105,9 +11115,9 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
validator@^9.4.1:
version "9.4.1"
resolved "https://registry.yarnpkg.com/validator/-/validator-9.4.1.tgz#abf466d398b561cd243050112c6ff1de6cc12663"
validator@^10.3.0:
version "10.3.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-10.3.0.tgz#157a8c0981858cff381f59aabcdb8f83b57317cc"
value-equal@^0.4.0:
version "0.4.0"