mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 18:18:27 -05:00
refactor(client): daily challenges to use path params (#61776)
This commit is contained in:
@@ -1,23 +1,21 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams } from '@gatsbyjs/reach-router';
|
||||
import store from 'store';
|
||||
import ShowClassic from '../../templates/Challenges/classic/show';
|
||||
import { Loader } from '../../components/helpers';
|
||||
import ShowClassic from '../templates/Challenges/classic/show';
|
||||
import { Loader } from '../components/helpers';
|
||||
import {
|
||||
DailyCodingChallengeLanguages,
|
||||
DailyCodingChallengeNode,
|
||||
DailyCodingChallengePageContext
|
||||
} from '../../redux/prop-types';
|
||||
import DailyCodingChallengeNotFound from '../../components/daily-coding-challenge/not-found';
|
||||
import FourOhFour from '../../components/FourOhFour';
|
||||
import {
|
||||
apiLocation,
|
||||
showDailyCodingChallenges
|
||||
} from '../../../config/env.json';
|
||||
import { isValidDateParam } from '../../components/daily-coding-challenge/helpers';
|
||||
} from '../redux/prop-types';
|
||||
import DailyCodingChallengeNotFound from '../components/daily-coding-challenge/not-found';
|
||||
import FourOhFour from '../components/FourOhFour';
|
||||
import { apiLocation, showDailyCodingChallenges } from '../../config/env.json';
|
||||
import { isValidDateString } from '../components/daily-coding-challenge/helpers';
|
||||
import {
|
||||
validateDailyCodingChallengeSchema,
|
||||
type DailyCodingChallengeFromDb
|
||||
} from '../../utils/daily-coding-challenge-validator';
|
||||
} from '../utils/daily-coding-challenge-validator';
|
||||
|
||||
interface DailyCodingChallengeLanguageData {
|
||||
data: {
|
||||
@@ -31,7 +29,7 @@ interface DailyCodingChallengeDataFormatted {
|
||||
python: DailyCodingChallengeLanguageData;
|
||||
}
|
||||
|
||||
// These are not included in the data from the DB (Daily Challenge API) - so we add them in
|
||||
// This is not included in the data from the DB (Daily Challenge API) - so we add it in
|
||||
function formatDescription(str: string) {
|
||||
return `<section id="description">\n${str}\n</section>`;
|
||||
}
|
||||
@@ -149,7 +147,9 @@ function formatChallengeData({
|
||||
return props;
|
||||
}
|
||||
|
||||
function DailyCodingChallenge(): JSX.Element {
|
||||
function ShowDailyCodingChallenge(): JSX.Element {
|
||||
const { date } = useParams<{ date?: string }>();
|
||||
|
||||
const initLanguage =
|
||||
(store.get(
|
||||
'dailyCodingChallengeLanguage'
|
||||
@@ -162,9 +162,6 @@ function DailyCodingChallenge(): JSX.Element {
|
||||
const [dailyCodingChallengeLanguage, setDailyCodingChallengeLanguage] =
|
||||
useState<DailyCodingChallengeLanguages>(initLanguage);
|
||||
|
||||
const dateParam =
|
||||
new URLSearchParams(window.location.search).get('date') || '';
|
||||
|
||||
const fetchChallenge = async (date: string) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
@@ -201,15 +198,15 @@ function DailyCodingChallenge(): JSX.Element {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// If dateParam is invalid, stop loading/fetching and show the not found page
|
||||
if (!isValidDateParam(dateParam)) {
|
||||
// If date is invalid, stop loading/fetching and show the not found page
|
||||
if (!date || !isValidDateString(date)) {
|
||||
setIsLoading(false);
|
||||
setChallengeFound(false);
|
||||
return;
|
||||
}
|
||||
|
||||
void fetchChallenge(dateParam);
|
||||
}, [dateParam]);
|
||||
void fetchChallenge(date);
|
||||
}, [date]);
|
||||
|
||||
if (!showDailyCodingChallenges) {
|
||||
return <FourOhFour />;
|
||||
@@ -230,6 +227,6 @@ function DailyCodingChallenge(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
DailyCodingChallenge.displayName = 'DailyCodingChallenge';
|
||||
ShowDailyCodingChallenge.displayName = 'ShowDailyCodingChallenge';
|
||||
|
||||
export default DailyCodingChallenge;
|
||||
export default ShowDailyCodingChallenge;
|
||||
@@ -16,7 +16,7 @@ import { updateAllChallengesInfo } from '../../redux/actions';
|
||||
import { CertificateNode, ChallengeNode } from '../../redux/prop-types';
|
||||
import { getIsDailyCodingChallenge } from '../../../../shared/config/challenge-types';
|
||||
import {
|
||||
isValidDateParam,
|
||||
isValidDateString,
|
||||
formatDisplayDate
|
||||
} from '../daily-coding-challenge/helpers';
|
||||
import ProgressInner from './progress-inner';
|
||||
@@ -81,7 +81,7 @@ function Progress({
|
||||
const dateParam =
|
||||
new URLSearchParams(window.location.search).get('date') || '';
|
||||
|
||||
if (isValidDateParam(dateParam)) {
|
||||
if (isValidDateString(dateParam)) {
|
||||
blockTitle += `: ${formatDisplayDate(dateParam)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ function DailyCodingChallengeCalendarDay({
|
||||
// isAvailable -> render link to challenge
|
||||
return (
|
||||
<Link
|
||||
to={`/learn/daily-coding-challenge?date=${date}`}
|
||||
to={`/learn/daily-coding-challenge/${date}`}
|
||||
className='calendar-day available'
|
||||
data-playwright-test-label='calendar-day'
|
||||
aria-label={`${date && formatDisplayDate(date)}`}
|
||||
|
||||
@@ -238,7 +238,7 @@ function DailyCodingChallengeCalendar({
|
||||
|
||||
<Button
|
||||
block={true}
|
||||
href={`/learn/daily-coding-challenge?date=${todayUsCentral}`}
|
||||
href={`/learn/daily-coding-challenge/${todayUsCentral}`}
|
||||
>
|
||||
{t('buttons.go-to-today')}
|
||||
</Button>
|
||||
|
||||
@@ -20,7 +20,7 @@ export function getTodayUsCentral(dateObj: Date = new Date()) {
|
||||
|
||||
// Validate that dateString is in the format yyyy-MM-dd
|
||||
// Leading zero's are accepted for single digit month/day
|
||||
export function isValidDateParam(dateString: string) {
|
||||
export function isValidDateString(dateString: string) {
|
||||
const parsedDate = parse(dateString, 'yyyy-MM-dd', new Date());
|
||||
return isValid(parsedDate);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ function DailyCodingChallengeNotFound(): JSX.Element {
|
||||
<div className='button-wrapper'>
|
||||
<Button
|
||||
block={true}
|
||||
href={`/learn/daily-coding-challenge?date=${getTodayUsCentral()}`}
|
||||
href={`/learn/daily-coding-challenge/${getTodayUsCentral()}`}
|
||||
>
|
||||
{t(`buttons.go-to-today-long`)}
|
||||
</Button>
|
||||
|
||||
@@ -30,7 +30,7 @@ function DailyCodingChallengeWidget({
|
||||
block
|
||||
size='large'
|
||||
className='map-superblock-link'
|
||||
href={`/learn/daily-coding-challenge?date=${getTodayUsCentral()}`}
|
||||
href={`/learn/daily-coding-challenge/${getTodayUsCentral()}`}
|
||||
>
|
||||
<div className='daily-coding-challenge-button'>
|
||||
<DailyCodingChallengeIcon className='map-icon' />
|
||||
|
||||
@@ -114,6 +114,7 @@ interface DefaultLayoutProps extends StateProps, DispatchProps {
|
||||
showFooter?: boolean;
|
||||
isChallenge?: boolean;
|
||||
isDailyChallenge?: boolean;
|
||||
dailyChallengeParam?: string;
|
||||
usesMultifileEditor?: boolean;
|
||||
block?: string;
|
||||
examInProgress: boolean;
|
||||
@@ -133,6 +134,7 @@ function DefaultLayout({
|
||||
showFooter = true,
|
||||
isChallenge = false,
|
||||
isDailyChallenge = false,
|
||||
dailyChallengeParam,
|
||||
usesMultifileEditor,
|
||||
block,
|
||||
superBlock,
|
||||
@@ -292,7 +294,9 @@ function DefaultLayout({
|
||||
<SignoutModal />
|
||||
{isDailyChallenge ? (
|
||||
<div className='breadcrumbs-demo'>
|
||||
<DailyChallengeBreadCrumb />
|
||||
<DailyChallengeBreadCrumb
|
||||
dailyChallengeParam={dailyChallengeParam}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
isChallenge &&
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { withPrefix } from 'gatsby';
|
||||
import createRedirect from './create-redirect';
|
||||
|
||||
export default createRedirect(
|
||||
withPrefix('/learn/daily-coding-challenge/archive')
|
||||
);
|
||||
31
client/src/pages/learn/daily-coding-challenge/[...].tsx
Normal file
31
client/src/pages/learn/daily-coding-challenge/[...].tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
/* eslint-disable filenames-simple/naming-convention */
|
||||
import { Router } from '@gatsbyjs/reach-router';
|
||||
import { withPrefix } from 'gatsby';
|
||||
import React from 'react';
|
||||
|
||||
import ShowDailyCodingChallenge from '../../../client-only-routes/show-daily-coding-challenge';
|
||||
import RedirectToArchive from '../../../components/redirect-daily-challenge-archive';
|
||||
|
||||
const inlineStyles = {
|
||||
minHeight: 0,
|
||||
height: '100%'
|
||||
};
|
||||
|
||||
function DailyCodingChallengeAll(): JSX.Element {
|
||||
return (
|
||||
// Router adds an element around the editor, messing with the layout because the editor is a flex item
|
||||
// These few inline styles fix it.
|
||||
<Router style={inlineStyles}>
|
||||
<ShowDailyCodingChallenge
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={withPrefix('/learn/daily-coding-challenge/:date')}
|
||||
/>
|
||||
|
||||
<RedirectToArchive default />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
DailyCodingChallengeAll.displayName = 'DailyCodingChallengeAll';
|
||||
export default DailyCodingChallengeAll;
|
||||
@@ -5,21 +5,18 @@ import { Link } from '../../../components/helpers/index';
|
||||
|
||||
import './challenge-title.css';
|
||||
import {
|
||||
isValidDateParam,
|
||||
isValidDateString,
|
||||
formatDisplayDate
|
||||
} from '../../../components/daily-coding-challenge/helpers';
|
||||
|
||||
function DailyChallengeBreadCrumb(): JSX.Element {
|
||||
const dateParam =
|
||||
new URLSearchParams(window.location.search).get('date') || '';
|
||||
let displayDate = '';
|
||||
|
||||
if (isValidDateParam(dateParam)) {
|
||||
displayDate = formatDisplayDate(dateParam);
|
||||
}
|
||||
|
||||
function DailyChallengeBreadCrumb({
|
||||
dailyChallengeParam
|
||||
}: {
|
||||
dailyChallengeParam?: string;
|
||||
}): JSX.Element | null {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
|
||||
return dailyChallengeParam && isValidDateString(dailyChallengeParam) ? (
|
||||
<nav
|
||||
className='challenge-title-breadcrumbs'
|
||||
aria-label={t('aria.breadcrumb-nav')}
|
||||
@@ -32,12 +29,12 @@ function DailyChallengeBreadCrumb(): JSX.Element {
|
||||
</li>
|
||||
<li className='breadcrumb-right'>
|
||||
<Link to={`/learn/daily-coding-challenge/archive`}>
|
||||
{displayDate}
|
||||
{formatDisplayDate(dailyChallengeParam)}
|
||||
</Link>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
) : null;
|
||||
}
|
||||
|
||||
DailyChallengeBreadCrumb.displayName = 'DailyChallengeBreadCrumb';
|
||||
|
||||
@@ -35,7 +35,9 @@ function getComponentNameAndProps(
|
||||
location: {
|
||||
pathname
|
||||
},
|
||||
pageContext
|
||||
pageContext,
|
||||
params: { '*': '' },
|
||||
path: ''
|
||||
}
|
||||
});
|
||||
utils.render(<Provider store={store}>{LayoutReactComponent}</Provider>);
|
||||
|
||||
@@ -10,6 +10,8 @@ interface LayoutSelectorProps {
|
||||
data: { challengeNode?: { challenge?: { usesMultifileEditor?: boolean } } };
|
||||
location: { pathname: string };
|
||||
pageContext?: { challengeMeta?: { block?: string; superBlock?: string } };
|
||||
params: { '*'?: string };
|
||||
path: string;
|
||||
};
|
||||
}
|
||||
export default function layoutSelector({
|
||||
@@ -20,8 +22,8 @@ export default function layoutSelector({
|
||||
location: { pathname }
|
||||
} = props;
|
||||
|
||||
const isDailyChallenge =
|
||||
props.location.pathname === '/learn/daily-coding-challenge';
|
||||
const isDailyChallenge = props.path === '/learn/daily-coding-challenge/*';
|
||||
const dailyChallengeParam = props.params['*'];
|
||||
|
||||
const isChallenge = !!props.pageContext?.challengeMeta || isDailyChallenge;
|
||||
|
||||
@@ -42,6 +44,7 @@ export default function layoutSelector({
|
||||
showFooter={false}
|
||||
isChallenge={true}
|
||||
isDailyChallenge={isDailyChallenge}
|
||||
dailyChallengeParam={dailyChallengeParam}
|
||||
usesMultifileEditor={
|
||||
props.data?.challengeNode?.challenge?.usesMultifileEditor
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ const mockDaysInMonth = new Date(year, month, 0).getDate();
|
||||
|
||||
test.describe('Daily Coding Challenges', () => {
|
||||
test('should show not found page for invalid date', async ({ page }) => {
|
||||
await page.goto('/learn/daily-coding-challenge?date=invalid-date');
|
||||
await page.goto('/learn/daily-coding-challenge/invalid-date');
|
||||
await expect(
|
||||
page.getByText(/daily coding challenge not found\./i)
|
||||
).toBeVisible();
|
||||
@@ -83,7 +83,7 @@ test.describe('Daily Coding Challenges', () => {
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/learn/daily-coding-challenge?date=2025-01-01');
|
||||
await page.goto('/learn/daily-coding-challenge/2025-01-01');
|
||||
await expect(
|
||||
page.getByText(/daily coding challenge not found\./i)
|
||||
).toBeVisible();
|
||||
@@ -98,7 +98,7 @@ test.describe('Daily Coding Challenges', () => {
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/learn/daily-coding-challenge?date=2025-01-01');
|
||||
await page.goto('/learn/daily-coding-challenge/2025-01-01');
|
||||
await expect(
|
||||
page.getByText(/daily coding challenge not found\./i)
|
||||
).toBeVisible();
|
||||
@@ -115,7 +115,7 @@ test.describe('Daily Coding Challenges', () => {
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/learn/daily-coding-challenge?date=2025-06-27');
|
||||
await page.goto('/learn/daily-coding-challenge/2025-06-27');
|
||||
await expect(
|
||||
page.getByText(/daily coding challenge not found\./i)
|
||||
).toBeVisible();
|
||||
@@ -132,7 +132,7 @@ test.describe('Daily Coding Challenges', () => {
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(`/learn/daily-coding-challenge?date=${todayUsCentral}`);
|
||||
await page.goto(`/learn/daily-coding-challenge/${todayUsCentral}`);
|
||||
|
||||
await expect(page.getByText('Test title')).toBeVisible();
|
||||
|
||||
@@ -171,14 +171,35 @@ test.describe('Daily Coding Challenges', () => {
|
||||
'# Python seed code'
|
||||
);
|
||||
|
||||
await page.goto(`/learn/daily-coding-challenge?date=${todayUsCentral}`);
|
||||
await page.goto(`/learn/daily-coding-challenge/${todayUsCentral}`);
|
||||
|
||||
await expect(page.getByRole('button', { name: /main.py/i })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Daily Coding Challenge Archive', () => {
|
||||
test('should load and display the calendar', async ({ page }) => {
|
||||
test('/learn/daily-coding-challenge should redirect to archive', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto('/learn/daily-coding-challenge');
|
||||
await expect(page).toHaveURL('/learn/daily-coding-challenge/archive');
|
||||
});
|
||||
|
||||
test('/learn/daily-coding-challenge/ should redirect to archive', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto('/learn/daily-coding-challenge/');
|
||||
await expect(page).toHaveURL('/learn/daily-coding-challenge/archive');
|
||||
});
|
||||
|
||||
test('/learn/daily-coding-challenge/path-1/path2 should redirect to archive', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto('/learn/daily-coding-challenge/path-1/path2');
|
||||
await expect(page).toHaveURL('/learn/daily-coding-challenge/archive');
|
||||
});
|
||||
|
||||
test('archive should load and display the calendar', async ({ page }) => {
|
||||
await page.route(allRouteRe, async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
|
||||
Reference in New Issue
Block a user