refactor(client): daily challenges to use path params (#61776)

This commit is contained in:
Tom
2025-08-12 01:39:52 -05:00
committed by GitHub
parent 8405f24a40
commit 7634b5c8a1
14 changed files with 114 additions and 53 deletions
@@ -1,23 +1,21 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useParams } from '@gatsbyjs/reach-router';
import store from 'store'; import store from 'store';
import ShowClassic from '../../templates/Challenges/classic/show'; import ShowClassic from '../templates/Challenges/classic/show';
import { Loader } from '../../components/helpers'; import { Loader } from '../components/helpers';
import { import {
DailyCodingChallengeLanguages, DailyCodingChallengeLanguages,
DailyCodingChallengeNode, DailyCodingChallengeNode,
DailyCodingChallengePageContext DailyCodingChallengePageContext
} from '../../redux/prop-types'; } from '../redux/prop-types';
import DailyCodingChallengeNotFound from '../../components/daily-coding-challenge/not-found'; import DailyCodingChallengeNotFound from '../components/daily-coding-challenge/not-found';
import FourOhFour from '../../components/FourOhFour'; import FourOhFour from '../components/FourOhFour';
import { import { apiLocation, showDailyCodingChallenges } from '../../config/env.json';
apiLocation, import { isValidDateString } from '../components/daily-coding-challenge/helpers';
showDailyCodingChallenges
} from '../../../config/env.json';
import { isValidDateParam } from '../../components/daily-coding-challenge/helpers';
import { import {
validateDailyCodingChallengeSchema, validateDailyCodingChallengeSchema,
type DailyCodingChallengeFromDb type DailyCodingChallengeFromDb
} from '../../utils/daily-coding-challenge-validator'; } from '../utils/daily-coding-challenge-validator';
interface DailyCodingChallengeLanguageData { interface DailyCodingChallengeLanguageData {
data: { data: {
@@ -31,7 +29,7 @@ interface DailyCodingChallengeDataFormatted {
python: DailyCodingChallengeLanguageData; 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) { function formatDescription(str: string) {
return `<section id="description">\n${str}\n</section>`; return `<section id="description">\n${str}\n</section>`;
} }
@@ -149,7 +147,9 @@ function formatChallengeData({
return props; return props;
} }
function DailyCodingChallenge(): JSX.Element { function ShowDailyCodingChallenge(): JSX.Element {
const { date } = useParams<{ date?: string }>();
const initLanguage = const initLanguage =
(store.get( (store.get(
'dailyCodingChallengeLanguage' 'dailyCodingChallengeLanguage'
@@ -162,9 +162,6 @@ function DailyCodingChallenge(): JSX.Element {
const [dailyCodingChallengeLanguage, setDailyCodingChallengeLanguage] = const [dailyCodingChallengeLanguage, setDailyCodingChallengeLanguage] =
useState<DailyCodingChallengeLanguages>(initLanguage); useState<DailyCodingChallengeLanguages>(initLanguage);
const dateParam =
new URLSearchParams(window.location.search).get('date') || '';
const fetchChallenge = async (date: string) => { const fetchChallenge = async (date: string) => {
try { try {
const response = await fetch( const response = await fetch(
@@ -201,15 +198,15 @@ function DailyCodingChallenge(): JSX.Element {
}; };
useEffect(() => { useEffect(() => {
// If dateParam is invalid, stop loading/fetching and show the not found page // If date is invalid, stop loading/fetching and show the not found page
if (!isValidDateParam(dateParam)) { if (!date || !isValidDateString(date)) {
setIsLoading(false); setIsLoading(false);
setChallengeFound(false); setChallengeFound(false);
return; return;
} }
void fetchChallenge(dateParam); void fetchChallenge(date);
}, [dateParam]); }, [date]);
if (!showDailyCodingChallenges) { if (!showDailyCodingChallenges) {
return <FourOhFour />; return <FourOhFour />;
@@ -230,6 +227,6 @@ function DailyCodingChallenge(): JSX.Element {
); );
} }
DailyCodingChallenge.displayName = 'DailyCodingChallenge'; ShowDailyCodingChallenge.displayName = 'ShowDailyCodingChallenge';
export default DailyCodingChallenge; export default ShowDailyCodingChallenge;
+2 -2
View File
@@ -16,7 +16,7 @@ import { updateAllChallengesInfo } from '../../redux/actions';
import { CertificateNode, ChallengeNode } from '../../redux/prop-types'; import { CertificateNode, ChallengeNode } from '../../redux/prop-types';
import { getIsDailyCodingChallenge } from '../../../../shared/config/challenge-types'; import { getIsDailyCodingChallenge } from '../../../../shared/config/challenge-types';
import { import {
isValidDateParam, isValidDateString,
formatDisplayDate formatDisplayDate
} from '../daily-coding-challenge/helpers'; } from '../daily-coding-challenge/helpers';
import ProgressInner from './progress-inner'; import ProgressInner from './progress-inner';
@@ -81,7 +81,7 @@ function Progress({
const dateParam = const dateParam =
new URLSearchParams(window.location.search).get('date') || ''; new URLSearchParams(window.location.search).get('date') || '';
if (isValidDateParam(dateParam)) { if (isValidDateString(dateParam)) {
blockTitle += `: ${formatDisplayDate(dateParam)}`; blockTitle += `: ${formatDisplayDate(dateParam)}`;
} }
} }
@@ -41,7 +41,7 @@ function DailyCodingChallengeCalendarDay({
// isAvailable -> render link to challenge // isAvailable -> render link to challenge
return ( return (
<Link <Link
to={`/learn/daily-coding-challenge?date=${date}`} to={`/learn/daily-coding-challenge/${date}`}
className='calendar-day available' className='calendar-day available'
data-playwright-test-label='calendar-day' data-playwright-test-label='calendar-day'
aria-label={`${date && formatDisplayDate(date)}`} aria-label={`${date && formatDisplayDate(date)}`}
@@ -238,7 +238,7 @@ function DailyCodingChallengeCalendar({
<Button <Button
block={true} block={true}
href={`/learn/daily-coding-challenge?date=${todayUsCentral}`} href={`/learn/daily-coding-challenge/${todayUsCentral}`}
> >
{t('buttons.go-to-today')} {t('buttons.go-to-today')}
</Button> </Button>
@@ -20,7 +20,7 @@ export function getTodayUsCentral(dateObj: Date = new Date()) {
// Validate that dateString is in the format yyyy-MM-dd // Validate that dateString is in the format yyyy-MM-dd
// Leading zero's are accepted for single digit month/day // 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()); const parsedDate = parse(dateString, 'yyyy-MM-dd', new Date());
return isValid(parsedDate); return isValid(parsedDate);
} }
@@ -41,7 +41,7 @@ function DailyCodingChallengeNotFound(): JSX.Element {
<div className='button-wrapper'> <div className='button-wrapper'>
<Button <Button
block={true} block={true}
href={`/learn/daily-coding-challenge?date=${getTodayUsCentral()}`} href={`/learn/daily-coding-challenge/${getTodayUsCentral()}`}
> >
{t(`buttons.go-to-today-long`)} {t(`buttons.go-to-today-long`)}
</Button> </Button>
@@ -30,7 +30,7 @@ function DailyCodingChallengeWidget({
block block
size='large' size='large'
className='map-superblock-link' className='map-superblock-link'
href={`/learn/daily-coding-challenge?date=${getTodayUsCentral()}`} href={`/learn/daily-coding-challenge/${getTodayUsCentral()}`}
> >
<div className='daily-coding-challenge-button'> <div className='daily-coding-challenge-button'>
<DailyCodingChallengeIcon className='map-icon' /> <DailyCodingChallengeIcon className='map-icon' />
+5 -1
View File
@@ -114,6 +114,7 @@ interface DefaultLayoutProps extends StateProps, DispatchProps {
showFooter?: boolean; showFooter?: boolean;
isChallenge?: boolean; isChallenge?: boolean;
isDailyChallenge?: boolean; isDailyChallenge?: boolean;
dailyChallengeParam?: string;
usesMultifileEditor?: boolean; usesMultifileEditor?: boolean;
block?: string; block?: string;
examInProgress: boolean; examInProgress: boolean;
@@ -133,6 +134,7 @@ function DefaultLayout({
showFooter = true, showFooter = true,
isChallenge = false, isChallenge = false,
isDailyChallenge = false, isDailyChallenge = false,
dailyChallengeParam,
usesMultifileEditor, usesMultifileEditor,
block, block,
superBlock, superBlock,
@@ -292,7 +294,9 @@ function DefaultLayout({
<SignoutModal /> <SignoutModal />
{isDailyChallenge ? ( {isDailyChallenge ? (
<div className='breadcrumbs-demo'> <div className='breadcrumbs-demo'>
<DailyChallengeBreadCrumb /> <DailyChallengeBreadCrumb
dailyChallengeParam={dailyChallengeParam}
/>
</div> </div>
) : ( ) : (
isChallenge && isChallenge &&
@@ -0,0 +1,6 @@
import { withPrefix } from 'gatsby';
import createRedirect from './create-redirect';
export default createRedirect(
withPrefix('/learn/daily-coding-challenge/archive')
);
@@ -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 './challenge-title.css';
import { import {
isValidDateParam, isValidDateString,
formatDisplayDate formatDisplayDate
} from '../../../components/daily-coding-challenge/helpers'; } from '../../../components/daily-coding-challenge/helpers';
function DailyChallengeBreadCrumb(): JSX.Element { function DailyChallengeBreadCrumb({
const dateParam = dailyChallengeParam
new URLSearchParams(window.location.search).get('date') || ''; }: {
let displayDate = ''; dailyChallengeParam?: string;
}): JSX.Element | null {
if (isValidDateParam(dateParam)) {
displayDate = formatDisplayDate(dateParam);
}
const { t } = useTranslation(); const { t } = useTranslation();
return (
return dailyChallengeParam && isValidDateString(dailyChallengeParam) ? (
<nav <nav
className='challenge-title-breadcrumbs' className='challenge-title-breadcrumbs'
aria-label={t('aria.breadcrumb-nav')} aria-label={t('aria.breadcrumb-nav')}
@@ -32,12 +29,12 @@ function DailyChallengeBreadCrumb(): JSX.Element {
</li> </li>
<li className='breadcrumb-right'> <li className='breadcrumb-right'>
<Link to={`/learn/daily-coding-challenge/archive`}> <Link to={`/learn/daily-coding-challenge/archive`}>
{displayDate} {formatDisplayDate(dailyChallengeParam)}
</Link> </Link>
</li> </li>
</ol> </ol>
</nav> </nav>
); ) : null;
} }
DailyChallengeBreadCrumb.displayName = 'DailyChallengeBreadCrumb'; DailyChallengeBreadCrumb.displayName = 'DailyChallengeBreadCrumb';
+3 -1
View File
@@ -35,7 +35,9 @@ function getComponentNameAndProps(
location: { location: {
pathname pathname
}, },
pageContext pageContext,
params: { '*': '' },
path: ''
} }
}); });
utils.render(<Provider store={store}>{LayoutReactComponent}</Provider>); utils.render(<Provider store={store}>{LayoutReactComponent}</Provider>);
+5 -2
View File
@@ -10,6 +10,8 @@ interface LayoutSelectorProps {
data: { challengeNode?: { challenge?: { usesMultifileEditor?: boolean } } }; data: { challengeNode?: { challenge?: { usesMultifileEditor?: boolean } } };
location: { pathname: string }; location: { pathname: string };
pageContext?: { challengeMeta?: { block?: string; superBlock?: string } }; pageContext?: { challengeMeta?: { block?: string; superBlock?: string } };
params: { '*'?: string };
path: string;
}; };
} }
export default function layoutSelector({ export default function layoutSelector({
@@ -20,8 +22,8 @@ export default function layoutSelector({
location: { pathname } location: { pathname }
} = props; } = props;
const isDailyChallenge = const isDailyChallenge = props.path === '/learn/daily-coding-challenge/*';
props.location.pathname === '/learn/daily-coding-challenge'; const dailyChallengeParam = props.params['*'];
const isChallenge = !!props.pageContext?.challengeMeta || isDailyChallenge; const isChallenge = !!props.pageContext?.challengeMeta || isDailyChallenge;
@@ -42,6 +44,7 @@ export default function layoutSelector({
showFooter={false} showFooter={false}
isChallenge={true} isChallenge={true}
isDailyChallenge={isDailyChallenge} isDailyChallenge={isDailyChallenge}
dailyChallengeParam={dailyChallengeParam}
usesMultifileEditor={ usesMultifileEditor={
props.data?.challengeNode?.challenge?.usesMultifileEditor props.data?.challengeNode?.challenge?.usesMultifileEditor
} }
+28 -7
View File
@@ -66,7 +66,7 @@ const mockDaysInMonth = new Date(year, month, 0).getDate();
test.describe('Daily Coding Challenges', () => { test.describe('Daily Coding Challenges', () => {
test('should show not found page for invalid date', async ({ page }) => { 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( await expect(
page.getByText(/daily coding challenge not found\./i) page.getByText(/daily coding challenge not found\./i)
).toBeVisible(); ).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( await expect(
page.getByText(/daily coding challenge not found\./i) page.getByText(/daily coding challenge not found\./i)
).toBeVisible(); ).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( await expect(
page.getByText(/daily coding challenge not found\./i) page.getByText(/daily coding challenge not found\./i)
).toBeVisible(); ).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( await expect(
page.getByText(/daily coding challenge not found\./i) page.getByText(/daily coding challenge not found\./i)
).toBeVisible(); ).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(); await expect(page.getByText('Test title')).toBeVisible();
@@ -171,14 +171,35 @@ test.describe('Daily Coding Challenges', () => {
'# Python seed code' '# 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(); await expect(page.getByRole('button', { name: /main.py/i })).toBeVisible();
}); });
}); });
test.describe('Daily Coding Challenge Archive', () => { 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 page.route(allRouteRe, async route => {
await route.fulfill({ await route.fulfill({
status: 200, status: 200,