From 9d8a2406e1fe2026d9f9967ab9459194f02333b4 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 3 Aug 2020 23:20:35 -0700 Subject: [PATCH] New components, convert hooks to components, bug fixes. --- assets/arrow-right.svg | 1 + components/Button.js | 13 +++ components/Button.module.css | 22 ++++ components/CheckVisible.js | 19 +++- components/DropDown.js | 4 +- components/Icon.js | 7 ++ components/Icon.module.css | 11 ++ components/Link.js | 12 ++ components/Link.module.css | 23 ++++ components/QuickButtons.js | 7 +- components/QuickButtons.module.css | 23 ++-- components/RankingsChart.js | 8 +- components/RankingsChart.module.css | 12 ++ components/StickyHeader.js | 49 ++++++++ components/WebsiteChart.js | 37 +++--- components/WebsiteDetails.js | 163 +++++++++++++++------------ components/WebsiteDetails.module.css | 1 + components/WebsiteList.js | 25 ++-- components/WebsiteList.module.css | 33 +++--- components/hooks/useSticky.js | 31 ----- styles/index.css | 10 +- 21 files changed, 330 insertions(+), 181 deletions(-) create mode 100644 assets/arrow-right.svg create mode 100644 components/Button.js create mode 100644 components/Button.module.css create mode 100644 components/Icon.js create mode 100644 components/Icon.module.css create mode 100644 components/Link.js create mode 100644 components/Link.module.css create mode 100644 components/StickyHeader.js delete mode 100644 components/hooks/useSticky.js diff --git a/assets/arrow-right.svg b/assets/arrow-right.svg new file mode 100644 index 00000000..6fc93909 --- /dev/null +++ b/assets/arrow-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/Button.js b/components/Button.js new file mode 100644 index 00000000..0ce0ea03 --- /dev/null +++ b/components/Button.js @@ -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 ( + + ); +} diff --git a/components/Button.module.css b/components/Button.module.css new file mode 100644 index 00000000..4d907040 --- /dev/null +++ b/components/Button.module.css @@ -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; +} diff --git a/components/CheckVisible.js b/components/CheckVisible.js index 94b97730..7d503aec 100644 --- a/components/CheckVisible.js +++ b/components/CheckVisible.js @@ -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(); @@ -30,5 +33,9 @@ export default function CheckVisible({ children }) { }; }, [visible]); - return
{typeof children === 'function' ? children(visible) : children}
; + return ( +
+ {typeof children === 'function' ? children(visible) : children} +
+ ); } diff --git a/components/DropDown.js b/components/DropDown.js index 4f36b77a..1e795d3d 100644 --- a/components/DropDown.js +++ b/components/DropDown.js @@ -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]); diff --git a/components/Icon.js b/components/Icon.js new file mode 100644 index 00000000..e5e7c82b --- /dev/null +++ b/components/Icon.js @@ -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
{icon}
; +} diff --git a/components/Icon.module.css b/components/Icon.module.css new file mode 100644 index 00000000..72f4286e --- /dev/null +++ b/components/Icon.module.css @@ -0,0 +1,11 @@ +.icon { + display: inline-flex; + justify-content: center; + align-items: center; + vertical-align: middle; +} + +.icon > svg { + width: 16px; + height: 16px; +} diff --git a/components/Link.js b/components/Link.js new file mode 100644 index 00000000..ca34110d --- /dev/null +++ b/components/Link.js @@ -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 ( + + {children} + + ); +} diff --git a/components/Link.module.css b/components/Link.module.css new file mode 100644 index 00000000..54cebc0c --- /dev/null +++ b/components/Link.module.css @@ -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; +} diff --git a/components/QuickButtons.js b/components/QuickButtons.js index 2ccac3e0..03bee6cc 100644 --- a/components/QuickButtons.js +++ b/components/QuickButtons.js @@ -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'; @@ -17,13 +18,13 @@ export default function QuickButtons({ value, onChange }) { return (
{Object.keys(options).map(key => ( -
handleClick(key)} > {options[key]} -
+ ))}
); diff --git a/components/QuickButtons.module.css b/components/QuickButtons.module.css index d56f1876..947479bb 100644 --- a/components/QuickButtons.module.css +++ b/components/QuickButtons.module.css @@ -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; + } +} diff --git a/components/RankingsChart.js b/components/RankingsChart.js index cf0ca85c..6a86dbbc 100644 --- a/components/RankingsChart.js +++ b/components/RankingsChart.js @@ -57,9 +57,11 @@ export default function RankingsChart({
{title}
{heading}
- {rankings.map(({ x, y, z }) => ( - - ))} +
+ {rankings.map(({ x, y, z }) => ( + + ))} +
)} diff --git a/components/RankingsChart.module.css b/components/RankingsChart.module.css index 9e9174ea..67095377 100644 --- a/components/RankingsChart.module.css +++ b/components/RankingsChart.module.css @@ -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; +} diff --git a/components/StickyHeader.js b/components/StickyHeader.js new file mode 100644 index 00000000..7378a857 --- /dev/null +++ b/components/StickyHeader.js @@ -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 ( +
+ {children} +
+ ); +} diff --git a/components/WebsiteChart.js b/components/WebsiteChart.js index 1e5ab536..696ff2e2 100644 --- a/components/WebsiteChart.js +++ b/components/WebsiteChart.js @@ -5,7 +5,7 @@ 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'; @@ -13,13 +13,13 @@ 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(() => { @@ -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(() => { @@ -54,10 +55,11 @@ export default function WebsiteChart({ return (
-
-
+
- + {visible => ( setChartLoaded(true), 300); + } + function handleDateChange(values) { setTimeout(() => setDateRange(values), 300); } @@ -41,84 +45,93 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })

