diff --git a/src/app/(main)/reports/funnel/FunnelChart.module.css b/src/app/(main)/reports/funnel/FunnelChart.module.css index 9e1690b3..60254922 100644 --- a/src/app/(main)/reports/funnel/FunnelChart.module.css +++ b/src/app/(main)/reports/funnel/FunnelChart.module.css @@ -1,3 +1,84 @@ -.loading { - height: 300px; +.chart { + display: grid; +} + +.num { + display: flex; + align-items: center; + justify-content: center; + border-radius: 100%; + width: 50px; + height: 50px; + font-size: 16px; + font-weight: 700; + color: var(--base800); + background: var(--base100); + z-index: 1; +} + +.step { + display: grid; + grid-template-columns: max-content 1fr; + column-gap: 30px; + position: relative; + padding-bottom: 60px; +} + +.step::before { + content: ''; + position: absolute; + top: 0; + left: 25px; + bottom: 0; + width: 2px; + background-color: var(--base100); +} + +.card { + display: grid; + gap: 20px; +} + +.header { + display: flex; + align-items: center; + font-weight: 700; + gap: 20px; +} + +.bar { + display: flex; + align-items: center; + justify-content: end; + background: var(--base900); + height: 50px; + border-radius: 5px; + overflow: hidden; + position: relative; +} + +.label { + color: var(--base700); +} + +.value { + color: var(--base50); + margin-right: 20px; +} + +.track { + background-color: var(--base100); + border-radius: 5px; +} + +.info { + display: flex; + justify-content: space-between; + text-transform: lowercase; +} + +.item { + padding: 6px 10px; + border-radius: 4px; + border: 1px solid var(--base300); } diff --git a/src/app/(main)/reports/funnel/FunnelChart.tsx b/src/app/(main)/reports/funnel/FunnelChart.tsx index 38373517..6207a177 100644 --- a/src/app/(main)/reports/funnel/FunnelChart.tsx +++ b/src/app/(main)/reports/funnel/FunnelChart.tsx @@ -1,83 +1,57 @@ -import { JSX, useCallback, useContext, useMemo } from 'react'; -import { Loading, StatusLight } from 'react-basics'; -import { useMessages, useTheme } from 'components/hooks'; -import BarChart from 'components/metrics/BarChart'; -import { formatLongNumber } from 'lib/format'; +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'; export interface FunnelChartProps { className?: string; isLoading?: boolean; } -export function FunnelChart({ className, isLoading }: FunnelChartProps) { +export function FunnelChart({ className }: FunnelChartProps) { const { report } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); - const { colors } = useTheme(); - const { parameters, data } = report || {}; - - const renderXLabel = useCallback( - (label: string, index: number) => { - return parameters.urls[index]; - }, - [parameters], - ); - - const renderTooltipPopup = useCallback( - ( - setTooltipPopup: (arg0: JSX.Element) => void, - model: { tooltip: { opacity: any; labelColors: any; dataPoints: any } }, - ) => { - const { opacity, labelColors, dataPoints } = model.tooltip; - - if (!dataPoints?.length || !opacity) { - setTooltipPopup(null); - return; - } - - setTooltipPopup( - <> -
- {formatLongNumber(dataPoints[0].raw.y)} {formatMessage(labels.visitors)} -
-
- - {formatLongNumber(dataPoints[0].raw.z)}% {formatMessage(labels.dropoff)} - -
- , - ); - }, - [], - ); - - const datasets = useMemo(() => { - return [ - { - label: formatMessage(labels.uniqueVisitors), - data: data, - borderWidth: 1, - ...colors.chart.visitors, - }, - ]; - }, [data, colors, formatMessage, labels]); - - if (isLoading) { - return ; - } + const { data } = report || {}; return ( - +
+ {data?.map(({ url, visitors, dropped, dropoff, remaining }, index: number) => { + return ( +
+
{index + 1}
+
+
+ {formatMessage(labels.viewedPage)}: + {url} +
+
+
+ + {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)}%) +
+ )} +
+
+
+ ); + })} +
); } diff --git a/src/app/(main)/reports/funnel/FunnelReport.tsx b/src/app/(main)/reports/funnel/FunnelReport.tsx index 1fd1a80f..7b9a6677 100644 --- a/src/app/(main)/reports/funnel/FunnelReport.tsx +++ b/src/app/(main)/reports/funnel/FunnelReport.tsx @@ -1,5 +1,4 @@ import FunnelChart from './FunnelChart'; -import FunnelTable from './FunnelTable'; import FunnelParameters from './FunnelParameters'; import Report from '../[reportId]/Report'; import ReportHeader from '../[reportId]/ReportHeader'; @@ -22,7 +21,6 @@ export default function FunnelReport({ reportId }: { reportId?: string }) { - ); diff --git a/src/app/(main)/reports/funnel/FunnelTable.tsx b/src/app/(main)/reports/funnel/FunnelTable.tsx deleted file mode 100644 index 1fe1fdd9..00000000 --- a/src/app/(main)/reports/funnel/FunnelTable.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useContext } from 'react'; -import ListTable from 'components/metrics/ListTable'; -import { useMessages } from 'components/hooks'; -import { ReportContext } from '../[reportId]/Report'; - -export function FunnelTable() { - const { report } = useContext(ReportContext); - const { formatMessage, labels } = useMessages(); - return ( - - ); -} - -export default FunnelTable; diff --git a/src/components/messages.ts b/src/components/messages.ts index 2710e99d..b38e5bcd 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -209,6 +209,18 @@ export const labels = defineMessages({ select: { id: 'label.select', defaultMessage: 'Select' }, myAccount: { id: 'label.my-account', defaultMessage: 'My account' }, transfer: { id: 'label.transfer', defaultMessage: 'Transfer' }, + viewedPage: { + id: 'message.viewed-page', + defaultMessage: 'Viewed page', + }, + triggeredEvent: { + id: 'message.triggered-event', + defaultMessage: 'Triggered event', + }, + visitorsDroppedOff: { + id: 'message.visitors-dropped-off', + defaultMessage: 'Visitors droppped off', + }, }); export const messages = defineMessages({ diff --git a/src/queries/analytics/reports/getFunnel.ts b/src/queries/analytics/reports/getFunnel.ts index 4387cf09..0a5dfd96 100644 --- a/src/queries/analytics/reports/getFunnel.ts +++ b/src/queries/analytics/reports/getFunnel.ts @@ -2,6 +2,25 @@ import clickhouse from 'lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import prisma from 'lib/prisma'; +const formatResults = (urls: string[]) => (results: unknown) => { + return urls.map((url: 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; + const dropoff = 1 - visitors / previous; + const remaining = visitors / Number(results[0].count); + + return { + url, + visitors, + previous, + dropped, + dropoff, + remaining, + }; + }); +}; + export async function getFunnel( ...args: [ websiteId: string, @@ -29,9 +48,9 @@ async function relationalQuery( }, ): Promise< { - x: string; - y: number; - z: number; + url: string; + visitors: number; + dropoff: number; }[] > { const { windowMinutes, startDate, endDate, urls } = criteria; @@ -98,13 +117,7 @@ async function relationalQuery( endDate, ...urls, }, - ).then(results => { - return urls.map((a, i) => ({ - x: a, - y: results[i]?.count || 0, - z: (1 - Number(results[i]?.count) / Number(results[i - 1]?.count)) * 100 || 0, // drop off - })); - }); + ).then(formatResults(urls)); } async function clickhouseQuery( @@ -117,9 +130,9 @@ async function clickhouseQuery( }, ): Promise< { - x: string; - y: number; - z: number; + url: string; + visitors: number; + dropoff: number; }[] > { const { windowMinutes, startDate, endDate, urls } = criteria; @@ -198,11 +211,5 @@ async function clickhouseQuery( endDate, ...urlParams, }, - ).then(results => { - return urls.map((a, i) => ({ - x: a, - y: Number(results[i]?.count) || 0, - z: (1 - Number(results[i]?.count) / Number(results[i - 1]?.count)) * 100 || 0, // drop off - })); - }); + ).then(formatResults(urls)); } diff --git a/src/store/app.ts b/src/store/app.ts index 53fdbd92..8fa1a0d5 100644 --- a/src/store/app.ts +++ b/src/store/app.ts @@ -20,27 +20,27 @@ const initialState = { const store = create(() => ({ ...initialState })); -export function setTheme(theme) { +export function setTheme(theme: string) { store.setState({ theme }); } -export function setLocale(locale) { +export function setLocale(locale: string) { store.setState({ locale }); } -export function setShareToken(shareToken) { +export function setShareToken(shareToken: string) { store.setState({ shareToken }); } -export function setUser(user) { +export function setUser(user: object) { store.setState({ user }); } -export function setConfig(config) { +export function setConfig(config: object) { store.setState({ config }); } -export function setDateRange(dateRange) { +export function setDateRange(dateRange: object) { store.setState({ dateRange }); }