Added reports section.

This commit is contained in:
Mike Cao 2023-05-17 23:20:06 -07:00
parent ad918c5bba
commit a5700d4a25
36 changed files with 422 additions and 43 deletions

1
assets/funnel.svg Normal file
View 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
View 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
View 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

View File

@ -3,31 +3,22 @@ import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics';
import { endOfYear, isSameDay } from 'date-fns';
import DatePickerForm from 'components/metrics/DatePickerForm';
import useLocale from 'hooks/useLocale';
import { dateFormat, getDateRangeValues } from 'lib/date';
import { dateFormat } from 'lib/date';
import Icons from 'components/icons';
import useApi from 'hooks/useApi';
import useDateRange from 'hooks/useDateRange';
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 { get } = useApi();
const [dateRange, setDateRange] = useDateRange(websiteId);
const { startDate, endDate } = dateRange;
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 = [
{ label: formatMessage(labels.today), value: '1day' },
{
@ -61,7 +52,7 @@ export function DateFilter({ websiteId, value, className }) {
value: '90day',
},
{ label: formatMessage(labels.thisYear), value: '1year' },
websiteId && {
showAllTime && {
label: formatMessage(labels.allTime),
value: 'all',
divider: true,
@ -86,12 +77,12 @@ export function DateFilter({ websiteId, value, className }) {
setShowPicker(true);
return;
}
handleDateChange(value);
onChange(value);
};
const handlePickerChange = value => {
setShowPicker(false);
handleDateChange(value);
onChange(value);
};
const handleClose = () => setShowPicker(false);
@ -103,7 +94,8 @@ export function DateFilter({ websiteId, value, className }) {
items={options}
renderValue={renderValue}
value={value}
alignment="end"
alignment={alignment}
placeholder={formatMessage(labels.selectDate)}
onChange={handleChange}
>
{({ label, value, divider }) => (

View 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} />
);
}

View File

@ -18,6 +18,7 @@ export function NavBar() {
const links = [
{ label: formatMessage(labels.dashboard), url: '/dashboard' },
{ label: formatMessage(labels.reports), url: '/reports' },
{ label: formatMessage(labels.realtime), url: '/realtime' },
!cloudMode && { label: formatMessage(labels.settings), url: '/settings' },
].filter(n => n);

View File

@ -97,6 +97,7 @@ export const labels = defineMessages({
allTime: { id: 'label.all-time', defaultMessage: 'All time' },
customRange: { id: 'label.custom-range', defaultMessage: 'Custom range' },
selectWebsite: { id: 'label.select-website', defaultMessage: 'Select website' },
selectDate: { id: 'label.select-date', defaultMessage: 'Select date' },
all: { id: 'label.all', defaultMessage: 'All' },
sessions: { id: 'label.sessions', defaultMessage: 'Sessions' },
pageNotFound: { id: 'message.page-not-found', defaultMessage: 'Page not found' },
@ -117,6 +118,8 @@ export const labels = defineMessages({
view: { id: 'label.view', defaultMessage: 'View' },
cities: { id: 'label.cities', defaultMessage: 'Cities' },
regions: { id: 'label.regions', defaultMessage: 'Regions' },
reports: { id: 'label.reports', defaultMessage: 'Reports' },
eventData: { id: 'label.event-data', defaultMessage: 'Event data' },
});
export const messages = defineMessages({

View File

@ -5,7 +5,7 @@ import classNames from 'classnames';
import PageviewsChart from './PageviewsChart';
import MetricsBar from './MetricsBar';
import WebsiteHeader from './WebsiteHeader';
import DateFilter from 'components/input/DateFilter';
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
import ErrorMessage from 'components/common/ErrorMessage';
import FilterTags from 'components/metrics/FilterTags';
import RefreshButton from 'components/input/RefreshButton';
@ -107,7 +107,7 @@ export function WebsiteChart({
<Column defaultSize={12} xl={4}>
<div className={styles.actions}>
<RefreshButton websiteId={websiteId} isLoading={isLoading} />
<DateFilter websiteId={websiteId} value={value} className={styles.dropdown} />
<WebsiteDateFilter websiteId={websiteId} value={value} className={styles.dropdown} />
</div>
</Column>
</Row>

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

View File

@ -0,0 +1,5 @@
import Page from 'components/layout/Page';
export default function Report({ children, ...props }) {
return <Page {...props}>{children}</Page>;
}

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

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

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

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

View File

@ -9,11 +9,12 @@ export function DateRangeSetting() {
const [dateRange, setDateRange] = useDateRange();
const { startDate, endDate, value } = dateRange;
const handleChange = value => setDateRange(value);
const handleReset = () => setDateRange(DEFAULT_DATE_RANGE);
return (
<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>
</Flexbox>
);

17
hooks/index.js Normal file
View 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';

View File

@ -7,7 +7,7 @@ import useStore from 'store/app';
const selector = state => state.shareToken;
export default function useApi() {
export function useApi() {
const { basePath } = useRouter();
const shareToken = useStore(selector);
@ -18,3 +18,5 @@ export default function useApi() {
return { get, post, put, del, ...reactQuery };
}
export default useApi;

View File

@ -4,7 +4,7 @@ import useApi from 'hooks/useApi';
let loading = false;
export default function useConfig() {
export function useConfig() {
const { config } = useStore();
const { get } = useApi();
@ -23,3 +23,5 @@ export default function useConfig() {
return config || {};
}
export default useConfig;

View File

@ -7,7 +7,7 @@ const countryNames = {
'en-US': enUS,
};
export default function useCountryNames(locale) {
export function useCountryNames(locale) {
const [list, setList] = useState(countryNames[locale] || enUS);
const { basePath } = useRouter();
@ -32,3 +32,5 @@ export default function useCountryNames(locale) {
return list;
}
export default useCountryNames;

View File

@ -5,7 +5,7 @@ import useLocale from './useLocale';
import websiteStore, { setWebsiteDateRange } from 'store/websites';
import appStore, { setDateRange } from 'store/app';
export default function useDateRange(websiteId) {
export function useDateRange(websiteId) {
const { locale } = useLocale();
const websiteConfig = websiteStore(state => state[websiteId]?.dateRange);
const defaultConfig = DEFAULT_DATE_RANGE;
@ -23,3 +23,5 @@ export default function useDateRange(websiteId) {
return [dateRange, saveDateRange];
}
export default useDateRange;

View File

@ -1,6 +1,6 @@
import { useEffect } from 'react';
export default function useDocumentClick(handler) {
export function useDocumentClick(handler) {
useEffect(() => {
document.addEventListener('click', handler);
@ -11,3 +11,5 @@ export default function useDocumentClick(handler) {
return null;
}
export default useDocumentClick;

View File

@ -1,6 +1,6 @@
import { useEffect, useCallback } from 'react';
export default function useEscapeKey(handler) {
export function useEscapeKey(handler) {
const escFunction = useCallback(event => {
if (event.keyCode === 27) {
handler(event);
@ -17,3 +17,5 @@ export default function useEscapeKey(handler) {
return null;
}
export default useEscapeKey;

View File

@ -1,9 +1,11 @@
import { useCallback, useState } from 'react';
export default function useForceUpdate() {
export function useForceUpdate() {
const [, update] = useState(Object.create(null));
return useCallback(() => {
update(Object.create(null));
}, [update]);
}
export default useForceUpdate;

View File

@ -7,7 +7,7 @@ const languageNames = {
'en-US': enUS,
};
export default function useLanguageNames(locale) {
export function useLanguageNames(locale) {
const [list, setList] = useState(languageNames[locale] || enUS);
const { basePath } = useRouter();
@ -32,3 +32,5 @@ export default function useLanguageNames(locale) {
return list;
}
export default useLanguageNames;

View File

@ -13,7 +13,7 @@ const messages = {
const selector = state => state.locale;
export default function useLocale() {
export function useLocale() {
const locale = useStore(selector);
const { basePath } = useRouter();
const forceUpdate = useForceUpdate();
@ -61,3 +61,5 @@ export default function useLocale() {
return { locale, saveLocale, messages, dir, dateLocale };
}
export default useLocale;

View File

@ -1,7 +1,7 @@
import { useIntl, FormattedMessage } from 'react-intl';
import { messages, labels } from 'components/messages';
export default function useMessages() {
export function useMessages() {
const { formatMessage } = useIntl();
function getMessage(id) {
@ -12,3 +12,5 @@ export default function useMessages() {
return { formatMessage, FormattedMessage, messages, labels, getMessage };
}
export default useMessages;

View File

@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { useRouter } from 'next/router';
import { buildUrl } from 'next-basics';
export default function usePageQuery() {
export function usePageQuery() {
const router = useRouter();
const { pathname, search } = location;
const { asPath } = router;
@ -29,3 +29,5 @@ export default function usePageQuery() {
return { pathname, query, resolveUrl, router };
}
export default usePageQuery;

View File

@ -3,7 +3,7 @@ import { useRouter } from 'next/router';
import useApi from 'hooks/useApi';
import useUser from 'hooks/useUser';
export default function useRequireLogin() {
export function useRequireLogin() {
const router = useRouter();
const { get } = useApi();
const { user, setUser } = useUser();
@ -26,3 +26,5 @@ export default function useRequireLogin() {
return { user };
}
export default useRequireLogin;

View File

@ -4,7 +4,7 @@ import useApi from './useApi';
const selector = state => state.shareToken;
export default function useShareToken(shareId) {
export function useShareToken(shareId) {
const shareToken = useStore(selector);
const { get } = useApi();
@ -24,3 +24,5 @@ export default function useShareToken(shareId) {
return shareToken;
}
export default useShareToken;

View File

@ -1,6 +1,6 @@
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 ref = useRef(null);
@ -21,3 +21,5 @@ export default function useSticky({ enabled = true, threshold = 1 }) {
return { ref, isSticky };
}
export default useSticky;

View File

@ -5,7 +5,7 @@ import { THEME_CONFIG } from 'lib/constants';
const selector = state => state.theme;
export default function useTheme() {
export function useTheme() {
const defaultTheme =
typeof window !== 'undefined'
? window?.matchMedia('(prefers-color-scheme: dark)')?.matches
@ -34,3 +34,5 @@ export default function useTheme() {
return [theme, saveTheme];
}
export default useTheme;

View File

@ -3,7 +3,7 @@ import { getTimezone } from 'lib/date';
import { getItem, setItem } from 'next-basics';
import { TIMEZONE_CONFIG } from 'lib/constants';
export default function useTimezone() {
export function useTimezone() {
const [timezone, setTimezone] = useState(getItem(TIMEZONE_CONFIG) || getTimezone());
const saveTimezone = useCallback(
@ -16,3 +16,5 @@ export default function useTimezone() {
return [timezone, saveTimezone];
}
export default useTimezone;

View File

@ -2,8 +2,10 @@ import useStore, { setUser } from 'store/app';
const selector = state => state.user;
export default function useUser() {
export function useUser() {
const user = useStore(selector);
return { user, setUser };
}
export default useUser;

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

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