diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json
index 3c0d7ca801f..42de1c1047e 100644
--- a/client/i18n/locales/english/translations.json
+++ b/client/i18n/locales/english/translations.json
@@ -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": {
diff --git a/client/src/components/Header/components/nav-links.tsx b/client/src/components/Header/components/nav-links.tsx
index 9697138f919..9212130c4b2 100644
--- a/client/src/components/Header/components/nav-links.tsx
+++ b/client/src/components/Header/components/nav-links.tsx
@@ -152,6 +152,11 @@ function NavLinks({
{t('buttons.curriculum')}
+
+
+ {t('buttons.catalog')}
+
+
{currentUserName && (
<>
diff --git a/client/src/components/catalog-item.tsx b/client/src/components/catalog-item.tsx
index 0a3a9a4e08b..17cb402b13c 100644
--- a/client/src/components/catalog-item.tsx
+++ b/client/src/components/catalog-item.tsx
@@ -28,6 +28,11 @@ const CatalogItem: React.FC = ({
summary: string[];
};
+ const duration =
+ hours === 1
+ ? t('curriculum.catalog.duration-singular', { duration: hours })
+ : t('curriculum.catalog.duration', { duration: hours });
+
return (
@@ -48,10 +53,7 @@ const CatalogItem: React.FC = ({
- {' '}
- {showAllSummaries
- ? t('curriculum.catalog.duration', { duration: hours })
- : `${hours} hours`}
+ {duration}
diff --git a/client/src/components/landing/components/landing-catalog.test.tsx b/client/src/components/landing/components/landing-catalog.test.tsx
new file mode 100644
index 00000000000..adc1ad00cc7
--- /dev/null
+++ b/client/src/components/landing/components/landing-catalog.test.tsx
@@ -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();
+ expect(screen.getByText('landing.catalog.heading')).toBeInTheDocument();
+ });
+
+ test('renders three featured course items and a see all link', () => {
+ render();
+ // 3 featured courses + 1 "See All" link
+ expect(screen.getAllByRole('link')).toHaveLength(4);
+ });
+
+ test('featured courses link to their superblock learn pages', () => {
+ render();
+ 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();
+ const seeAllLink = screen.getByRole('link', {
+ name: 'landing.catalog.seeAll'
+ });
+ expect(seeAllLink).toHaveAttribute('href', '/catalog');
+ });
+});
diff --git a/client/src/pages/catalog.test.tsx b/client/src/pages/catalog.test.tsx
new file mode 100644
index 00000000000..924ca3ea55e
--- /dev/null
+++ b/client/src/pages/catalog.test.tsx
@@ -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 }) => (
+
+ {superBlock}
+
+ )
+}));
+
+describe('CatalogPage', () => {
+ test('renders the catalog page title', () => {
+ render();
+ expect(screen.getByText('curriculum.catalog.title')).toBeInTheDocument();
+ });
+
+ test('renders a catalog item for each entry', () => {
+ render();
+ const items = screen.getAllByTestId('catalog-item');
+ expect(items).toHaveLength(catalog.length);
+ });
+
+ test('catalog items link to their superblock learn pages', () => {
+ render();
+ 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();
+ expect(screen.getByText(/Level:/)).toBeInTheDocument();
+ expect(screen.getByText(/Topic:/)).toBeInTheDocument();
+ });
+});
diff --git a/client/src/pages/catalog.tsx b/client/src/pages/catalog.tsx
index 5cd54b35ede..b6b2a1cad53 100644
--- a/client/src/pages/catalog.tsx
+++ b/client/src/pages/catalog.tsx
@@ -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 (
{t('curriculum.catalog.title')}
@@ -164,8 +162,6 @@ const CatalogPage = () => {
- ) : (
-
);
};
diff --git a/client/src/pages/index.tsx b/client/src/pages/index.tsx
index 5e0be95d5cc..67f76173e1c 100644
--- a/client/src/pages/index.tsx
+++ b/client/src/pages/index.tsx
@@ -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 = () => (
- {showUpcomingChanges && }
+
);
diff --git a/e2e/header.spec.ts b/e2e/header.spec.ts
index 8543c1c0da3..c3615224e6a 100644
--- a/e2e/header.spec.ts
+++ b/e2e/header.spec.ts
@@ -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