280 lines
9.0 KiB
TypeScript
280 lines
9.0 KiB
TypeScript
"use client";
|
|
|
|
import { BarChart, LineChart } from "@mantine/charts";
|
|
import { Box, Group, ScrollArea, SimpleGrid, Stack, Text, useMantineColorScheme } from "@mantine/core";
|
|
|
|
import { clientApi } from "@homarr/api/client";
|
|
import { formatDuration } from "@homarr/common";
|
|
import type { UmamiVisitorStats } from "@homarr/integrations/types";
|
|
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
|
|
|
|
import { UmamiEventsContent } from "./umami-events-content";
|
|
import { UmamiTopPagesContent, UmamiTopReferrersContent } from "./umami-top-list";
|
|
import { formatTimeFrameLabel, formatXLabel, umamiQueryOptions } from "./umami-utils";
|
|
import type { TimeFrame } from "./umami-utils";
|
|
|
|
interface UmamiContentProps {
|
|
integrationIds: string[];
|
|
websiteId: string;
|
|
timeFrame: TimeFrame;
|
|
eventName?: string;
|
|
eventNames: string[];
|
|
chartStyle: string;
|
|
chartType: string;
|
|
viewMode: string;
|
|
topCount: number;
|
|
width: number;
|
|
height: number;
|
|
displayMode: "compact" | "advanced";
|
|
}
|
|
|
|
export function UmamiContent({
|
|
integrationIds,
|
|
websiteId,
|
|
timeFrame,
|
|
eventName,
|
|
eventNames,
|
|
chartStyle,
|
|
chartType,
|
|
viewMode,
|
|
topCount,
|
|
width,
|
|
height,
|
|
displayMode,
|
|
}: UmamiContentProps) {
|
|
const t = useScopedI18n("widget.umami");
|
|
const tCommon = useScopedI18n("common");
|
|
const locale = useCurrentIntlLocale();
|
|
const { colorScheme } = useMantineColorScheme();
|
|
const tickColor = colorScheme === "dark" ? "#c1c2c5" : "#495057";
|
|
|
|
const { data: results = [], isPending: isStatsPending } = clientApi.widget.umami.getVisitorStats.useQuery(
|
|
{ integrationIds, websiteId, timeFrame, eventName },
|
|
umamiQueryOptions,
|
|
);
|
|
|
|
const activeVisitorsInput = { integrationId: integrationIds[0] ?? "", websiteId };
|
|
const { data: activeVisitors = 0 } = clientApi.widget.umami.getActiveVisitors.useQuery(
|
|
activeVisitorsInput,
|
|
umamiQueryOptions,
|
|
);
|
|
|
|
const { data: multiEventSeries } = clientApi.widget.umami.getMultiEventTimeSeries.useQuery(
|
|
{ integrationId: integrationIds[0] ?? "", websiteId, timeFrame, eventNames: [...eventNames].toSorted() },
|
|
{ enabled: viewMode === "events" && eventNames.length > 0, ...umamiQueryOptions },
|
|
);
|
|
|
|
const multiEventTotal = multiEventSeries
|
|
? multiEventSeries.flatMap(({ dataPoints }) => dataPoints).reduce((sum, { y }) => sum + y, 0)
|
|
: undefined;
|
|
|
|
const firstResult = results[0];
|
|
if (isStatsPending) {
|
|
return (
|
|
<Stack align="center" justify="center" h="100%">
|
|
<Text c="dimmed" size="sm">
|
|
{tCommon("action.loading")}
|
|
</Text>
|
|
</Stack>
|
|
);
|
|
}
|
|
if (!firstResult) {
|
|
return (
|
|
<Stack align="center" justify="center" h="100%">
|
|
<Text c="dimmed" size="sm">
|
|
{t("error.noData")}
|
|
</Text>
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
const { visitorStats } = firstResult;
|
|
|
|
const chartData = visitorStats.dataPoints.map((point: UmamiVisitorStats["dataPoints"][number]) => ({
|
|
label: formatXLabel(point.timestamp, timeFrame),
|
|
visitors: point.visitors,
|
|
...(point.events !== undefined ? { events: point.events } : {}),
|
|
}));
|
|
|
|
const hasEventSeries = visitorStats.dataPoints.some(({ events }) => events !== undefined);
|
|
const series = [
|
|
{ name: "visitors", color: "blue.5" },
|
|
...(hasEventSeries ? [{ name: "events", color: "orange.5" }] : []),
|
|
];
|
|
|
|
const isOverlay = chartStyle === "overlay" && hasEventSeries;
|
|
const overlayBarSize = 16;
|
|
const isAdvanced = displayMode === "advanced";
|
|
const isDense = !isAdvanced && height < 120;
|
|
const showXAxis = isAdvanced || height >= 140;
|
|
const showSecondaryStats = isAdvanced || height >= 96;
|
|
const showDetailedStats = isAdvanced || (width >= 260 && height >= 150);
|
|
|
|
const selectedView =
|
|
viewMode === "events" ? (
|
|
<UmamiEventsContent
|
|
integrationIds={integrationIds}
|
|
websiteId={websiteId}
|
|
timeFrame={timeFrame}
|
|
eventNames={eventNames}
|
|
chartType={chartType}
|
|
showXAxis={showXAxis}
|
|
/>
|
|
) : viewMode === "topPages" ? (
|
|
<UmamiTopPagesContent
|
|
integrationIds={integrationIds}
|
|
websiteId={websiteId}
|
|
timeFrame={timeFrame}
|
|
limit={topCount}
|
|
/>
|
|
) : viewMode === "topReferrers" ? (
|
|
<UmamiTopReferrersContent
|
|
integrationIds={integrationIds}
|
|
websiteId={websiteId}
|
|
timeFrame={timeFrame}
|
|
limit={topCount}
|
|
/>
|
|
) : chartType === "sparkline" ? (
|
|
<LineChart
|
|
h="100%"
|
|
data={chartData}
|
|
dataKey="label"
|
|
series={series}
|
|
withDots={false}
|
|
curveType="monotone"
|
|
tickLine="none"
|
|
gridAxis="none"
|
|
withLegend={false}
|
|
withTooltip
|
|
withXAxis={showXAxis}
|
|
withYAxis={false}
|
|
xAxisProps={{ tick: { fontSize: 9, fill: tickColor }, interval: "preserveStartEnd" }}
|
|
/>
|
|
) : (
|
|
<BarChart
|
|
h="100%"
|
|
data={chartData}
|
|
dataKey="label"
|
|
series={series}
|
|
tickLine="none"
|
|
gridAxis="none"
|
|
withLegend={false}
|
|
withTooltip
|
|
withXAxis={showXAxis}
|
|
withYAxis={false}
|
|
barProps={isOverlay ? { barSize: overlayBarSize, radius: 2 } : { radius: 2 }}
|
|
barChartProps={isOverlay ? { barGap: -overlayBarSize } : undefined}
|
|
xAxisProps={{ tick: { fontSize: 9, fill: tickColor }, interval: "preserveStartEnd" }}
|
|
/>
|
|
);
|
|
|
|
const advancedLists = (
|
|
<SimpleGrid cols={viewMode === "topPages" || viewMode === "topReferrers" ? 1 : 2} spacing="md" mih={260}>
|
|
{viewMode !== "topPages" && (
|
|
<UmamiTopPagesContent
|
|
integrationIds={integrationIds}
|
|
websiteId={websiteId}
|
|
timeFrame={timeFrame}
|
|
limit={topCount}
|
|
/>
|
|
)}
|
|
{viewMode !== "topReferrers" && (
|
|
<UmamiTopReferrersContent
|
|
integrationIds={integrationIds}
|
|
websiteId={websiteId}
|
|
timeFrame={timeFrame}
|
|
limit={topCount}
|
|
/>
|
|
)}
|
|
</SimpleGrid>
|
|
);
|
|
|
|
const advancedContent =
|
|
width < 900 ? (
|
|
<ScrollArea h="100%">
|
|
<Stack gap="md">
|
|
<Box h={Math.max(280, Math.min(420, height - 160))}>{selectedView}</Box>
|
|
{advancedLists}
|
|
</Stack>
|
|
</ScrollArea>
|
|
) : (
|
|
<SimpleGrid cols={2} spacing="md" h="100%" style={{ gridTemplateRows: "minmax(0, 1fr)" }}>
|
|
<Box h="100%" mih={260}>
|
|
{selectedView}
|
|
</Box>
|
|
<ScrollArea h="100%">{advancedLists}</ScrollArea>
|
|
</SimpleGrid>
|
|
);
|
|
|
|
return (
|
|
<Stack gap={isDense ? 2 : 4} p={isDense ? 4 : "xs"} h="100%">
|
|
<Group justify="space-between" align="baseline" wrap="nowrap">
|
|
<Text size="xs" c="dimmed" truncate="end" style={{ maxWidth: "55%" }}>
|
|
{visitorStats.domain} ({formatTimeFrameLabel(timeFrame, t)})
|
|
</Text>
|
|
{(eventName && viewMode === "chart" && visitorStats.eventCount !== undefined) ||
|
|
(viewMode === "events" && multiEventTotal !== undefined) ? (
|
|
<Group gap={6} align="baseline" wrap="nowrap">
|
|
<Group gap={4} align="baseline">
|
|
<Text size="lg" fw={700} lh={1}>
|
|
{viewMode === "events"
|
|
? (multiEventTotal ?? 0).toLocaleString(locale)
|
|
: (visitorStats.eventCount ?? 0).toLocaleString(locale)}
|
|
</Text>
|
|
<Text size="xs" c="dimmed">
|
|
{t("event.label")}
|
|
</Text>
|
|
</Group>
|
|
<Text size="xs" c="dimmed">
|
|
/
|
|
</Text>
|
|
<Group gap={4} align="baseline">
|
|
<Text size="lg" fw={700} lh={1}>
|
|
{visitorStats.totalVisitors.toLocaleString(locale)}
|
|
</Text>
|
|
<Text size="xs" c="dimmed">
|
|
{t("visitors")}
|
|
</Text>
|
|
</Group>
|
|
</Group>
|
|
) : (
|
|
<Group gap={4} align="baseline">
|
|
<Text size="xl" fw={700} lh={1}>
|
|
{visitorStats.totalVisitors.toLocaleString(locale)}
|
|
</Text>
|
|
<Text size="xs" c="dimmed">
|
|
{t("visitors")}
|
|
</Text>
|
|
</Group>
|
|
)}
|
|
</Group>
|
|
{showSecondaryStats && (
|
|
<Group wrap="wrap" style={{ columnGap: 12, rowGap: 4 }}>
|
|
<Text size="xs" c="green">
|
|
● {activeVisitors.toLocaleString(locale)} {t("active")}
|
|
</Text>
|
|
<Text size="xs" c="dimmed">
|
|
{visitorStats.totalPageviews.toLocaleString(locale)} {t("pageviews")}
|
|
</Text>
|
|
<Text size="xs" c="dimmed">
|
|
{visitorStats.totalVisits.toLocaleString(locale)} {t("visits")}
|
|
</Text>
|
|
{showDetailedStats && (
|
|
<>
|
|
<Text size="xs" c="dimmed">
|
|
{visitorStats.bounceRate.toLocaleString(locale)}% {t("bounceRate")}
|
|
</Text>
|
|
<Text size="xs" c="dimmed">
|
|
{formatDuration(visitorStats.avgDuration * 1000)} {t("avgDuration")}
|
|
</Text>
|
|
</>
|
|
)}
|
|
</Group>
|
|
)}
|
|
<Box mt={4} style={{ flex: 1, minHeight: 0 }}>
|
|
{displayMode === "advanced" ? advancedContent : selectedView}
|
|
</Box>
|
|
</Stack>
|
|
);
|
|
}
|