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',
data: data.uniques,
lineTension: 0,
backgroundColor: 'rgb(146, 86, 217, 0.4)',
borderColor: 'rgb(122, 66, 191, 0.4)',
backgroundColor: 'rgb(38, 128, 235, 0.4)',
borderColor: 'rgb(13, 102, 208, 0.4)',
borderWidth: 1,
},
{
label: 'page views',
data: data.pageviews,
lineTension: 0,
backgroundColor: 'rgb(38, 128, 235, 0.4)',
borderColor: 'rgb(13, 102, 208, 0.4)',
backgroundColor: 'rgb(38, 128, 235, 0.2)',
borderColor: 'rgb(13, 102, 208, 0.2)',
borderWidth: 1,
},
],
@ -165,7 +165,9 @@ const Tooltip = ({ title, value, label, labelColor }) => (
<div className={styles.content}>
<div className={styles.title}>{title}</div>
<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}
</div>
</div>

View File

@ -30,9 +30,14 @@
}
.dot {
position: relative;
overflow: hidden;
border-radius: 100%;
margin-right: 8px;
background: #fff;
}
.color {
width: 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 { useSpring, animated } from 'react-spring';
import { useSpring, animated, config } from 'react-spring';
import classNames from 'classnames';
import CheckVisible from './CheckVisible';
import { get } from 'lib/web';
import { percentFilter } from 'lib/filters';
import styles from './RankingsChart.module.css';
@ -14,7 +15,6 @@ export default function RankingsChart({
heading,
className,
dataFilter,
animate = true,
onDataLoad = () => {},
}) {
const [data, setData] = useState();
@ -50,43 +50,42 @@ export default function RankingsChart({
}
return (
<div className={classNames(styles.container, className)}>
<div className={styles.header}>
<div className={styles.title}>{title}</div>
<div className={styles.heading}>{heading}</div>
</div>
{rankings.map(({ x, y, z }) =>
animate ? (
<AnimatedRow key={x} label={x} value={y} percent={z} />
) : (
<Row key={x} label={x} value={y} percent={z} />
),
<CheckVisible>
{visible => (
<div className={classNames(styles.container, className)}>
<div className={styles.header}>
<div className={styles.title}>{title}</div>
<div className={styles.heading}>{heading}</div>
</div>
{rankings.map(({ x, y, z }) => (
<Row key={x} label={x} value={y} percent={z} animate={visible} />
))}
</div>
)}
</div>
</CheckVisible>
);
}
const Row = ({ label, value, percent }) => (
<div className={styles.row}>
<div className={styles.label}>{label}</div>
<div className={styles.value}>{value.toFixed(0)}</div>
<div className={styles.percent}>
<div>{`${percent.toFixed(0)}%`}</div>
<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 } });
const Row = ({ label, value, percent, animate }) => {
const props = useSpring({
width: percent,
y: value,
from: { width: 0, y: 0 },
config: animate ? config.default : { duration: 0 },
});
return (
<div className={styles.row}>
<div className={styles.label}>{label}</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={{ width: props.width }} />
<animated.div
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>
);

View File

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

View File

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

View File

@ -13,7 +13,7 @@
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding: 10px 0;
}
.sticky {
@ -21,7 +21,6 @@
top: 0;
margin: auto;
background: #fff;
padding: 10px 0;
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 (
<>
<div className={styles.container}>
<div className="row">
<div className={classNames(styles.chart, 'col')}>
<h1>{data.label}</h1>
<CheckVisible>
{visible => (
<WebsiteChart
websiteId={data.website_id}
onDateChange={handleDateChange}
animate={visible}
stickHeader
/>
)}
</CheckVisible>
<h2>{data.label}</h2>
<WebsiteChart websiteId={websiteId} onDateChange={handleDateChange} stickHeader />
</div>
</div>
<div className={classNames(styles.row, 'row')}>
<div className={pageviewClasses}>
<CheckVisible>
{visible => (
<RankingsChart
title="Pages"
type="url"
heading="Views"
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
dataFilter={urlFilter}
animate={visible}
/>
)}
</CheckVisible>
<RankingsChart
title="Pages"
type="url"
heading="Views"
websiteId={websiteId}
startDate={startDate}
endDate={endDate}
dataFilter={urlFilter}
/>
</div>
<div className={pageviewClasses}>
<CheckVisible>
{visible => (
<RankingsChart
title="Referrers"
type="referrer"
heading="Views"
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
dataFilter={refFilter}
animate={visible}
/>
)}
</CheckVisible>
<RankingsChart
title="Referrers"
type="referrer"
heading="Views"
websiteId={websiteId}
startDate={startDate}
endDate={endDate}
dataFilter={refFilter}
/>
</div>
</div>
<div className={classNames(styles.row, 'row')}>
<div className={sessionClasses}>
<CheckVisible>
{visible => (
<RankingsChart
title="Browsers"
type="browser"
heading="Visitors"
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
dataFilter={browserFilter}
animate={visible}
/>
)}
</CheckVisible>
<RankingsChart
title="Browsers"
type="browser"
heading="Visitors"
websiteId={websiteId}
startDate={startDate}
endDate={endDate}
dataFilter={browserFilter}
/>
</div>
<div className={sessionClasses}>
<CheckVisible>
{visible => (
<RankingsChart
title="Operating system"
type="os"
heading="Visitors"
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
animate={visible}
/>
)}
</CheckVisible>
<RankingsChart
title="Operating system"
type="os"
heading="Visitors"
websiteId={websiteId}
startDate={startDate}
endDate={endDate}
/>
</div>
<div className={sessionClasses}>
<CheckVisible>
{visible => (
<RankingsChart
title="Devices"
type="screen"
heading="Visitors"
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
dataFilter={deviceFilter}
animate={visible}
/>
)}
</CheckVisible>
<RankingsChart
title="Devices"
type="screen"
heading="Visitors"
websiteId={websiteId}
startDate={startDate}
endDate={endDate}
dataFilter={deviceFilter}
/>
</div>
</div>
<div className={classNames(styles.row, 'row')}>
@ -141,23 +107,18 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
<WorldMap data={countryData} />
</div>
<div className="col-12 col-md-12 col-lg-4">
<CheckVisible>
{visible => (
<RankingsChart
title="Countries"
type="country"
heading="Visitors"
websiteId={data.website_id}
startDate={startDate}
endDate={endDate}
dataFilter={countryFilter}
onDataLoad={data => setCountryData(data)}
animate={visible}
/>
)}
</CheckVisible>
<RankingsChart
title="Countries"
type="country"
heading="Visitors"
websiteId={websiteId}
startDate={startDate}
endDate={endDate}
dataFilter={countryFilter}
onDataLoad={data => setCountryData(data)}
/>
</div>
</div>
</>
</div>
);
}

View File

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

View File

@ -1,3 +1,32 @@
.container > div {
padding-bottom: 30px;
border-bottom: 1px solid #e1e1e1;
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 {
overflow: hidden;
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(
prisma.website.findOne({
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(
prisma.session.findOne({
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);
if (website_uuid) {
const website = await getWebsite(website_uuid);
const website = await getWebsite({ website_uuid });
if (website) {
const { website_id } = website;
const session_uuid = uuid(website_id, hostname, ip, userAgent, os);
let session = await getSession(session_uuid);
let session = await getSession({ session_uuid });
if (!session) {
session = await createSession(website_id, {

View File

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

View File

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

View File

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

View File

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