feat: added stats component to profile page (#54301)

This commit is contained in:
Jenna (Ju Hee) Han
2024-04-27 16:20:26 -04:00
committed by GitHub
parent c14cfeed5a
commit 5cbe0b709e
9 changed files with 199 additions and 91 deletions

View File

@@ -310,7 +310,7 @@
"tweet": "I just earned the {{certTitle}} certification @freeCodeCamp! Check it out here: {{certURL}}",
"avatar": "{{username}}'s avatar",
"joined": "Joined {{date}}",
"total-points": "Number of points: {{count}}",
"total-points": "Total Points:",
"points": "{{count}} point on {{date}}",
"points_plural": "{{count}} points on {{date}}",
"page-number": "{{pageNumber}} of {{totalPages}}"

View File

@@ -65,18 +65,4 @@ describe('<HeatMap/>', () => {
screen.getByText(`${startOfCalendar} - ${endOfCalendar}`)
).toBeInTheDocument();
});
it('calculates the correct longest streak', () => {
render(<HeatMap {...props} />);
expect(screen.getByTestId('longest-streak')).toHaveTextContent(
'profile.longest-streak'
);
});
it('calculates the correct current streak', () => {
render(<HeatMap {...props} />);
expect(screen.getByTestId('current-streak')).toHaveTextContent(
'profile.current-streak'
);
});
});

View File

