diff --git a/client/src/pages/learn/daily-coding-challenge.tsx b/client/src/client-only-routes/show-daily-coding-challenge.tsx similarity index 84% rename from client/src/pages/learn/daily-coding-challenge.tsx rename to client/src/client-only-routes/show-daily-coding-challenge.tsx index 30d1a7b7fd0..d00454eda27 100644 --- a/client/src/pages/learn/daily-coding-challenge.tsx +++ b/client/src/client-only-routes/show-daily-coding-challenge.tsx @@ -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 `
\n${str}\n
`; } @@ -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(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 ; @@ -230,6 +227,6 @@ function DailyCodingChallenge(): JSX.Element { ); } -DailyCodingChallenge.displayName = 'DailyCodingChallenge'; +ShowDailyCodingChallenge.displayName = 'ShowDailyCodingChallenge'; -export default DailyCodingChallenge; +export default ShowDailyCodingChallenge; diff --git a/client/src/components/Progress/progress.tsx b/client/src/components/Progress/progress.tsx index a05980918c9..68874e7a7c5 100644 --- a/client/src/components/Progress/progress.tsx +++ b/client/src/components/Progress/progress.tsx @@ -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)}`; } } diff --git a/client/src/components/daily-coding-challenge/calendar-day.tsx b/client/src/components/daily-coding-challenge/calendar-day.tsx index f76eae7b43d..91d5968a1b5 100644 --- a/client/src/components/daily-coding-challenge/calendar-day.tsx +++ b/client/src/components/daily-coding-challenge/calendar-day.tsx @@ -41,7 +41,7 @@ function DailyCodingChallengeCalendarDay({ // isAvailable -> render link to challenge return ( {t('buttons.go-to-today')} diff --git a/client/src/components/daily-coding-challenge/helpers.ts b/client/src/components/daily-coding-challenge/helpers.ts index 7ee7c6a5c3a..b01b67c9637 100644 --- a/client/src/components/daily-coding-challenge/helpers.ts +++ b/client/src/components/daily-coding-challenge/helpers.ts @@ -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); } diff --git a/client/src/components/daily-coding-challenge/not-found.tsx b/client/src/components/daily-coding-challenge/not-found.tsx index 4a76cca1256..293c99453a2 100644 --- a/client/src/components/daily-coding-challenge/not-found.tsx +++ b/client/src/components/daily-coding-challenge/not-found.tsx @@ -41,7 +41,7 @@ function DailyCodingChallengeNotFound(): JSX.Element {
diff --git a/client/src/components/daily-coding-challenge/widget.tsx b/client/src/components/daily-coding-challenge/widget.tsx index f0f3d048e6e..5fe9871b814 100644 --- a/client/src/components/daily-coding-challenge/widget.tsx +++ b/client/src/components/daily-coding-challenge/widget.tsx @@ -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()}`} >
diff --git a/client/src/components/layouts/default.tsx b/client/src/components/layouts/default.tsx index b9da69a81fc..4558baf8ad5 100644 --- a/client/src/components/layouts/default.tsx +++ b/client/src/components/layouts/default.tsx @@ -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({ {isDailyChallenge ? (
- +
) : ( isChallenge && diff --git a/client/src/components/redirect-daily-challenge-archive.tsx b/client/src/components/redirect-daily-challenge-archive.tsx new file mode 100644 index 00000000000..a302fc82bc8 --- /dev/null +++ b/client/src/components/redirect-daily-challenge-archive.tsx @@ -0,0 +1,6 @@ +import { withPrefix } from 'gatsby'; +import createRedirect from './create-redirect'; + +export default createRedirect( + withPrefix('/learn/daily-coding-challenge/archive') +); diff --git a/client/src/pages/learn/daily-coding-challenge/[...].tsx b/client/src/pages/learn/daily-coding-challenge/[...].tsx new file mode 100644 index 00000000000..bb583d185c5 --- /dev/null +++ b/client/src/pages/learn/daily-coding-challenge/[...].tsx @@ -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. + + + + + + ); +} + +DailyCodingChallengeAll.displayName = 'DailyCodingChallengeAll'; +export default DailyCodingChallengeAll; diff --git a/client/src/templates/Challenges/components/daily-challenge-bread-crumb.tsx b/client/src/templates/Challenges/components/daily-challenge-bread-crumb.tsx index a805f6dd259..f1636db2c12 100644 --- a/client/src/templates/Challenges/components/daily-challenge-bread-crumb.tsx +++ b/client/src/templates/Challenges/components/daily-challenge-bread-crumb.tsx @@ -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) ? ( - ); + ) : null; } DailyChallengeBreadCrumb.displayName = 'DailyChallengeBreadCrumb'; diff --git a/client/utils/gatsby/layout-selector.test.tsx b/client/utils/gatsby/layout-selector.test.tsx index f9ac06688f4..2b3af30379f 100644 --- a/client/utils/gatsby/layout-selector.test.tsx +++ b/client/utils/gatsby/layout-selector.test.tsx @@ -35,7 +35,9 @@ function getComponentNameAndProps( location: { pathname }, - pageContext + pageContext, + params: { '*': '' }, + path: '' } }); utils.render({LayoutReactComponent}); diff --git a/client/utils/gatsby/layout-selector.tsx b/client/utils/gatsby/layout-selector.tsx index 905493833f4..b2d6f44588e 100644 --- a/client/utils/gatsby/layout-selector.tsx +++ b/client/utils/gatsby/layout-selector.tsx @@ -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 } diff --git a/e2e/daily-coding-challenge.spec.ts b/e2e/daily-coding-challenge.spec.ts index f8c66f717d1..41d25fbaf20 100644 --- a/e2e/daily-coding-challenge.spec.ts +++ b/e2e/daily-coding-challenge.spec.ts @@ -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,