diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json index 2c4a5dfbaa5..f9267403944 100644 --- a/client/i18n/locales/english/translations.json +++ b/client/i18n/locales/english/translations.json @@ -214,6 +214,7 @@ "next-heading": "Try our beta curriculum:", "upcoming-heading": "Upcoming curriculum:", "catalog-heading": "Explore our Catalog:", + "archive-link": "Looking for older coursework? Check out <0>our archive page.", "faq": "Frequently asked questions:", "faqs": [ { @@ -662,6 +663,10 @@ "warm-up": "Warm-up", "learn": "Learn", "practice": "Practice" + }, + "archive": { + "title": "Archived Coursework", + "content-not-updated": "<0>Warning: The content in this section is not being updated, but is still available for you to further your learning. We recommend trying <1>our current curriculum." } }, "donate": { diff --git a/client/src/components/Map/index.tsx b/client/src/components/Map/index.tsx index 024226a4e2a..b68b52d60a2 100644 --- a/client/src/components/Map/index.tsx +++ b/client/src/components/Map/index.tsx @@ -1,17 +1,18 @@ import i18next from 'i18next'; import React, { Fragment } from 'react'; import { Spacer } from '@freecodecamp/ui'; -import { useTranslation } from 'react-i18next'; +import { useTranslation, Trans } from 'react-i18next'; import { type SuperBlocks, SuperBlockStage, getStageOrder, - superBlockStages + superBlockStages, + archivedSuperBlocks } from '../../../../shared-dist/config/curriculum'; import { SuperBlockIcon } from '../../assets/superblock-icon'; import LinkButton from '../../assets/icons/link-button'; -import { ButtonLink } from '../helpers'; +import { ButtonLink, Link } from '../helpers'; import { showUpcomingChanges } from '../../../config/env.json'; import DailyCodingChallengeWidget from '../daily-coding-challenge/widget'; @@ -68,6 +69,20 @@ function MapLi({ ); } +// used on /learn/archive +export function ArchiveMap() { + return ( +
+ +
+ ); +} + +// used on /learn and landing page function Map({ forLanding = false }: MapProps) { const { t } = useTranslation(); @@ -75,39 +90,48 @@ function Map({ forLanding = false }: MapProps) {
{getStageOrder({ showUpcomingChanges - }).map(stage => { - const superblocks = superBlockStages[stage]; - if (superblocks.length === 0) { - return null; - } + }) + // remove legacy superblocks from main maps - shown in archive map only + .filter(stage => stage !== SuperBlockStage.Legacy) + .map(stage => { + const superblocks = superBlockStages[stage]; + if (superblocks.length === 0) { + return null; + } - return ( - - { - /* Show the daily coding challenge before the "English" curriculum */ - stage === SuperBlockStage.English && ( - <> - - - - ) - } -

- {t(superBlockHeadings[stage])} -

-
    - {superblocks.map(superblock => ( - - ))} -
- -
- ); - })} + return ( + + { + /* Show the daily coding challenge before the "English" curriculum */ + stage === SuperBlockStage.English && ( + <> + + + + ) + } +

+ {t(superBlockHeadings[stage])} +

+
    + {superblocks.map(superblock => ( + + ))} +
+ +
+ ); + })} + +

