diff --git a/assets/funnel.svg b/assets/funnel.svg
new file mode 100644
index 00000000..46f4623f
--- /dev/null
+++ b/assets/funnel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/lightbulb.svg b/assets/lightbulb.svg
new file mode 100644
index 00000000..73b699a3
--- /dev/null
+++ b/assets/lightbulb.svg
@@ -0,0 +1,80 @@
+
+
+
diff --git a/assets/nodes.svg b/assets/nodes.svg
new file mode 100644
index 00000000..0bbd37c4
--- /dev/null
+++ b/assets/nodes.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/input/DateFilter.js b/components/input/DateFilter.js
index b6c1ee72..b7521e27 100644
--- a/components/input/DateFilter.js
+++ b/components/input/DateFilter.js
@@ -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 }) => (
diff --git a/components/input/WebsiteDateFilter.js b/components/input/WebsiteDateFilter.js
new file mode 100644
index 00000000..0d5d7569
--- /dev/null
+++ b/components/input/WebsiteDateFilter.js
@@ -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 (
+
+ );
+}
diff --git a/components/layout/NavBar.js b/components/layout/NavBar.js
index 5a6c877e..b5532ba0 100644
--- a/components/layout/NavBar.js
+++ b/components/layout/NavBar.js
@@ -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);
diff --git a/components/messages.js b/components/messages.js
index 245e8591..bdc8770d 100644
--- a/components/messages.js
+++ b/components/messages.js
@@ -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({
diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js
index 6614d40f..7b902df1 100644
--- a/components/metrics/WebsiteChart.js
+++ b/components/metrics/WebsiteChart.js
@@ -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({
-
+
diff --git a/components/pages/reports/EventDataReport.js b/components/pages/reports/EventDataReport.js
new file mode 100644
index 00000000..8c01e25f
--- /dev/null
+++ b/components/pages/reports/EventDataReport.js
@@ -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 (
+
+
+ } />
+
+
+
+ );
+}
diff --git a/components/pages/reports/Report.js b/components/pages/reports/Report.js
new file mode 100644
index 00000000..1754e89f
--- /dev/null
+++ b/components/pages/reports/Report.js
@@ -0,0 +1,5 @@
+import Page from 'components/layout/Page';
+
+export default function Report({ children, ...props }) {
+ return {children};
+}
diff --git a/components/pages/reports/ReportHeader.js b/components/pages/reports/ReportHeader.js
new file mode 100644
index 00000000..44502149
--- /dev/null
+++ b/components/pages/reports/ReportHeader.js
@@ -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}
+ {title}
+ >
+ );
+ };
+
+ return (
+ }>
+
+
+
+
+
+ );
+}
diff --git a/components/pages/reports/ReportsList.js b/components/pages/reports/ReportsList.js
new file mode 100644
index 00000000..44dc556d
--- /dev/null
+++ b/components/pages/reports/ReportsList.js
@@ -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: ,
+ },
+ {
+ title: 'Funnel',
+ description: 'Understand the conversion and drop-off rate of users.',
+ url: '/reports/funnel',
+ icon: ,
+ },
+ {
+ title: 'Insights',
+ description: 'Explore your data by applying segments and filters.',
+ url: '/reports/insights',
+ icon: ,
+ },
+];
+
+function Report({ title, description, url, icon }) {
+ return (
+
+
+ {icon}
+ {title}
+
+
{description}
+
+
+
+
+
+
+ );
+}
+
+export default function ReportsList() {
+ return (
+
+
+
+ {reports.map(({ title, description, url, icon }) => {
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/components/pages/reports/ReportsList.module.css b/components/pages/reports/ReportsList.module.css
new file mode 100644
index 00000000..0cdcb835
--- /dev/null
+++ b/components/pages/reports/ReportsList.module.css
@@ -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;
+}
diff --git a/components/pages/reports/reports.module.css b/components/pages/reports/reports.module.css
new file mode 100644
index 00000000..e18a857a
--- /dev/null
+++ b/components/pages/reports/reports.module.css
@@ -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;
+}
diff --git a/components/pages/settings/profile/DateRangeSetting.js b/components/pages/settings/profile/DateRangeSetting.js
index 152aba1d..44c3dc42 100644
--- a/components/pages/settings/profile/DateRangeSetting.js
+++ b/components/pages/settings/profile/DateRangeSetting.js
@@ -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 (
-
+
);
diff --git a/hooks/index.js b/hooks/index.js
new file mode 100644
index 00000000..15898eb5
--- /dev/null
+++ b/hooks/index.js
@@ -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';
diff --git a/hooks/useApi.ts b/hooks/useApi.ts
index 9e1e7e20..f41547a9 100644
--- a/hooks/useApi.ts
+++ b/hooks/useApi.ts
@@ -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;
diff --git a/hooks/useConfig.js b/hooks/useConfig.js
index b395829c..6dda7b74 100644
--- a/hooks/useConfig.js
+++ b/hooks/useConfig.js
@@ -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;
diff --git a/hooks/useCountryNames.js b/hooks/useCountryNames.js
index 0834202b..60c992a5 100644
--- a/hooks/useCountryNames.js
+++ b/hooks/useCountryNames.js
@@ -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;
diff --git a/hooks/useDateRange.js b/hooks/useDateRange.js
index a9896065..e8a542f4 100644
--- a/hooks/useDateRange.js
+++ b/hooks/useDateRange.js
@@ -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;
diff --git a/hooks/useDocumentClick.js b/hooks/useDocumentClick.js
index e1baae7e..be3d09be 100644
--- a/hooks/useDocumentClick.js
+++ b/hooks/useDocumentClick.js
@@ -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;
diff --git a/hooks/useEscapeKey.js b/hooks/useEscapeKey.js
index b8020c31..1a17f18f 100644
--- a/hooks/useEscapeKey.js
+++ b/hooks/useEscapeKey.js
@@ -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;
diff --git a/hooks/useForceUpdate.js b/hooks/useForceUpdate.js
index 2b8d6101..35f7fe16 100644
--- a/hooks/useForceUpdate.js
+++ b/hooks/useForceUpdate.js
@@ -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;
diff --git a/hooks/useLanguageNames.js b/hooks/useLanguageNames.js
index 3b153f28..aa5cbe39 100644
--- a/hooks/useLanguageNames.js
+++ b/hooks/useLanguageNames.js
@@ -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;
diff --git a/hooks/useLocale.js b/hooks/useLocale.js
index 5cece347..1279cea9 100644
--- a/hooks/useLocale.js
+++ b/hooks/useLocale.js
@@ -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;
diff --git a/hooks/useMessages.js b/hooks/useMessages.js
index 1bb65778..0719afd8 100644
--- a/hooks/useMessages.js
+++ b/hooks/useMessages.js
@@ -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;
diff --git a/hooks/usePageQuery.js b/hooks/usePageQuery.js
index b2f0acf1..b275d580 100644
--- a/hooks/usePageQuery.js
+++ b/hooks/usePageQuery.js
@@ -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;
diff --git a/hooks/useRequireLogin.js b/hooks/useRequireLogin.js
index 24cfdf0b..3a95c988 100644
--- a/hooks/useRequireLogin.js
+++ b/hooks/useRequireLogin.js
@@ -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;
diff --git a/hooks/useShareToken.js b/hooks/useShareToken.js
index bac7ec97..3d6b9698 100644
--- a/hooks/useShareToken.js
+++ b/hooks/useShareToken.js
@@ -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;
diff --git a/hooks/useSticky.js b/hooks/useSticky.js
index ae4dce72..be33f6ed 100644
--- a/hooks/useSticky.js
+++ b/hooks/useSticky.js
@@ -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;
diff --git a/hooks/useTheme.js b/hooks/useTheme.js
index c50f442f..a10894e5 100644
--- a/hooks/useTheme.js
+++ b/hooks/useTheme.js
@@ -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;
diff --git a/hooks/useTimezone.js b/hooks/useTimezone.js
index 8eb5d5f8..fb347c4d 100644
--- a/hooks/useTimezone.js
+++ b/hooks/useTimezone.js
@@ -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;
diff --git a/hooks/useUser.js b/hooks/useUser.js
index 6b73c113..c5f1a826 100644
--- a/hooks/useUser.js
+++ b/hooks/useUser.js
@@ -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;
diff --git a/pages/api/websites/[id]/data.ts b/pages/api/websites/[id]/data.ts
new file mode 100644
index 00000000..93e97067
--- /dev/null
+++ b/pages/api/websites/[id]/data.ts
@@ -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,
+ res: NextApiResponse,
+) => {
+ await useAuth(req, res);
+
+ return ok(res, req.auth.user);
+};
diff --git a/pages/reports/event-data/index.js b/pages/reports/event-data/index.js
new file mode 100644
index 00000000..154c79be
--- /dev/null
+++ b/pages/reports/event-data/index.js
@@ -0,0 +1,5 @@
+import EventDataReport from 'components/pages/reports/EventDataReport';
+
+export default function Report() {
+ return ;
+}
diff --git a/pages/reports/index.js b/pages/reports/index.js
new file mode 100644
index 00000000..b26023cc
--- /dev/null
+++ b/pages/reports/index.js
@@ -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 (
+
+
+
+ );
+}