{data.label}

- -
-
-
-
- -
-
- -
-
-
-
- -
-
- -
-
- -
-
-
-
- -
-
- setCountryData(data)} + onDataLoad={handleDataLoad} + onDateChange={handleDateChange} + stickyHeader />
+ {chartLoaded && ( + <> +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ setCountryData(data)} + /> +
+
+ + )}
); } diff --git a/components/WebsiteDetails.module.css b/components/WebsiteDetails.module.css index a22b1d6d..cba5ed18 100644 --- a/components/WebsiteDetails.module.css +++ b/components/WebsiteDetails.module.css @@ -4,6 +4,7 @@ .row { border-top: 1px solid #e1e1e1; + min-height: 430px; } .row > [class*='col-'] { diff --git a/components/WebsiteList.js b/components/WebsiteList.js index a18afe6b..64403142 100644 --- a/components/WebsiteList.js +++ b/components/WebsiteList.js @@ -1,7 +1,9 @@ import React, { useState, useEffect } from 'react'; -import Link from 'next/link'; -import { get } from 'lib/web'; +import Link from './Link'; import WebsiteChart from './WebsiteChart'; +import Icon from './Icon'; +import { get } from 'lib/web'; +import Arrow from 'assets/arrow-right.svg'; import styles from './WebsiteList.module.css'; export default function WebsiteList() { @@ -16,18 +18,23 @@ export default function WebsiteList() { }, []); return ( -
+ <> {data && data.websites.map(({ website_id, label }) => ( -
-

- - {label} +
+
+

+ + {label} + +

+ + } /> View details -

+
))} -
+ ); } diff --git a/components/WebsiteList.module.css b/components/WebsiteList.module.css index cb284497..12822f5b 100644 --- a/components/WebsiteList.module.css +++ b/components/WebsiteList.module.css @@ -1,32 +1,29 @@ -.container > div { +.website { padding-bottom: 30px; border-bottom: 1px solid #e1e1e1; margin-bottom: 30px; } -.container > div:last-child { +.website:last-child { border-bottom: 0; margin-bottom: 0; } -.container a { - position: relative; - color: #2c2c2c; - text-decoration: none; +.header { + display: flex; + justify-content: space-between; + align-items: center; } -.container a:before { - content: ''; - position: absolute; - bottom: -2px; - width: 0; - height: 2px; - background: #2680eb; - opacity: 0.5; - transition: width 100ms; +.title { + color: #2c2c2c !important; } -.container a:hover:before { - width: 100%; - transition: width 100ms; +.details { + font-size: 14px; + font-weight: 600; +} + +.details svg { + margin-right: 5px; } diff --git a/components/hooks/useSticky.js b/components/hooks/useSticky.js deleted file mode 100644 index 751217e1..00000000 --- a/components/hooks/useSticky.js +++ /dev/null @@ -1,31 +0,0 @@ -import { useState, useEffect, useCallback, useRef } from 'react'; - -export default function useSticky(enabled) { - const [node, setNode] = useState(null); - const [sticky, setSticky] = useState(false); - const offsetTop = useRef(0); - - const ref = useCallback(node => { - offsetTop.current = node?.offsetTop; - setNode(node); - }, []); - - useEffect(() => { - const checkPosition = () => { - const state = window.pageYOffset > offsetTop.current; - if (node && sticky !== state) { - setSticky(state); - } - }; - - if (enabled) { - window.addEventListener('scroll', checkPosition); - } - - return () => { - window.removeEventListener('scroll', checkPosition); - }; - }, [node, sticky, enabled]); - - return [ref, sticky]; -} diff --git a/styles/index.css b/styles/index.css index aae75718..40aba9e2 100644 --- a/styles/index.css +++ b/styles/index.css @@ -9,6 +9,7 @@ body { width: 100%; height: 100%; box-sizing: border-box; + color: #2c2c2c; background: #fafafa; } @@ -25,6 +26,12 @@ body { height: 100%; } +button, +input, +select { + font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif; +} + a, a:active, a:visited { @@ -32,7 +39,7 @@ a:visited { } header a { - color: #000 !important; + color: #2c2c2c !important; text-decoration: none; } @@ -52,6 +59,7 @@ select { } main { + flex: 1; background: #fff; }