Merge dev.

This commit is contained in:
Mike Cao 2025-04-28 20:09:58 -07:00
commit be1b2fc272
88 changed files with 4120 additions and 21010 deletions

View file

@ -81,7 +81,7 @@ export function Chart({
dataset.data = data?.datasets[index]?.data;
if (chart.current.legend.legendItems[index]) {
chart.current.legend.legendItems[index].text = data?.datasets[index]?.label;
chart.current.legend.legendItems[index].text = data.datasets[index]?.label;
}
}
});
@ -90,6 +90,12 @@ export function Chart({
}
}
if (data.focusLabel !== null) {
chart.current.data.datasets.forEach(ds => {
ds.hidden = data.focusLabel ? ds.label !== data.focusLabel : false;
});
}
chart.current.options = options;
// Allow config changes before update
@ -100,16 +106,6 @@ export function Chart({
setLegendItems(chart.current.legend.legendItems);
};
useEffect(() => {
if (data) {
if (!chart.current) {
createChart(data);
} else {
updateChart(data);
}
}
}, [data, options]);
const handleLegendClick = (item: LegendItem) => {
if (type === 'bar') {
const { datasetIndex } = item;
@ -131,6 +127,16 @@ export function Chart({
setLegendItems(chart.current.legend.legendItems);
};
useEffect(() => {
if (data) {
if (!chart.current) {
createChart(data);
} else {
updateChart(data);
}
}
}, [data, options]);
return (
<>
<div {...props}>

View file

@ -1,4 +1,4 @@
import { GROUPED_DOMAINS } from '@/lib/constants';
import { FAVICON_URL, GROUPED_DOMAINS } from '@/lib/constants';
function getHostName(url: string) {
const match = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?([^:/\n?=]+)/im);
@ -10,10 +10,10 @@ export function Favicon({ domain, ...props }) {
return null;
}
const url = process.env.faviconURL || FAVICON_URL;
const hostName = domain ? getHostName(domain) : null;
const src = hostName
? `https://icons.duckduckgo.com/ip3/${GROUPED_DOMAINS[hostName]?.domain || hostName}.ico`
: null;
const domainName = GROUPED_DOMAINS[hostName]?.domain || hostName;
const src = hostName ? url.replace(/\{\{\s*domain\s*}}/, domainName) : null;
return hostName ? <img src={src} width={16} height={16} alt="" {...props} /> : null;
}

View file

@ -27,8 +27,8 @@ export function useFormat() {
return countryNames[value] || value;
};
const formatRegion = (value: string): string => {
const [country] = value.split('-');
const formatRegion = (value?: string): string => {
const [country] = value?.split('-') || [];
return regions[value] ? `${regions[value]}, ${countryNames[country]}` : value;
};

View file

@ -164,7 +164,13 @@ export const labels = defineMessages({
id: 'label.revenue-description',
defaultMessage: 'Look into your revenue data and how users are spending.',
},
attribution: { id: 'label.attribution', defaultMessage: 'Attribution' },
attributionDescription: {
id: 'label.attribution-description',
defaultMessage: 'See how users engage with your marketing and what drives conversions.',
},
currency: { id: 'label.currency', defaultMessage: 'Currency' },
model: { id: 'label.model', defaultMessage: 'Model' },
url: { id: 'label.url', defaultMessage: 'URL' },
urls: { id: 'label.urls', defaultMessage: 'URLs' },
path: { id: 'label.path', defaultMessage: 'Path' },
@ -258,6 +264,7 @@ export const labels = defineMessages({
id: 'label.utm-description',
defaultMessage: 'Track your campaigns through UTM parameters.',
},
conversionStep: { id: 'label.conversion-step', defaultMessage: 'Conversion Step' },
steps: { id: 'label.steps', defaultMessage: 'Steps' },
startStep: { id: 'label.start-step', defaultMessage: 'Start Step' },
endStep: { id: 'label.end-step', defaultMessage: 'End Step' },
@ -283,6 +290,11 @@ export const labels = defineMessages({
firstSeen: { id: 'label.first-seen', defaultMessage: 'First seen' },
properties: { id: 'label.properties', defaultMessage: 'Properties' },
channels: { id: 'label.channels', defaultMessage: 'Channels' },
sources: { id: 'label.sources', defaultMessage: 'Sources' },
medium: { id: 'label.medium', defaultMessage: 'Medium' },
campaigns: { id: 'label.campaigns', defaultMessage: 'Campaigns' },
content: { id: 'label.content', defaultMessage: 'Content' },
terms: { id: 'label.terms', defaultMessage: 'Terms' },
direct: { id: 'label.direct', defaultMessage: 'Direct' },
referral: { id: 'label.referral', defaultMessage: 'Referral' },
affiliate: { id: 'label.affiliate', defaultMessage: 'Affiliate' },

View file

@ -1,21 +1,23 @@
import { useMemo, useState, useEffect } from 'react';
import { colord } from 'colord';
import { BarChart } from '@/components/charts/BarChart';
import { useDateRange, useLocale, useWebsiteEventsSeriesQuery } from '@/components/hooks';
import { renderDateLabels } from '@/lib/charts';
import { CHART_COLORS } from '@/lib/constants';
import { useMemo } from 'react';
export interface EventsChartProps {
websiteId: string;
className?: string;
focusLabel?: string;
}
export function EventsChart({ websiteId, className }: EventsChartProps) {
export function EventsChart({ websiteId, className, focusLabel }: EventsChartProps) {
const {
dateRange: { startDate, endDate, unit, value },
} = useDateRange(websiteId);
const { locale } = useLocale();
const { data, isLoading } = useWebsiteEventsSeriesQuery(websiteId);
const [label, setLabel] = useState<string>(focusLabel);
const chartData = useMemo(() => {
if (!data) return [];
@ -42,8 +44,15 @@ export function EventsChart({ websiteId, className }: EventsChartProps) {
borderWidth: 1,
};
}),
focusLabel,
};
}, [data, startDate, endDate, unit]);
}, [data, startDate, endDate, unit, focusLabel]);
useEffect(() => {
if (label !== focusLabel) {
setLabel(focusLabel);
}
}, [focusLabel]);
return (
<BarChart

View file

@ -1,12 +1,28 @@
import { MetricsTable, MetricsTableProps } from './MetricsTable';
import { useMessages } from '@/components/hooks';
export function EventsTable(props: MetricsTableProps) {
export interface EventsTableProps extends MetricsTableProps {
onLabelClick?: (value: string) => void;
}
export function EventsTable({ onLabelClick, ...props }: EventsTableProps) {
const { formatMessage, labels } = useMessages();
function handleDataLoad(data: any) {
const handleDataLoad = (data: any) => {
props.onDataLoad?.(data);
}
};
const renderLabel = ({ x: label }) => {
if (onLabelClick) {
return (
<div onClick={() => onLabelClick(label)} style={{ cursor: 'pointer' }}>
{label}
</div>
);
}
return label;
};
return (
<MetricsTable
@ -15,6 +31,7 @@ export function EventsTable(props: MetricsTableProps) {
type="event"
metric={formatMessage(labels.actions)}
onDataLoad={handleDataLoad}
renderLabel={renderLabel}
/>
);
}

View file

@ -4,8 +4,8 @@ import { useSpring, config } from '@react-spring/web';
import classNames from 'classnames';
import { AnimatedDiv } from '@/components/common/AnimatedDiv';
import { Empty } from '@/components/common/Empty';
import { formatLongNumber } from '@/lib/format';
import { useMessages } from '@/components/hooks';
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
import styles from './ListTable.module.css';
const ITEM_SIZE = 30;
@ -21,6 +21,7 @@ export interface ListTableProps {
virtualize?: boolean;
showPercentage?: boolean;
itemCount?: number;
currency?: string;
}
export function ListTable({
@ -34,6 +35,7 @@ export function ListTable({
virtualize = false,
showPercentage = true,
itemCount = 10,
currency,
}: ListTableProps) {
const { formatMessage, labels } = useMessages();
@ -49,6 +51,7 @@ export function ListTable({
animate={animate && !virtualize}
showPercentage={showPercentage}
change={renderChange ? renderChange(row, index) : null}
currency={currency}
/>
);
};
@ -82,7 +85,15 @@ export function ListTable({
);
}
const AnimatedRow = ({ label, value = 0, percent, change, animate, showPercentage = true }) => {
const AnimatedRow = ({
label,
value = 0,
percent,
change,
animate,
showPercentage = true,
currency,
}) => {
const props = useSpring({
width: percent,
y: value,
@ -96,7 +107,9 @@ const AnimatedRow = ({ label, value = 0, percent, change, animate, showPercentag
<div className={styles.value}>
{change}
<AnimatedDiv className={styles.value} title={props?.y as any}>
{props.y?.to(formatLongNumber)}
{currency
? props.y?.to(n => formatLongCurrency(n, currency))
: props.y?.to(formatLongNumber)}
</AnimatedDiv>
</div>
{showPercentage && (

View file

@ -24,6 +24,7 @@ export { default as Logo } from './Logo';
export { default as Magnet } from './Magnet';
export { default as Money } from './Money';
export { default as Moon } from './Moon';
export { default as Network } from './Network';
export { default as Nodes } from './Nodes';
export { default as Overview } from './Overview';
export { default as Path } from './Path';