feat(client): add donate faq (#44380)

* feat: add donate faq

* Apply suggestions from code review

Co-authored-by: Kristofer Koishigawa <scissorsneedfoodtoo@gmail.com>

* clean up

* Apply suggestions from code review

Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>

* Update client/src/components/Donation/donation-text-components.tsx

Co-authored-by: Kristofer Koishigawa <scissorsneedfoodtoo@gmail.com>
Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
pull/44543/head
Ahmad Abdolsaheb 2021-12-22 01:42:10 +03:00 committed by GitHub
parent e878b3ee57
commit 449c37fb33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 269 additions and 32 deletions

View File

@ -13,7 +13,10 @@
"copyright-url": "https://www.freecodecamp.org/news/copyright-policy/"
},
"donate": {
"other-ways-url": "https://www.freecodecamp.org/news/how-to-donate-to-free-code-camp"
"other-ways-url": "https://www.freecodecamp.org/news/how-to-donate-to-free-code-camp",
"download-irs-url": "https://s3.amazonaws.com/freecodecamp/Free+Code+Camp+Inc+IRS+Determination+Letter.pdf",
"download-990-url": "https://freecodecamp.s3.amazonaws.com/freeCodeCamp+2019+f990.pdf",
"one-time-url": "https://paypal.me/freecodecamp"
},
"nav": {
"forum": "https://forum.freecodecamp.org/",

View File

@ -293,7 +293,6 @@
},
"help-translate": "We are still translating the following certifications.",
"help-translate-link": "Help us translate.",
"season-greetings": "Season's Greetings to you and your family.",
"season-greetings-fcc": "Season's Greetings from the freeCodeCamp community 🎉",
"if-getting-value": "If you're getting a lot out of freeCodeCamp, now is a great time to donate to support our nonprofit's mission.",
"project-preview-title": "Here's a preview of what you will build"
@ -347,7 +346,42 @@
"try-again": "Please try again.",
"card-number": "Your Card Number:",
"expiration": "Expiration Date:",
"only-you": "Only you can see this message. Congratulations on earning this certification. Its no easy task. Running freeCodeCamp isnt easy either. Nor is it cheap. Help us help you and many other people around the world. Make a tax-deductible supporting donation to our nonprofit today."
"faq": "Frequently asked questions",
"only-you": "Only you can see this message. Congratulations on earning this certification. It's no easy task. Running freeCodeCamp isn't easy either. Nor is it cheap. Help us help you and many other people around the world. Make a tax-deductible supporting donation to our nonprofit today.",
"get-help": "How can I get help with my donations?",
"how-transparent": "How transparent is freeCodeCamp.org?",
"very-transparent": "Very. We have a Platinum transparency rating from GuideStar.org.",
"download-irs": "You can <0>download our IRS Determination Letter here</0>.",
"download-990": "You can <0>download our most recent 990 (annual tax report) here</0>.",
"how-efficient": "How efficient is freeCodeCamp?",
"fcc-budget": "freeCodeCamp's budget is much smaller than most comparable nonprofits. We haven't brought in professional fundraisers. Instead, Quincy does everything himself.",
"help-millions": "However, on a budget of only a few hundred thousand dollars per year, we have been able to help millions of people.",
"how-one-time":"How can I make a one-time donation?",
"one-time": "If you'd prefer to make one-time donations, you can support freeCodeCamp's mission whenever you have cash to spare. You can use <0>this link to donate whatever amount feels right through PayPal</0>.",
"wire-transfer": "You can also send money to freeCodeCamp directly through a wire transfer. If you need our wire details, email Quincy at quincy@freecodecamp.org",
"does-crypto": "Does freeCodeCamp accept donations in Bitcoin or other cryptocurrencies?",
"yes-crypto": "Yes, and we would welcome your cryptocurrency donations. Here are our wallet details:",
"can-check": "Can I mail a physical check?",
"yes-check": "Yes, we would welcome a check. You can mail it to us at:",
"how-matching-gift": "How can I set up matching gifts from my employer, or payroll deductions?",
"employers-vary": "This varies from employer to employer, and our nonprofit is already listed in many of the big donation-matching databases.",
"some-volunteer": "Some people are able to volunteer for freeCodeCamp and their employer matches by donating a fixed amount per hour they volunteer. Other employers will match any donations the donors make up to a certain amount",
"help-matching-gift": "If you need help with this, please email Quincy directly: quincy@freecodecamp.org",
"how-endowment": "How can I set up an Endowment Gift to freeCodeCamp.org?",
"endowment": "This would be a huge help. Since this is a more manual process, Quincy can help walk you through it personally. Please email him directly at quincy@freecodecamp.org.",
"how-legacy": "How can I set up a Legacy gift to freeCodeCamp.org?",
"we-honored": "We would be honored to put such a gift to good use helping people around the world learn to code. Depending on where you live, this may also be tax exempt.",
"legacy-gift-message": "I give, devise, and bequeath [the sum of _____ USD (or other currency) OR _____ percent of the rest and residue of my estate] to freeCodeCamp.org (Free Code Camp, Inc. tax identification number 82-0779546), a charitable corporation organized under the laws of the State of Delaware, United States, currently located at 3905 Hedgcoxe Rd, PO Box 250352, Plano, Texas, 75025 United States, to be used for its general charitable purposes at its discretion.",
"thank-wikimedia": "We would like to thank the Wikimedia Foundation for providing this formal language for us to use.",
"legacy-gift-questions": "If you have any questions about this process, please email Quincy at quincy@freecodecamp.org.",
"how-stock": "How can I donate stock to freeCodeCamp.org?",
"welcome-stock": "We would welcome your stock donations. Please email Quincy directly and he can help you with this, and share our nonprofit's brokerage account details: quincy@freecodecamp.org.",
"how-receipt": "Can I get a donation receipt so that I can deduct my donation from my taxes?",
"just-forward": "Absolutely. Just forward the receipt from your transaction to donors@freecodecamp.org, tell us you'd like a receipt and any special instructions you may have, and we'll reply with a receipt for you.",
"how-update": "I set up a monthly donation, but I need to update or pause the monthly recurrence. How can I do this?",
"take-care-of-this": "Just forward one of your monthly donation receipts to donors@freecodecamp.org and tell us what you'd like us to do. We'll take care of this for you and send you confirmation.",
"anything-else": "Is there anything else I can learn about donating to freeCodeCamp.org?",
"other-support": "If there is some other way you'd like to support our nonprofit and its mission that isn't listed here, or if you have any questions at all, please email Quincy at quincy@freecodecamp.org."
},
"report": {
"sign-in": "You need to be signed in to report a user",

View File

@ -1,5 +1,36 @@
import React from 'react';
import React, { useState } from 'react';
import { useTranslation, Trans } from 'react-i18next';
import Caret from '../../assets/icons/caret';
const WALLETS = (
<>
<code>Bitcoin: 3B4QShnJawtzBd1FFzHPpVCpxBpVbcbPRg</code>
<br />
<code>Ethereum: 0x1ee753faa97BE3C4b9b1dE775dB44c9Bfac0EC91</code>
<br />
<code>Litecoin: MVDLr18spSjd9nDyKG2Y94BFmgzbjXgfCD</code>
<br />
<code>Bitcoin Cash: qqw8lhpnu635za8f5c22ghynl6yz5zelp52t25lmnm</code>
<br />
<code>USD Coin: 0xad4f0c8363fE733DdbfEDBdAf6600E2b6dF2900d</code>
<br />
<code>DAI: 0xad4f0c8363fE733DdbfEDBdAf6600E2b6dF2900d</code>
<br />
<code>Dash: XyRp67PQVBRaZu2LJU6Ndc5kp3AaRaHnXt</code>
</>
);
const POBOX = (
<>
<code>Free Code Camp, Inc.</code>
<br />
<code>3905 Hedgcoxe Rd</code>
<br />
<code>PO Box 250352</code>
<br />
<code>Plano, TX 75025</code>
</>
);
export const DonationSupportText = (): JSX.Element => {
const { t } = useTranslation();
@ -53,3 +84,130 @@ export const DonationOptionsAlertText = (): JSX.Element => {
</p>
);
};
const FaqItem = (
title: string,
text: JSX.Element,
key: number
): JSX.Element => {
const [isExpanded, setExpanded] = useState(false);
return (
<div className={`faq-item ${isExpanded ? 'open' : ''}`} key={key}>
<button className='map-title' onClick={() => setExpanded(!isExpanded)}>
<Caret />
<h4>
<b>{title}</b>
</h4>
</button>
{isExpanded && (
<>
<div className='map-challenges-ul'>{text}</div>
</>
)}
</div>
);
};
export const DonationFaqText = (): JSX.Element => {
const { t } = useTranslation();
const faqItems = [
{ Q: t('donate.get-help'), A: <p>{t('donate.forward-receipt')}</p> },
{
Q: t('donate.how-transparent'),
A: (
<>
<p>{t('donate.very-transparent')}</p>
<p>
<Trans i18nKey='donate.download-irs'>
<a href={t('links:donate.download-irs-url')}>placeholder</a>
</Trans>
</p>
<p>
<Trans i18nKey='donate.download-990'>
<a href={t('links:donate.download-990-url')}>placeholder</a>
</Trans>
</p>
</>
)
},
{
Q: t('donate.how-efficient'),
A: (
<>
<p>{t('donate.fcc-budget')}</p>
<p>{t('donate.help-millions')}</p>
</>
)
},
{
Q: t('donate.how-one-time'),
A: (
<>
<p>
<Trans i18nKey='donate.one-time'>
<a href={t('links:donate.one-time-url')}>placeholder</a>
</Trans>
</p>
<p>{t('donate.wire-transfer')}</p>
</>
)
},
{
Q: t('donate.does-crypto'),
A: (
<>
<p>{t('donate.yes-crypto')}</p>
<p>{WALLETS}</p>
</>
)
},
{
Q: t('donate.can-check'),
A: (
<>
<p>{t('donate.yes-check')}</p>
<p>{POBOX}</p>
</>
)
},
{
Q: t('donate.how-matching-gift'),
A: (
<>
<p>{t('donate.employers-vary')}</p>
<p>{t('donate.some-volunteer')}</p>
<p>{t('donate.help-matching-gift')}</p>
</>
)
},
{ Q: t('donate.how-endowment'), A: <p>{t('donate.endowment')}</p> },
{
Q: t('donate.how-legacy'),
A: (
<>
<p>{t('donate.we-honored')}</p>
<blockquote>
<p>{t('donate.legacy-gift-message')}</p>
</blockquote>
<p>{t('donate.thank-wikimedia')}</p>
<p>{t('donate.legacy-gift-questions')}</p>
</>
)
},
{ Q: t('donate.how-stock'), A: <p>{t('donate.welcome-stock')}</p> },
{ Q: t('donate.how-update'), A: <p>{t('donate.forward-receipt')}</p> },
{
Q: t('donate.anything-else'),
A: (
<>
<p>{t('donate.other-support')}</p>
</>
)
}
];
return (
<>{faqItems.map((item, iterator) => FaqItem(item.Q, item.A, iterator))}</>
);
};

View File

@ -527,3 +527,13 @@ a.patreon-button:hover {
.separator:not(:empty)::after {
margin-left: 0.25em;
}
.faq-item div {
width: 100%;
padding: 10px 15px;
}
.faq-item blockquote {
font-size: 1em;
border-left-color: var(--quaternary-background);
}

View File

@ -1,7 +1,6 @@
import { Alert } from '@freecodecamp/react-bootstrap';
import React from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { emailToABVariant } from '../../utils/A-B-tester';
import { randomQuote } from '../../utils/get-words';
import Login from '../Header/components/Login';
import { Link, Spacer, Loader } from '../helpers';
@ -17,7 +16,6 @@ interface IntroProps {
pending?: boolean;
slug?: string;
username?: string;
email?: string;
onAlertClick?: () => void;
}
@ -28,15 +26,9 @@ const Intro = ({
complete,
completedChallengeCount,
slug,
email,
onAlertClick
}: IntroProps): JSX.Element => {
const { t } = useTranslation();
const titleVariation = (email: string | undefined): string => {
if (!email || emailToABVariant(email).isAVariant)
return t('learn.season-greetings');
return t('learn.season-greetings-fcc');
};
if (pending && !complete) {
return (
<>
@ -80,7 +72,7 @@ const Intro = ({
)}
<Alert bsStyle='info' className='annual-donation-alert'>
<p>
<b>{titleVariation(email)}</b>
<b>{t('learn.season-greetings-fcc')}</b>
</p>
<p>{t('learn.if-getting-value')}</p>
<hr />

View File

@ -11,13 +11,20 @@ import { createSelector } from 'reselect';
import DonateForm from '../components/Donation/donate-form';
import {
DonationText,
DonationOptionsAlertText,
DonationSupportText,
DonationOptionsText,
DonationOptionsAlertText
DonationFaqText
} from '../components/Donation/donation-text-components';
import { Spacer, Loader } from '../components/helpers';
import CampersImage from '../components/landing/components/campers-image';
import { signInLoadingSelector, userSelector, executeGA } from '../redux';
import {
signInLoadingSelector,
userSelector,
executeGA,
isAVariantSelector
} from '../redux';
interface ExecuteGaArg {
type: string;
@ -33,15 +40,22 @@ interface DonatePageProps {
executeGA: (arg: ExecuteGaArg) => void;
isDonating?: boolean;
showLoading: boolean;
isAVariant: boolean;
t: TFunction;
}
const mapStateToProps = createSelector(
userSelector,
signInLoadingSelector,
({ isDonating }: { isDonating: boolean }, showLoading: boolean) => ({
isAVariantSelector,
(
{ isDonating }: { isDonating: boolean },
showLoading: boolean,
isAVariant: boolean
) => ({
isDonating,
showLoading
showLoading,
isAVariant
})
);
@ -53,6 +67,7 @@ function DonatePage({
executeGA = () => {},
isDonating = false,
showLoading,
isAVariant,
t
}: DonatePageProps) {
useEffect(() => {
@ -79,6 +94,34 @@ function DonatePage({
});
}
const donationSupport = (
<>
<Row className='donate-support'>
<Col xs={12}>
<hr />
<DonationOptionsText />
<DonationSupportText />
</Col>
</Row>
</>
);
const donationFaq = (
<>
<Spacer size={3} />
<Row className='donate-support' id='FAQ'>
<Col className={'text-center'} xs={12}>
<hr />
<h2>{t('donate.faq')}</h2>
<Spacer />
</Col>
<Col xs={12}>
<DonationFaqText />
</Col>
</Row>
</>
);
return showLoading ? (
<Loader fullScreen={true} />
) : (
@ -112,13 +155,7 @@ function DonatePage({
<DonateForm handleProcessing={handleProcessing} />
</Col>
</Row>
<Row className='donate-support'>
<Col xs={12}>
<hr />
<DonationOptionsText />
<DonationSupportText />
</Col>
</Row>
{isAVariant ? donationSupport : donationFaq}
</Col>
<Col lg={6}>
<CampersImage pageName='donate' />

View File

@ -25,7 +25,6 @@ interface FetchState {
}
interface User {
email: string;
name: string;
username: string;
completedChallengeCount: number;
@ -66,7 +65,6 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
function LearnPage({
isSignedIn,
user,
fetchState: { pending, complete },
user: { name = '', completedChallengeCount = 0 },
executeGA,
@ -99,7 +97,6 @@ function LearnPage({
<Intro
complete={complete}
completedChallengeCount={completedChallengeCount}
email={user.email}
isSignedIn={isSignedIn}
name={name}
onAlertClick={onAlertClick}

View File

@ -3,7 +3,6 @@ import { takeEvery, call, all, select } from 'redux-saga/effects';
import { aBTestConfig } from '../../../config/donation-settings';
import ga from '../analytics';
import {
isSignedInSelector,
emailSelector,
completionCountSelector,
completedChallengesSelector
@ -18,9 +17,8 @@ function* callGaType({ payload: { type, data } }) {
data.category.includes('Donation') &&
aBTestConfig.isTesting
) {
const isSignedIn = yield select(isSignedInSelector);
if (isSignedIn) {
const email = yield select(emailSelector);
const email = yield select(emailSelector);
if (email) {
const completedChallengeTotal = yield select(completedChallengesSelector);
const completedChallengeSession = yield select(completionCountSelector);
const customDimensions = {

View File

@ -12,6 +12,7 @@ import { createDonationSaga } from './donation-saga';
import failedUpdatesEpic from './failed-updates-epic';
import { createFetchUserSaga } from './fetch-user-saga';
import { createGaSaga } from './ga-saga';
import { emailToABVariant } from '../utils/A-B-tester';
import hardGoToEpic from './hard-go-to-epic';
import { createReportUserSaga } from './report-user-saga';
@ -202,6 +203,13 @@ export const stepsToClaimSelector = state => {
};
};
export const emailSelector = state => userSelector(state).email;
export const isAVariantSelector = state => {
const email = emailSelector(state);
// if the user is not signed in and the user info is not available.
// always return A the control variant
if (!email) return true;
return emailToABVariant(email).isAVariant;
};
export const isDonatingSelector = state => userSelector(state).isDonating;
export const isOnlineSelector = state => state[MainApp].isOnline;
export const isServerOnlineSelector = state => state[MainApp].isServerOnline;

View File

@ -94,7 +94,7 @@ const patreonDefaultPledgeAmount = 500;
const aBTestConfig = {
isTesting: true,
type: 'LearnAlertTitle'
type: 'AddDonateFAQ'
};
module.exports = {