diff --git a/package.json b/package.json
index 7a06fdfa..70fd1f73 100644
--- a/package.json
+++ b/package.json
@@ -81,7 +81,7 @@
"@react-spring/web": "^9.7.3",
"@svgr/cli": "^8.1.0",
"@tanstack/react-query": "^5.28.6",
- "@umami/react-zen": "^0.134.0",
+ "@umami/react-zen": "^0.136.0",
"@umami/redis-client": "^0.27.0",
"bcryptjs": "^2.4.3",
"chalk": "^4.1.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6d7a3926..1fac552a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -45,8 +45,8 @@ importers:
specifier: ^5.28.6
version: 5.77.2(react@19.1.0)
'@umami/react-zen':
- specifier: ^0.134.0
- version: 0.134.0(@babel/core@7.27.1)(@types/react@19.1.5)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))
+ specifier: ^0.136.0
+ version: 0.136.0(@babel/core@7.27.1)(@types/react@19.1.5)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))
'@umami/redis-client':
specifier: ^0.27.0
version: 0.27.0
@@ -2585,8 +2585,8 @@ packages:
resolution: {integrity: sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@umami/react-zen@0.134.0':
- resolution: {integrity: sha512-RBSD50mTw2YKY0Z73OSxVtjrMvIq3nGtWYtcZHPXl/4oYj3Ph0cKTKto14Jx2qs2kHm2DxcS3ND1FR1OrPEknw==}
+ '@umami/react-zen@0.136.0':
+ resolution: {integrity: sha512-4dStzemPNxGB1nVdfnSxfkmYUnIXTRwqBqJpn4N9RvhmnQQeUfYQvJc4eqMSM0hrQToQcgGdvB/HucDpk30W7Q==}
'@umami/redis-client@0.27.0':
resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==}
@@ -9805,7 +9805,7 @@ snapshots:
'@typescript-eslint/types': 8.32.1
eslint-visitor-keys: 4.2.0
- '@umami/react-zen@0.134.0(@babel/core@7.27.1)(@types/react@19.1.5)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))':
+ '@umami/react-zen@0.136.0(@babel/core@7.27.1)(@types/react@19.1.5)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))':
dependencies:
'@fontsource/jetbrains-mono': 5.2.5
'@internationalized/date': 3.8.2
diff --git a/src/app/(main)/reports/journey/JourneyParameters.tsx b/src/app/(main)/reports/journey/JourneyParameters.tsx
deleted file mode 100644
index 839ce6dd..00000000
--- a/src/app/(main)/reports/journey/JourneyParameters.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { useMessages, useReport } from '@/components/hooks';
-import {
- Select,
- Form,
- FormButtons,
- FormField,
- ListItem,
- FormSubmitButton,
- TextField,
-} from '@umami/react-zen';
-import { BaseParameters } from '../[reportId]/BaseParameters';
-
-export function JourneyParameters() {
- const { report, runReport, isRunning } = useReport();
- const { formatMessage, labels } = useMessages();
-
- const { id, parameters } = report || {};
- const { websiteId, dateRange, steps } = parameters || {};
- const queryDisabled = !websiteId || !dateRange || !steps;
-
- const handleSubmit = (data: any, e: any) => {
- e.stopPropagation();
- e.preventDefault();
-
- if (!queryDisabled) {
- runReport(data);
- }
- };
-
- return (
-
- );
-}
diff --git a/src/app/(main)/reports/journey/JourneyReport.tsx b/src/app/(main)/reports/journey/JourneyReport.tsx
deleted file mode 100644
index a75de880..00000000
--- a/src/app/(main)/reports/journey/JourneyReport.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-'use client';
-import { Report } from '../[reportId]/Report';
-import { ReportHeader } from '../[reportId]/ReportHeader';
-import { ReportMenu } from '../[reportId]/ReportMenu';
-import { ReportBody } from '../[reportId]/ReportBody';
-import { JourneyParameters } from './JourneyParameters';
-import { JourneyView } from './JourneyView';
-import { Path } from '@/components/icons';
-import { REPORT_TYPES } from '@/lib/constants';
-
-const defaultParameters = {
- type: REPORT_TYPES.journey,
- parameters: { steps: 5 },
-};
-
-export function JourneyReport({ reportId }: { reportId?: string }) {
- return (
-
- } />
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(main)/reports/journey/JourneyReportPage.tsx b/src/app/(main)/reports/journey/JourneyReportPage.tsx
deleted file mode 100644
index ecc6d053..00000000
--- a/src/app/(main)/reports/journey/JourneyReportPage.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { JourneyReport } from './JourneyReport';
-
-export function JourneyReportPage() {
- return ;
-}
diff --git a/src/app/(main)/reports/journey/JourneyView.tsx b/src/app/(main)/reports/journey/JourneyView.tsx
deleted file mode 100644
index 0ec73126..00000000
--- a/src/app/(main)/reports/journey/JourneyView.tsx
+++ /dev/null
@@ -1,257 +0,0 @@
-import { useMemo, useState } from 'react';
-import { TooltipTrigger, Tooltip, Focusable } from '@umami/react-zen';
-import { firstBy } from 'thenby';
-import classNames from 'classnames';
-import { useEscapeKey, useMessages, useReport } from '@/components/hooks';
-import { objectToArray } from '@/lib/data';
-import styles from './JourneyView.module.css';
-import { formatLongNumber } from '@/lib/format';
-
-const NODE_HEIGHT = 60;
-const NODE_GAP = 10;
-const LINE_WIDTH = 3;
-
-export function JourneyView() {
- const [selectedNode, setSelectedNode] = useState(null);
- const [activeNode, setActiveNode] = useState(null);
- const { report } = useReport();
- const { data, parameters } = report || {};
- const { formatMessage, labels } = useMessages();
-
- useEscapeKey(() => setSelectedNode(null));
-
- const columns = useMemo(() => {
- if (!data) {
- return [];
- }
-
- const selectedPaths = selectedNode?.paths ?? [];
- const activePaths = activeNode?.paths ?? [];
- const columns = [];
-
- for (let columnIndex = 0; columnIndex < +parameters.steps; columnIndex++) {
- const nodes = {};
-
- data.forEach(({ items, count }: any, nodeIndex: any) => {
- const name = items[columnIndex];
-
- if (name) {
- const selected = !!selectedPaths.find(({ items }) => items[columnIndex] === name);
- const active = selected && !!activePaths.find(({ items }) => items[columnIndex] === name);
-
- if (!nodes[name]) {
- const paths = data.filter(({ items }) => items[columnIndex] === name);
-
- nodes[name] = {
- name,
- count,
- totalCount: count,
- nodeIndex,
- columnIndex,
- selected,
- active,
- paths,
- pathMap: paths.map(({ items, count }) => ({
- [`${columnIndex}:${items.join(':')}`]: count,
- })),
- };
- } else {
- nodes[name].totalCount += count;
- }
- }
- });
-
- columns.push({
- nodes: objectToArray(nodes).sort(firstBy('total', -1)),
- });
- }
-
- columns.forEach((column, columnIndex) => {
- const nodes = column.nodes.map((currentNode, currentNodeIndex) => {
- const previousNodes = columns[columnIndex - 1]?.nodes;
- let selectedCount = previousNodes ? 0 : currentNode.totalCount;
- let activeCount = selectedCount;
-
- const lines =
- previousNodes?.reduce((arr: any[][], previousNode: any, previousNodeIndex: number) => {
- const fromCount = selectedNode?.paths.reduce((sum, path) => {
- if (
- previousNode.name === path.items[columnIndex - 1] &&
- currentNode.name === path.items[columnIndex]
- ) {
- sum += path.count;
- }
- return sum;
- }, 0);
-
- if (currentNode.selected && previousNode.selected && fromCount) {
- arr.push([previousNodeIndex, currentNodeIndex]);
- selectedCount += fromCount;
-
- if (previousNode.active) {
- activeCount += fromCount;
- }
- }
-
- return arr;
- }, []) || [];
-
- return { ...currentNode, selectedCount, activeCount, lines };
- });
-
- const visitorCount = nodes.reduce(
- (sum: number, { selected, selectedCount, active, activeCount, totalCount }) => {
- if (!selectedNode) {
- sum += totalCount;
- } else if (!activeNode && selectedNode && selected) {
- sum += selectedCount;
- } else if (activeNode && active) {
- sum += activeCount;
- }
- return sum;
- },
- 0,
- );
-
- const previousTotal = columns[columnIndex - 1]?.visitorCount ?? 0;
- const dropOff =
- previousTotal > 0 ? ((visitorCount - previousTotal) / previousTotal) * 100 : 0;
-
- Object.assign(column, { nodes, visitorCount, dropOff });
- });
-
- return columns;
- }, [data, selectedNode, activeNode]);
-
- const handleClick = (name: string, columnIndex: number, paths: any[]) => {
- if (name !== selectedNode?.name || columnIndex !== selectedNode?.columnIndex) {
- setSelectedNode({ name, columnIndex, paths });
- } else {
- setSelectedNode(null);
- }
- setActiveNode(null);
- };
-
- if (!data) {
- return null;
- }
-
- return (
-
-
- {columns.map((column, columnIndex) => {
- const dropOffPercent = `${~~column.dropOff}%`;
- return (
-
-
-
{columnIndex + 1}
-
-
- {formatLongNumber(column.visitorCount)} {formatMessage(labels.visitors)}
-
- {columnIndex > 0 &&
{dropOffPercent}
}
-
-
-
- {column.nodes.map(
- ({
- name,
- totalCount,
- selected,
- active,
- paths,
- activeCount,
- selectedCount,
- lines,
- }) => {
- const nodeCount = selected
- ? active
- ? activeCount
- : selectedCount
- : totalCount;
-
- return (
-
selected && setActiveNode({ name, columnIndex, paths })}
- onMouseLeave={() => selected && setActiveNode(null)}
- >
-
handleClick(name, columnIndex, paths)}
- >
-
- {name}
-
-
-
- {dropOffPercent}
-
-
-
- {formatLongNumber(nodeCount)}
-
-
-
- {columnIndex < columns.length &&
- lines.map(([fromIndex, nodeIndex], i) => {
- const height =
- (Math.abs(nodeIndex - fromIndex) + 1) * (NODE_HEIGHT + NODE_GAP) -
- NODE_GAP;
- const midHeight =
- (Math.abs(nodeIndex - fromIndex) - 1) * (NODE_HEIGHT + NODE_GAP) +
- NODE_GAP +
- LINE_WIDTH;
- const nodeName = columns[columnIndex - 1]?.nodes[fromIndex].name;
-
- return (
-
- path.items[columnIndex] === name &&
- path.items[columnIndex - 1] === nodeName,
- ),
- [styles.up]: fromIndex < nodeIndex,
- [styles.down]: fromIndex > nodeIndex,
- [styles.flat]: fromIndex === nodeIndex,
- })}
- style={{ height }}
- >
-
-
-
-
- );
- })}
-
-
- );
- },
- )}
-
-
- );
- })}
-
-
- );
-}
diff --git a/src/app/(main)/reports/journey/page.tsx b/src/app/(main)/reports/journey/page.tsx
deleted file mode 100644
index 320acebd..00000000
--- a/src/app/(main)/reports/journey/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Metadata } from 'next';
-import { JourneyReportPage } from './JourneyReportPage';
-
-export default function () {
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Journey Report',
-};
diff --git a/src/app/(main)/reports/journey/JourneyView.module.css b/src/app/(main)/websites/[websiteId]/journeys/Journey.module.css
similarity index 86%
rename from src/app/(main)/reports/journey/JourneyView.module.css
rename to src/app/(main)/websites/[websiteId]/journeys/Journey.module.css
index 6e6516c7..96b2f50f 100644
--- a/src/app/(main)/reports/journey/JourneyView.module.css
+++ b/src/app/(main)/websites/[websiteId]/journeys/Journey.module.css
@@ -3,9 +3,9 @@
height: 100%;
position: relative;
- --journey-line-color: var(--base600);
+ --journey-line-color: var(--base-color-6);
--journey-active-color: var(--primary-color);
- --journey-faded-color: var(--base300);
+ --journey-faded-color: var(--base-color-3);
}
.view {
@@ -43,8 +43,8 @@
.dropoff {
font-weight: 600;
- color: var(--blue800);
- background: var(--blue100);
+ color: var(--font-color-muted);
+ background: var(--base-color-3);
padding: 4px 8px;
border-radius: 5px;
}
@@ -58,8 +58,8 @@
height: 50px;
font-size: 16px;
font-weight: 700;
- color: var(--base100);
- background: var(--base800);
+ color: var(--base-color-1);
+ background: var(--base-color-12);
z-index: 1;
margin: 0 auto 20px;
}
@@ -84,7 +84,7 @@
position: relative;
cursor: pointer;
padding: 10px 20px;
- background: var(--base75);
+ background: var(--base-color-3);
border-radius: 5px;
display: flex;
align-items: center;
@@ -96,40 +96,33 @@
}
.node:hover:not(.selected) {
- color: var(--base900);
- background: var(--base100);
+ background: var(--base-color-4);
}
.node.selected {
- color: var(--base75);
- background: var(--base900);
- font-weight: 400;
+ color: var(--base-color-1);
+ background: var(--base-color-12);
}
.node.active {
- color: var(--light50);
+ color: var(--primary-font-color);
background: var(--primary-color);
}
.node.selected .count {
- color: var(--base50);
- background: var(--base800);
+ color: var(--base-color-1);
+ background: var(--base-color-12);
}
.node.selected.active .count {
- background: var(--primary600);
+ color: var(--primary-font-color);
+ background: var(--primary-color);
}
.name {
max-width: 200px;
}
-.count {
- border-radius: 4px;
- padding: 5px 10px;
- background: var(--base200);
-}
-
.line {
position: absolute;
bottom: 0;
@@ -219,7 +212,7 @@
position: absolute;
border-radius: 100%;
border: 3px solid var(--journey-line-color);
- background: var(--light50);
+ background: var(--base-color-1);
width: 14px;
height: 14px;
}
diff --git a/src/app/(main)/websites/[websiteId]/journeys/Journey.tsx b/src/app/(main)/websites/[websiteId]/journeys/Journey.tsx
index efc04ec6..fd5b35d5 100644
--- a/src/app/(main)/websites/[websiteId]/journeys/Journey.tsx
+++ b/src/app/(main)/websites/[websiteId]/journeys/Journey.tsx
@@ -1,99 +1,289 @@
-import { Grid, Row, Column, Text, Icon, Button, Dialog } from '@umami/react-zen';
-import { ReportEditButton } from '@/components/input/ReportEditButton';
-import { useMessages, useResultQuery } from '@/components/hooks';
-import { Arrow, Eye } from '@/components/icons';
+import { useMemo, useState } from 'react';
+import { TooltipTrigger, Tooltip, Focusable } from '@umami/react-zen';
+import { firstBy } from 'thenby';
+import classNames from 'classnames';
+import { useEscapeKey, useMessages, useResultQuery } from '@/components/hooks';
+import { objectToArray } from '@/lib/data';
+import styles from './Journey.module.css';
+import { formatLongNumber } from '@/lib/format';
import { LoadingPanel } from '@/components/common/LoadingPanel';
-import { JourneyEditForm } from './JourneyEditForm';
+
+const NODE_HEIGHT = 60;
+const NODE_GAP = 10;
+const LINE_WIDTH = 3;
export interface JourneyProps {
- id: string;
- name: string;
- type: string;
- parameters: {
- steps: string;
- startStep: string;
- endStep: string;
- };
websiteId: string;
startDate: Date;
endDate: Date;
+ steps: number;
+ startStep?: string;
+ endStep?: string;
}
-export type GoalData = { num: number; total: number };
-
export function Journey({
- id,
- name,
- type,
- parameters,
websiteId,
startDate,
endDate,
+ steps,
+ startStep,
+ endStep,
}: JourneyProps) {
+ const [selectedNode, setSelectedNode] = useState(null);
+ const [activeNode, setActiveNode] = useState(null);
const { formatMessage, labels } = useMessages();
- const { data, error, isLoading } = useResultQuery(type, {
+ const { data, error, isLoading } = useResultQuery('journey', {
websiteId,
dateRange: {
startDate,
endDate,
},
- parameters,
+ parameters: {
+ steps,
+ startStep,
+ endStep,
+ },
});
+ useEscapeKey(() => setSelectedNode(null));
+
+ const columns = useMemo(() => {
+ if (!data) {
+ return [];
+ }
+
+ const selectedPaths = selectedNode?.paths ?? [];
+ const activePaths = activeNode?.paths ?? [];
+ const columns = [];
+
+ for (let columnIndex = 0; columnIndex < +steps; columnIndex++) {
+ const nodes = {};
+
+ data.forEach(({ items, count }: any, nodeIndex: any) => {
+ const name = items[columnIndex];
+
+ if (name) {
+ const selected = !!selectedPaths.find(({ items }) => items[columnIndex] === name);
+ const active = selected && !!activePaths.find(({ items }) => items[columnIndex] === name);
+
+ if (!nodes[name]) {
+ const paths = data.filter(({ items }) => items[columnIndex] === name);
+
+ nodes[name] = {
+ name,
+ count,
+ totalCount: count,
+ nodeIndex,
+ columnIndex,
+ selected,
+ active,
+ paths,
+ pathMap: paths.map(({ items, count }) => ({
+ [`${columnIndex}:${items.join(':')}`]: count,
+ })),
+ };
+ } else {
+ nodes[name].totalCount += count;
+ }
+ }
+ });
+
+ columns.push({
+ nodes: objectToArray(nodes).sort(firstBy('total', -1)),
+ });
+ }
+
+ columns.forEach((column, columnIndex) => {
+ const nodes = column.nodes.map(
+ (
+ currentNode: { totalCount: number; name: string; selected: boolean },
+ currentNodeIndex: any,
+ ) => {
+ const previousNodes = columns[columnIndex - 1]?.nodes;
+ let selectedCount = previousNodes ? 0 : currentNode.totalCount;
+ let activeCount = selectedCount;
+
+ const lines =
+ previousNodes?.reduce((arr: any[][], previousNode: any, previousNodeIndex: number) => {
+ const fromCount = selectedNode?.paths.reduce((sum, path) => {
+ if (
+ previousNode.name === path.items[columnIndex - 1] &&
+ currentNode.name === path.items[columnIndex]
+ ) {
+ sum += path.count;
+ }
+ return sum;
+ }, 0);
+
+ if (currentNode.selected && previousNode.selected && fromCount) {
+ arr.push([previousNodeIndex, currentNodeIndex]);
+ selectedCount += fromCount;
+
+ if (previousNode.active) {
+ activeCount += fromCount;
+ }
+ }
+
+ return arr;
+ }, []) || [];
+
+ return { ...currentNode, selectedCount, activeCount, lines };
+ },
+ );
+
+ const visitorCount = nodes.reduce(
+ (sum: number, { selected, selectedCount, active, activeCount, totalCount }) => {
+ if (!selectedNode) {
+ sum += totalCount;
+ } else if (!activeNode && selectedNode && selected) {
+ sum += selectedCount;
+ } else if (activeNode && active) {
+ sum += activeCount;
+ }
+ return sum;
+ },
+ 0,
+ );
+
+ const previousTotal = columns[columnIndex - 1]?.visitorCount ?? 0;
+ const dropOff =
+ previousTotal > 0 ? ((visitorCount - previousTotal) / previousTotal) * 100 : 0;
+
+ Object.assign(column, { nodes, visitorCount, dropOff });
+ });
+
+ return columns;
+ }, [data, selectedNode, activeNode]);
+
+ const handleClick = (name: string, columnIndex: number, paths: any[]) => {
+ if (name !== selectedNode?.name || columnIndex !== selectedNode?.columnIndex) {
+ setSelectedNode({ name, columnIndex, paths });
+ } else {
+ setSelectedNode(null);
+ }
+ setActiveNode(null);
+ };
+
return (
-
-
-
-
-
- {name}
-
-
-
-
-
- {({ close }) => {
- return (
-
- );
- }}
-
-
-
-
-
- {formatMessage(labels.steps)}: {parameters?.steps}
-
-
-
-
-
- {formatMessage(labels.startStep)}: {parameters?.startStep}
-
-
-
-
-
- {formatMessage(labels.endStep)}: {parameters?.endStep || formatMessage(labels.none)}
-
-
-
-
-
+
+
+ {columns.map((column, columnIndex) => {
+ const dropOffPercent = `${~~column.dropOff}%`;
+ return (
+
+
+
{columnIndex + 1}
+
+
+ {formatLongNumber(column.visitorCount)} {formatMessage(labels.visitors)}
+
+ {columnIndex > 0 &&
{dropOffPercent}
}
+
+
+
+ {column.nodes.map(
+ ({
+ name,
+ totalCount,
+ selected,
+ active,
+ paths,
+ activeCount,
+ selectedCount,
+ lines,
+ }) => {
+ const nodeCount = selected
+ ? active
+ ? activeCount
+ : selectedCount
+ : totalCount;
+
+ return (
+
+ selected && setActiveNode({ name, columnIndex, paths })
+ }
+ onMouseLeave={() => selected && setActiveNode(null)}
+ >
+
handleClick(name, columnIndex, paths)}
+ >
+
+ {name}
+
+
+
+
+ {formatLongNumber(nodeCount)}
+
+
+ {dropOffPercent}
+
+
+
+ {columnIndex < columns.length &&
+ lines.map(([fromIndex, nodeIndex], i) => {
+ const height =
+ (Math.abs(nodeIndex - fromIndex) + 1) * (NODE_HEIGHT + NODE_GAP) -
+ NODE_GAP;
+ const midHeight =
+ (Math.abs(nodeIndex - fromIndex) - 1) * (NODE_HEIGHT + NODE_GAP) +
+ NODE_GAP +
+ LINE_WIDTH;
+ const nodeName = columns[columnIndex - 1]?.nodes[fromIndex].name;
+
+ return (
+
+ path.items[columnIndex] === name &&
+ path.items[columnIndex - 1] === nodeName,
+ ),
+ [styles.up]: fromIndex < nodeIndex,
+ [styles.down]: fromIndex > nodeIndex,
+ [styles.flat]: fromIndex === nodeIndex,
+ })}
+ style={{ height }}
+ >
+
+
+
+
+ );
+ })}
+
+
+ );
+ },
+ )}
+
+
+ );
+ })}
+
+
);
}
diff --git a/src/app/(main)/websites/[websiteId]/journeys/JourneyAddButton.tsx b/src/app/(main)/websites/[websiteId]/journeys/JourneyAddButton.tsx
deleted file mode 100644
index b75de0b2..00000000
--- a/src/app/(main)/websites/[websiteId]/journeys/JourneyAddButton.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Button, DialogTrigger, Dialog, Icon, Text, Modal } from '@umami/react-zen';
-import { useMessages } from '@/components/hooks';
-import { JourneyEditForm } from './JourneyEditForm';
-import { Plus } from '@/components/icons';
-
-export function JourneyAddButton({ websiteId }: { websiteId: string }) {
- const { formatMessage, labels } = useMessages();
-
- return (
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(main)/websites/[websiteId]/journeys/JourneyEditForm.tsx b/src/app/(main)/websites/[websiteId]/journeys/JourneyEditForm.tsx
deleted file mode 100644
index 1a07d6b6..00000000
--- a/src/app/(main)/websites/[websiteId]/journeys/JourneyEditForm.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import {
- Form,
- FormField,
- TextField,
- FormButtons,
- FormSubmitButton,
- Button,
- Select,
- ListItem,
- Loading,
-} from '@umami/react-zen';
-import { useApi, useMessages, useModified, useReportQuery } from '@/components/hooks';
-
-const JOURNEY_STEPS = ['3', '4', '5', '6', '7'];
-
-export function JourneyEditForm({
- id,
- websiteId,
- onSave,
- onClose,
-}: {
- id?: string;
- websiteId: string;
- onSave?: () => void;
- onClose?: () => void;
-}) {
- const { formatMessage, labels } = useMessages();
- const { touch } = useModified();
- const { post, useMutation } = useApi();
- const { data } = useReportQuery(id);
- const { mutate, error, isPending } = useMutation({
- mutationFn: (params: any) => post(`/reports${id ? `/${id}` : ''}`, params),
- });
-
- const handleSubmit = async ({ name, ...parameters }) => {
- mutate(
- { ...data, id, name, type: 'journey', websiteId, parameters },
- {
- onSuccess: async () => {
- if (id) touch(`report:${id}`);
- touch('reports:journey');
- onSave?.();
- onClose?.();
- },
- },
- );
- };
-
- if (id && !data) {
- return ;
- }
-
- const defaultValues = {
- name: data?.name || '',
- steps: data?.steps || '5',
- startStep: data?.parameters?.startStep || '',
- endStep: data?.parameters?.endStep || '',
- };
-
- return (
-
- );
-}
diff --git a/src/app/(main)/websites/[websiteId]/journeys/JourneysPage.tsx b/src/app/(main)/websites/[websiteId]/journeys/JourneysPage.tsx
index 604f72ed..93bdf9b1 100644
--- a/src/app/(main)/websites/[websiteId]/journeys/JourneysPage.tsx
+++ b/src/app/(main)/websites/[websiteId]/journeys/JourneysPage.tsx
@@ -1,38 +1,67 @@
'use client';
-import { Grid, Loading } from '@umami/react-zen';
-import { SectionHeader } from '@/components/common/SectionHeader';
-import { Journey } from './Journey';
-import { JourneyAddButton } from './JourneyAddButton';
-import { WebsiteControls } from '../WebsiteControls';
-import { useDateRange, useReportsQuery } from '@/components/hooks';
-import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useState } from 'react';
+import { ListItem, Select, Column, Grid, SearchField } from '@umami/react-zen';
+import { useDateRange, useMessages } from '@/components/hooks';
import { Panel } from '@/components/common/Panel';
+import { Journey } from './Journey';
+import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+
+const JOURNEY_STEPS = [2, 3, 4, 5, 6, 7];
+const DEFAULT_STEP = 3;
export function JourneysPage({ websiteId }: { websiteId: string }) {
- const { result } = useReportsQuery({ websiteId, type: 'journey' });
+ const { formatMessage, labels } = useMessages();
const {
dateRange: { startDate, endDate },
} = useDateRange(websiteId);
-
- if (!result) {
- return ;
- }
+ const [steps, setSteps] = useState(DEFAULT_STEP);
+ const [startStep, setStartStep] = useState('');
+ const [endStep, setEndStep] = useState('');
return (
- <>
+
-
-
-
-
-
- {result?.data?.map((report: any) => (
-
-
-
+
+
-
- >
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/app/api/reports/journey/route.ts b/src/app/api/reports/journey/route.ts
index 19ad98fa..130a494e 100644
--- a/src/app/api/reports/journey/route.ts
+++ b/src/app/api/reports/journey/route.ts
@@ -1,19 +1,11 @@
-import { z } from 'zod';
import { canViewWebsite } from '@/lib/auth';
import { unauthorized, json } from '@/lib/response';
import { parseRequest } from '@/lib/request';
import { getJourney } from '@/queries';
-import { reportParms } from '@/lib/schema';
+import { reportResultSchema } from '@/lib/schema';
export async function POST(request: Request) {
- const schema = z.object({
- ...reportParms,
- steps: z.coerce.number().min(3).max(7),
- startStep: z.string().optional(),
- endStep: z.string().optional(),
- });
-
- const { auth, body, error } = await parseRequest(request, schema);
+ const { auth, body, error } = await parseRequest(request, reportResultSchema);
if (error) {
return error();
@@ -22,9 +14,7 @@ export async function POST(request: Request) {
const {
websiteId,
dateRange: { startDate, endDate },
- steps,
- startStep,
- endStep,
+ parameters: { steps, startStep, endStep },
} = body;
if (!(await canViewWebsite(auth, websiteId))) {
diff --git a/src/components/common/Panel.tsx b/src/components/common/Panel.tsx
index c199f814..54024c84 100644
--- a/src/components/common/Panel.tsx
+++ b/src/components/common/Panel.tsx
@@ -1,6 +1,46 @@
-import { Box } from '@umami/react-zen';
-import type { BoxProps } from '@umami/react-zen/Box';
+import { useState } from 'react';
+import { Column, type ColumnProps, Row, Icon, Button } from '@umami/react-zen';
+import { Maximize, Close } from '@/components/icons';
-export function Panel(props: BoxProps) {
- return ;
+export interface PanelProps extends ColumnProps {
+ allowFullscreen?: boolean;
+}
+
+const fullscreenStyles = {
+ position: 'fixed',
+ width: '100%',
+ height: '100%',
+ top: 0,
+ left: 0,
+ zIndex: 9999,
+} as any;
+
+export function Panel({ allowFullscreen, style, children, ...props }: PanelProps) {
+ const [isFullscreen, setIsFullscreen] = useState(false);
+
+ const handleFullscreen = () => {
+ setIsFullscreen(!isFullscreen);
+ };
+
+ return (
+
+ {allowFullscreen && (
+
+
+
+ )}
+ {children}
+
+ );
}
diff --git a/src/components/icons.ts b/src/components/icons.ts
index ad341def..883ad681 100644
--- a/src/components/icons.ts
+++ b/src/components/icons.ts
@@ -18,7 +18,9 @@ export {
ListFilter,
LockKeyhole,
LogOut,
+ Maximize,
Menu,
+ Minimize,
Moon,
MoreHorizontal as More,
PanelLeft,
diff --git a/src/lib/schema.ts b/src/lib/schema.ts
index 7402af8c..c07efe16 100644
--- a/src/lib/schema.ts
+++ b/src/lib/schema.ts
@@ -113,7 +113,7 @@ export const funnelReportSchema = z.object({
export const journeyReportSchema = z.object({
type: z.literal('journey'),
parameters: z.object({
- steps: z.coerce.number().positive(),
+ steps: z.coerce.number().min(2).max(7),
startStep: z.string().optional(),
endStep: z.string().optional(),
}),