diff --git a/components/WebsiteDetails.js b/components/WebsiteDetails.js index bb29f2dd..7d6de00e 100644 --- a/components/WebsiteDetails.js +++ b/components/WebsiteDetails.js @@ -4,7 +4,7 @@ import WebsiteChart from 'components/charts/WebsiteChart'; import RankingsChart from 'components/charts/RankingsChart'; import WorldMap from 'components/common/WorldMap'; import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; +import WebsiteHeader from 'components/charts/WebsiteHeader'; import MenuLayout from 'components/layout/MenuLayout'; import Button from 'components/common/Button'; import { getDateRange } from 'lib/date'; @@ -88,7 +88,7 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
- {data.name} + {data?.map(({ website_id, name }) => (
- -
- - {name} - -
- -
+
))} diff --git a/components/WebsiteList.module.css b/components/WebsiteList.module.css index b6c4acca..942b71b2 100644 --- a/components/WebsiteList.module.css +++ b/components/WebsiteList.module.css @@ -8,7 +8,3 @@ border-bottom: 0; margin-bottom: 0; } - -.button { - font-size: var(--font-size-small); -} diff --git a/components/charts/ActiveUsers.js b/components/charts/ActiveUsers.js new file mode 100644 index 00000000..53fe8480 --- /dev/null +++ b/components/charts/ActiveUsers.js @@ -0,0 +1,45 @@ +import React, { useState, useEffect } from 'react'; +import { useSpring, animated } from 'react-spring'; +import classNames from 'classnames'; +import { get } from 'lib/web'; +import styles from './ActiveUsers.module.css'; + +export default function ActiveUsers({ websiteId, className }) { + const [count, setCount] = useState(0); + + async function loadData() { + const result = await get(`/api/website/${websiteId}/active`); + setCount(result?.[0]?.x); + } + + const props = useSpring({ + x: count, + from: { x: 0 }, + }); + + useEffect(() => { + loadData(); + + const id = setInterval(() => loadData(), 10000); + + return () => { + clearInterval(id); + }; + }, []); + + if (count === 0) { + return null; + } + + return ( +
+
+
+ + {props.x.interpolate(x => x.toFixed(0))} + +
{`current vistor${count !== 1 ? 's' : ''}`}
+
+
+ ); +} diff --git a/components/charts/ActiveUsers.module.css b/components/charts/ActiveUsers.module.css new file mode 100644 index 00000000..b9152226 --- /dev/null +++ b/components/charts/ActiveUsers.module.css @@ -0,0 +1,22 @@ +.container { + display: flex; + align-items: center; +} + +.text { + display: flex; + font-size: var(--font-size-normal); +} + +.value { + font-weight: 600; + margin-right: 4px; +} + +.dot { + background: var(--green400); + width: 10px; + height: 10px; + border-radius: 100%; + margin-right: 10px; +} diff --git a/components/charts/QuickButtons.module.css b/components/charts/QuickButtons.module.css index 00c490a6..c5c6e3c7 100644 --- a/components/charts/QuickButtons.module.css +++ b/components/charts/QuickButtons.module.css @@ -20,7 +20,7 @@ font-weight: 600; } -@media only screen and (max-width: 720px) { +@media only screen and (max-width: 768px) { .buttons button:last-child { display: none; } diff --git a/components/charts/WebsiteHeader.js b/components/charts/WebsiteHeader.js new file mode 100644 index 00000000..47166228 --- /dev/null +++ b/components/charts/WebsiteHeader.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { useRouter } from 'next/router'; +import PageHeader from 'components/layout/PageHeader'; +import Link from 'components/common/Link'; +import Button from 'components/common/Button'; +import ActiveUsers from './ActiveUsers'; +import Arrow from 'assets/arrow-right.svg'; +import styles from './WebsiteHeader.module.css'; + +export default function WebsiteHeader({ websiteId, name, showLink = false }) { + const router = useRouter(); + + return ( + + {showLink ? ( + + {name} + + ) : ( +
{name}
+ )} + + {showLink && ( + + )} +
+ ); +} diff --git a/components/charts/WebsiteHeader.module.css b/components/charts/WebsiteHeader.module.css new file mode 100644 index 00000000..99dbd8bc --- /dev/null +++ b/components/charts/WebsiteHeader.module.css @@ -0,0 +1,15 @@ +.title { + color: var(--gray-900); + font-size: var(--font-size-large); + line-height: var(--font-size-large); +} + +.button { + font-size: var(--font-size-small); +} + +@media only screen and (max-width: 576px) { + .active { + display: none; + } +} diff --git a/components/layout/PageHeader.module.css b/components/layout/PageHeader.module.css index 2a7b3c55..74f7d1a2 100644 --- a/components/layout/PageHeader.module.css +++ b/components/layout/PageHeader.module.css @@ -3,6 +3,5 @@ justify-content: space-between; align-items: center; align-content: center; - line-height: 80px; - font-size: var(--font-size-large); + min-height: 80px; } diff --git a/components/settings/AccountSettings.js b/components/settings/AccountSettings.js index 4383d26d..e888b782 100644 --- a/components/settings/AccountSettings.js +++ b/components/settings/AccountSettings.js @@ -44,7 +44,7 @@ export default function AccountSettings() { render: Checkmark, }, { - className: classNames(styles.buttons, 'col-12 col-md-4'), + className: classNames(styles.buttons, 'col-12 col-md-4 pt-2 pt-md-0'), render: Buttons, }, ]; diff --git a/components/settings/WebsiteSettings.js b/components/settings/WebsiteSettings.js index 640f5764..1148497b 100644 --- a/components/settings/WebsiteSettings.js +++ b/components/settings/WebsiteSettings.js @@ -58,7 +58,7 @@ export default function WebsiteSettings() { { key: 'domain', label: 'Domain', className: 'col-6 col-md-4' }, { key: 'action', - className: classNames(styles.buttons, 'col-12 col-md-4 pt-1'), + className: classNames(styles.buttons, 'col-12 col-md-4 pt-2 pt-md-0'), render: Buttons, }, ]; diff --git a/lib/queries.js b/lib/queries.js index e698f9b5..387520f2 100644 --- a/lib/queries.js +++ b/lib/queries.js @@ -1,5 +1,6 @@ import moment from 'moment-timezone'; import prisma, { runQuery } from 'lib/db'; +import { subMinutes } from 'date-fns'; const POSTGRESQL = 'postgresql'; const MYSQL = 'mysql'; @@ -366,3 +367,16 @@ export function getRankings(website_id, start_at, end_at, type, table) { return Promise.resolve([]); } + +export function getActiveVisitors(website_id) { + return prisma.$queryRaw( + ` + select count(distinct session_id) x + from pageview + where website_id=$1 + and created_at >= $2 + `, + website_id, + subMinutes(new Date(), 5), + ); +} diff --git a/pages/api/website/[id]/active.js b/pages/api/website/[id]/active.js new file mode 100644 index 00000000..06731408 --- /dev/null +++ b/pages/api/website/[id]/active.js @@ -0,0 +1,11 @@ +import { getActiveVisitors } from 'lib/queries'; +import { ok } from 'lib/response'; + +export default async (req, res) => { + const { id } = req.query; + const website_id = +id; + + const result = await getActiveVisitors(website_id); + + return ok(res, result); +}; diff --git a/styles/variables.css b/styles/variables.css index 7a628d9f..ba504a19 100644 --- a/styles/variables.css +++ b/styles/variables.css @@ -31,4 +31,9 @@ --red500: #d7373f; --red600: #c9252d; --red700: #bb121a; + + --green400: #2d9d78; + --green500: #268e6c; + --green600: #12805c; + --green700: #107154; }