Skip to content

Commit

Permalink
New components, convert hooks to components, bug fixes.
Browse files Browse the repository at this point in the history
  • Loading branch information
mikecao committed Aug 4, 2020
1 parent a2db278 commit 9d8a240
Show file tree
Hide file tree
Showing 21 changed files with 330 additions and 181 deletions.
1 change: 1 addition & 0 deletions assets/arrow-right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions components/Button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import classNames from 'classnames';
import Icon from './Icon';
import styles from './Button.module.css';

export default function Button({ icon, children, className, onClick }) {
return (
<button type="button" className={classNames(styles.button, className)} onClick={onClick}>
{icon && <Icon icon={icon} />}
{children}
</button>
);
}
22 changes: 22 additions & 0 deletions components/Button.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.button {
display: flex;
justify-content: center;
align-items: center;
background: #f5f5f5;
padding: 4px 8px;
border-radius: 4px;
border: 0;
outline: none;
cursor: pointer;
}

.button:hover {
background: #eaeaea;
}

.button svg {
display: block;
width: 16px;
height: 16px;
margin-right: 8px;
}
19 changes: 13 additions & 6 deletions components/CheckVisible.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React, { useState, useRef, useEffect } from 'react';

function isInViewport(node) {
return (
window.pageYOffset < node.offsetTop + node.clientHeight ||
window.pageXOffset < node.offsetLeft + node.clientWidth
function isInViewport(element) {
const rect = element.getBoundingClientRect();
return !(
rect.bottom < 0 ||
rect.right < 0 ||
rect.left > window.innerWidth ||
rect.top > window.innerHeight
);
}

export default function CheckVisible({ children }) {
export default function CheckVisible({ className, children }) {
const [visible, setVisible] = useState(false);
const ref = useRef();

Expand All @@ -30,5 +33,9 @@ export default function CheckVisible({ children }) {
};
}, [visible]);

return <div ref={ref}>{typeof children === 'function' ? children(visible) : children}</div>;
return (
<div ref={ref} className={className} data-visible={visible}>
{typeof children === 'function' ? children(visible) : children}
</div>
);
}
4 changes: 2 additions & 2 deletions components/DropDown.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export default function DropDown({ value, options = [], onChange, className }) {
}
}

document.body.addEventListener('click', hideMenu);
document.addEventListener('click', hideMenu);

return () => {
document.body.removeEventListener('click', hideMenu);
document.removeEventListener('click', hideMenu);
};
}, [ref]);

Expand Down
7 changes: 7 additions & 0 deletions components/Icon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import styles from './Icon.module.css';

export default function Icon({ icon, className }) {
return <div className={classNames(styles.icon, className)}>{icon}</div>;
}
11 changes: 11 additions & 0 deletions components/Icon.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.icon {
display: inline-flex;
justify-content: center;
align-items: center;
vertical-align: middle;
}

.icon > svg {
width: 16px;
height: 16px;
}
12 changes: 12 additions & 0 deletions components/Link.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import classNames from 'classnames';
import NextLink from 'next/link';
import styles from './Link.module.css';

export default function Link({ href, className, children }) {
return (
<NextLink href={href}>
<a className={classNames(styles.link, className)}>{children}</a>
</NextLink>
);
}
23 changes: 23 additions & 0 deletions components/Link.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.link,
.link:active,
.link:visited {
position: relative;
color: #2c2c2c;
text-decoration: none;
}

.link:before {
content: '';
position: absolute;
bottom: -2px;
width: 0;
height: 2px;
background: #2680eb;
opacity: 0.5;
transition: width 100ms;
}

.link:hover:before {
width: 100%;
transition: width 100ms;
}
7 changes: 4 additions & 3 deletions components/QuickButtons.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import classNames from 'classnames';
import Button from './Button';
import { getDateRange } from 'lib/date';
import styles from './QuickButtons.module.css';

Expand All @@ -17,13 +18,13 @@ export default function QuickButtons({ value, onChange }) {
return (
<div className={styles.buttons}>
{Object.keys(options).map(key => (
<div
<Button
key={key}
className={classNames(styles.button, { [styles.active]: value === key })}
className={classNames({ [styles.active]: value === key })}
onClick={() => handleClick(key)}
>
{options[key]}
</div>
</Button>
))}
</div>
);
Expand Down
23 changes: 8 additions & 15 deletions components/QuickButtons.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,16 @@
margin: auto;
}

.button {
font-size: 12px;
background: #f5f5f5;
padding: 4px 8px;
border-radius: 4px;
margin-right: 10px;
cursor: pointer;
}

