Merge branch 'dev' into master

This commit is contained in:
Mike Cao 2025-02-18 13:13:47 -08:00 committed by GitHub
commit 910a14296b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
469 changed files with 7142 additions and 7426 deletions

View file

@ -1,7 +1,7 @@
import BarChartTooltip from 'components/charts/BarChartTooltip';
import Chart, { ChartProps } from 'components/charts/Chart';
import { useTheme } from 'components/hooks';
import { renderNumberLabels } from 'lib/charts';
import BarChartTooltip from '@/components/charts/BarChartTooltip';
import Chart, { ChartProps } from '@/components/charts/Chart';
import { useTheme } from '@/components/hooks';
import { renderNumberLabels } from '@/lib/charts';
import { useMemo, useState } from 'react';
export interface BarChartProps extends ChartProps {

View file

@ -1,6 +1,6 @@
import { useLocale } from 'components/hooks';
import { formatDate } from 'lib/date';
import { formatLongCurrency, formatLongNumber } from 'lib/format';
import { useLocale } from '@/components/hooks';
import { formatDate } from '@/lib/date';
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
import { Flexbox, StatusLight } from 'react-basics';
const formats = {

View file

@ -1,7 +1,7 @@
import { Chart, ChartProps } from 'components/charts/Chart';
import { Chart, ChartProps } from '@/components/charts/Chart';
import { useState } from 'react';
import { StatusLight } from 'react-basics';
import { formatLongNumber } from 'lib/format';
import { formatLongNumber } from '@/lib/format';
export interface BubbleChartProps extends ChartProps {
type?: 'bubble';

View file

@ -2,9 +2,9 @@ import { useState, useRef, useEffect, useMemo, ReactNode } from 'react';
import { Loading } from 'react-basics';
import classNames from 'classnames';
import ChartJS, { LegendItem, ChartOptions } from 'chart.js/auto';
import HoverTooltip from 'components/common/HoverTooltip';
import Legend from 'components/metrics/Legend';
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import HoverTooltip from '@/components/common/HoverTooltip';
import Legend from '@/components/metrics/Legend';
import { DEFAULT_ANIMATION_DURATION } from '@/lib/constants';
import styles from './Chart.module.css';
export interface ChartProps {

View file

@ -1,7 +1,7 @@
import { Chart, ChartProps } from 'components/charts/Chart';
import { Chart, ChartProps } from '@/components/charts/Chart';
import { useState } from 'react';
import { StatusLight } from 'react-basics';
import { formatLongNumber } from 'lib/format';
import { formatLongNumber } from '@/lib/format';
export interface PieChartProps extends ChartProps {
type?: 'doughnut' | 'pie';

View file

@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { createAvatar } from '@dicebear/core';
import { lorelei } from '@dicebear/collection';
import { getColor, getPastel } from 'lib/colors';
import { getColor, getPastel } from '@/lib/colors';
const lib = lorelei;

View file

@ -1,6 +1,6 @@
import { ReactNode } from 'react';
import { Button, LoadingButton, Form, FormButtons } from 'react-basics';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
export interface ConfirmationFormProps {
message: ReactNode;

View file

@ -1,12 +1,12 @@
import { ReactNode } from 'react';
import classNames from 'classnames';
import { Loading, SearchField } from 'react-basics';
import { useMessages, useNavigation } from 'components/hooks';
import Empty from 'components/common/Empty';
import Pager from 'components/common/Pager';
import { PagedQueryResult } from 'lib/types';
import { useMessages, useNavigation } from '@/components/hooks';
import Empty from '@/components/common/Empty';
import Pager from '@/components/common/Pager';
import { PagedQueryResult } from '@/lib/types';
import styles from './DataTable.module.css';
import { LoadingPanel } from 'components/common/LoadingPanel';
import { LoadingPanel } from '@/components/common/LoadingPanel';
const DEFAULT_SEARCH_DELAY = 600;
@ -37,26 +37,26 @@ export function DataTable({
query: { error, isLoading, isFetched },
} = queryResult || {};
const { page, pageSize, count, data } = result || {};
const { query } = params || {};
const { search } = params || {};
const hasData = Boolean(!isLoading && data?.length);
const noResults = Boolean(query && !hasData);
const noResults = Boolean(search && !hasData);
const { router, renderUrl } = useNavigation();
const handleSearch = (query: string) => {
setParams({ ...params, query, page: params.page ? page : 1 });
const handleSearch = (search: string) => {
setParams({ ...params, search, page: params.page ? page : 1 });
};
const handlePageChange = (page: number) => {
setParams({ ...params, query, page });
setParams({ ...params, search, page });
router.push(renderUrl({ page }));
};
return (
<>
{allowSearch && (hasData || query) && (
{allowSearch && (hasData || search) && (
<SearchField
className={styles.search}
value={query}
value={search}
onSearch={handleSearch}
delay={searchDelay || DEFAULT_SEARCH_DELAY}
autoFocus={autoFocus}
@ -71,7 +71,7 @@ export function DataTable({
>
{hasData ? (typeof children === 'function' ? children(result) : children) : null}
{isLoading && <Loading position="page" />}
{!isLoading && !hasData && !query && (renderEmpty ? renderEmpty() : <Empty />)}
{!isLoading && !hasData && !search && (renderEmpty ? renderEmpty() : <Empty />)}
{!isLoading && noResults && <Empty message={formatMessage(messages.noResultsFound)} />}
</div>
{allowPaging && hasData && (

View file

@ -1,5 +1,5 @@
import classNames from 'classnames';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
import styles from './Empty.module.css';
export interface EmptyProps {

View file

@ -1,6 +1,6 @@
import { ReactNode } from 'react';
import { Icon, Text, Flexbox } from 'react-basics';
import Logo from 'assets/logo.svg';
import Logo from '@/assets/logo.svg';
export interface EmptyPlaceholderProps {
message?: string;

View file

@ -1,7 +1,7 @@
import { ErrorInfo, ReactNode } from 'react';
import { ErrorBoundary as Boundary } from 'react-error-boundary';
import { Button } from 'react-basics';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
import styles from './ErrorBoundary.module.css';
const logError = (error: Error, info: ErrorInfo) => {

View file

@ -1,6 +1,6 @@
import { Icon, Icons, Text } from 'react-basics';
import styles from './ErrorMessage.module.css';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
export function ErrorMessage() {
const { formatMessage, messages } = useMessages();

View file

@ -1,3 +1,5 @@
import { GROUPED_DOMAINS } from '@/lib/constants';
function getHostName(url: string) {
const match = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?([^:/\n?=]+)/im);
return match && match.length > 1 ? match[1] : null;
@ -9,16 +11,11 @@ export function Favicon({ domain, ...props }) {
}
const hostName = domain ? getHostName(domain) : null;
const src = hostName
? `https://icons.duckduckgo.com/ip3/${GROUPED_DOMAINS[hostName]?.domain || hostName}.ico`
: null;
return hostName ? (
<img
src={`https://icons.duckduckgo.com/ip3/${hostName}.ico`}
width={16}
height={16}
alt=""
{...props}
/>
) : null;
return hostName ? <img src={src} width={16} height={16} alt="" {...props} /> : null;
}
export default Favicon;

View file

@ -1,6 +1,5 @@
import classNames from 'classnames';
import { useMessages, useNavigation } from 'components/hooks';
import { safeDecodeURIComponent } from 'next-basics';
import { useMessages, useNavigation } from '@/components/hooks';
import Link from 'next/link';
import { ReactNode } from 'react';
import { Icon, Icons } from 'react-basics';
@ -39,7 +38,7 @@ export function FilterLink({
{!value && `(${label || formatMessage(labels.unknown)})`}
{value && (
<Link href={renderUrl({ [id]: value })} className={styles.label} replace>
{safeDecodeURIComponent(label || value)}
{label || value}
</Link>
)}
{externalUrl && (

View file

@ -1,8 +1,8 @@
import { ReactNode } from 'react';
import classNames from 'classnames';
import Link from 'next/link';
import { useLocale } from 'components/hooks';
import { useLocale } from '@/components/hooks';
import styles from './LinkButton.module.css';
import { ReactNode } from 'react';
export interface LinkButtonProps {
href: string;

View file

@ -1,8 +1,8 @@
import { ReactNode } from 'react';
import classNames from 'classnames';
import { Loading } from 'react-basics';
import ErrorMessage from 'components/common/ErrorMessage';
import Empty from 'components/common/Empty';
import ErrorMessage from '@/components/common/ErrorMessage';
import Empty from '@/components/common/Empty';
import styles from './LoadingPanel.module.css';
export function LoadingPanel({

View file

@ -1,6 +1,6 @@
import classNames from 'classnames';
import { Button, Icon, Icons } from 'react-basics';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
import styles from './Pager.module.css';
export interface PagerProps {

View file

@ -7,7 +7,7 @@ import {
TextField,
SubmitButton,
} from 'react-basics';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
export function TypeConfirmationForm({
confirmationValue,
@ -26,7 +26,7 @@ export function TypeConfirmationForm({
onConfirm?: () => void;
onClose?: () => void;
}) {
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
const { formatMessage, labels, messages } = useMessages();
if (!confirmationValue) {
return null;
@ -35,10 +35,7 @@ export function TypeConfirmationForm({
return (
<Form onSubmit={onConfirm} error={error}>
<p>
<FormattedMessage
{...messages.actionConfirmation}
values={{ confirmation: <b>{confirmationValue}</b> }}
/>
{formatMessage(messages.actionConfirmation, { confirmation: <b>{confirmationValue}</b> })}
</p>
<FormRow label={formatMessage(labels.confirm)}>
<FormInput name="confirm" rules={{ validate: value => value === confirmationValue }}>

View file

@ -1,23 +1,16 @@
import { useEffect } from 'react';
import useStore, { setConfig } from 'store/app';
import { useApi } from '../useApi';
let loading = false;
import useStore, { setConfig } from '@/store/app';
import { getConfig } from '@/app/actions/getConfig';
export function useConfig() {
const { config } = useStore();
const { get } = useApi();
const configUrl = process.env.configUrl;
async function loadConfig() {
const data = await get(configUrl);
loading = false;
setConfig(data);
setConfig(await getConfig());
}
useEffect(() => {
if (!config && !loading && configUrl) {
loading = true;
if (!config) {
loadConfig();
}
}, []);

View file

@ -1,5 +1,5 @@
import { UseQueryResult } from '@tanstack/react-query';
import useStore, { setUser } from 'store/app';
import useStore, { setUser } from '@/store/app';
import { useApi } from '../useApi';
const selector = (state: { user: any }) => state.user;

View file

@ -1,6 +1,6 @@
import { useTimezone } from 'components/hooks';
import { REALTIME_INTERVAL } from 'lib/constants';
import { RealtimeData } from 'lib/types';
import { useTimezone } from '@/components/hooks/useTimezone';
import { REALTIME_INTERVAL } from '@/lib/constants';
import { RealtimeData } from '@/lib/types';
import { useApi } from '../useApi';
export function useRealtime(websiteId: string) {

View file

@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from 'react';
import { useApi } from '../useApi';
import { useTimezone } from '../useTimezone';
import { useMessages } from '../useMessages';
import { parseDateRange } from '@/lib/date';
export function useReport(
reportId: string,
@ -24,14 +25,12 @@ export function useReport(
const data: any = await get(`/reports/${id}`);
const { dateRange } = data?.parameters || {};
const { startDate, endDate } = dateRange || {};
if (startDate && endDate) {
dateRange.startDate = new Date(startDate);
dateRange.endDate = new Date(endDate);
}
data.parameters = { ...defaultParameters?.parameters, ...data.parameters };
data.parameters = {
...defaultParameters?.parameters,
...data.parameters,
dateRange: parseDateRange(dateRange.value),
};
setReport(data);
};

View file

@ -10,7 +10,7 @@ export function useSessionDataProperties(
const params = useFilterParams(websiteId);
return useQuery<any>({
queryKey: ['websites:event-data:properties', { websiteId, ...params }],
queryKey: ['websites:session-data:properties', { websiteId, ...params }],
queryFn: () => get(`/websites/${websiteId}/session-data/properties`, { ...params }),
enabled: !!websiteId,
...options,

View file

@ -1,4 +1,4 @@
import useStore, { setShareToken } from 'store/app';
import useStore, { setShareToken } from '@/store/app';
import { useApi } from '../useApi';
const selector = (state: { shareToken: string }) => state.shareToken;

View file

@ -11,6 +11,7 @@ export function useTeams(userId: string) {
queryFn: (params: any) => {
return get(`/users/${userId}/teams`, params);
},
enabled: !!userId,
});
}

View file

@ -1,6 +1,6 @@
import { UseQueryOptions } from '@tanstack/react-query';
import { useApi } from '../useApi';
import { useFilterParams } from '..//useFilterParams';
import { useFilterParams } from '../useFilterParams';
export function useWebsitePageviews(
websiteId: string,

View file

@ -1,7 +1,7 @@
import { useApi } from '../useApi';
import { usePagedQuery } from '../usePagedQuery';
import useModified from '../useModified';
import { useFilterParams } from 'components/hooks/useFilterParams';
import { useFilterParams } from '@/components/hooks/useFilterParams';
export function useWebsiteSessions(websiteId: string, params?: { [key: string]: string | number }) {
const { get } = useApi();

View file

@ -1,6 +1,6 @@
import { useApi } from '../useApi';
import useModified from '../useModified';
import { useFilterParams } from 'components/hooks/useFilterParams';
import { useFilterParams } from '@/components/hooks/useFilterParams';
export function useWebsiteSessionsWeekly(
websiteId: string,

View file

@ -1,5 +1,6 @@
import { useApi } from '../useApi';
import { useCountryNames, useRegionNames } from 'components/hooks';
import { useCountryNames } from '@/components/hooks/useCountryNames';
import { useRegionNames } from '@/components/hooks/useRegionNames';
import useLocale from '../useLocale';
export function useWebsiteValues({

View file

@ -1,20 +1,78 @@
import { useCallback } from 'react';
import * as reactQuery from '@tanstack/react-query';
import { useApi as nextUseApi } from 'next-basics';
import { getClientAuthToken } from 'lib/client';
import { SHARE_TOKEN_HEADER } from 'lib/constants';
import useStore from 'store/app';
import { getClientAuthToken } from '@/lib/client';
import { SHARE_TOKEN_HEADER } from '@/lib/constants';
import { httpGet, httpPost, httpPut, httpDelete, FetchResponse } from '@/lib/fetch';
import useStore from '@/store/app';
const selector = (state: { shareToken: { token?: string } }) => state.shareToken;
async function handleResponse(res: FetchResponse): Promise<any> {
if (!res.ok) {
return Promise.reject(new Error(res.error));
}
return Promise.resolve(res.data);
}
function handleError(err: Error | string) {
return Promise.reject((err as Error)?.message || err || null);
}
export function useApi() {
const shareToken = useStore(selector);
const { get, post, put, del } = nextUseApi(
{ authorization: `Bearer ${getClientAuthToken()}`, [SHARE_TOKEN_HEADER]: shareToken?.token },
process.env.basePath,
);
const defaultHeaders = {
authorization: `Bearer ${getClientAuthToken()}`,
[SHARE_TOKEN_HEADER]: shareToken?.token,
};
const basePath = process.env.basePath;
return { get, post, put, del, ...reactQuery };
const getUrl = (url: string) => {
return url.startsWith('http') ? url : `${basePath || ''}/api${url}`;
};
const getHeaders = (headers: any = {}) => {
return { ...defaultHeaders, ...headers };
};
return {
get: useCallback(
async (url: string, params: object = {}, headers: object = {}) => {
return httpGet(getUrl(url), params, getHeaders(headers))
.then(handleResponse)
.catch(handleError);
},
[httpGet],
),
post: useCallback(
async (url: string, params: object = {}, headers: object = {}) => {
return httpPost(getUrl(url), params, getHeaders(headers))
.then(handleResponse)
.catch(handleError);
},
[httpPost],
),
put: useCallback(
async (url: string, params: object = {}, headers: object = {}) => {
return httpPut(getUrl(url), params, getHeaders(headers))
.then(handleResponse)
.catch(handleError);
},
[httpPut],
),
del: useCallback(
async (url: string, params: object = {}, headers: object = {}) => {
return httpDelete(getUrl(url), params, getHeaders(headers))
.then(handleResponse)
.catch(handleError);
},
[httpDelete],
),
...reactQuery,
};
}
export default useApi;

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { httpGet } from 'next-basics';
import { httpGet } from '@/lib/fetch';
import enUS from '../../../public/intl/country/en-US.json';
const countryNames = {

View file

@ -1,9 +1,9 @@
import { getMinimumUnit, parseDateRange } from 'lib/date';
import { setItem } from 'next-basics';
import { DATE_RANGE_CONFIG, DEFAULT_DATE_COMPARE, DEFAULT_DATE_RANGE } from 'lib/constants';
import websiteStore, { setWebsiteDateRange, setWebsiteDateCompare } from 'store/websites';
import appStore, { setDateRange } from 'store/app';
import { DateRange } from 'lib/types';
import { getMinimumUnit, parseDateRange } from '@/lib/date';
import { setItem } from '@/lib/storage';
import { DATE_RANGE_CONFIG, DEFAULT_DATE_COMPARE, DEFAULT_DATE_RANGE } from '@/lib/constants';
import websiteStore, { setWebsiteDateRange, setWebsiteDateCompare } from '@/store/websites';
import appStore, { setDateRange } from '@/store/app';
import { DateRange } from '@/lib/types';
import { useLocale } from './useLocale';
import { useApi } from './useApi';

View file

@ -1,5 +1,5 @@
import { useMessages } from './useMessages';
import { OPERATORS } from 'lib/constants';
import { OPERATORS } from '@/lib/constants';
export function useFilters() {
const { formatMessage, labels } = useMessages();

View file

@ -1,5 +1,5 @@
import useMessages from './useMessages';
import { BROWSERS, OS_NAMES } from 'lib/constants';
import { BROWSERS, OS_NAMES } from '@/lib/constants';
import useLocale from './useLocale';
import useCountryNames from './useCountryNames';
import useLanguageNames from './useLanguageNames';

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { httpGet } from 'next-basics';
import { httpGet } from '@/lib/fetch';
import enUS from '../../../public/intl/language/en-US.json';
const languageNames = {

View file

@ -1,8 +1,9 @@
import { useEffect } from 'react';
import { httpGet, setItem } from 'next-basics';
import { LOCALE_CONFIG } from 'lib/constants';
import { getDateLocale, getTextDirection } from 'lib/lang';
import useStore, { setLocale } from 'store/app';
import { httpGet } from '@/lib/fetch';
import { setItem } from '@/lib/storage';
import { LOCALE_CONFIG } from '@/lib/constants';
import { getDateLocale, getTextDirection } from '@/lib/lang';
import useStore, { setLocale } from '@/store/app';
import { useForceUpdate } from './useForceUpdate';
import enUS from '../../../public/intl/country/en-US.json';
@ -19,13 +20,9 @@ export function useLocale() {
const dateLocale = getDateLocale(locale);
async function loadMessages(locale: string) {
const { ok, data } = await httpGet(
`${process.env.basePath || ''}/intl/messages/${locale}.json`,
);
const { data } = await httpGet(`${process.env.basePath || ''}/intl/messages/${locale}.json`);
if (ok) {
messages[locale] = data;
}
messages[locale] = data;
}
async function saveLocale(value: string) {

View file

@ -1,5 +1,5 @@
import { useIntl, FormattedMessage } from 'react-intl';
import { messages, labels } from 'components/messages';
import { useIntl } from 'react-intl';
import { messages, labels } from '@/components/messages';
export function useMessages(): any {
const intl = useIntl();
@ -21,7 +21,7 @@ export function useMessages(): any {
return descriptor ? intl.formatMessage(descriptor, values, opts) : null;
};
return { formatMessage, FormattedMessage, messages, labels, getMessage };
return { formatMessage, messages, labels, getMessage };
}
export default useMessages;

View file

@ -1,4 +1,4 @@
import useStore from 'store/modified';
import useStore from '@/store/modified';
export function useModified(key?: string) {
const modified = useStore(state => state?.[key]);

View file

@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { buildUrl, safeDecodeURIComponent } from 'next-basics';
import { buildUrl } from '@/lib/url';
export function useNavigation(): {
pathname: string;
@ -16,7 +16,7 @@ export function useNavigation(): {
const obj = {};
for (const [key, value] of params.entries()) {
obj[key] = safeDecodeURIComponent(value);
obj[key] = value;
}
return obj;

View file

@ -1,6 +1,6 @@
import { UseQueryOptions } from '@tanstack/react-query';
import { useState } from 'react';
import { PageResult, PageParams, PagedQueryResult } from 'lib/types';
import { PageResult, PageParams, PagedQueryResult } from '@/lib/types';
import { useApi } from './useApi';
import { useNavigation } from './useNavigation';
@ -11,7 +11,7 @@ export function usePagedQuery<T = any>({
}: Omit<UseQueryOptions, 'queryFn'> & { queryFn: (params?: object) => any }): PagedQueryResult<T> {
const { query: queryParams } = useNavigation();
const [params, setParams] = useState<PageParams>({
query: '',
search: '',
page: +queryParams.page || 1,
});

View file

@ -1,7 +1,7 @@
import { useEffect, useMemo } from 'react';
import useStore, { setTheme } from 'store/app';
import { getItem, setItem } from 'next-basics';
import { DEFAULT_THEME, THEME_COLORS, THEME_CONFIG } from 'lib/constants';
import useStore, { setTheme } from '@/store/app';
import { getItem, setItem } from '@/lib/storage';
import { DEFAULT_THEME, THEME_COLORS, THEME_CONFIG } from '@/lib/constants';
import { colord } from 'colord';
const selector = (state: { theme: string }) => state.theme;

View file

@ -1,5 +1,5 @@
import { setItem } from 'next-basics';
import { TIMEZONE_CONFIG } from 'lib/constants';
import { setItem } from '@/lib/storage';
import { TIMEZONE_CONFIG } from '@/lib/constants';
import { formatInTimeZone, zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz';
import useStore, { setTimezone } from 'store/app';
import useLocale from './useLocale';

View file

@ -1,30 +1,30 @@
import { Icons } from 'react-basics';
import AddUser from 'assets/add-user.svg';
import Bars from 'assets/bars.svg';
import BarChart from 'assets/bar-chart.svg';
import Bolt from 'assets/bolt.svg';
import Calendar from 'assets/calendar.svg';
import Change from 'assets/change.svg';
import Clock from 'assets/clock.svg';
import Compare from 'assets/compare.svg';
import Dashboard from 'assets/dashboard.svg';
import Eye from 'assets/eye.svg';
import Gear from 'assets/gear.svg';
import Globe from 'assets/globe.svg';
import Location from 'assets/location.svg';
import Lock from 'assets/lock.svg';
import Logo from 'assets/logo.svg';
import Magnet from 'assets/magnet.svg';
import Moon from 'assets/moon.svg';
import Nodes from 'assets/nodes.svg';
import Overview from 'assets/overview.svg';
import Profile from 'assets/profile.svg';
import PushPin from 'assets/pushpin.svg';
import Reports from 'assets/reports.svg';
import Sun from 'assets/sun.svg';
import User from 'assets/user.svg';
import Users from 'assets/users.svg';
import Visitor from 'assets/visitor.svg';
import AddUser from '@/assets/add-user.svg';
import Bars from '@/assets/bars.svg';
import BarChart from '@/assets/bar-chart.svg';
import Bolt from '@/assets/bolt.svg';
import Calendar from '@/assets/calendar.svg';
import Change from '@/assets/change.svg';
import Clock from '@/assets/clock.svg';
import Compare from '@/assets/compare.svg';
import Dashboard from '@/assets/dashboard.svg';
import Eye from '@/assets/eye.svg';
import Gear from '@/assets/gear.svg';
import Globe from '@/assets/globe.svg';
import Location from '@/assets/location.svg';
import Lock from '@/assets/lock.svg';
import Logo from '@/assets/logo.svg';
import Magnet from '@/assets/magnet.svg';
import Moon from '@/assets/moon.svg';
import Nodes from '@/assets/nodes.svg';
import Overview from '@/assets/overview.svg';
import Profile from '@/assets/profile.svg';
import PushPin from '@/assets/pushpin.svg';
import Reports from '@/assets/reports.svg';
import Sun from '@/assets/sun.svg';
import User from '@/assets/user.svg';
import Users from '@/assets/users.svg';
import Visitor from '@/assets/visitor.svg';
const icons = {
...Icons,

View file

@ -1,10 +1,10 @@
import { useState } from 'react';
import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics';
import { endOfYear, isSameDay } from 'date-fns';
import DatePickerForm from 'components/metrics/DatePickerForm';
import { useLocale, useMessages } from 'components/hooks';
import Icons from 'components/icons';
import { formatDate, parseDateValue } from 'lib/date';
import DatePickerForm from '@/components/metrics/DatePickerForm';
import { useLocale, useMessages } from '@/components/hooks';
import Icons from '@/components/icons';
import { formatDate, parseDateValue } from '@/lib/date';
import styles from './DateFilter.module.css';
import classNames from 'classnames';

View file

@ -1,8 +1,8 @@
import { Icon, Button, PopupTrigger, Popup } from 'react-basics';
import classNames from 'classnames';
import { languages } from 'lib/lang';
import { useLocale } from 'components/hooks';
import Icons from 'components/icons';
import { languages } from '@/lib/lang';
import { useLocale } from '@/components/hooks';
import Icons from '@/components/icons';
import styles from './LanguageButton.module.css';
export function LanguageButton() {

View file

@ -1,6 +1,6 @@
import { Button, Icon, Icons, TooltipPopup } from 'react-basics';
import Link from 'next/link';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
export function LogoutButton({
tooltipPosition = 'top',

View file

@ -9,9 +9,9 @@ import {
Popup,
} from 'react-basics';
import { startOfMonth, endOfMonth } from 'date-fns';
import Icons from 'components/icons';
import { useLocale } from 'components/hooks';
import { formatDate } from 'lib/date';
import Icons from '@/components/icons';
import { useLocale } from '@/components/hooks';
import { formatDate } from '@/lib/date';
import styles from './MonthSelect.module.css';
export function MonthSelect({ date = new Date(), onChange }) {

View file

@ -1,9 +1,9 @@
import { Key } from 'react';
import { Icon, Button, PopupTrigger, Popup, Menu, Item, Text } from 'react-basics';
import { useRouter } from 'next/navigation';
import Icons from 'components/icons';
import { useMessages, useLogin, useLocale } from 'components/hooks';
import { CURRENT_VERSION } from 'lib/constants';
import Icons from '@/components/icons';
import { useMessages, useLogin, useLocale } from '@/components/hooks';
import { CURRENT_VERSION } from '@/lib/constants';
import styles from './ProfileButton.module.css';
export function ProfileButton() {

View file

@ -1,8 +1,8 @@
import { LoadingButton, Icon, TooltipPopup } from 'react-basics';
import { setWebsiteDateRange } from 'store/websites';
import { useDateRange } from 'components/hooks';
import Icons from 'components/icons';
import { useMessages } from 'components/hooks';
import { setWebsiteDateRange } from '@/store/websites';
import { useDateRange } from '@/components/hooks';
import Icons from '@/components/icons';
import { useMessages } from '@/components/hooks';
export function RefreshButton({
websiteId,

View file

@ -1,8 +1,8 @@
import { Button, Icon, PopupTrigger, Popup, Form, FormRow } from 'react-basics';
import TimezoneSetting from 'app/(main)/profile/TimezoneSetting';
import DateRangeSetting from 'app/(main)/profile/DateRangeSetting';
import Icons from 'components/icons';
import { useMessages } from 'components/hooks';
import TimezoneSetting from '@/app/(main)/profile/TimezoneSetting';
import DateRangeSetting from '@/app/(main)/profile/DateRangeSetting';
import Icons from '@/components/icons';
import { useMessages } from '@/components/hooks';
import styles from './SettingsButton.module.css';
export function SettingsButton() {

View file

@ -1,8 +1,8 @@
import { Key } from 'react';
import { Text, Icon, Button, Popup, Menu, Item, PopupTrigger, Flexbox } from 'react-basics';
import classNames from 'classnames';
import Icons from 'components/icons';
import { useLogin, useMessages, useTeams, useTeamUrl } from 'components/hooks';
import Icons from '@/components/icons';
import { useLogin, useMessages, useTeams, useTeamUrl } from '@/components/hooks';
import styles from './TeamsButton.module.css';
export function TeamsButton({
@ -16,7 +16,7 @@ export function TeamsButton({
}) {
const { user } = useLogin();
const { formatMessage, labels } = useMessages();
const { result } = useTeams(user?.id);
const { result } = useTeams(user.id);
const { teamId } = useTeamUrl();
const team = result?.data?.find(({ id }) => id === teamId);

View file

@ -1,7 +1,7 @@
import { useTransition, animated } from '@react-spring/web';
import { Button, Icon } from 'react-basics';
import { useTheme } from 'components/hooks';
import Icons from 'components/icons';
import { useTheme } from '@/components/hooks';
import Icons from '@/components/icons';
import styles from './ThemeButton.module.css';
export function ThemeButton() {
@ -28,7 +28,7 @@ export function ThemeButton() {
<Button variant="quiet" className={styles.button} onClick={handleClick}>
{transitions((style, item) => (
<animated.div key={item} style={style}>
<Icon className={styles.icon}>{item === 'light' ? <Icons.Sun /> : <Icons.Moon />}</Icon>
<Icon>{item === 'light' ? <Icons.Sun /> : <Icons.Moon />}</Icon>
</animated.div>
))}
</Button>

View file

@ -1,10 +1,10 @@
import { useDateRange, useLocale } from 'components/hooks';
import { useDateRange, useLocale } from '@/components/hooks';
import { isAfter } from 'date-fns';
import { getOffsetDateRange } from 'lib/date';
import { getOffsetDateRange } from '@/lib/date';
import { Button, Icon, Icons } from 'react-basics';
import DateFilter from './DateFilter';
import styles from './WebsiteDateFilter.module.css';
import { DateRange } from 'lib/types';
import { DateRange } from '@/lib/types';
export function WebsiteDateFilter({
websiteId,

View file

@ -1,7 +1,7 @@
import { useState, Key } from 'react';
import { Dropdown, Item } from 'react-basics';
import { useWebsite, useWebsites, useMessages } from 'components/hooks';
import Empty from 'components/common/Empty';
import { useWebsite, useWebsites, useMessages } from '@/components/hooks';
import Empty from '@/components/common/Empty';
import styles from './WebsiteSelect.module.css';
export function WebsiteSelect({
@ -14,12 +14,12 @@ export function WebsiteSelect({
onSelect?: (key: any) => void;
}) {
const { formatMessage, labels, messages } = useMessages();
const [query, setQuery] = useState('');
const [search, setSearch] = useState('');
const [selectedId, setSelectedId] = useState<Key>(websiteId);
const { data: website } = useWebsite(selectedId as string);
const queryResult = useWebsites({ teamId }, { query, pageSize: 5 });
const queryResult = useWebsites({ teamId }, { search, pageSize: 5 });
const renderValue = () => {
return website?.name;
@ -35,7 +35,7 @@ export function WebsiteSelect({
};
const handleSearch = (value: string) => {
setQuery(value);
setSearch(value);
};
return (

View file

@ -1,6 +1,6 @@
import { ReactNode } from 'react';
import { usePathname } from 'next/navigation';
import SideNav from 'components/layout/SideNav';
import SideNav from '@/components/layout/SideNav';
import styles from './MenuLayout.module.css';
export function MenuLayout({ items = [], children }: { items: any[]; children: ReactNode }) {

View file

@ -51,11 +51,6 @@ a.item {
color: var(--base900);
}
.item.disabled {
color: var(--base500) !important;
pointer-events: none;
}
.minimized .text,
.minimized .header {
display: none;

View file

@ -3,7 +3,7 @@ import { Icon, Text, TooltipPopup } from 'react-basics';
import classNames from 'classnames';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import Icons from 'components/icons';
import Icons from '@/components/icons';
import styles from './NavGroup.module.css';
export interface NavGroupProps {

View file

@ -2,7 +2,7 @@
import { ReactNode } from 'react';
import classNames from 'classnames';
import { Banner, Loading } from 'react-basics';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
import styles from './Page.module.css';
export function Page({

View file

@ -280,6 +280,23 @@ export const labels = defineMessages({
lastSeen: { id: 'label.last-seen', defaultMessage: 'Last seen' },
firstSeen: { id: 'label.first-seen', defaultMessage: 'First seen' },
properties: { id: 'label.properties', defaultMessage: 'Properties' },
channels: { id: 'label.channels', defaultMessage: 'Channels' },
direct: { id: 'label.direct', defaultMessage: 'Direct' },
referral: { id: 'label.referral', defaultMessage: 'Referral' },
affiliate: { id: 'label.affiliate', defaultMessage: 'Affiliate' },
email: { id: 'label.email', defaultMessage: 'Email' },
sms: { id: 'label.sms', defaultMessage: 'SMS' },
organicSearch: { id: 'label.organic-search', defaultMessage: 'Organic search' },
organicSocial: { id: 'label.organic-social', defaultMessage: 'Organic social' },
organicShopping: { id: 'label.organic-shopping', defaultMessage: 'Organic shopping' },
organicVideo: { id: 'label.organic-video', defaultMessage: 'Organic video' },
paidAds: { id: 'label.paid-ads', defaultMessage: 'Paid ads' },
paidSearch: { id: 'label.paid-search', defaultMessage: 'Paid search' },
paidSocial: { id: 'label.paid-social', defaultMessage: 'Paid social' },
paidShopping: { id: 'label.paid-shopping', defaultMessage: 'Paid shopping' },
paidVideo: { id: 'label.paid-video', defaultMessage: 'Paid video' },
grouped: { id: 'label.grouped', defaultMessage: 'Grouped' },
other: { id: 'label.other', defaultMessage: 'Other' },
});
export const messages = defineMessages({

View file

@ -10,8 +10,3 @@
font-size: var(--font-size-md);
font-weight: 400;
}
.value {
font-weight: 600;
margin-inline-end: 4px;
}

View file

@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { StatusLight } from 'react-basics';
import { useApi } from 'components/hooks';
import { useMessages } from 'components/hooks';
import { useApi } from '@/components/hooks';
import { useMessages } from '@/components/hooks';
import styles from './ActiveUsers.module.css';
export function ActiveUsers({
@ -24,7 +24,7 @@ export function ActiveUsers({
const count = useMemo(() => {
if (websiteId) {
return data?.x || 0;
return data?.visitors || 0;
}
return value !== undefined ? value : 0;

View file

@ -1,8 +1,8 @@
import FilterLink from 'components/common/FilterLink';
import MetricsTable, { MetricsTableProps } from 'components/metrics/MetricsTable';
import { useMessages } from 'components/hooks';
import { useFormat } from 'components/hooks';
import TypeIcon from 'components/common/TypeIcon';
import FilterLink from '@/components/common/FilterLink';
import MetricsTable, { MetricsTableProps } from '@/components/metrics/MetricsTable';
import { useMessages } from '@/components/hooks';
import { useFormat } from '@/components/hooks';
import TypeIcon from '@/components/common/TypeIcon';
export function BrowsersTable(props: MetricsTableProps) {
const { formatMessage, labels } = useMessages();

View file

@ -0,0 +1,22 @@
import MetricsTable, { MetricsTableProps } from '@/components/metrics/MetricsTable';
import { useMessages } from '@/components/hooks';
export function ChannelsTable(props: MetricsTableProps) {
const { formatMessage, labels } = useMessages();
const renderLabel = ({ x }) => {
return formatMessage(labels[x]);
};
return (
<MetricsTable
{...props}
title={formatMessage(labels.channels)}
type="channel"
renderLabel={renderLabel}
metric={formatMessage(labels.visitors)}
/>
);
}
export default ChannelsTable;

View file

@ -1,8 +1,8 @@
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import { emptyFilter } from 'lib/filters';
import FilterLink from 'components/common/FilterLink';
import { useMessages } from 'components/hooks';
import { useFormat } from 'components/hooks';
import { emptyFilter } from '@/lib/filters';
import FilterLink from '@/components/common/FilterLink';
import { useMessages } from '@/components/hooks';
import { useFormat } from '@/components/hooks';
export function CitiesTable(props: MetricsTableProps) {
const { formatMessage, labels } = useMessages();

View file

@ -1,8 +1,8 @@
import FilterLink from 'components/common/FilterLink';
import { useCountryNames } from 'components/hooks';
import { useLocale, useMessages, useFormat } from 'components/hooks';
import FilterLink from '@/components/common/FilterLink';
import { useCountryNames } from '@/components/hooks';
import { useLocale, useMessages, useFormat } from '@/components/hooks';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import TypeIcon from 'components/common/TypeIcon';
import TypeIcon from '@/components/common/TypeIcon';
export function CountriesTable({ ...props }: MetricsTableProps) {
const { locale } = useLocale();

View file

@ -1,9 +1,9 @@
import { useState } from 'react';
import { Button, ButtonGroup, Calendar } from 'react-basics';
import { isAfter, isBefore, isSameDay, startOfDay, endOfDay } from 'date-fns';
import { useLocale } from 'components/hooks';
import { FILTER_DAY, FILTER_RANGE } from 'lib/constants';
import { useMessages } from 'components/hooks';
import { useLocale } from '@/components/hooks';
import { FILTER_DAY, FILTER_RANGE } from '@/lib/constants';
import { useMessages } from '@/components/hooks';
import styles from './DatePickerForm.module.css';
export function DatePickerForm({

View file

@ -1,8 +1,8 @@
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import FilterLink from 'components/common/FilterLink';
import { useMessages } from 'components/hooks';
import { useFormat } from 'components/hooks';
import TypeIcon from 'components/common/TypeIcon';
import FilterLink from '@/components/common/FilterLink';
import { useMessages } from '@/components/hooks';
import { useFormat } from '@/components/hooks';
import TypeIcon from '@/components/common/TypeIcon';
export function DevicesTable(props: MetricsTableProps) {
const { formatMessage, labels } = useMessages();

View file

@ -1,8 +1,8 @@
import { colord } from 'colord';
import BarChart from 'components/charts/BarChart';
import { useDateRange, useLocale, useWebsiteEventsSeries } from 'components/hooks';
import { renderDateLabels } from 'lib/charts';
import { CHART_COLORS } from 'lib/constants';
import BarChart from '@/components/charts/BarChart';
import { useDateRange, useLocale, useWebsiteEventsSeries } from '@/components/hooks';
import { renderDateLabels } from '@/lib/charts';
import { CHART_COLORS } from '@/lib/constants';
import { useMemo } from 'react';
export interface EventsChartProps {

View file

@ -1,5 +1,5 @@
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
export function EventsTable(props: MetricsTableProps) {
const { formatMessage, labels } = useMessages();

View file

@ -7,13 +7,13 @@ import {
useMessages,
useFormat,
useFilters,
} from 'components/hooks';
import PopupForm from 'app/(main)/reports/[reportId]/PopupForm';
import FieldFilterEditForm from 'app/(main)/reports/[reportId]/FieldFilterEditForm';
import { OPERATOR_PREFIXES } from 'lib/constants';
import { isSearchOperator, parseParameterValue } from 'lib/params';
} from '@/components/hooks';
import PopupForm from '@/app/(main)/reports/[reportId]/PopupForm';
import FieldFilterEditForm from '@/app/(main)/reports/[reportId]/FieldFilterEditForm';
import { OPERATOR_PREFIXES } from '@/lib/constants';
import { isSearchOperator, parseParameterValue } from '@/lib/params';
import styles from './FilterTags.module.css';
import WebsiteFilterButton from 'app/(main)/websites/[websiteId]/WebsiteFilterButton';
import WebsiteFilterButton from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
export function FilterTags({
websiteId,

View file

@ -1,6 +1,6 @@
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import FilterLink from 'components/common/FilterLink';
import { useMessages } from 'components/hooks';
import FilterLink from '@/components/common/FilterLink';
import { useMessages } from '@/components/hooks';
import { Flexbox } from 'react-basics';
export function HostsTable(props: MetricsTableProps) {

View file

@ -1,8 +1,8 @@
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import { percentFilter } from 'lib/filters';
import { useLocale } from 'components/hooks';
import { useMessages } from 'components/hooks';
import { useFormat } from 'components/hooks';
import { percentFilter } from '@/lib/filters';
import { useLocale } from '@/components/hooks';
import { useMessages } from '@/components/hooks';
import { useFormat } from '@/components/hooks';
export function LanguagesTable({
onDataLoad,

View file

@ -1,5 +1,4 @@
import { StatusLight } from 'react-basics';
import { safeDecodeURIComponent } from 'next-basics';
import { colord } from 'colord';
import classNames from 'classnames';
import { LegendItem } from 'chart.js/auto';
@ -28,9 +27,7 @@ export function Legend({
className={classNames(styles.label, { [styles.hidden]: hidden })}
onClick={() => onClick(item)}
>
<StatusLight color={color.alpha(color.alpha() + 0.2).toHex()}>
{safeDecodeURIComponent(text)}
</StatusLight>
<StatusLight color={color.alpha(color.alpha() + 0.2).toHex()}>{text}</StatusLight>
</div>
);
})}

View file

@ -1,9 +1,9 @@
import { FixedSizeList } from 'react-window';
import { useSpring, animated, config } from '@react-spring/web';
import classNames from 'classnames';
import Empty from 'components/common/Empty';
import { formatLongNumber } from 'lib/format';
import { useMessages } from 'components/hooks';
import Empty from '@/components/common/Empty';
import { formatLongNumber } from '@/lib/format';
import { useMessages } from '@/components/hooks';
import styles from './ListTable.module.css';
import { ReactNode } from 'react';

View file

@ -1,7 +1,7 @@
import classNames from 'classnames';
import { useSpring, animated } from '@react-spring/web';
import { formatNumber } from 'lib/format';
import ChangeLabel from 'components/metrics/ChangeLabel';
import { formatNumber } from '@/lib/format';
import ChangeLabel from '@/components/metrics/ChangeLabel';
import styles from './MetricCard.module.css';
export interface MetricCardProps {

View file

@ -1,7 +1,7 @@
import { ReactNode } from 'react';
import { Loading, cloneChildren } from 'react-basics';
import ErrorMessage from 'components/common/ErrorMessage';
import { formatLongNumber } from 'lib/format';
import ErrorMessage from '@/components/common/ErrorMessage';
import { formatLongNumber } from '@/lib/format';
import styles from './MetricsBar.module.css';
export interface MetricsBarProps {

View file

@ -1,18 +1,18 @@
import { ReactNode, useMemo, useState } from 'react';
import { Loading, Icon, Text, SearchField } from 'react-basics';
import classNames from 'classnames';
import ErrorMessage from 'components/common/ErrorMessage';
import LinkButton from 'components/common/LinkButton';
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import { percentFilter } from 'lib/filters';
import ErrorMessage from '@/components/common/ErrorMessage';
import LinkButton from '@/components/common/LinkButton';
import { DEFAULT_ANIMATION_DURATION } from '@/lib/constants';
import { percentFilter } from '@/lib/filters';
import {
useNavigation,
useWebsiteMetrics,
useMessages,
useLocale,
useFormat,
} from 'components/hooks';
import Icons from 'components/icons';
} from '@/components/hooks';
import Icons from '@/components/icons';
import ListTable, { ListTableProps } from './ListTable';
import styles from './MetricsTable.module.css';
@ -72,7 +72,7 @@ export function MetricsTable({
return filter(arr);
}, items);
} else {
items = dataFilter(data);
items = dataFilter(items);
}
}

View file

@ -1,7 +1,7 @@
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import FilterLink from 'components/common/FilterLink';
import { useMessages, useFormat } from 'components/hooks';
import TypeIcon from 'components/common/TypeIcon';
import FilterLink from '@/components/common/FilterLink';
import { useMessages, useFormat } from '@/components/hooks';
import TypeIcon from '@/components/common/TypeIcon';
export function OSTable(props: MetricsTableProps) {
const { formatMessage, labels } = useMessages();

View file

@ -1,8 +1,8 @@
import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider';
import FilterButtons from 'components/common/FilterButtons';
import FilterLink from 'components/common/FilterLink';
import { useMessages, useNavigation } from 'components/hooks';
import { emptyFilter } from 'lib/filters';
import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
import FilterButtons from '@/components/common/FilterButtons';
import FilterLink from '@/components/common/FilterLink';
import { useMessages, useNavigation } from '@/components/hooks';
import { emptyFilter } from '@/lib/filters';
import { useContext } from 'react';
import MetricsTable, { MetricsTableProps } from './MetricsTable';

View file

@ -1,7 +1,7 @@
import { useMemo } from 'react';
import BarChart, { BarChartProps } from 'components/charts/BarChart';
import { useLocale, useTheme, useMessages } from 'components/hooks';
import { renderDateLabels } from 'lib/charts';
import BarChart, { BarChartProps } from '@/components/charts/BarChart';
import { useLocale, useTheme, useMessages } from '@/components/hooks';
import { renderDateLabels } from '@/lib/charts';
export interface PagepageviewsChartProps extends BarChartProps {
data: {

View file

@ -1,10 +1,9 @@
import { useState } from 'react';
import { safeDecodeURI } from 'next-basics';
import FilterButtons from 'components/common/FilterButtons';
import { emptyFilter, paramFilter } from 'lib/filters';
import { FILTER_RAW, FILTER_COMBINED } from 'lib/constants';
import FilterButtons from '@/components/common/FilterButtons';
import { emptyFilter, paramFilter } from '@/lib/filters';
import { FILTER_RAW, FILTER_COMBINED } from '@/lib/constants';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
import styles from './QueryParametersTable.module.css';
const filters = {
@ -39,8 +38,8 @@ export function QueryParametersTable({
x
) : (
<div className={styles.item}>
<div className={styles.param}>{safeDecodeURI(p)}</div>
<div className={styles.value}>{safeDecodeURI(v)}</div>
<div className={styles.param}>{p}</div>
<div className={styles.value}>{v}</div>
</div>
)
}

View file

@ -1,8 +1,8 @@
import { useMemo, useRef } from 'react';
import { startOfMinute, subMinutes, isBefore } from 'date-fns';
import PageviewsChart from './PageviewsChart';
import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from 'lib/constants';
import { RealtimeData } from 'lib/types';
import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from '@/lib/constants';
import { RealtimeData } from '@/lib/types';
export interface RealtimeChartProps {
data: RealtimeData;

View file

@ -1,12 +1,53 @@
import FilterLink from 'components/common/FilterLink';
import Favicon from 'components/common/Favicon';
import { useMessages } from 'components/hooks';
import FilterLink from '@/components/common/FilterLink';
import Favicon from '@/components/common/Favicon';
import { useMessages, useNavigation } from '@/components/hooks';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import FilterButtons from '@/components/common/FilterButtons';
import thenby from 'thenby';
import { GROUPED_DOMAINS } from '@/lib/constants';
import { Flexbox } from 'react-basics';
export function ReferrersTable(props: MetricsTableProps) {
export interface ReferrersTableProps extends MetricsTableProps {
allowFilter?: boolean;
}
export function ReferrersTable({ allowFilter, ...props }: ReferrersTableProps) {
const {
router,
renderUrl,
query: { view = 'referrer' },
} = useNavigation();
const { formatMessage, labels } = useMessages();
const handleSelect = (key: any) => {
router.push(renderUrl({ view: key }), { scroll: false });
};
const buttons = [
{
label: formatMessage(labels.domain),
key: 'referrer',
},
{
label: formatMessage(labels.grouped),
key: 'grouped',
},
];
const renderLink = ({ x: referrer }) => {
if (view === 'grouped') {
if (referrer === '_other') {
return `(${formatMessage(labels.other)})`;
} else {
return (
<Flexbox alignItems="center" gap={10}>
<Favicon domain={referrer} />
{GROUPED_DOMAINS.find(({ domain }) => domain === referrer)?.name}
</Flexbox>
);
}
}
return (
<FilterLink
id="referrer"
@ -19,15 +60,41 @@ export function ReferrersTable(props: MetricsTableProps) {
);
};
const groupedFilter = (data: any[]) => {
const groups = { _other: 0 };
for (const { x, y } of data) {
for (const { domain, match } of GROUPED_DOMAINS) {
if (Array.isArray(match) ? match.some(str => x.includes(str)) : x.includes(match)) {
if (!groups[domain]) {
groups[domain] = 0;
}
groups[domain] += y;
} else {
groups._other += y;
}
}
}
return Object.keys(groups)
.map((key: any) => ({ x: key, y: groups[key] }))
.sort(thenby.firstBy('y', -1));
};
return (
<>
<MetricsTable
{...props}
title={formatMessage(labels.referrers)}
type="referrer"
metric={formatMessage(labels.views)}
metric={formatMessage(labels.visitors)}
dataFilter={view === 'grouped' ? groupedFilter : undefined}
renderLabel={renderLink}
/>
>
{allowFilter && (
<FilterButtons items={buttons} selectedKey={view} onSelect={handleSelect} />
)}
</MetricsTable>
</>
);
}

View file

@ -1,8 +1,8 @@
import FilterLink from 'components/common/FilterLink';
import { emptyFilter } from 'lib/filters';
import { useMessages, useLocale, useRegionNames } from 'components/hooks';
import FilterLink from '@/components/common/FilterLink';
import { emptyFilter } from '@/lib/filters';
import { useMessages, useLocale, useRegionNames } from '@/components/hooks';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import TypeIcon from 'components/common/TypeIcon';
import TypeIcon from '@/components/common/TypeIcon';
export function RegionsTable(props: MetricsTableProps) {
const { locale } = useLocale();

View file

@ -1,5 +1,5 @@
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
export function ScreenTable(props: MetricsTableProps) {
const { formatMessage, labels } = useMessages();

View file

@ -1,6 +1,6 @@
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import FilterLink from 'components/common/FilterLink';
import { useMessages } from 'components/hooks';
import FilterLink from '@/components/common/FilterLink';
import { useMessages } from '@/components/hooks';
import { Flexbox } from 'react-basics';
export function TagsTable(props: MetricsTableProps) {

View file

@ -2,14 +2,14 @@ import { useState, useMemo, HTMLAttributes } from 'react';
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
import classNames from 'classnames';
import { colord } from 'colord';
import HoverTooltip from 'components/common/HoverTooltip';
import { ISO_COUNTRIES, MAP_FILE } from 'lib/constants';
import { useDateRange, useTheme, useWebsiteMetrics } from 'components/hooks';
import { useCountryNames } from 'components/hooks';
import { useLocale } from 'components/hooks';
import { useMessages } from 'components/hooks';
import { formatLongNumber } from 'lib/format';
import { percentFilter } from 'lib/filters';
import HoverTooltip from '@/components/common/HoverTooltip';
import { ISO_COUNTRIES, MAP_FILE } from '@/lib/constants';
import { useDateRange, useTheme, useWebsiteMetrics } from '@/components/hooks';
import { useCountryNames } from '@/components/hooks';
import { useLocale } from '@/components/hooks';
import { useMessages } from '@/components/hooks';
import { formatLongNumber } from '@/lib/format';
import { percentFilter } from '@/lib/filters';
import styles from './WorldMap.module.css';
export function WorldMap({