diff --git a/components/WebsiteDetails.js b/components/WebsiteDetails.js
new file mode 100644
index 00000000..ba6d56c0
--- /dev/null
+++ b/components/WebsiteDetails.js
@@ -0,0 +1,118 @@
+import React, { useEffect, useState } from 'react';
+import classNames from 'classnames';
+import { get } from 'lib/web';
+import WebsiteChart from './WebsiteChart';
+import RankingsChart from './RankingsChart';
+import { getDateRange } from '../lib/date';
+import styles from './WebsiteDetails.module.css';
+
+const osFilter = data => data.map(({ x, y }) => ({ x: !x ? 'Unknown' : x, y }));
+
+const urlFilter = data => data.filter(({ x }) => x !== '' && !x.startsWith('#'));
+
+const refFilter = data =>
+ data.filter(({ x }) => x !== '' && !x.startsWith('/') && !x.startsWith('#'));
+
+const deviceFilter = data => {
+ const devices = data.reduce(
+ (obj, { x, y }) => {
+ const [width] = x.split('x');
+ if (width >= 1920) {
+ obj.desktop += +y;
+ } else if (width >= 1024) {
+ obj.laptop += +y;
+ } else if (width >= 767) {
+ obj.tablet += +y;
+ } else {
+ obj.mobile += +y;
+ }
+ return obj;
+ },
+ { desktop: 0, laptop: 0, tablet: 0, mobile: 0 },
+ );
+
+ return Object.keys(devices).map(key => ({ x: key, y: devices[key] }));
+};
+
+export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) {
+ const [data, setData] = useState();
+ const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
+ const { startDate, endDate } = dateRange;
+
+ async function loadData() {
+ setData(await get(`/api/website/${websiteId}`));
+ }
+
+ function handleDateChange(values) {
+ setDateRange(values);
+ }
+
+ useEffect(() => {
+ if (websiteId) {
+ loadData();
+ }
+ }, [websiteId]);
+
+ if (!data) {
+ return
loading...
;
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/components/WebsiteDetails.module.css b/components/WebsiteDetails.module.css
new file mode 100644
index 00000000..8497bc84
--- /dev/null
+++ b/components/WebsiteDetails.module.css
@@ -0,0 +1,3 @@
+.row {
+ margin: 20px 0;
+}
diff --git a/components/WebsiteList.js b/components/WebsiteList.js
index 225d462b..e0667d1b 100644
--- a/components/WebsiteList.js
+++ b/components/WebsiteList.js
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
+import Link from 'next/link';
import { get } from 'lib/web';
import WebsiteChart from './WebsiteChart';
import styles from './WebsiteList.module.css';
@@ -17,8 +18,15 @@ export default function WebsiteList() {
return (
{data &&
- data.websites.map(({ website_id, label }) => (
-
+ data.websites.map(({ website_id, website_uuid, label }) => (
+ <>
+
+
+ >
))}
);
diff --git a/lib/db.js b/lib/db.js
index 8b3ae6d7..244a4d2b 100644
--- a/lib/db.js
+++ b/lib/db.js
@@ -154,6 +154,24 @@ export async function getPageviews(website_id, start_at, end_at) {
);
}
+export async function getRankings(website_id, start_at, end_at, type, table) {
+ return runQuery(
+ prisma.queryRaw(
+ `
+ select distinct "${type}" x, count(*) y
+ from "${table}"
+ where website_id=$1
+ and created_at between $2 and $3
+ group by 1
+ order by 2 desc
+ `,
+ website_id,
+ start_at,
+ end_at,
+ ),
+ );
+}
+
export async function getPageviewData(
website_id,
start_at,
diff --git a/lib/format.js b/lib/format.js
index c00f84d0..35d173a2 100644
--- a/lib/format.js
+++ b/lib/format.js
@@ -24,10 +24,6 @@ export function formatTime(val) {
}
export function formatShortTime(val, formats = ['m', 's'], space = '') {
- if (val <= 0) {
- return `0${formats[formats.length - 1]}`;
- }
-
const { days, hours, minutes, seconds, ms } = parseTime(val);
let t = '';
@@ -37,5 +33,9 @@ export function formatShortTime(val, formats = ['m', 's'], space = '') {
if (seconds > 0 && formats.indexOf('s') !== -1) t += `${seconds}s${space}`;
if (ms > 0 && formats.indexOf('ms') !== -1) t += `${ms}ms`;
+ if (!t) {
+ return `0${formats[formats.length - 1]}`;
+ }
+
return t;
}
diff --git a/pages/[id].js b/pages/[id].js
new file mode 100644
index 00000000..e9321edd
--- /dev/null
+++ b/pages/[id].js
@@ -0,0 +1,15 @@
+import React from 'react';
+import { useRouter } from 'next/router';
+import Layout from 'components/Layout';
+import WebsiteDetails from '../components/WebsiteDetails';
+
+export default function DetailsPage() {
+ const router = useRouter();
+ const { id } = router.query;
+
+ return (
+
+
+
+ );
+}
diff --git a/pages/api/website/[id]/index.js b/pages/api/website/[id]/index.js
new file mode 100644
index 00000000..0a215219
--- /dev/null
+++ b/pages/api/website/[id]/index.js
@@ -0,0 +1,12 @@
+import { getWebsite } from 'lib/db';
+import { useAuth } from 'lib/middleware';
+
+export default async (req, res) => {
+ await useAuth(req, res);
+
+ const { id } = req.query;
+
+ const website = await getWebsite(id);
+
+ return res.status(200).json(website);
+};
diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/website/[id]/pageviews.js
index 1987f72f..a2a2fbab 100644
--- a/pages/api/website/[id]/pageviews.js
+++ b/pages/api/website/[id]/pageviews.js
@@ -2,12 +2,14 @@ import moment from 'moment-timezone';
import { getPageviewData } from 'lib/db';
import { useAuth } from 'lib/middleware';
+const unitTypes = ['month', 'hour', 'day'];
+
export default async (req, res) => {
await useAuth(req, res);
const { id, start_at, end_at, unit, tz } = req.query;
- if (!moment.tz.zone(tz) || !['month', 'hour', 'day'].includes(unit)) {
+ if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
return res.status(400).end();
}
diff --git a/pages/api/website/[id]/rankings.js b/pages/api/website/[id]/rankings.js
new file mode 100644
index 00000000..8d76e5fb
--- /dev/null
+++ b/pages/api/website/[id]/rankings.js
@@ -0,0 +1,21 @@
+import { getRankings } from 'lib/db';
+import { useAuth } from 'lib/middleware';
+
+const sessionColumns = ['browser', 'os', 'screen'];
+const pageviewColumns = ['url', 'referrer'];
+
+export default async (req, res) => {
+ await useAuth(req, res);
+
+ const { id, type, start_at, end_at } = req.query;
+
+ if (!sessionColumns.includes(type) && !pageviewColumns.includes(type)) {
+ return res.status(400).end();
+ }
+
+ const table = sessionColumns.includes(type) ? 'session' : 'pageview';
+
+ const rankings = await getRankings(+id, new Date(+start_at), new Date(+end_at), type, table);
+
+ return res.status(200).json(rankings);
+};