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