mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-22 09:57:00 +01:00
Show active visitor count.
This commit is contained in:
parent
9f6e17b986
commit
b96cb0d975
@ -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}
|
||||
|
@ -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>
|
||||
))}
|
||||
|
@ -8,7 +8,3 @@
|
||||
border-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
45
components/charts/ActiveUsers.js
Normal file
45
components/charts/ActiveUsers.js
Normal 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>
|
||||
);
|
||||
}
|
22
components/charts/ActiveUsers.module.css
Normal file
22
components/charts/ActiveUsers.module.css
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
38
components/charts/WebsiteHeader.js
Normal file
38
components/charts/WebsiteHeader.js
Normal 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>
|
||||
);
|
||||
}
|
15
components/charts/WebsiteHeader.module.css
Normal file
15
components/charts/WebsiteHeader.module.css
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
11
pages/api/website/[id]/active.js
Normal file
11
pages/api/website/[id]/active.js
Normal 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);
|
||||
};
|
@ -31,4 +31,9 @@
|
||||
--red500: #d7373f;
|
||||
--red600: #c9252d;
|
||||
--red700: #bb121a;
|
||||
|
||||
--green400: #2d9d78;
|
||||
--green500: #268e6c;
|
||||
--green600: #12805c;
|
||||
--green700: #107154;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user