diff --git a/src/app/(main)/reports/create/ReportTemplates.tsx b/src/app/(main)/reports/create/ReportTemplates.tsx index fdf5c5f5..0777cc1f 100644 --- a/src/app/(main)/reports/create/ReportTemplates.tsx +++ b/src/app/(main)/reports/create/ReportTemplates.tsx @@ -6,6 +6,7 @@ import Lightbulb from 'assets/lightbulb.svg'; import Magnet from 'assets/magnet.svg'; import Tag from 'assets/tag.svg'; import Target from 'assets/target.svg'; +import Path from 'assets/path.svg'; import styles from './ReportTemplates.module.css'; import { useMessages, useTeamUrl } from 'components/hooks'; @@ -44,6 +45,12 @@ export function ReportTemplates({ showHeader = true }: { showHeader?: boolean }) url: renderTeamUrl('/reports/goals'), icon: , }, + { + title: formatMessage(labels.journey), + description: formatMessage(labels.journeyDescription), + url: renderTeamUrl('/reports/journey'), + icon: , + }, ]; return ( diff --git a/src/app/(main)/reports/journey/JourneyView.module.css b/src/app/(main)/reports/journey/JourneyView.module.css index fa7cc0b4..4d5a96a4 100644 --- a/src/app/(main)/reports/journey/JourneyView.module.css +++ b/src/app/(main)/reports/journey/JourneyView.module.css @@ -1,14 +1,74 @@ -.title { - font-size: 24px; - line-height: 36px; - font-weight: 700; +.container { + height: 900px; + position: relative; } -.row { - display: grid; - grid-template-columns: 50% 50%; - gap: 20px; - border-bottom: 1px solid var(--base300); - padding-bottom: 30px; - margin-bottom: 30px; +.view { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + gap: 60px; + overflow: auto; +} + +.header { + display: flex; + margin-bottom: 20px; +} + +.num { + display: flex; + align-items: center; + justify-content: center; + border-radius: 100%; + width: 50px; + height: 50px; + font-size: 16px; + font-weight: 700; + color: var(--base100); + background: var(--base800); + z-index: 1; + margin: 0 auto; +} + +.column { + min-width: 300px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.item { + cursor: pointer; + padding: 10px 20px; + background: var(--base75); + border-radius: 5px; +} + +.item:hover:not(.highlight) { + color: var(--base900); + background: var(--base100); +} + +.highlight { + color: var(--base75); + background: var(--base900); + font-weight: 400; +} + +.behind { + color: var(--base400); +} + +.ahead { + color: var(--base400); +} + +.current { + color: var(--base500); } diff --git a/src/app/(main)/reports/journey/JourneyView.tsx b/src/app/(main)/reports/journey/JourneyView.tsx index 6905d74c..ff1941dc 100644 --- a/src/app/(main)/reports/journey/JourneyView.tsx +++ b/src/app/(main)/reports/journey/JourneyView.tsx @@ -1,13 +1,103 @@ -import { useContext } from 'react'; +import { useContext, useMemo, useState } from 'react'; import { ReportContext } from '../[reportId]/Report'; +import { firstBy } from 'thenby'; +import styles from './JourneyView.module.css'; +import classNames from 'classnames'; +import { useEscapeKey } from 'components/hooks'; export default function JourneyView() { + const [selected, setSelected] = useState(null); const { report } = useContext(ReportContext); const { data } = report || {}; + useEscapeKey(() => setSelected(null)); + + const columns = useMemo(() => { + if (!data) { + return []; + } + return Array(data[0].items.length) + .fill(undefined) + .map((col = {}, index) => { + data.forEach(({ items, count }) => { + const item = items[index]; + + if (item) { + if (!col[item]) { + col[item] = { + item, + total: +count, + index, + paths: [ + data.filter((d, i) => { + return d.items[index] === item && i !== index; + }), + ], + }; + } else { + col[item].total += +count; + } + } + }); + + return Object.keys(col) + .map(key => col[key]) + .sort(firstBy('total', -1)); + }); + }, [data]); + + const handleClick = (item: string, index: number, paths: any[]) => { + if (item !== selected?.item || index !== selected?.index) { + setSelected({ item, index, paths }); + } else { + setSelected(null); + } + }; if (!data) { return null; } - return
{JSON.stringify(data)}
; + return ( +
+
+ {columns.map((column, index) => { + const current = index === selected?.index; + const behind = index <= selected?.index - 1; + const ahead = index > selected?.index; + + return ( +
+
+
{index + 1}
+
+ {column.map(({ item, total, paths }) => { + const highlight = selected?.paths.find(arr => { + return arr.find(a => a.items[index] === item); + }); + + return ( +
handleClick(item, index, paths)} + > + {item} ({total}) +
+ ); + })} +
+ ); + })} +
+
+ ); } diff --git a/src/queries/analytics/reports/getJourney.ts b/src/queries/analytics/reports/getJourney.ts index 088f7ee8..a02bf9bf 100644 --- a/src/queries/analytics/reports/getJourney.ts +++ b/src/queries/analytics/reports/getJourney.ts @@ -2,6 +2,15 @@ import clickhouse from 'lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import prisma from 'lib/prisma'; +interface JourneyResult { + e1: string; + e2: string; + e3: string; + e4: string; + e5: string; + count: string; +} + export async function getJourney( ...args: [ websiteId: string, @@ -23,16 +32,7 @@ async function relationalQuery( startDate: Date; endDate: Date; }, -): Promise< - { - e1: string; - e2: string; - e3: string; - e4: string; - e5: string; - count: string; - }[] -> { +): Promise { const { startDate, endDate } = filters; const { rawQuery } = prisma; @@ -79,7 +79,7 @@ async function relationalQuery( startDate, endDate, }, - ); + ).then(parseResult); } async function clickhouseQuery( @@ -88,16 +88,7 @@ async function clickhouseQuery( startDate: Date; endDate: Date; }, -): Promise< - { - e1: string; - e2: string; - e3: string; - e4: string; - e5: string; - count: string; - }[] -> { +): Promise { const { startDate, endDate } = filters; const { rawQuery } = clickhouse; @@ -144,5 +135,9 @@ async function clickhouseQuery( startDate, endDate, }, - ); + ).then(parseResult); +} + +function parseResult(data: any) { + return data.map(({ e1, e2, e3, e4, e5, count }) => ({ items: [e1, e2, e3, e4, e5], count })); }