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

View File

@@ -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;

View File

@@ -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)}`;
}
}

View File

@@ -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)}`}

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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' />

View File

@@ -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 &&

View File

@@ -0,0 +1,6 @@
import { withPrefix } from 'gatsby';
import createRedirect from './create-redirect';
export default createRedirect(
withPrefix('/learn/daily-coding-challenge/archive')
);

View 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;

View File

@@ -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';

View File

@@ -35,7 +35,9 @@ function getComponentNameAndProps(
location: {
pathname
},
pageContext
pageContext,
params: { '*': '' },
path: ''
}
});
utils.render(<Provider store={store}>{LayoutReactComponent}</Provider>);

View File

@@ -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
}

View File

@@ -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,