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 ( -
- - - - - - - - - - - - - {formatMessage(labels.runQuery)} - - - - ); -} 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 ( - - - - - {({ close }) => } - - - - ); -} 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 ( -
- - - - - - - - - - - - - - - {formatMessage(labels.save)} - -
- ); -} 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(), }),