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 }) => (
+
+);
+
+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 });
};