diff --git a/src/app/(main)/reports/[reportId]/Report.tsx b/src/app/(main)/reports/[reportId]/Report.tsx index 76f73595..d6de9d42 100644 --- a/src/app/(main)/reports/[reportId]/Report.tsx +++ b/src/app/(main)/reports/[reportId]/Report.tsx @@ -13,7 +13,7 @@ export function Report({ className, }: { reportId: string; - defaultParameters: { [key: string]: any }; + defaultParameters: { type: string; parameters: { [key: string]: any } }; children: ReactNode; className?: string; }) { diff --git a/src/app/(main)/reports/funnel/FunnelChart.module.css b/src/app/(main)/reports/funnel/FunnelChart.module.css index 0279ea03..be2a9cdc 100644 --- a/src/app/(main)/reports/funnel/FunnelChart.module.css +++ b/src/app/(main)/reports/funnel/FunnelChart.module.css @@ -51,7 +51,7 @@ align-items: center; justify-content: flex-end; background: var(--base900); - height: 50px; + height: 30px; border-radius: 5px; overflow: hidden; position: relative; @@ -61,19 +61,12 @@ color: var(--base700); } -.value { - color: var(--base50); - margin-inline-end: 20px; -} - .track { background-color: var(--base100); border-radius: 5px; } .info { - display: flex; - justify-content: space-between; text-transform: lowercase; } @@ -82,3 +75,24 @@ border-radius: 4px; border: 1px solid var(--base300); } + +.metric { + color: var(--base700); + display: flex; + justify-content: space-between; + gap: 10px; + font-size: 24px; + margin: 10px 0; + text-transform: lowercase; +} + +.visitors { + color: var(--base900); + font-weight: 900; + margin-right: 10px; +} + +.percent { + font-weight: 700; + align-self: flex-end; +} diff --git a/src/app/(main)/reports/funnel/FunnelChart.tsx b/src/app/(main)/reports/funnel/FunnelChart.tsx index 6207a177..70076354 100644 --- a/src/app/(main)/reports/funnel/FunnelChart.tsx +++ b/src/app/(main)/reports/funnel/FunnelChart.tsx @@ -2,8 +2,8 @@ import { useContext } from 'react'; import classNames from 'classnames'; import { useMessages } from 'components/hooks'; import { ReportContext } from '../[reportId]/Report'; -import styles from './FunnelChart.module.css'; import { formatLongNumber } from 'lib/format'; +import styles from './FunnelChart.module.css'; export interface FunnelChartProps { className?: string; @@ -18,35 +18,33 @@ export function FunnelChart({ className }: FunnelChartProps) { return (
- {data?.map(({ url, visitors, dropped, dropoff, remaining }, index: number) => { + {data?.map(({ type, value, visitors, dropped, dropoff, remaining }, index: number) => { return ( -
+
{index + 1}
- {formatMessage(labels.viewedPage)}: - {url} + + {formatMessage(type === 'url' ? labels.viewedPage : labels.triggeredEvent)}: + + {value} +
+
+
+ {formatLongNumber(visitors)} + {formatMessage(labels.visitors)} +
+
{(remaining * 100).toFixed(2)}%
-
- - {remaining > 0.1 && `${(remaining * 100).toFixed(2)}%`} - -
+
-
-
- {formatLongNumber(visitors)} - {formatMessage(labels.visitors)} - ({(remaining * 100).toFixed(2)}%) + {dropoff > 0 && ( +
+ {formatLongNumber(dropped)} {formatMessage(labels.visitorsDroppedOff)} ( + {(dropoff * 100).toFixed(2)}%)
- {dropoff > 0 && ( -
- {formatLongNumber(dropped)} {formatMessage(labels.visitorsDroppedOff)} ( - {(dropoff * 100).toFixed(2)}%) -
- )} -
+ )}
); diff --git a/src/app/(main)/reports/funnel/FunnelParameters.module.css b/src/app/(main)/reports/funnel/FunnelParameters.module.css new file mode 100644 index 00000000..81ef9216 --- /dev/null +++ b/src/app/(main)/reports/funnel/FunnelParameters.module.css @@ -0,0 +1,9 @@ +.item { + display: flex; + align-items: center; + gap: 10px; +} + +.type { + color: var(--base700); +} diff --git a/src/app/(main)/reports/funnel/FunnelParameters.tsx b/src/app/(main)/reports/funnel/FunnelParameters.tsx index 2ea64cf5..248318f1 100644 --- a/src/app/(main)/reports/funnel/FunnelParameters.tsx +++ b/src/app/(main)/reports/funnel/FunnelParameters.tsx @@ -10,48 +10,54 @@ import { Popup, SubmitButton, TextField, + Button, } from 'react-basics'; import Icons from 'components/icons'; -import UrlAddForm from './UrlAddForm'; +import FunnelStepAddForm from './FunnelStepAddForm'; import { ReportContext } from '../[reportId]/Report'; import BaseParameters from '../[reportId]/BaseParameters'; import ParameterList from '../[reportId]/ParameterList'; import PopupForm from '../[reportId]/PopupForm'; +import styles from './FunnelParameters.module.css'; export function FunnelParameters() { const { report, runReport, updateReport, isRunning } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); const { id, parameters } = report || {}; - const { websiteId, dateRange, urls } = parameters || {}; - const queryDisabled = !websiteId || !dateRange || urls?.length < 2; + const { websiteId, dateRange, steps } = parameters || {}; + const queryDisabled = !websiteId || !dateRange || steps?.length < 2; const handleSubmit = (data: any, e: any) => { e.stopPropagation(); e.preventDefault(); + if (!queryDisabled) { runReport(data); } }; - const handleAddUrl = (url: string) => { - updateReport({ parameters: { urls: parameters.urls.concat(url) } }); + const handleAddStep = (step: { type: string; value: string }) => { + updateReport({ parameters: { steps: parameters.steps.concat(step) } }); }; - const handleRemoveUrl = (url: string) => { - const urls = [...parameters.urls]; - updateReport({ parameters: { urls: urls.filter(n => n !== url) } }); + const handleRemoveStep = (index: number) => { + const steps = [...parameters.steps]; + delete steps[index]; + updateReport({ parameters: { steps: steps.filter(n => n) } }); }; - const AddUrlButton = () => { + const AddStepButton = () => { return ( - - - - + + - + @@ -69,12 +75,17 @@ export function FunnelParameters() { - }> + }> - {urls.map(url => { + {steps.map((step: { type: string; value: string }, index: number) => { return ( - handleRemoveUrl(url)}> - {url} + handleRemoveStep(index)}> +
+
+ {step.type === 'url' ? : } +
+
{step.value}
+
); })} diff --git a/src/app/(main)/reports/funnel/FunnelReport.tsx b/src/app/(main)/reports/funnel/FunnelReport.tsx index 7b9a6677..850bbd90 100644 --- a/src/app/(main)/reports/funnel/FunnelReport.tsx +++ b/src/app/(main)/reports/funnel/FunnelReport.tsx @@ -9,7 +9,7 @@ import { REPORT_TYPES } from 'lib/constants'; const defaultParameters = { type: REPORT_TYPES.funnel, - parameters: { window: 60, urls: [] }, + parameters: { window: 60, steps: [] }, }; export default function FunnelReport({ reportId }: { reportId?: string }) { diff --git a/src/app/(main)/reports/funnel/FunnelStepAddForm.module.css b/src/app/(main)/reports/funnel/FunnelStepAddForm.module.css new file mode 100644 index 00000000..a254ff08 --- /dev/null +++ b/src/app/(main)/reports/funnel/FunnelStepAddForm.module.css @@ -0,0 +1,7 @@ +.dropdown { + width: 140px; +} + +.input { + width: 200px; +} diff --git a/src/app/(main)/reports/funnel/FunnelStepAddForm.tsx b/src/app/(main)/reports/funnel/FunnelStepAddForm.tsx new file mode 100644 index 00000000..978747c9 --- /dev/null +++ b/src/app/(main)/reports/funnel/FunnelStepAddForm.tsx @@ -0,0 +1,71 @@ +import { useState } from 'react'; +import { useMessages } from 'components/hooks'; +import { Button, FormRow, TextField, Flexbox, Dropdown, Item } from 'react-basics'; +import styles from './FunnelStepAddForm.module.css'; + +export interface UrlAddFormProps { + defaultValue?: string; + onAdd?: (step: { type: string; value: string }) => void; +} + +export function FunnelStepAddForm({ defaultValue = '', onAdd }: UrlAddFormProps) { + const [type, setType] = useState('url'); + const [value, setValue] = useState(defaultValue); + const { formatMessage, labels } = useMessages(); + const items = [ + { label: formatMessage(labels.url), value: 'url' }, + { label: formatMessage(labels.event), value: 'event' }, + ]; + const isDisabled = !type || !value; + + const handleSave = () => { + onAdd({ type, value }); + setValue(''); + }; + + const handleChange = e => { + setValue(e.target.value); + }; + + const handleKeyDown = e => { + if (e.key === 'Enter') { + e.stopPropagation(); + handleSave(); + } + }; + + const renderTypeValue = (value: any) => { + return items.find(item => item.value === value)?.label; + }; + + return ( + + + setType(value)} + > + {({ value, label }) => { + return {label}; + }} + + + + + + ); +} + +export default FunnelStepAddForm; diff --git a/src/app/(main)/reports/funnel/UrlAddForm.module.css b/src/app/(main)/reports/funnel/UrlAddForm.module.css deleted file mode 100644 index 6a3e03b5..00000000 --- a/src/app/(main)/reports/funnel/UrlAddForm.module.css +++ /dev/null @@ -1,14 +0,0 @@ -.form { - position: absolute; - background: var(--base50); - width: 300px; - padding: 30px; - margin-top: 10px; - border: 1px solid var(--base400); - border-radius: var(--border-radius); - box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.1); -} - -.input { - width: 100%; -} diff --git a/src/app/(main)/reports/funnel/UrlAddForm.tsx b/src/app/(main)/reports/funnel/UrlAddForm.tsx deleted file mode 100644 index 88c27ae9..00000000 --- a/src/app/(main)/reports/funnel/UrlAddForm.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { useState } from 'react'; -import { useMessages } from 'components/hooks'; -import { Button, Form, FormRow, TextField, Flexbox } from 'react-basics'; -import styles from './UrlAddForm.module.css'; - -export interface UrlAddFormProps { - defaultValue?: string; - onAdd?: (url: string) => void; -} - -export function UrlAddForm({ defaultValue = '', onAdd }: UrlAddFormProps) { - const [url, setUrl] = useState(defaultValue); - const { formatMessage, labels } = useMessages(); - - const handleSave = () => { - onAdd(url); - setUrl(''); - }; - - const handleChange = e => { - setUrl(e.target.value); - }; - - const handleKeyDown = e => { - if (e.key === 'Enter') { - e.stopPropagation(); - handleSave(); - } - }; - - return ( -
- - - - - - -
- ); -} - -export default UrlAddForm; diff --git a/src/components/hooks/queries/useReport.ts b/src/components/hooks/queries/useReport.ts index 4dade4db..3aacabb4 100644 --- a/src/components/hooks/queries/useReport.ts +++ b/src/components/hooks/queries/useReport.ts @@ -4,7 +4,10 @@ import { useApi } from './useApi'; import { useTimezone } from '../useTimezone'; import { useMessages } from '../useMessages'; -export function useReport(reportId: string, defaultParameters: { [key: string]: any } = {}) { +export function useReport( + reportId: string, + defaultParameters: { type: string; parameters: { [key: string]: any } }, +) { const [report, setReport] = useState(null); const [isRunning, setIsRunning] = useState(false); const { get, post } = useApi(); @@ -28,6 +31,8 @@ export function useReport(reportId: string, defaultParameters: { [key: string]: dateRange.endDate = new Date(endDate); } + data.parameters = { ...defaultParameters?.parameters, ...data.parameters }; + setReport(data); }; @@ -41,7 +46,7 @@ export function useReport(reportId: string, defaultParameters: { [key: string]: setReport( produce((state: any) => { - state.parameters = parameters; + state.parameters = { ...defaultParameters?.parameters, ...parameters }; state.data = data; return state; @@ -60,7 +65,11 @@ export function useReport(reportId: string, defaultParameters: { [key: string]: const { parameters, ...rest } = data; if (parameters) { - state.parameters = { ...state.parameters, ...parameters }; + state.parameters = { + ...defaultParameters?.parameters, + ...state.parameters, + ...parameters, + }; } for (const key in rest) { @@ -80,7 +89,7 @@ export function useReport(reportId: string, defaultParameters: { [key: string]: } else { loadReport(reportId); } - }, []); + }, [reportId]); return { report, runReport, updateReport, isRunning }; } diff --git a/src/components/messages.ts b/src/components/messages.ts index 50774dfd..4057bbfd 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -232,6 +232,8 @@ export const labels = defineMessages({ id: 'label.utm-description', defaultMessage: 'Track your campaigns through UTM parameters.', }, + steps: { id: 'label.steps', defaultMessage: 'Steps' }, + addStep: { id: 'label.add-step', defaultMessage: 'Add step' }, }); export const messages = defineMessages({ diff --git a/src/queries/analytics/reports/getFunnel.ts b/src/queries/analytics/reports/getFunnel.ts index a7dfd542..f9ceb85c 100644 --- a/src/queries/analytics/reports/getFunnel.ts +++ b/src/queries/analytics/reports/getFunnel.ts @@ -3,8 +3,7 @@ import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import prisma from 'lib/prisma'; const formatResults = (steps: { type: string; value: string }[]) => (results: unknown) => { - return steps.map((steps: { type: string; value: string }, i: number) => { - const url = steps.value; + return steps.map((step: { type: string; value: string }, i: number) => { const visitors = Number(results[i]?.count) || 0; const previous = Number(results[i - 1]?.count) || 0; const dropped = previous > 0 ? previous - visitors : 0; @@ -12,7 +11,7 @@ const formatResults = (steps: { type: string; value: string }[]) => (results: un const remaining = visitors / Number(results[0].count); return { - url, + ...step, visitors, previous, dropped, @@ -49,7 +48,7 @@ async function relationalQuery( }, ): Promise< { - url: string; + value: string; visitors: number; dropoff: number; }[] @@ -141,7 +140,7 @@ async function clickhouseQuery( }, ): Promise< { - url: string; + value: string; visitors: number; dropoff: number; }[]