diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json index c2dc838ac24..e419c72e16a 100644 --- a/client/i18n/locales/english/translations.json +++ b/client/i18n/locales/english/translations.json @@ -93,7 +93,9 @@ "link-account": "Link Account", "unlink-account": "Unlink Account", "update-card": "Update your card", - "donate-now": "Donate Now" + "donate-now": "Donate Now", + "confirm-amount": "Confirm amount", + "skip-advertisement": "Skip Advertisement" }, "landing": { "big-heading-1": "Learn to code — for free.", @@ -578,6 +580,7 @@ "other-support": "If there is some other way you'd like to support our charity and its mission that isn't listed here, or if you have any questions at all, please email Quincy at quincy@freecodecamp.org.", "bear-progress-alt": "Illustration of an adorable teddy bear with a pleading expression holding an empty money jar.", "bear-completion-alt": "Illustration of an adorable teddy bear holding a large trophy.", + "flying-bear": "Illustration of an adorable teddy bear wearing a graduation cap and flying with a Supporter badge.", "crucial-contribution": "Your contribution will be crucial in creating resources that empower millions of people to learn new skills and support their families.", "support-benefits-title": "Benefits from becoming a Supporter:", "support-benefits-1": "No more donation prompt popups", @@ -598,7 +601,14 @@ "community-achivements-3": "Merged <0>2,753 code contributions into our open source repositories on GitHub", "community-achivements-4": "Translated <0>2,106,203 words to make our curriculum and tutorials more accessible to speakers of many world languages", "as-you-see": "As you can see, we're getting things done. So you can rest assured that we'll put your donations to good use.", - "get-benefits": "Get the benefits and the knowledge that you’re helping our charity change education for the better. Become a Supporter today." + "get-benefits": "Get the benefits and the knowledge that you’re helping our charity change education for the better. Become a supporter today.", + "modal-benefits-title": "Support us", + "help-us-more-certifications": "Help us build more certifications", + "remove-donation-popups": "Remove donation popups", + "help-millions-learn": "Help millions of people learn", + "reach-goals-faster": "Reach your goals faster", + "remove-distractions": "Remove distractions", + "animation-description": "This is a 20 second animated advertisement to encourage campers to become supporters of freeCodeCamp. The animation starts with a teddy bear who becomes a supporter. As a result, distracting pop-ups disappear and the bear gets to complete all of its goals. Then, it graduates and becomes an education super hero helping people around the world." }, "report": { "sign-in": "You need to be signed in to report a user", diff --git a/client/src/assets/images/donation-bear-animation.svg b/client/src/assets/images/donation-bear-animation.svg new file mode 100644 index 00000000000..4d632b80720 --- /dev/null +++ b/client/src/assets/images/donation-bear-animation.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/client/src/assets/images/supporter-bear.svg b/client/src/assets/images/supporter-bear.svg new file mode 100644 index 00000000000..3735c62e87c --- /dev/null +++ b/client/src/assets/images/supporter-bear.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/client/src/components/Donation/donation-modal-body.tsx b/client/src/components/Donation/donation-modal-body.tsx new file mode 100644 index 00000000000..af20a74154c --- /dev/null +++ b/client/src/components/Donation/donation-modal-body.tsx @@ -0,0 +1,306 @@ +import { Modal } from '@freecodecamp/react-bootstrap'; +import { Col, Row } from '@freecodecamp/ui'; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useFeature } from '@growthbook/growthbook-react'; +import BearProgressModal from '../../assets/images/components/bear-progress-modal'; +import BearBlockCompletion from '../../assets/images/components/bear-block-completion-modal'; +import { closeDonationModal } from '../../redux/actions'; +import { Spacer } from '../helpers'; +import { PaymentContext } from '../../../../shared/config/donation-settings'; // +import donationAnimation from '../../assets/images/donation-bear-animation.svg'; +import supporterBear from '../../assets/images/supporter-bear.svg'; +import MultiTierDonationForm from './multi-tier-donation-form'; +import { ModalBenefitList } from './donation-text-components'; + +type RecentlyClaimedBlock = null | { block: string; superBlock: string }; + +type DonationModalBodyProps = { + activeDonors?: number; + closeDonationModal: typeof closeDonationModal; + recentlyClaimedBlock: RecentlyClaimedBlock; + executeGA: (arg: { event: string; action: string }) => void; +}; + +const Illustration = ({ + recentlyClaimedBlock, + showAnimation +}: { + recentlyClaimedBlock: RecentlyClaimedBlock; + showAnimation?: boolean; +}) => { + const { t } = useTranslation(); + if (showAnimation) { + return ( + {t('donate.flying-bear')} + ); + } else + return recentlyClaimedBlock ? ( + + ) : ( + + ); +}; + +function ModalHeader({ + recentlyClaimedBlock, + showHeaderAndFooter, + donationAttempted, + showForm, + donationAnimationFlag +}: { + recentlyClaimedBlock: RecentlyClaimedBlock; + showHeaderAndFooter: boolean; + donationAttempted: boolean; + showForm: boolean; + donationAnimationFlag: boolean; +}) { + const { t } = useTranslation(); + if (!showHeaderAndFooter || donationAttempted) { + return null; + } else if (!donationAnimationFlag) { + return ( + + + {recentlyClaimedBlock !== null && ( + + {t('donate.nicely-done', { + block: t( + `intro:${recentlyClaimedBlock.superBlock}.blocks.${recentlyClaimedBlock.block}.title` + ) + })} + + )} +

