From 590a70c2ffe8df0c59a43164f0bef4aaf29c7598 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 26 Jul 2020 00:12:42 -0700 Subject: [PATCH] Chart component. Update web utils. --- components/Chart.js | 111 ++++++++++++++++++++++++++++ lib/db.js | 8 +- lib/web.js | 26 ++++++- pages/api/website/[id]/pageviews.js | 10 +++ pages/index.js | 10 ++- public/umami.js | 2 +- 6 files changed, 159 insertions(+), 8 deletions(-) create mode 100644 components/Chart.js create mode 100644 pages/api/website/[id]/pageviews.js diff --git a/components/Chart.js b/components/Chart.js new file mode 100644 index 00000000..1324e51b --- /dev/null +++ b/components/Chart.js @@ -0,0 +1,111 @@ +import React, { useState, useMemo, useRef, useEffect } from 'react'; +import ChartJS from 'chart.js'; +import { format, subDays, subHours, startOfHour } from 'date-fns'; +import { get } from 'lib/web'; + +export default function Chart({ websiteId, startDate, endDate }) { + const [data, setData] = useState(); + const canvas = useRef(); + const chart = useRef(); + const metrics = useMemo(() => { + if (data) { + const points = {}; + const now = startOfHour(new Date()); + + for (let i = 0; i <= 168; i++) { + const d = new Date(subHours(now, 168 - i)); + const key = format(d, 'yyyy-MM-dd-HH'); + points[key] = { t: startOfHour(d).toISOString(), y: 0 }; + } + + data.pageviews.forEach(e => { + const key = format(new Date(e.created_at), 'yyyy-MM-dd-HH'); + points[key].y += 1; + }); + + return points; + } + }, [data]); + console.log(metrics); + + async function loadData() { + setData( + await get(`/api/website/${websiteId}/pageviews`, { + start_at: startDate, + end_at: endDate, + }), + ); + } + + function draw() { + if (!chart.current && canvas.current) { + chart.current = new ChartJS(canvas.current, { + type: 'bar', + data: { + datasets: [ + { + label: 'page views', + data: Object.values(metrics), + lineTension: 0, + }, + ], + }, + options: { + animation: { + duration: 300, + }, + tooltips: { + intersect: false, + }, + hover: { + animationDuration: 0, + }, + scales: { + xAxes: [ + { + type: 'time', + distribution: 'series', + time: { + unit: 'hour', + displayFormats: { + hour: 'ddd M/DD', + }, + tooltipFormat: 'ddd M/DD hA', + }, + ticks: { + autoSkip: true, + minRotation: 0, + maxRotation: 0, + maxTicksLimit: 7, + }, + }, + ], + yAxes: [ + { + ticks: { + beginAtZero: true, + }, + }, + ], + }, + }, + }); + } + } + + useEffect(() => { + loadData(); + }, []); + + useEffect(() => { + if (metrics) { + draw(); + } + }, [metrics]); + + return ( +
+ +
+ ); +} diff --git a/lib/db.js b/lib/db.js index 29eaf809..bfc6de0e 100644 --- a/lib/db.js +++ b/lib/db.js @@ -11,7 +11,7 @@ export const prisma = new PrismaClient({ prisma.on('query', e => { if (process.env.LOG_QUERY) { - console.log(`${e.query} (${e.duration}ms)`); + console.log(`${e.params} -> ${e.query} (${e.duration}ms)`); } }); @@ -113,11 +113,15 @@ export async function getAccount(username = '') { ); } -export async function getPageviews(website_id) { +export async function getPageviews(website_id, start_at, end_at) { return runQuery( prisma.pageview.findMany({ where: { website_id, + created_at: { + gte: start_at, + lte: end_at, + }, }, }), ); diff --git a/lib/web.js b/lib/web.js index 0b8acb5a..3bd967a9 100644 --- a/lib/web.js +++ b/lib/web.js @@ -1,13 +1,31 @@ -export const post = (url, params) => +export const apiRequest = (method, url, body) => fetch(url, { - method: 'post', + method, cache: 'no-cache', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, - body: JSON.stringify(params), - }).then(res => (res.status === 200 ? res.json() : null)); + body, + }).then(res => (res.ok ? res.json() : null)); + +function parseQuery(url, params) { + const query = + params && + Object.keys(params).reduce((values, key) => { + if (params[key] !== undefined) { + return values.concat(`${key}=${encodeURIComponent(params[key])}`); + } + return values; + }, []); + return query.length ? `${url}?${query.join('&')}` : url; +} + +export const get = (url, params) => apiRequest('get', parseQuery(url, params)); + +export const post = (url, params) => apiRequest('post', url, JSON.stringify(params)); + +export const del = (url, params) => apiRequest('del', parseQuery(url, params)); export const hook = (_this, method, callback) => { const orig = _this[method]; diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/website/[id]/pageviews.js new file mode 100644 index 00000000..a4bfb18a --- /dev/null +++ b/pages/api/website/[id]/pageviews.js @@ -0,0 +1,10 @@ +import { getPageviews } from 'lib/db'; + +export default async (req, res) => { + console.log(req.query); + const { id, start_at, end_at } = req.query; + + const pageviews = await getPageviews(+id, new Date(+start_at), new Date(+end_at)); + + res.status(200).json({ pageviews }); +}; diff --git a/pages/index.js b/pages/index.js index c9a58dfd..c5187520 100644 --- a/pages/index.js +++ b/pages/index.js @@ -2,6 +2,7 @@ import React from 'react'; import Link from 'next/link'; import cookies from 'next-cookies'; import Layout from 'components/Layout'; +import Chart from 'components/Chart'; import { verifySecureToken } from 'lib/crypto'; export default function HomePage({ username }) { @@ -10,6 +11,13 @@ export default function HomePage({ username }) {

You've successfully logged in as {username}.

+
+ +
Logout 🡒 @@ -25,7 +33,7 @@ export async function getServerSideProps(context) { return { props: { - username: payload.username, + ...payload, }, }; } catch { diff --git a/public/umami.js b/public/umami.js index 210f8a56..d5580f55 100644 --- a/public/umami.js +++ b/public/umami.js @@ -1 +1 @@ -!function(){"use strict";function e(e){var t=this.constructor;return this.then((function(n){return t.resolve(e()).then((function(){return n}))}),(function(n){return t.resolve(e()).then((function(){return t.reject(n)}))}))}var t=setTimeout;function n(e){return Boolean(e&&void 0!==e.length)}function r(){}function o(e){if(!(this instanceof o))throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=void 0,this._deferreds=[],f(e,this)}function i(e,t){for(;3===e._state;)e=e._value;0!==e._state?(e._handled=!0,o._immediateFn((function(){var n=1===e._state?t.onFulfilled:t.onRejected;if(null!==n){var r;try{r=n(e._value)}catch(e){return void s(t.promise,e)}u(t.promise,r)}else(1===e._state?u:s)(t.promise,e._value)}))):e._deferreds.push(t)}function u(e,t){try{if(t===e)throw new TypeError("A promise cannot be resolved with itself.");if(t&&("object"==typeof t||"function"==typeof t)){var n=t.then;if(t instanceof o)return e._state=3,e._value=t,void a(e);if("function"==typeof n)return void f((r=n,i=t,function(){r.apply(i,arguments)}),e)}e._state=1,e._value=t,a(e)}catch(t){s(e,t)}var r,i}function s(e,t){e._state=2,e._value=t,a(e)}function a(e){2===e._state&&0===e._deferreds.length&&o._immediateFn((function(){e._handled||o._unhandledRejectionFn(e._value)}));for(var t=0,n=e._deferreds.length;t