Typescript refactor.

This commit is contained in:
Mike Cao 2023-12-03 03:07:03 -08:00
parent b578162cb6
commit 7c42f0da82
173 changed files with 968 additions and 549 deletions

View file

@ -36,11 +36,11 @@ export function DataTable({
const hasData = Boolean(!isLoading && data?.length);
const noResults = Boolean(!isLoading && query && !hasData);
const handleSearch = query => {
const handleSearch = (query: string) => {
setParams({ ...params, query, page: params.page ? page : 1 });
};
const handlePageChange = page => {
const handlePageChange = (page: number) => {
setParams({ ...params, query, page });
};
@ -54,7 +54,7 @@ export function DataTable({
<SearchField
className={styles.search}
value={query}
onChange={handleSearch}
onSearch={handleSearch}
delay={searchDelay || DEFAULT_SEARCH_DELAY}
autoFocus={true}
placeholder={formatMessage(labels.search)}

View file

@ -3,11 +3,11 @@ import { Icon, Text, Flexbox } from 'react-basics';
import Logo from 'assets/logo.svg';
export interface EmptyPlaceholderProps {
message: string;
message?: string;
children?: ReactNode;
}
export function EmptyPlaceholder({ message, children }) {
export function EmptyPlaceholder({ message, children }: EmptyPlaceholderProps) {
return (
<Flexbox direction="column" alignItems="center" justifyContent="center" gap={60} height={600}>
<Icon size="xl">

View file

@ -4,7 +4,7 @@ import { ButtonGroup, Button, Flexbox } from 'react-basics';
export interface FilterButtonsProps {
items: any[];
selectedKey?: Key;
onSelect: () => void;
onSelect: (key: any) => void;
}
export function FilterButtons({ items, selectedKey, onSelect }: FilterButtonsProps) {

View file

@ -1,3 +1,4 @@
declare module '*.css';
declare module '*.svg';
declare module '*.json';
declare module 'uuid';

View file

@ -1,28 +0,0 @@
import { useState } from 'react';
export function useApiFilter() {
const [filter, setFilter] = useState();
const [filterType, setFilterType] = useState('All');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const handleFilterChange = value => setFilter(value);
const handlePageChange = value => setPage(value);
const handlePageSizeChange = value => setPageSize(value);
return {
filter,
setFilter,
filterType,
setFilterType,
page,
setPage,
pageSize,
setPageSize,
handleFilterChange,
handlePageChange,
handlePageSizeChange,
};
}
export default useApiFilter;

View file

@ -6,7 +6,7 @@ import websiteStore, { setWebsiteDateRange } from 'store/websites';
import appStore, { setDateRange } from 'store/app';
import useApi from './useApi';
export function useDateRange(websiteId: string) {
export function useDateRange(websiteId?: string) {
const { get } = useApi();
const { locale } = useLocale();
const websiteConfig = websiteStore(state => state[websiteId]?.dateRange);

View file

@ -1,24 +1,33 @@
import { UseQueryOptions } from '@tanstack/react-query';
import { useState, Dispatch, SetStateAction } from 'react';
import { useApi } from 'components/hooks/useApi';
import { SearchFilter, FilterResult } from 'lib/types';
import { FilterResult, SearchFilter } from 'lib/types';
export interface FilterQueryResult<T> {
result: FilterResult<any[]>;
result: FilterResult<T>;
query: any;
params: SearchFilter;
setParams: Dispatch<SetStateAction<T | SearchFilter>>;
}
export function useFilterQuery<T>(props = {}): FilterQueryResult<T> {
export function useFilterQuery<T>({
queryKey,
queryFn,
...options
}: UseQueryOptions): FilterQueryResult<T> {
const [params, setParams] = useState<T | SearchFilter>({
query: '',
page: 1,
});
const { useQuery } = useApi();
const { data, ...query } = useQuery<FilterResult<any[]>>({ ...props });
const { data, ...query } = useQuery({
queryKey: [...queryKey, params],
queryFn: (data: any) => queryFn({ ...data, ...params }),
...options,
});
return {
result: data,
result: data as FilterResult<any>,
query,
params,
setParams,

View file

@ -2,7 +2,12 @@ import { useMemo } from 'react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { buildUrl } from 'next-basics';
export function useNavigation() {
export function useNavigation(): {
pathname: string;
query: { [key: string]: string };
router: any;
makeUrl: (params: any, reset?: boolean) => string;
} {
const router = useRouter();
const pathname = usePathname();
const params = useSearchParams();

View file

@ -1,19 +1,19 @@
import { useState } from 'react';
import useApi from './useApi';
import useApiFilter from 'components/hooks/useApiFilter';
import useFilterQuery from 'components/hooks/useFilterQuery';
export function useReports() {
export function useReports(websiteId?: string) {
const [modified, setModified] = useState(Date.now());
const { get, useQuery, del, useMutation } = useApi();
const { mutate } = useMutation(reportId => del(`/reports/${reportId}`));
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
useApiFilter();
const { data, error, isLoading } = useQuery({
queryKey: ['reports', { modified, filter, page, pageSize }],
queryFn: () => get(`/reports`, { filter, page, pageSize }),
const { get, del, useMutation } = useApi();
const { mutate } = useMutation({ mutationFn: (reportId: string) => del(`/reports/${reportId}`) });
const queryResult = useFilterQuery({
queryKey: ['reports', { websiteId, modified }],
queryFn: (params: any) => {
return get(websiteId ? `/websites/${websiteId}/reports` : `/reports`, params);
},
});
const deleteReport = id => {
const deleteReport = (id: any) => {
mutate(id, {
onSuccess: () => {
setModified(Date.now());
@ -22,16 +22,8 @@ export function useReports() {
};
return {
reports: data,
error,
isLoading,
...queryResult,
deleteReport,
filter,
page,
pageSize,
handleFilterChange,
handlePageChange,
handlePageSizeChange,
};
}

View file

@ -3,7 +3,11 @@ import useApi from './useApi';
const selector = (state: { shareToken: string }) => state.shareToken;
export function useShareToken(shareId: string) {
export function useShareToken(shareId: string): {
shareToken: any;
isLoading?: boolean;
error?: Error;
} {
const shareToken = useStore(selector);
const { get, useQuery } = useApi();
const { isLoading, error } = useQuery({

View file

@ -3,9 +3,20 @@ 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 'components/hooks/useLocale';
import { formatDate } from 'lib/date';
import Icons from 'components/icons';
import useMessages from 'components/hooks/useMessages';
import Icons from 'components/icons';
import { formatDate } from 'lib/date';
export interface DateFilterProps {
value: string;
startDate: Date;
endDate: Date;
className?: string;
onChange?: (value: string) => void;
selectedUnit?: string;
showAllTime?: boolean;
alignment?: 'start' | 'center' | 'end';
}
export function DateFilter({
value,
@ -16,7 +27,7 @@ export function DateFilter({
selectedUnit,
showAllTime = false,
alignment = 'end',
}) {
}: DateFilterProps) {
const { formatMessage, labels } = useMessages();
const [showPicker, setShowPicker] = useState(false);
@ -65,7 +76,7 @@ export function DateFilter({
},
].filter(n => n);
const renderValue = value => {
const renderValue = (value: string) => {
return value.startsWith('range') ? (
<CustomRange
startDate={startDate}
@ -78,7 +89,7 @@ export function DateFilter({
);
};
const handleChange = value => {
const handleChange = (value: string) => {
if (value === 'custom') {
setShowPicker(true);
return;
@ -86,7 +97,7 @@ export function DateFilter({
onChange(value);
};
const handlePickerChange = value => {
const handlePickerChange = (value: string) => {
setShowPicker(false);
onChange(value);
};
@ -102,7 +113,7 @@ export function DateFilter({
value={value}
alignment={alignment}
placeholder={formatMessage(labels.selectDate)}
onChange={handleChange}
onChange={key => handleChange(key as any)}
>
{({ label, value, divider }) => (
<Item key={value} divider={divider}>

View file

@ -9,7 +9,7 @@ export function LanguageButton() {
const { locale, saveLocale, dir } = useLocale();
const items = Object.keys(languages).map(key => ({ ...languages[key], value: key }));
function handleSelect(value, close, e) {
function handleSelect(value: string, close: () => void, e: MouseEvent) {
e.stopPropagation();
saveLocale(value);
close();
@ -23,7 +23,7 @@ export function LanguageButton() {
</Icon>
</Button>
<Popup position="bottom" alignment={dir === 'rtl' ? 'start' : 'end'}>
{close => {
{(close: () => void) => {
return (
<div className={styles.menu}>
{items.map(({ value, label }) => {
@ -31,7 +31,7 @@ export function LanguageButton() {
<div
key={value}
className={classNames(styles.item, { [styles.selected]: value === locale })}
onClick={handleSelect.bind(null, value, close)}
onClick={(e: any) => handleSelect(value, close, e)}
>
<Text>{label}</Text>
{value === locale && (

View file

@ -2,7 +2,11 @@ import { Button, Icon, Icons, TooltipPopup } from 'react-basics';
import Link from 'next/link';
import useMessages from 'components/hooks/useMessages';
export function LogoutButton({ tooltipPosition = 'top' }) {
export function LogoutButton({
tooltipPosition = 'top',
}: {
tooltipPosition?: 'top' | 'bottom' | 'left' | 'right';
}) {
const { formatMessage, labels } = useMessages();
return (
<Link href="/src/app/logout/logout">

View file

@ -20,7 +20,7 @@ export function MonthSelect({ date = new Date(), onChange }) {
const year = date.getFullYear();
const ref = useRef();
const handleChange = (close, date) => {
const handleChange = (close: () => void, date: Date) => {
onChange(`range:${startOfMonth(date).getTime()}:${endOfMonth(date).getTime()}`);
close();
};
@ -53,12 +53,8 @@ export function MonthSelect({ date = new Date(), onChange }) {
</Icon>
</Button>
<Popup className={styles.popup} alignment="start">
{close => (
<CalendarYearSelect
date={date}
locale={dateLocale}
onSelect={handleChange.bind(null, close)}
/>
{(close: any) => (
<CalendarYearSelect date={date} onSelect={handleChange.bind(null, close)} />
)}
</Popup>
</PopupTrigger>

View file

@ -4,7 +4,13 @@ import useDateRange from 'components/hooks/useDateRange';
import Icons from 'components/icons';
import useMessages from 'components/hooks/useMessages';
export function RefreshButton({ websiteId, isLoading }) {
export function RefreshButton({
websiteId,
isLoading,
}: {
websiteId: string;
isLoading?: boolean;
}) {
const { formatMessage, labels } = useMessages();
const [dateRange] = useDateRange(websiteId);

View file

@ -5,7 +5,7 @@ import { Button, Icon, Icons } from 'react-basics';
import DateFilter from './DateFilter';
import styles from './WebsiteDateFilter.module.css';
export function WebsiteDateFilter({ websiteId }) {
export function WebsiteDateFilter({ websiteId }: { websiteId: string }) {
const [dateRange, setDateRange] = useDateRange(websiteId);
const { value, startDate, endDate, selectedUnit } = dateRange;
const isFutureDate =

View file

@ -3,7 +3,13 @@ import useApi from 'components/hooks/useApi';
import useMessages from 'components/hooks/useMessages';
import styles from './WebsiteSelect.module.css';
export function WebsiteSelect({ websiteId, onSelect }) {
export function WebsiteSelect({
websiteId,
onSelect,
}: {
websiteId: string;
onSelect?: (key: any) => void;
}) {
const { formatMessage, labels } = useMessages();
const { get, useQuery } = useApi();
const { data } = useQuery({

View file

@ -1,18 +0,0 @@
import classNames from 'classnames';
import { mapChildren } from 'react-basics';
import styles from './Grid.module.css';
export function Grid({ className, ...otherProps }) {
return <div {...otherProps} className={classNames(styles.grid, className)} />;
}
export function GridRow(props) {
const { columns = 'two', className, children, ...otherProps } = props;
return (
<div {...otherProps} className={classNames(styles.row, className)}>
{mapChildren(children, child => {
return <div className={classNames(styles.col, { [styles[columns]]: true })}>{child}</div>;
})}
</div>
);
}

View file

@ -0,0 +1,34 @@
import { CSSProperties } from 'react';
import classNames from 'classnames';
import { mapChildren } from 'react-basics';
import styles from './Grid.module.css';
export interface GridProps {
className?: string;
style?: CSSProperties;
children?: any;
}
export function Grid({ className, style, children }: GridProps) {
return (
<div className={classNames(styles.grid, className)} style={style}>
{children}
</div>
);
}
export function GridRow(props: {
[x: string]: any;
columns?: 'one' | 'two' | 'three' | 'one-two' | 'two-one';
className?: string;
children?: any;
}) {
const { columns = 'two', className, children, ...otherProps } = props;
return (
<div {...otherProps} className={classNames(styles.row, className)}>
{mapChildren(children, child => {
return <div className={classNames(styles.col, { [styles[columns]]: true })}>{child}</div>;
})}
</div>
);
}

View file

@ -6,13 +6,21 @@ import Link from 'next/link';
import Icons from 'components/icons';
import styles from './NavGroup.module.css';
export interface NavGroupProps {
title: string;
items: any[];
defaultExpanded?: boolean;
allowExpand?: boolean;
minimized?: boolean;
}
export function NavGroup({
title,
items,
defaultExpanded = true,
allowExpand = true,
minimized = false,
}) {
}: NavGroupProps) {
const pathname = usePathname();
const [expanded, setExpanded] = useState(defaultExpanded);

View file

@ -4,6 +4,15 @@ import { usePathname } from 'next/navigation';
import Link from 'next/link';
import styles from './SideNav.module.css';
export interface SideNavProps {
selectedKey: string;
items: any[];
shallow?: boolean;
scroll?: boolean;
className?: boolean;
onSelect?: () => void;
}
export function SideNav({
selectedKey,
items,
@ -11,7 +20,7 @@ export function SideNav({
scroll = false,
className,
onSelect = () => {},
}) {
}: SideNavProps) {
const pathname = usePathname();
return (
<Menu

View file

@ -4,14 +4,22 @@ import useApi from 'components/hooks/useApi';
import useMessages from 'components/hooks/useMessages';
import styles from './ActiveUsers.module.css';
export function ActiveUsers({ websiteId, value, refetchInterval = 60000 }) {
export function ActiveUsers({
websiteId,
value,
refetchInterval = 60000,
}: {
websiteId: string;
value?: number;
refetchInterval?: number;
}) {
const { formatMessage, messages } = useMessages();
const { get, useQuery } = useApi();
const { data } = useQuery({
queryKey: ['websites:active', websiteId],
queryFn: () => get(`/websites/${websiteId}/active`),
refetchInterval,
enabled: !!websiteId,
refetchInterval,
});
const count = useMemo(() => {

View file

@ -10,12 +10,28 @@ import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import { renderNumberLabels } from 'lib/charts';
import styles from './BarChart.module.css';
export interface BarChartProps {
datasets?: any[];
unit?: string;
animationDuration?: number;
stacked?: boolean;
isLoading?: boolean;
renderXLabel?: (label: string, index: number, values: any[]) => string;
renderYLabel?: (label: string, index: number, values: any[]) => string;
XAxisType?: string;
YAxisType?: string;
renderTooltipPopup?: (setTooltipPopup: (data: any) => void, model: any) => void;
onCreate?: (chart: any) => void;
onUpdate?: (chart: any) => void;
className?: string;
}
export function BarChart({
datasets,
datasets = [],
unit,
animationDuration = DEFAULT_ANIMATION_DURATION,
stacked = false,
loading = false,
isLoading = false,
renderXLabel,
renderYLabel,
XAxisType = 'time',
@ -24,7 +40,7 @@ export function BarChart({
onCreate,
onUpdate,
className,
}) {
}: BarChartProps) {
const canvas = useRef();
const chart = useRef(null);
const [tooltip, setTooltipPopup] = useState(null);
@ -85,7 +101,7 @@ export function BarChart({
color: colors.chart.line,
},
ticks: {
color: colors.text,
color: colors.chart.text,
callback: renderYLabel || renderNumberLabels,
},
},
@ -106,16 +122,15 @@ export function BarChart({
const createChart = () => {
Chart.defaults.font.family = 'Inter';
const options = getOptions();
chart.current = new Chart(canvas.current, {
type: 'bar',
data: {
datasets,
},
options,
});
chart.current.options = getOptions();
onCreate?.(chart.current);
};
@ -147,7 +162,7 @@ export function BarChart({
return (
<div className={styles.container}>
<div className={classNames(styles.chart, className)}>
{loading && <Loading position="page" icon="dots" />}
{isLoading && <Loading position="page" icon="dots" />}
<canvas ref={canvas} />
</div>
<Legend chart={chart.current} />

View file

@ -1,9 +1,9 @@
import FilterLink from 'components/common/FilterLink';
import MetricsTable from 'components/metrics/MetricsTable';
import MetricsTable, { MetricsTableProps } from 'components/metrics/MetricsTable';
import useMessages from 'components/hooks/useMessages';
import useFormat from 'components/hooks/useFormat';
export function BrowsersTable({ websiteId, ...props }) {
export function BrowsersTable(props: MetricsTableProps) {
const { formatMessage, labels } = useMessages();
const { formatBrowser } = useFormat();
@ -26,7 +26,6 @@ export function BrowsersTable({ websiteId, ...props }) {
title={formatMessage(labels.browsers)}
type="browser"
metric={formatMessage(labels.visitors)}
websiteId={websiteId}
renderLabel={renderLink}
/>
);

View file

@ -1,16 +1,16 @@
import MetricsTable from './MetricsTable';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import { emptyFilter } from 'lib/filters';
import FilterLink from 'components/common/FilterLink';
import useLocale from 'components/hooks/useLocale';
import useMessages from 'components/hooks/useMessages';
import useCountryNames from 'components/hooks/useCountryNames';
export function CitiesTable({ websiteId, ...props }) {
export function CitiesTable(props: MetricsTableProps) {
const { locale } = useLocale();
const { formatMessage, labels } = useMessages();
const countryNames = useCountryNames(locale);
const renderLabel = (city, country) => {
const renderLabel = (city: string, country: string) => {
const name = countryNames[country];
return name ? `${city}, ${name}` : city;
};
@ -34,7 +34,6 @@ export function CitiesTable({ websiteId, ...props }) {
title={formatMessage(labels.cities)}
type="city"
metric={formatMessage(labels.visitors)}
websiteId={websiteId}
dataFilter={emptyFilter}
renderLabel={renderLink}
/>

View file

@ -1,15 +1,24 @@
import FilterLink from 'components/common/FilterLink';
import useCountryNames from 'components/hooks/useCountryNames';
import { useLocale, useMessages, useFormat } from 'components/hooks';
import MetricsTable from './MetricsTable';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
export function CountriesTable({ websiteId, ...props }) {
export function CountriesTable({
onDataLoad,
...props
}: {
onDataLoad: (data: any) => void;
} & MetricsTableProps) {
const { locale } = useLocale();
const countryNames = useCountryNames(locale);
const { formatMessage, labels } = useMessages();
const { formatCountry } = useFormat();
function renderLink({ x: code }) {
const handleDataLoad = (data: any) => {
onDataLoad?.(data);
};
const renderLink = ({ x: code }) => {
return (
<FilterLink
id="country"
@ -23,7 +32,7 @@ export function CountriesTable({ websiteId, ...props }) {
/>
</FilterLink>
);
}
};
return (
<MetricsTable
@ -31,8 +40,8 @@ export function CountriesTable({ websiteId, ...props }) {
title={formatMessage(labels.countries)}
type="country"
metric={formatMessage(labels.visitors)}
websiteId={websiteId}
renderLabel={renderLink}
onDataLoad={handleDataLoad}
/>
);
}

View file

@ -39,7 +39,7 @@ export function DatePickerForm({
return (
<div className={styles.container}>
<div className={styles.filter}>
<ButtonGroup selectedKey={selected} onSelect={setSelected}>
<ButtonGroup selectedKey={selected} onSelect={key => setSelected(key as any)}>
<Button key={FILTER_DAY}>{formatMessage(labels.singleDay)}</Button>
<Button key={FILTER_RANGE}>{formatMessage(labels.dateRange)}</Button>
</ButtonGroup>

View file

@ -1,9 +1,9 @@
import MetricsTable from './MetricsTable';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import FilterLink from 'components/common/FilterLink';
import useMessages from 'components/hooks/useMessages';
import { useFormat } from 'components/hooks';
export function DevicesTable({ websiteId, ...props }) {
export function DevicesTable(props: MetricsTableProps) {
const { formatMessage, labels } = useMessages();
const { formatDevice } = useFormat();
@ -26,7 +26,6 @@ export function DevicesTable({ websiteId, ...props }) {
title={formatMessage(labels.devices)}
type="device"
metric={formatMessage(labels.visitors)}
websiteId={websiteId}
renderLabel={renderLink}
/>
);

View file

@ -7,7 +7,13 @@ import { useApi, useLocale, useDateRange, useTimezone, useNavigation } from 'com
import { EVENT_COLORS } from 'lib/constants';
import { renderDateLabels, renderStatusTooltipPopup } from 'lib/charts';
export function EventsChart({ websiteId, className, token }) {
export interface EventsChartProps {
websiteId: string;
className?: string;
token?: string;
}
export function EventsChart({ websiteId, className, token }: EventsChartProps) {
const { get, useQuery } = useApi();
const [{ startDate, endDate, unit, modified }] = useDateRange(websiteId);
const { locale } = useLocale();
@ -71,7 +77,6 @@ export function EventsChart({ websiteId, className, token }) {
className={className}
datasets={datasets}
unit={unit}
height={300}
loading={isLoading}
stacked
renderXLabel={renderDateLabels(unit, locale)}

View file

@ -1,10 +1,10 @@
import MetricsTable from './MetricsTable';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import useMessages from 'components/hooks/useMessages';
export function EventsTable({ websiteId, ...props }) {
export function EventsTable(props: MetricsTableProps) {
const { formatMessage, labels } = useMessages();
function handleDataLoad(data) {
function handleDataLoad(data: any) {
props.onDataLoad?.(data);
}
@ -14,7 +14,6 @@ export function EventsTable({ websiteId, ...props }) {
title={formatMessage(labels.events)}
type="event"
metric={formatMessage(labels.actions)}
websiteId={websiteId}
onDataLoad={handleDataLoad}
/>
);

View file

@ -18,7 +18,7 @@ export function FilterTags({ params }) {
return null;
}
function handleCloseFilter(param) {
function handleCloseFilter(param?: string) {
if (!param) {
router.push(makeUrl({ view }, true));
} else {
@ -44,7 +44,7 @@ export function FilterTags({ params }) {
</div>
);
})}
<Button size="sm" variant="quiet" onClick={() => handleCloseFilter()}>
<Button size="sm" variant="quiet" onClick={handleCloseFilter}>
<Icon>
<Icons.Close />
</Icon>

View file

@ -1,10 +1,13 @@
import MetricsTable from './MetricsTable';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import { percentFilter } from 'lib/filters';
import useLanguageNames from 'components/hooks/useLanguageNames';
import useLocale from 'components/hooks/useLocale';
import useMessages from 'components/hooks/useMessages';
export function LanguagesTable({ websiteId, onDataLoad, ...props }) {
export function LanguagesTable({
onDataLoad,
...props
}: { onDataLoad: (data: any) => void } & MetricsTableProps) {
const { formatMessage, labels } = useMessages();
const { locale } = useLocale();
const languageNames = useLanguageNames(locale);
@ -19,7 +22,6 @@ export function LanguagesTable({ websiteId, onDataLoad, ...props }) {
title={formatMessage(labels.languages)}
type="language"
metric={formatMessage(labels.visitors)}
websiteId={websiteId}
onDataLoad={data => onDataLoad?.(percentFilter(data))}
renderLabel={renderLabel}
/>

View file

@ -5,9 +5,22 @@ import Empty from 'components/common/Empty';
import { formatLongNumber } from 'lib/format';
import useMessages from 'components/hooks/useMessages';
import styles from './ListTable.module.css';
import { ReactNode } from 'react';
const ITEM_SIZE = 30;
export interface ListTableProps {
data?: any[];
title?: string;
metric?: string;
className?: string;
renderLabel?: (row: any) => ReactNode;
animate?: boolean;
virtualize?: boolean;
showPercentage?: boolean;
itemCount?: number;
}
export function ListTable({
data = [],
title,
@ -18,7 +31,7 @@ export function ListTable({
virtualize = false,
showPercentage = true,
itemCount = 10,
}) {
}: ListTableProps) {
const { formatMessage, labels } = useMessages();
const getRow = row => {
@ -76,7 +89,7 @@ const AnimatedRow = ({ label, value = 0, percent, animate, showPercentage = true
<div className={styles.row}>
<div className={styles.label}>{label}</div>
<div className={styles.value}>
<animated.div className={styles.value} title={props?.y}>
<animated.div className={styles.value} title={props?.y as any}>
{props.y?.to(formatLongNumber)}
</animated.div>
</div>

View file

@ -3,6 +3,16 @@ import { useSpring, animated } from '@react-spring/web';
import { formatNumber } from 'lib/format';
import styles from './MetricCard.module.css';
export interface MetricCardProps {
value: number;
change?: number;
label: string;
reverseColors?: boolean;
format?: typeof formatNumber;
hideComparison?: boolean;
className?: string;
}
export const MetricCard = ({
value = 0,
change = 0,
@ -11,13 +21,13 @@ export const MetricCard = ({
format = formatNumber,
hideComparison = false,
className,
}) => {
}: MetricCardProps) => {
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
const changeProps = useSpring({ x: Number(change) || 0, from: { x: 0 } });
return (
<div className={classNames(styles.card, className)}>
<animated.div className={styles.value} title={props?.x}>
<animated.div className={styles.value} title={props?.x as any}>
{props?.x?.to(x => format(x))}
</animated.div>
<div className={styles.label}>
@ -29,7 +39,7 @@ export const MetricCard = ({
[styles.negative]: change * (reverseColors ? -1 : 1) < 0,
[styles.plusSign]: change > 0,
})}
title={changeProps?.x}
title={changeProps?.x as any}
>
{changeProps?.x?.to(x => format(x))}
</animated.span>

View file

@ -1,9 +1,17 @@
import { ReactNode } from 'react';
import { Loading, cloneChildren } from 'react-basics';
import ErrorMessage from 'components/common/ErrorMessage';
import { formatLongNumber } from 'lib/format';
import styles from './MetricsBar.module.css';
export function MetricsBar({ children, isLoading, isFetched, error }) {
export interface MetricsBarProps {
isLoading?: boolean;
isFetched?: boolean;
error?: unknown;
children?: ReactNode;
}
export function MetricsBar({ children, isLoading, isFetched, error }: MetricsBarProps) {
const formatFunc = n => (n >= 0 ? formatLongNumber(n) : `-${formatLongNumber(Math.abs(n))}`);
return (

View file

@ -1,6 +1,5 @@
import { useMemo } from 'react';
import { Loading, Icon, Text } from 'react-basics';
import firstBy from 'thenby';
import classNames from 'classnames';
import useApi from 'components/hooks/useApi';
import { percentFilter } from 'lib/filters';
@ -8,24 +7,33 @@ import useDateRange from 'components/hooks/useDateRange';
import useNavigation from 'components/hooks/useNavigation';
import ErrorMessage from 'components/common/ErrorMessage';
import LinkButton from 'components/common/LinkButton';
import ListTable from './ListTable';
import ListTable, { ListTableProps } from './ListTable';
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import Icons from 'components/icons';
import useMessages from 'components/hooks/useMessages';
import useLocale from 'components/hooks/useLocale';
import styles from './MetricsTable.module.css';
export interface MetricsTableProps extends ListTableProps {
websiteId: string;
type?: string;
className?: string;
dataFilter?: (data: any) => any;
limit?: number;
delay?: number;
onDataLoad?: (data: any) => void;
}
export function MetricsTable({
websiteId,
type,
className,
dataFilter,
filterOptions,
limit,
onDataLoad,
delay = null,
...props
}) {
}: MetricsTableProps) {
const [{ startDate, endDate, modified }] = useDateRange(websiteId);
const {
makeUrl,
@ -53,26 +61,28 @@ export function MetricsTable({
city,
},
],
queryFn: () => {
queryFn: async () => {
const filters = { url, title, referrer, os, browser, device, country, region, city };
filters[type] = undefined;
onDataLoad?.();
return get(`/websites/${websiteId}/metrics`, {
const data = await get(`/websites/${websiteId}/metrics`, {
type,
startAt: +startDate,
endAt: +endDate,
...filters,
});
onDataLoad?.(data);
return data;
},
retryDelay: delay || DEFAULT_ANIMATION_DURATION,
});
const filteredData = useMemo(() => {
if (data) {
let items = data;
let items: any[] = data;
if (dataFilter) {
if (Array.isArray(dataFilter)) {
@ -89,20 +99,19 @@ export function MetricsTable({
if (limit) {
items = items.filter((e, i) => i < limit);
}
if (filterOptions?.sort === false) {
return items;
}
return items.sort(firstBy('y', -1).thenBy('x'));
return items;
}
return [];
}, [data, error, dataFilter, filterOptions, limit]);
}, [data, error, dataFilter, limit]);
return (
<div className={classNames(styles.container, className)}>
{!data && isLoading && !isFetched && <Loading icon="dots" />}
{error && <ErrorMessage />}
{data && !error && <ListTable {...props} data={filteredData} className={className} />}
{data && !error && (
<ListTable {...(props as ListTableProps)} data={filteredData} className={className} />
)}
<div className={styles.footer}>
{data && !error && limit && (
<LinkButton href={makeUrl({ view: type })} variant="quiet">

View file

@ -2,7 +2,7 @@ import MetricsTable from './MetricsTable';
import FilterLink from 'components/common/FilterLink';
import useMessages from 'components/hooks/useMessages';
export function OSTable({ websiteId, ...props }) {
export function OSTable({ websiteId, limit }: { websiteId: string; limit?: number }) {
const { formatMessage, labels } = useMessages();
function renderLink({ x: os }) {
@ -10,7 +10,7 @@ export function OSTable({ websiteId, ...props }) {
<FilterLink id="os" value={os}>
<img
src={`${process.env.basePath}/images/os/${
os?.toLowerCase().replaceAll(/[^\w]+/g, '-') || 'unknown'
os?.toLowerCase().replaceAll(/\W/g, '-') || 'unknown'
}.png`}
alt={os}
width={16}
@ -22,8 +22,8 @@ export function OSTable({ websiteId, ...props }) {
return (
<MetricsTable
{...props}
websiteId={websiteId}
limit={limit}
title={formatMessage(labels.os)}
metric={formatMessage(labels.visitors)}
renderLabel={renderLink}

View file

@ -1,11 +1,15 @@
import FilterLink from 'components/common/FilterLink';
import FilterButtons from 'components/common/FilterButtons';
import MetricsTable from './MetricsTable';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import useMessages from 'components/hooks/useMessages';
import useNavigation from 'components/hooks/useNavigation';
import { emptyFilter } from 'lib/filters';
export function PagesTable({ websiteId, showFilters, ...props }) {
export interface PagesTableProps extends MetricsTableProps {
showFilters?: boolean;
}
export function PagesTable({ showFilters, ...props }: PagesTableProps) {
const {
router,
makeUrl,
@ -40,7 +44,6 @@ export function PagesTable({ websiteId, showFilters, ...props }) {
title={formatMessage(labels.pages)}
type={view}
metric={formatMessage(labels.views)}
websiteId={websiteId}
dataFilter={emptyFilter}
renderLabel={renderLink}
/>

View file

@ -1,9 +1,18 @@
import { useMemo } from 'react';
import BarChart from './BarChart';
import BarChart, { BarChartProps } from './BarChart';
import { useLocale, useTheme, useMessages } from 'components/hooks';
import { renderDateLabels, renderStatusTooltipPopup } from 'lib/charts';
export function PageviewsChart({ websiteId, data, unit, loading, ...props }) {
export interface PageviewsChartProps extends BarChartProps {
data: {
sessions: any[];
pageviews: any[];
};
unit: string;
isLoading?: boolean;
}
export function PageviewsChart({ data, unit, isLoading, ...props }: PageviewsChartProps) {
const { formatMessage, labels } = useMessages();
const { colors } = useTheme();
const { locale } = useLocale();
@ -30,10 +39,9 @@ export function PageviewsChart({ websiteId, data, unit, loading, ...props }) {
return (
<BarChart
{...props}
key={websiteId}
datasets={datasets}
unit={unit}
loading={loading}
isLoading={isLoading}
renderXLabel={renderDateLabels(unit, locale)}
renderTooltipPopup={renderStatusTooltipPopup(unit, locale)}
/>

View file

@ -3,7 +3,7 @@ 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 MetricsTable from './MetricsTable';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import useMessages from 'components/hooks/useMessages';
import styles from './QueryParametersTable.module.css';
@ -12,7 +12,10 @@ const filters = {
[FILTER_COMBINED]: [emptyFilter, paramFilter],
};
export function QueryParametersTable({ websiteId, showFilters, ...props }) {
export function QueryParametersTable({
showFilters,
...props
}: { showFilters: boolean } & MetricsTableProps) {
const [filter, setFilter] = useState(FILTER_COMBINED);
const { formatMessage, labels } = useMessages();
@ -32,7 +35,6 @@ export function QueryParametersTable({ websiteId, showFilters, ...props }) {
title={formatMessage(labels.query)}
type="query"
metric={formatMessage(labels.views)}
websiteId={websiteId}
dataFilter={filters[filter]}
renderLabel={({ x, p, v }) =>
filter === FILTER_RAW ? (

View file

@ -4,7 +4,7 @@ import PageviewsChart from './PageviewsChart';
import { getDateArray } from 'lib/date';
import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from 'lib/constants';
function mapData(data) {
function mapData(data: any[]) {
let last = 0;
const arr = [];
@ -23,7 +23,15 @@ function mapData(data) {
return arr;
}
export function RealtimeChart({ data, unit, ...props }) {
export interface RealtimeChartProps {
data: {
pageviews: any[];
visitors: any[];
};
unit: string;
}
export function RealtimeChart({ data, unit, ...props }: RealtimeChartProps) {
const endDate = startOfMinute(new Date());
const startDate = subMinutes(endDate, REALTIME_RANGE);
const prevEndDate = useRef(endDate);
@ -49,13 +57,7 @@ export function RealtimeChart({ data, unit, ...props }) {
}, [endDate]);
return (
<PageviewsChart
{...props}
height={200}
unit={unit}
data={chartData}
animationDuration={animationDuration}
/>
<PageviewsChart {...props} unit={unit} data={chartData} animationDuration={animationDuration} />
);
}

View file

@ -1,8 +1,8 @@
import MetricsTable from './MetricsTable';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import FilterLink from 'components/common/FilterLink';
import useMessages from 'components/hooks/useMessages';
export function ReferrersTable({ websiteId, ...props }) {
export function ReferrersTable(props: MetricsTableProps) {
const { formatMessage, labels } = useMessages();
const renderLink = ({ x: referrer }) => {
@ -23,7 +23,6 @@ export function ReferrersTable({ websiteId, ...props }) {
title={formatMessage(labels.referrers)}
type="referrer"
metric={formatMessage(labels.views)}
websiteId={websiteId}
renderLabel={renderLink}
/>
</>

View file

@ -3,15 +3,15 @@ import { emptyFilter } from 'lib/filters';
import useLocale from 'components/hooks/useLocale';
import useMessages from 'components/hooks/useMessages';
import useCountryNames from 'components/hooks/useCountryNames';
import MetricsTable from './MetricsTable';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import regions from 'public/iso-3166-2.json';
export function RegionsTable({ websiteId, ...props }) {
export function RegionsTable(props: MetricsTableProps) {
const { locale } = useLocale();
const { formatMessage, labels } = useMessages();
const countryNames = useCountryNames(locale);
const renderLabel = (code, country) => {
const renderLabel = (code: string, country: string) => {
const region = code.includes('-') ? code : `${country}-${code}`;
return regions[region] ? `${regions[region]}, ${countryNames[country]}` : region;
};
@ -33,7 +33,6 @@ export function RegionsTable({ websiteId, ...props }) {
title={formatMessage(labels.regions)}
type="region"
metric={formatMessage(labels.visitors)}
websiteId={websiteId}
dataFilter={emptyFilter}
renderLabel={renderLink}
/>

View file

@ -1,7 +1,7 @@
import MetricsTable from './MetricsTable';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import useMessages from 'components/hooks/useMessages';
export function ScreenTable({ websiteId, ...props }) {
export function ScreenTable(props: MetricsTableProps) {
const { formatMessage, labels } = useMessages();
return (
@ -10,7 +10,6 @@ export function ScreenTable({ websiteId, ...props }) {
title={formatMessage(labels.screens)}
type="screen"
metric={formatMessage(labels.visitors)}
websiteId={websiteId}
/>
);
}

View file

@ -12,7 +12,7 @@ import { formatLongNumber } from 'lib/format';
import { percentFilter } from 'lib/filters';
import styles from './WorldMap.module.css';
export function WorldMap({ data, className }) {
export function WorldMap({ data = [], className }: { data?: any[]; className?: string }) {
const [tooltip, setTooltipPopup] = useState();
const { theme, colors } = useTheme();
const { locale } = useLocale();
@ -21,7 +21,7 @@ export function WorldMap({ data, className }) {
const visitorsLabel = formatMessage(labels.visitors).toLocaleLowerCase(locale);
const metrics = useMemo(() => (data ? percentFilter(data) : []), [data]);
function getFillColor(code) {
function getFillColor(code: string) {
if (code === 'AQ') return;
const country = metrics?.find(({ x }) => x === code);
@ -41,7 +41,9 @@ export function WorldMap({ data, className }) {
function handleHover(code) {
if (code === 'AQ') return;
const country = metrics?.find(({ x }) => x === code);
setTooltipPopup(`${countryNames[code]}: ${formatLongNumber(country?.y || 0)} ${visitorsLabel}`);
setTooltipPopup(
`${countryNames[code]}: ${formatLongNumber(country?.y || 0)} ${visitorsLabel}` as any,
);
}
return (