mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(client): add details to daily challenges calendar (#63003)
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -331,7 +331,7 @@ export type DailyCodingChallengeLanguages = 'javascript' | 'python';
|
||||
export interface CompletedDailyCodingChallenge {
|
||||
id: string;
|
||||
completedDate: number;
|
||||
completedLanguages: DailyCodingChallengeLanguages[];
|
||||
languages: DailyCodingChallengeLanguages[];
|
||||
}
|
||||
|
||||
type Quiz = {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user