+ + placeholder + +

); } diff --git a/client/src/components/Map/map.css b/client/src/components/Map/map.css index c57ac074943..c1fe76ac1fa 100644 --- a/client/src/components/Map/map.css +++ b/client/src/components/Map/map.css @@ -8,6 +8,10 @@ padding: 0; } +.map-ui .archive-link { + font-size: 1.25rem; +} + @media (max-width: 640px) { .map-ui .block ul { padding-inline-start: 6rem; diff --git a/client/src/components/archived-warning/index.css b/client/src/components/archived-warning/index.css new file mode 100644 index 00000000000..3441a875acf --- /dev/null +++ b/client/src/components/archived-warning/index.css @@ -0,0 +1,8 @@ +.archived-warning { + margin-bottom: 0; + font-size: 1rem; +} + +.archived-warning strong { + color: var(--blue-dark); +} diff --git a/client/src/components/archived-warning/index.tsx b/client/src/components/archived-warning/index.tsx new file mode 100644 index 00000000000..0972aa3b8b9 --- /dev/null +++ b/client/src/components/archived-warning/index.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Trans } from 'react-i18next'; +import { Callout } from '@freecodecamp/ui'; + +import { Link } from '../helpers'; + +import './index.css'; + +const ArchivedWarning = () => { + return ( + +

+ + placeholder + placeholder + +

+
+ ); +}; + +export default ArchivedWarning; diff --git a/client/src/pages/learn/archive.tsx b/client/src/pages/learn/archive.tsx new file mode 100644 index 00000000000..f44ff50c027 --- /dev/null +++ b/client/src/pages/learn/archive.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Container, Row, Col, Spacer } from '@freecodecamp/ui'; +import Helmet from 'react-helmet'; +import LearnLayout from '../../components/layouts/learn'; + +import { ArchiveMap } from '../../components/Map'; +import CalendarIcon from '../../assets/icons/calendar'; +import ArchivedWarning from '../../components/archived-warning'; + +const ArchivePage = () => { + const { t } = useTranslation(); + + return ( + + + + + + +

+ {t('learn.archive.title')} +

+ + + + + +

{t('landing.legacy-curriculum-heading')}

+ + + +
+
+
+ ); +}; + +export default ArchivePage; diff --git a/client/src/templates/Introduction/components/legacy-links.tsx b/client/src/templates/Introduction/components/legacy-links.tsx index 6ef7d93dc39..f50a38bf6ce 100644 --- a/client/src/templates/Introduction/components/legacy-links.tsx +++ b/client/src/templates/Introduction/components/legacy-links.tsx @@ -2,12 +2,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { Alert } from '@freecodecamp/ui'; import { SuperBlocks } from '../../../../../shared-dist/config/curriculum'; -import { - isOldRespCert, - isRelationalDbCert, - isExamCert -} from '../../../utils/is-a-cert'; -import { Link } from '../../../components/helpers'; +import { isRelationalDbCert, isExamCert } from '../../../utils/is-a-cert'; import { CodeAllyDown } from '../../../components/growth-book/codeally-down'; import envData from '../../../../config/env.json'; @@ -22,18 +17,7 @@ interface LegacyLinksProps { function LegacyLinks({ superBlock }: LegacyLinksProps): JSX.Element { const { t } = useTranslation(); - if (isOldRespCert(superBlock)) { - return ( - -

- {t('intro:misc-text.legacy-desc')}{' '} - - {t('intro:misc-text.legacy-go-back')} - -

-
- ); - } else if (isRelationalDbCert(superBlock)) { + if (isRelationalDbCert(superBlock)) { return ( <> diff --git a/client/src/templates/Introduction/components/super-block-intro.tsx b/client/src/templates/Introduction/components/super-block-intro.tsx index d2419b1a62b..c9e30e641ee 100644 --- a/client/src/templates/Introduction/components/super-block-intro.tsx +++ b/client/src/templates/Introduction/components/super-block-intro.tsx @@ -4,7 +4,10 @@ import { useTranslation, Trans } from 'react-i18next'; import { Alert, Spacer, Container, Row, Col, Callout } from '@freecodecamp/ui'; import { ConnectedProps, connect } from 'react-redux'; import { useFeatureIsOn } from '@growthbook/growthbook-react'; -import { SuperBlocks } from '../../../../../shared-dist/config/curriculum'; +import { + archivedSuperBlocks, + SuperBlocks +} from '../../../../../shared-dist/config/curriculum'; import { SuperBlockIcon } from '../../../assets/superblock-icon'; import { Link } from '../../../components/helpers'; import CapIcon from '../../../assets/icons/cap'; @@ -12,6 +15,7 @@ import DumbbellIcon from '../../../assets/icons/dumbbell'; import CommunityIcon from '../../../assets/icons/community'; import { CompletedChallenge } from '../../../redux/prop-types'; import { completedChallengesSelector } from '../../../redux/selectors'; +import ArchivedWarning from '../../../components/archived-warning'; interface SuperBlockIntroQueryData { challengeNode: { @@ -142,6 +146,8 @@ function SuperBlockIntro({ const introTopA = ( <> + {archivedSuperBlocks.includes(superBlock) && } +

{i18nSuperBlock}

diff --git a/client/src/utils/is-a-cert.ts b/client/src/utils/is-a-cert.ts index a047fea5f9f..c6992b5ef5d 100644 --- a/client/src/utils/is-a-cert.ts +++ b/client/src/utils/is-a-cert.ts @@ -1,9 +1,5 @@ import { SuperBlocks } from '../../../shared-dist/config/curriculum'; -export function isOldRespCert(superBlock: string): boolean { - return superBlock === String(SuperBlocks.RespWebDesign); -} - export function isRelationalDbCert(superBlock: string): boolean { return superBlock === String(SuperBlocks.RelationalDb); } diff --git a/e2e/archive.spec.ts b/e2e/archive.spec.ts new file mode 100644 index 00000000000..782b10d07b0 --- /dev/null +++ b/e2e/archive.spec.ts @@ -0,0 +1,44 @@ +import { expect, test } from '@playwright/test'; +import intro from '../client/i18n/locales/english/intro.json'; +import { SuperBlocks } from '../shared/config/curriculum'; + +const archivedSuperBlocks = [ + intro[SuperBlocks.RespWebDesignNew].title, + intro[SuperBlocks.JsAlgoDataStructNew].title, + intro[SuperBlocks.FrontEndDevLibs].title, + intro[SuperBlocks.DataVis].title, + intro[SuperBlocks.RelationalDb].title, + intro[SuperBlocks.BackEndDevApis].title, + intro[SuperBlocks.QualityAssurance].title, + intro[SuperBlocks.SciCompPy].title, + intro[SuperBlocks.DataAnalysisPy].title, + intro[SuperBlocks.InfoSec].title, + intro[SuperBlocks.MachineLearningPy].title, + intro[SuperBlocks.CollegeAlgebraPy].title, + intro[SuperBlocks.RespWebDesign].title, + intro[SuperBlocks.JsAlgoDataStruct].title, + intro[SuperBlocks.PythonForEverybody].title +]; + +test.describe('Archive Page', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/learn/archive'); + }); + + test('Links to new curriculum', async ({ page }) => { + const newCurriculumLink = page.locator( + 'a[href="/learn/full-stack-developer"]' + ); + await expect(newCurriculumLink).toBeVisible(); + }); + + test('Links to all archived superblocks in order', async ({ page }) => { + const curriculumBtns = page.getByTestId('curriculum-map-button'); + await expect(curriculumBtns).toHaveCount(archivedSuperBlocks.length); + for (let index = 0; index < archivedSuperBlocks.length; index++) { + const btn = curriculumBtns.nth(index); + const link = btn.getByRole('link', { name: archivedSuperBlocks[index] }); + await expect(link).toBeVisible(); + } + }); +}); diff --git a/e2e/landing.spec.ts b/e2e/landing.spec.ts index 62385373d54..070469d5049 100644 --- a/e2e/landing.spec.ts +++ b/e2e/landing.spec.ts @@ -15,7 +15,7 @@ const landingPageElements = { jobs: 'More than 100,000 freeCodeCamp.org graduates have gotten jobs at tech companies including:' } as const; -const superBlocks = [ +const nonArchivedSuperBlocks = [ intro[SuperBlocks.FullStackDeveloper].title, intro[SuperBlocks.A2English].title, intro[SuperBlocks.B1English].title, @@ -23,21 +23,6 @@ const superBlocks = [ intro[SuperBlocks.CodingInterviewPrep].title, intro[SuperBlocks.ProjectEuler].title, intro[SuperBlocks.RosettaCode].title, - intro[SuperBlocks.RespWebDesignNew].title, - intro[SuperBlocks.JsAlgoDataStructNew].title, - intro[SuperBlocks.FrontEndDevLibs].title, - intro[SuperBlocks.DataVis].title, - intro[SuperBlocks.RelationalDb].title, - intro[SuperBlocks.BackEndDevApis].title, - intro[SuperBlocks.QualityAssurance].title, - intro[SuperBlocks.SciCompPy].title, - intro[SuperBlocks.DataAnalysisPy].title, - intro[SuperBlocks.InfoSec].title, - intro[SuperBlocks.MachineLearningPy].title, - intro[SuperBlocks.CollegeAlgebraPy].title, - intro[SuperBlocks.RespWebDesign].title, - intro[SuperBlocks.JsAlgoDataStruct].title, - intro[SuperBlocks.PythonForEverybody].title, intro[SuperBlocks.FoundationalCSharp].title ]; @@ -209,16 +194,23 @@ test.describe('Landing Page', () => { } }); - test('Links to all superblocks in order', async ({ page }) => { + test('Links to all non-archived superblocks in order', async ({ page }) => { const curriculumBtns = page.getByTestId(landingPageElements.curriculumBtns); - await expect(curriculumBtns).toHaveCount(superBlocks.length); - for (let index = 0; index < superBlocks.length; index++) { + await expect(curriculumBtns).toHaveCount(nonArchivedSuperBlocks.length); + for (let index = 0; index < nonArchivedSuperBlocks.length; index++) { const btn = curriculumBtns.nth(index); - const link = btn.getByRole('link', { name: superBlocks[index] }); + const link = btn.getByRole('link', { + name: nonArchivedSuperBlocks[index] + }); await expect(link).toBeVisible(); } }); + test('Links to the archive page', async ({ page }) => { + const archiveLink = page.locator('a[href="/learn/archive"]'); + await expect(archiveLink).toBeVisible(); + }); + test('Has FAQ section', async ({ page }) => { const faqs = page.getByTestId(landingPageElements.faq); await expect(faqs).toHaveCount(9); diff --git a/e2e/map.spec.ts b/e2e/map.spec.ts index d00eafcc805..635f9fbd51a 100644 --- a/e2e/map.spec.ts +++ b/e2e/map.spec.ts @@ -6,42 +6,6 @@ test.beforeEach(async ({ page }) => { }); const LANDING_PAGE_LINKS = [ - { - slug: '2022/responsive-web-design', - name: 'Responsive Web Design' - }, - { - slug: 'javascript-algorithms-and-data-structures-v8', - name: 'JavaScript Algorithms and Data Structures' - }, - { - slug: 'front-end-development-libraries', - name: 'Front End Development Libraries' - }, - { slug: 'data-visualization', name: 'Data Visualization' }, - { slug: 'relational-database', name: 'Relational Database' }, - { - slug: 'back-end-development-and-apis', - name: 'Back End Development and APIs' - }, - { slug: 'quality-assurance', name: 'Quality Assurance' }, - { - slug: 'scientific-computing-with-python', - name: 'Scientific Computing with Python' - }, - { - slug: 'data-analysis-with-python', - name: 'Data Analysis with Python' - }, - { slug: 'information-security', name: 'Information Security' }, - { - slug: 'machine-learning-with-python', - name: 'Machine Learning with Python' - }, - { - slug: 'college-algebra-with-python', - name: 'College Algebra with Python' - }, { slug: 'full-stack-developer', name: 'Certified Full Stack Developer Curriculum' @@ -61,16 +25,7 @@ const LANDING_PAGE_LINKS = [ { slug: 'the-odin-project', name: 'The Odin Project - freeCodeCamp Remix' }, { slug: 'coding-interview-prep', name: 'Coding Interview Prep' }, { slug: 'project-euler', name: 'Project Euler' }, - { slug: 'rosetta-code', name: 'Rosetta Code' }, - { - slug: 'responsive-web-design', - name: 'Legacy Responsive Web Design Challenges' - }, - { - slug: 'javascript-algorithms-and-data-structures', - name: 'Legacy JavaScript Algorithms and Data Structures' - }, - { slug: 'python-for-everybody', name: 'Legacy Python for Everybody' } + { slug: 'rosetta-code', name: 'Rosetta Code' } ]; test.describe('Map Component', () => { @@ -85,7 +40,7 @@ test.describe('Map Component', () => { page.getByText(translations.landing['interview-prep-heading']) ).toBeVisible(); const curriculumBtns = page.getByTestId('curriculum-map-button'); - await expect(curriculumBtns).toHaveCount(23); + await expect(curriculumBtns).toHaveCount(8); for (const { name, slug } of LANDING_PAGE_LINKS) { const superblockLink = page.getByRole('link', { diff --git a/shared/config/curriculum.ts b/shared/config/curriculum.ts index 91853e428f2..01ff5c45510 100644 --- a/shared/config/curriculum.ts +++ b/shared/config/curriculum.ts @@ -133,6 +133,8 @@ export const superBlockStages: StageMap = { Object.freeze(superBlockStages); +export const archivedSuperBlocks = superBlockStages[SuperBlockStage.Legacy]; + export const catalogSuperBlocks = superBlockStages[SuperBlockStage.Catalog]; type NotAuditedSuperBlocks = {