@@ -43,8 +43,6 @@ interface CalendarData {
interface HeatMapInnerProps {
calendarData: CalendarData[];
currentStreak: number;
longestStreak: number;
pages: PageData[];
points?: number;
t: TFunction;
@@ -85,7 +83,7 @@ class HeatMapInner extends Component<HeatMapInnerProps, HeatMapInnerState> {
}
render() {
const { calendarData, currentStreak, longestStreak, pages, t } = this.props;
const { calendarData, pages, t } = this.props;
const { startOfCalendar, endOfCalendar } = pages[this.state.pageIndex];
const title = `${startOfCalendar.toLocaleDateString([localeCode, 'en-US'], {
year: 'numeric',
@@ -164,18 +162,6 @@ class HeatMapInner extends Component<HeatMapInnerProps, HeatMapInnerState> {
values={dataToDisplay}
/>
<ReactTooltip className='react-tooltip' effect='solid' html={true} />
<Spacer size='medium' />
<Row>
<div className='streak-container'>
<span className='streak' data-testid='longest-streak'>
<b>{t('profile.longest-streak')}</b> {longestStreak || 0}
</span>
<span className='streak' data-testid='current-streak'>
<b>{t('profile.current-streak')}</b> {currentStreak || 0}
</span>
</div>
</Row>
<hr />
</FullWidthRow>
);
@@ -188,7 +174,7 @@ const HeatMap = (props: HeatMapProps): JSX.Element => {
/**
* the following logic creates the data for the heatmap
* from the users calendar and calculates their streaks
* from the users calendar
*/
// create array of timestamps and turn into milliseconds
@@ -232,11 +218,7 @@ const HeatMap = (props: HeatMapProps): JSX.Element => {
dayCounter = addDays(dayCounter, 1);
}
let longestStreak = 0;
let currentStreak = 0;
let lastIndex = -1;
// add a point to each day with a completed timestamp and calculate streaks
// add a point to each day with a completed timestamp
timestamps.forEach(stamp => {
const index = calendarData.findIndex(day =>
isEqual(day.date, startOfDay(stamp))
@@ -245,42 +227,10 @@ const HeatMap = (props: HeatMapProps): JSX.Element => {
if (index >= 0) {
// add one point for today
calendarData[index].count++;
// if timestamp is on a new day, deal with streaks
if (index !== lastIndex) {
// if yesterday has points
if (calendarData[index - 1] && calendarData[index - 1].count > 0) {
currentStreak++;
} else {
currentStreak = 1;
}
if (currentStreak > longestStreak) {
longestStreak = currentStreak;
}
}
lastIndex = index;
}
});
// if today has no points
if (
calendarData[calendarData.length - 1] &&
calendarData[calendarData.length - 1].count === 0
) {
currentStreak = 0;
}
return (
<HeatMapInner
calendarData={calendarData}
currentStreak={currentStreak}
longestStreak={longestStreak}
pages={pages}
t={t}
/>
);
return <HeatMapInner calendarData={calendarData} pages={pages} t={t} />;
};
HeatMap.displayName = 'HeatMap';

View File

@@ -1,11 +1,3 @@
.streak-container {
display: flex;
justify-content: space-around;
align-items: center;
font-size: 18px;
color: var(--primary-color);
}
.heatmap-nav {
text-align: center;
}

View File

@@ -0,0 +1,22 @@
.stats {
display: flex;
justify-content: space-between;
align-items: center;
text-align: center;
color: var(--primary-color);
}
.stats dt {
font-size: 18px;
}
.stats dd {
font-size: 2rem;
margin-top: 16px;
}
@media (max-width: 600px) {
.stats dd {
font-size: 1.5rem;
}
}

View File

@@ -0,0 +1,24 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import Stats from './stats';
const props: { calendar: { [key: number]: number }; points: number } = {
calendar: {},
points: 0
};
describe('<Stats/>', () => {
it('calculates the correct longest streak', () => {
render(<Stats {...props} />);
expect(screen.getByTestId('longest-streak')).toHaveTextContent(
'profile.longest-streak'
);
});
it('calculates the correct current streak', () => {
render(<Stats {...props} />);
expect(screen.getByTestId('current-streak')).toHaveTextContent(
'profile.current-streak'
);
});
});

View File

@@ -0,0 +1,142 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import addDays from 'date-fns/addDays';
import addMonths from 'date-fns/addMonths';
import isEqual from 'date-fns/isEqual';
import startOfDay from 'date-fns/startOfDay';
import { User } from '../../../redux/prop-types';
import { FullWidthRow, Spacer } from '../../helpers';
import './stats.css';
interface StatsProps {
points: number;
calendar: User['calendar'];
}
function Stats({ points, calendar }: StatsProps): JSX.Element {
const { t } = useTranslation();
/**
* the following logic calculates streaks from the
* users calendar
*/
interface PageData {
startOfCalendar: Date;
endOfCalendar: Date;
}
interface CalendarData {
date: Date;
count: number;
}
// create array of timestamps and turn into milliseconds
const timestamps = Object.keys(calendar).map(
stamp => Number.parseInt(stamp, 10) * 1000
);
const startOfTimestamps = startOfDay(new Date(timestamps[0]));
let endOfCalendar = startOfDay(Date.now());
let startOfCalendar;
const pages: PageData[] = [];
do {
startOfCalendar = addDays(addMonths(endOfCalendar, -6), 1);
const newPage = {
startOfCalendar: startOfCalendar,
endOfCalendar: endOfCalendar
};
pages.push(newPage);
endOfCalendar = addDays(startOfCalendar, -1);
} while (startOfTimestamps < startOfCalendar);
pages.reverse();
const calendarData: CalendarData[] = [];
let dayCounter = pages[0].startOfCalendar;
// create an object for each day of the calendar period
while (dayCounter <= pages[pages.length - 1].endOfCalendar) {
const newDay = {
date: startOfDay(dayCounter),
count: 0
};
calendarData.push(newDay);
dayCounter = addDays(dayCounter, 1);
}
let longestStreak = 0;
let currentStreak = 0;
let lastIndex = -1;
// add a point to each day with a completed timestamp and calculate streaks
timestamps.forEach(stamp => {
const index = calendarData.findIndex(day =>
isEqual(day.date, startOfDay(stamp))
);
if (index >= 0) {
// add one point for today
calendarData[index].count++;
// if timestamp is on a new day, deal with streaks
if (index !== lastIndex) {
// if yesterday has points
if (calendarData[index - 1] && calendarData[index - 1].count > 0) {
currentStreak++;
} else {
currentStreak = 1;
}
if (currentStreak > longestStreak) {
longestStreak = currentStreak;
}
}
lastIndex = index;
}
});
// if today has no points
if (
calendarData[calendarData.length - 1] &&
calendarData[calendarData.length - 1].count === 0
) {
currentStreak = 0;
}
return (
<FullWidthRow>
<h2 className='text-center'>Stats</h2>
<Spacer size='small' />
<dl className='stats'>
<div>
<dt>
<b data-testid='current-streak'>{t('profile.current-streak')}</b>
</dt>
<dd>{currentStreak || 0}</dd>
</div>
<div>
<dt>
<b>{t('profile.total-points')}</b>
</dt>
<dd>{points}</dd>
</div>
<div>
<dt>
<b data-testid='longest-streak'>{t('profile.longest-streak')}</b>
</dt>
<dd>{longestStreak || 0}</dd>
</div>
</dl>
<hr />
</FullWidthRow>
);
}
export default Stats;

View File

@@ -9,6 +9,7 @@ import { User } from './../../redux/prop-types';
import Timeline from './components/time-line';
import Camper from './components/camper';
import Certifications from './components/certifications';
import Stats from './components/stats';
import HeatMap from './components/heat-map';
import { PortfolioProjects } from './components/portfolio-projects';
@@ -56,13 +57,7 @@ const Message = ({ isSessionUser, t, username }: MessageProps) => {
return <VisitorMessage t={t} username={username} />;
};
function UserProfile({
user,
t
}: {
user: ProfileProps['user'];
t: TFunction;
}): JSX.Element {
function UserProfile({ user }: { user: ProfileProps['user'] }): JSX.Element {
const {
profileUI: {
showAbout,
@@ -92,6 +87,7 @@ function UserProfile({
yearsTopContributor,
isDonating
} = user;
return (
<>
<Camper
@@ -108,11 +104,7 @@ function UserProfile({
website={website}
yearsTopContributor={yearsTopContributor}
/>
{showPoints && (
<p className='text-center points'>
{t('profile.total-points', { count: points })}
</p>
)}
{showPoints ? <Stats points={points} calendar={calendar} /> : null}
{showHeatMap ? <HeatMap calendar={calendar} /> : null}
{showCerts ? <Certifications username={username} /> : null}
{showPortfolio ? (
@@ -146,7 +138,7 @@ function Profile({ user, isSessionUser }: ProfileProps): JSX.Element {
{isLocked && (
<Message username={username} isSessionUser={isSessionUser} t={t} />
)}
{showUserProfile && <UserProfile user={user} t={t} />}
{showUserProfile && <UserProfile user={user} />}
{!isSessionUser && (
<Row className='text-center'>
<Link to={`/user/${username}/report-user`}>

View File

@@ -116,7 +116,7 @@ test.describe('Profile component', () => {
});
test('renders total points correctly', async ({ page }) => {
await expect(page.getByText('Number of points: 1')).toBeVisible();
await expect(page.getByText('Total Points:')).toBeVisible();
});
// The date range computation in this test doesn't match the implementation code,