Sticky metrics header. CSS updates.

This commit is contained in:
Mike Cao 2020-08-01 21:20:52 -07:00
parent a65f637df2
commit 9c5762b8a2
16 changed files with 193 additions and 104 deletions

View File

@ -13,10 +13,12 @@ const filterOptions = [
{ label: 'This year', value: '1year' },
];
export default function DateFilter({ value, onChange }) {
export default function DateFilter({ value, onChange, className }) {
function handleChange(value) {
onChange(getDateRange(value));
}
return <DropDown value={value} options={filterOptions} onChange={handleChange} />;
return (
<DropDown className={className} value={value} options={filterOptions} onChange={handleChange} />
);
}

View File

@ -1,7 +1,8 @@
import React, { useState, useEffect, useRef } from 'react';
import classNames from 'classnames';
import styles from './Dropdown.module.css';
export default function DropDown({ value, options = [], onChange }) {
export default function DropDown({ value, options = [], onChange, className }) {
const [showMenu, setShowMenu] = useState(false);
const ref = useRef();
@ -30,7 +31,7 @@ export default function DropDown({ value, options = [], onChange }) {
}, [ref]);
return (
<div ref={ref} className={styles.dropdown} onClick={handleShowMenu}>
<div ref={ref} className={classNames(styles.dropdown, className)} onClick={handleShowMenu}>
<div className={styles.value}>
{options.find(e => e.value === value).label}
<div className={styles.caret} />

View File

@ -5,6 +5,8 @@
}
.value {
white-space: nowrap;
position: relative;
padding: 4px 32px 4px 16px;
border: 1px solid #b3b3b3;
border-radius: 4px;

View File

@ -1,10 +1,11 @@
import React, { useState, useEffect } from 'react';
import classNames from 'classnames';
import MetricCard from './MetricCard';
import { get } from '../lib/web';
import { get } from 'lib/web';
import { formatShortTime } from 'lib/format';
import styles from './MetricsBar.module.css';
export default function MetricsBar({ websiteId, startDate, endDate }) {
export default function MetricsBar({ websiteId, startDate, endDate, className }) {
const [data, setData] = useState({});
const { pageviews, uniques, bounces, totaltime } = data;
@ -19,10 +20,10 @@ export default function MetricsBar({ websiteId, startDate, endDate }) {
useEffect(() => {
loadData();
}, [startDate, endDate]);
}, [websiteId, startDate, endDate]);
return (
<div className={styles.container}>
<div className={classNames(styles.container, className)}>
<MetricCard label="Views" value={pageviews} />
<MetricCard label="Visitors" value={uniques} />
<MetricCard

View File

@ -1,9 +1,10 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import classNames from 'classnames';
import ChartJS from 'chart.js';
import { format } from 'date-fns';
import styles from './PageviewsChart.module.css';
export default function PageviewsChart({ data, unit, children }) {
export default function PageviewsChart({ data, unit, className, children }) {
const canvas = useRef();
const chart = useRef();
const [tooltip, setTooltip] = useState({});
@ -138,7 +139,7 @@ export default function PageviewsChart({ data, unit, children }) {
}, [data]);
return (
<div className={styles.chart}>
<div className={classNames(styles.chart, className)}>
<canvas ref={canvas} width={960} height={400} />
<Tootip {...tooltip} />
{children}

View File

@ -62,18 +62,15 @@ export default function RankingsChart({
}
const Row = ({ label, value, percent }) => {
const props = useSpring({ width: percent, from: { width: 0 } });
const valueProps = useSpring({ y: value, from: { y: 0 } });
const props = useSpring({ width: percent, y: value, from: { width: 0, y: 0 } });
return (
<div className={styles.row}>
<div className={styles.label}>{label}</div>
<animated.div className={styles.value}>
{valueProps.y.interpolate(n => n.toFixed(0))}
</animated.div>
<animated.div className={styles.value}>{props.y.interpolate(n => n.toFixed(0))}</animated.div>
<div className={styles.percent}>
<animated.div>{props.width.interpolate(n => `${n.toFixed(0)}%`)}</animated.div>
<animated.div className={styles.bar} style={{ ...props }} />
<animated.div className={styles.bar} style={{ width: props.width }} />
</div>
</div>
);

View File

@ -2,18 +2,7 @@
position: relative;
min-height: 430px;
font-size: 14px;
border-left: 1px solid #e1e1e1;
border-top: 1px solid #e1e1e1;
padding: 20px;
}
.container:first-child {
padding-left: 0;
border-left: 0;
}
.container:last-child {
padding-right: 0;
padding: 20px 0;
}
.header {

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect, useMemo } from 'react';
import React, { useState, useEffect, useMemo, useRef } from 'react';
import classNames from 'classnames';
import PageviewsChart from './PageviewsChart';
import { get } from 'lib/web';
import { getDateArray, getDateRange, getTimezone } from 'lib/date';
@ -6,15 +7,19 @@ import MetricsBar from './MetricsBar';
import QuickButtons from './QuickButtons';
import styles from './WebsiteChart.module.css';
import DateFilter from './DateFilter';
import useSticky from './hooks/useSticky';
export default function WebsiteChart({
websiteId,
defaultDateRange = '7day',
stickHeader = false,
onDateChange = () => {},
}) {
const [data, setData] = useState();
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
const { startDate, endDate, unit, value } = dateRange;
const [ref, sticky] = useSticky(stickHeader);
const width = useRef();
const [pageviews, uniques] = useMemo(() => {
if (data) {
@ -46,15 +51,34 @@ export default function WebsiteChart({
loadData();
}, [websiteId, startDate, endDate, unit]);
useEffect(() => {
width.current = document.querySelector('main').offsetWidth;
}, [sticky]);
return (
<div className={styles.container}>
<div className={styles.header}>
<MetricsBar websiteId={websiteId} startDate={startDate} endDate={endDate} />
<DateFilter value={value} onChange={handleDateChange} />
<>
<div
ref={ref}
className={classNames(styles.header, 'row', { [styles.sticky]: sticky })}
style={{ width: sticky ? width.current : 'auto' }}
>
<MetricsBar
className="col-12 col-md-9 col-lg-10"
websiteId={websiteId}
startDate={startDate}
endDate={endDate}
/>
<DateFilter
className="col-12 col-md-3 col-lg-2"
value={value}
onChange={handleDateChange}
/>
</div>
<PageviewsChart data={{ pageviews, uniques }} unit={unit}>
<QuickButtons value={value} onChange={handleDateChange} />
</PageviewsChart>
</div>
<div className="row">
<PageviewsChart className="col" data={{ pageviews, uniques }} unit={unit}>
<QuickButtons value={value} onChange={handleDateChange} />
</PageviewsChart>
</div>
</>
);
}

View File

@ -15,3 +15,13 @@
align-items: center;
margin-bottom: 10px;
}
.sticky {
position: fixed;
top: 0;
margin: auto;
background: #fff;
padding: 10px 0;
border-bottom: 1px solid #e1e1e1;
z-index: 1;
}

View File

@ -40,75 +40,83 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
<div className="row">
<div className={classNames(styles.chart, 'col')}>
<h1>{data.label}</h1>
<WebsiteChart websiteId={data.website_id} onDateChange={handleDateChange} />
<WebsiteChart websiteId={data.website_id} onDateChange={handleDateChange} stickHeader />
</div>
</div>
<div className={classNames(styles.row, 'row justify-content-between')}>
<RankingsChart
title="Top URLs"
type="url"
heading="Views"
className={pageviewClasses}
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
dataFilter={urlFilter}
/>
<RankingsChart
title="Top referrers"
type="referrer"
heading="Views"
className={pageviewClasses}
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
dataFilter={refFilter}
/>
<div className={pageviewClasses}>
<RankingsChart
title="Top URLs"
type="url"
heading="Views"
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
dataFilter={urlFilter}
/>
</div>
<div className={pageviewClasses}>
<RankingsChart
title="Top referrers"
type="referrer"
heading="Views"
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
dataFilter={refFilter}
/>
</div>
</div>
<div className={classNames(styles.row, 'row justify-content-between')}>
<RankingsChart
title="Browsers"
type="browser"
heading="Visitors"
className={sessionClasses}
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
dataFilter={browserFilter}
/>
<RankingsChart
title="Operating system"
type="os"
heading="Visitors"
className={sessionClasses}
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
/>
<RankingsChart
title="Devices"
type="screen"
heading="Visitors"
className={sessionClasses}
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
dataFilter={deviceFilter}
/>
<div className={sessionClasses}>
<RankingsChart
title="Browsers"
type="browser"
heading="Visitors"
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
dataFilter={browserFilter}
/>
</div>
<div className={sessionClasses}>
<RankingsChart
title="Operating system"
type="os"
heading="Visitors"
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
/>
</div>
<div className={sessionClasses}>
<RankingsChart
title="Devices"
type="screen"
heading="Visitors"
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
dataFilter={deviceFilter}
/>
</div>
</div>
<div className="row">
<WorldMap data={countryData} className="col-12 col-md-12 col-lg-8" />
<RankingsChart
title="Countries"
type="country"
heading="Visitors"
className="col-12 col-md-12 col-lg-4"
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
dataFilter={countryFilter}
onDataLoad={data => setCountryData(data)}
/>
<div className={classNames(styles.row, 'row justify-content-between')}>
<div className="col-12 col-md-12 col-lg-8">
<WorldMap data={countryData} />
</div>
<div className="col-12 col-md-12 col-lg-4">
<RankingsChart
title="Countries"
type="country"
heading="Visitors"
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
dataFilter={countryFilter}
onDataLoad={data => setCountryData(data)}
/>
</div>
</div>
</>
);

View File

@ -1,3 +1,17 @@
.chart {
margin-bottom: 30px;
}
.row {
border-top: 1px solid #e1e1e1;
}
.row > [class*='col-'] {
border-left: 1px solid #e1e1e1;
padding: 0 20px;
}
.row > [class*='col-']:first-child {
border-left: 0;
padding-left: 0;
}

View File

@ -19,14 +19,14 @@ export default function WebsiteList() {
<div className={styles.container}>
{data &&
data.websites.map(({ website_id, website_uuid, label }) => (
<>
<div key={website_id}>
<h2>
<Link href={`/${website_uuid}`}>
<a>{label}</a>
</Link>
</h2>
<WebsiteChart key={website_id} title={label} websiteId={website_id} />
</>
</div>
))}
</div>
);

View File

@ -1,5 +1,4 @@
.container {
overflow: hidden;
position: relative;
border-top: 1px solid #e1e1e1;
}

View File

@ -0,0 +1,31 @@
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];
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import 'styles/index.css';
import 'styles/bootstrap-grid.css';
import 'styles/index.css';
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />;

View File

@ -43,3 +43,13 @@ select {
border: 1px solid #b3b3b3;
border-radius: 4px;
}
.row {
margin-right: 0;
margin-left: 0;
}
.row > .col,
.row > [class*='col-'] {
padding-right: 0;
padding-left: 0;
}