feat: add catalog (#66680)

Co-authored-by: jdwilkin4 <jwilkin4@hotmail.com>
This commit is contained in:
Ahmad Abdolsaheb
2026-03-30 22:12:07 +03:00
committed by GitHub
parent d94a4ef671
commit 9a0086e2a6
8 changed files with 104 additions and 12 deletions
@@ -29,6 +29,7 @@
"sign-in": "Sign in",
"sign-up-email-list": "Sign up for Quincy's weekly emails",
"sign-out": "Sign out",
"catalog": "Catalog",
"curriculum": "Curriculum",
"contribute": "Contribute",
"podcast": "Podcast",
@@ -1500,6 +1501,7 @@
"intermediate": "Intermediate",
"advanced": "Advanced"
},
"duration-singular": "{{duration}} hour",
"duration": "{{duration}} hours",
"no-results": "No courses found. Try adjusting your filters to see more results.",
"topic": {
@@ -152,6 +152,11 @@ function NavLinks({
{t('buttons.curriculum')}
</Link>
</li>
<li key='catalog'>
<Link className='nav-link' onKeyDown={handleMenuKeyDown} to='/catalog'>
{t('buttons.catalog')}
</Link>
</li>
{currentUserName && (
<>
<li key='profile'>
+6 -4
View File
@@ -28,6 +28,11 @@ const CatalogItem: React.FC<CatalogItemProps> = ({
summary: string[];
};
const duration =
hours === 1
? t('curriculum.catalog.duration-singular', { duration: hours })
: t('curriculum.catalog.duration', { duration: hours });
return (
<Link to={`/learn/${superBlock}`} key={superBlock} className='catalog-item'>
<div className='catalog-item-top'>
@@ -48,10 +53,7 @@ const CatalogItem: React.FC<CatalogItemProps> = ({
</div>
<div>
<FontAwesomeIcon icon={faClock} />
&nbsp;{' '}
{showAllSummaries
? t('curriculum.catalog.duration', { duration: hours })
: `${hours} hours`}
&nbsp; {duration}
</div>
</div>
</Link>
@@ -0,0 +1,44 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { describe, expect, test } from 'vitest';
import { SuperBlocks } from '@freecodecamp/shared/config/curriculum';
import { catalog } from '@freecodecamp/shared/config/catalog';
import LandingCatalog from './landing-catalog';
const featuredSuperBlocks = [
SuperBlocks.LearnPythonForBeginners,
SuperBlocks.ComputerBasics,
SuperBlocks.BasicHtml
];
describe('LandingCatalog', () => {
test('renders the catalog heading', () => {
render(<LandingCatalog />);
expect(screen.getByText('landing.catalog.heading')).toBeInTheDocument();
});
test('renders three featured course items and a see all link', () => {
render(<LandingCatalog />);
// 3 featured courses + 1 "See All" link
expect(screen.getAllByRole('link')).toHaveLength(4);
});
test('featured courses link to their superblock learn pages', () => {
render(<LandingCatalog />);
for (const superBlock of featuredSuperBlocks) {
const course = catalog.find(c => c.superBlock === superBlock);
const link = screen.getByRole('link', {
name: new RegExp(`topic\\.${course!.topic}`)
});
expect(link).toHaveAttribute('href', `/learn/${superBlock}`);
}
});
test('has a "See All Courses" link to /catalog', () => {
render(<LandingCatalog />);
const seeAllLink = screen.getByRole('link', {
name: 'landing.catalog.seeAll'
});
expect(seeAllLink).toHaveAttribute('href', '/catalog');
});
});
+40
View File
@@ -0,0 +1,40 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { describe, expect, test, vi } from 'vitest';
import { catalog } from '@freecodecamp/shared/config/catalog';
import CatalogPage from './catalog';
vi.mock('../components/catalog-item', () => ({
default: ({ superBlock }: { superBlock: string }) => (
<a data-testid='catalog-item' href={`/learn/${superBlock}`}>
{superBlock}
</a>
)
}));
describe('CatalogPage', () => {
test('renders the catalog page title', () => {
render(<CatalogPage />);
expect(screen.getByText('curriculum.catalog.title')).toBeInTheDocument();
});
test('renders a catalog item for each entry', () => {
render(<CatalogPage />);
const items = screen.getAllByTestId('catalog-item');
expect(items).toHaveLength(catalog.length);
});
test('catalog items link to their superblock learn pages', () => {
render(<CatalogPage />);
for (const course of catalog) {
const item = screen.getByRole('link', { name: course.superBlock });
expect(item).toHaveAttribute('href', `/learn/${course.superBlock}`);
}
});
test('renders level and topic filter dropdowns', () => {
render(<CatalogPage />);
expect(screen.getByText(/Level:/)).toBeInTheDocument();
expect(screen.getByText(/Topic:/)).toBeInTheDocument();
});
});
+1 -5
View File
@@ -2,8 +2,6 @@ import React, { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Col, Spacer, Dropdown, MenuItem, Alert } from '@freecodecamp/ui';
import { catalog } from '@freecodecamp/shared/config/catalog';
import { showUpcomingChanges } from '../../config/env.json';
import FourOhFour from '../components/FourOhFour';
import CatalogItem from '../components/catalog-item';
import './catalog.css';
@@ -69,7 +67,7 @@ const CatalogPage = () => {
});
}, [selectedLevels, selectedTopics]);
return showUpcomingChanges ? (
return (
<main>
<Spacer size='l' />
<h1 className='text-center'>{t('curriculum.catalog.title')}</h1>
@@ -164,8 +162,6 @@ const CatalogPage = () => {
</Col>
<Spacer size='l' />
</main>
) : (
<FourOhFour />
);
};
+1 -2
View File
@@ -10,7 +10,6 @@ import LandingCatalog from '../components/landing/components/landing-catalog';
import Faq from '../components/landing/components/faq';
import Benefits from '../components/landing/components/benefits';
import { useClaimableCertsNotification } from '../components/helpers/use-claimable-certs-notification';
import { showUpcomingChanges } from '../../config/env.json';
import '../components/landing/landing.css';
@@ -24,7 +23,7 @@ const Landing = () => (
<Benefits />
<Testimonials />
<Certifications />
{showUpcomingChanges && <LandingCatalog />}
<LandingCatalog />
<Faq />
</main>
);
+5 -1
View File
@@ -138,7 +138,7 @@ test.describe('Header', () => {
await expect(menuButton).toBeFocused();
});
test('The menu should contain links to: donate, curriculum, forum, news, radio, contribute, and podcast', async ({
test('The menu should contain links to: donate, curriculum, catalog, forum, news, radio, contribute, and podcast', async ({
page
}) => {
const menuButton = page.getByTestId(headerComponentElements.menuButton);
@@ -157,6 +157,10 @@ test.describe('Header', () => {
name: translations.buttons.curriculum,
href: '/learn'
},
{
name: translations.buttons.catalog,
href: '/catalog'
},
{
name: translations.buttons.forum,
href: links.nav.forum