mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-24 18:26:20 +01:00
Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
4c06487b11
@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { Tooltip } from 'react-basics';
|
import { Tooltip } from 'react-basics';
|
||||||
import styles from './HoverTooltip.module.css';
|
import styles from './HoverTooltip.module.css';
|
||||||
|
|
||||||
export function HoverTooltip({ tooltip }) {
|
export function HoverTooltip({ children }) {
|
||||||
const [position, setPosition] = useState({ x: -1000, y: -1000 });
|
const [position, setPosition] = useState({ x: -1000, y: -1000 });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -18,8 +18,8 @@ export function HoverTooltip({ tooltip }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.tooltip} style={{ left: position.x, top: position.y }}>
|
<div className={styles.tooltip} style={{ left: position.x, top: position.y - 16 }}>
|
||||||
<Tooltip position="top" action="none" label={tooltip} />
|
<Tooltip position="top" action="none" label={children} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ import styles from './WorldMap.module.css';
|
|||||||
export function WorldMap({ data, className }) {
|
export function WorldMap({ data, className }) {
|
||||||
const { basePath } = useRouter();
|
const { basePath } = useRouter();
|
||||||
const [tooltip, setTooltip] = useState();
|
const [tooltip, setTooltip] = useState();
|
||||||
const [theme] = useTheme();
|
const { theme } = useTheme();
|
||||||
const colors = useMemo(
|
const colors = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
baseColor: THEME_COLORS[theme].primary,
|
baseColor: THEME_COLORS[theme].primary,
|
||||||
@ -86,7 +86,7 @@ export function WorldMap({ data, className }) {
|
|||||||
</Geographies>
|
</Geographies>
|
||||||
</ZoomableGroup>
|
</ZoomableGroup>
|
||||||
</ComposableMap>
|
</ComposableMap>
|
||||||
{tooltip && <HoverTooltip tooltip={tooltip} />}
|
{tooltip && <HoverTooltip>{tooltip}</HoverTooltip>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,9 @@ export function LanguageButton() {
|
|||||||
const { locale, saveLocale, dir } = useLocale();
|
const { locale, saveLocale, dir } = useLocale();
|
||||||
const items = Object.keys(languages).map(key => ({ ...languages[key], value: key }));
|
const items = Object.keys(languages).map(key => ({ ...languages[key], value: key }));
|
||||||
|
|
||||||
function handleSelect(value) {
|
function handleSelect(value, close) {
|
||||||
//saveLocale(value);
|
saveLocale(value);
|
||||||
console.log('WTFFFF');
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -22,24 +22,28 @@ export function LanguageButton() {
|
|||||||
</Icon>
|
</Icon>
|
||||||
</Button>
|
</Button>
|
||||||
<Popup position="bottom" alignment={dir === 'rtl' ? 'start' : 'end'}>
|
<Popup position="bottom" alignment={dir === 'rtl' ? 'start' : 'end'}>
|
||||||
<div className={styles.menu}>
|
{close => {
|
||||||
{items.map(({ value, label }) => {
|
return (
|
||||||
return (
|
<div className={styles.menu}>
|
||||||
<div
|
{items.map(({ value, label }) => {
|
||||||
key={value}
|
return (
|
||||||
className={classNames(styles.item, { [styles.selected]: value === locale })}
|
<div
|
||||||
onClick={handleSelect.bind(null, value)}
|
key={value}
|
||||||
>
|
className={classNames(styles.item, { [styles.selected]: value === locale })}
|
||||||
<Text>{label}</Text>
|
onClick={handleSelect.bind(null, value, close)}
|
||||||
{value === locale && (
|
>
|
||||||
<Icon className={styles.icon}>
|
<Text>{label}</Text>
|
||||||
<Icons.Check />
|
{value === locale && (
|
||||||
</Icon>
|
<Icon className={styles.icon}>
|
||||||
)}
|
<Icons.Check />
|
||||||
</div>
|
</Icon>
|
||||||
);
|
)}
|
||||||
})}
|
</div>
|
||||||
</div>
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
</Popup>
|
</Popup>
|
||||||
</PopupTrigger>
|
</PopupTrigger>
|
||||||
);
|
);
|
||||||
|
@ -5,7 +5,7 @@ import Icons from 'components/icons';
|
|||||||
import styles from './ThemeButton.module.css';
|
import styles from './ThemeButton.module.css';
|
||||||
|
|
||||||
export function ThemeButton() {
|
export function ThemeButton() {
|
||||||
const [theme, setTheme] = useTheme();
|
const { theme, saveTheme } = useTheme();
|
||||||
|
|
||||||
const transitions = useTransition(theme, {
|
const transitions = useTransition(theme, {
|
||||||
initial: { opacity: 1 },
|
initial: { opacity: 1 },
|
||||||
@ -21,7 +21,7 @@ export function ThemeButton() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
setTheme(theme === 'light' ? 'dark' : 'light');
|
saveTheme(theme === 'light' ? 'dark' : 'light');
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -123,7 +123,11 @@ export const labels = defineMessages({
|
|||||||
reports: { id: 'label.reports', defaultMessage: 'Reports' },
|
reports: { id: 'label.reports', defaultMessage: 'Reports' },
|
||||||
eventData: { id: 'label.event-data', defaultMessage: 'Event data' },
|
eventData: { id: 'label.event-data', defaultMessage: 'Event data' },
|
||||||
funnel: { id: 'label.funnel', defaultMessage: 'Funnel' },
|
funnel: { id: 'label.funnel', defaultMessage: 'Funnel' },
|
||||||
|
url: { id: 'label.url', defaultMessage: 'URL' },
|
||||||
urls: { id: 'label.urls', defaultMessage: 'URLs' },
|
urls: { id: 'label.urls', defaultMessage: 'URLs' },
|
||||||
|
add: { id: 'label.add', defaultMessage: 'Add' },
|
||||||
|
window: { id: 'label.window', defaultMessage: 'Window' },
|
||||||
|
addUrl: { id: 'label.add-url', defaultMessage: 'Add URL' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
import { StatusLight, Loading } from 'react-basics';
|
import { Loading } from 'react-basics';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Chart from 'chart.js/auto';
|
import Chart from 'chart.js/auto';
|
||||||
import HoverTooltip from 'components/common/HoverTooltip';
|
import HoverTooltip from 'components/common/HoverTooltip';
|
||||||
import Legend from 'components/metrics/Legend';
|
import Legend from 'components/metrics/Legend';
|
||||||
import { formatLongNumber } from 'lib/format';
|
|
||||||
import { dateFormat } from 'lib/date';
|
|
||||||
import useLocale from 'hooks/useLocale';
|
import useLocale from 'hooks/useLocale';
|
||||||
import useTheme from 'hooks/useTheme';
|
import useTheme from 'hooks/useTheme';
|
||||||
import { DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants';
|
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||||
|
import { renderNumberLabels } from 'lib/charts';
|
||||||
import styles from './BarChart.module.css';
|
import styles from './BarChart.module.css';
|
||||||
|
|
||||||
export function BarChart({
|
export function BarChart({
|
||||||
@ -17,86 +16,20 @@ export function BarChart({
|
|||||||
animationDuration = DEFAULT_ANIMATION_DURATION,
|
animationDuration = DEFAULT_ANIMATION_DURATION,
|
||||||
stacked = false,
|
stacked = false,
|
||||||
loading = false,
|
loading = false,
|
||||||
onCreate = () => {},
|
renderXLabel,
|
||||||
onUpdate = () => {},
|
renderYLabel,
|
||||||
|
XAxisType = 'time',
|
||||||
|
YAxisType = 'linear',
|
||||||
|
renderTooltip,
|
||||||
|
onCreate,
|
||||||
|
onUpdate,
|
||||||
className,
|
className,
|
||||||
}) {
|
}) {
|
||||||
const canvas = useRef();
|
const canvas = useRef();
|
||||||
const chart = useRef(null);
|
const chart = useRef(null);
|
||||||
const [tooltip, setTooltip] = useState(null);
|
const [tooltip, setTooltip] = useState(null);
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const [theme] = useTheme();
|
const { theme, colors } = useTheme();
|
||||||
|
|
||||||
const colors = useMemo(
|
|
||||||
() => ({
|
|
||||||
text: THEME_COLORS[theme].gray700,
|
|
||||||
line: THEME_COLORS[theme].gray200,
|
|
||||||
}),
|
|
||||||
[theme],
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderYLabel = label => {
|
|
||||||
return +label > 1000 ? formatLongNumber(label) : label;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderXLabel = useCallback(
|
|
||||||
(label, index, values) => {
|
|
||||||
const d = new Date(values[index].value);
|
|
||||||
|
|
||||||
switch (unit) {
|
|
||||||
case 'minute':
|
|
||||||
return dateFormat(d, 'h:mm', locale);
|
|
||||||
case 'hour':
|
|
||||||
return dateFormat(d, 'p', locale);
|
|
||||||
case 'day':
|
|
||||||
return dateFormat(d, 'MMM d', locale);
|
|
||||||
case 'month':
|
|
||||||
return dateFormat(d, 'MMM', locale);
|
|
||||||
case 'year':
|
|
||||||
return dateFormat(d, 'YYY', locale);
|
|
||||||
default:
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[locale, unit],
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderTooltip = useCallback(
|
|
||||||
model => {
|
|
||||||
const { opacity, labelColors, dataPoints } = model.tooltip;
|
|
||||||
|
|
||||||
if (!dataPoints?.length || !opacity) {
|
|
||||||
setTooltip(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formats = {
|
|
||||||
millisecond: 'T',
|
|
||||||
second: 'pp',
|
|
||||||
minute: 'p',
|
|
||||||
hour: 'h:mm aaa - PP',
|
|
||||||
day: 'PPPP',
|
|
||||||
week: 'PPPP',
|
|
||||||
month: 'LLLL yyyy',
|
|
||||||
quarter: 'qqq',
|
|
||||||
year: 'yyyy',
|
|
||||||
};
|
|
||||||
|
|
||||||
setTooltip(
|
|
||||||
<div className={styles.tooltip}>
|
|
||||||
<div>{dateFormat(new Date(dataPoints[0].raw.x), formats[unit], locale)}</div>
|
|
||||||
<div>
|
|
||||||
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
|
||||||
<div className={styles.value}>
|
|
||||||
{formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label}
|
|
||||||
</div>
|
|
||||||
</StatusLight>
|
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[unit],
|
|
||||||
);
|
|
||||||
|
|
||||||
const getOptions = useCallback(() => {
|
const getOptions = useCallback(() => {
|
||||||
return {
|
return {
|
||||||
@ -117,12 +50,12 @@ export function BarChart({
|
|||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
external: renderTooltip,
|
external: renderTooltip ? renderTooltip.bind(null, setTooltip) : undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
type: 'time',
|
type: XAxisType,
|
||||||
stacked: true,
|
stacked: true,
|
||||||
time: {
|
time: {
|
||||||
unit,
|
unit,
|
||||||
@ -131,34 +64,44 @@ export function BarChart({
|
|||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
border: {
|
border: {
|
||||||
color: colors.line,
|
color: colors.chart.line,
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
color: colors.text,
|
color: colors.chart.text,
|
||||||
autoSkip: false,
|
autoSkip: false,
|
||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
callback: renderXLabel,
|
callback: renderXLabel,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
type: 'linear',
|
type: YAxisType,
|
||||||
min: 0,
|
min: 0,
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
stacked,
|
stacked,
|
||||||
grid: {
|
grid: {
|
||||||
color: colors.line,
|
color: colors.chart.line,
|
||||||
},
|
},
|
||||||
border: {
|
border: {
|
||||||
color: colors.line,
|
color: colors.chart.line,
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
callback: renderYLabel,
|
callback: renderYLabel || renderNumberLabels,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [animationDuration, renderTooltip, renderXLabel, stacked, colors, unit, locale]);
|
}, [
|
||||||
|
animationDuration,
|
||||||
|
renderTooltip,
|
||||||
|
renderXLabel,
|
||||||
|
XAxisType,
|
||||||
|
YAxisType,
|
||||||
|
stacked,
|
||||||
|
colors,
|
||||||
|
unit,
|
||||||
|
locale,
|
||||||
|
]);
|
||||||
|
|
||||||
const createChart = () => {
|
const createChart = () => {
|
||||||
Chart.defaults.font.family = 'Inter';
|
Chart.defaults.font.family = 'Inter';
|
||||||
@ -173,7 +116,7 @@ export function BarChart({
|
|||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
|
|
||||||
onCreate(chart.current);
|
onCreate?.(chart.current);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateChart = () => {
|
const updateChart = () => {
|
||||||
@ -186,7 +129,7 @@ export function BarChart({
|
|||||||
|
|
||||||
chart.current.options = getOptions();
|
chart.current.options = getOptions();
|
||||||
|
|
||||||
onUpdate(chart.current);
|
onUpdate?.(chart.current);
|
||||||
|
|
||||||
chart.current.update();
|
chart.current.update();
|
||||||
};
|
};
|
||||||
@ -208,7 +151,11 @@ export function BarChart({
|
|||||||
<canvas ref={canvas} />
|
<canvas ref={canvas} />
|
||||||
</div>
|
</div>
|
||||||
<Legend chart={chart.current} />
|
<Legend chart={chart.current} />
|
||||||
{tooltip && <HoverTooltip tooltip={tooltip} />}
|
{tooltip && (
|
||||||
|
<HoverTooltip>
|
||||||
|
<div className={styles.tooltip}>{tooltip}</div>
|
||||||
|
</HoverTooltip>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,8 @@ import { useSpring, animated, config } from 'react-spring';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import NoData from 'components/common/NoData';
|
import NoData from 'components/common/NoData';
|
||||||
import { formatNumber, formatLongNumber } from 'lib/format';
|
import { formatNumber, formatLongNumber } from 'lib/format';
|
||||||
|
import useMessages from 'hooks/useMessages';
|
||||||
import styles from './DataTable.module.css';
|
import styles from './DataTable.module.css';
|
||||||
import useMessages from '../../hooks/useMessages';
|
|
||||||
|
|
||||||
export function DataTable({
|
export function DataTable({
|
||||||
data = [],
|
data = [],
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
.table {
|
.table {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: fit-content(100%) auto;
|
grid-template-rows: fit-content(100%) auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
|
@ -2,16 +2,15 @@ import { useMemo } from 'react';
|
|||||||
import { Loading } from 'react-basics';
|
import { Loading } from 'react-basics';
|
||||||
import { colord } from 'colord';
|
import { colord } from 'colord';
|
||||||
import BarChart from './BarChart';
|
import BarChart from './BarChart';
|
||||||
import { getDateArray, getDateLength } from 'lib/date';
|
import { getDateArray } from 'lib/date';
|
||||||
import useApi from 'hooks/useApi';
|
import { useApi, useLocale, useDateRange, useTimezone, usePageQuery } from 'hooks';
|
||||||
import useDateRange from 'hooks/useDateRange';
|
|
||||||
import useTimezone from 'hooks/useTimezone';
|
|
||||||
import usePageQuery from 'hooks/usePageQuery';
|
|
||||||
import { EVENT_COLORS } from 'lib/constants';
|
import { EVENT_COLORS } from 'lib/constants';
|
||||||
|
import { renderDateLabels, renderStatusTooltip } from 'lib/charts';
|
||||||
|
|
||||||
export function EventsChart({ websiteId, className, token }) {
|
export function EventsChart({ websiteId, className, token }) {
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const [{ startDate, endDate, unit, modified }] = useDateRange(websiteId);
|
const [{ startDate, endDate, unit, modified }] = useDateRange(websiteId);
|
||||||
|
const { locale } = useLocale();
|
||||||
const [timezone] = useTimezone();
|
const [timezone] = useTimezone();
|
||||||
const {
|
const {
|
||||||
query: { url, eventName },
|
query: { url, eventName },
|
||||||
@ -70,9 +69,10 @@ export function EventsChart({ websiteId, className, token }) {
|
|||||||
datasets={datasets}
|
datasets={datasets}
|
||||||
unit={unit}
|
unit={unit}
|
||||||
height={300}
|
height={300}
|
||||||
records={getDateLength(startDate, endDate, unit)}
|
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
stacked
|
stacked
|
||||||
|
renderXLabel={renderDateLabels(unit, locale)}
|
||||||
|
renderTooltip={renderStatusTooltip(unit, locale)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
.bar {
|
.bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
min-height: 80px;
|
min-height: 110px;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
@ -1,34 +1,13 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { colord } from 'colord';
|
|
||||||
import BarChart from './BarChart';
|
import BarChart from './BarChart';
|
||||||
import { THEME_COLORS } from 'lib/constants';
|
import { useLocale, useTheme, useMessages } from 'hooks';
|
||||||
import useTheme from 'hooks/useTheme';
|
import { renderDateLabels, renderStatusTooltip } from 'lib/charts';
|
||||||
import useMessages from 'hooks/useMessages';
|
|
||||||
import useLocale from 'hooks/useLocale';
|
|
||||||
|
|
||||||
export function PageviewsChart({ websiteId, data, unit, records, className, loading, ...props }) {
|
export function PageviewsChart({ websiteId, data, unit, className, loading, ...props }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const [theme] = useTheme();
|
const { colors } = useTheme();
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
|
|
||||||
const colors = useMemo(() => {
|
|
||||||
const primaryColor = colord(THEME_COLORS[theme].primary);
|
|
||||||
return {
|
|
||||||
views: {
|
|
||||||
hoverBackgroundColor: primaryColor.alpha(0.7).toRgbString(),
|
|
||||||
backgroundColor: primaryColor.alpha(0.4).toRgbString(),
|
|
||||||
borderColor: primaryColor.alpha(0.7).toRgbString(),
|
|
||||||
hoverBorderColor: primaryColor.toRgbString(),
|
|
||||||
},
|
|
||||||
visitors: {
|
|
||||||
hoverBackgroundColor: primaryColor.alpha(0.9).toRgbString(),
|
|
||||||
backgroundColor: primaryColor.alpha(0.6).toRgbString(),
|
|
||||||
borderColor: primaryColor.alpha(0.9).toRgbString(),
|
|
||||||
hoverBorderColor: primaryColor.toRgbString(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
const datasets = useMemo(() => {
|
const datasets = useMemo(() => {
|
||||||
if (!data) return [];
|
if (!data) return [];
|
||||||
|
|
||||||
@ -37,13 +16,13 @@ export function PageviewsChart({ websiteId, data, unit, records, className, load
|
|||||||
label: formatMessage(labels.uniqueVisitors),
|
label: formatMessage(labels.uniqueVisitors),
|
||||||
data: data.sessions,
|
data: data.sessions,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
...colors.visitors,
|
...colors.chart.visitors,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: formatMessage(labels.pageViews),
|
label: formatMessage(labels.pageViews),
|
||||||
data: data.pageviews,
|
data: data.pageviews,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
...colors.views,
|
...colors.chart.views,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [data, locale, colors]);
|
}, [data, locale, colors]);
|
||||||
@ -55,8 +34,9 @@ export function PageviewsChart({ websiteId, data, unit, records, className, load
|
|||||||
className={className}
|
className={className}
|
||||||
datasets={datasets}
|
datasets={datasets}
|
||||||
unit={unit}
|
unit={unit}
|
||||||
records={records}
|
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
renderXLabel={renderDateLabels(unit, locale)}
|
||||||
|
renderTooltip={renderStatusTooltip(unit, locale)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -16,8 +16,6 @@ export function ReportHeader({ report, icon }) {
|
|||||||
const { id, websiteId, name, parameters } = report || {};
|
const { id, websiteId, name, parameters } = report || {};
|
||||||
const { value, startDate, endDate } = parameters?.dateRange || {};
|
const { value, startDate, endDate } = parameters?.dateRange || {};
|
||||||
|
|
||||||
console.log('REPORT HEADER', report);
|
|
||||||
|
|
||||||
const handleSelect = websiteId => {
|
const handleSelect = websiteId => {
|
||||||
updateReport(id, { websiteId });
|
updateReport(id, { websiteId });
|
||||||
};
|
};
|
||||||
|
@ -1,184 +1,60 @@
|
|||||||
import Chart from 'chart.js/auto';
|
import { useCallback, useMemo } from 'react';
|
||||||
import classNames from 'classnames';
|
import { Loading } from 'react-basics';
|
||||||
import { colord } from 'colord';
|
|
||||||
import HoverTooltip from 'components/common/HoverTooltip';
|
|
||||||
import Legend from 'components/metrics/Legend';
|
|
||||||
import useLocale from 'hooks/useLocale';
|
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
import useTheme from 'hooks/useTheme';
|
import useTheme from 'hooks/useTheme';
|
||||||
import { DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants';
|
import BarChart from 'components/metrics/BarChart';
|
||||||
import { formatLongNumber } from 'lib/format';
|
import { formatLongNumber } from 'lib/format';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { Loading, StatusLight } from 'react-basics';
|
|
||||||
import styles from './FunnelChart.module.css';
|
import styles from './FunnelChart.module.css';
|
||||||
|
|
||||||
export function FunnelChart({
|
export function FunnelChart({ report, data, loading, className }) {
|
||||||
data,
|
|
||||||
animationDuration = DEFAULT_ANIMATION_DURATION,
|
|
||||||
stacked = false,
|
|
||||||
loading = false,
|
|
||||||
onCreate = () => {},
|
|
||||||
onUpdate = () => {},
|
|
||||||
className,
|
|
||||||
}) {
|
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const canvas = useRef();
|
const { colors } = useTheme();
|
||||||
const chart = useRef(null);
|
|
||||||
const [tooltip, setTooltip] = useState(null);
|
|
||||||
const { locale } = useLocale();
|
|
||||||
const [theme] = useTheme();
|
|
||||||
|
|
||||||
const datasets = useMemo(() => {
|
const { parameters } = report || {};
|
||||||
const primaryColor = colord(THEME_COLORS[theme].primary);
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: formatMessage(labels.uniqueVisitors),
|
|
||||||
data: data,
|
|
||||||
borderWidth: 1,
|
|
||||||
hoverBackgroundColor: primaryColor.alpha(0.9).toRgbString(),
|
|
||||||
backgroundColor: primaryColor.alpha(0.6).toRgbString(),
|
|
||||||
borderColor: primaryColor.alpha(0.9).toRgbString(),
|
|
||||||
hoverBorderColor: primaryColor.toRgbString(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const colors = useMemo(
|
const renderXLabel = useCallback(
|
||||||
() => ({
|
(label, index) => {
|
||||||
text: THEME_COLORS[theme].gray700,
|
return parameters.urls[index];
|
||||||
line: THEME_COLORS[theme].gray200,
|
},
|
||||||
}),
|
[parameters],
|
||||||
[theme],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderYLabel = label => {
|
const renderTooltip = useCallback((setTooltip, model) => {
|
||||||
return +label > 1000 ? formatLongNumber(label) : label;
|
const { opacity, dataPoints } = model.tooltip;
|
||||||
};
|
|
||||||
|
|
||||||
const renderTooltip = useCallback(model => {
|
|
||||||
const { opacity, labelColors, dataPoints } = model.tooltip;
|
|
||||||
|
|
||||||
if (!dataPoints?.length || !opacity) {
|
if (!dataPoints?.length || !opacity) {
|
||||||
setTooltip(null);
|
setTooltip(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTooltip(
|
setTooltip(`${formatLongNumber(dataPoints[0].raw.y)} ${formatMessage(labels.visitors)}`);
|
||||||
<div className={styles.tooltip}>
|
|
||||||
<div>
|
|
||||||
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
|
||||||
<div className={styles.value}>
|
|
||||||
<div>{dataPoints[0].raw.x}</div>
|
|
||||||
<div>{formatLongNumber(dataPoints[0].raw.y)}</div>
|
|
||||||
</div>
|
|
||||||
</StatusLight>
|
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getOptions = useCallback(() => {
|
const datasets = useMemo(() => {
|
||||||
return {
|
return [
|
||||||
responsive: true,
|
{
|
||||||
maintainAspectRatio: false,
|
label: formatMessage(labels.uniqueVisitors),
|
||||||
animation: {
|
data: data,
|
||||||
duration: animationDuration,
|
borderWidth: 1,
|
||||||
resize: {
|
...colors.chart.visitors,
|
||||||
duration: 0,
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
duration: 0,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
plugins: {
|
];
|
||||||
legend: {
|
}, [data]);
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
enabled: false,
|
|
||||||
external: renderTooltip,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
grid: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
border: {
|
|
||||||
color: colors.line,
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
color: colors.text,
|
|
||||||
autoSkip: false,
|
|
||||||
maxRotation: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
type: 'linear',
|
|
||||||
min: 0,
|
|
||||||
beginAtZero: true,
|
|
||||||
stacked,
|
|
||||||
grid: {
|
|
||||||
color: colors.line,
|
|
||||||
},
|
|
||||||
border: {
|
|
||||||
color: colors.line,
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
color: colors.text,
|
|
||||||
callback: renderYLabel,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [animationDuration, renderTooltip, stacked, colors, locale]);
|
|
||||||
|
|
||||||
const createChart = () => {
|
if (loading) {
|
||||||
Chart.defaults.font.family = 'Inter';
|
return <Loading icon="dots" className={styles.loading} />;
|
||||||
|
}
|
||||||
const options = getOptions();
|
|
||||||
|
|
||||||
chart.current = new Chart(canvas.current, {
|
|
||||||
type: 'bar',
|
|
||||||
data: { datasets },
|
|
||||||
options,
|
|
||||||
});
|
|
||||||
|
|
||||||
onCreate(chart.current);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateChart = () => {
|
|
||||||
setTooltip(null);
|
|
||||||
|
|
||||||
chart.current.data.datasets[0].data = datasets[0].data;
|
|
||||||
chart.current.data.datasets[0].label = datasets[0].label;
|
|
||||||
|
|
||||||
chart.current.options = getOptions();
|
|
||||||
|
|
||||||
onUpdate(chart.current);
|
|
||||||
|
|
||||||
chart.current.update();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (datasets) {
|
|
||||||
if (!chart.current) {
|
|
||||||
createChart();
|
|
||||||
} else {
|
|
||||||
updateChart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [datasets, theme, animationDuration, locale]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<BarChart
|
||||||
<div className={classNames(styles.chart, className)}>
|
className={className}
|
||||||
{loading && <Loading position="page" icon="dots" />}
|
datasets={datasets}
|
||||||
<canvas ref={canvas} />
|
unit="day"
|
||||||
</div>
|
loading={loading}
|
||||||
<Legend chart={chart.current} />
|
renderXLabel={renderXLabel}
|
||||||
{tooltip && <HoverTooltip tooltip={tooltip} />}
|
renderTooltip={renderTooltip}
|
||||||
</>
|
XAxisType="category"
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,23 +1,3 @@
|
|||||||
.chart {
|
.loading {
|
||||||
position: relative;
|
height: 300px;
|
||||||
height: 400px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip .value {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
text-transform: lowercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 992px) {
|
|
||||||
.chart {
|
|
||||||
/*height: 200px;*/
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,47 @@
|
|||||||
import { useMessages } from 'hooks';
|
import { useMessages } from 'hooks';
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
Icon,
|
Icon,
|
||||||
Form,
|
Form,
|
||||||
FormButtons,
|
FormButtons,
|
||||||
FormInput,
|
FormInput,
|
||||||
FormRow,
|
FormRow,
|
||||||
PopupTrigger,
|
ModalTrigger,
|
||||||
Popup,
|
Modal,
|
||||||
SubmitButton,
|
SubmitButton,
|
||||||
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
|
Tooltip,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
import Icons from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import { updateReport } from 'store/reports';
|
import { updateReport } from 'store/reports';
|
||||||
import { useRef } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import styles from './FunnelParameters.module.css';
|
import styles from './FunnelParameters.module.css';
|
||||||
|
|
||||||
export function FunnelParameters({ report }) {
|
export function FunnelParameters({ report }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const { id, websiteId, parameters, isLoading } = report || {};
|
const { id, websiteId, parameters, isLoading } = report || {};
|
||||||
|
const queryDisabled = !websiteId || parameters?.urls?.length < 2;
|
||||||
|
|
||||||
const handleSubmit = values => {
|
const handleSubmit = values => {
|
||||||
console.log({ values });
|
updateReport(id, { parameters: values, isLoading: false, update: Date.now() });
|
||||||
updateReport(id, { parameters: values, isLoading: false });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('PARAMETERS', parameters);
|
const handleAdd = url => {
|
||||||
|
updateReport(id, { parameters: { ...parameters, urls: parameters.urls.concat(url) } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = index => {
|
||||||
|
const urls = [...parameters.urls];
|
||||||
|
urls.splice(index, 1);
|
||||||
|
updateReport(id, { parameters: { ...parameters, urls } });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form ref={ref} values={parameters} onSubmit={handleSubmit}>
|
<Form ref={ref} values={parameters} onSubmit={handleSubmit}>
|
||||||
<FormRow label="Window (minutes)">
|
<FormRow label={formatMessage(labels.window)}>
|
||||||
<FormInput
|
<FormInput
|
||||||
name="window"
|
name="window"
|
||||||
rules={{ required: formatMessage(labels.required), pattern: /[0-9]+/ }}
|
rules={{ required: formatMessage(labels.required), pattern: /[0-9]+/ }}
|
||||||
@ -38,11 +49,22 @@ export function FunnelParameters({ report }) {
|
|||||||
<TextField autoComplete="off" />
|
<TextField autoComplete="off" />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label={formatMessage(labels.urls)} action={<AddURLButton />}>
|
<FormRow label={formatMessage(labels.urls)} action={<AddURLButton onAdd={handleAdd} />}>
|
||||||
hi
|
<div className={styles.urls}>
|
||||||
|
{parameters?.urls.map((url, index) => {
|
||||||
|
return (
|
||||||
|
<div key={index} className={styles.url}>
|
||||||
|
<Text>{url}</Text>
|
||||||
|
<Icon onClick={() => handleRemove(index)}>
|
||||||
|
<Icons.Close />
|
||||||
|
</Icon>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormButtons>
|
<FormButtons>
|
||||||
<SubmitButton variant="primary" disabled={!websiteId} loading={isLoading}>
|
<SubmitButton variant="primary" disabled={queryDisabled} loading={isLoading}>
|
||||||
{formatMessage(labels.query)}
|
{formatMessage(labels.query)}
|
||||||
</SubmitButton>
|
</SubmitButton>
|
||||||
</FormButtons>
|
</FormButtons>
|
||||||
@ -51,16 +73,49 @@ export function FunnelParameters({ report }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AddURLButton() {
|
function AddURLButton({ onAdd }) {
|
||||||
|
const [url, setUrl] = useState('');
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
|
const handleAdd = close => {
|
||||||
|
onAdd?.(url);
|
||||||
|
setUrl('');
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = e => {
|
||||||
|
setUrl(e.target.value);
|
||||||
|
};
|
||||||
|
const handleClose = close => {
|
||||||
|
setUrl('');
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopupTrigger>
|
<Tooltip label={formatMessage(labels.addUrl)}>
|
||||||
<Icon>
|
<ModalTrigger>
|
||||||
<Icons.Plus />
|
<Icon>
|
||||||
</Icon>
|
<Icons.Plus />
|
||||||
<Popup className={styles.popup} position="right" alignment="start">
|
</Icon>
|
||||||
HALLO
|
<Modal>
|
||||||
</Popup>
|
{close => {
|
||||||
</PopupTrigger>
|
return (
|
||||||
|
<Form>
|
||||||
|
<FormRow label={formatMessage(labels.url)}>
|
||||||
|
<TextField name="url" value={url} onChange={handleChange} autoComplete="off" />
|
||||||
|
</FormRow>
|
||||||
|
<FormButtons align="center" flex>
|
||||||
|
<Button variant="primary" onClick={() => handleAdd(close)}>
|
||||||
|
{formatMessage(labels.add)}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => handleClose(close)}>{formatMessage(labels.cancel)}</Button>
|
||||||
|
</FormButtons>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Modal>
|
||||||
|
</ModalTrigger>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,16 @@
|
|||||||
.popup {
|
.urls {
|
||||||
background: var(--base50);
|
display: flex;
|
||||||
padding: 20px;
|
flex-direction: column;
|
||||||
margin-left: 10px;
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px;
|
||||||
border: 1px solid var(--base400);
|
border: 1px solid var(--base400);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
}
|
box-shadow: 1px 1px 1px var(--base400);
|
||||||
|
}
|
||||||
|
@ -7,21 +7,35 @@ import ReportMenu from '../ReportMenu';
|
|||||||
import ReportBody from '../ReportBody';
|
import ReportBody from '../ReportBody';
|
||||||
import Funnel from 'assets/funnel.svg';
|
import Funnel from 'assets/funnel.svg';
|
||||||
import { useReport } from 'hooks';
|
import { useReport } from 'hooks';
|
||||||
|
import useApi from 'hooks/useApi';
|
||||||
|
|
||||||
export default function FunnelReport({ reportId }) {
|
export default function FunnelReport({ reportId }) {
|
||||||
const report = useReport(reportId);
|
const report = useReport(reportId, { window: 60, urls: ['/', '/docs'] });
|
||||||
|
const { post, useQuery } = useApi();
|
||||||
|
const { data, isLoading, error } = useQuery(
|
||||||
|
['report:funnel', report?.update],
|
||||||
|
() => {
|
||||||
|
const { websiteId, parameters } = report || {};
|
||||||
|
|
||||||
console.log('REPORT', { report });
|
return post(`/reports/funnel`, {
|
||||||
|
websiteId: websiteId,
|
||||||
|
...parameters,
|
||||||
|
startAt: +parameters.dateRange.startDate,
|
||||||
|
endAt: +parameters.dateRange.endDate,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ enabled: !!report?.update },
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Report>
|
<Report error={error} loading={data && isLoading}>
|
||||||
<ReportHeader icon={<Funnel />} report={report} />
|
<ReportHeader icon={<Funnel />} report={report} />
|
||||||
<ReportMenu>
|
<ReportMenu>
|
||||||
<FunnelParameters report={report} />
|
<FunnelParameters report={report} />
|
||||||
</ReportMenu>
|
</ReportMenu>
|
||||||
<ReportBody>
|
<ReportBody>
|
||||||
<FunnelChart report={report} />
|
<FunnelChart report={report} data={data} />
|
||||||
<FunnelTable report={report} />
|
<FunnelTable data={data} />
|
||||||
</ReportBody>
|
</ReportBody>
|
||||||
</Report>
|
</Report>
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
import DataTable from 'components/metrics/DataTable';
|
import DataTable from 'components/metrics/DataTable';
|
||||||
|
import { useMessages } from 'hooks';
|
||||||
|
|
||||||
export function FunnelTable({ ...props }) {
|
export function FunnelTable({ data }) {
|
||||||
const { data } = props;
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
const tableData =
|
return (
|
||||||
data?.map(a => ({ x: a.x, y: a.y, z: Math.floor(a.y / data[0].y) * 100 })) || [];
|
<DataTable
|
||||||
|
data={data}
|
||||||
return <DataTable data={tableData} title="Url" type="device" />;
|
title={formatMessage(labels.url)}
|
||||||
|
metric={formatMessage(labels.visitors)}
|
||||||
|
showPercentage={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FunnelTable;
|
export default FunnelTable;
|
||||||
|
@ -6,13 +6,13 @@ import Moon from 'assets/moon.svg';
|
|||||||
import styles from './ThemeSetting.module.css';
|
import styles from './ThemeSetting.module.css';
|
||||||
|
|
||||||
export function ThemeSetting() {
|
export function ThemeSetting() {
|
||||||
const [theme, setTheme] = useTheme();
|
const { theme, saveTheme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
<Button
|
<Button
|
||||||
className={classNames({ [styles.active]: theme === 'light' })}
|
className={classNames({ [styles.active]: theme === 'light' })}
|
||||||
onClick={() => setTheme('light')}
|
onClick={() => saveTheme('light')}
|
||||||
>
|
>
|
||||||
<Icon>
|
<Icon>
|
||||||
<Sun />
|
<Sun />
|
||||||
@ -20,7 +20,7 @@ export function ThemeSetting() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className={classNames({ [styles.active]: theme === 'dark' })}
|
className={classNames({ [styles.active]: theme === 'dark' })}
|
||||||
onClick={() => setTheme('dark')}
|
onClick={() => saveTheme('dark')}
|
||||||
>
|
>
|
||||||
<Icon>
|
<Icon>
|
||||||
<Moon />
|
<Moon />
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import useStore, { createReport } from 'store/reports';
|
import useStore, { createReport } from 'store/reports';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useForceUpdate } from 'hooks';
|
|
||||||
|
|
||||||
export function useReport(reportId) {
|
export function useReport(reportId, defaultParameters) {
|
||||||
const [id, setId] = useState(reportId);
|
const [id, setId] = useState(reportId);
|
||||||
|
|
||||||
const selector = useCallback(state => state[id], [id]);
|
const selector = useCallback(state => state[id], [id]);
|
||||||
@ -10,12 +9,11 @@ export function useReport(reportId) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!report) {
|
if (!report) {
|
||||||
setId(createReport().id);
|
const newReport = createReport(defaultParameters);
|
||||||
|
setId(newReport.id);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
console.log('USE REPORT', report);
|
|
||||||
|
|
||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import useStore, { setTheme } from 'store/app';
|
import useStore, { setTheme } from 'store/app';
|
||||||
import { getItem, setItem } from 'next-basics';
|
import { getItem, setItem } from 'next-basics';
|
||||||
import { THEME_CONFIG } from 'lib/constants';
|
import { THEME_COLORS, THEME_CONFIG } from 'lib/constants';
|
||||||
|
import { colord } from 'colord';
|
||||||
|
|
||||||
const selector = state => state.theme;
|
const selector = state => state.theme;
|
||||||
|
|
||||||
@ -13,6 +14,26 @@ export function useTheme() {
|
|||||||
: 'light'
|
: 'light'
|
||||||
: 'light';
|
: 'light';
|
||||||
const theme = useStore(selector) || getItem(THEME_CONFIG) || defaultTheme;
|
const theme = useStore(selector) || getItem(THEME_CONFIG) || defaultTheme;
|
||||||
|
const primaryColor = colord(THEME_COLORS[theme].primary);
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
chart: {
|
||||||
|
text: THEME_COLORS[theme].gray700,
|
||||||
|
line: THEME_COLORS[theme].gray200,
|
||||||
|
views: {
|
||||||
|
hoverBackgroundColor: primaryColor.alpha(0.7).toRgbString(),
|
||||||
|
backgroundColor: primaryColor.alpha(0.4).toRgbString(),
|
||||||
|
borderColor: primaryColor.alpha(0.7).toRgbString(),
|
||||||
|
hoverBorderColor: primaryColor.toRgbString(),
|
||||||
|
},
|
||||||
|
visitors: {
|
||||||
|
hoverBackgroundColor: primaryColor.alpha(0.9).toRgbString(),
|
||||||
|
backgroundColor: primaryColor.alpha(0.6).toRgbString(),
|
||||||
|
borderColor: primaryColor.alpha(0.9).toRgbString(),
|
||||||
|
hoverBorderColor: primaryColor.toRgbString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
function saveTheme(value) {
|
function saveTheme(value) {
|
||||||
setItem(THEME_CONFIG, value);
|
setItem(THEME_CONFIG, value);
|
||||||
@ -32,7 +53,7 @@ export function useTheme() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return [theme, saveTheme];
|
return { theme, saveTheme, colors };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useTheme;
|
export default useTheme;
|
||||||
|
62
lib/charts.js
Normal file
62
lib/charts.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { StatusLight } from 'react-basics';
|
||||||
|
import { dateFormat } from 'lib/date';
|
||||||
|
import { formatLongNumber } from 'lib/format';
|
||||||
|
|
||||||
|
export function renderNumberLabels(label) {
|
||||||
|
return +label > 1000 ? formatLongNumber(label) : label;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderDateLabels(unit, locale) {
|
||||||
|
return (label, index, values) => {
|
||||||
|
const d = new Date(values[index].value);
|
||||||
|
|
||||||
|
switch (unit) {
|
||||||
|
case 'minute':
|
||||||
|
return dateFormat(d, 'h:mm', locale);
|
||||||
|
case 'hour':
|
||||||
|
return dateFormat(d, 'p', locale);
|
||||||
|
case 'day':
|
||||||
|
return dateFormat(d, 'MMM d', locale);
|
||||||
|
case 'month':
|
||||||
|
return dateFormat(d, 'MMM', locale);
|
||||||
|
case 'year':
|
||||||
|
return dateFormat(d, 'YYY', locale);
|
||||||
|
default:
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderStatusTooltip(unit, locale) {
|
||||||
|
return (setTooltip, model) => {
|
||||||
|
const { opacity, labelColors, dataPoints } = model.tooltip;
|
||||||
|
|
||||||
|
if (!dataPoints?.length || !opacity) {
|
||||||
|
setTooltip(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formats = {
|
||||||
|
millisecond: 'T',
|
||||||
|
second: 'pp',
|
||||||
|
minute: 'p',
|
||||||
|
hour: 'h:mm aaa - PP',
|
||||||
|
day: 'PPPP',
|
||||||
|
week: 'PPPP',
|
||||||
|
month: 'LLLL yyyy',
|
||||||
|
quarter: 'qqq',
|
||||||
|
year: 'yyyy',
|
||||||
|
};
|
||||||
|
|
||||||
|
setTooltip(
|
||||||
|
<>
|
||||||
|
<div>{dateFormat(new Date(dataPoints[0].raw.x), formats[unit], locale)}</div>
|
||||||
|
<div>
|
||||||
|
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
||||||
|
{formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label}
|
||||||
|
</StatusLight>
|
||||||
|
</div>
|
||||||
|
</>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
@ -29,6 +29,7 @@ export default async (
|
|||||||
|
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const { websiteId, urls, window, startAt, endAt } = req.body;
|
const { websiteId, urls, window, startAt, endAt } = req.body;
|
||||||
|
|
||||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import replace from '@rollup/plugin-replace';
|
|||||||
import { terser } from 'rollup-plugin-terser';
|
import { terser } from 'rollup-plugin-terser';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: 'tracker/event-data.js',
|
input: 'tracker/index.js',
|
||||||
output: {
|
output: {
|
||||||
file: 'public/script.js',
|
file: 'public/script.js',
|
||||||
format: 'iife',
|
format: 'iife',
|
||||||
|
@ -15,8 +15,6 @@ const store = create(() => ({ ...initialState }));
|
|||||||
export function updateReport(id, data) {
|
export function updateReport(id, data) {
|
||||||
const report = store.getState()[id];
|
const report = store.getState()[id];
|
||||||
|
|
||||||
console.log('UPDATE STORE START', id, report);
|
|
||||||
|
|
||||||
if (report) {
|
if (report) {
|
||||||
store.setState(
|
store.setState(
|
||||||
produce(state => {
|
produce(state => {
|
||||||
@ -37,9 +35,9 @@ export function updateReport(id, data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createReport() {
|
export function createReport(parameters) {
|
||||||
const id = `new_${getRandomChars(16)}`;
|
const id = `new_${getRandomChars(16)}`;
|
||||||
const report = { ...emptyReport, id };
|
const report = { ...emptyReport, id, parameters };
|
||||||
|
|
||||||
store.setState(
|
store.setState(
|
||||||
produce(state => {
|
produce(state => {
|
||||||
@ -49,8 +47,6 @@ export function createReport() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('CREATE STORE', report);
|
|
||||||
|
|
||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user