Skip to content

Commit

Permalink
feat: added stats component to profile page (#54301)
Browse files Browse the repository at this point in the history
  • Loading branch information
jenna5376 committed Apr 27, 2024
1 parent c14cfee commit 5cbe0b7
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 91 deletions.
2 changes: 1 addition & 1 deletion client/i18n/locales/english/translations.json
Expand Up @@ -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}}"
Expand Down
14 changes: 0 additions & 14 deletions client/src/components/profile/components/heat-map.test.tsx
Expand Up @@ -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'
);
});
});
58 changes: 4 additions & 54 deletions client/src/components/profile/components/heat-map.tsx
Expand Up @@ -43,8 +43,6 @@ interface CalendarData {

interface HeatMapInnerProps {
calendarData: CalendarData[];
currentStreak: number;
longestStreak: number;
pages: PageData[];
points?: number;
t: TFunction;
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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>
);
Expand All @@ -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
Expand Down Expand Up @@ -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))
Expand All @@ -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';
Expand Down
8 changes: 0 additions & 8 deletions client/src/components/profile/components/heatmap.css
@@ -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;
}
Expand Down
22 changes: 22 additions & 0 deletions client/src/components/profile/components/stats.css
@@ -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;
}
}
24 changes: 24 additions & 0 deletions client/src/components/profile/components/stats.test.tsx
@@ -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'
);
});
});
142 changes: 142 additions & 0 deletions client/src/components/profile/components/stats.tsx
@@ -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;
18 changes: 5 additions & 13 deletions client/src/components/profile/profile.tsx
Expand Up @@ -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';

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -92,6 +87,7 @@ function UserProfile({
yearsTopContributor,
isDonating
} = user;

return (
<>
<Camper
Expand All @@ -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 ? (
Expand Down Expand Up @@ -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`}>
Expand Down
2 changes: 1 addition & 1 deletion e2e/profile.spec.ts
Expand Up @@ -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,
Expand Down

0 comments on commit 5cbe0b7

Please sign in to comment.