Refactor components and styles.

This commit is contained in:
Mike Cao 2020-08-03 18:12:28 -07:00
parent c5599f1e20
commit a2db27894f
16 changed files with 189 additions and 184 deletions

View File

@ -66,16 +66,16 @@ export default function PageviewsChart({
label: 'unique visitors', label: 'unique visitors',
data: data.uniques, data: data.uniques,
lineTension: 0, lineTension: 0,
backgroundColor: 'rgb(146, 86, 217, 0.4)', backgroundColor: 'rgb(38, 128, 235, 0.4)',
borderColor: 'rgb(122, 66, 191, 0.4)', borderColor: 'rgb(13, 102, 208, 0.4)',
borderWidth: 1, borderWidth: 1,
}, },
{ {
label: 'page views', label: 'page views',
data: data.pageviews, data: data.pageviews,
lineTension: 0, lineTension: 0,
backgroundColor: 'rgb(38, 128, 235, 0.4)', backgroundColor: 'rgb(38, 128, 235, 0.2)',
borderColor: 'rgb(13, 102, 208, 0.4)', borderColor: 'rgb(13, 102, 208, 0.2)',
borderWidth: 1, borderWidth: 1,
}, },
], ],
@ -165,7 +165,9 @@ const Tooltip = ({ title, value, label, labelColor }) => (
<div className={styles.content}> <div className={styles.content}>
<div className={styles.title}>{title}</div> <div className={styles.title}>{title}</div>
<div className={styles.metric}> <div className={styles.metric}>
<div className={styles.dot} style={{ backgroundColor: labelColor }} /> <div className={styles.dot}>
<div className={styles.color} style={{ backgroundColor: labelColor }} />
</div>
{value} {label} {value} {label}
</div> </div>
</div> </div>

View File

@ -30,9 +30,14 @@
} }
.dot { .dot {
position: relative;
overflow: hidden;
border-radius: 100%;
margin-right: 8px;
background: #fff;
}
.color {
width: 10px; width: 10px;
height: 10px; height: 10px;
border-radius: 100%;
border: 1px solid #b3b3b3;
margin-right: 8px;
} }

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { useSpring, animated } from 'react-spring'; import { useSpring, animated, config } from 'react-spring';
import classNames from 'classnames'; import classNames from 'classnames';
import CheckVisible from './CheckVisible';
import { get } from 'lib/web'; import { get } from 'lib/web';
import { percentFilter } from 'lib/filters'; import { percentFilter } from 'lib/filters';
import styles from './RankingsChart.module.css'; import styles from './RankingsChart.module.css';
@ -14,7 +15,6 @@ export default function RankingsChart({
heading, heading,
className, className,
dataFilter, dataFilter,
animate = true,
onDataLoad = () => {}, onDataLoad = () => {},
}) { }) {
const [data, setData] = useState(); const [data, setData] = useState();
@ -50,43 +50,42 @@ export default function RankingsChart({
} }
return ( return (
<div className={classNames(styles.container, className)}> <CheckVisible>
<div className={styles.header}> {visible => (
<div className={styles.title}>{title}</div> <div className={classNames(styles.container, className)}>
<div className={styles.heading}>{heading}</div> <div className={styles.header}>
</div> <div className={styles.title}>{title}</div>
{rankings.map(({ x, y, z }) => <div className={styles.heading}>{heading}</div>
animate ? ( </div>
<AnimatedRow key={x} label={x} value={y} percent={z} /> {rankings.map(({ x, y, z }) => (
) : ( <Row key={x} label={x} value={y} percent={z} animate={visible} />
<Row key={x} label={x} value={y} percent={z} /> ))}
), </div>
)} )}
</div> </CheckVisible>
); );
} }
const Row = ({ label, value, percent }) => ( const Row = ({ label, value, percent, animate }) => {
<div className={styles.row}> const props = useSpring({
<div className={styles.label}>{label}</div> width: percent,
<div className={styles.value}>{value.toFixed(0)}</div> y: value,
<div className={styles.percent}> from: { width: 0, y: 0 },
<div>{`${percent.toFixed(0)}%`}</div> config: animate ? config.default : { duration: 0 },
<div className={styles.bar} style={{ width: percent }} /> });
</div>
</div>
);
const AnimatedRow = ({ label, value, percent }) => {
const props = useSpring({ width: percent, y: value, from: { width: 0, 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}>{props.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}> <div className={styles.percent}>
<animated.div>{props.width.interpolate(n => `${n.toFixed(0)}%`)}</animated.div> <animated.div
<animated.div className={styles.bar} style={{ width: props.width }} /> className={styles.bar}
style={{ width: props.width.interpolate(n => `${n}%`) }}
/>
<animated.span className={styles.percentValue}>
{props.width.interpolate(n => `${n.toFixed(0)}%`)}
</animated.span>
</div> </div>
</div> </div>
); );

View File

@ -56,11 +56,12 @@
} }
.percent { .percent {
position: relative;
width: 50px; width: 50px;
color: #6e6e6e; color: #6e6e6e;
position: relative;
border-left: 1px solid #8e8e8e; border-left: 1px solid #8e8e8e;
padding-left: 10px; padding-left: 10px;
z-index: 1;
} }
.bar { .bar {

View File

@ -1,26 +1,26 @@
import React, { useState, useEffect, useMemo, useRef } from 'react'; import React, { useState, useEffect, useMemo, useRef } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import PageviewsChart from './PageviewsChart'; import PageviewsChart from './PageviewsChart';
import { get } from 'lib/web'; import CheckVisible from './CheckVisible';
import { getDateArray, getDateRange, getTimezone } from 'lib/date';
import MetricsBar from './MetricsBar'; import MetricsBar from './MetricsBar';
import QuickButtons from './QuickButtons'; import QuickButtons from './QuickButtons';
import styles from './WebsiteChart.module.css';
import DateFilter from './DateFilter'; import DateFilter from './DateFilter';
import useSticky from './hooks/useSticky'; import useSticky from './hooks/useSticky';
import { get } from 'lib/web';
import { getDateArray, getDateRange, getTimezone } from 'lib/date';
import styles from './WebsiteChart.module.css';
export default function WebsiteChart({ export default function WebsiteChart({
websiteId, websiteId,
defaultDateRange = '7day', defaultDateRange = '7day',
stickHeader = false, stickHeader = false,
animate = true,
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 [ref, sticky] = useSticky(stickHeader);
const width = useRef(); const container = useRef();
const [pageviews, uniques] = useMemo(() => { const [pageviews, uniques] = useMemo(() => {
if (data) { if (data) {
@ -52,16 +52,12 @@ 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 ref={container}>
<div <div
ref={ref} ref={ref}
className={classNames(styles.header, 'row', { [styles.sticky]: sticky })} className={classNames(styles.header, 'row', { [styles.sticky]: sticky })}
style={{ width: sticky ? width.current : 'auto' }} style={{ width: sticky ? container.current.clientWidth : 'auto' }}
> >
<MetricsBar <MetricsBar
className="col-12 col-md-9 col-lg-10" className="col-12 col-md-9 col-lg-10"
@ -76,16 +72,20 @@ export default function WebsiteChart({
/> />
</div> </div>
<div className="row"> <div className="row">
<PageviewsChart <CheckVisible>
className="col" {visible => (
websiteId={websiteId} <PageviewsChart
data={{ pageviews, uniques }} className="col"
unit={unit} websiteId={websiteId}
animationDuration={animate ? 300 : 0} data={{ pageviews, uniques }}
> unit={unit}
<QuickButtons value={value} onChange={handleDateChange} /> animationDuration={visible ? 300 : 0}
</PageviewsChart> >
<QuickButtons value={value} onChange={handleDateChange} />
</PageviewsChart>
)}
</CheckVisible>
</div> </div>
</> </div>
); );
} }

View File

@ -13,7 +13,7 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 10px; padding: 10px 0;
} }
.sticky { .sticky {
@ -21,7 +21,6 @@
top: 0; top: 0;
margin: auto; margin: auto;
background: #fff; background: #fff;
padding: 10px 0;
border-bottom: 1px solid #e1e1e1; border-bottom: 1px solid #e1e1e1;
z-index: 1; z-index: 2;
} }

View File

@ -37,103 +37,69 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
} }
return ( return (
<> <div className={styles.container}>
<div className="row"> <div className="row">
<div className={classNames(styles.chart, 'col')}> <div className={classNames(styles.chart, 'col')}>
<h1>{data.label}</h1> <h2>{data.label}</h2>
<CheckVisible> <WebsiteChart websiteId={websiteId} onDateChange={handleDateChange} stickHeader />
{visible => (
<WebsiteChart
websiteId={data.website_id}
onDateChange={handleDateChange}
animate={visible}
stickHeader
/>
)}
</CheckVisible>
</div> </div>
</div> </div>
<div className={classNames(styles.row, 'row')}> <div className={classNames(styles.row, 'row')}>
<div className={pageviewClasses}> <div className={pageviewClasses}>
<CheckVisible> <RankingsChart
{visible => ( title="Pages"
<RankingsChart type="url"
title="Pages" heading="Views"
type="url" websiteId={websiteId}
heading="Views" startDate={startDate}
websiteId={data.website_id} endDate={endDate}
startDate={startDate} dataFilter={urlFilter}
endDate={endDate} />
dataFilter={urlFilter}
animate={visible}
/>
)}
</CheckVisible>
</div> </div>
<div className={pageviewClasses}> <div className={pageviewClasses}>
<CheckVisible> <RankingsChart
{visible => ( title="Referrers"
<RankingsChart type="referrer"
title="Referrers" heading="Views"
type="referrer" websiteId={websiteId}
heading="Views" startDate={startDate}
websiteId={data.website_id} endDate={endDate}
startDate={startDate} dataFilter={refFilter}
endDate={endDate} />
dataFilter={refFilter}
animate={visible}
/>
)}
</CheckVisible>
</div> </div>
</div> </div>
<div className={classNames(styles.row, 'row')}> <div className={classNames(styles.row, 'row')}>
<div className={sessionClasses}> <div className={sessionClasses}>
<CheckVisible> <RankingsChart
{visible => ( title="Browsers"
<RankingsChart type="browser"
title="Browsers" heading="Visitors"
type="browser" websiteId={websiteId}
heading="Visitors" startDate={startDate}
websiteId={data.website_id} endDate={endDate}
startDate={startDate} dataFilter={browserFilter}
endDate={endDate} />
dataFilter={browserFilter}
animate={visible}
/>
)}
</CheckVisible>
</div> </div>
<div className={sessionClasses}> <div className={sessionClasses}>
<CheckVisible> <RankingsChart
{visible => ( title="Operating system"
<RankingsChart type="os"
title="Operating system" heading="Visitors"
type="os" websiteId={websiteId}
heading="Visitors" startDate={startDate}
websiteId={data.website_id} endDate={endDate}
startDate={startDate} />
endDate={endDate}
animate={visible}
/>
)}
</CheckVisible>
</div> </div>
<div className={sessionClasses}> <div className={sessionClasses}>
<CheckVisible> <RankingsChart
{visible => ( title="Devices"
<RankingsChart type="screen"
title="Devices" heading="Visitors"
type="screen" websiteId={websiteId}
heading="Visitors" startDate={startDate}
websiteId={data.website_id} endDate={endDate}
startDate={startDate} dataFilter={deviceFilter}
endDate={endDate} />
dataFilter={deviceFilter}
animate={visible}
/>
)}
</CheckVisible>
</div> </div>
</div> </div>
<div className={classNames(styles.row, 'row')}> <div className={classNames(styles.row, 'row')}>
@ -141,23 +107,18 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
<WorldMap data={countryData} /> <WorldMap data={countryData} />
</div> </div>
<div className="col-12 col-md-12 col-lg-4"> <div className="col-12 col-md-12 col-lg-4">
<CheckVisible> <RankingsChart
{visible => ( title="Countries"
<RankingsChart type="country"
title="Countries" heading="Visitors"
type="country" websiteId={websiteId}
heading="Visitors" startDate={startDate}
websiteId={data.website_id} endDate={endDate}
startDate={startDate} dataFilter={countryFilter}
endDate={endDate} onDataLoad={data => setCountryData(data)}
dataFilter={countryFilter} />
onDataLoad={data => setCountryData(data)}
animate={visible}
/>
)}
</CheckVisible>
</div> </div>
</div> </div>
</> </div>
); );
} }

View File

@ -18,10 +18,10 @@ export default function WebsiteList() {
return ( return (
<div className={styles.container}> <div className={styles.container}>
{data && {data &&
data.websites.map(({ website_id, website_uuid, label }) => ( data.websites.map(({ website_id, label }) => (
<div key={website_id}> <div key={website_id}>
<h2> <h2>
<Link href={`/${website_uuid}`}> <Link href={`/website/${website_id}/${label}`}>
<a>{label}</a> <a>{label}</a>
</Link> </Link>
</h2> </h2>

View File

@ -1,3 +1,32 @@
.container > div { .container > div {
padding-bottom: 30px;
border-bottom: 1px solid #e1e1e1;
margin-bottom: 30px; margin-bottom: 30px;
} }
.container > div:last-child {
border-bottom: 0;
margin-bottom: 0;
}
.container a {
position: relative;
color: #2c2c2c;
text-decoration: none;
}
.container a:before {
content: '';
position: absolute;
bottom: -2px;
width: 0;
height: 2px;
background: #2680eb;
opacity: 0.5;
transition: width 100ms;
}
.container a:hover:before {
width: 100%;
transition: width 100ms;
}

View File

@ -1,4 +1,5 @@
.container { .container {
overflow: hidden; overflow: hidden;
position: relative; position: relative;
background: #fff;
} }

View File

@ -39,11 +39,12 @@ export async function runQuery(query) {
}); });
} }
export async function getWebsite(website_uuid) { export async function getWebsite({ website_id, website_uuid }) {
return runQuery( return runQuery(
prisma.website.findOne({ prisma.website.findOne({
where: { where: {
website_uuid, ...(website_id && { website_id }),
...(website_uuid && { website_uuid }),
}, },
}), }),
); );
@ -77,11 +78,12 @@ export async function createSession(website_id, data) {
); );
} }
export async function getSession(session_uuid) { export async function getSession({ session_id, session_uuid }) {
return runQuery( return runQuery(
prisma.session.findOne({ prisma.session.findOne({
where: { where: {
session_uuid, ...(session_id && { session_id }),
...(session_uuid && { session_uuid }),
}, },
}), }),
); );

View File

@ -18,13 +18,13 @@ export default async req => {
const country = await getCountry(req, ip); const country = await getCountry(req, ip);
if (website_uuid) { if (website_uuid) {
const website = await getWebsite(website_uuid); const website = await getWebsite({ website_uuid });
if (website) { if (website) {
const { website_id } = website; const { website_id } = website;
const session_uuid = uuid(website_id, hostname, ip, userAgent, os); const session_uuid = uuid(website_id, hostname, ip, userAgent, os);
let session = await getSession(session_uuid); let session = await getSession({ session_uuid });
if (!session) { if (!session) {
session = await createSession(website_id, { session = await createSession(website_id, {

View File

@ -6,7 +6,7 @@ export default async (req, res) => {
const { id } = req.query; const { id } = req.query;
const website = await getWebsite(id); const website = await getWebsite({ website_id: +id });
return res.status(200).json(website); return res.status(200).json(website);
}; };

View File

@ -1,26 +1,13 @@
import React from 'react'; import React from 'react';
import Link from 'next/link';
import { parse } from 'cookie'; import { parse } from 'cookie';
import Layout from 'components/Layout'; import Layout from 'components/Layout';
import PageviewsChart from 'components/PageviewsChart';
import { verifySecureToken } from 'lib/crypto'; import { verifySecureToken } from 'lib/crypto';
import { subDays, endOfDay } from 'date-fns';
import WebsiteList from '../components/WebsiteList'; import WebsiteList from '../components/WebsiteList';
export default function HomePage({ username }) { export default function HomePage({ username }) {
return ( return (
<Layout> <Layout>
<WebsiteList /> <WebsiteList />
<div>
<PageviewsChart
websiteId={3}
startDate={subDays(endOfDay(new Date()), 6)}
endDate={endOfDay(new Date())}
/>
</div>
<Link href="/logout">
<a>Logout 🡒</a>
</Link>
</Layout> </Layout>
); );
} }

View File

@ -1,15 +1,19 @@
import React from 'react'; import React from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Layout from 'components/Layout'; import Layout from 'components/Layout';
import WebsiteDetails from '../components/WebsiteDetails'; import WebsiteDetails from '../../components/WebsiteDetails';
export default function DetailsPage() { export default function DetailsPage() {
const router = useRouter(); const router = useRouter();
const { id } = router.query; const { id } = router.query;
if (!id) {
return null;
}
return ( return (
<Layout> <Layout>
<WebsiteDetails websiteId={id} /> <WebsiteDetails websiteId={+id[0]} />
</Layout> </Layout>
); );
} }

View File

@ -9,6 +9,7 @@ body {
width: 100%; width: 100%;
height: 100%; height: 100%;
box-sizing: border-box; box-sizing: border-box;
background: #fafafa;
} }
*, *,
@ -24,8 +25,14 @@ body {
height: 100%; height: 100%;
} }
a,
a:active,
a:visited {
color: #2680eb;
}
header a { header a {
color: #000; color: #000 !important;
text-decoration: none; text-decoration: none;
} }
@ -44,6 +51,14 @@ select {
border-radius: 4px; border-radius: 4px;
} }
main {
background: #fff;
}
.container {
padding: 0 20px;
}
.row { .row {
margin-right: 0; margin-right: 0;
margin-left: 0; margin-left: 0;