diff --git a/client/config/growthbook-features-default.json b/client/config/growthbook-features-default.json index f73e3d55181..7a16d1bd9ef 100644 --- a/client/config/growthbook-features-default.json +++ b/client/config/growthbook-features-default.json @@ -62,5 +62,37 @@ "name": "stg show modal randomly" } ] + }, + "show-benefits": { + "defaultValue": false, + "rules": [ + { + "coverage": 1, + "hashAttribute": "id", + "seed": "show-benefits", + "hashVersion": 2, + "variations": [ + false, + true + ], + "weights": [ + 0.5, + 0.5 + ], + "key": "show-benefits", + "meta": [ + { + "key": "0", + "name": "Control" + }, + { + "key": "1", + "name": "Variation 1" + } + ], + "phase": "0", + "name": "prod-show-benefits" + } + ] } } \ No newline at end of file diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json index 36d9999ecb7..6aa08815765 100644 --- a/client/i18n/locales/english/translations.json +++ b/client/i18n/locales/english/translations.json @@ -112,7 +112,7 @@ "big-heading-3": "Earn certifications.", "big-heading-4": "All for free.", "h2-heading": "Since 2014, more than 40,000 freeCodeCamp.org graduates have gotten jobs at tech companies including:", - "h2-heading-b": "More than 100,000 freeCodeCamp.org graduates have gotten jobs at tech companies including:", + "h2-heading-b": "More than 100,000 freeCodeCamp.org graduates have gotten jobs at tech companies including:", "hero-img-description": "freeCodeCamp students at a local study group in South Korea.", "hero-img-alt": "A group of people, including a White man, a Black woman, and an Asian woman, gathered around a laptop.", "hero-img-uis": "A group of screenshots showing the freeCodeCamp editor interface on both a mobile and desktop device and a certification.", @@ -135,6 +135,29 @@ "testimony": "\"I've always struggled with learning JavaScript. I've taken many courses but freeCodeCamp's course was the one which stuck. Studying JavaScript as well as data structures and algorithms on freeCodeCamp gave me the skills and confidence I needed to land my dream job as a software engineer at Spotify.\"" } }, + "benefits": { + "heading": "Why learn with freeCodeCamp:", + "list": [ + { + "title": "Large Community", + "description": "Join our vibrant learning community of students, alumni, and educators." + }, + { + "title": "Free Education", + "description": "Learn from our charity and save money on your education. No paywalls. No hidden costs." + }, + { + "title": "Extensive Certifications", + "description": "Earn industry-recognized, verifiable certifications in high-demand technologies." + }, + { + "title": "Comprehensive Curriculum", + "description": "Enhance your technical skills with our linear, world-class, project-based curriculum." + } + ], + "cta": "Start Learning Now (it's free)" + }, + "certification-heading": "Earn free verified certifications in:", "core-certs-heading": "Earn free verified certifications with freeCodeCamp's core curriculum:", "learn-english-heading": "Learn English for Developers:", diff --git a/client/src/assets/icons/cap.tsx b/client/src/assets/icons/cap.tsx new file mode 100644 index 00000000000..27d884de3d3 --- /dev/null +++ b/client/src/assets/icons/cap.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +function FreeIcon( + props: JSX.IntrinsicAttributes & React.SVGProps +): JSX.Element { + return ( + + + + + ); +} + +FreeIcon.displayName = 'FreeIcon'; + +export default FreeIcon; diff --git a/client/src/assets/icons/community.tsx b/client/src/assets/icons/community.tsx new file mode 100644 index 00000000000..8df72f013c3 --- /dev/null +++ b/client/src/assets/icons/community.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +function CommunityIcon( + props: JSX.IntrinsicAttributes & React.SVGProps +): JSX.Element { + return ( + + + + + + + + + ); +} + +CommunityIcon.displayName = 'CommunityIcon'; + +export default CommunityIcon; diff --git a/client/src/assets/icons/curriculum.tsx b/client/src/assets/icons/curriculum.tsx new file mode 100644 index 00000000000..1a97d5188c2 --- /dev/null +++ b/client/src/assets/icons/curriculum.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +function CurriculumIcon( + props: JSX.IntrinsicAttributes & React.SVGProps +): JSX.Element { + return ( + + + + ); +} + +CurriculumIcon.displayName = 'CurriculumIcon'; + +export default CurriculumIcon; diff --git a/client/src/assets/icons/free.tsx b/client/src/assets/icons/free.tsx new file mode 100644 index 00000000000..5b6e5280d14 --- /dev/null +++ b/client/src/assets/icons/free.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +function FreeIcon( + props: JSX.IntrinsicAttributes & React.SVGProps +): JSX.Element { + return ( + + + + ); +} + +FreeIcon.displayName = 'FreeIcon'; + +export default FreeIcon; diff --git a/client/src/components/landing/components/benefits.tsx b/client/src/components/landing/components/benefits.tsx new file mode 100644 index 00000000000..c6e0f697165 --- /dev/null +++ b/client/src/components/landing/components/benefits.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Col, Row, Container } from '@freecodecamp/ui'; + +import { Spacer } from '../../helpers'; +import FreeIcon from '../../../assets/icons/free'; +import CapIcon from '../../../assets/icons/cap'; +import CommunityIcon from '../../../assets/icons/community'; +import CurriculumIcon from '../../../assets/icons/curriculum'; +import BigCallToAction from './big-call-to-action'; + +interface BenefitsItem { + title: string; + description: string; +} + +const iconsList = [CommunityIcon, FreeIcon, CapIcon, CurriculumIcon]; + +const Benefits = (): JSX.Element => { + const { t } = useTranslation(); + const benefitItems: BenefitsItem[] = t('landing.benefits.list', { + returnObjects: true + }); + + return ( + + + + +

+ {t('landing.benefits.heading')} +

+ +
+ + + + {benefitItems.map((benefit, index) => { + const IconComponent = iconsList[index % iconsList.length]; // Get the correct icon component based on index + return ( +
+ {/* Dynamically render the icon */} + +

{benefit.title}

+

{benefit.description}

+ +
+ ); + })} + +
+ + + + + + +
+
+ ); +}; + +Benefits.displayName = 'Benefits'; +export default Benefits; diff --git a/client/src/components/landing/components/campers-image.tsx b/client/src/components/landing/components/campers-image.tsx index 25808944dd5..5f1b1b19a66 100644 --- a/client/src/components/landing/components/campers-image.tsx +++ b/client/src/components/landing/components/campers-image.tsx @@ -6,37 +6,16 @@ import { LazyImage } from '../../helpers'; const LARGE_SCREEN_SIZE = 1200; -interface CampersImageProps { - pageName: string; -} - -const donateImageSize = { - height: 345, - width: 585 -}; - -const landingImageSize = { - marginTop: '30px', - height: 442, - width: 750 -}; -function CampersImage({ pageName }: CampersImageProps): JSX.Element { +function CampersImage(): JSX.Element { const { t } = useTranslation(); - const figureSize = pageName === 'donate' ? donateImageSize : landingImageSize; - return (
- +
{t('landing.hero-img-description')}
diff --git a/client/src/components/landing/components/landing-top-b.tsx b/client/src/components/landing/components/landing-top-b.tsx index eba471ceef4..9a667a25673 100644 --- a/client/src/components/landing/components/landing-top-b.tsx +++ b/client/src/components/landing/components/landing-top-b.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import { Container, Col, Row } from '@freecodecamp/ui'; import { clientLocale } from '../../../../config/env.json'; import { @@ -13,10 +13,9 @@ import { } from '../../../assets/images/components'; import { Spacer } from '../../helpers'; import BigCallToAction from './big-call-to-action'; -import UIImages from './ui-images'; +import CampersImage from './campers-image'; const LogoRow = (): JSX.Element => { - const { t } = useTranslation(); const showChineseLogos = ['chinese', 'chinese-tradition'].includes( clientLocale ); @@ -27,9 +26,8 @@ const LogoRow = (): JSX.Element => { className='logo-row-title' data-playwright-test-label='landing-h2-heading-b' > - {t('landing.h2-heading-b')} + landing.h2-heading-b

-
    - + + - - +

    {t('landing.big-heading-4')}

    - + - - - - + - - - + + + diff --git a/client/src/components/landing/components/landing-top.tsx b/client/src/components/landing/components/landing-top.tsx index 336317f749c..9106de1ff19 100644 --- a/client/src/components/landing/components/landing-top.tsx +++ b/client/src/components/landing/components/landing-top.tsx @@ -22,7 +22,7 @@ function LandingTop(): JSX.Element { clientLocale ); return ( - + @@ -70,7 +70,7 @@ function LandingTop(): JSX.Element { - + diff --git a/client/src/components/landing/landing.css b/client/src/components/landing/landing.css index 7c0aa8f1c82..4871d6ac18f 100644 --- a/client/src/components/landing/landing.css +++ b/client/src/components/landing/landing.css @@ -177,6 +177,7 @@ figcaption.caption { .landing-top, .as-seen-in, .certification-section, +.benefits, .testimonials { padding: 4vw 15px; } @@ -218,23 +219,25 @@ figcaption.caption { /* AB testing styles */ .landing-page-b .mega-heading { - font-size: 2rem; - margin: 0px 0px 1rem; + font-size: 2.5rem; + margin: 0px 0px 2rem; font-weight: 700; - line-height: 3rem; + line-height: 2rem; +} + +.landing-page-b .landing-top .btn-cta-big { + max-width: fit-content; + padding: 12px 20px; } @media (min-width: 500px) { .landing-page-b .mega-heading { font-size: 3rem; + line-height: 2.5rem; + } + .landing-page-b .landing-top .btn-cta-big { + padding: 12px 40px; } -} - -.landing-page-b .landing-top .btn-cta-big { - margin: 0px; - min-width: 300px; - max-width: fit-content; - padding: 12px; } .landing-page-b .logo-row-title { @@ -257,3 +260,40 @@ figure.ui-images img { width: 100%; height: auto; } + +.landing-benefits { + display: grid; + grid-template-columns: 1fr; /* Default: 1 column */ + gap: 30px; /* Adjust the space between the grid items */ +} + +/* When the screen is larger than 700px */ +@media screen and (min-width: 700px) { + .landing-benefits { + grid-template-columns: 1fr 1fr; /* Two columns */ + } +} + +.landing-top figure, +.landing-top figure img { + margin-top: 50px; + height: 442px; + width: 750px; +} + +.landing-benefits svg path { + fill: var(--gray-15); +} + +.benefits-container { + background: var(--gray-75); +} + +.benefits-container h2, +.benefits-container h3 { + color: var(--gray-10); +} + +.benefits-container p { + color: var(--gray-15); +} diff --git a/client/src/components/layouts/global.css b/client/src/components/layouts/global.css index 9321ac8ede3..64dead38208 100644 --- a/client/src/components/layouts/global.css +++ b/client/src/components/layouts/global.css @@ -177,7 +177,7 @@ p { } .big-heading { - font-size: 2.5rem !important; + font-size: 2rem !important; overflow-wrap: break-word; } diff --git a/client/src/pages/index.tsx b/client/src/pages/index.tsx index 3c78f9f0d43..8e2d3ddf1d2 100644 --- a/client/src/pages/index.tsx +++ b/client/src/pages/index.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { graphql } from 'gatsby'; import { useTranslation } from 'react-i18next'; import { useGrowthBook } from '@growthbook/growthbook-react'; - import { SuperBlocks } from '../../../shared/config/curriculum'; import SEO from '../components/seo'; import { Loader } from '../components/helpers'; @@ -12,6 +11,7 @@ import AsSeenIn from '../components/landing/components/as-seen-in'; import Testimonials from '../components/landing/components/testimonials'; import Certifications from '../components/landing/components/certifications'; import Faq from '../components/landing/components/faq'; +import Benefits from '../components/landing/components/benefits'; import '../components/landing/landing.css'; type Challenge = { @@ -31,21 +31,21 @@ type Props = { type LandingProps = { allChallenges: Challenge[]; + showLandingPageRedesign: boolean; + showBenefitsSection: boolean; }; -const LandingA = ({ allChallenges }: LandingProps) => ( -
    - - - - - -
    -); +const Landing = ({ + allChallenges, + showLandingPageRedesign, + showBenefitsSection +}: LandingProps) => ( +
    + {showLandingPageRedesign ? : } + {showBenefitsSection ? : } -const LandingB = ({ allChallenges }: LandingProps) => ( -
    - @@ -65,14 +65,19 @@ function IndexPage({ 'landing-page-redesign', false ); + const showBenefitsSection = growthbook.getFeatureValue( + 'show-benefits', + false + ); + return ( <> - {showLandingPageRedesign === true ? ( - - ) : ( - - )} + ); } else { diff --git a/e2e/landing.spec.ts b/e2e/landing.spec.ts index 8eceb6b7cd8..f5d21ba6b7f 100644 --- a/e2e/landing.spec.ts +++ b/e2e/landing.spec.ts @@ -11,7 +11,8 @@ const landingPageElements = { curriculumBtns: 'curriculum-map-button', testimonials: 'testimonial-card', landingPageImage: 'landing-page-figure', - faq: 'landing-page-faq' + faq: 'landing-page-faq', + jobs: 'More than 100,000 freeCodeCamp.org graduates have gotten jobs at tech companies including:' } as const; const superBlocks = [ @@ -42,7 +43,7 @@ async function goToLandingPage(page: Page) { await page.goto('/'); } -test.describe('Landing Page - Variation B', () => { +test.describe('Landing Top - Variation B', () => { test.beforeEach(async ({ context, page }) => { await addGrowthbookCookie({ context, variation: 'B' }); await goToLandingPage(page); @@ -72,54 +73,27 @@ test.describe('Landing Page - Variation B', () => { const landingH2Heading = page.getByTestId('landing-h2-heading-b'); await expect(landingH2Heading).toHaveText( - translations.landing['h2-heading-b'] + translations.landing['h2-heading-b'].replace(/<\/?strong>/g, '') ); }); - - test('CTA buttons should render correctly', async ({ page }) => { - const mainCta = page.getByRole('link', { - name: translations.buttons['get-started'], - exact: true - }); - await expect(mainCta).toHaveCount(1); - for (const cta of await mainCta.all()) { - await expect(cta).toBeVisible(); - } - - const ctas = page.getByRole('link', { - name: translations.buttons['logged-in-cta-btn'], - exact: true - }); - await expect(ctas).toHaveCount(3); - for (const cta of await ctas.all()) { - await expect(cta).toBeVisible(); - } - }); - - test('Hero image should have a descriptive alt', async ({ - isMobile, - page - }) => { - const campersImage = page.getByAltText( - translations.landing['hero-img-uis'] - ); - - if (isMobile) { - await expect(campersImage).toBeHidden(); - } else { - await expect(campersImage).toBeVisible(); - } - }); - - test('The as seen in container with featured logos should not exist', async ({ - page - }) => { - const asSeenInContainer = page.getByTestId('landing-as-seen-in-text'); - await expect(asSeenInContainer).toHaveCount(0); - }); }); -test.describe('Landing Page - Variation A', () => { +test.describe('Second section - Variation B', () => { + test('The component Why learn with freeCodeCamp renders correctly', async ({ + context, + page + }) => { + await addGrowthbookCookie({ context, variation: 'C' }); + await goToLandingPage(page); + const h2Element = page.locator( + `h2:has-text("${translations.landing.benefits['heading']}")` + ); + + await expect(h2Element).toBeVisible(); + }); +}); + +test.describe('Landing Top - Variation A', () => { test.beforeEach(async ({ context, page }) => { await addGrowthbookCookie({ context, variation: 'A' }); await goToLandingPage(page); @@ -146,6 +120,24 @@ test.describe('Landing Page - Variation A', () => { translations.landing['h2-heading'] ); }); +}); + +test.describe('Second section - Variation A', () => { + test('The component As Seen renders correctly', async ({ context, page }) => { + await addGrowthbookCookie({ context, variation: 'E' }); + await goToLandingPage(page); + const h2Element = page.locator( + `h2:has-text("${translations.landing['as-seen-in']}")` + ); + + await expect(h2Element).toBeVisible(); + }); +}); + +test.describe('Landing Page', () => { + test.beforeEach(async ({ page }) => { + await goToLandingPage(page); + }); test('Call to action buttons should render correctly', async ({ page }) => { const ctas = page.getByRole('link', { @@ -177,25 +169,6 @@ test.describe('Landing Page - Variation A', () => { } }); - test('The as seen in container is visible with featured logos', async ({ - page - }) => { - const asSeenInContainer = page.getByTestId('landing-as-seen-in-text'); - await expect(asSeenInContainer).toHaveText( - translations.landing['as-seen-in'] - ); - const featuredLogos = page.getByTestId( - 'landing-as-seen-in-container-logos' - ); - await expect(featuredLogos).toBeVisible(); - }); -}); - -test.describe('Landing Page - common', () => { - test.beforeEach(async ({ page }) => { - await goToLandingPage(page); - }); - test('Has 5 brand logos', async ({ page }) => { const logos = page.getByTestId('brand-logo-container').locator('svg'); await expect(logos).toHaveCount(5);