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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,18 +2,7 @@
position: relative; position: relative;
min-height: 430px; min-height: 430px;
font-size: 14px; font-size: 14px;
border-left: 1px solid #e1e1e1; padding: 20px 0;
border-top: 1px solid #e1e1e1;
padding: 20px;
}
.container:first-child {
padding-left: 0;
border-left: 0;
}
.container:last-child {
padding-right: 0;
} }
.header { .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 PageviewsChart from './PageviewsChart';
import { get } from 'lib/web'; import { get } from 'lib/web';
import { getDateArray, getDateRange, getTimezone } from 'lib/date'; import { getDateArray, getDateRange, getTimezone } from 'lib/date';
@ -6,15 +7,19 @@ import MetricsBar from './MetricsBar';
import QuickButtons from './QuickButtons'; import QuickButtons from './QuickButtons';
import styles from './WebsiteChart.module.css'; import styles from './WebsiteChart.module.css';
import DateFilter from './DateFilter'; import DateFilter from './DateFilter';
import useSticky from './hooks/useSticky';
export default function WebsiteChart({ export default function WebsiteChart({
websiteId, websiteId,
defaultDateRange = '7day', defaultDateRange = '7day',
stickHeader = false,
onDateChange = () => {}, onDateChange = () => {},
}) { }) {
const [data, setData] = useState(); const [data, setData] = useState();
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange)); const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
const { startDate, endDate, unit, value } = dateRange; const { startDate, endDate, unit, value } = dateRange;
const [ref, sticky] = useSticky(stickHeader);
const width = useRef();
const [pageviews, uniques] = useMemo(() => { const [pageviews, uniques] = useMemo(() => {
if (data) { if (data) {
@ -46,15 +51,34 @@ export default function WebsiteChart({
loadData(); loadData();
}, [websiteId, startDate, endDate, unit]); }, [websiteId, startDate, endDate, unit]);
useEffect(() => {
width.current = document.querySelector('main').offsetWidth;
}, [sticky]);
return ( return (
<div className={styles.container}> <>
<div className={styles.header}> <div
<MetricsBar websiteId={websiteId} startDate={startDate} endDate={endDate} /> ref={ref}
<DateFilter value={value} onChange={handleDateChange} /> 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> </div>
<PageviewsChart data={{ pageviews, uniques }} unit={unit}> <div className="row">
<QuickButtons value={value} onChange={handleDateChange} /> <PageviewsChart className="col" data={{ pageviews, uniques }} unit={unit}>
</PageviewsChart> <QuickButtons value={value} onChange={handleDateChange} />
</div> </PageviewsChart>
</div>
</>
); );
} }

View File

@ -15,3 +15,13 @@
align-items: center; align-items: center;
margin-bottom: 10px; 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="row">
<div className={classNames(styles.chart, 'col')}> <div className={classNames(styles.chart, 'col')}>
<h1>{data.label}</h1> <h1>{data.label}</h1>
<WebsiteChart websiteId={data.website_id} onDateChange={handleDateChange} /> <WebsiteChart websiteId={data.website_id} onDateChange={handleDateChange} stickHeader />
</div> </div>
</div> </div>
<div className={classNames(styles.row, 'row justify-content-between')}> <div className={classNames(styles.row, 'row justify-content-between')}>
<RankingsChart <div className={pageviewClasses}>
title="Top URLs" <RankingsChart
type="url" title="Top URLs"
heading="Views" type="url"
className={pageviewClasses} heading="Views"
websiteId={data.website_id} websiteId={data.website_id}
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
dataFilter={urlFilter} dataFilter={urlFilter}
/> />
<RankingsChart </div>
title="Top referrers" <div className={pageviewClasses}>
type="referrer" <RankingsChart
heading="Views" title="Top referrers"
className={pageviewClasses} type="referrer"
websiteId={data.website_id} heading="Views"
startDate={startDate} websiteId={data.website_id}
endDate={endDate} startDate={startDate}
dataFilter={refFilter} endDate={endDate}
/> dataFilter={refFilter}
/>
</div>
</div> </div>
<div className={classNames(styles.row, 'row justify-content-between')}> <div className={classNames(styles.row, 'row justify-content-between')}>
<RankingsChart <div className={sessionClasses}>
title="Browsers" <RankingsChart
type="browser" title="Browsers"
heading="Visitors" type="browser"
className={sessionClasses} heading="Visitors"
websiteId={data.website_id} websiteId={data.website_id}
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
dataFilter={browserFilter} dataFilter={browserFilter}
/> />
<RankingsChart </div>
title="Operating system" <div className={sessionClasses}>
type="os" <RankingsChart
heading="Visitors" title="Operating system"
className={sessionClasses} type="os"
websiteId={data.website_id} heading="Visitors"
startDate={startDate} websiteId={data.website_id}
endDate={endDate} startDate={startDate}
/> endDate={endDate}
<RankingsChart />
title="Devices" </div>
type="screen" <div className={sessionClasses}>
heading="Visitors" <RankingsChart
className={sessionClasses} title="Devices"
websiteId={data.website_id} type="screen"
startDate={startDate} heading="Visitors"
endDate={endDate} websiteId={data.website_id}
dataFilter={deviceFilter} startDate={startDate}
/> endDate={endDate}
dataFilter={deviceFilter}
/>
</div>
</div> </div>
<div className="row"> <div className={classNames(styles.row, 'row justify-content-between')}>
<WorldMap data={countryData} className="col-12 col-md-12 col-lg-8" /> <div className="col-12 col-md-12 col-lg-8">
<RankingsChart <WorldMap data={countryData} />
title="Countries" </div>
type="country" <div className="col-12 col-md-12 col-lg-4">
heading="Visitors" <RankingsChart
className="col-12 col-md-12 col-lg-4" title="Countries"
websiteId={data.website_id} type="country"
startDate={startDate} heading="Visitors"
endDate={endDate} websiteId={data.website_id}
dataFilter={countryFilter} startDate={startDate}
onDataLoad={data => setCountryData(data)} endDate={endDate}
/> dataFilter={countryFilter}
onDataLoad={data => setCountryData(data)}
/>
</div>
</div> </div>
</> </>
); );

View File

@ -1,3 +1,17 @@
.chart { .chart {
margin-bottom: 30px; 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}> <div className={styles.container}>
{data && {data &&
data.websites.map(({ website_id, website_uuid, label }) => ( data.websites.map(({ website_id, website_uuid, label }) => (
<> <div key={website_id}>
<h2> <h2>
<Link href={`/${website_uuid}`}> <Link href={`/${website_uuid}`}>
<a>{label}</a> <a>{label}</a>
</Link> </Link>
</h2> </h2>
<WebsiteChart key={website_id} title={label} websiteId={website_id} /> <WebsiteChart key={website_id} title={label} websiteId={website_id} />
</> </div>
))} ))}
</div> </div>
); );

View File

@ -1,5 +1,4 @@
.container { .container {
overflow: hidden; overflow: hidden;
position: relative; 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 React from 'react';
import 'styles/index.css';
import 'styles/bootstrap-grid.css'; import 'styles/bootstrap-grid.css';
import 'styles/index.css';
export default function App({ Component, pageProps }) { export default function App({ Component, pageProps }) {
return <Component {...pageProps} />; return <Component {...pageProps} />;

View File

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