mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 10:07:46 -05:00
feat(client): archive page (#62450)
This commit is contained in:
@@ -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</0>.",
|
||||
"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:</0> 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</1>."
|
||||
}
|
||||
},
|
||||
"donate": {
|
||||
|
||||
@@ -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 (
|
||||
<div className='map-ui' data-test-label='curriculum-map'>
|
||||
<ul>
|
||||
{archivedSuperBlocks.map(superblock => (
|
||||
<MapLi key={superblock} superBlock={superblock} landing={false} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// used on /learn and landing page
|
||||
function Map({ forLanding = false }: MapProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -75,39 +90,48 @@ function Map({ forLanding = false }: MapProps) {
|
||||
<div className='map-ui' data-test-label='curriculum-map'>
|
||||
{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 (
|
||||
<Fragment key={stage}>
|
||||
{
|
||||
/* Show the daily coding challenge before the "English" curriculum */
|
||||
stage === SuperBlockStage.English && (
|
||||
<>
|
||||
<DailyCodingChallengeWidget forLanding={forLanding} />
|
||||
<Spacer size='m' />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<h2 className={forLanding ? 'big-heading' : ''}>
|
||||
{t(superBlockHeadings[stage])}
|
||||
</h2>
|
||||
<ul key={stage}>
|
||||
{superblocks.map(superblock => (
|
||||
<MapLi
|
||||
key={superblock}
|
||||
superBlock={superblock}
|
||||
landing={forLanding}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<Spacer size='m' />
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<Fragment key={stage}>
|
||||
{
|
||||
/* Show the daily coding challenge before the "English" curriculum */
|
||||
stage === SuperBlockStage.English && (
|
||||
<>
|
||||
<DailyCodingChallengeWidget forLanding={forLanding} />
|
||||
<Spacer size='m' />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<h2 className={forLanding ? 'big-heading' : ''}>
|
||||
{t(superBlockHeadings[stage])}
|
||||
</h2>
|
||||
<ul key={stage}>
|
||||
{superblocks.map(superblock => (
|
||||
<MapLi
|
||||
key={superblock}
|
||||
superBlock={superblock}
|
||||
landing={forLanding}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<Spacer size='m' />
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
<Spacer size='m' />
|
||||
<p className='archive-link'>
|
||||
<Trans i18nKey='landing.archive-link'>
|
||||
<Link to={'/learn/archive'}>placeholder</Link>
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
8
client/src/components/archived-warning/index.css
Normal file
8
client/src/components/archived-warning/index.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.archived-warning {
|
||||
margin-bottom: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.archived-warning strong {
|
||||
color: var(--blue-dark);
|
||||
}
|
||||
22
client/src/components/archived-warning/index.tsx
Normal file
22
client/src/components/archived-warning/index.tsx
Normal file
@@ -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 (
|
||||
<Callout variant='info'>
|
||||
<p className='text-center archived-warning'>
|
||||
<Trans i18nKey='learn.archive.content-not-updated'>
|
||||
<strong>placeholder</strong>
|
||||
<Link to={'/learn/full-stack-developer'}>placeholder</Link>
|
||||
</Trans>
|
||||
</p>
|
||||
</Callout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArchivedWarning;
|
||||
39
client/src/pages/learn/archive.tsx
Normal file
39
client/src/pages/learn/archive.tsx
Normal file
@@ -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 (
|
||||
<LearnLayout>
|
||||
<Helmet title={t('metaTags:title')} />
|
||||
<Container>
|
||||
<Row>
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<Spacer size='l' />
|
||||
<h1 className='text-center big-heading'>
|
||||
{t('learn.archive.title')}
|
||||
</h1>
|
||||
<Spacer size='m' />
|
||||
<CalendarIcon className='cert-header-icon' />
|
||||
<Spacer size='l' />
|
||||
<ArchivedWarning />
|
||||
<Spacer size='m' />
|
||||
<h2>{t('landing.legacy-curriculum-heading')}</h2>
|
||||
<ArchiveMap />
|
||||
<Spacer size='l' />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</LearnLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArchivePage;
|
||||
@@ -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 (
|
||||
<Alert variant='info'>
|
||||
<p>
|
||||
{t('intro:misc-text.legacy-desc')}{' '}
|
||||
<Link sameTab={false} to={`/learn/2022/responsive-web-design`}>
|
||||
{t('intro:misc-text.legacy-go-back')}
|
||||
</Link>
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
} else if (isRelationalDbCert(superBlock)) {
|
||||
if (isRelationalDbCert(superBlock)) {
|
||||
return (
|
||||
<>
|
||||
<CodeAllyDown />
|
||||
|
||||
@@ -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) && <ArchivedWarning />}
|
||||
<Spacer size='s' />
|
||||
<h1 id='content-start' className='text-center big-heading'>
|
||||
{i18nSuperBlock}
|
||||
</h1>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
44
e2e/archive.spec.ts
Normal file
44
e2e/archive.spec.ts
Normal file
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,7 @@ const landingPageElements = {
|
||||
jobs: 'More than <strong>100,000</strong> freeCodeCamp.org graduates have gotten <strong>jobs</strong> 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);
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user