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' })
-
-
-
-
-
-
-
- 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;
}