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}.

+
-