feat(client): add details to daily challenges calendar (#63003)

This commit is contained in:
Tom
2025-11-17 01:42:00 -06:00
committed by GitHub
parent 3653717d9a
commit 981c6024f6
6 changed files with 278 additions and 91 deletions
@@ -1,26 +1,50 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Spacer } from '@freecodecamp/ui';
import { Link } from '../helpers';
import GreenPass from '../../assets/icons/green-pass';
import GreenNotCompleted from '../../assets/icons/green-not-completed';
import JavaScriptIcon from '../../assets/icons/javascript';
import PythonIcon from '../../assets/icons/python';
import { formatDisplayDate } from './helpers';
interface CalendarDayProps {
dayNumber: number;
date?: string;
isCompleted?: boolean;
challengeNumber?: number;
completedLanguages?: string[];
isAvailable?: boolean;
title?: string;
}
// Todo: Change this to render checkmarks for JS and Python
function Checkmark({ completed }: { completed: boolean }): JSX.Element {
return completed ? (
<span
className='dc-checkmark dc-small-checkmark completed'
data-playwright-test-label='calendar-day-completed'
>
<GreenPass />
</span>
) : (
<span
className='dc-checkmark dc-small-checkmark not-completed'
data-playwright-test-label='calendar-day-not-completed'
>
<GreenNotCompleted />
</span>
);
}
function DailyCodingChallengeCalendarDay({
dayNumber,
date,
isCompleted = false,
isAvailable = false
isAvailable = false,
title,
completedLanguages = [],
challengeNumber
}: CalendarDayProps): JSX.Element {
const { t } = useTranslation();
// dayNumber = 0 -> render nothing
if (dayNumber === 0) return <div></div>;
@@ -50,21 +74,41 @@ function DailyCodingChallengeCalendarDay({
{dayNumber}
</span>
{isCompleted ? (
<span
className='completed'
data-playwright-test-label='calendar-day-completed'
>
<GreenPass />
</span>
) : (
<span
className='not-completed'
data-playwright-test-label='calendar-day-not-completed'
>
<GreenNotCompleted />
</span>
)}
<div className='dc-number'>#{challengeNumber}</div>
<div className='dc-info'>
<div className='dc-title'>{title}</div>
{completedLanguages.length === 2 ? (
<span className='dc-checkmark dc-big-checkmark completed'>
<span className='dc-spacer'>
<Spacer size='s' />
</span>
<GreenPass />
</span>
) : (
<div className='dc-languages'>
<hr />
<div className='dc-language'>
<div className='dc-language-icon'>
<JavaScriptIcon />
</div>
<div className='dc-language-name'>JavaScript</div>
<Checkmark
completed={completedLanguages.includes('javascript')}
/>
</div>
<div className='dc-language'>
<div className='dc-language-icon'>
<PythonIcon />
</div>
<div className='dc-language-name'>Python</div>
<Checkmark completed={completedLanguages.includes('python')} />
</div>
</div>
)}
</div>
</Link>
);
}
@@ -2,6 +2,9 @@
display: grid;
grid-template-columns: repeat(7, 1fr);
text-align: center;
margin: 0 auto;
max-width: 1500px;
padding: 0 20px;
}
.calendar-head {
@@ -18,18 +21,85 @@
display: grid;
grid-template-columns: repeat(7, 1fr); /* 7 columns for days of the week */
gap: 4px;
margin: 0 auto;
max-width: 1500px;
padding: 0 20px;
}
.calendar-day {
display: flex;
justify-content: center;
align-items: center;
position: relative;
padding: 10px;
min-height: 100px;
padding: 10px 10px 20px;
min-height: 200px;
border: 1px solid var(--tertiary-color);
text-align: center;
background-color: var(--primary-background);
text-decoration: none;
}
.calendar-day-number {
position: absolute;
top: 0;
left: 5px;
}
.dc-number {
font-style: italic;
color: var(--gray-45);
text-align: center;
}
.dc-info {
display: flex;
flex-direction: column;
height: 100%;
}
.dc-title {
width: 100%;
text-align: center;
align-self: center;
margin-top: 5px;
height: 50px;
margin-bottom: -5px;
}
.dc-languages {
padding: 0 15px;
}
.dc-language {
font-size: 0.8rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
}
.dc-language-icon {
fill: var(--primary-color);
}
.dc-language-icon svg {
width: 25px;
height: 25px;
}
.dc-small-checkmark {
position: relative;
/* top: -5px; */
}
.dc-small-checkmark svg {
width: 20px;
height: 20px;
}
.dc-big-checkmark {
margin: 0 auto;
}
.dc-big-checkmark svg {
width: 50px;
height: 50px;
}
.not-available:hover,
@@ -38,22 +108,24 @@
background-color: var(--primary-background);
}
.not-available:hover .calendar-day-number,
.not-available:active .calendar-day-number {
color: var(--primary-color);
}
.available:hover,
.available:active {
cursor: pointer;
background-color: var(--primary-color);
}
.not-available:hover .calendar-day-number,
.not-available:active .calendar-day-number {
color: var(--primary-color);
}
.available:hover .calendar-day-number,
.available:active .calendar-day-number {
color: var(--primary-background);
}
.available:hover .dc-language-icon svg path,
.available:active .dc-language-icon svg path,
.available:hover .completed svg circle,
.available:active .completed svg circle {
fill: var(--primary-background);
@@ -72,27 +144,16 @@
stroke: var(--primary-color);
}
.calendar-day-number {
position: absolute;
top: 0;
left: 5px;
.available:hover .dc-title,
.available:active .dc-title,
.available:hover .dc-language-name,
.available:active .dc-language-name {
color: var(--primary-background);
}
.calendar-day svg,
.empty-cirle {
width: calc(10px + 2vw);
height: calc(10px + 2vw);
max-width: 40px;
max-height: 40px;
}
.empty-cirle {
width: calc(10px + 2vw);
height: calc(10px + 2vw);
max-width: 40px;
max-height: 40px;
border-radius: 50%;
border: 2px solid var(--primary-color);
.available:hover .dc-number,
.available:active .dc-number {
color: var(--quaternary-background);
}
@media (max-width: 500px) {
@@ -100,3 +161,75 @@
min-height: 75px;
}
}
@media (max-width: 1345px) {
.calendar-day-number,
.dc-number,
.dc-title {
font-size: 0.8rem;
}
.dc-info hr {
margin: 10px 0;
}
.calendar-day {
min-height: 170px;
}
.dc-language {
justify-content: center;
}
.dc-language-name {
display: none;
}
}
@media (max-width: 1115px) {
.calendar-grid {
max-width: unset;
}
.calendar-day {
min-height: 125px;
padding: 10px;
}
.dc-title,
.dc-info hr {
display: none;
}
.dc-info {
display: flex;
justify-content: center;
align-items: center;
height: 80%;
}
.dc-spacer {
display: none;
}
}
@media (max-width: 815px) {
.calendar-day {
min-height: 100px;
padding: 0;
}
.dc-number {
text-align: right;
padding-right: 5px;
}
.dc-languages {
padding: 0;
}
.dc-big-checkmark svg {
width: 40px;
height: 40px;
}
}
@@ -1,12 +1,15 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Button, Callout, Col, Spacer } from '@freecodecamp/ui';
import { Button, Callout, Container, Col, Row, Spacer } from '@freecodecamp/ui';
import {
completedDailyCodingChallengesSelector,
isSignedInSelector
} from '../../redux/selectors';
import { CompletedDailyCodingChallenge } from '../../redux/prop-types';
import {
CompletedDailyCodingChallenge,
DailyCodingChallengeLanguages
} from '../../redux/prop-types';
import { Loader } from '../helpers';
import envData from '../../../config/env.json';
import Login from '../Header/components/login';
@@ -40,9 +43,9 @@ interface AllDailyChallengeFromDb {
interface DailyChallengeMap {
id: string;
date: string;
isCompleted: boolean;
challengeNumber: number;
title: string;
completedLanguages: DailyCodingChallengeLanguages[];
}
type DailyChallengesMap = Map<string, DailyChallengeMap>;
@@ -85,16 +88,20 @@ const getMonthInfo = (
});
const challengeData = dailyChallengesMap.get(formattedDate);
const isCompleted = challengeData?.isCompleted || false;
const completedLanguages = challengeData?.completedLanguages || [];
const title = challengeData?.title || '';
const isAvailable = challengeData !== undefined;
const challengeNumber = challengeData?.challengeNumber;
days.push(
<CalendarDay
key={`day-${day}`}
date={formattedDate}
dayNumber={day}
isCompleted={isCompleted}
challengeNumber={challengeNumber}
title={title}
isAvailable={isAvailable}
completedLanguages={completedLanguages}
/>
);
}
@@ -118,10 +125,6 @@ function DailyCodingChallengeCalendar({
const todayUsCentral = getTodayUsCentral();
const completedDailyCodingChallengeIds = completedDailyCodingChallenges.map(
c => c.id
);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(false);
const [monthInfo, setMonthInfo] = useState<MonthInfo | null>(null);
@@ -145,7 +148,9 @@ function DailyCodingChallengeCalendar({
newDailyChallengesMap.set(date, {
...c,
date,
isCompleted: completedDailyCodingChallengeIds.includes(c.id)
completedLanguages:
completedDailyCodingChallenges.find(ch => ch.id === c.id)
?.languages ?? []
});
});
@@ -231,18 +236,22 @@ function DailyCodingChallengeCalendar({
return (
<>
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
<Callout variant='info'>
{t('daily-coding-challenges.release-note')}
</Callout>
<Container>
<Row>
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
<Callout variant='info'>
{t('daily-coding-challenges.release-note')}
</Callout>
<Button
block={true}
href={`/learn/daily-coding-challenge/${todayUsCentral}`}
>
{t('buttons.go-to-dcc-today')}
</Button>
</Col>
<Button
block={true}
href={`/learn/daily-coding-challenge/${todayUsCentral}`}
>
{t('buttons.go-to-dcc-today')}
</Button>
</Col>
</Row>
</Container>
<Spacer size='l' />
@@ -9,25 +9,26 @@ function Archive(): JSX.Element {
const { t } = useTranslation();
return (
<Container>
<Row>
<Col md={12} sm={12} xs={12}>
<Spacer size='l' />
<h1 className='text-center big-heading'>
{t('daily-coding-challenges.title')}
</h1>
<Spacer size='m' />
<DailyCodingChallengeIcon className='cert-header-icon' />
<Spacer size='l' />
<DailyCodingChallengeCalendar />
</Col>
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
<Spacer size='l' />
<Map />
<Spacer size='l' />
</Col>
</Row>
</Container>
<>
<Spacer size='l' />
<h1 className='text-center big-heading'>
{t('daily-coding-challenges.title')}
</h1>
<Spacer size='m' />
<DailyCodingChallengeIcon className='cert-header-icon' />
<Spacer size='l' />
<DailyCodingChallengeCalendar />
<Container>
<Row>
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
<Spacer size='l' />
<Map />
<Spacer size='l' />
</Col>
</Row>
</Container>
</>
);
}
+1 -1
View File
@@ -331,7 +331,7 @@ export type DailyCodingChallengeLanguages = 'javascript' | 'python';
export interface CompletedDailyCodingChallenge {
id: string;
completedDate: number;
completedLanguages: DailyCodingChallengeLanguages[];
languages: DailyCodingChallengeLanguages[];
}
type Quiz = {
+1 -1
View File
@@ -251,6 +251,6 @@ test.describe('Daily Coding Challenge Archive', () => {
await expect(page.getByTestId('calendar-day-completed')).toHaveCount(1);
await expect(page.getByTestId('calendar-day-not-completed')).toHaveCount(1);
await expect(page.getByTestId('calendar-day-not-completed')).toHaveCount(3);
});
});