mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-24 18:26:20 +01:00
Added reports section.
This commit is contained in:
parent
ad918c5bba
commit
a5700d4a25
1
assets/funnel.svg
Normal file
1
assets/funnel.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg enable-background="new 0 0 32 32" height="512" viewBox="0 0 32 32" width="512" xmlns="http://www.w3.org/2000/svg"><g id="_x31_6"><path d="m29 11h-26c-.5522 0-1-.4473-1-1v-6c0-.5527.4478-1 1-1h26c.5522 0 1 .4473 1 1v6c0 .5527-.4478 1-1 1zm-25-2h24v-4h-24z"/><path d="m25 17h-18c-.5522 0-1-.4473-1-1v-6c0-.5527.4478-1 1-1h18c.5522 0 1 .4473 1 1v6c0 .5527-.4478 1-1 1zm-17-2h16v-4h-16z"/><path d="m22 23h-12c-.5522 0-1-.4473-1-1v-6c0-.5527.4478-1 1-1h12c.5522 0 1 .4473 1 1v6c0 .5527-.4478 1-1 1zm-11-2h10v-4h-10z"/><path d="m19 29h-6c-.5522 0-1-.4473-1-1v-6c0-.5527.4478-1 1-1h6c.5522 0 1 .4473 1 1v6c0 .5527-.4478 1-1 1zm-5-2h4v-4h-4z"/></g></svg>
|
After Width: | Height: | Size: 651 B |
80
assets/lightbulb.svg
Normal file
80
assets/lightbulb.svg
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M223.718,124.76c-48.027,11.198-86.688,49.285-98.494,97.031c-11.843,47.899,1.711,96.722,36.259,130.601
|
||||||
|
C173.703,364.377,181,383.586,181,403.777V407c0,13.296,5.801,25.26,15,33.505V467c0,24.813,20.187,45,45,45h30
|
||||||
|
c24.813,0,45-20.187,45-45v-26.495c9.199-8.245,15-20.208,15-33.505v-3.282c0-19.884,7.687-39.458,20.563-52.361
|
||||||
|
C376.994,325.87,391,292.005,391,256C391,169.921,311.231,104.362,223.718,124.76z M286,467c0,8.271-6.729,15-15,15h-30
|
||||||
|
c-8.271,0-15-6.729-15-15v-15h60V467z M330.326,330.166C311.689,348.843,301,375.651,301,403.718V407c0,8.271-6.729,15-15,15h-60
|
||||||
|
c-8.271,0-15-6.729-15-15v-3.223c0-28.499-10.393-55.035-28.513-72.804c-26.89-26.37-37.409-64.493-28.141-101.981
|
||||||
|
c9.125-36.907,39.029-66.353,76.184-75.015C299.202,137.964,361,189.228,361,256C361,284.004,350.106,310.343,330.326,330.166z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M139.327,118.114L96.9,75.688c-5.857-5.858-15.355-5.858-21.213,0c-5.858,5.858-5.858,15.355,0,21.213l42.427,42.426
|
||||||
|
c5.857,5.858,15.356,5.858,21.213,0C145.185,133.469,145.185,123.972,139.327,118.114z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M76,241H15c-8.284,0-15,6.716-15,15s6.716,15,15,15h61c8.284,0,15-6.716,15-15S84.284,241,76,241z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M497,241h-61c-8.284,0-15,6.716-15,15s6.716,15,15,15h61c8.284,0,15-6.716,15-15S505.284,241,497,241z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M436.313,75.688c-5.856-5.858-15.354-5.858-21.213,0l-42.427,42.426c-5.858,5.857-5.858,15.355,0,21.213
|
||||||
|
c5.857,5.858,15.355,5.858,21.213,0l42.427-42.426C442.171,91.044,442.171,81.546,436.313,75.688z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M256,0c-8.284,0-15,6.716-15,15v61c0,8.284,6.716,15,15,15s15-6.716,15-15V15C271,6.716,264.284,0,256,0z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M256,181c-6.166,0-12.447,0.739-18.658,2.194c-25.865,6.037-47.518,27.328-53.879,52.979
|
||||||
|
c-1.994,8.041,2.907,16.175,10.947,18.17c8.042,1.994,16.176-2.909,18.17-10.948c3.661-14.758,16.647-27.5,31.593-30.989
|
||||||
|
C248.155,211.473,252.135,211,256,211c8.284,0,15-6.716,15-15S264.284,181,256,181z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
1
assets/nodes.svg
Normal file
1
assets/nodes.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="m19 9.87398c1.7252-.44404 3-2.01014 3-3.87398 0-2.20914-1.7909-4-4-4-1.8638 0-3.4299 1.27477-3.874 3h-4.25202c-.44404-1.72523-2.01014-3-3.87398-3-2.20914 0-4 1.79086-4 4 0 1.86384 1.27477 3.42994 3 3.87398v4.25202c-1.72523.4441-3 2.0102-3 3.874 0 2.2091 1.79086 4 4 4 1.86384 0 3.42994-1.2748 3.87398-3h4.25202c.4441 1.7252 2.0102 3 3.874 3 2.2091 0 4-1.7909 4-4 0-1.8638-1.2748-3.4299-3-3.874zm-13-5.87398c1.10457 0 2 .89543 2 2 0 1.1043-.895 1.99957-1.99919 2-1.10457 0-2.00081-.89543-2.00081-2s.89543-2 2-2zm3.87398 3c-.36178 1.40561-1.46837 2.5122-2.87398 2.87398v4.25202c1.40561.3618 2.5122 1.4684 2.87398 2.874h4.25202c.3618-1.4056 1.4684-2.5122 2.874-2.874v-4.25202c-1.4056-.36178-2.5122-1.46837-2.874-2.87398zm8.12602 1c-1.1046 0-2-.89543-2-2s.8954-2 2-2 2 .89543 2 2-.8954 2-2 2zm0 8c-1.1046 0-2 .8954-2 2s.8954 2 2 2 2-.8954 2-2-.8954-2-2-2zm-10 2c0-1.1046-.89543-2-2-2s-2 .8954-2 2 .89543 2 2 2 2-.8954 2-2z" fill="rgb(0,0,0)" fill-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -3,31 +3,22 @@ import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics';
|
|||||||
import { endOfYear, isSameDay } from 'date-fns';
|
import { endOfYear, isSameDay } from 'date-fns';
|
||||||
import DatePickerForm from 'components/metrics/DatePickerForm';
|
import DatePickerForm from 'components/metrics/DatePickerForm';
|
||||||
import useLocale from 'hooks/useLocale';
|
import useLocale from 'hooks/useLocale';
|
||||||
import { dateFormat, getDateRangeValues } from 'lib/date';
|
import { dateFormat } from 'lib/date';
|
||||||
import Icons from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import useApi from 'hooks/useApi';
|
|
||||||
import useDateRange from 'hooks/useDateRange';
|
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
|
|
||||||
export function DateFilter({ websiteId, value, className }) {
|
export function DateFilter({
|
||||||
|
value,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
className,
|
||||||
|
onChange,
|
||||||
|
showAllTime = false,
|
||||||
|
alignment = 'end',
|
||||||
|
}) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { get } = useApi();
|
|
||||||
const [dateRange, setDateRange] = useDateRange(websiteId);
|
|
||||||
const { startDate, endDate } = dateRange;
|
|
||||||
const [showPicker, setShowPicker] = useState(false);
|
const [showPicker, setShowPicker] = useState(false);
|
||||||
|
|
||||||
async function handleDateChange(value) {
|
|
||||||
if (value === 'all' && websiteId) {
|
|
||||||
const data = await get(`/websites/${websiteId}`);
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) });
|
|
||||||
}
|
|
||||||
} else if (value !== 'all') {
|
|
||||||
setDateRange(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
{ label: formatMessage(labels.today), value: '1day' },
|
{ label: formatMessage(labels.today), value: '1day' },
|
||||||
{
|
{
|
||||||
@ -61,7 +52,7 @@ export function DateFilter({ websiteId, value, className }) {
|
|||||||
value: '90day',
|
value: '90day',
|
||||||
},
|
},
|
||||||
{ label: formatMessage(labels.thisYear), value: '1year' },
|
{ label: formatMessage(labels.thisYear), value: '1year' },
|
||||||
websiteId && {
|
showAllTime && {
|
||||||
label: formatMessage(labels.allTime),
|
label: formatMessage(labels.allTime),
|
||||||
value: 'all',
|
value: 'all',
|
||||||
divider: true,
|
divider: true,
|
||||||
@ -86,12 +77,12 @@ export function DateFilter({ websiteId, value, className }) {
|
|||||||
setShowPicker(true);
|
setShowPicker(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleDateChange(value);
|
onChange(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePickerChange = value => {
|
const handlePickerChange = value => {
|
||||||
setShowPicker(false);
|
setShowPicker(false);
|
||||||
handleDateChange(value);
|
onChange(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => setShowPicker(false);
|
const handleClose = () => setShowPicker(false);
|
||||||
@ -103,7 +94,8 @@ export function DateFilter({ websiteId, value, className }) {
|
|||||||
items={options}
|
items={options}
|
||||||
renderValue={renderValue}
|
renderValue={renderValue}
|
||||||
value={value}
|
value={value}
|
||||||
alignment="end"
|
alignment={alignment}
|
||||||
|
placeholder={formatMessage(labels.selectDate)}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{({ label, value, divider }) => (
|
{({ label, value, divider }) => (
|
||||||
|
26
components/input/WebsiteDateFilter.js
Normal file
26
components/input/WebsiteDateFilter.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { getDateRangeValues } from 'lib/date';
|
||||||
|
import useApi from 'hooks/useApi';
|
||||||
|
import useDateRange from 'hooks/useDateRange';
|
||||||
|
import DateFilter from './DateFilter';
|
||||||
|
|
||||||
|
export default function WebsiteDateFilter({ websiteId, value }) {
|
||||||
|
const { get } = useApi();
|
||||||
|
const [dateRange, setDateRange] = useDateRange(websiteId);
|
||||||
|
const { startDate, endDate } = dateRange;
|
||||||
|
|
||||||
|
const handleChange = async value => {
|
||||||
|
if (value === 'all' && websiteId) {
|
||||||
|
const data = await get(`/websites/${websiteId}`);
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) });
|
||||||
|
}
|
||||||
|
} else if (value !== 'all') {
|
||||||
|
setDateRange(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DateFilter value={value} startDate={startDate} endDate={endDate} onChange={handleChange} />
|
||||||
|
);
|
||||||
|
}
|
@ -18,6 +18,7 @@ export function NavBar() {
|
|||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
{ label: formatMessage(labels.dashboard), url: '/dashboard' },
|
{ label: formatMessage(labels.dashboard), url: '/dashboard' },
|
||||||
|
{ label: formatMessage(labels.reports), url: '/reports' },
|
||||||
{ label: formatMessage(labels.realtime), url: '/realtime' },
|
{ label: formatMessage(labels.realtime), url: '/realtime' },
|
||||||
!cloudMode && { label: formatMessage(labels.settings), url: '/settings' },
|
!cloudMode && { label: formatMessage(labels.settings), url: '/settings' },
|
||||||
].filter(n => n);
|
].filter(n => n);
|
||||||
|
@ -97,6 +97,7 @@ export const labels = defineMessages({
|
|||||||
allTime: { id: 'label.all-time', defaultMessage: 'All time' },
|
allTime: { id: 'label.all-time', defaultMessage: 'All time' },
|
||||||
customRange: { id: 'label.custom-range', defaultMessage: 'Custom range' },
|
customRange: { id: 'label.custom-range', defaultMessage: 'Custom range' },
|
||||||
selectWebsite: { id: 'label.select-website', defaultMessage: 'Select website' },
|
selectWebsite: { id: 'label.select-website', defaultMessage: 'Select website' },
|
||||||
|
selectDate: { id: 'label.select-date', defaultMessage: 'Select date' },
|
||||||
all: { id: 'label.all', defaultMessage: 'All' },
|
all: { id: 'label.all', defaultMessage: 'All' },
|
||||||
sessions: { id: 'label.sessions', defaultMessage: 'Sessions' },
|
sessions: { id: 'label.sessions', defaultMessage: 'Sessions' },
|
||||||
pageNotFound: { id: 'message.page-not-found', defaultMessage: 'Page not found' },
|
pageNotFound: { id: 'message.page-not-found', defaultMessage: 'Page not found' },
|
||||||
@ -117,6 +118,8 @@ export const labels = defineMessages({
|
|||||||
view: { id: 'label.view', defaultMessage: 'View' },
|
view: { id: 'label.view', defaultMessage: 'View' },
|
||||||
cities: { id: 'label.cities', defaultMessage: 'Cities' },
|
cities: { id: 'label.cities', defaultMessage: 'Cities' },
|
||||||
regions: { id: 'label.regions', defaultMessage: 'Regions' },
|
regions: { id: 'label.regions', defaultMessage: 'Regions' },
|
||||||
|
reports: { id: 'label.reports', defaultMessage: 'Reports' },
|
||||||
|
eventData: { id: 'label.event-data', defaultMessage: 'Event data' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
|
@ -5,7 +5,7 @@ import classNames from 'classnames';
|
|||||||
import PageviewsChart from './PageviewsChart';
|
import PageviewsChart from './PageviewsChart';
|
||||||
import MetricsBar from './MetricsBar';
|
import MetricsBar from './MetricsBar';
|
||||||
import WebsiteHeader from './WebsiteHeader';
|
import WebsiteHeader from './WebsiteHeader';
|
||||||
import DateFilter from 'components/input/DateFilter';
|
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
|
||||||
import ErrorMessage from 'components/common/ErrorMessage';
|
import ErrorMessage from 'components/common/ErrorMessage';
|
||||||
import FilterTags from 'components/metrics/FilterTags';
|
import FilterTags from 'components/metrics/FilterTags';
|
||||||
import RefreshButton from 'components/input/RefreshButton';
|
import RefreshButton from 'components/input/RefreshButton';
|
||||||
@ -107,7 +107,7 @@ export function WebsiteChart({
|
|||||||
<Column defaultSize={12} xl={4}>
|
<Column defaultSize={12} xl={4}>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<RefreshButton websiteId={websiteId} isLoading={isLoading} />
|
<RefreshButton websiteId={websiteId} isLoading={isLoading} />
|
||||||
<DateFilter websiteId={websiteId} value={value} className={styles.dropdown} />
|
<WebsiteDateFilter websiteId={websiteId} value={value} className={styles.dropdown} />
|
||||||
</div>
|
</div>
|
||||||
</Column>
|
</Column>
|
||||||
</Row>
|
</Row>
|
||||||
|
33
components/pages/reports/EventDataReport.js
Normal file
33
components/pages/reports/EventDataReport.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Form, FormRow, FormInput, TextField } from 'react-basics';
|
||||||
|
import AppLayout from 'components/layout/AppLayout';
|
||||||
|
import Report from './Report';
|
||||||
|
import ReportHeader from './ReportHeader';
|
||||||
|
import useMessages from 'hooks/useMessages';
|
||||||
|
import Nodes from 'assets/nodes.svg';
|
||||||
|
import styles from './reports.module.css';
|
||||||
|
|
||||||
|
export default function EventDataReport({ websiteId, data }) {
|
||||||
|
const [values, setValues] = useState({ query: '' });
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<Report>
|
||||||
|
<ReportHeader title={formatMessage(labels.eventData)} icon={<Nodes />} />
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.menu}>
|
||||||
|
<Form>
|
||||||
|
<FormRow label="Properties">
|
||||||
|
<FormInput name="query">
|
||||||
|
<TextField value={values.query} />
|
||||||
|
</FormInput>
|
||||||
|
</FormRow>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<div className={styles.content}></div>
|
||||||
|
</div>
|
||||||
|
</Report>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
5
components/pages/reports/Report.js
Normal file
5
components/pages/reports/Report.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import Page from 'components/layout/Page';
|
||||||
|
|
||||||
|
export default function Report({ children, ...props }) {
|
||||||
|
return <Page {...props}>{children}</Page>;
|
||||||
|
}
|
42
components/pages/reports/ReportHeader.js
Normal file
42
components/pages/reports/ReportHeader.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Flexbox, Icon, Text } from 'react-basics';
|
||||||
|
import WebsiteSelect from 'components/input/WebsiteSelect';
|
||||||
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
|
import DateFilter from 'components/input/DateFilter';
|
||||||
|
import { parseDateRange } from 'lib/date';
|
||||||
|
|
||||||
|
export default function ReportHeader({ title, icon }) {
|
||||||
|
const [websiteId, setWebsiteId] = useState();
|
||||||
|
const [dateRange, setDateRange] = useState({});
|
||||||
|
const { value, startDate, endDate } = dateRange;
|
||||||
|
|
||||||
|
const handleSelect = id => {
|
||||||
|
setWebsiteId(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateChange = value => setDateRange(parseDateRange(value));
|
||||||
|
|
||||||
|
const Title = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Icon size="xl">{icon}</Icon>
|
||||||
|
<Text>{title}</Text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageHeader title={<Title />}>
|
||||||
|
<Flexbox gap={20}>
|
||||||
|
<DateFilter
|
||||||
|
value={value}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
onChange={handleDateChange}
|
||||||
|
showAllTime
|
||||||
|
/>
|
||||||
|
<WebsiteSelect websiteId={websiteId} onSelect={handleSelect} />
|
||||||
|
</Flexbox>
|
||||||
|
</PageHeader>
|
||||||
|
);
|
||||||
|
}
|
66
components/pages/reports/ReportsList.js
Normal file
66
components/pages/reports/ReportsList.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import { Button, Icons, Text, Icon } from 'react-basics';
|
||||||
|
import Page from 'components/layout/Page';
|
||||||
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
|
import Funnel from 'assets/funnel.svg';
|
||||||
|
import Nodes from 'assets/nodes.svg';
|
||||||
|
import Lightbulb from 'assets/lightbulb.svg';
|
||||||
|
import styles from './ReportsList.module.css';
|
||||||
|
|
||||||
|
const reports = [
|
||||||
|
{
|
||||||
|
title: 'Event data',
|
||||||
|
description: 'Query your event data.',
|
||||||
|
url: '/reports/event-data',
|
||||||
|
icon: <Nodes />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Funnel',
|
||||||
|
description: 'Understand the conversion and drop-off rate of users.',
|
||||||
|
url: '/reports/funnel',
|
||||||
|
icon: <Funnel />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Insights',
|
||||||
|
description: 'Explore your data by applying segments and filters.',
|
||||||
|
url: '/reports/insights',
|
||||||
|
icon: <Lightbulb />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function Report({ title, description, url, icon }) {
|
||||||
|
return (
|
||||||
|
<div className={styles.report}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
<Icon size="lg">{icon}</Icon>
|
||||||
|
<Text>{title}</Text>
|
||||||
|
</div>
|
||||||
|
<div className={styles.description}>{description}</div>
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<Link href={url}>
|
||||||
|
<Button variant="primary">
|
||||||
|
<Icon>
|
||||||
|
<Icons.Plus />
|
||||||
|
</Icon>
|
||||||
|
<Text>Create</Text>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReportsList() {
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader title="Reports" />
|
||||||
|
<div className={styles.reports}>
|
||||||
|
{reports.map(({ title, description, url, icon }) => {
|
||||||
|
return (
|
||||||
|
<Report key={title} icon={icon} title={title} description={description} url={url} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
32
components/pages/reports/ReportsList.module.css
Normal file
32
components/pages/reports/ReportsList.module.css
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
.reports {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid var(--base500);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
14
components/pages/reports/reports.module.css
Normal file
14
components/pages/reports/reports.module.css
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
grid-template-columns: max-content 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
width: 300px;
|
||||||
|
grid-column: 1 / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
grid-column: 2 / 3;
|
||||||
|
}
|
@ -9,11 +9,12 @@ export function DateRangeSetting() {
|
|||||||
const [dateRange, setDateRange] = useDateRange();
|
const [dateRange, setDateRange] = useDateRange();
|
||||||
const { startDate, endDate, value } = dateRange;
|
const { startDate, endDate, value } = dateRange;
|
||||||
|
|
||||||
|
const handleChange = value => setDateRange(value);
|
||||||
const handleReset = () => setDateRange(DEFAULT_DATE_RANGE);
|
const handleReset = () => setDateRange(DEFAULT_DATE_RANGE);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flexbox gap={10}>
|
<Flexbox gap={10}>
|
||||||
<DateFilter value={value} startDate={startDate} endDate={endDate} />
|
<DateFilter value={value} startDate={startDate} endDate={endDate} onChange={handleChange} />
|
||||||
<Button onClick={handleReset}>{formatMessage(labels.reset)}</Button>
|
<Button onClick={handleReset}>{formatMessage(labels.reset)}</Button>
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
);
|
);
|
||||||
|
17
hooks/index.js
Normal file
17
hooks/index.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export * from './useApi';
|
||||||
|
export * from './useConfig';
|
||||||
|
export * from './useCountryNames';
|
||||||
|
export * from './useDateRange';
|
||||||
|
export * from './useDocumentClick';
|
||||||
|
export * from './useEscapeKey';
|
||||||
|
export * from './useForceUpdate';
|
||||||
|
export * from './useLanguageNames';
|
||||||
|
export * from './useLocale';
|
||||||
|
export * from './useMessages';
|
||||||
|
export * from './usePageQuery';
|
||||||
|
export * from './useRequireLogin';
|
||||||
|
export * from './useShareToken';
|
||||||
|
export * from './useSticky';
|
||||||
|
export * from './useTheme';
|
||||||
|
export * from './useTimezone';
|
||||||
|
export * from './useUser';
|
@ -7,7 +7,7 @@ import useStore from 'store/app';
|
|||||||
|
|
||||||
const selector = state => state.shareToken;
|
const selector = state => state.shareToken;
|
||||||
|
|
||||||
export default function useApi() {
|
export function useApi() {
|
||||||
const { basePath } = useRouter();
|
const { basePath } = useRouter();
|
||||||
const shareToken = useStore(selector);
|
const shareToken = useStore(selector);
|
||||||
|
|
||||||
@ -18,3 +18,5 @@ export default function useApi() {
|
|||||||
|
|
||||||
return { get, post, put, del, ...reactQuery };
|
return { get, post, put, del, ...reactQuery };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useApi;
|
||||||
|
@ -4,7 +4,7 @@ import useApi from 'hooks/useApi';
|
|||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
|
||||||
export default function useConfig() {
|
export function useConfig() {
|
||||||
const { config } = useStore();
|
const { config } = useStore();
|
||||||
const { get } = useApi();
|
const { get } = useApi();
|
||||||
|
|
||||||
@ -23,3 +23,5 @@ export default function useConfig() {
|
|||||||
|
|
||||||
return config || {};
|
return config || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useConfig;
|
||||||
|
@ -7,7 +7,7 @@ const countryNames = {
|
|||||||
'en-US': enUS,
|
'en-US': enUS,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function useCountryNames(locale) {
|
export function useCountryNames(locale) {
|
||||||
const [list, setList] = useState(countryNames[locale] || enUS);
|
const [list, setList] = useState(countryNames[locale] || enUS);
|
||||||
const { basePath } = useRouter();
|
const { basePath } = useRouter();
|
||||||
|
|
||||||
@ -32,3 +32,5 @@ export default function useCountryNames(locale) {
|
|||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useCountryNames;
|
||||||
|
@ -5,7 +5,7 @@ import useLocale from './useLocale';
|
|||||||
import websiteStore, { setWebsiteDateRange } from 'store/websites';
|
import websiteStore, { setWebsiteDateRange } from 'store/websites';
|
||||||
import appStore, { setDateRange } from 'store/app';
|
import appStore, { setDateRange } from 'store/app';
|
||||||
|
|
||||||
export default function useDateRange(websiteId) {
|
export function useDateRange(websiteId) {
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const websiteConfig = websiteStore(state => state[websiteId]?.dateRange);
|
const websiteConfig = websiteStore(state => state[websiteId]?.dateRange);
|
||||||
const defaultConfig = DEFAULT_DATE_RANGE;
|
const defaultConfig = DEFAULT_DATE_RANGE;
|
||||||
@ -23,3 +23,5 @@ export default function useDateRange(websiteId) {
|
|||||||
|
|
||||||
return [dateRange, saveDateRange];
|
return [dateRange, saveDateRange];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useDateRange;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export default function useDocumentClick(handler) {
|
export function useDocumentClick(handler) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener('click', handler);
|
document.addEventListener('click', handler);
|
||||||
|
|
||||||
@ -11,3 +11,5 @@ export default function useDocumentClick(handler) {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useDocumentClick;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useCallback } from 'react';
|
import { useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
export default function useEscapeKey(handler) {
|
export function useEscapeKey(handler) {
|
||||||
const escFunction = useCallback(event => {
|
const escFunction = useCallback(event => {
|
||||||
if (event.keyCode === 27) {
|
if (event.keyCode === 27) {
|
||||||
handler(event);
|
handler(event);
|
||||||
@ -17,3 +17,5 @@ export default function useEscapeKey(handler) {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useEscapeKey;
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
export default function useForceUpdate() {
|
export function useForceUpdate() {
|
||||||
const [, update] = useState(Object.create(null));
|
const [, update] = useState(Object.create(null));
|
||||||
|
|
||||||
return useCallback(() => {
|
return useCallback(() => {
|
||||||
update(Object.create(null));
|
update(Object.create(null));
|
||||||
}, [update]);
|
}, [update]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useForceUpdate;
|
||||||
|
@ -7,7 +7,7 @@ const languageNames = {
|
|||||||
'en-US': enUS,
|
'en-US': enUS,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function useLanguageNames(locale) {
|
export function useLanguageNames(locale) {
|
||||||
const [list, setList] = useState(languageNames[locale] || enUS);
|
const [list, setList] = useState(languageNames[locale] || enUS);
|
||||||
const { basePath } = useRouter();
|
const { basePath } = useRouter();
|
||||||
|
|
||||||
@ -32,3 +32,5 @@ export default function useLanguageNames(locale) {
|
|||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useLanguageNames;
|
||||||
|
@ -13,7 +13,7 @@ const messages = {
|
|||||||
|
|
||||||
const selector = state => state.locale;
|
const selector = state => state.locale;
|
||||||
|
|
||||||
export default function useLocale() {
|
export function useLocale() {
|
||||||
const locale = useStore(selector);
|
const locale = useStore(selector);
|
||||||
const { basePath } = useRouter();
|
const { basePath } = useRouter();
|
||||||
const forceUpdate = useForceUpdate();
|
const forceUpdate = useForceUpdate();
|
||||||
@ -61,3 +61,5 @@ export default function useLocale() {
|
|||||||
|
|
||||||
return { locale, saveLocale, messages, dir, dateLocale };
|
return { locale, saveLocale, messages, dir, dateLocale };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useLocale;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useIntl, FormattedMessage } from 'react-intl';
|
import { useIntl, FormattedMessage } from 'react-intl';
|
||||||
import { messages, labels } from 'components/messages';
|
import { messages, labels } from 'components/messages';
|
||||||
|
|
||||||
export default function useMessages() {
|
export function useMessages() {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
function getMessage(id) {
|
function getMessage(id) {
|
||||||
@ -12,3 +12,5 @@ export default function useMessages() {
|
|||||||
|
|
||||||
return { formatMessage, FormattedMessage, messages, labels, getMessage };
|
return { formatMessage, FormattedMessage, messages, labels, getMessage };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useMessages;
|
||||||
|
@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { buildUrl } from 'next-basics';
|
import { buildUrl } from 'next-basics';
|
||||||
|
|
||||||
export default function usePageQuery() {
|
export function usePageQuery() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { pathname, search } = location;
|
const { pathname, search } = location;
|
||||||
const { asPath } = router;
|
const { asPath } = router;
|
||||||
@ -29,3 +29,5 @@ export default function usePageQuery() {
|
|||||||
|
|
||||||
return { pathname, query, resolveUrl, router };
|
return { pathname, query, resolveUrl, router };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default usePageQuery;
|
||||||
|
@ -3,7 +3,7 @@ import { useRouter } from 'next/router';
|
|||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
import useUser from 'hooks/useUser';
|
import useUser from 'hooks/useUser';
|
||||||
|
|
||||||
export default function useRequireLogin() {
|
export function useRequireLogin() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { get } = useApi();
|
const { get } = useApi();
|
||||||
const { user, setUser } = useUser();
|
const { user, setUser } = useUser();
|
||||||
@ -26,3 +26,5 @@ export default function useRequireLogin() {
|
|||||||
|
|
||||||
return { user };
|
return { user };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useRequireLogin;
|
||||||
|
@ -4,7 +4,7 @@ import useApi from './useApi';
|
|||||||
|
|
||||||
const selector = state => state.shareToken;
|
const selector = state => state.shareToken;
|
||||||
|
|
||||||
export default function useShareToken(shareId) {
|
export function useShareToken(shareId) {
|
||||||
const shareToken = useStore(selector);
|
const shareToken = useStore(selector);
|
||||||
const { get } = useApi();
|
const { get } = useApi();
|
||||||
|
|
||||||
@ -24,3 +24,5 @@ export default function useShareToken(shareId) {
|
|||||||
|
|
||||||
return shareToken;
|
return shareToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useShareToken;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
export default function useSticky({ enabled = true, threshold = 1 }) {
|
export function useSticky({ enabled = true, threshold = 1 }) {
|
||||||
const [isSticky, setIsSticky] = useState(false);
|
const [isSticky, setIsSticky] = useState(false);
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
|
||||||
@ -21,3 +21,5 @@ export default function useSticky({ enabled = true, threshold = 1 }) {
|
|||||||
|
|
||||||
return { ref, isSticky };
|
return { ref, isSticky };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useSticky;
|
||||||
|
@ -5,7 +5,7 @@ import { THEME_CONFIG } from 'lib/constants';
|
|||||||
|
|
||||||
const selector = state => state.theme;
|
const selector = state => state.theme;
|
||||||
|
|
||||||
export default function useTheme() {
|
export function useTheme() {
|
||||||
const defaultTheme =
|
const defaultTheme =
|
||||||
typeof window !== 'undefined'
|
typeof window !== 'undefined'
|
||||||
? window?.matchMedia('(prefers-color-scheme: dark)')?.matches
|
? window?.matchMedia('(prefers-color-scheme: dark)')?.matches
|
||||||
@ -34,3 +34,5 @@ export default function useTheme() {
|
|||||||
|
|
||||||
return [theme, saveTheme];
|
return [theme, saveTheme];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useTheme;
|
||||||
|
@ -3,7 +3,7 @@ import { getTimezone } from 'lib/date';
|
|||||||
import { getItem, setItem } from 'next-basics';
|
import { getItem, setItem } from 'next-basics';
|
||||||
import { TIMEZONE_CONFIG } from 'lib/constants';
|
import { TIMEZONE_CONFIG } from 'lib/constants';
|
||||||
|
|
||||||
export default function useTimezone() {
|
export function useTimezone() {
|
||||||
const [timezone, setTimezone] = useState(getItem(TIMEZONE_CONFIG) || getTimezone());
|
const [timezone, setTimezone] = useState(getItem(TIMEZONE_CONFIG) || getTimezone());
|
||||||
|
|
||||||
const saveTimezone = useCallback(
|
const saveTimezone = useCallback(
|
||||||
@ -16,3 +16,5 @@ export default function useTimezone() {
|
|||||||
|
|
||||||
return [timezone, saveTimezone];
|
return [timezone, saveTimezone];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useTimezone;
|
||||||
|
@ -2,8 +2,10 @@ import useStore, { setUser } from 'store/app';
|
|||||||
|
|
||||||
const selector = state => state.user;
|
const selector = state => state.user;
|
||||||
|
|
||||||
export default function useUser() {
|
export function useUser() {
|
||||||
const user = useStore(selector);
|
const user = useStore(selector);
|
||||||
|
|
||||||
return { user, setUser };
|
return { user, setUser };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useUser;
|
||||||
|
13
pages/api/websites/[id]/data.ts
Normal file
13
pages/api/websites/[id]/data.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { NextApiResponse } from 'next';
|
||||||
|
import { useAuth } from 'lib/middleware';
|
||||||
|
import { NextApiRequestQueryBody, User } from 'lib/types';
|
||||||
|
import { ok } from 'next-basics';
|
||||||
|
|
||||||
|
export default async (
|
||||||
|
req: NextApiRequestQueryBody<unknown, unknown>,
|
||||||
|
res: NextApiResponse<User>,
|
||||||
|
) => {
|
||||||
|
await useAuth(req, res);
|
||||||
|
|
||||||
|
return ok(res, req.auth.user);
|
||||||
|
};
|
5
pages/reports/event-data/index.js
Normal file
5
pages/reports/event-data/index.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import EventDataReport from 'components/pages/reports/EventDataReport';
|
||||||
|
|
||||||
|
export default function Report() {
|
||||||
|
return <EventDataReport />;
|
||||||
|
}
|
13
pages/reports/index.js
Normal file
13
pages/reports/index.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import AppLayout from 'components/layout/AppLayout';
|
||||||
|
import ReportsList from 'components/pages/reports/ReportsList';
|
||||||
|
import useMessages from 'hooks/useMessages';
|
||||||
|
|
||||||
|
export default function ReportsPage() {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout title={formatMessage(labels.reports)}>
|
||||||
|
<ReportsList />
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user