Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Heatmap #9

Merged
merged 9 commits into from
Jun 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions global-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = async () => {
process.env.TZ = 'America/New_York';
};
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test": "react-scripts test --env=jest-environment-jsdom-sixteen",
jkettmann marked this conversation as resolved.
Show resolved Hide resolved
"eject": "react-scripts eject",
"lint": "eslint .",
"format": "eslint --fix .",
Expand All @@ -30,6 +30,9 @@
"pre-commit": "sh scripts/validate-branch-name"
}
},
"jest": {
"globalSetup": "./global-setup.js"
},
"browserslist": {
"production": [
">0.2%",
Expand All @@ -54,6 +57,7 @@
"eslint-plugin-react-hooks": "^2.5.0",
"history": "^4.10.1",
"husky": "^4.2.5",
"jest-environment-jsdom-sixteen": "^1.0.3",
"react-test-renderer": "^16.13.1"
}
}
47 changes: 46 additions & 1 deletion src/components/Heatmap.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,50 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import HeatmapGrid from './HeatmapGrid';
import HeatmapHoursHeader from './HeatmapHoursHeader';

const Wrapper = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 60px;
`;

const Table = styled.table`
border-spacing: 0;
cell-collapse: 0;
`;

const TimezoneWrapper = styled.div`
margin-top: 12px;
font-size: ${({ theme }) => theme.font.size.small};
`;

const Timezone = styled.span`
font-weight: bold;
`;

const Heatmap = ({ posts }) => (
<Wrapper>
<Table>
<HeatmapHoursHeader />
<tbody>
<HeatmapGrid posts={posts} />
</tbody>
</Table>
<TimezoneWrapper>
All times are shown in your timezone:
{' '}
<Timezone>{Intl.DateTimeFormat().resolvedOptions().timeZone}</Timezone>
</TimezoneWrapper>
</Wrapper>
);

Heatmap.propTypes = {
posts: PropTypes.arrayOf(PropTypes.object).isRequired,
};

const Heatmap = () => <div>Heatmap Placeholder</div>;

export default Heatmap;
42 changes: 42 additions & 0 deletions src/components/HeatmapCell.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';

const TableCell = styled.td`
text-align: center;
width: 39px;
height: 39px;
padding: 0;
color: white;
font-weight: bold;
font-size: ${({ theme }) => theme.font.size.small};
background-color: ${({ theme, colorValue }) => theme.color.count[colorValue]};
border: 1px solid ${({ theme, colorValue, clicked }) => (
clicked ? theme.color.tableRowHeader : theme.color.count[colorValue]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice usage of styled-components props 👍

)};
&:hover {
border: 1px solid ${({ theme }) => theme.color.tableRowHeader};
}
`;

const HeatmapCell = ({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a small suggestion 😄 You could make this component shorter by removing the const.

const HeatmapCell = ({
children, colorValue, handleCellClick, id, clickedCellId,
}) => (
    <TableCell
      colorValue={colorValue}
      clicked={clickedCellId === id}
      onClick={() => handleCellClick(id)}
    >
      {children}
    </TableCell>
  );

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love these kinds of suggestions. I think I get caught up in the tiny details sometimes and I forget to step back and see where I can make things more efficient.

children, colorValue, handleCellClick, id, clickedCellId,
}) => (
<TableCell
colorValue={colorValue}
clicked={clickedCellId === id}
onClick={() => handleCellClick(id)}
>
{children}
</TableCell>
);

HeatmapCell.propTypes = {
children: PropTypes.node.isRequired,
colorValue: PropTypes.number.isRequired,
handleCellClick: PropTypes.func.isRequired,
id: PropTypes.string.isRequired,
clickedCellId: PropTypes.string.isRequired,
};

export default HeatmapCell;
72 changes: 72 additions & 0 deletions src/components/HeatmapGrid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { useMemo, useState } from 'react';
import styled from 'styled-components';
import HeatmapCell from './HeatmapCell';

const daysOfWeek = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
];

const getPostCountByHour = (posts) => {
const counts = Array(7)
.fill()
.map(() => Array(24).fill(0));

posts.forEach((post) => {
const date = new Date(post.data.created_utc * 1000);
counts[date.getDay()][date.getHours()] += 1;
});

return counts;
};

const RowHeader = styled.td`
color: ${({ theme }) => theme.color.background};
background-color: ${({ theme }) => theme.color.tableRowHeader};
text-align: center;
width: 154px;
height: 40px;
padding: 0;
font-weight: 600;
font-size: ${({ theme }) => theme.font.size.medium};
`;

const HeatmapGrid = ({ posts }) => {
const postCountByHour = useMemo(() => getPostCountByHour(posts), [posts]);
const [clickedCellId, setClickedCellId] = useState(null);

return (
postCountByHour.map((days, day) => {
const dayOfWeek = daysOfWeek[day];
return (
<tr key={dayOfWeek}>
<RowHeader key={dayOfWeek}>
{dayOfWeek}
</RowHeader>
{days.map((hourCount, hour) => {
const id = `${dayOfWeek}-${hour}`;
const colorValue = Math.min(hourCount, 10);
return (
<HeatmapCell
colorValue={colorValue}
key={id}
id={id}
handleCellClick={setClickedCellId}
clickedCellId={clickedCellId}
>
{hourCount}
</HeatmapCell>
);
})}
</tr>
);
})
);
};

export default React.memo(HeatmapGrid);
45 changes: 45 additions & 0 deletions src/components/HeatmapHoursHeader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import styled from 'styled-components';

const hoursOfDay = [
'12:00am',
'2:00am',
'4:00am',
'6:00am',
'8:00am',
'10:00am',
'12:00pm',
'2:00pm',
'4:00pm',
'6:00pm',
'8:00pm',
'10:00pm',
];

const TableHeader = styled.th`
background-image: linear-gradient(
${({ theme }) => theme.color.topGradient},
${({ theme }) => theme.color.bottomGradient}
);
border: none;
height: 52px;
padding: 0;
color: ${({ theme }) => theme.color.tableColumnHeader};
font-weight: 500;
font-size: ${({ theme }) => theme.font.size.small};
`;

const HeatmapHoursHeader = () => (
<thead>
<tr>
<th>&nbsp;</th>
{hoursOfDay.map((hour) => (
<TableHeader colSpan="2" key={hour}>
{hour}
</TableHeader>
))}
</tr>
</thead>
);

export default HeatmapHoursHeader;
2 changes: 1 addition & 1 deletion src/components/PostTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
const PostTable = ({ posts }) => (
<>
<div>Data Table Placeholder</div>
{posts.slice(0, 10).map((post) => (
{posts.slice(0, 1).map((post) => (
<div key={post.data.name}>{post.data.title}</div>
))}
</>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Search.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const Search = () => {
}
return (
<>
<Heatmap />
<Heatmap posts={posts} />
<PostTable posts={posts} />
</>
);
Expand Down
18 changes: 18 additions & 0 deletions src/styles/themes.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,30 @@ const mainTheme = {
externalLink: '#0087ff',
searchBoxBorder: '#e6e6e6',
spinner: '#fdb755',
tableColumnHeader: '#787878',
tableRowHeader: '#1E2537',
topGradient: '#fefefe',
bottomGradient: '#e9e9e9',
count: {
0: '#e0e592',
1: '#aed396',
2: '#a9d194',
3: '#a0ce93',
4: '#99cd94',
5: '#8cc894',
6: '#5eb391',
7: '#5db492',
8: '#5cb391',
9: '#5aad8c',
10: '#3984a3',
},
Comment on lines +46 to +58
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect 🚀

},
font: {
primary: "'Montserrat', san-serif",
secondary: "'Bitter', serif",
size: {
normal: '16px',
medium: '15px',
small: '14px',
},
},
Expand Down
58 changes: 58 additions & 0 deletions src/tests/Heatmap.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { render, fireEvent } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import fetchMock from 'jest-fetch-mock';
import App from '../App';
import * as kittens1 from './responses/1.json';
import * as kittens2 from './responses/2.json';
import * as kittens3 from './responses/3.json';
import * as kittens4 from './responses/4.json';
import * as kittens5 from './responses/5.json';

fetchMock.enableMocks();

const renderSearchPage = (route) => render(
<MemoryRouter initialEntries={[route]}>
<App />
</MemoryRouter>,
);

describe('Heatmap', () => {
beforeEach(() => {
fetch.resetMocks();

fetch
.once(JSON.stringify(kittens1))
.once(JSON.stringify(kittens2))
.once(JSON.stringify(kittens3))
.once(JSON.stringify(kittens4))
.once(JSON.stringify(kittens5));
});

it('to contain post counts for each hour of each day', async () => {
let tableCells;
await act(async () => {
const { findAllByRole } = renderSearchPage('/search/kittens');
tableCells = await findAllByRole('cell');
});

expect(tableCells.length).toEqual(175); // Includes row headers
expect(tableCells[1].innerHTML).toEqual('3'); // Sunday at 12:00am
});

it('cell highlights on click', async () => {
const { findAllByRole } = renderSearchPage('/search/kittens');
const tableCells = await findAllByRole('cell');

const originalStyle = window.getComputedStyle(tableCells[1]);
const originalBorder = originalStyle.getPropertyValue('border');
expect(originalBorder).toEqual('1px solid #a0ce93');

fireEvent.click(tableCells[1]);

const updatedStyle = window.getComputedStyle(tableCells[1]);
const updatedBorder = updatedStyle.getPropertyValue('border');
expect(updatedBorder).toEqual('1px solid #1e2537');
Comment on lines +48 to +56
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't seen anyone testing the borders yet! 🎉

});
});
Loading