mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-14 21:10:34 +01:00
DropDown component. Renamed files.
This commit is contained in:
parent
ff4492ffb5
commit
9f112c8cc9
@ -1,24 +1,18 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { getDateRange } from 'lib/date';
|
import { getDateRange } from 'lib/date';
|
||||||
|
import DropDown from './DropDown';
|
||||||
|
|
||||||
const filterOptions = ['24hour', '7day', '30day', '60day', '90day'];
|
const filterOptions = [
|
||||||
|
{ label: 'Last 24 hours', value: '24hour' },
|
||||||
|
{ label: 'Last 7 days', value: '7day' },
|
||||||
|
{ label: 'Last 30 days', value: '30day' },
|
||||||
|
{ label: 'Last 90 days', value: '90day' },
|
||||||
|
];
|
||||||
|
|
||||||
export default function DateFilter({ onChange }) {
|
export default function DateFilter({ value, onChange }) {
|
||||||
const [selected, setSelected] = useState('7day');
|
function handleChange(value) {
|
||||||
|
|
||||||
function handleChange(e) {
|
|
||||||
const value = e.target.value;
|
|
||||||
setSelected(value);
|
|
||||||
onChange(getDateRange(value));
|
onChange(getDateRange(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <DropDown value={value} options={filterOptions} onChange={handleChange} />;
|
||||||
<select value={selected} onChange={handleChange}>
|
|
||||||
{filterOptions.map(option => (
|
|
||||||
<option key={option} value={option}>
|
|
||||||
{option}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
47
components/DropDown.js
Normal file
47
components/DropDown.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import styles from './Dropdown.module.css';
|
||||||
|
|
||||||
|
export default function DropDown({ value, options = [], onChange }) {
|
||||||
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
|
const ref = useRef();
|
||||||
|
|
||||||
|
function handleShowMenu() {
|
||||||
|
setShowMenu(state => !state);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelect(value) {
|
||||||
|
onChange(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function hideMenu(e) {
|
||||||
|
if (ref.current && !ref.current.contains(e.target)) {
|
||||||
|
setShowMenu(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.addEventListener('click', hideMenu);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.removeEventListener('click', hideMenu);
|
||||||
|
};
|
||||||
|
}, [ref]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={styles.dropdown} onClick={handleShowMenu}>
|
||||||
|
<div className={styles.value}>
|
||||||
|
{options.find(e => e.value === value).label}
|
||||||
|
<div className={styles.caret} />
|
||||||
|
</div>
|
||||||
|
{showMenu && (
|
||||||
|
<div className={styles.menu}>
|
||||||
|
{options.map(({ label, value }) => (
|
||||||
|
<div key={value} className={styles.option} onClick={e => handleSelect(value, e)}>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
46
components/Dropdown.module.css
Normal file
46
components/Dropdown.module.css
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
.dropdown {
|
||||||
|
position: relative;
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
padding: 4px 24px 4px 8px;
|
||||||
|
border: 1px solid #b3b3b3;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: absolute;
|
||||||
|
min-width: 100px;
|
||||||
|
top: 100%;
|
||||||
|
margin-top: 4px;
|
||||||
|
border: 1px solid #b3b3b3;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option {
|
||||||
|
background: #fff;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option:hover {
|
||||||
|
background: #eaeaea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caret {
|
||||||
|
position: absolute;
|
||||||
|
height: 8px;
|
||||||
|
width: 8px;
|
||||||
|
border-right: 2px solid #8e8e8e;
|
||||||
|
border-bottom: 2px solid #8e8e8e;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
top: -4px;
|
||||||
|
bottom: 0;
|
||||||
|
right: 8px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
@ -2,15 +2,15 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import MetricCard from './MetricCard';
|
import MetricCard from './MetricCard';
|
||||||
import { get } from '../lib/web';
|
import { get } from '../lib/web';
|
||||||
import { formatShortTime } from 'lib/format';
|
import { formatShortTime } from 'lib/format';
|
||||||
import styles from './WebsiteSummary.module.css';
|
import styles from './MetricsBar.module.css';
|
||||||
|
|
||||||
export default function WebsiteSummary({ websiteId, startDate, endDate }) {
|
export default function MetricsBar({ websiteId, startDate, endDate }) {
|
||||||
const [data, setData] = useState({});
|
const [data, setData] = useState({});
|
||||||
const { pageviews, uniques, bounces, totaltime } = data;
|
const { pageviews, uniques, bounces, totaltime } = data;
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
setData(
|
setData(
|
||||||
await get(`/api/website/${websiteId}/summary`, {
|
await get(`/api/website/${websiteId}/metrics`, {
|
||||||
start_at: +startDate,
|
start_at: +startDate,
|
||||||
end_at: +endDate,
|
end_at: +endDate,
|
||||||
}),
|
}),
|
@ -3,7 +3,7 @@ import ChartJS from 'chart.js';
|
|||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import styles from './PageviewsChart.module.css';
|
import styles from './PageviewsChart.module.css';
|
||||||
|
|
||||||
export default function PageviewsChart({ data, unit }) {
|
export default function PageviewsChart({ data, unit, children }) {
|
||||||
const canvas = useRef();
|
const canvas = useRef();
|
||||||
const chart = useRef();
|
const chart = useRef();
|
||||||
const [tooltip, setTooltip] = useState({});
|
const [tooltip, setTooltip] = useState({});
|
||||||
@ -138,6 +138,7 @@ export default function PageviewsChart({ data, unit }) {
|
|||||||
<div className={styles.chart}>
|
<div className={styles.chart}>
|
||||||
<canvas ref={canvas} width={960} height={400} />
|
<canvas ref={canvas} width={960} height={400} />
|
||||||
<Tootip {...tooltip} />
|
<Tootip {...tooltip} />
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { getDateRange } from 'lib/date';
|
import { getDateRange } from 'lib/date';
|
||||||
import styles from './QuickButtons.module.css';
|
import styles from './QuickButtons.module.css';
|
||||||
@ -9,11 +9,8 @@ const options = {
|
|||||||
'30day': '30d',
|
'30day': '30d',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function QuickButtons({ onChange }) {
|
export default function QuickButtons({ value, onChange }) {
|
||||||
const [active, setActive] = useState('7day');
|
|
||||||
|
|
||||||
function handleClick(value) {
|
function handleClick(value) {
|
||||||
setActive(value);
|
|
||||||
onChange(getDateRange(value));
|
onChange(getDateRange(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,7 +19,7 @@ export default function QuickButtons({ onChange }) {
|
|||||||
{Object.keys(options).map(key => (
|
{Object.keys(options).map(key => (
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
className={classNames(styles.button, { [styles.active]: active === key })}
|
className={classNames(styles.button, { [styles.active]: value === key })}
|
||||||
onClick={() => handleClick(key)}
|
onClick={() => handleClick(key)}
|
||||||
>
|
>
|
||||||
{options[key]}
|
{options[key]}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
.buttons {
|
.buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
@ -11,6 +15,10 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.button:hover {
|
.button:hover {
|
||||||
background: #eaeaea;
|
background: #eaeaea;
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,12 @@ import React, { useState, useEffect, useMemo } from 'react';
|
|||||||
import PageviewsChart from './PageviewsChart';
|
import PageviewsChart from './PageviewsChart';
|
||||||
import { get } from 'lib/web';
|
import { get } from 'lib/web';
|
||||||
import { getDateArray, getDateRange, getTimezone } from 'lib/date';
|
import { getDateArray, getDateRange, getTimezone } from 'lib/date';
|
||||||
import WebsiteSummary from './WebsiteSummary';
|
import MetricsBar from './MetricsBar';
|
||||||
import QuickButtons from './QuickButtons';
|
import QuickButtons from './QuickButtons';
|
||||||
import styles from './WebsiteStats.module.css';
|
import styles from './WebsiteChart.module.css';
|
||||||
|
import DateFilter from './DateFilter';
|
||||||
|
|
||||||
export default function WebsiteStats({ title, websiteId }) {
|
export default function WebsiteChart({ title, websiteId }) {
|
||||||
const [data, setData] = useState();
|
const [data, setData] = useState();
|
||||||
const [dateRange, setDateRange] = useState(getDateRange('7day'));
|
const [dateRange, setDateRange] = useState(getDateRange('7day'));
|
||||||
const { startDate, endDate, unit } = dateRange;
|
const { startDate, endDate, unit } = dateRange;
|
||||||
@ -44,10 +45,12 @@ export default function WebsiteStats({ title, websiteId }) {
|
|||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.title}>{title}</div>
|
<div className={styles.title}>{title}</div>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<WebsiteSummary websiteId={websiteId} startDate={startDate} endDate={endDate} />
|
<MetricsBar websiteId={websiteId} startDate={startDate} endDate={endDate} />
|
||||||
<QuickButtons onChange={handleDateChange} />
|
<DateFilter value={dateRange.value} onChange={handleDateChange} />
|
||||||
</div>
|
</div>
|
||||||
<PageviewsChart data={{ pageviews, uniques }} unit={unit} />
|
<PageviewsChart data={{ pageviews, uniques }} unit={unit}>
|
||||||
|
<QuickButtons value={dateRange.value} onChange={handleDateChange} />
|
||||||
|
</PageviewsChart>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { get } from 'lib/web';
|
import { get } from 'lib/web';
|
||||||
import WebsiteStats from './WebsiteStats';
|
import WebsiteChart from './WebsiteChart';
|
||||||
import DateFilter from './DateFilter';
|
import DateFilter from './DateFilter';
|
||||||
import styles from './WebsiteList.module.css';
|
import styles from './WebsiteList.module.css';
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ export default function WebsiteList() {
|
|||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
{data &&
|
{data &&
|
||||||
data.websites.map(({ website_id, label }) => (
|
data.websites.map(({ website_id, label }) => (
|
||||||
<WebsiteStats key={website_id} title={label} websiteId={website_id} />
|
<WebsiteChart key={website_id} title={label} websiteId={website_id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
14
lib/date.js
14
lib/date.js
@ -2,13 +2,13 @@ import moment from 'moment-timezone';
|
|||||||
import {
|
import {
|
||||||
addMinutes,
|
addMinutes,
|
||||||
addHours,
|
addHours,
|
||||||
|
addDays,
|
||||||
|
subHours,
|
||||||
|
subDays,
|
||||||
|
startOfHour,
|
||||||
startOfDay,
|
startOfDay,
|
||||||
endOfHour,
|
endOfHour,
|
||||||
endOfDay,
|
endOfDay,
|
||||||
startOfHour,
|
|
||||||
addDays,
|
|
||||||
subDays,
|
|
||||||
subHours,
|
|
||||||
differenceInHours,
|
differenceInHours,
|
||||||
differenceInDays,
|
differenceInDays,
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
@ -17,10 +17,6 @@ export function getTimezone() {
|
|||||||
return moment.tz.guess();
|
return moment.tz.guess();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTimezonAbbr() {
|
|
||||||
return moment.tz.zone(getTimezone()).abbr(new Date().getTimezoneOffset());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLocalTime(t) {
|
export function getLocalTime(t) {
|
||||||
return addMinutes(new Date(t), new Date().getTimezoneOffset());
|
return addMinutes(new Date(t), new Date().getTimezoneOffset());
|
||||||
}
|
}
|
||||||
@ -38,12 +34,14 @@ export function getDateRange(value) {
|
|||||||
startDate: subDays(day, num),
|
startDate: subDays(day, num),
|
||||||
endDate: day,
|
endDate: day,
|
||||||
unit,
|
unit,
|
||||||
|
value,
|
||||||
};
|
};
|
||||||
case 'hour':
|
case 'hour':
|
||||||
return {
|
return {
|
||||||
startDate: subHours(hour, num),
|
startDate: subHours(hour, num),
|
||||||
endDate: hour,
|
endDate: hour,
|
||||||
unit,
|
unit,
|
||||||
|
value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -180,7 +180,7 @@ export async function getPageviewData(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSummary(website_id, start_at, end_at) {
|
export async function getMetrics(website_id, start_at, end_at) {
|
||||||
return runQuery(
|
return runQuery(
|
||||||
prisma.queryRaw(
|
prisma.queryRaw(
|
||||||
`
|
`
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { getSummary } from 'lib/db';
|
import { getMetrics } from 'lib/db';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
@ -6,14 +6,14 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
const { id, start_at, end_at } = req.query;
|
const { id, start_at, end_at } = req.query;
|
||||||
|
|
||||||
const summary = await getSummary(
|
const metrics = await getMetrics(
|
||||||
+id,
|
+id,
|
||||||
new Date(+start_at).toISOString(),
|
new Date(+start_at).toISOString(),
|
||||||
new Date(+end_at).toISOString(),
|
new Date(+end_at).toISOString(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const stats = Object.keys(summary[0]).reduce((obj, key) => {
|
const stats = Object.keys(metrics[0]).reduce((obj, key) => {
|
||||||
obj[key] = +summary[0][key];
|
obj[key] = +metrics[0][key];
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
@ -37,3 +37,9 @@ form label {
|
|||||||
form input {
|
form input {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid #b3b3b3;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user