Skip to content

Commit bde1e6f

Browse files
moT01huyenltnguyen
andauthored
feat(client): add daily challenges (freeCodeCamp#60867)
Co-authored-by: Huyen Nguyen <[email protected]>
1 parent 2cdd62b commit bde1e6f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1607
-114
lines changed

client/i18n/locales/english/intro.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4770,6 +4770,14 @@
47704770
}
47714771
}
47724772
},
4773+
"daily-coding-challenge": {
4774+
"title": "Daily Coding Challenge",
4775+
"blocks": {
4776+
"daily-coding-challenge": {
4777+
"title": "Daily Coding Challenge"
4778+
}
4779+
}
4780+
},
47734781
"misc-text": {
47744782
"browse-other": "Browse our other free certifications",
47754783
"courses": "Courses",

client/i18n/locales/english/translations.json

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,38 @@
117117
"share-on-bluesky": "Share on BlueSky",
118118
"share-on-threads": "Share on Threads",
119119
"play-scene": "Press Play",
120-
"download-latest-version": "Download the Latest Version"
120+
"download-latest-version": "Download the Latest Version",
121+
"start": "Start",
122+
"go-to-today": "Go to Today's Challenge",
123+
"go-to-today-long": "Go to Today's Coding Challenge",
124+
"go-to-archive": "Go to Archive",
125+
"go-to-archive-long": "Go to Daily Coding Challenge Archive"
126+
},
127+
"daily-coding-challenges": {
128+
"title": "Daily Coding Challenges",
129+
"map-title": "Try the coding challenge of the day:",
130+
"not-found": "Daily Coding Challenge Not Found.",
131+
"release-note": "New challenges are released at midnight US Central time."
132+
},
133+
"weekdays": {
134+
"short": {
135+
"sunday": "S",
136+
"monday": "M",
137+
"tuesday": "T",
138+
"wednesday": "W",
139+
"thursday": "T",
140+
"friday": "F",
141+
"saturday": "S"
142+
},
143+
"long": {
144+
"sunday": "Sunday",
145+
"monday": "Monday",
146+
"tuesday": "Tuesday",
147+
"wednesday": "Wednesday",
148+
"thursday": "Thursday",
149+
"friday": "Friday",
150+
"saturday": "Saturday"
151+
}
121152
},
122153
"landing": {
123154
"big-heading-1": "Learn to code — for free.",
@@ -851,6 +882,8 @@
851882
"github": "Link to {{username}}'s GitHub",
852883
"website": "Link to {{username}}'s website",
853884
"twitter": "Link to {{username}}'s Twitter",
885+
"next-month": "Go to next month",
886+
"previous-month": "Go to previous month",
854887
"first-page": "Go to first page",
855888
"previous-page": "Go to previous page",
856889
"next-page": "Go to next page",
@@ -880,7 +913,8 @@
880913
"editor-a11y-off-non-macos": "{{editorName}} editor content. Press Alt+F1 for accessibility options.",
881914
"editor-a11y-on-macos": "{{editorName}} editor content. Accessibility mode set to 'on'. Press Command+E to disable or press Option+F1 for more options.",
882915
"editor-a11y-on-non-macos": "{{editorName}} editor content. Accessibility mode set to 'on'. Press Ctrl+E to disable or press Alt+F1 for more options.",
883-
"terminal-output": "Terminal output"
916+
"terminal-output": "Terminal output",
917+
"not-available": "Not available"
884918
},
885919
"flash": {
886920
"no-email-in-userinfo": "We could not retrieve an email from your chosen provider. Please try another provider or use the 'Continue with Email' option.",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react';
2+
3+
function CalendarIcon(
4+
props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>
5+
): JSX.Element {
6+
return (
7+
<svg
8+
aria-hidden='true'
9+
viewBox='0 0 448 512'
10+
fill='none'
11+
xmlns='http://www.w3.org/2000/svg'
12+
{...props}
13+
>
14+
<path d='M128 0c17.7 0 32 14.3 32 32l0 32 128 0 0-32c0-17.7 14.3-32 32-32s32 14.3 32 32l0 32 48 0c26.5 0 48 21.5 48 48l0 48L0 160l0-48C0 85.5 21.5 64 48 64l48 0 0-32c0-17.7 14.3-32 32-32zM0 192l448 0 0 272c0 26.5-21.5 48-48 48L48 512c-26.5 0-48-21.5-48-48L0 192zm64 80l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16zm128 0l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16zm144-16c-8.8 0-16 7.2-16 16l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0zM64 400l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16zm144-16c-8.8 0-16 7.2-16 16l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0zm112 16l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16z' />
15+
</svg>
16+
);
17+
}
18+
19+
CalendarIcon.displayName = 'CalendarIcon';
20+
21+
export default CalendarIcon;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react';
2+
3+
function DailyCodingChallengeIcon(
4+
props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>
5+
): JSX.Element {
6+
return (
7+
<svg
8+
aria-hidden='true'
9+
viewBox='0 0 512 512'
10+
fill='none'
11+
xmlns='http://www.w3.org/2000/svg'
12+
{...props}
13+
>
14+
<path d='M152.1 38.2c9.9 8.9 10.7 24 1.8 33.9l-72 80c-4.4 4.9-10.6 7.8-17.2 7.9s-12.9-2.4-17.6-7L7 113C-2.3 103.6-2.3 88.4 7 79s24.6-9.4 33.9 0l22.1 22.1 55.1-61.2c8.9-9.9 24-10.7 33.9-1.8zm0 160c9.9 8.9 10.7 24 1.8 33.9l-72 80c-4.4 4.9-10.6 7.8-17.2 7.9s-12.9-2.4-17.6-7L7 273c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l22.1 22.1 55.1-61.2c8.9-9.9 24-10.7 33.9-1.8zM224 96c0-17.7 14.3-32 32-32l224 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-224 0c-17.7 0-32-14.3-32-32zm0 160c0-17.7 14.3-32 32-32l224 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-224 0c-17.7 0-32-14.3-32-32zM160 416c0-17.7 14.3-32 32-32l288 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-288 0c-17.7 0-32-14.3-32-32zM48 368a48 48 0 1 1 0 96 48 48 0 1 1 0-96z' />
15+
</svg>
16+
);
17+
}
18+
19+
DailyCodingChallengeIcon.displayName = 'DailyCodingChallengeIcon';
20+
21+
export default DailyCodingChallengeIcon;

client/src/components/Map/index.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import {
1212
import { SuperBlockIcon } from '../../assets/superblock-icon';
1313
import LinkButton from '../../assets/icons/link-button';
1414
import { ButtonLink } from '../helpers';
15-
import { showUpcomingChanges } from '../../../config/env.json';
15+
import {
16+
showUpcomingChanges,
17+
showDailyCodingChallenges
18+
} from '../../../config/env.json';
19+
import DailyCodingChallengeWidget from '../daily-coding-challenge/widget';
1620

1721
import './map.css';
1822
interface MapProps {
@@ -82,6 +86,16 @@ function Map({ forLanding = false }: MapProps) {
8286

8387
return (
8488
<Fragment key={stage}>
89+
{
90+
/* Show the daily coding challenge before the "English" curriculum */
91+
showDailyCodingChallenges &&
92+
stage === SuperBlockStage.English && (
93+
<>
94+
<DailyCodingChallengeWidget forLanding={forLanding} />
95+
<Spacer size='m' />
96+
</>
97+
)
98+
}
8599
<h2 className={forLanding ? 'big-heading' : ''}>
86100
{t(superBlockHeadings[stage])}
87101
</h2>

client/src/components/Progress/progress-inner.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,24 @@ function ProgressInner({
4141
const [shownPercent, setShownPercent] = useState(0);
4242
const [lastShownPercent, setLastShownPercent] = useState(0);
4343
const progressInnerWrap = useRef<HTMLDivElement>(null);
44+
const intervalRef = useRef<number | null>(null);
4445
const isProgressInViewport = useIsInViewport(progressInnerWrap);
4546

4647
const animateProgressInner = (completedPercent: number) => {
48+
// Clear any existing interval
49+
if (intervalRef.current) {
50+
clearInterval(intervalRef.current);
51+
intervalRef.current = null;
52+
}
53+
4754
if (completedPercent > 100) completedPercent = 100;
4855
if (completedPercent < 0) completedPercent = 0;
4956

5057
const transitionLength = completedPercent * 10 + 750;
5158
const intervalsToFinish = transitionLength / intervalLength;
5259
const amountPerInterval = completedPercent / intervalsToFinish;
5360

54-
const myInterval = window.setInterval(() => {
61+
intervalRef.current = window.setInterval(() => {
5562
percent += amountPerInterval;
5663

5764
if (percent > completedPercent) percent = completedPercent;
@@ -61,10 +68,14 @@ function ProgressInner({
6168
);
6269
if (percent >= completedPercent) {
6370
percent = 0;
64-
clearInterval(myInterval);
71+
if (intervalRef.current) {
72+
clearInterval(intervalRef.current);
73+
intervalRef.current = null;
74+
}
6575
}
6676
}, intervalLength);
6777
};
78+
6879
useEffect(() => {
6980
if (lastShownPercent !== completedPercent && isProgressInViewport) {
7081
setLastShownPercent(completedPercent);
@@ -73,6 +84,16 @@ function ProgressInner({
7384
// eslint-disable-next-line react-hooks/exhaustive-deps
7485
}, [isProgressInViewport]);
7586

87+
// Cleanup interval on unmount
88+
useEffect(() => {
89+
return () => {
90+
if (intervalRef.current) {
91+
clearInterval(intervalRef.current);
92+
intervalRef.current = null;
93+
}
94+
};
95+
}, []);
96+
7697
return (
7798
<>
7899
<div className='completion-block-name'>{title}</div>

client/src/components/Progress/progress.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import {
1414
import { liveCerts } from '../../../config/cert-and-project-map';
1515
import { updateAllChallengesInfo } from '../../redux/actions';
1616
import { CertificateNode, ChallengeNode } from '../../redux/prop-types';
17+
import { getIsDailyCodingChallenge } from '../../../../shared/config/challenge-types';
18+
import {
19+
isValidDateParam,
20+
formatDisplayDate
21+
} from '../daily-coding-challenge/helpers';
1722
import ProgressInner from './progress-inner';
1823

1924
const mapStateToProps = createSelector(
@@ -24,10 +29,12 @@ const mapStateToProps = createSelector(
2429
(
2530
currentBlockIds: string[],
2631
{
32+
challengeType,
2733
id,
2834
block,
2935
superBlock
3036
}: {
37+
challengeType: number;
3138
id: string;
3239
block: string;
3340
superBlock: string;
@@ -36,6 +43,7 @@ const mapStateToProps = createSelector(
3643
completedPercent: number
3744
) => ({
3845
currentBlockIds,
46+
challengeType,
3947
id,
4048
block,
4149
superBlock,
@@ -56,17 +64,28 @@ function Progress({
5664
block,
5765
id,
5866
superBlock,
67+
challengeType,
5968
completedChallengesInBlock,
6069
completedPercent,
6170
t,
6271
updateAllChallengesInfo
6372
}: ProgressProps): JSX.Element {
64-
const blockTitle = t(`intro:${superBlock}.blocks.${block}.title`);
73+
let blockTitle = t(`intro:${superBlock}.blocks.${block}.title`);
6574
// Always false for legacy full stack, since it has no projects.
6675
const isCertificationProject = liveCerts.some(cert =>
6776
cert.projects?.some((project: { id: string }) => project.id === id)
6877
);
6978

79+
// Display the date of the challenge in the completion modal for daily challenges
80+
if (getIsDailyCodingChallenge(challengeType)) {
81+
const dateParam =
82+
new URLSearchParams(window.location.search).get('date') || '';
83+
84+
if (isValidDateParam(dateParam)) {
85+
blockTitle += `: ${formatDisplayDate(dateParam)}`;
86+
}
87+
}
88+
7089
const { challengeNodes, certificateNodes } = useGetAllBlockIds();
7190
useEffect(() => {
7291
updateAllChallengesInfo({ challengeNodes, certificateNodes });
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { Link } from '../helpers';
4+
import GreenPass from '../../assets/icons/green-pass';
5+
import GreenNotCompleted from '../../assets/icons/green-not-completed';
6+
import { formatDisplayDate } from './helpers';
7+
8+
interface CalendarDayProps {
9+
dayNumber: number;
10+
date?: string;
11+
isCompleted?: boolean;
12+
isAvailable?: boolean;
13+
}
14+
15+
// Todo: Change this to render checkmarks for JS and Python
16+
17+
function DailyCodingChallengeCalendarDay({
18+
dayNumber,
19+
date,
20+
isCompleted = false,
21+
isAvailable = false
22+
}: CalendarDayProps): JSX.Element {
23+
const { t } = useTranslation();
24+
// dayNumber = 0 -> render nothing
25+
if (dayNumber === 0) return <div></div>;
26+
27+
if (!isAvailable)
28+
return (
29+
<button
30+
disabled
31+
className='calendar-day not-available'
32+
aria-label={`${date && formatDisplayDate(date)}, (${t('aria.not-available')})`}
33+
>
34+
<span className='calendar-day-number' aria-hidden='true'>
35+
{dayNumber}
36+
</span>
37+
</button>
38+
);
39+
40+
// isAvailable -> render link to challenge
41+
return (
42+
<Link
43+
to={`/learn/daily-coding-challenge?date=${date}`}
44+
className='calendar-day available'
45+
aria-label={`${date && formatDisplayDate(date)}`}
46+
>
47+
<span className='calendar-day-number' aria-hidden='true'>
48+
{dayNumber}
49+
</span>
50+
51+
{isCompleted ? (
52+
<span className='completed'>
53+
<GreenPass />
54+
</span>
55+
) : (
56+
<span className='not-completed'>
57+
<GreenNotCompleted />
58+
</span>
59+
)}
60+
</Link>
61+
);
62+
}
63+
64+
DailyCodingChallengeCalendarDay.displayName = 'DailyCodingChallengeCalendarDay';
65+
66+
export default DailyCodingChallengeCalendarDay;

0 commit comments

Comments
 (0)