Show active visitor count.

This commit is contained in:
Mike Cao 2020-08-18 00:51:32 -07:00
parent 9f6e17b986
commit b96cb0d975
14 changed files with 158 additions and 35 deletions

View File

@ -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' })
<Page>
<div className="row">
<div className={classNames(styles.chart, 'col')}>
<PageHeader>{data.name}</PageHeader>
<WebsiteHeader websiteId={websiteId} name={data.name} showLink={false} />
<WebsiteChart
websiteId={websiteId}
onDataLoad={handleDataLoad}

View File

@ -1,10 +1,9 @@
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import Link from 'components/common/Link';
import WebsiteHeader from 'components/charts/WebsiteHeader';
import WebsiteChart from 'components/charts/WebsiteChart';
import Page from 'components/layout/Page';
import Button from 'components/common/Button';
import PageHeader from 'components/layout/PageHeader';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import Arrow from 'assets/arrow-right.svg';
import { get } from 'lib/web';
@ -30,28 +29,7 @@ export default function WebsiteList() {
<Page>
{data?.map(({ website_id, name }) => (
<div key={website_id} className={styles.website}>
<PageHeader>
<div>
<Link
href="/website/[...id]"
as={`/website/${website_id}/${name}`}
className={styles.title}
>
{name}
</Link>
</div>
<Button
icon={<Arrow />}
onClick={() =>
router.push('/website/[...id]', `/website/${website_id}/${name}`, {
shallow: true,
})
}
size="small"
>
<div>View details</div>
</Button>
</PageHeader>
<WebsiteHeader websiteId={website_id} name={name} showLink />
<WebsiteChart key={website_id} title={name} websiteId={website_id} />
</div>
))}

View File

@ -8,7 +8,3 @@
border-bottom: 0;
margin-bottom: 0;
}
.button {
font-size: var(--font-size-small);
}

View File

@ -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 (
<div className={classNames(styles.container, className)}>
<div className={styles.dot} />
<div className={styles.text}>
<animated.div className={styles.value}>
{props.x.interpolate(x => x.toFixed(0))}
</animated.div>
<div>{`current vistor${count !== 1 ? 's' : ''}`}</div>
</div>
</div>
);
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 (
<PageHeader>
{showLink ? (
<Link href="/website/[...id]" as={`/website/${websiteId}/${name}`}>
<a className={styles.title}>{name}</a>
</Link>
) : (
<div className={styles.title}>{name}</div>
)}
<ActiveUsers className={styles.active} websiteId={websiteId} />
{showLink && (
<Button
icon={<Arrow />}
onClick={() =>
router.push('/website/[...id]', `/website/${websiteId}/${name}`, {
shallow: true,
})
}
size="small"
>
<div>View details</div>
</Button>
)}
</PageHeader>
);
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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,
},
];

View File

@ -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,
},
];

View File

@ -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),
);
}

View File

@ -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);
};

View File

@ -31,4 +31,9 @@
--red500: #d7373f;
--red600: #c9252d;
--red700: #bb121a;
--green400: #2d9d78;
--green500: #268e6c;
--green600: #12805c;
--green700: #107154;
}