Merge branch 'dev' into analytics

This commit is contained in:
Mike Cao 2024-06-01 14:10:02 -07:00
commit 5a4a9b6472
12 changed files with 204 additions and 60 deletions

View file

@ -60,7 +60,7 @@ export function ReportHeader({ icon }) {
<div className={styles.type}> <div className={styles.type}>
<Breadcrumb <Breadcrumb
data={[ data={[
{ label: formatMessage(labels.reports), url: '/reports' }, { label: formatMessage(labels.reports), url: renderTeamUrl('/reports') },
{ {
label: formatMessage( label: formatMessage(
labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === report?.type)], labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === report?.type)],

View file

@ -6,6 +6,7 @@ import Lightbulb from 'assets/lightbulb.svg';
import Magnet from 'assets/magnet.svg'; import Magnet from 'assets/magnet.svg';
import Tag from 'assets/tag.svg'; import Tag from 'assets/tag.svg';
import Target from 'assets/target.svg'; import Target from 'assets/target.svg';
import Path from 'assets/path.svg';
import styles from './ReportTemplates.module.css'; import styles from './ReportTemplates.module.css';
import { useMessages, useTeamUrl } from 'components/hooks'; import { useMessages, useTeamUrl } from 'components/hooks';
@ -44,6 +45,12 @@ export function ReportTemplates({ showHeader = true }: { showHeader?: boolean })
url: renderTeamUrl('/reports/goals'), url: renderTeamUrl('/reports/goals'),
icon: <Target />, icon: <Target />,
}, },
{
title: formatMessage(labels.journey),
description: formatMessage(labels.journeyDescription),
url: renderTeamUrl('/reports/journey'),
icon: <Path />,
},
]; ];
return ( return (

View file

@ -1,14 +1,74 @@
.title { .container {
font-size: 24px; height: 900px;
line-height: 36px; position: relative;
font-weight: 700;
} }
.row { .view {
display: grid; position: absolute;
grid-template-columns: 50% 50%; top: 0;
gap: 20px; left: 0;
border-bottom: 1px solid var(--base300); right: 0;
padding-bottom: 30px; bottom: 0;
margin-bottom: 30px; 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);
} }

View file

@ -1,13 +1,103 @@
import { useContext } from 'react'; import { useContext, useMemo, useState } from 'react';
import { ReportContext } from '../[reportId]/Report'; 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() { export default function JourneyView() {
const [selected, setSelected] = useState(null);
const { report } = useContext(ReportContext); const { report } = useContext(ReportContext);
const { data } = report || {}; 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) { if (!data) {
return null; return null;
} }
return <div>{JSON.stringify(data)}</div>; return (
<div className={styles.container}>
<div className={styles.view}>
{columns.map((column, index) => {
const current = index === selected?.index;
const behind = index <= selected?.index - 1;
const ahead = index > selected?.index;
return (
<div
key={index}
className={classNames(styles.column, {
[styles.current]: current,
[styles.behind]: behind,
[styles.ahead]: ahead,
})}
>
<div className={styles.header}>
<div className={styles.num}>{index + 1}</div>
</div>
{column.map(({ item, total, paths }) => {
const highlight = selected?.paths.find(arr => {
return arr.find(a => a.items[index] === item);
});
return (
<div
key={item}
className={classNames(styles.item, {
[styles.highlight]: highlight,
})}
onClick={() => handleClick(item, index, paths)}
>
{item} ({total})
</div>
);
})}
</div>
);
})}
</div>
</div>
);
} }

View file

@ -1,3 +0,0 @@
import Page from 'app/(main)/reports/goals/page';
export default Page;

View file

@ -1,8 +1,11 @@
'use client'; 'use client';
import WebsitesHeader from 'app/(main)/settings/websites/WebsitesHeader'; import WebsitesHeader from 'app/(main)/settings/websites/WebsitesHeader';
import WebsitesDataTable from 'app/(main)/settings/websites/WebsitesDataTable'; import WebsitesDataTable from 'app/(main)/settings/websites/WebsitesDataTable';
import { useTeamUrl } from 'components/hooks';
export default function WebsitesPage() {
const { teamId } = useTeamUrl();
export default function WebsitesPage({ teamId }: { teamId: string }) {
return ( return (
<> <>
<WebsitesHeader teamId={teamId} allowCreate={false} /> <WebsitesHeader teamId={teamId} allowCreate={false} />

View file

@ -1,8 +1,8 @@
import WebsitesPage from './WebsitesPage'; import WebsitesPage from './WebsitesPage';
import { Metadata } from 'next'; import { Metadata } from 'next';
export default function ({ params: { teamId, userId } }) { export default function () {
return <WebsitesPage teamId={teamId} userId={userId} />; return <WebsitesPage />;
} }
export const metadata: Metadata = { export const metadata: Metadata = {

View file

@ -1,3 +0,0 @@
.favicon {
margin-inline-end: 8px;
}

View file

@ -1,5 +1,3 @@
import styles from './Favicon.module.css';
function getHostName(url: string) { function getHostName(url: string) {
const match = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:/\n?=]+)/im); const match = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:/\n?=]+)/im);
return match && match.length > 1 ? match[1] : null; return match && match.length > 1 ? match[1] : null;
@ -14,7 +12,6 @@ export function Favicon({ domain, ...props }) {
return hostName ? ( return hostName ? (
<img <img
className={styles.favicon}
src={`https://icons.duckduckgo.com/ip3/${hostName}.ico`} src={`https://icons.duckduckgo.com/ip3/${hostName}.ico`}
width={16} width={16}
height={16} height={16}

View file

@ -1,23 +1,21 @@
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import FilterLink from 'components/common/FilterLink'; import FilterLink from 'components/common/FilterLink';
import Favicon from 'components/common/Favicon'; import Favicon from 'components/common/Favicon';
import { useMessages } from 'components/hooks'; import { useMessages } from 'components/hooks';
import { Flexbox } from 'react-basics'; import MetricsTable, { MetricsTableProps } from './MetricsTable';
export function ReferrersTable(props: MetricsTableProps) { export function ReferrersTable(props: MetricsTableProps) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const renderLink = ({ x: referrer }) => { const renderLink = ({ x: referrer }) => {
return ( return (
<Flexbox alignItems="center"> <FilterLink
id="referrer"
value={referrer}
externalUrl={`https://${referrer}`}
label={!referrer && formatMessage(labels.none)}
>
<Favicon domain={referrer} /> <Favicon domain={referrer} />
<FilterLink </FilterLink>
id="referrer"
value={referrer}
externalUrl={`https://${referrer}`}
label={!referrer && formatMessage(labels.none)}
/>
</Flexbox>
); );
}; };

View file

@ -2,6 +2,15 @@ import clickhouse from 'lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
interface JourneyResult {
e1: string;
e2: string;
e3: string;
e4: string;
e5: string;
count: string;
}
export async function getJourney( export async function getJourney(
...args: [ ...args: [
websiteId: string, websiteId: string,
@ -23,16 +32,7 @@ async function relationalQuery(
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
}, },
): Promise< ): Promise<JourneyResult[]> {
{
e1: string;
e2: string;
e3: string;
e4: string;
e5: string;
count: string;
}[]
> {
const { startDate, endDate } = filters; const { startDate, endDate } = filters;
const { rawQuery } = prisma; const { rawQuery } = prisma;
@ -79,7 +79,7 @@ async function relationalQuery(
startDate, startDate,
endDate, endDate,
}, },
); ).then(parseResult);
} }
async function clickhouseQuery( async function clickhouseQuery(
@ -88,16 +88,7 @@ async function clickhouseQuery(
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
}, },
): Promise< ): Promise<JourneyResult[]> {
{
e1: string;
e2: string;
e3: string;
e4: string;
e5: string;
count: string;
}[]
> {
const { startDate, endDate } = filters; const { startDate, endDate } = filters;
const { rawQuery } = clickhouse; const { rawQuery } = clickhouse;
@ -144,5 +135,9 @@ async function clickhouseQuery(
startDate, startDate,
endDate, endDate,
}, },
); ).then(parseResult);
}
function parseResult(data: any) {
return data.map(({ e1, e2, e3, e4, e5, count }) => ({ items: [e1, e2, e3, e4, e5], count }));
} }

View file

@ -66,8 +66,8 @@ async function clickhouseQuery(
order by t order by t
`, `,
params, params,
).then(a => { ).then(result => {
return Object.values(a).map(a => { return Object.values(result).map((a: any) => {
return { x: a.x, y: Number(a.y) }; return { x: a.x, y: Number(a.y) };
}); });
}); });