diff --git a/components/common/DateFilter.js b/components/common/DateFilter.js index 77bab32f..a340fe48 100644 --- a/components/common/DateFilter.js +++ b/components/common/DateFilter.js @@ -1,68 +1,74 @@ -import { endOfYear, isSameDay } from 'date-fns'; import { useState } from 'react'; -import { Icon, Modal, Dropdown, Item } from 'react-basics'; -import { useIntl, defineMessages } from 'react-intl'; +import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics'; +import { useIntl } from 'react-intl'; +import { endOfYear, isSameDay } from 'date-fns'; import DatePickerForm from 'components/metrics/DatePickerForm'; import useLocale from 'hooks/useLocale'; -import { dateFormat } from 'lib/date'; -import Calendar from 'assets/calendar.svg'; +import { dateFormat, getDateRangeValues } from 'lib/date'; +import Icons from 'components/icons'; +import { labels } from 'components/messages'; +import useApi from 'hooks/useApi'; +import useDateRange from 'hooks/useDateRange'; -const messages = defineMessages({ - today: { id: 'label.today', defaultMessage: 'Today' }, - lastHours: { id: 'label.last-hours', defaultMessage: 'Last {x} hours' }, - yesterday: { id: 'label.yesterday', defaultMessage: 'Yesterday' }, - thisWeek: { id: 'label.this-week', defaultMessage: 'This week' }, - lastDays: { id: 'label.last-days', defaultMessage: 'Last {x} days' }, - thisMonth: { id: 'label.this-month', defaultMessage: 'This month' }, - thisYear: { id: 'label.this-year', defaultMessage: 'This year' }, - allTime: { id: 'label.all-time', defaultMessage: 'All time' }, - customRange: { id: 'label.custom-range', defaultMessage: 'Custom-range' }, -}); - -function DateFilter({ value, startDate, endDate, onChange, className }) { +function DateFilter({ websiteId, value, className }) { const { formatMessage } = useIntl(); + const { get } = useApi(); + const [dateRange, setDateRange] = useDateRange(websiteId); + const { startDate, endDate } = dateRange; const [showPicker, setShowPicker] = useState(false); + async function handleDateChange(value) { + if (value === 'all') { + const data = await get(`/websites/${websiteId}`); + + if (data) { + setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) }); + } + } else { + setDateRange(value); + } + } + const options = [ - { label: formatMessage(messages.today), value: '1day' }, + { label: formatMessage(labels.today), value: '1day' }, { - label: formatMessage(messages.lastHours, { x: 24 }), + label: formatMessage(labels.lastHours, { x: 24 }), value: '24hour', }, { - label: formatMessage(messages.yesterday), + label: formatMessage(labels.yesterday), value: '-1day', }, { - label: formatMessage(messages.thisWeek), + label: formatMessage(labels.thisWeek), value: '1week', divider: true, }, { - label: formatMessage(messages.lastDays, { x: 7 }), + label: formatMessage(labels.lastDays, { x: 7 }), value: '7day', }, { - label: formatMessage(messages.thisMonth), + label: formatMessage(labels.thisMonth), value: '1month', divider: true, }, { - label: formatMessage(messages.lastDays, { x: 30 }), + label: formatMessage(labels.lastDays, { x: 30 }), value: '30day', }, { - label: formatMessage(messages.lastDays, { x: 90 }), + label: formatMessage(labels.lastDays, { x: 90 }), value: '90day', }, - { label: formatMessage(messages.thisYear), value: '1year' }, + { label: formatMessage(labels.thisYear), value: '1year' }, { - label: formatMessage(messages.allTime), + label: formatMessage(labels.allTime), value: 'all', divider: true, }, { - label: formatMessage(messages.customRange), + label: formatMessage(labels.customRange), value: 'custom', divider: true, }, @@ -76,17 +82,17 @@ function DateFilter({ value, startDate, endDate, onChange, className }) { ); }; - const handleChange = async value => { + const handleChange = value => { if (value === 'custom') { setShowPicker(true); return; } - onChange(value); + handleDateChange(value); }; const handlePickerChange = value => { setShowPicker(false); - onChange(value); + handleDateChange(value); }; const handleClose = () => setShowPicker(false); @@ -98,9 +104,14 @@ function DateFilter({ value, startDate, endDate, onChange, className }) { items={options} renderValue={renderValue} value={value} + alignment="end" onChange={handleChange} > - {({ label, value }) => {label}} + {({ label, value, divider }) => ( + + {label} + + )} {showPicker && ( @@ -128,13 +139,15 @@ const CustomRange = ({ startDate, endDate, onClick }) => { } return ( - <> + - + - {dateFormat(startDate, 'd LLL y', locale)} - {!isSameDay(startDate, endDate) && ` — ${dateFormat(endDate, 'd LLL y', locale)}`} - > + + {dateFormat(startDate, 'd LLL y', locale)} + {!isSameDay(startDate, endDate) && ` — ${dateFormat(endDate, 'd LLL y', locale)}`} + + ); }; diff --git a/components/common/FilterLink.js b/components/common/FilterLink.js index f91d4764..eb410719 100644 --- a/components/common/FilterLink.js +++ b/components/common/FilterLink.js @@ -6,13 +6,13 @@ import { Icon, Icons } from 'react-basics'; import styles from './FilterLink.module.css'; export default function FilterLink({ id, value, label, externalUrl }) { - const { resolve, query } = usePageQuery(); + const { resolveUrl, query } = usePageQuery(); const active = query[id] !== undefined; const selected = query[id] === value; return ( - + state[`/websites/${websiteId}/stats`], [websiteId]); - const completed = useStore(selector); function handleClick() { - if (!loading && dateRange) { - setLoading(true); + if (!isLoading && dateRange) { if (/^\d+/.test(dateRange.value)) { setDateRange(websiteId, dateRange.value); } else { @@ -25,17 +19,13 @@ function RefreshButton({ websiteId }) { } } - useEffect(() => { - setLoading(false); - }, [completed]); - return ( - + - + ); } diff --git a/components/layout/Grid.js b/components/layout/Grid.js new file mode 100644 index 00000000..0276063b --- /dev/null +++ b/components/layout/Grid.js @@ -0,0 +1,13 @@ +import { Row, Column } from 'react-basics'; +import classNames from 'classnames'; +import styles from './Grid.module.css'; + +export function GridRow(props) { + const { className, ...otherProps } = props; + return ; +} + +export function GridColumn(props) { + const { className, ...otherProps } = props; + return ; +} diff --git a/components/layout/Grid.module.css b/components/layout/Grid.module.css new file mode 100644 index 00000000..20df43c9 --- /dev/null +++ b/components/layout/Grid.module.css @@ -0,0 +1,35 @@ +.col { + display: flex; + flex-direction: column; + padding: 20px; +} + +.row { + border-top: 1px solid var(--base300); + min-height: 430px; +} + +.row > .col { + border-left: 1px solid var(--base300); +} + +.row > .col:first-child { + border-left: 0; + padding-left: 0; +} + +.row > .col:last-child { + padding-right: 0; +} + +@media only screen and (max-width: 992px) { + .row { + border: 0; + } + + .row > .col { + border-top: 1px solid var(--base300); + border-left: 0; + padding: 20px 0; + } +} diff --git a/components/messages.js b/components/messages.js index c29432b1..ce506fd1 100644 --- a/components/messages.js +++ b/components/messages.js @@ -81,9 +81,21 @@ export const labels = defineMessages({ visitors: { id: 'label.visitors', defaultMessage: 'Visitors' }, filterCombined: { id: 'label.filter-combined', defaultMessage: 'Combined' }, filterRaw: { id: 'label.filter-raw', defaultMessage: 'Raw' }, - views: { id: 'label.views', defaultMessage: 'View' }, + views: { id: 'label.views', defaultMessage: 'Views' }, none: { id: 'label.none', defaultMessage: 'None' }, clearAll: { id: 'label.clear-all', defaultMessage: 'Clear all' }, + today: { id: 'label.today', defaultMessage: 'Today' }, + lastHours: { id: 'label.last-hours', defaultMessage: 'Last {x} hours' }, + yesterday: { id: 'label.yesterday', defaultMessage: 'Yesterday' }, + thisWeek: { id: 'label.this-week', defaultMessage: 'This week' }, + lastDays: { id: 'label.last-days', defaultMessage: 'Last {x} days' }, + thisMonth: { id: 'label.this-month', defaultMessage: 'This month' }, + thisYear: { id: 'label.this-year', defaultMessage: 'This year' }, + allTime: { id: 'label.all-time', defaultMessage: 'All time' }, + customRange: { id: 'label.custom-range', defaultMessage: 'Custom-range' }, + selectWebsite: { id: 'label.select-website', defaultMessage: 'Select website' }, + all: { id: 'label.all', defaultMessage: 'All' }, + sessions: { id: 'label.sessions', defaultMessage: 'Sessions' }, }); export const messages = defineMessages({ diff --git a/components/metrics/ActiveUsers.js b/components/metrics/ActiveUsers.js index 664e23a9..cb3afe80 100644 --- a/components/metrics/ActiveUsers.js +++ b/components/metrics/ActiveUsers.js @@ -13,6 +13,7 @@ export default function ActiveUsers({ websiteId, value, refetchInterval = 60000 () => get(`/websites/${websiteId}/active`), { refetchInterval, + enabled: !!websiteId, }, ); diff --git a/components/metrics/DatePickerForm.js b/components/metrics/DatePickerForm.js index 17c7b5e5..5235d339 100644 --- a/components/metrics/DatePickerForm.js +++ b/components/metrics/DatePickerForm.js @@ -42,7 +42,7 @@ export default function DatePickerForm({ return ( - + {formatMessage(labels.singleDay)} {formatMessage(labels.dateRange)} diff --git a/components/metrics/FilterTags.js b/components/metrics/FilterTags.js index 0e68504b..37f5981c 100644 --- a/components/metrics/FilterTags.js +++ b/components/metrics/FilterTags.js @@ -1,26 +1,39 @@ import { useIntl } from 'react-intl'; -import classNames from 'classnames'; import { safeDecodeURI } from 'next-basics'; import { Button, Icon, Icons, Text } from 'react-basics'; import { labels } from 'components/messages'; +import usePageQuery from 'hooks/usePageQuery'; import styles from './FilterTags.module.css'; -export default function FilterTags({ className, params, onClick }) { +export default function FilterTags({ websiteId, params, onClick }) { const { formatMessage } = useIntl(); + const { + router, + resolveUrl, + query: { view }, + } = usePageQuery(); if (Object.keys(params).filter(key => params[key]).length === 0) { return null; } + function handleCloseFilter(param) { + if (param === null) { + router.push(`/websites/${websiteId}/?view=${view}`); + } else { + router.push(resolveUrl({ [param]: undefined })); + } + } + return ( - + {Object.keys(params).map(key => { if (!params[key]) { return null; } return ( - onClick(key)} variant="primary" size="sm"> + handleCloseFilter(key)} variant="primary" size="sm"> {`${key}`} — {`${safeDecodeURI(params[key])}`} @@ -31,7 +44,7 @@ export default function FilterTags({ className, params, onClick }) { ); })} - onClick(null)}> + handleCloseFilter(null)}> diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index c1c6b126..de356dc7 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -31,7 +31,7 @@ export default function MetricsTable({ }) { const [{ startDate, endDate, modified }] = useDateRange(websiteId); const { - resolve, + resolveUrl, router, query: { url, referrer, os, browser, device, country }, } = usePageQuery(); @@ -79,7 +79,7 @@ export default function MetricsTable({ {data && !error && } {data && !error && limit && ( - + {formatMessage(messages.more)} diff --git a/components/metrics/RealtimeHeader.js b/components/metrics/RealtimeHeader.js deleted file mode 100644 index c0b6cbcc..00000000 --- a/components/metrics/RealtimeHeader.js +++ /dev/null @@ -1,52 +0,0 @@ -import { useMemo } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { differenceInMinutes } from 'date-fns'; -import PageHeader from 'components/layout/PageHeader'; -import ActiveUsers from './ActiveUsers'; -import MetricCard from './MetricCard'; -import styles from './RealtimeHeader.module.css'; - -export default function RealtimeHeader({ data, websiteId }) { - const { pageviews, sessions, events, countries } = data; - - const count = useMemo(() => { - return sessions.filter( - ({ createdAt }) => differenceInMinutes(new Date(), new Date(createdAt)) <= 5, - ).length; - }, [sessions, websiteId]); - - return ( - <> - - - - - - - - - - } - value={pageviews.length} - hideComparison - /> - } - value={sessions.length} - hideComparison - /> - } - value={events.length} - hideComparison - /> - } - value={countries.length} - hideComparison - /> - - > - ); -} diff --git a/components/metrics/RealtimeHeader.module.css b/components/metrics/RealtimeHeader.module.css deleted file mode 100644 index 2f902181..00000000 --- a/components/metrics/RealtimeHeader.module.css +++ /dev/null @@ -1,11 +0,0 @@ -.metrics { - display: flex; - margin-bottom: 10px; - overflow: auto; -} - -@media only screen and (max-width: 576px) { - .active { - display: none; - } -} diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js index 73b9b6da..2f495a37 100644 --- a/components/metrics/WebsiteChart.js +++ b/components/metrics/WebsiteChart.js @@ -1,6 +1,6 @@ import { useMemo } from 'react'; import { useIntl } from 'react-intl'; -import { Button, Icon, Text, Row, Column, Container } from 'react-basics'; +import { Button, Icon, Text, Row, Column, Flexbox } from 'react-basics'; import Link from 'next/link'; import PageviewsChart from './PageviewsChart'; import MetricsBar from './MetricsBar'; @@ -9,6 +9,7 @@ import DateFilter from 'components/common/DateFilter'; import StickyHeader from 'components/helpers/StickyHeader'; import ErrorMessage from 'components/common/ErrorMessage'; import FilterTags from 'components/metrics/FilterTags'; +import RefreshButton from 'components/input/RefreshButton'; import useApi from 'hooks/useApi'; import useDateRange from 'hooks/useDateRange'; import useTimezone from 'hooks/useTimezone'; @@ -28,13 +29,11 @@ export default function WebsiteChart({ onDataLoad = () => {}, }) { const { formatMessage } = useIntl(); - const [dateRange, setDateRange] = useDateRange(websiteId); + const [dateRange] = useDateRange(websiteId); const { startDate, endDate, unit, value, modified } = dateRange; const [timezone] = useTimezone(); const { - router, - resolve, - query: { view, url, referrer, os, browser, device, country }, + query: { url, referrer, os, browser, device, country }, } = usePageQuery(); const { get, useQuery } = useApi(); @@ -66,26 +65,6 @@ export default function WebsiteChart({ return { pageviews: [], sessions: [] }; }, [data, startDate, endDate, unit]); - function handleCloseFilter(param) { - if (param === null) { - router.push(`/websites/${websiteId}/?view=${view}`); - } else { - router.push(resolve({ [param]: undefined })); - } - } - - async function handleDateChange(value) { - if (value === 'all') { - const data = await get(`/websites/${websiteId}`); - - if (data) { - setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) }); - } - } else { - setDateRange(value); - } - } - return ( <> @@ -102,22 +81,15 @@ export default function WebsiteChart({ )} - + - + - - + + + diff --git a/components/metrics/WebsiteChart.module.css b/components/metrics/WebsiteChart.module.css index 8c66b1b0..05bfcbb8 100644 --- a/components/metrics/WebsiteChart.module.css +++ b/components/metrics/WebsiteChart.module.css @@ -7,7 +7,7 @@ .chart { position: relative; - padding-bottom: 20px; + padding-bottom: 10px; } .title { @@ -32,9 +32,17 @@ border-bottom: 1px solid var(--base300); z-index: 3; width: inherit; - padding-top: 20px; + padding-top: 10px; } -.filter { - align-self: center; +.actions { + display: flex; + flex-direction: row; + justify-content: flex-end; + gap: 10px; + flex: 1; +} + +.dropdown { + min-width: 200px; } diff --git a/components/pages/realtime/RealtimeDashboard.js b/components/pages/realtime/RealtimeDashboard.js index 552c726a..8a9b77b4 100644 --- a/components/pages/realtime/RealtimeDashboard.js +++ b/components/pages/realtime/RealtimeDashboard.js @@ -1,21 +1,25 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; -import { Row, Column } from 'react-basics'; -import { FormattedMessage } from 'react-intl'; -import { subMinutes, startOfMinute } from 'date-fns'; +import { useIntl } from 'react-intl'; +import { subMinutes, startOfMinute, differenceInMinutes } from 'date-fns'; import firstBy from 'thenby'; +import { GridRow, GridColumn } from 'components/layout/Grid'; import Page from 'components/layout/Page'; import RealtimeChart from 'components/metrics/RealtimeChart'; -import RealtimeLog from 'components/metrics/RealtimeLog'; -import RealtimeHeader from 'components/metrics/RealtimeHeader'; +import RealtimeLog from 'components/pages/realtime/RealtimeLog'; +import RealtimeHeader from 'components/pages/realtime/RealtimeHeader'; import WorldMap from 'components/common/WorldMap'; import DataTable from 'components/metrics/DataTable'; -import RealtimeViews from 'components/metrics/RealtimeViews'; +import RealtimeViews from 'components/pages/realtime/RealtimeViews'; import useApi from 'hooks/useApi'; import useLocale from 'hooks/useLocale'; import useCountryNames from 'hooks/useCountryNames'; import { percentFilter } from 'lib/filters'; +import { labels } from 'components/messages'; import { SHARE_TOKEN_HEADER, REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants'; import styles from './RealtimeDashboard.module.css'; +import StickyHeader from 'components/helpers/StickyHeader'; +import PageHeader from 'components/layout/PageHeader'; +import ActiveUsers from 'components/metrics/ActiveUsers'; function mergeData(state, data, time) { const ids = state.map(({ __id }) => __id); @@ -29,18 +33,19 @@ function filterWebsite(data, id) { } export default function RealtimeDashboard() { + const { formatMessage } = useIntl(); const { locale } = useLocale(); const countryNames = useCountryNames(locale); const [data, setData] = useState(); - const [websiteId, setWebsiteId] = useState(null); + const [websiteId, setWebsiteId] = useState(); const { get, useQuery } = useApi(); - const { data: init, isLoading } = useQuery(['realtime:init'], () => get('/realtime/init')); + const { data: websites, isLoading } = useQuery(['websites:me'], () => get('/me/websites')); + const { data: updates } = useQuery( ['realtime:updates'], - () => - get('/realtime/update', { startAt: data?.timestamp }, { [SHARE_TOKEN_HEADER]: init?.token }), + () => get('/realtime/update', { startAt: data?.timestamp }), { - disabled: !init?.websites?.length || !data, + enabled: !!websiteId, retryInterval: REALTIME_INTERVAL, }, ); @@ -55,7 +60,7 @@ export default function RealtimeDashboard() { const { pageviews, sessions, events } = data; if (websiteId) { - const { id } = init.websites.find(n => n.id === websiteId); + const { id } = websites.find(n => n.id === websiteId); return { pageviews: filterWebsite(pageviews, id), sessions: filterWebsite(sessions, id), @@ -67,6 +72,15 @@ export default function RealtimeDashboard() { return data; }, [data, websiteId]); + const count = useMemo(() => { + if (data) { + const { sessions } = data; + return sessions.filter( + ({ createdAt }) => differenceInMinutes(new Date(), new Date(createdAt)) <= 5, + ).length; + } + }, [data, websiteId]); + const countries = useMemo(() => { if (realtimeData?.sessions) { return percentFilter( @@ -89,14 +103,6 @@ export default function RealtimeDashboard() { return []; }, [realtimeData?.sessions]); - useEffect(() => { - if (init && !data) { - const { websites, data } = init; - - setData({ websites, ...data }); - } - }, [init]); - useEffect(() => { if (updates) { const { pageviews, sessions, events, timestamp } = updates; @@ -112,44 +118,43 @@ export default function RealtimeDashboard() { } }, [updates]); - if (!init || !data || isLoading) { - return null; - } - - const { websites } = data; - return ( - - + + + + + + + - - + + - - + + - - - - + + + + } - metric={} + title={formatMessage(labels.countries)} + metric={formatMessage(labels.visitors)} data={countries} renderLabel={renderCountryName} /> - - + + - - + + ); } diff --git a/components/pages/realtime/RealtimeDashboard.module.css b/components/pages/realtime/RealtimeDashboard.module.css index b9b5a632..5adcd435 100644 --- a/components/pages/realtime/RealtimeDashboard.module.css +++ b/components/pages/realtime/RealtimeDashboard.module.css @@ -5,3 +5,12 @@ .chart { margin-bottom: 30px; } + +.sticky { + position: fixed; + top: 0; + background: var(--base50); + border-bottom: 1px solid var(--base300); + z-index: 3; + padding: 10px 0; +} diff --git a/components/pages/realtime/RealtimeHeader.js b/components/pages/realtime/RealtimeHeader.js new file mode 100644 index 00000000..69013a70 --- /dev/null +++ b/components/pages/realtime/RealtimeHeader.js @@ -0,0 +1,44 @@ +import { useIntl } from 'react-intl'; +import { Dropdown, Item } from 'react-basics'; +import MetricCard from 'components/metrics/MetricCard'; +import { labels } from 'components/messages'; +import styles from './RealtimeHeader.module.css'; + +export default function RealtimeHeader({ data, websiteId, websites, onSelect }) { + const { formatMessage } = useIntl(); + + const { pageviews, sessions, events, countries } = data; + + const renderValue = value => { + return websites?.find(({ id }) => id === value)?.name; + }; + + return ( + + + + + + + + + {item => {item.name}} + + + ); +} diff --git a/components/pages/realtime/RealtimeHeader.module.css b/components/pages/realtime/RealtimeHeader.module.css new file mode 100644 index 00000000..a7ebf36c --- /dev/null +++ b/components/pages/realtime/RealtimeHeader.module.css @@ -0,0 +1,9 @@ +.header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.metrics { + display: flex; +} diff --git a/components/metrics/RealtimeLog.js b/components/pages/realtime/RealtimeLog.js similarity index 81% rename from components/metrics/RealtimeLog.js rename to components/pages/realtime/RealtimeLog.js index d7a71ac8..8d293e37 100644 --- a/components/metrics/RealtimeLog.js +++ b/components/pages/realtime/RealtimeLog.js @@ -1,41 +1,42 @@ import { useMemo, useState } from 'react'; -import { StatusLight } from 'react-basics'; -import { FormattedMessage, useIntl } from 'react-intl'; +import { StatusLight, Icon } from 'react-basics'; +import { useIntl, FormattedMessage } from 'react-intl'; import { FixedSizeList } from 'react-window'; import firstBy from 'thenby'; -import { Icon } from 'react-basics'; import FilterButtons from 'components/common/FilterButtons'; import NoData from 'components/common/NoData'; import { getDeviceMessage, labels } from 'components/messages'; import useLocale from 'hooks/useLocale'; import useCountryNames from 'hooks/useCountryNames'; import { BROWSERS } from 'lib/constants'; -import Bolt from 'assets/bolt.svg'; -import Visitor from 'assets/visitor.svg'; -import Eye from 'assets/eye.svg'; import { stringToColor } from 'lib/format'; import { dateFormat } from 'lib/date'; import { safeDecodeURI } from 'next-basics'; +import Icons from 'components/icons'; import styles from './RealtimeLog.module.css'; -const TYPE_ALL = 0; -const TYPE_PAGEVIEW = 1; -const TYPE_SESSION = 2; -const TYPE_EVENT = 3; +const TYPE_ALL = 'type-all'; +const TYPE_PAGEVIEW = 'type-pageview'; +const TYPE_SESSION = 'type-session'; +const TYPE_EVENT = 'type-event'; const TYPE_ICONS = { - [TYPE_PAGEVIEW]: , - [TYPE_SESSION]: , - [TYPE_EVENT]: , + [TYPE_PAGEVIEW]: , + [TYPE_SESSION]: , + [TYPE_EVENT]: , }; export default function RealtimeLog({ data, websites, websiteId }) { - const intl = useIntl(); + const { formatMessage } = useIntl(); const { locale } = useLocale(); const countryNames = useCountryNames(locale); const [filter, setFilter] = useState(TYPE_ALL); const logs = useMemo(() => { + if (!data) { + return []; + } + const { pageviews, sessions, events } = data; const logs = [...pageviews, ...sessions, ...events].sort(firstBy('createdAt', -1)); if (filter) { @@ -45,6 +46,10 @@ export default function RealtimeLog({ data, websites, websiteId }) { }, [data, filter]); const uuids = useMemo(() => { + if (!data) { + return []; + } + return data.sessions.reduce((obj, { sessionId, sessionUuid }) => { obj[sessionId] = sessionUuid; return obj; @@ -53,19 +58,19 @@ export default function RealtimeLog({ data, websites, websiteId }) { const buttons = [ { - label: , + label: formatMessage(labels.all), key: TYPE_ALL, }, { - label: , + label: formatMessage(labels.views), key: TYPE_PAGEVIEW, }, { - label: , + label: formatMessage(labels.sessions), key: TYPE_SESSION, }, { - label: , + label: formatMessage(labels.events), key: TYPE_EVENT, }, ]; @@ -124,10 +129,10 @@ export default function RealtimeLog({ data, websites, websiteId }) { id="message.log.visitor" defaultMessage="Visitor from {country} using {browser} on {os} {device}" values={{ - country: {countryNames[country] || intl.formatMessage(labels.unknown)}, + country: {countryNames[country] || formatMessage(labels.unknown)}, browser: {BROWSERS[browser]}, os: {os}, - device: {intl.formatMessage(getDeviceMessage(device))}, + device: {formatMessage(getDeviceMessage(device))}, }} /> ); diff --git a/components/metrics/RealtimeLog.module.css b/components/pages/realtime/RealtimeLog.module.css similarity index 100% rename from components/metrics/RealtimeLog.module.css rename to components/pages/realtime/RealtimeLog.module.css diff --git a/components/metrics/RealtimeViews.js b/components/pages/realtime/RealtimeViews.js similarity index 74% rename from components/metrics/RealtimeViews.js rename to components/pages/realtime/RealtimeViews.js index 19054837..c1f93be1 100644 --- a/components/metrics/RealtimeViews.js +++ b/components/pages/realtime/RealtimeViews.js @@ -1,12 +1,14 @@ import { useMemo, useState, useCallback } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { ButtonGroup, Button } from 'react-basics'; +import { useIntl } from 'react-intl'; import firstBy from 'thenby'; import { percentFilter } from 'lib/filters'; -import DataTable from './DataTable'; -import FilterButtons from 'components/common/FilterButtons'; +import DataTable from 'components/metrics/DataTable'; import { FILTER_PAGES, FILTER_REFERRERS } from 'lib/constants'; +import { labels } from 'components/messages'; -export default function RealtimeViews({ websiteId, data, websites }) { +export default function RealtimeViews({ websiteId, data = {}, websites }) { + const { formatMessage } = useIntl(); const { pageviews } = data; const [filter, setFilter] = useState(FILTER_REFERRERS); const domains = useMemo(() => websites.map(({ domain }) => domain), [websites]); @@ -20,11 +22,11 @@ export default function RealtimeViews({ websiteId, data, websites }) { const buttons = [ { - label: , + label: formatMessage(labels.referrers), key: FILTER_REFERRERS, }, { - label: , + label: formatMessage(labels.pages), key: FILTER_PAGES, }, ]; @@ -38,7 +40,7 @@ export default function RealtimeViews({ websiteId, data, websites }) { ); }; - const [referrers, pages] = useMemo(() => { + const [referrers = [], pages = []] = useMemo(() => { if (pageviews) { const referrers = percentFilter( pageviews @@ -83,24 +85,27 @@ export default function RealtimeViews({ websiteId, data, websites }) { return [referrers, pages]; } + return []; }, [pageviews]); return ( <> - + + {({ key, label }) => {label}} + {filter === FILTER_REFERRERS && ( } - metric={} + title={formatMessage(labels.referrers)} + metric={formatMessage(labels.views)} renderLabel={renderLink} data={referrers} /> )} {filter === FILTER_PAGES && ( } - metric={} + title={formatMessage(labels.pages)} + metric={formatMessage(labels.views)} renderLabel={renderLink} data={pages} /> diff --git a/components/pages/websites/WebsiteDetails.js b/components/pages/websites/WebsiteDetails.js index 903a482a..d5b1c655 100644 --- a/components/pages/websites/WebsiteDetails.js +++ b/components/pages/websites/WebsiteDetails.js @@ -8,13 +8,10 @@ import WebsiteChart from 'components/metrics/WebsiteChart'; import useApi from 'hooks/useApi'; import usePageQuery from 'hooks/usePageQuery'; import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; -import { labels } from 'components/messages'; -import styles from './WebsiteDetails.module.css'; import WebsiteTableView from './WebsiteTableView'; import WebsiteMenuView from './WebsiteMenuView'; export default function WebsiteDetails({ websiteId }) { - const { formatMessage } = useIntl(); const { get, useQuery } = useApi(); const { data, isLoading, error } = useQuery(['websites', websiteId], () => get(`/websites/${websiteId}`), @@ -22,7 +19,6 @@ export default function WebsiteDetails({ websiteId }) { const [chartLoaded, setChartLoaded] = useState(false); const { - resolve, query: { view }, } = usePageQuery(); diff --git a/components/pages/websites/WebsiteMenuView.js b/components/pages/websites/WebsiteMenuView.js index 07a93268..c8b9db9c 100644 --- a/components/pages/websites/WebsiteMenuView.js +++ b/components/pages/websites/WebsiteMenuView.js @@ -1,7 +1,7 @@ -import { Row, Column, Menu, Item, Icon, Button, Flexbox, Text } from 'react-basics'; +import { Menu, Item, Icon, Button, Flexbox, Text } from 'react-basics'; import { useIntl } from 'react-intl'; import Link from 'next/link'; -import classNames from 'classnames'; +import { GridRow, GridColumn } from 'components/layout/Grid'; import BrowsersTable from 'components/metrics/BrowsersTable'; import CountriesTable from 'components/metrics/CountriesTable'; import DevicesTable from 'components/metrics/DevicesTable'; @@ -33,7 +33,7 @@ const views = { export default function WebsiteMenuView({ websiteId, websiteDomain }) { const { formatMessage } = useIntl(); const { - resolve, + resolveUrl, query: { view }, } = usePageQuery(); @@ -80,12 +80,12 @@ export default function WebsiteMenuView({ websiteId, websiteDomain }) { }, ]; - const DetailsComponent = views[view]; + const DetailsComponent = views[view] || (() => null); return ( - - - + + + @@ -100,14 +100,14 @@ export default function WebsiteMenuView({ websiteId, websiteDomain }) { {({ key, label }) => ( - + {label} )} - - + + - - + + ); } diff --git a/components/pages/websites/WebsiteMenuView.module.css b/components/pages/websites/WebsiteMenuView.module.css index 00e451c6..f3d664c2 100644 --- a/components/pages/websites/WebsiteMenuView.module.css +++ b/components/pages/websites/WebsiteMenuView.module.css @@ -1,17 +1,3 @@ -.row { - border-top: 1px solid var(--base300); -} - -.col { - border-left: 1px solid var(--base300); - padding: 30px; -} - -.col:first-child { - padding-left: 0; - border-left: 0; -} - .menu { gap: 20px; } diff --git a/components/pages/websites/WebsiteTableView.js b/components/pages/websites/WebsiteTableView.js index 372a8f30..ce2a2bac 100644 --- a/components/pages/websites/WebsiteTableView.js +++ b/components/pages/websites/WebsiteTableView.js @@ -1,5 +1,6 @@ import { useState } from 'react'; -import { Row, Column } from 'react-basics'; +import { GridRow, GridColumn } from 'components/layout/Grid'; +//import { Row as GridRow, Column as GridColumn } from 'react-basics'; import PagesTable from 'components/metrics/PagesTable'; import ReferrersTable from 'components/metrics/ReferrersTable'; import BrowsersTable from 'components/metrics/BrowsersTable'; @@ -9,7 +10,6 @@ import WorldMap from 'components/common/WorldMap'; import CountriesTable from 'components/metrics/CountriesTable'; import EventsTable from 'components/metrics/EventsTable'; import EventsChart from 'components/metrics/EventsChart'; -import styles from './WebsiteTableView.module.css'; export default function WebsiteTableView({ websiteId }) { const [countryData, setCountryData] = useState(); @@ -20,41 +20,41 @@ export default function WebsiteTableView({ websiteId }) { return ( <> - - + + - - + + - - - - + + + + - - + + - - + + - - - - + + + + - - + + - - - - + + + + - - + + - - + + > ); } diff --git a/hooks/usePageQuery.js b/hooks/usePageQuery.js index 28c1d221..55dab688 100644 --- a/hooks/usePageQuery.js +++ b/hooks/usePageQuery.js @@ -23,9 +23,9 @@ export default function usePageQuery() { }, {}); }, [search]); - function resolve(params) { + function resolveUrl(params) { return buildUrl(asPath.split('?')[0], { ...query, ...params }); } - return { pathname, query, resolve, router }; + return { pathname, query, resolveUrl, router }; } diff --git a/pages/api/realtime/init.ts b/pages/api/realtime/init.ts index 4e04c37b..1cb5746f 100644 --- a/pages/api/realtime/init.ts +++ b/pages/api/realtime/init.ts @@ -1,6 +1,5 @@ import { subMinutes } from 'date-fns'; -import { RealtimeInit } from 'lib/types'; -import { NextApiRequestAuth } from 'lib/types'; +import { RealtimeInit, NextApiRequestAuth } from 'lib/types'; import { secret } from 'lib/crypto'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; diff --git a/pages/api/realtime/update.ts b/pages/api/realtime/update.ts index 9c9efc25..8125d02d 100644 --- a/pages/api/realtime/update.ts +++ b/pages/api/realtime/update.ts @@ -3,9 +3,8 @@ import { useAuth } from 'lib/middleware'; import { getRealtimeData } from 'queries'; import { SHARE_TOKEN_HEADER } from 'lib/constants'; import { secret } from 'lib/crypto'; -import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiRequestQueryBody, RealtimeUpdate } from 'lib/types'; import { NextApiResponse } from 'next'; -import { RealtimeUpdate } from 'lib/types'; export interface InitUpdateRequestQuery { startAt: string; diff --git a/store/queries.js b/store/queries.js index fe6e80c2..92a8f3d5 100644 --- a/store/queries.js +++ b/store/queries.js @@ -2,8 +2,12 @@ import create from 'zustand'; const store = create(() => ({})); -export function saveQuery(url, data) { - store.setState({ [url]: data }); +export function saveQuery(key, data) { + store.setState({ [key]: data }); +} + +export function getQuery(key) { + return store.getState()[key]; } export default store; diff --git a/store/websites.js b/store/websites.js index 8801d0e2..0b30cc1f 100644 --- a/store/websites.js +++ b/store/websites.js @@ -1,7 +1,7 @@ import create from 'zustand'; import produce from 'immer'; import app from './app'; -import { getDateRange } from '../lib/date'; +import { getDateRange } from 'lib/date'; const store = create(() => ({}));