From ce92c7897d7e5be0fe2e25d0ee5752c51a18d439 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 28 Jul 2020 19:04:45 -0700 Subject: [PATCH] Display page views and unique visitors. --- components/DateFilter.js | 4 +- components/MetricCard.js | 11 +++++ components/MetricCard.module.css | 16 ++++++++ components/PageviewsChart.js | 33 +++++++++------ components/WebsiteList.js | 2 +- components/WebsiteStats.js | 23 +++++++++-- components/WebsiteSummary.js | 16 ++++++++ components/WebsiteSummary.module.css | 3 ++ lib/date.js | 60 +++++++++++++++++++++++----- lib/db.js | 15 ++++--- pages/api/auth.js | 6 +-- pages/api/collect.js | 2 +- pages/api/verify.js | 4 +- pages/api/website.js | 2 +- pages/api/website/[id]/pageviews.js | 7 +++- pages/api/website/[id]/summary.js | 2 +- 16 files changed, 162 insertions(+), 44 deletions(-) create mode 100644 components/MetricCard.js create mode 100644 components/MetricCard.module.css create mode 100644 components/WebsiteSummary.js create mode 100644 components/WebsiteSummary.module.css diff --git a/components/DateFilter.js b/components/DateFilter.js index 20a7bece..586044b0 100644 --- a/components/DateFilter.js +++ b/components/DateFilter.js @@ -15,7 +15,9 @@ export default function DateFilter({ onChange }) { return ( ); diff --git a/components/MetricCard.js b/components/MetricCard.js new file mode 100644 index 00000000..2a13176d --- /dev/null +++ b/components/MetricCard.js @@ -0,0 +1,11 @@ +import React from 'react'; +import styles from './MetricCard.module.css'; + +const MetricCard = ({ value, label }) => ( +
+
{value}
+
{label}
+
+); + +export default MetricCard; diff --git a/components/MetricCard.module.css b/components/MetricCard.module.css new file mode 100644 index 00000000..8434f640 --- /dev/null +++ b/components/MetricCard.module.css @@ -0,0 +1,16 @@ +.card { + display: flex; + flex-direction: column; + justify-content: center; + margin-right: 50px; +} + +.value { + font-size: 36px; + line-height: 40px; + font-weight: 600; +} + +.label { + font-size: 16px; +} diff --git a/components/PageviewsChart.js b/components/PageviewsChart.js index 98666d55..576e5997 100644 --- a/components/PageviewsChart.js +++ b/components/PageviewsChart.js @@ -1,16 +1,9 @@ -import React, { useRef, useEffect, useMemo } from 'react'; +import React, { useRef, useEffect } from 'react'; import ChartJS from 'chart.js'; -import { getLocalTime } from 'lib/date'; export default function PageviewsChart({ data }) { const canvas = useRef(); const chart = useRef(); - const pageviews = useMemo(() => { - if (data) { - return data.pageviews.map(({ t, y }) => ({ t: getLocalTime(t), y })); - } - return []; - }, [data]); function draw() { if (!canvas.current) return; @@ -21,11 +14,19 @@ export default function PageviewsChart({ data }) { data: { datasets: [ { - label: 'page views', - data: pageviews, + label: 'unique visitors', + data: data.uniques, lineTension: 0, - backgroundColor: 'rgb(38, 128, 235, 0.1)', - borderColor: 'rgb(13, 102, 208, 0.2)', + backgroundColor: 'rgb(146, 86, 217, 0.2)', + borderColor: 'rgb(122, 66, 191, 0.3)', + borderWidth: 1, + }, + { + label: 'page views', + data: data.pageviews, + lineTension: 0, + backgroundColor: 'rgb(38, 128, 235, 0.2)', + borderColor: 'rgb(13, 102, 208, 0.3)', borderWidth: 1, }, ], @@ -52,6 +53,10 @@ export default function PageviewsChart({ data }) { }, tooltipFormat: 'ddd M/DD hA', }, + gridLines: { + display: false, + }, + stacked: true, }, ], yAxes: [ @@ -59,13 +64,15 @@ export default function PageviewsChart({ data }) { ticks: { beginAtZero: true, }, + stacked: true, }, ], }, }, }); } else { - chart.current.data.datasets[0].data = pageviews; + chart.current.data.datasets[0].data = data.uniques; + chart.current.data.datasets[1].data = data.pageviews; chart.current.update(); } } diff --git a/components/WebsiteList.js b/components/WebsiteList.js index 0ff47a5d..4b1eee0f 100644 --- a/components/WebsiteList.js +++ b/components/WebsiteList.js @@ -26,7 +26,7 @@ export default function WebsiteList() { {data && data.websites.map(({ website_id, label }) => ( -
+

{label}

{ + if (data) { + return [ + getDateArray(data.pageviews, startDate, endDate, unit), + getDateArray(data.uniques, startDate, endDate, unit), + ]; + } + return [[], []]; + }, [data]); async function loadData() { setData( @@ -19,7 +29,12 @@ export default function WebsiteStats({ websiteId, startDate, endDate, unit }) { useEffect(() => { loadData(); - }, [websiteId, startDate, endDate]); + }, [websiteId, startDate, endDate, unit]); - return ; + return ( +
+ + +
+ ); } diff --git a/components/WebsiteSummary.js b/components/WebsiteSummary.js new file mode 100644 index 00000000..fb8eb244 --- /dev/null +++ b/components/WebsiteSummary.js @@ -0,0 +1,16 @@ +import React from 'react'; +import MetricCard from './MetricCard'; +import styles from './WebsiteSummary.module.css'; + +function getTotal(data) { + return data.reduce((n, v) => n + v.y, 0); +} + +export default function WebsiteSummary({ data }) { + return ( +
+ + +
+ ); +} diff --git a/components/WebsiteSummary.module.css b/components/WebsiteSummary.module.css new file mode 100644 index 00000000..abde53a3 --- /dev/null +++ b/components/WebsiteSummary.module.css @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/lib/date.js b/lib/date.js index f4fd272c..a3daf139 100644 --- a/lib/date.js +++ b/lib/date.js @@ -1,9 +1,24 @@ import moment from 'moment-timezone'; -import { addMinutes, endOfDay, subDays, subHours } from 'date-fns'; +import { + addMinutes, + addHours, + startOfDay, + endOfHour, + endOfDay, + startOfHour, + addDays, + subDays, + subHours, + differenceInHours, + differenceInDays, +} from 'date-fns'; export function getTimezone() { - const tz = moment.tz.guess(); - return moment.tz.zone(tz).abbr(new Date().getTimezoneOffset()); + return moment.tz.guess(); +} + +export function getTimezonAbbr() { + return moment.tz.zone(getTimezone()).abbr(new Date().getTimezoneOffset()); } export function getLocalTime(t) { @@ -12,26 +27,51 @@ export function getLocalTime(t) { export function getDateRange(value) { const now = new Date(); - const endToday = endOfDay(now); + const hour = endOfHour(now); + const day = endOfDay(now); switch (value) { case '7d': return { - startDate: subDays(endToday, 7), - endDate: endToday, + startDate: subDays(day, 7), + endDate: day, unit: 'day', }; case '30d': return { - startDate: subDays(endToday, 30), - endDate: endToday, + startDate: subDays(day, 30), + endDate: day, unit: 'day', }; default: return { - startDate: subHours(now, 24), - endDate: now, + startDate: subHours(hour, 24), + endDate: hour, unit: 'hour', }; } } + +const dateFuncs = { + hour: [differenceInHours, addHours, startOfHour], + day: [differenceInDays, addDays, startOfDay], +}; + +export function getDateArray(data, startDate, endDate, unit) { + const arr = []; + const [diff, add, normalize] = dateFuncs[unit]; + const n = diff(endDate, startDate); + + function findData(t) { + return data.find(e => getLocalTime(e.t).getTime() === normalize(t).getTime())?.y || 0; + } + + for (let i = 0; i < n; i++) { + const t = add(startDate, i + 1); + const y = findData(t); + + arr.push({ t, y }); + } + + return arr; +} diff --git a/lib/db.js b/lib/db.js index 76798450..198c52e2 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,4 +1,5 @@ import { PrismaClient } from '@prisma/client'; +import chalk from 'chalk'; const options = { log: [ @@ -9,13 +10,21 @@ const options = { ], }; +function logQuery(e) { + if (process.env.LOG_QUERY) { + console.log(chalk.yellow(e.params), '->', e.query, chalk.greenBright(`${e.duration}ms`)); + } +} + let prisma; if (process.env.NODE_ENV === 'production') { prisma = new PrismaClient(options); + prisma.on('query', logQuery); } else { if (!global.prisma) { global.prisma = new PrismaClient(options); + global.prisma.on('query', logQuery); } prisma = global.prisma; @@ -23,12 +32,6 @@ if (process.env.NODE_ENV === 'production') { export default prisma; -prisma.on('query', e => { - if (process.env.LOG_QUERY) { - console.log(`${e.params} -> ${e.query} (${e.duration}ms)`); - } -}); - export async function runQuery(query) { return query.catch(e => { console.error(e); diff --git a/pages/api/auth.js b/pages/api/auth.js index bd50a801..9170dee7 100644 --- a/pages/api/auth.js +++ b/pages/api/auth.js @@ -18,8 +18,8 @@ export default async (req, res) => { res.setHeader('Set-Cookie', [cookie]); - res.status(200).json({ token }); - } else { - res.status(401).end(); + return res.status(200).json({ token }); } + + return res.status(401).end(); }; diff --git a/pages/api/collect.js b/pages/api/collect.js index 5e977047..325b45d5 100644 --- a/pages/api/collect.js +++ b/pages/api/collect.js @@ -26,5 +26,5 @@ export default async (req, res) => { ok = true; } - res.status(200).json({ ok, session: token }); + return res.status(200).json({ ok, session: token }); }; diff --git a/pages/api/verify.js b/pages/api/verify.js index 4088172e..1af56731 100644 --- a/pages/api/verify.js +++ b/pages/api/verify.js @@ -5,8 +5,8 @@ export default async (req, res) => { try { const payload = await verifySecureToken(token); - res.status(200).json(payload); + return res.status(200).json(payload); } catch { - res.status(400).end(); + return res.status(400).end(); } }; diff --git a/pages/api/website.js b/pages/api/website.js index 0bd2478d..cb8f840a 100644 --- a/pages/api/website.js +++ b/pages/api/website.js @@ -8,5 +8,5 @@ export default async (req, res) => { const websites = await getWebsites(user_id); - res.status(200).json({ websites }); + return res.status(200).json({ websites }); }; diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/website/[id]/pageviews.js index 60f032cc..99e86949 100644 --- a/pages/api/website/[id]/pageviews.js +++ b/pages/api/website/[id]/pageviews.js @@ -1,3 +1,4 @@ +import moment from 'moment-timezone'; import { getPageviewData } from 'lib/db'; import { useAuth } from 'lib/middleware'; @@ -6,10 +7,14 @@ export default async (req, res) => { const { id, start_at, end_at, unit, tz } = req.query; + if (!moment.tz.zone(tz) || !['hour', 'day'].includes(unit)) { + return res.status(400).end(); + } + const [pageviews, uniques] = await Promise.all([ getPageviewData(+id, new Date(+start_at), new Date(+end_at), tz, unit, '*'), getPageviewData(+id, new Date(+start_at), new Date(+end_at), tz, unit, 'distinct session_id'), ]); - res.status(200).json({ pageviews, uniques }); + return res.status(200).json({ pageviews, uniques }); }; diff --git a/pages/api/website/[id]/summary.js b/pages/api/website/[id]/summary.js index ed7b7315..b6d35dc3 100644 --- a/pages/api/website/[id]/summary.js +++ b/pages/api/website/[id]/summary.js @@ -9,5 +9,5 @@ export default async (req, res) => { const pageviews = await getPageviews(+id, new Date(+start_at), new Date(+end_at)); - res.status(200).json({ pageviews }); + return res.status(200).json({ pageviews }); };