diff --git a/components/DateFilter.js b/components/DateFilter.js
new file mode 100644
index 00000000..20a7bece
--- /dev/null
+++ b/components/DateFilter.js
@@ -0,0 +1,22 @@
+import React, { useState } from 'react';
+import { getDateRange } from 'lib/date';
+
+const filterOptions = ['24h', '7d', '30d'];
+
+export default function DateFilter({ onChange }) {
+ const [selected, setSelected] = useState('7d');
+
+ function handleChange(e) {
+ const value = e.target.value;
+ setSelected(value);
+ onChange(getDateRange(value));
+ }
+
+ return (
+
+ );
+}
diff --git a/components/Chart.js b/components/PageviewsChart.js
similarity index 68%
rename from components/Chart.js
rename to components/PageviewsChart.js
index 91e7bb39..98666d55 100644
--- a/components/Chart.js
+++ b/components/PageviewsChart.js
@@ -1,38 +1,28 @@
-import React, { useState, useMemo, useRef, useEffect } from 'react';
+import React, { useRef, useEffect, useMemo } from 'react';
import ChartJS from 'chart.js';
-import { get } from 'lib/web';
-import { getTimezone, getLocalTime } from 'lib/date';
+import { getLocalTime } from 'lib/date';
-export default function Chart({ websiteId, startDate, endDate }) {
- const [data, setData] = useState();
+export default function PageviewsChart({ data }) {
const canvas = useRef();
const chart = useRef();
- const metrics = useMemo(() => {
+ const pageviews = useMemo(() => {
if (data) {
return data.pageviews.map(({ t, y }) => ({ t: getLocalTime(t), y }));
}
+ return [];
}, [data]);
- console.log(metrics);
-
- async function loadData() {
- setData(
- await get(`/api/website/${websiteId}/pageviews`, {
- start_at: +startDate,
- end_at: +endDate,
- tz: getTimezone(),
- }),
- );
- }
function draw() {
- if (!chart.current && canvas.current) {
+ if (!canvas.current) return;
+
+ if (!chart.current) {
chart.current = new ChartJS(canvas.current, {
type: 'bar',
data: {
datasets: [
{
label: 'page views',
- data: Object.values(metrics),
+ data: pageviews,
lineTension: 0,
backgroundColor: 'rgb(38, 128, 235, 0.1)',
borderColor: 'rgb(13, 102, 208, 0.2)',
@@ -74,18 +64,17 @@ export default function Chart({ websiteId, startDate, endDate }) {
},
},
});
+ } else {
+ chart.current.data.datasets[0].data = pageviews;
+ chart.current.update();
}
}
useEffect(() => {
- loadData();
- }, []);
-
- useEffect(() => {
- if (metrics) {
+ if (data) {
draw();
}
- }, [metrics]);
+ }, [data]);
return (
diff --git a/components/WebsiteList.js b/components/WebsiteList.js
new file mode 100644
index 00000000..0ff47a5d
--- /dev/null
+++ b/components/WebsiteList.js
@@ -0,0 +1,41 @@
+import React, { useState, useEffect } from 'react';
+import { get } from 'lib/web';
+import WebsiteStats from './WebsiteStats';
+import DateFilter from './DateFilter';
+import { getDateRange } from '../lib/date';
+
+export default function WebsiteList() {
+ const [data, setData] = useState();
+ const [dateRange, setDateRange] = useState(getDateRange('7d'));
+ const { startDate, endDate, unit } = dateRange;
+
+ async function loadData() {
+ setData(await get(`/api/website`));
+ }
+
+ function handleDateChange(value) {
+ setDateRange(value);
+ }
+
+ useEffect(() => {
+ loadData();
+ }, []);
+
+ return (
+
+
+ {data &&
+ data.websites.map(({ website_id, label }) => (
+
+
{label}
+
+
+ ))}
+
+ );
+}
diff --git a/components/WebsiteStats.js b/components/WebsiteStats.js
new file mode 100644
index 00000000..c5d10f6a
--- /dev/null
+++ b/components/WebsiteStats.js
@@ -0,0 +1,25 @@
+import React, { useState, useEffect } from 'react';
+import PageviewsChart from './PageviewsChart';
+import { get } from 'lib/web';
+import { getTimezone } from 'lib/date';
+
+export default function WebsiteStats({ websiteId, startDate, endDate, unit }) {
+ const [data, setData] = useState();
+
+ async function loadData() {
+ setData(
+ await get(`/api/website/${websiteId}/pageviews`, {
+ start_at: +startDate,
+ end_at: +endDate,
+ unit,
+ tz: getTimezone(),
+ }),
+ );
+ }
+
+ useEffect(() => {
+ loadData();
+ }, [websiteId, startDate, endDate]);
+
+ return
;
+}
diff --git a/lib/date.js b/lib/date.js
index 659e1b9c..f4fd272c 100644
--- a/lib/date.js
+++ b/lib/date.js
@@ -1,5 +1,5 @@
import moment from 'moment-timezone';
-import { addMinutes } from 'date-fns';
+import { addMinutes, endOfDay, subDays, subHours } from 'date-fns';
export function getTimezone() {
const tz = moment.tz.guess();
@@ -9,3 +9,29 @@ export function getTimezone() {
export function getLocalTime(t) {
return addMinutes(new Date(t), new Date().getTimezoneOffset());
}
+
+export function getDateRange(value) {
+ const now = new Date();
+ const endToday = endOfDay(now);
+
+ switch (value) {
+ case '7d':
+ return {
+ startDate: subDays(endToday, 7),
+ endDate: endToday,
+ unit: 'day',
+ };
+ case '30d':
+ return {
+ startDate: subDays(endToday, 30),
+ endDate: endToday,
+ unit: 'day',
+ };
+ default:
+ return {
+ startDate: subHours(now, 24),
+ endDate: now,
+ unit: 'hour',
+ };
+ }
+}
diff --git a/lib/db.js b/lib/db.js
index a02b906d..76798450 100644
--- a/lib/db.js
+++ b/lib/db.js
@@ -1,13 +1,27 @@
import { PrismaClient } from '@prisma/client';
-export const prisma = new PrismaClient({
+const options = {
log: [
{
emit: 'event',
level: 'query',
},
],
-});
+};
+
+let prisma;
+
+if (process.env.NODE_ENV === 'production') {
+ prisma = new PrismaClient(options);
+} else {
+ if (!global.prisma) {
+ global.prisma = new PrismaClient(options);
+ }
+
+ prisma = global.prisma;
+}
+
+export default prisma;
prisma.on('query', e => {
if (process.env.LOG_QUERY) {
diff --git a/lib/web.js b/lib/web.js
index 3bd967a9..d3430600 100644
--- a/lib/web.js
+++ b/lib/web.js
@@ -7,17 +7,20 @@ export const apiRequest = (method, url, body) =>
'Content-Type': 'application/json',
},
body,
- }).then(res => (res.ok ? res.json() : null));
+ }).then(res => {
+ if (res.ok) {
+ return res.json();
+ }
+ return 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;
- }, []);
+function parseQuery(url, params = {}) {
+ const query = 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;
}
diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/website/[id]/pageviews.js
index e9570a7b..60f032cc 100644
--- a/pages/api/website/[id]/pageviews.js
+++ b/pages/api/website/[id]/pageviews.js
@@ -4,11 +4,11 @@ import { useAuth } from 'lib/middleware';
export default async (req, res) => {
await useAuth(req, res);
- const { id, start_at, end_at, tz } = req.query;
+ const { id, start_at, end_at, unit, tz } = req.query;
const [pageviews, uniques] = await Promise.all([
- getPageviewData(+id, new Date(+start_at), new Date(+end_at), tz, 'day', '*'),
- getPageviewData(+id, new Date(+start_at), new Date(+end_at), tz, 'day', 'distinct session_id'),
+ 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 });
diff --git a/pages/index.js b/pages/index.js
index 0bca544e..5804baa9 100644
--- a/pages/index.js
+++ b/pages/index.js
@@ -2,9 +2,10 @@ import React from 'react';
import Link from 'next/link';
import { parse } from 'cookie';
import Layout from 'components/Layout';
-import Chart from 'components/Chart';
+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 (
@@ -12,8 +13,9 @@ export default function HomePage({ username }) {
You've successfully logged in as {username}.
+
-