{t('donate.help-us-develop')}

+ +
+ ); + } else if (!showForm) { + return ( + + +

{t('donate.modal-benefits-title')}

+ +
+ ); + } else { + return null; + } +} + +function CloseButtonRow({ + donationAttempted, + closeDonationModal +}: { + donationAttempted: boolean; + closeDonationModal: () => void; +}) { + const { t } = useTranslation(); + return ( + + + + + + ); +} + +const Benefits = ({ + setShowForm, + executeGA +}: { + setShowForm: (arg: boolean) => void; + executeGA: (arg: { event: string; action: string }) => void; +}) => { + const { t } = useTranslation(); + + const handleBecomeSupporterClick = () => { + executeGA({ + event: 'donation_related', + action: `Modal Become Supporter Click` + }); + setShowForm(true); + }; + return ( + + + + + + + + + ); +}; + +const AnimationContainer = ({ + setIsAnimationVisible +}: { + setIsAnimationVisible: (arg: boolean) => void; +}) => { + const { t } = useTranslation(); + return ( + <> +

+ {t('donate.animation-description')}{' '} +

+ + + + ); +}; + +const BecomeASupporterConfirmation = ({ + donationAnimationFlag, + recentlyClaimedBlock, + showHeaderAndFooter, + closeDonationModal, + donationAttempted, + showForm, + setShowHeaderAndFooter, + handleProcessing, + setShowForm, + executeGA +}: { + donationAnimationFlag: boolean; + recentlyClaimedBlock: RecentlyClaimedBlock; + showHeaderAndFooter: boolean; + closeDonationModal: () => void; + donationAttempted: boolean; + showForm: boolean; + setShowHeaderAndFooter: (arg: boolean) => void; + handleProcessing: () => void; + setShowForm: (arg: boolean) => void; + executeGA: (arg: { event: string; action: string }) => void; +}) => { + return ( +
+
+ +
+ + + {showForm || !donationAnimationFlag ? ( + + ) : ( + + )} + {(showHeaderAndFooter || donationAttempted) && ( + + )} +
+ ); +}; + +function DonationModalBody({ + closeDonationModal, + recentlyClaimedBlock, + executeGA +}: DonationModalBodyProps): JSX.Element { + const [donationAttempted, setDonationAttempted] = useState(false); + const [showHeaderAndFooter, setShowHeaderAndFooter] = useState(true); + const donationAnimationFlag = useFeature('donation-animation').on; + const [isAnimationVisible, setIsAnimationVisible] = useState(true); + const [showForm, setShowForm] = useState(false); + const handleProcessing = () => { + setDonationAttempted(true); + }; + + useEffect(() => { + const timer = setTimeout(() => { + setIsAnimationVisible(false); + }, 20000); // 20000 milliseconds = 20 seconds + + // Clear the timer if the component unmounts + return () => clearTimeout(timer); + }, []); + + return ( + +
+ {donationAnimationFlag && isAnimationVisible ? ( + + ) : ( + + )} +
+
+ ); +} + +DonationModalBody.displayName = 'DonationModalBody'; + +export default DonationModalBody; diff --git a/client/src/components/Donation/donation-modal.tsx b/client/src/components/Donation/donation-modal.tsx index 80a080a5920..a0c5f90c28d 100644 --- a/client/src/components/Donation/donation-modal.tsx +++ b/client/src/components/Donation/donation-modal.tsx @@ -1,15 +1,11 @@ import { Modal } from '@freecodecamp/react-bootstrap'; -import { Col, Row } from '@freecodecamp/ui'; import { WindowLocation } from '@reach/router'; -import React, { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import { goToAnchor } from 'react-scrollable-anchor'; import { bindActionCreators, Dispatch, AnyAction } from 'redux'; import { createSelector } from 'reselect'; -import BearProgressModal from '../../assets/images/components/bear-progress-modal'; -import BearBlockCompletion from '../../assets/images/components/bear-block-completion-modal'; import { closeDonationModal, executeGA } from '../../redux/actions'; import { isDonationModalOpenSelector, @@ -17,9 +13,7 @@ import { } from '../../redux/selectors'; import { isLocationSuperBlock } from '../../utils/path-parsers'; import { playTone } from '../../utils/tone'; -import { Spacer } from '../helpers'; -import { PaymentContext } from '../../../../shared/config/donation-settings'; // -import MultiTierDonationForm from './multi-tier-donation-form'; +import DonationModalBody from './donation-modal-body'; type RecentlyClaimedBlock = null | { block: string; superBlock: string }; @@ -50,65 +44,6 @@ type DonateModalProps = { show: boolean; }; -const Illustration = ({ - recentlyClaimedBlock -}: { - recentlyClaimedBlock: RecentlyClaimedBlock; -}) => { - return recentlyClaimedBlock ? ( - - ) : ( - - ); -}; - -function ModalHeader({ - recentlyClaimedBlock -}: { - recentlyClaimedBlock: RecentlyClaimedBlock; -}) { - const { t } = useTranslation(); - return ( - - - {recentlyClaimedBlock !== null && ( - - {t('donate.nicely-done', { - block: t( - `intro:${recentlyClaimedBlock.superBlock}.blocks.${recentlyClaimedBlock.block}.title` - ) - })} - - )} -

{t('donate.help-us-develop')}

- -
- ); -} - -function CloseButtonRow({ - donationAttempted, - closeDonationModal -}: { - donationAttempted: boolean; - closeDonationModal: () => void; -}) { - const { t } = useTranslation(); - return ( - - - - - - ); -} - function DonateModal({ show, closeDonationModal, @@ -116,13 +51,6 @@ function DonateModal({ location, recentlyClaimedBlock }: DonateModalProps): JSX.Element { - const [donationAttempted, setDonationAttempted] = useState(false); - const [showHeaderAndFooter, setShowHeaderAndFooter] = useState(true); - - const handleProcessing = () => { - setDonationAttempted(true); - }; - useEffect(() => { if (show) { void playTone('donation'); @@ -149,28 +77,13 @@ function DonateModal({ className='donation-modal' onExited={handleModalHide} show={show} + data-cy='donation-modal' > - -
- -
- {showHeaderAndFooter && !donationAttempted && ( - - )} - - - {(showHeaderAndFooter || donationAttempted) && ( - - )} -
+ ); } diff --git a/client/src/components/Donation/donation-text-components.tsx b/client/src/components/Donation/donation-text-components.tsx index 9d7b54327e1..7a9de9d6bd4 100644 --- a/client/src/components/Donation/donation-text-components.tsx +++ b/client/src/components/Donation/donation-text-components.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { useTranslation, Trans } from 'react-i18next'; import Caret from '../../assets/icons/caret'; import { Spacer } from '../helpers'; +import GreenPass from '../../assets/icons/green-pass'; const POBOX = ( <> @@ -225,7 +226,7 @@ export const SupportBenefitsText = ({ ); }; -const BenefitsList = (): JSX.Element => { +export const BenefitsList = (): JSX.Element => { const { t } = useTranslation(); return (
    @@ -317,3 +318,23 @@ export const GetSupporterBenefitsText = ({ ); }; + +export const ModalBenefitList = () => { + const { t } = useTranslation(); + return ( +
      +
    • + + {t('donate.help-us-more-certifications')} +
    • +
    • + + {t('donate.remove-donation-popups')} +
    • +
    • + + {t('donate.help-millions-learn')} +
    • +
    + ); +}; diff --git a/client/src/components/Donation/donation.css b/client/src/components/Donation/donation.css index f48a7423864..46c509700f5 100644 --- a/client/src/components/Donation/donation.css +++ b/client/src/components/Donation/donation.css @@ -273,6 +273,19 @@ li.disabled > a { .donation-modal p { font-size: 0.9rem; } + +.donation-modal ul { + padding: 0; +} + +.donation-modal li { + list-style-type: none; + margin-bottom: 4px; +} +.donation-modal li svg { + margin: 0 5px; +} + .donation-label-modal { font-weight: normal; text-align: center; @@ -736,3 +749,139 @@ a.patreon-button:hover { height: 6em; width: auto; } + +#supporter-bear { + height: 250px; +} + +.donation-animation-container, +.donation-animation-bullet-points { + position: relative; + width: 100%; +} + +.donation-animation-bullet-points p { + position: absolute; + color: white; + text-align: center; + width: 100%; + font-size: 200%; + padding: 0 20px; +} + +.donation-animation-bullet-1 { + animation-name: animation-bullet-1; +} + +.donation-animation-bullet-2 { + animation-name: animation-bullet-2; +} + +.donation-animation-bullet-3 { + animation-name: animation-bullet-3; +} + +.donation-animation-bullet-4 { + animation-name: animation-bullet-4; +} + +.donation-animation-bullet-1, +.donation-animation-bullet-2, +.donation-animation-bullet-3, +.donation-animation-bullet-4 { + opacity: 0; + animation-fill-mode: forwards; + animation-duration: 20s; + margin-top: 40px; + animation-timing-function: normal; +} + +@keyframes animation-bullet-1 { + 1% { + opacity: 0; + margin-top: 40px; + } + 2.5% { + opacity: 1; + margin-top: 80px; + } + 19.5% { + opacity: 1; + margin-top: 80px; + } + 21% { + opacity: 0; + margin-top: 120px; + } +} + +@keyframes animation-bullet-2 { + 21% { + opacity: 0; + margin-top: 40px; + } + 22.5% { + opacity: 1; + margin-top: 80px; + } + 38.5% { + opacity: 1; + margin-top: 80px; + } + 40% { + opacity: 0; + margin-top: 120px; + } +} + +@keyframes animation-bullet-3 { + 40% { + opacity: 0; + margin-top: 40px; + } + 41.5% { + opacity: 1; + margin-top: 80px; + } + 64% { + opacity: 1; + margin-top: 80px; + } + 65.5% { + opacity: 0; + margin-top: 120px; + } +} + +@keyframes animation-bullet-4 { + 65.5% { + opacity: 0; + margin-top: 40px; + } + 67% { + opacity: 1; + margin-top: 80px; + } + 96% { + opacity: 1; + margin-top: 80px; + } + 97.5% { + opacity: 0; + } +} + +.tester-text { + margin-top: 80px; +} + +@media (max-width: 768px) { + .donation-animation-bullet-points p { + font-size: 150%; + } + #donation-animation { + object-fit: cover; + min-height: 600px; + width: 100%; + } +} diff --git a/client/src/components/Donation/multi-tier-donation-form.tsx b/client/src/components/Donation/multi-tier-donation-form.tsx index 7b1e353c4b0..f3dd6fcbd5a 100644 --- a/client/src/components/Donation/multi-tier-donation-form.tsx +++ b/client/src/components/Donation/multi-tier-donation-form.tsx @@ -27,15 +27,18 @@ type MultiTierDonationFormProps = { paymentContext: PaymentContext; isMinimalForm?: boolean; defaultTheme?: Themes; + isAnimationEnabled?: boolean; }; function SelectionTabs({ donationAmount, setDonationAmount, - setShowDonateForm + setShowDonateForm, + isAnimationEnabled }: { donationAmount: DonationAmount; setDonationAmount: React.Dispatch>; setShowDonateForm: React.Dispatch>; + isAnimationEnabled?: boolean; }) { const { t } = useTranslation(); const switchTab = (value: string): void => { @@ -97,7 +100,9 @@ function SelectionTabs({ data-cy='donation-tier-selection-button' onClick={() => setShowDonateForm(true)} > - {t('buttons.donate')} + {isAnimationEnabled + ? t('buttons.confirm-amount') + : t('buttons.donate')} @@ -138,7 +143,8 @@ const MultiTierDonationForm: React.FC = ({ handleProcessing, setShowHeaderAndFooter, isMinimalForm, - paymentContext + paymentContext, + isAnimationEnabled }) => { const [donationAmount, setDonationAmount] = useState(defaultTierAmount); @@ -155,6 +161,7 @@ const MultiTierDonationForm: React.FC = ({ donationAmount={donationAmount} setDonationAmount={setDonationAmount} setShowDonateForm={setShowDonateForm} + isAnimationEnabled={isAnimationEnabled} />
    diff --git a/cypress/e2e/default/learn/donate/donation-block-completion-modal.ts b/cypress/e2e/default/learn/donate/donation-block-completion-modal.ts index 785cdad002f..5b0df3e095c 100644 --- a/cypress/e2e/default/learn/donate/donation-block-completion-modal.ts +++ b/cypress/e2e/default/learn/donate/donation-block-completion-modal.ts @@ -34,8 +34,6 @@ describe('Donate page', () => { projects.forEach(project => submitProject(project)); // pop up modal - cy.contains( - 'Nicely done. You just completed Front End Development Libraries Projects.' - ); + cy.get("[data-cy='donation-modal']"); }); });