.button:last-child {
margin-right: 0;
}

.button:hover {
background: #eaeaea;
.buttons button + button {
margin-left: 10px;
}

.active {
font-weight: 600;
}

@media only screen and (max-width: 720px) {
.buttons button:last-child {
display: none;
}
}
8 changes: 5 additions & 3 deletions components/RankingsChart.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ export default function RankingsChart({
<div className={styles.title}>{title}</div>
<div className={styles.heading}>{heading}</div>
</div>
{rankings.map(({ x, y, z }) => (
<Row key={x} label={x} value={y} percent={z} animate={visible} />
))}
<div className={styles.body}>
{rankings.map(({ x, y, z }) => (
<Row key={x} label={x} value={y} percent={z} animate={visible} />
))}
</div>
</div>
)}
</CheckVisible>
Expand Down
12 changes: 12 additions & 0 deletions components/RankingsChart.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,15 @@
background: #2680eb;
z-index: -1;
}

.body {
position: relative;
}

.body:empty:before {
content: 'No data available';
display: block;
color: #b3b3b3;
text-align: center;
line-height: 50px;
}
49 changes: 49 additions & 0 deletions components/StickyHeader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { useState, useRef, useEffect } from 'react';
import classNames from 'classnames';

export default function StickyHeader({
className,
stickyClassName,
stickyStyle,
children,
enabled = true,
}) {
const [sticky, setSticky] = useState(false);
const ref = useRef();
const offsetTop = useRef(0);

useEffect(() => {
const checkPosition = () => {
if (ref.current) {
if (!offsetTop.current) {
offsetTop.current = ref.current.offsetTop;
}
const state = window.pageYOffset > offsetTop.current;
if (sticky !== state) {
setSticky(state);
}
}
};

checkPosition();

if (enabled) {
window.addEventListener('scroll', checkPosition);
}

return () => {
window.removeEventListener('scroll', checkPosition);
};
}, [sticky, enabled]);

return (
<div
ref={ref}
data-sticky={sticky}
className={classNames(className, { [stickyClassName]: sticky })}
{...(sticky && { style: stickyStyle })}
>
{children}
</div>
);
}
37 changes: 19 additions & 18 deletions components/WebsiteChart.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ import CheckVisible from './CheckVisible';
import MetricsBar from './MetricsBar';
import QuickButtons from './QuickButtons';
import DateFilter from './DateFilter';
import useSticky from './hooks/useSticky';
import StickyHeader from './StickyHeader';
import { get } from 'lib/web';
import { getDateArray, getDateRange, getTimezone } from 'lib/date';
import styles from './WebsiteChart.module.css';

export default function WebsiteChart({
websiteId,
defaultDateRange = '7day',
stickHeader = false,
stickyHeader = false,
onDataLoad = () => {},
onDateChange = () => {},
}) {
const [data, setData] = useState();
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
const { startDate, endDate, unit, value } = dateRange;
const [ref, sticky] = useSticky(stickHeader);
const container = useRef();

const [pageviews, uniques] = useMemo(() => {
Expand All @@ -38,14 +38,15 @@ export default function WebsiteChart({
}

async function loadData() {
setData(
await get(`/api/website/${websiteId}/pageviews`, {
start_at: +startDate,
end_at: +endDate,
unit,
tz: getTimezone(),
}),
);
const data = await get(`/api/website/${websiteId}/pageviews`, {
start_at: +startDate,
end_at: +endDate,
unit,
tz: getTimezone(),
});

setData(data);
onDataLoad(data);
}

useEffect(() => {
Expand All @@ -54,10 +55,11 @@ export default function WebsiteChart({

return (
<div ref={container}>
<div
ref={ref}
className={classNames(styles.header, 'row', { [styles.sticky]: sticky })}
style={{ width: sticky ? container.current.clientWidth : 'auto' }}
<StickyHeader
className={classNames(styles.header, 'row')}
stickyClassName={styles.sticky}
stickyStyle={{ width: container?.current?.clientWidth }}
enabled={stickyHeader}
>
<MetricsBar
className="col-12 col-md-9 col-lg-10"
Expand All @@ -70,12 +72,11 @@ export default function WebsiteChart({
value={value}
onChange={handleDateChange}
/>
</div>
</StickyHeader>
<div className="row">
<CheckVisible>
<CheckVisible className="col">
{visible => (
<PageviewsChart
className="col"
websiteId={websiteId}
data={{ pageviews, uniques }}
unit={unit}
Expand Down
Loading

0 comments on commit 9d8a240

Please sign in to comment.