From 37eb157ab5b6e783d833b1edbf1a9743db290dfe Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 16 May 2024 22:24:15 -0700 Subject: [PATCH 01/30] Added bash build script. --- ignore-build-step.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 ignore-build-step.sh diff --git a/ignore-build-step.sh b/ignore-build-step.sh new file mode 100644 index 000000000..da63c205a --- /dev/null +++ b/ignore-build-step.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +echo "VERCEL_GIT_COMMIT_REF: $VERCEL_GIT_COMMIT_REF" + +if [[ "$VERCEL_GIT_COMMIT_REF" != "analytics" ]] ; then + # Proceed with the build + echo "✅ - Build can proceed" + exit 1; + +else + # Don't build + echo "🛑 - Build cancelled" + exit 0; +fi \ No newline at end of file From 76cab03bb231632cfde4290ed578ab937326de45 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 17 May 2024 01:42:36 -0700 Subject: [PATCH 02/30] Bootstrap User Journey report. --- ignore-build-step.sh | 14 -- .../(main)/reports/create/ReportTemplates.tsx | 7 + .../reports/journey/JourneyParameters.tsx | 36 +++++ .../(main)/reports/journey/JourneyReport.tsx | 28 ++++ .../reports/journey/JourneyReportPage.tsx | 5 + .../reports/journey/JourneyView.module.css | 14 ++ .../(main)/reports/journey/JourneyView.tsx | 13 ++ src/app/(main)/reports/journey/page.tsx | 10 ++ src/app/(main)/reports/utm/UTMView.tsx | 1 + .../websites/[websiteId]/WebsiteSettings.tsx | 2 +- src/assets/path.svg | 1 + src/components/charts/Chart.tsx | 4 +- src/components/messages.ts | 7 +- src/lib/constants.ts | 1 + src/pages/api/reports/[reportId].ts | 2 +- src/pages/api/reports/journey.ts | 54 +++++++ src/queries/analytics/reports/getJourney.ts | 148 ++++++++++++++++++ src/queries/index.ts | 1 + 18 files changed, 329 insertions(+), 19 deletions(-) delete mode 100644 ignore-build-step.sh create mode 100644 src/app/(main)/reports/journey/JourneyParameters.tsx create mode 100644 src/app/(main)/reports/journey/JourneyReport.tsx create mode 100644 src/app/(main)/reports/journey/JourneyReportPage.tsx create mode 100644 src/app/(main)/reports/journey/JourneyView.module.css create mode 100644 src/app/(main)/reports/journey/JourneyView.tsx create mode 100644 src/app/(main)/reports/journey/page.tsx create mode 100644 src/assets/path.svg create mode 100644 src/pages/api/reports/journey.ts create mode 100644 src/queries/analytics/reports/getJourney.ts diff --git a/ignore-build-step.sh b/ignore-build-step.sh deleted file mode 100644 index da63c205a..000000000 --- a/ignore-build-step.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -echo "VERCEL_GIT_COMMIT_REF: $VERCEL_GIT_COMMIT_REF" - -if [[ "$VERCEL_GIT_COMMIT_REF" != "analytics" ]] ; then - # Proceed with the build - echo "✅ - Build can proceed" - exit 1; - -else - # Don't build - echo "🛑 - Build cancelled" - exit 0; -fi \ No newline at end of file diff --git a/src/app/(main)/reports/create/ReportTemplates.tsx b/src/app/(main)/reports/create/ReportTemplates.tsx index fdf5c5f52..0777cc1f9 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/JourneyParameters.tsx b/src/app/(main)/reports/journey/JourneyParameters.tsx new file mode 100644 index 000000000..b0544168b --- /dev/null +++ b/src/app/(main)/reports/journey/JourneyParameters.tsx @@ -0,0 +1,36 @@ +import { useContext } from 'react'; +import { useMessages } from 'components/hooks'; +import { Form, FormButtons, SubmitButton } from 'react-basics'; +import { ReportContext } from '../[reportId]/Report'; +import BaseParameters from '../[reportId]/BaseParameters'; + +export function JourneyParameters() { + const { report, runReport, isRunning } = useContext(ReportContext); + const { formatMessage, labels } = useMessages(); + + const { id, parameters } = report || {}; + const { websiteId, dateRange } = parameters || {}; + const queryDisabled = !websiteId || !dateRange; + + const handleSubmit = (data: any, e: any) => { + e.stopPropagation(); + e.preventDefault(); + + if (!queryDisabled) { + runReport(data); + } + }; + + return ( +
+ + + + {formatMessage(labels.runQuery)} + + + + ); +} + +export default JourneyParameters; diff --git a/src/app/(main)/reports/journey/JourneyReport.tsx b/src/app/(main)/reports/journey/JourneyReport.tsx new file mode 100644 index 000000000..7b8927b4d --- /dev/null +++ b/src/app/(main)/reports/journey/JourneyReport.tsx @@ -0,0 +1,28 @@ +'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 'assets/path.svg'; +import { REPORT_TYPES } from 'lib/constants'; + +const defaultParameters = { + type: REPORT_TYPES.journey, + parameters: {}, +}; + +export default function JourneyReport({ reportId }: { reportId?: string }) { + return ( + + } /> + + + + + + + + ); +} diff --git a/src/app/(main)/reports/journey/JourneyReportPage.tsx b/src/app/(main)/reports/journey/JourneyReportPage.tsx new file mode 100644 index 000000000..0f4b78cad --- /dev/null +++ b/src/app/(main)/reports/journey/JourneyReportPage.tsx @@ -0,0 +1,5 @@ +import JourneyReport from './JourneyReport'; + +export default function JourneyReportPage() { + return ; +} diff --git a/src/app/(main)/reports/journey/JourneyView.module.css b/src/app/(main)/reports/journey/JourneyView.module.css new file mode 100644 index 000000000..fa7cc0b42 --- /dev/null +++ b/src/app/(main)/reports/journey/JourneyView.module.css @@ -0,0 +1,14 @@ +.title { + font-size: 24px; + line-height: 36px; + font-weight: 700; +} + +.row { + display: grid; + grid-template-columns: 50% 50%; + gap: 20px; + border-bottom: 1px solid var(--base300); + padding-bottom: 30px; + margin-bottom: 30px; +} diff --git a/src/app/(main)/reports/journey/JourneyView.tsx b/src/app/(main)/reports/journey/JourneyView.tsx new file mode 100644 index 000000000..6905d74cb --- /dev/null +++ b/src/app/(main)/reports/journey/JourneyView.tsx @@ -0,0 +1,13 @@ +import { useContext } from 'react'; +import { ReportContext } from '../[reportId]/Report'; + +export default function JourneyView() { + const { report } = useContext(ReportContext); + const { data } = report || {}; + + if (!data) { + return null; + } + + return
{JSON.stringify(data)}
; +} diff --git a/src/app/(main)/reports/journey/page.tsx b/src/app/(main)/reports/journey/page.tsx new file mode 100644 index 000000000..447747cc3 --- /dev/null +++ b/src/app/(main)/reports/journey/page.tsx @@ -0,0 +1,10 @@ +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/utm/UTMView.tsx b/src/app/(main)/reports/utm/UTMView.tsx index e59b60ebe..f10a68d8c 100644 --- a/src/app/(main)/reports/utm/UTMView.tsx +++ b/src/app/(main)/reports/utm/UTMView.tsx @@ -34,6 +34,7 @@ export default function UTMView() { { data: items.map(({ value }) => value), backgroundColor: CHART_COLORS, + borderWidth: 0, }, ], }; diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx index 1a92f1f11..11f662b19 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx @@ -61,7 +61,7 @@ export function WebsiteSettings({ {tab === 'details' && } {tab === 'tracking' && } - {tab === 'share' && } + {tab === 'share' && } {tab === 'data' && } ); diff --git a/src/assets/path.svg b/src/assets/path.svg new file mode 100644 index 000000000..29501565e --- /dev/null +++ b/src/assets/path.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/charts/Chart.tsx b/src/components/charts/Chart.tsx index 40829cac0..d5d22874b 100644 --- a/src/components/charts/Chart.tsx +++ b/src/components/charts/Chart.tsx @@ -1,7 +1,7 @@ import { useState, useRef, useEffect, useMemo, ReactNode } from 'react'; import { Loading } from 'react-basics'; import classNames from 'classnames'; -import ChartJS, { LegendItem } from 'chart.js/auto'; +import ChartJS, { LegendItem, ChartOptions } from 'chart.js/auto'; import HoverTooltip from 'components/common/HoverTooltip'; import Legend from 'components/metrics/Legend'; import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; @@ -17,7 +17,7 @@ export interface ChartProps { onUpdate?: (chart: any) => void; onTooltip?: (model: any) => void; className?: string; - chartOptions?: { [key: string]: any }; + chartOptions?: ChartOptions; tooltip?: ReactNode; } diff --git a/src/components/messages.ts b/src/components/messages.ts index 1413549fd..238ebf524 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -239,9 +239,14 @@ export const labels = defineMessages({ goals: { id: 'label.goals', defaultMessage: 'Goals' }, goalsDescription: { id: 'label.goals-description', - defaultMessage: 'Track your goals for pageviews or events.', + defaultMessage: 'Track your goals for pageviews and events.', }, count: { id: 'label.count', defaultMessage: 'Count' }, + journey: { id: 'label.journey', defaultMessage: 'Journey' }, + journeyDescription: { + id: 'label.journey-description', + defaultMessage: 'Understand how users nagivate through your website.', + }, }); export const messages = defineMessages({ diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 697a48367..038626606 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -115,6 +115,7 @@ export const REPORT_TYPES = { insights: 'insights', retention: 'retention', utm: 'utm', + journey: 'journey', } as const; export const REPORT_PARAMETERS = { diff --git a/src/pages/api/reports/[reportId].ts b/src/pages/api/reports/[reportId].ts index be2db82f5..3a7c4c536 100644 --- a/src/pages/api/reports/[reportId].ts +++ b/src/pages/api/reports/[reportId].ts @@ -27,7 +27,7 @@ const schema: YupRequest = { websiteId: yup.string().uuid().required(), type: yup .string() - .matches(/funnel|insights|retention|utm|goals/i) + .matches(/funnel|insights|retention|utm|goals|journey/i) .required(), name: yup.string().max(200).required(), description: yup.string().max(500), diff --git a/src/pages/api/reports/journey.ts b/src/pages/api/reports/journey.ts new file mode 100644 index 000000000..84246f05b --- /dev/null +++ b/src/pages/api/reports/journey.ts @@ -0,0 +1,54 @@ +import { canViewWebsite } from 'lib/auth'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; +import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getJourney } from 'queries'; +import * as yup from 'yup'; + +export interface RetentionRequestBody { + websiteId: string; + dateRange: { startDate: string; endDate: string }; +} + +const schema = { + POST: yup.object().shape({ + websiteId: yup.string().uuid().required(), + dateRange: yup + .object() + .shape({ + startDate: yup.date().required(), + endDate: yup.date().required(), + }) + .required(), + }), +}; + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useCors(req, res); + await useAuth(req, res); + await useValidate(schema, req, res); + + if (req.method === 'POST') { + const { + websiteId, + dateRange: { startDate, endDate }, + } = req.body; + + if (!(await canViewWebsite(req.auth, websiteId))) { + return unauthorized(res); + } + + const data = await getJourney(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + }); + + return ok(res, data); + } + + return methodNotAllowed(res); +}; diff --git a/src/queries/analytics/reports/getJourney.ts b/src/queries/analytics/reports/getJourney.ts new file mode 100644 index 000000000..088f7ee88 --- /dev/null +++ b/src/queries/analytics/reports/getJourney.ts @@ -0,0 +1,148 @@ +import clickhouse from 'lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; +import prisma from 'lib/prisma'; + +export async function getJourney( + ...args: [ + websiteId: string, + filters: { + startDate: Date; + endDate: Date; + }, + ] +) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId: string, + filters: { + startDate: Date; + endDate: Date; + }, +): Promise< + { + e1: string; + e2: string; + e3: string; + e4: string; + e5: string; + count: string; + }[] +> { + const { startDate, endDate } = filters; + const { rawQuery } = prisma; + + return rawQuery( + ` + WITH events AS ( + select distinct + session_id, + referrer_path, + COALESCE(event_name, url_path) event, + ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY created_at) AS event_number + from website_event + where website_id = {{websiteId::uuid}} + and created_at between {{startDate}} and {{endDate}} + and referrer_path != url_path), + sequences as ( + SELECT s.e1, + s.e2, + s.e3, + s.e4, + s.e5, + count(*) count + FROM ( + SELECT session_id, + MAX(CASE WHEN event_number = 1 THEN event ELSE NULL END) AS e1, + MAX(CASE WHEN event_number = 2 THEN event ELSE NULL END) AS e2, + MAX(CASE WHEN event_number = 3 THEN event ELSE NULL END) AS e3, + MAX(CASE WHEN event_number = 4 THEN event ELSE NULL END) AS e4, + MAX(CASE WHEN event_number = 5 THEN event ELSE NULL END) AS e5 + FROM events + group by session_id) s + group by s.e1, + s.e2, + s.e3, + s.e4, + s.e5) + select * + from sequences + order by count desc + limit 100 + `, + { + websiteId, + startDate, + endDate, + }, + ); +} + +async function clickhouseQuery( + websiteId: string, + filters: { + startDate: Date; + endDate: Date; + }, +): Promise< + { + e1: string; + e2: string; + e3: string; + e4: string; + e5: string; + count: string; + }[] +> { + const { startDate, endDate } = filters; + const { rawQuery } = clickhouse; + + return rawQuery( + ` + WITH events AS ( + select distinct + session_id, + referrer_path, + coalesce(nullIf(event_name, ''), url_path) event, + row_number() OVER (PARTITION BY session_id ORDER BY created_at) AS event_number + from umami.website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and referrer_path != url_path), + sequences as ( + SELECT s.e1, + s.e2, + s.e3, + s.e4, + s.e5, + count(*) count + FROM ( + SELECT session_id, + max(CASE WHEN event_number = 1 THEN event ELSE NULL END) AS e1, + max(CASE WHEN event_number = 2 THEN event ELSE NULL END) AS e2, + max(CASE WHEN event_number = 3 THEN event ELSE NULL END) AS e3, + max(CASE WHEN event_number = 4 THEN event ELSE NULL END) AS e4, + max(CASE WHEN event_number = 5 THEN event ELSE NULL END) AS e5 + FROM events + group by session_id) s + group by s.e1, + s.e2, + s.e3, + s.e4, + s.e5) + select * + from sequences + order by count desc + limit 100 + `, + { + websiteId, + startDate, + endDate, + }, + ); +} diff --git a/src/queries/index.ts b/src/queries/index.ts index f0002881b..8cef080aa 100644 --- a/src/queries/index.ts +++ b/src/queries/index.ts @@ -12,6 +12,7 @@ export * from './analytics/eventData/getEventDataStats'; export * from './analytics/eventData/getEventDataUsage'; export * from './analytics/events/saveEvent'; export * from './analytics/reports/getFunnel'; +export * from './analytics/reports/getJourney'; export * from './analytics/reports/getRetention'; export * from './analytics/reports/getInsights'; export * from './analytics/reports/getUTM'; From 79a93ed9fc3e99fce9f89456796acb9bd4d0c0d5 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 20 May 2024 21:10:46 -0700 Subject: [PATCH 03/30] add event_data to goal. --- src/app/(main)/reports/goals/GoalsAddForm.tsx | 54 +++++++++++- src/app/(main)/reports/goals/GoalsChart.tsx | 27 ++++-- .../reports/goals/GoalsParameters.module.css | 11 +++ .../(main)/reports/goals/GoalsParameters.tsx | 75 ++++++++++------ src/components/messages.ts | 11 ++- src/pages/api/reports/goals.ts | 16 +++- src/queries/analytics/reports/getGoals.ts | 85 ++++++++++++++++--- 7 files changed, 231 insertions(+), 48 deletions(-) diff --git a/src/app/(main)/reports/goals/GoalsAddForm.tsx b/src/app/(main)/reports/goals/GoalsAddForm.tsx index a8a77c58e..a82eea28c 100644 --- a/src/app/(main)/reports/goals/GoalsAddForm.tsx +++ b/src/app/(main)/reports/goals/GoalsAddForm.tsx @@ -6,27 +6,48 @@ import styles from './GoalsAddForm.module.css'; export function GoalsAddForm({ type: defaultType = 'url', value: defaultValue = '', + property: defaultProperty = '', + operator: defaultAggregae = null, goal: defaultGoal = 10, onChange, }: { type?: string; value?: string; + operator?: string; + property?: string; goal?: number; - onChange?: (step: { type: string; value: string; goal: number }) => void; + onChange?: (step: { + type: string; + value: string; + goal: number; + operator?: string; + property?: string; + }) => void; }) { const [type, setType] = useState(defaultType); const [value, setValue] = useState(defaultValue); + const [operator, setOperator] = useState(defaultAggregae); + const [property, setProperty] = useState(defaultProperty); const [goal, setGoal] = useState(defaultGoal); const { formatMessage, labels } = useMessages(); const items = [ { label: formatMessage(labels.url), value: 'url' }, { label: formatMessage(labels.event), value: 'event' }, + { label: formatMessage(labels.eventData), value: 'event-data' }, + ]; + const operators = [ + { label: formatMessage(labels.count), value: 'count' }, + { label: formatMessage(labels.average), value: 'average' }, + { label: formatMessage(labels.sum), value: 'sum' }, ]; const isDisabled = !type || !value; const handleSave = () => { - onChange({ type, value, goal }); + onChange( + type === 'event-data' ? { type, value, goal, operator, property } : { type, value, goal }, + ); setValue(''); + setProperty(''); setGoal(10); }; @@ -45,6 +66,10 @@ export function GoalsAddForm({ return items.find(item => item.value === value)?.label; }; + const renderoperatorValue = (value: any) => { + return operators.find(item => item.value === value)?.label; + }; + return ( @@ -70,6 +95,31 @@ export function GoalsAddForm({ /> + {type === 'event-data' && ( + + + setOperator(value)} + > + {({ value, label }) => { + return {label}; + }} + + handleChange(e, setProperty)} + autoFocus={true} + autoComplete="off" + onKeyDown={handleKeyDown} + /> + + + )} { + let label = ''; + switch (type) { + case 'url': + label = labels.viewedPage; + break; + case 'event': + label = labels.triggeredEvent; + break; + default: + label = labels.collectedData; + break; + } + + return label; + }; + return (
- {data?.map(({ type, value, goal, result }, index: number) => { + {data?.map(({ type, value, goal, result, property, operator }, index: number) => { const percent = result > goal ? 100 : (result / goal) * 100; return (
- - {formatMessage(type === 'url' ? labels.viewedPage : labels.triggeredEvent)} - - {value} + {formatMessage(getLabel(type))} + {`${value}${ + type === 'event-data' ? `:(${operator}):${property}` : '' + }`}
}> - {goals.map((goal: { type: string; value: string; goal: number }, index: number) => { - return ( - - : } - onRemove={() => handleRemoveGoals(index)} - > -
{goal.value}
-
- {formatMessage(labels.goal)}: {formatNumber(goal.goal)} -
-
- - {(close: () => void) => ( - - - - )} - -
- ); - })} + {goals.map( + ( + goal: { + type: string; + value: string; + goal: number; + operator?: string; + property?: string; + }, + index: number, + ) => { + return ( + + : } + onRemove={() => handleRemoveGoals(index)} + > + +
{goal.value}
+ {goal.type === 'event-data' && ( +
+ {formatMessage(labels[goal.operator])}: {goal.property} +
+ )} +
+ {formatMessage(labels.goal)}: {formatNumber(goal.goal)} +
+
+
+ + {(close: () => void) => ( + + + + )} + +
+ ); + }, + )}
diff --git a/src/components/messages.ts b/src/components/messages.ts index 238ebf524..084484765 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -95,6 +95,9 @@ export const labels = defineMessages({ devices: { id: 'label.devices', defaultMessage: 'Devices' }, countries: { id: 'label.countries', defaultMessage: 'Countries' }, languages: { id: 'label.languages', defaultMessage: 'Languages' }, + count: { id: 'label.count', defaultMessage: 'Count' }, + average: { id: 'label.average', defaultMessage: 'Average' }, + sum: { id: 'label.sum', defaultMessage: 'Sum' }, event: { id: 'label.event', defaultMessage: 'Event' }, events: { id: 'label.events', defaultMessage: 'Events' }, query: { id: 'label.query', defaultMessage: 'Query' }, @@ -107,6 +110,7 @@ export const labels = defineMessages({ views: { id: 'label.views', defaultMessage: 'Views' }, none: { id: 'label.none', defaultMessage: 'None' }, clearAll: { id: 'label.clear-all', defaultMessage: 'Clear all' }, + property: { id: 'label.property', defaultMessage: 'Property' }, today: { id: 'label.today', defaultMessage: 'Today' }, lastHours: { id: 'label.last-hours', defaultMessage: 'Last {x} hours' }, yesterday: { id: 'label.yesterday', defaultMessage: 'Yesterday' }, @@ -178,8 +182,6 @@ export const labels = defineMessages({ before: { id: 'label.before', defaultMessage: 'Before' }, after: { id: 'label.after', defaultMessage: 'After' }, total: { id: 'label.total', defaultMessage: 'Total' }, - sum: { id: 'label.sum', defaultMessage: 'Sum' }, - average: { id: 'label.average', defaultMessage: 'Average' }, min: { id: 'label.min', defaultMessage: 'Min' }, max: { id: 'label.max', defaultMessage: 'Max' }, unique: { id: 'label.unique', defaultMessage: 'Unique' }, @@ -220,6 +222,10 @@ export const labels = defineMessages({ id: 'message.viewed-page', defaultMessage: 'Viewed page', }, + collectedData: { + id: 'message.collected-data', + defaultMessage: 'Collected data', + }, triggeredEvent: { id: 'message.triggered-event', defaultMessage: 'Triggered event', @@ -241,7 +247,6 @@ export const labels = defineMessages({ id: 'label.goals-description', defaultMessage: 'Track your goals for pageviews and events.', }, - count: { id: 'label.count', defaultMessage: 'Count' }, journey: { id: 'label.journey', defaultMessage: 'Journey' }, journeyDescription: { id: 'label.journey-description', diff --git a/src/pages/api/reports/goals.ts b/src/pages/api/reports/goals.ts index bb7667751..f775dc3c8 100644 --- a/src/pages/api/reports/goals.ts +++ b/src/pages/api/reports/goals.ts @@ -28,9 +28,23 @@ const schema = { .array() .of( yup.object().shape({ - type: yup.string().required(), + type: yup + .string() + .matches(/url|event|event-data/i) + .required(), value: yup.string().required(), goal: yup.number().required(), + operator: yup + .string() + .matches(/count|sum|average/i) + .when('type', { + is: 'eventData', + then: yup.string().required(), + }), + property: yup.string().when('type', { + is: 'eventData', + then: yup.string().required(), + }), }), ) .min(1) diff --git a/src/queries/analytics/reports/getGoals.ts b/src/queries/analytics/reports/getGoals.ts index d26998d03..66c1d9518 100644 --- a/src/queries/analytics/reports/getGoals.ts +++ b/src/queries/analytics/reports/getGoals.ts @@ -8,7 +8,7 @@ export async function getGoals( criteria: { startDate: Date; endDate: Date; - goals: { type: string; value: string; goal: number }[]; + goals: { type: string; value: string; goal: number; operator?: string }[]; }, ] ) { @@ -23,7 +23,7 @@ async function relationalQuery( criteria: { startDate: Date; endDate: Date; - goals: { type: string; value: string; goal: number }[]; + goals: { type: string; value: string; goal: number; operator?: string }[]; }, ): Promise { const { startDate, endDate, goals } = criteria; @@ -119,7 +119,7 @@ async function clickhouseQuery( criteria: { startDate: Date; endDate: Date; - goals: { type: string; value: string; goal: number }[]; + goals: { type: string; value: string; goal: number; operator?: string; property?: string }[]; }, ): Promise<{ type: string; value: string; goal: number; result: number }[]> { const { startDate, endDate, goals } = criteria; @@ -127,13 +127,22 @@ async function clickhouseQuery( const urls = goals.filter(a => a.type === 'url'); const events = goals.filter(a => a.type === 'event'); + const eventData = goals.filter(a => a.type === 'event-data'); const hasUrl = urls.length > 0; const hasEvent = events.length > 0; + const hasEventData = eventData.length > 0; function getParameters( urls: { type: string; value: string; goal: number }[], events: { type: string; value: string; goal: number }[], + eventData: { + type: string; + value: string; + goal: number; + operator?: string; + property?: string; + }[], ) { const urlParam = urls.reduce((acc, cv, i) => { acc[`${cv.type}${i}`] = cv.value; @@ -145,41 +154,77 @@ async function clickhouseQuery( return acc; }, {}); + const eventDataParam = eventData.reduce((acc, cv, i) => { + acc[`eventData${i}`] = cv.value; + acc[`property${i}`] = cv.property; + return acc; + }, {}); + return { urls: { ...urlParam, startDate, endDate, websiteId }, events: { ...eventParam, startDate, endDate, websiteId }, + eventData: { ...eventDataParam, startDate, endDate, websiteId }, }; } function getColumns( urls: { type: string; value: string; goal: number }[], events: { type: string; value: string; goal: number }[], + eventData: { + type: string; + value: string; + goal: number; + operator?: string; + property?: string; + }[], ) { const urlColumns = urls .map((a, i) => `countIf(url_path = {url${i}:String}) AS URL${i},`) .join('\n') .slice(0, -1); const eventColumns = events - .map((a, i) => `countIf(event_name = {event${i}:String}) AS EVENT${i}`) + .map((a, i) => `countIf(event_name = {event${i}:String}) AS EVENT${i},`) + .join('\n') + .slice(0, -1); + const eventDataColumns = eventData + .map( + (a, i) => + `${a.operator === 'average' ? 'avg' : a.operator}If(${ + a.operator !== 'count' ? 'number_value, ' : '' + }event_name = {eventData${i}:String} AND data_key = {property${i}:String}) AS EVENT_DATA${i},`, + ) .join('\n') .slice(0, -1); - return { url: urlColumns, events: eventColumns }; + return { url: urlColumns, events: eventColumns, eventData: eventDataColumns }; } function getWhere( urls: { type: string; value: string; goal: number }[], events: { type: string; value: string; goal: number }[], + eventData: { + type: string; + value: string; + goal: number; + operator?: string; + property?: string; + }[], ) { const urlWhere = urls.map((a, i) => `{url${i}:String}`).join(','); const eventWhere = events.map((a, i) => `{event${i}:String}`).join(','); + const eventDataNameWhere = eventData.map((a, i) => `{eventData${i}:String}`).join(','); + const eventDataKeyWhere = eventData.map((a, i) => `{property${i}:String}`).join(','); - return { urls: `and url_path in (${urlWhere})`, events: `and event_name in (${eventWhere})` }; + return { + urls: `and url_path in (${urlWhere})`, + events: `and event_name in (${eventWhere})`, + eventData: `and event_name in (${eventDataNameWhere}) and data_key in (${eventDataKeyWhere})`, + }; } - const parameters = getParameters(urls, events); - const columns = getColumns(urls, events); - const where = getWhere(urls, events); + const parameters = getParameters(urls, events, eventData); + const columns = getColumns(urls, events, eventData); + const where = getWhere(urls, events, eventData); const urlResults = hasUrl ? await rawQuery( @@ -221,5 +266,25 @@ async function clickhouseQuery( }) : []; - return [...urlResults, ...eventResults]; + const eventDataResults = hasEventData + ? await rawQuery( + ` + select + ${columns.eventData} + from event_data + where website_id = {websiteId:UUID} + ${where.eventData} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + `, + parameters.eventData, + ).then(a => { + const results = a[0]; + + return Object.keys(results).map((key, i) => { + return { ...eventData[i], goal: Number(eventData[i].goal), result: Number(results[key]) }; + }); + }) + : []; + + return [...urlResults, ...eventResults, ...eventDataResults]; } From 5c9abe966bddc12964e78bd938c28bbc3c012193 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 20 May 2024 23:58:40 -0700 Subject: [PATCH 04/30] add psql. --- src/queries/analytics/reports/getGoals.ts | 181 ++++++++++++++++------ 1 file changed, 134 insertions(+), 47 deletions(-) diff --git a/src/queries/analytics/reports/getGoals.ts b/src/queries/analytics/reports/getGoals.ts index 66c1d9518..cecdd10c6 100644 --- a/src/queries/analytics/reports/getGoals.ts +++ b/src/queries/analytics/reports/getGoals.ts @@ -29,89 +29,176 @@ async function relationalQuery( const { startDate, endDate, goals } = criteria; const { rawQuery } = prisma; - const hasUrl = goals.some(a => a.type === 'url'); - const hasEvent = goals.some(a => a.type === 'event'); + const urls = goals.filter(a => a.type === 'url'); + const events = goals.filter(a => a.type === 'event'); + const eventData = goals.filter(a => a.type === 'event-data'); - function getParameters(goals: { type: string; value: string; goal: number }[]) { - const urls = goals - .filter(a => a.type === 'url') - .reduce((acc, cv, i) => { - acc[`${cv.type}${i}`] = cv.value; - return acc; - }, {}); + const hasUrl = urls.length > 0; + const hasEvent = events.length > 0; + const hasEventData = eventData.length > 0; - const events = goals - .filter(a => a.type === 'event') - .reduce((acc, cv, i) => { - acc[`${cv.type}${i}`] = cv.value; - return acc; - }, {}); + function getParameters( + urls: { type: string; value: string; goal: number }[], + events: { type: string; value: string; goal: number }[], + eventData: { + type: string; + value: string; + goal: number; + operator?: string; + property?: string; + }[], + ) { + const urlParam = urls.reduce((acc, cv, i) => { + acc[`${cv.type}${i}`] = cv.value; + return acc; + }, {}); + + const eventParam = events.reduce((acc, cv, i) => { + acc[`${cv.type}${i}`] = cv.value; + return acc; + }, {}); + + const eventDataParam = eventData.reduce((acc, cv, i) => { + acc[`eventData${i}`] = cv.value; + acc[`property${i}`] = cv.property; + acc[`eventData${i + 999}`] = cv.value; + acc[`property${i + 999}`] = cv.property; + return acc; + }, {}); return { - urls: { ...urls, startDate, endDate, websiteId }, - events: { ...events, startDate, endDate, websiteId }, + urls: { ...urlParam, startDate, endDate, websiteId }, + events: { ...eventParam, startDate, endDate, websiteId }, + eventData: { ...eventDataParam, startDate, endDate, websiteId }, }; } - function getColumns(goals: { type: string; value: string; goal: number }[]) { - const urls = goals - .filter(a => a.type === 'url') - .map((a, i) => `COUNT(CASE WHEN url_path = {{url${i}}} THEN 1 END) AS URL${i}`) - .join('\n'); - const events = goals - .filter(a => a.type === 'event') - .map((a, i) => `COUNT(CASE WHEN url_path = {{event${i}}} THEN 1 END) AS EVENT${i}`) - .join('\n'); + function getColumns( + urls: { type: string; value: string; goal: number }[], + events: { type: string; value: string; goal: number }[], + eventData: { + type: string; + value: string; + goal: number; + operator?: string; + property?: string; + }[], + ) { + const urlColumns = urls + .map((a, i) => `COUNT(CASE WHEN url_path = {{url${i}}} THEN 1 END) AS URL${i},`) + .join('\n') + .slice(0, -1); + const eventColumns = events + .map((a, i) => `COUNT(CASE WHEN url_path = {{event${i}}} THEN 1 END) AS EVENT${i},`) + .join('\n') + .slice(0, -1); + const eventDataColumns = eventData + .map( + (a, i) => + `${ + a.operator === 'average' ? 'avg' : a.operator + }(CASE WHEN event_name = {{eventData${i}}} AND data_key = {{property${i}}} THEN ${ + a.operator === 'count' ? '1' : 'number_value' + } END) AS EVENT_DATA${i},`, + ) + .join('\n') + .slice(0, -1); - return { urls, events }; + return { urls: urlColumns, events: eventColumns, eventData: eventDataColumns }; } - function getWhere(goals: { type: string; value: string; goal: number }[]) { - const urls = goals - .filter(a => a.type === 'url') - .map((a, i) => `{{url${i}}}`) - .join(','); - const events = goals - .filter(a => a.type === 'event') - .map((a, i) => `{{event${i}}}`) - .join(','); + function getWhere( + urls: { type: string; value: string; goal: number }[], + events: { type: string; value: string; goal: number }[], + eventData: { + type: string; + value: string; + goal: number; + operator?: string; + property?: string; + }[], + ) { + const urlWhere = urls.map((a, i) => `{{url${i}}}`).join(','); + const eventWhere = events.map((a, i) => `{{event${i}}}`).join(','); + const eventDataNameWhere = eventData.map((a, i) => `{{eventData${i + 999}}}`).join(','); + const eventDataKeyWhere = eventData.map((a, i) => `{{property${i + 999}}}`).join(','); - return { urls: `and url_path in (${urls})`, events: `and event_name in (${events})` }; + return { + urls: `and url_path in (${urlWhere})`, + events: `and event_name in (${eventWhere})`, + eventData: `and event_name in (${eventDataNameWhere}) and data_key in (${eventDataKeyWhere})`, + }; } - const parameters = getParameters(goals); - const columns = getColumns(goals); - const where = getWhere(goals); + const parameters = getParameters(urls, events, eventData); + const columns = getColumns(urls, events, eventData); + const where = getWhere(urls, events, eventData); - const urls = hasUrl + const urlResults = hasUrl ? await rawQuery( ` select ${columns.urls} from website_event - where websiteId = {{websiteId::uuid}} + where website_id = {{websiteId::uuid}} ${where.urls} and created_at between {{startDate}} and {{endDate}} `, parameters.urls, - ) + ).then(a => { + const results = a[0]; + + return Object.keys(results).map((key, i) => ({ + ...urls[i], + goal: Number(urls[i].goal), + result: Number(results[key]), + })); + }) : []; - const events = hasEvent + const eventResults = hasEvent ? await rawQuery( ` select ${columns.events} from website_event - where websiteId = {{websiteId::uuid}} + where website_id = {{websiteId::uuid}} ${where.events} and created_at between {{startDate}} and {{endDate}} `, parameters.events, - ) + ).then(a => { + const results = a[0]; + + return Object.keys(results).map((key, i) => { + return { ...events[i], goal: Number(events[i].goal), result: Number(results[key]) }; + }); + }) : []; - return [...urls, ...events]; + const eventDataResults = hasEventData + ? await rawQuery( + ` + select + ${columns.eventData} + from website_event w + join event_data d + on d.website_event_id = w.event_id + where w.website_id = {{websiteId::uuid}} + ${where.eventData} + and w.created_at between {{startDate}} and {{endDate}} + `, + parameters.eventData, + ).then(a => { + const results = a[0]; + + return Object.keys(results).map((key, i) => { + return { ...eventData[i], goal: Number(eventData[i].goal), result: Number(results[key]) }; + }); + }) + : []; + + return [...urlResults, ...eventResults, ...eventDataResults]; } async function clickhouseQuery( From 7add6d583a1296f3d4be273aed81a0ddd290020a Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Tue, 21 May 2024 10:44:27 -0700 Subject: [PATCH 05/30] fix params --- src/queries/analytics/reports/getGoals.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/queries/analytics/reports/getGoals.ts b/src/queries/analytics/reports/getGoals.ts index cecdd10c6..21ab2f7f2 100644 --- a/src/queries/analytics/reports/getGoals.ts +++ b/src/queries/analytics/reports/getGoals.ts @@ -61,8 +61,6 @@ async function relationalQuery( const eventDataParam = eventData.reduce((acc, cv, i) => { acc[`eventData${i}`] = cv.value; acc[`property${i}`] = cv.property; - acc[`eventData${i + 999}`] = cv.value; - acc[`property${i + 999}`] = cv.property; return acc; }, {}); @@ -120,8 +118,8 @@ async function relationalQuery( ) { const urlWhere = urls.map((a, i) => `{{url${i}}}`).join(','); const eventWhere = events.map((a, i) => `{{event${i}}}`).join(','); - const eventDataNameWhere = eventData.map((a, i) => `{{eventData${i + 999}}}`).join(','); - const eventDataKeyWhere = eventData.map((a, i) => `{{property${i + 999}}}`).join(','); + const eventDataNameWhere = eventData.map((a, i) => `{{eventData${i}}}`).join(','); + const eventDataKeyWhere = eventData.map((a, i) => `{{property${i}}}`).join(','); return { urls: `and url_path in (${urlWhere})`, From 8e00a278db2788bf01822a480de1e3d0e2f43074 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Tue, 21 May 2024 11:14:38 -0700 Subject: [PATCH 06/30] fix eent query --- src/queries/analytics/reports/getGoals.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/queries/analytics/reports/getGoals.ts b/src/queries/analytics/reports/getGoals.ts index 21ab2f7f2..83b0ce971 100644 --- a/src/queries/analytics/reports/getGoals.ts +++ b/src/queries/analytics/reports/getGoals.ts @@ -87,7 +87,7 @@ async function relationalQuery( .join('\n') .slice(0, -1); const eventColumns = events - .map((a, i) => `COUNT(CASE WHEN url_path = {{event${i}}} THEN 1 END) AS EVENT${i},`) + .map((a, i) => `COUNT(CASE WHEN event_name = {{event${i}}} THEN 1 END) AS EVENT${i},`) .join('\n') .slice(0, -1); const eventDataColumns = eventData From 6589bc6ecb10a4afabeb73cc97c1819340fb78b0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 21 May 2024 21:15:31 -0700 Subject: [PATCH 07/30] Redesigned filter bar. --- .../websites/[websiteId]/WebsiteDetails.tsx | 12 ++++++++++-- .../websites/[websiteId]/WebsiteFilterButton.tsx | 10 ++++++++-- .../(main)/websites/[websiteId]/WebsiteHeader.tsx | 5 +++++ .../[websiteId]/WebsiteMetricsBar.module.css | 6 ------ .../websites/[websiteId]/WebsiteMetricsBar.tsx | 14 +++----------- .../[websiteId]/compare/WebsiteComparePage.tsx | 13 +++++++++++++ .../(main)/websites/[websiteId]/compare/page.tsx | 10 ++++++++++ src/assets/compare.svg | 1 + src/assets/target.svg | 2 +- src/components/icons.ts | 2 ++ src/components/messages.ts | 1 + src/components/metrics/FilterTags.module.css | 15 ++++++++++++--- src/components/metrics/FilterTags.tsx | 2 ++ src/components/metrics/MetricCard.module.css | 9 ++++++++- src/components/metrics/MetricCard.tsx | 6 +++--- src/declaration.d.ts | 1 + src/lib/clickhouse.ts | 5 +---- .../analytics/pageviews/getPageviewMetrics.ts | 10 +++++----- 18 files changed, 86 insertions(+), 38 deletions(-) create mode 100644 src/app/(main)/websites/[websiteId]/compare/WebsiteComparePage.tsx create mode 100644 src/app/(main)/websites/[websiteId]/compare/page.tsx create mode 100644 src/assets/compare.svg diff --git a/src/app/(main)/websites/[websiteId]/WebsiteDetails.tsx b/src/app/(main)/websites/[websiteId]/WebsiteDetails.tsx index 1a131da13..8c8ab2f22 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteDetails.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteDetails.tsx @@ -9,6 +9,7 @@ import WebsiteExpandedView from './WebsiteExpandedView'; import WebsiteHeader from './WebsiteHeader'; import WebsiteMetricsBar from './WebsiteMetricsBar'; import WebsiteTableView from './WebsiteTableView'; +import { FILTER_COLUMNS } from 'lib/constants'; export default function WebsiteDetails({ websiteId }: { websiteId: string }) { const { data: website, isLoading, error } = useWebsite(websiteId); @@ -20,13 +21,20 @@ export default function WebsiteDetails({ websiteId }: { websiteId: string }) { } const showLinks = !pathname.includes('/share/'); - const { view, ...params } = query; + const { view } = query; + + const params = Object.keys(query).reduce((obj, key) => { + if (FILTER_COLUMNS[key]) { + obj[key] = query[key]; + } + return obj; + }, {}); return ( <> - + {!website && } {website && ( diff --git a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx index a96717571..1d0c7a6a4 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx @@ -9,9 +9,15 @@ import styles from './WebsiteFilterButton.module.css'; export function WebsiteFilterButton({ websiteId, className, + position = 'bottom', + alignment = 'end', + showText = true, }: { websiteId: string; className?: string; + position?: 'bottom' | 'top' | 'left' | 'right'; + alignment?: 'end' | 'center' | 'start'; + showText?: boolean; }) { const { formatMessage, labels } = useMessages(); const { renderUrl, router } = useNavigation(); @@ -30,9 +36,9 @@ export function WebsiteFilterButton({ - {formatMessage(labels.filter)} + {showText && {formatMessage(labels.filter)}} - + {(close: () => void) => { return ( diff --git a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx index dc0f4338e..0cbaeb44d 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx @@ -30,6 +30,11 @@ export function WebsiteHeader({ icon: , path: '', }, + { + label: formatMessage(labels.compare), + icon: , + path: '/compare', + }, { label: formatMessage(labels.realtime), icon: , diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.module.css b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.module.css index db48bd550..e98639613 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.module.css +++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.module.css @@ -38,9 +38,3 @@ border-bottom: 1px solid var(--base300); } } - -@media screen and (max-width: 768px) { - .button { - display: none; - } -} diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx index e4acea3bf..06947a515 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx @@ -5,18 +5,10 @@ import MetricCard from 'components/metrics/MetricCard'; import MetricsBar from 'components/metrics/MetricsBar'; import { formatShortTime } from 'lib/format'; import WebsiteFilterButton from './WebsiteFilterButton'; -import styles from './WebsiteMetricsBar.module.css'; import useWebsiteStats from 'components/hooks/queries/useWebsiteStats'; +import styles from './WebsiteMetricsBar.module.css'; -export function WebsiteMetricsBar({ - websiteId, - showFilter = true, - sticky, -}: { - websiteId: string; - showFilter?: boolean; - sticky?: boolean; -}) { +export function WebsiteMetricsBar({ websiteId, sticky }: { websiteId: string; sticky?: boolean }) { const { formatMessage, labels } = useMessages(); const { ref, isSticky } = useSticky({ enabled: sticky }); const { data, isLoading, isFetched, error } = useWebsiteStats(websiteId); @@ -89,7 +81,7 @@ export function WebsiteMetricsBar({ )}
- {showFilter && } +
diff --git a/src/app/(main)/websites/[websiteId]/compare/WebsiteComparePage.tsx b/src/app/(main)/websites/[websiteId]/compare/WebsiteComparePage.tsx new file mode 100644 index 000000000..2fe971ae1 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/compare/WebsiteComparePage.tsx @@ -0,0 +1,13 @@ +import WebsiteHeader from '../WebsiteHeader'; +import WebsiteMetricsBar from '../WebsiteMetricsBar'; + +export function WebsiteComparePage({ websiteId }) { + return ( + <> + + + + ); +} + +export default WebsiteComparePage; diff --git a/src/app/(main)/websites/[websiteId]/compare/page.tsx b/src/app/(main)/websites/[websiteId]/compare/page.tsx new file mode 100644 index 000000000..b3009fcaa --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/compare/page.tsx @@ -0,0 +1,10 @@ +import WebsiteComparePage from './WebsiteComparePage'; +import { Metadata } from 'next'; + +export default function ({ params: { websiteId } }) { + return ; +} + +export const metadata: Metadata = { + title: 'Website Comparison', +}; diff --git a/src/assets/compare.svg b/src/assets/compare.svg new file mode 100644 index 000000000..e037c2435 --- /dev/null +++ b/src/assets/compare.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/target.svg b/src/assets/target.svg index 000f34c7d..c2e47e32d 100644 --- a/src/assets/target.svg +++ b/src/assets/target.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/components/icons.ts b/src/components/icons.ts index 3cbb09d25..8e5a481cb 100644 --- a/src/components/icons.ts +++ b/src/components/icons.ts @@ -6,6 +6,7 @@ import Bolt from 'assets/bolt.svg'; import Calendar from 'assets/calendar.svg'; import Change from 'assets/change.svg'; import Clock from 'assets/clock.svg'; +import Compare from 'assets/compare.svg'; import Dashboard from 'assets/dashboard.svg'; import Eye from 'assets/eye.svg'; import Gear from 'assets/gear.svg'; @@ -32,6 +33,7 @@ const icons = { Calendar, Change, Clock, + Compare, Dashboard, Eye, Gear, diff --git a/src/components/messages.ts b/src/components/messages.ts index 238ebf524..6231c6f3f 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -247,6 +247,7 @@ export const labels = defineMessages({ id: 'label.journey-description', defaultMessage: 'Understand how users nagivate through your website.', }, + compare: { id: 'label.compare', defaultMessage: 'Compare' }, }); export const messages = defineMessages({ diff --git a/src/components/metrics/FilterTags.module.css b/src/components/metrics/FilterTags.module.css index fe5c345ce..ea7714f47 100644 --- a/src/components/metrics/FilterTags.module.css +++ b/src/components/metrics/FilterTags.module.css @@ -2,6 +2,12 @@ display: flex; align-items: center; gap: 10px; + background: var(--base75); + padding: 10px 20px; + border: 1px solid var(--base400); + border-radius: 8px; + margin-bottom: 20px; + flex-wrap: wrap; } .label { @@ -12,12 +18,13 @@ display: flex; flex-direction: row; align-items: center; - gap: 10px; - background: var(--base75); + gap: 4px; + font-size: 12px; + background: var(--base50); border: 1px solid var(--base400); border-radius: var(--border-radius); box-shadow: 1px 1px 1px var(--base500); - padding: 8px 16px; + padding: 6px 14px; cursor: pointer; } @@ -27,6 +34,8 @@ .close { font-weight: 700; + align-self: center; + margin-left: auto; } .name, diff --git a/src/components/metrics/FilterTags.tsx b/src/components/metrics/FilterTags.tsx index 35d12556a..ee136ae39 100644 --- a/src/components/metrics/FilterTags.tsx +++ b/src/components/metrics/FilterTags.tsx @@ -13,6 +13,7 @@ import FieldFilterEditForm from 'app/(main)/reports/[reportId]/FieldFilterEditFo import { OPERATOR_PREFIXES } from 'lib/constants'; import { isSearchOperator, parseParameterValue } from 'lib/params'; import styles from './FilterTags.module.css'; +import WebsiteFilterButton from 'app/(main)/websites/[websiteId]/WebsiteFilterButton'; export function FilterTags({ websiteId, @@ -100,6 +101,7 @@ export function FilterTags({ ); })} +
diff --git a/src/components/hooks/queries/useWebsiteStats.ts b/src/components/hooks/queries/useWebsiteStats.ts index c2c4b74fd..b24399fa9 100644 --- a/src/components/hooks/queries/useWebsiteStats.ts +++ b/src/components/hooks/queries/useWebsiteStats.ts @@ -1,13 +1,17 @@ import { useApi } from './useApi'; import { useFilterParams } from '../useFilterParams'; -export function useWebsiteStats(websiteId: string, options?: { [key: string]: string }) { +export function useWebsiteStats( + websiteId: string, + compare?: string, + options?: { [key: string]: string }, +) { const { get, useQuery } = useApi(); const params = useFilterParams(websiteId); return useQuery({ - queryKey: ['websites:stats', { websiteId, ...params }], - queryFn: () => get(`/websites/${websiteId}/stats`, params), + queryKey: ['websites:stats', { websiteId, ...params, compare }], + queryFn: () => get(`/websites/${websiteId}/stats`, { ...params, compare }), enabled: !!websiteId, ...options, }); diff --git a/src/components/messages.ts b/src/components/messages.ts index 17f7b035d..f4d986ff7 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -134,7 +134,7 @@ export const labels = defineMessages({ uniqueVisitors: { id: 'label.unique-visitors', defaultMessage: 'Unique visitors' }, bounceRate: { id: 'label.bounce-rate', defaultMessage: 'Bounce rate' }, viewsPerVisit: { id: 'label.views-per-visit', defaultMessage: 'Views per visit' }, - averageVisitTime: { id: 'label.average-visit-time', defaultMessage: 'Average visit time' }, + visitDuration: { id: 'label.visit-duration', defaultMessage: 'Visit duration' }, desktop: { id: 'label.desktop', defaultMessage: 'Desktop' }, laptop: { id: 'label.laptop', defaultMessage: 'Laptop' }, tablet: { id: 'label.tablet', defaultMessage: 'Tablet' }, @@ -253,6 +253,8 @@ export const labels = defineMessages({ defaultMessage: 'Understand how users nagivate through your website.', }, compare: { id: 'label.compare', defaultMessage: 'Compare' }, + previousPeriod: { id: 'label.previous-period', defaultMessage: 'Previous period' }, + yearOverYear: { id: 'label.year-over-year', defaultMessage: 'Year over year' }, }); export const messages = defineMessages({ diff --git a/src/components/metrics/MetricCard.module.css b/src/components/metrics/MetricCard.module.css index 03cddd38f..92c5712c6 100644 --- a/src/components/metrics/MetricCard.module.css +++ b/src/components/metrics/MetricCard.module.css @@ -2,7 +2,16 @@ display: flex; flex-direction: column; justify-content: center; - min-width: 140px; + min-width: 150px; +} + +.card.compare { + gap: 10px; +} + +.card.compare .change { + font-size: 16px; + padding: 5px 10px; } .card:first-child { @@ -14,30 +23,33 @@ } .value { - display: flex; - align-items: center; - font-size: 36px; + font-size: 40px; font-weight: 700; white-space: nowrap; - min-height: 60px; color: var(--base900); + line-height: 1.5; +} + +.value.prev { + color: var(--base800); } .label { - display: flex; - align-items: center; font-weight: 700; - gap: 10px; white-space: nowrap; - min-height: 30px; color: var(--base800); } .change { - font-size: 12px; + display: flex; + align-items: center; + gap: 5px; + font-size: 13px; + font-weight: 700; padding: 0 5px; border-radius: 5px; color: var(--base500); + align-self: flex-start; } .change.positive { @@ -49,7 +61,3 @@ color: var(--red700); background: var(--red100); } - -.change.plusSign::before { - content: '+'; -} diff --git a/src/components/metrics/MetricCard.tsx b/src/components/metrics/MetricCard.tsx index 0db164a26..2bb38fbaa 100644 --- a/src/components/metrics/MetricCard.tsx +++ b/src/components/metrics/MetricCard.tsx @@ -1,15 +1,19 @@ import classNames from 'classnames'; +import { Icon, Icons } from 'react-basics'; import { useSpring, animated } from '@react-spring/web'; import { formatNumber } from 'lib/format'; import styles from './MetricCard.module.css'; export interface MetricCardProps { value: number; + previousValue?: number; change?: number; - label: string; + label?: string; reverseColors?: boolean; format?: typeof formatNumber; - hideComparison?: boolean; + showLabel?: boolean; + showChange?: boolean; + showPrevious?: boolean; className?: string; } @@ -19,32 +23,43 @@ export const MetricCard = ({ label, reverseColors = false, format = formatNumber, - hideComparison = false, + showLabel = true, + showChange = true, + showPrevious = false, className, }: MetricCardProps) => { const props = useSpring({ x: Number(value) || 0, from: { x: 0 } }); const changeProps = useSpring({ x: Number(change) || 0, from: { x: 0 } }); + const prevProps = useSpring({ x: Number(value - change) || 0, from: { x: 0 } }); + const positive = change * (reverseColors ? -1 : 1) >= 0; + const negative = change * (reverseColors ? -1 : 1) < 0; return ( -
-
- {label} - {~~change !== 0 && !hideComparison && ( - = 0, - [styles.negative]: change * (reverseColors ? -1 : 1) < 0, - [styles.plusSign]: change > 0, - })} - title={changeProps?.x as any} - > - {changeProps?.x?.to(x => format(x))} - - )} -
+
+ {showLabel &&
{label}
} {props?.x?.to(x => format(x))} + {showChange && ( +
+ + + + + {changeProps?.x?.to(x => format(Math.abs(x)))} + +
+ )} + {showPrevious && ( + + {prevProps?.x?.to(x => format(x))} + + )}
); }; diff --git a/src/lang/am-ET.json b/src/lang/am-ET.json index e289ddc5f..9546ffdd9 100644 --- a/src/lang/am-ET.json +++ b/src/lang/am-ET.json @@ -13,7 +13,7 @@ "label.all-time": "All time", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Average visit time", + "label.visit-duration": "Average visit time", "label.back": "Back", "label.before": "Before", "label.bounce-rate": "Bounce rate", diff --git a/src/lang/ar-SA.json b/src/lang/ar-SA.json index d01ff65fd..aff31f678 100644 --- a/src/lang/ar-SA.json +++ b/src/lang/ar-SA.json @@ -13,7 +13,7 @@ "label.all-time": "كل الوقت", "label.analytics": "تحليلات", "label.average": "المتوسط", - "label.average-visit-time": "متوسط وقت الزيارة", + "label.visit-duration": "متوسط وقت الزيارة", "label.back": "للخلف", "label.before": "قبل", "label.bounce-rate": "معدل الارتداد", diff --git a/src/lang/be-BY.json b/src/lang/be-BY.json index b68c8dab4..a831fe0fa 100644 --- a/src/lang/be-BY.json +++ b/src/lang/be-BY.json @@ -13,7 +13,7 @@ "label.all-time": "Увесь час", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Сярэдняя даўжыня наведвання", + "label.visit-duration": "Сярэдняя даўжыня наведвання", "label.back": "Назад", "label.before": "Before", "label.bounce-rate": "Паказчык адмоваў", diff --git a/src/lang/bn-BD.json b/src/lang/bn-BD.json index f94292159..7436a002b 100644 --- a/src/lang/bn-BD.json +++ b/src/lang/bn-BD.json @@ -13,7 +13,7 @@ "label.all-time": "সব সময়", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "গড় পরিদর্শনের সময়", + "label.visit-duration": "গড় পরিদর্শনের সময়", "label.back": "পেছনে", "label.before": "Before", "label.bounce-rate": "বহিষ্কারের হার", diff --git a/src/lang/bs-BA.json b/src/lang/bs-BA.json index 27318df0d..3eda0f854 100644 --- a/src/lang/bs-BA.json +++ b/src/lang/bs-BA.json @@ -13,7 +13,7 @@ "label.all-time": "Cijelo vrijeme", "label.analytics": "Analitike", "label.average": "Prosjek", - "label.average-visit-time": "Prosječno vrijeme posjete", + "label.visit-duration": "Prosječno vrijeme posjete", "label.back": "Nazad", "label.before": "Prije", "label.bounce-rate": "Bounce rate", diff --git a/src/lang/ca-ES.json b/src/lang/ca-ES.json index 4a1dee619..a12fd3c8e 100644 --- a/src/lang/ca-ES.json +++ b/src/lang/ca-ES.json @@ -13,7 +13,7 @@ "label.all-time": "Sempre", "label.analytics": "Analítiques", "label.average": "Mitjana", - "label.average-visit-time": "Temps mitjà de visita", + "label.visit-duration": "Temps mitjà de visita", "label.back": "Enrere", "label.before": "Abans", "label.bounce-rate": "Percentatge de rebot", diff --git a/src/lang/cs-CZ.json b/src/lang/cs-CZ.json index 7f3599c9b..64153d7d0 100644 --- a/src/lang/cs-CZ.json +++ b/src/lang/cs-CZ.json @@ -13,7 +13,7 @@ "label.all-time": "All time", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Průměrný čas návštěvy", + "label.visit-duration": "Průměrný čas návštěvy", "label.back": "Zpět", "label.before": "Before", "label.bounce-rate": "Okamžité opuštění", diff --git a/src/lang/da-DK.json b/src/lang/da-DK.json index fe6d483f0..d38d2a821 100644 --- a/src/lang/da-DK.json +++ b/src/lang/da-DK.json @@ -13,7 +13,7 @@ "label.all-time": "Altid", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Gennemsnitlig besøgstid", + "label.visit-duration": "Gennemsnitlig besøgstid", "label.back": "Tilbage", "label.before": "Before", "label.bounce-rate": "Afvisningsprocent", diff --git a/src/lang/de-CH.json b/src/lang/de-CH.json index a99826310..dca1e3516 100644 --- a/src/lang/de-CH.json +++ b/src/lang/de-CH.json @@ -13,7 +13,7 @@ "label.all-time": "Gesamte Zitruum", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Durchschn. Bsuechsziit", + "label.visit-duration": "Durchschn. Bsuechsziit", "label.back": "Zrugg", "label.before": "Before", "label.bounce-rate": "Absprungsrate", diff --git a/src/lang/de-DE.json b/src/lang/de-DE.json index 3272abaf1..b0a296b51 100644 --- a/src/lang/de-DE.json +++ b/src/lang/de-DE.json @@ -13,7 +13,7 @@ "label.all-time": "Gesamter Zeitraum", "label.analytics": "Analytics", "label.average": "Durchschnitt", - "label.average-visit-time": "Durchschn. Besuchszeit", + "label.visit-duration": "Durchschn. Besuchszeit", "label.back": "Zurück", "label.before": "Vor", "label.bounce-rate": "Absprungrate", diff --git a/src/lang/el-GR.json b/src/lang/el-GR.json index 1c1fd1b22..3d44bd005 100644 --- a/src/lang/el-GR.json +++ b/src/lang/el-GR.json @@ -13,7 +13,7 @@ "label.all-time": "All time", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Μέσος χρόνος επίσκεψης", + "label.visit-duration": "Μέσος χρόνος επίσκεψης", "label.back": "Πίσω", "label.before": "Before", "label.bounce-rate": "Ποσοστό αναπήδησης", diff --git a/src/lang/en-GB.json b/src/lang/en-GB.json index 3df625f8e..82e60c5e0 100644 --- a/src/lang/en-GB.json +++ b/src/lang/en-GB.json @@ -13,7 +13,7 @@ "label.all-time": "All time", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Average visit time", + "label.visit-duration": "Average visit time", "label.back": "Back", "label.before": "Before", "label.bounce-rate": "Bounce rate", diff --git a/src/lang/en-US.json b/src/lang/en-US.json index 5ceb39d18..77e6daa1e 100644 --- a/src/lang/en-US.json +++ b/src/lang/en-US.json @@ -13,7 +13,7 @@ "label.all-time": "All time", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Average visit time", + "label.visit-duration": "Average visit time", "label.back": "Back", "label.before": "Before", "label.bounce-rate": "Bounce rate", diff --git a/src/lang/es-ES.json b/src/lang/es-ES.json index 43e74e214..85796f426 100644 --- a/src/lang/es-ES.json +++ b/src/lang/es-ES.json @@ -13,7 +13,7 @@ "label.all-time": "Todos los tiempos", "label.analytics": "Analíticas", "label.average": "Media", - "label.average-visit-time": "Tiempo promedio de visita", + "label.visit-duration": "Tiempo promedio de visita", "label.back": "Atrás", "label.before": "Antes", "label.bounce-rate": "Porcentaje de rebote", diff --git a/src/lang/fa-IR.json b/src/lang/fa-IR.json index 70c60f018..cab0ffa41 100644 --- a/src/lang/fa-IR.json +++ b/src/lang/fa-IR.json @@ -13,7 +13,7 @@ "label.all-time": "همه زمان", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "میانگین زمان بازدید", + "label.visit-duration": "میانگین زمان بازدید", "label.back": "برگشت", "label.before": "Before", "label.bounce-rate": "نرخ Bounce", diff --git a/src/lang/fi-FI.json b/src/lang/fi-FI.json index f827c482a..0389cf82b 100644 --- a/src/lang/fi-FI.json +++ b/src/lang/fi-FI.json @@ -13,7 +13,7 @@ "label.all-time": "Alusta lähtien", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Keskimääräinen vierailuaika", + "label.visit-duration": "Keskimääräinen vierailuaika", "label.back": "Takaisin", "label.before": "Before", "label.bounce-rate": "Välitön poistuminen", diff --git a/src/lang/fo-FO.json b/src/lang/fo-FO.json index 6b9e42d0c..d13d40ec6 100644 --- a/src/lang/fo-FO.json +++ b/src/lang/fo-FO.json @@ -13,7 +13,7 @@ "label.all-time": "All time", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Miðal vitjurnartíð ", + "label.visit-duration": "Miðal vitjurnartíð ", "label.back": "Aftur", "label.before": "Before", "label.bounce-rate": "Bounce prosenttal", diff --git a/src/lang/fr-FR.json b/src/lang/fr-FR.json index bd71a85f8..b52ae875a 100644 --- a/src/lang/fr-FR.json +++ b/src/lang/fr-FR.json @@ -13,7 +13,7 @@ "label.all-time": "Toutes les données", "label.analytics": "Analytics", "label.average": "Moyenne", - "label.average-visit-time": "Temps de visite moyen", + "label.visit-duration": "Temps de visite moyen", "label.back": "Retour", "label.before": "Avant", "label.bounce-rate": "Taux de rebond", diff --git a/src/lang/ga-ES.json b/src/lang/ga-ES.json index fa9a1cc5a..cdf139324 100644 --- a/src/lang/ga-ES.json +++ b/src/lang/ga-ES.json @@ -13,7 +13,7 @@ "label.all-time": "Sempre", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Tempo medio de visita", + "label.visit-duration": "Tempo medio de visita", "label.back": "Atrás", "label.before": "Before", "label.bounce-rate": "Proporción de rebote", diff --git a/src/lang/he-IL.json b/src/lang/he-IL.json index 3c422a76f..f513c807e 100644 --- a/src/lang/he-IL.json +++ b/src/lang/he-IL.json @@ -13,7 +13,7 @@ "label.all-time": "All time", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "זמן ביקור ממוצע", + "label.visit-duration": "זמן ביקור ממוצע", "label.back": "חזרה", "label.before": "Before", "label.bounce-rate": "Bounce rate", diff --git a/src/lang/hi-IN.json b/src/lang/hi-IN.json index 791111c83..2e2e79f71 100644 --- a/src/lang/hi-IN.json +++ b/src/lang/hi-IN.json @@ -13,7 +13,7 @@ "label.all-time": "All time", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "औसत दृश्य समय", + "label.visit-duration": "औसत दृश्य समय", "label.back": "पीछे", "label.before": "Before", "label.bounce-rate": "उछाल दर", diff --git a/src/lang/hr-HR.json b/src/lang/hr-HR.json index 7ba496c75..ea4884903 100644 --- a/src/lang/hr-HR.json +++ b/src/lang/hr-HR.json @@ -13,7 +13,7 @@ "label.all-time": "Svo vrijeme", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Average visit time", + "label.visit-duration": "Average visit time", "label.back": "Natrag ", "label.before": "Before", "label.bounce-rate": "Bounce rate", diff --git a/src/lang/hu-HU.json b/src/lang/hu-HU.json index b73b36a77..74f0ce6fe 100644 --- a/src/lang/hu-HU.json +++ b/src/lang/hu-HU.json @@ -13,7 +13,7 @@ "label.all-time": "All time", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Átlagos látogatási idő", + "label.visit-duration": "Átlagos látogatási idő", "label.back": "Vissza", "label.before": "Before", "label.bounce-rate": "Visszafordulási arány", diff --git a/src/lang/id-ID.json b/src/lang/id-ID.json index 64d17d9a9..faf773157 100644 --- a/src/lang/id-ID.json +++ b/src/lang/id-ID.json @@ -13,7 +13,7 @@ "label.all-time": "Semua waktu", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Waktu kunjungan rata-rata", + "label.visit-duration": "Waktu kunjungan rata-rata", "label.back": "Kembali", "label.before": "Before", "label.bounce-rate": "Rasio pentalan", diff --git a/src/lang/it-IT.json b/src/lang/it-IT.json index 9425d5e69..084f86eef 100644 --- a/src/lang/it-IT.json +++ b/src/lang/it-IT.json @@ -13,7 +13,7 @@ "label.all-time": "Sempre", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Tempo medio di visita", + "label.visit-duration": "Tempo medio di visita", "label.back": "Indietro", "label.before": "Before", "label.bounce-rate": "Frequenza di rimbalzo", diff --git a/src/lang/ja-JP.json b/src/lang/ja-JP.json index b646bd51e..5c8747ba9 100644 --- a/src/lang/ja-JP.json +++ b/src/lang/ja-JP.json @@ -13,7 +13,7 @@ "label.all-time": "すべての時間帯", "label.analytics": "アナリティクス", "label.average": "平均", - "label.average-visit-time": "平均滞在時間", + "label.visit-duration": "平均滞在時間", "label.back": "戻る", "label.before": "直前", "label.bounce-rate": "直帰率", diff --git a/src/lang/km-KH.json b/src/lang/km-KH.json index 17ddd9145..6bb9f4763 100644 --- a/src/lang/km-KH.json +++ b/src/lang/km-KH.json @@ -13,7 +13,7 @@ "label.all-time": "គ្រប់ពេល", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "មើលជាមធ្យម", + "label.visit-duration": "មើលជាមធ្យម", "label.back": "ថយក្រោយ", "label.before": "Before", "label.bounce-rate": "ចំនួនវិលត្រឡប់", diff --git a/src/lang/ko-KR.json b/src/lang/ko-KR.json index f64f03aa6..1bfb9ca06 100644 --- a/src/lang/ko-KR.json +++ b/src/lang/ko-KR.json @@ -13,7 +13,7 @@ "label.all-time": "All time", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "평균 방문 시간", + "label.visit-duration": "평균 방문 시간", "label.back": "뒤로", "label.before": "Before", "label.bounce-rate": "이탈률", diff --git a/src/lang/lt-LT.json b/src/lang/lt-LT.json index 0d8f53e99..2fc0f5e13 100644 --- a/src/lang/lt-LT.json +++ b/src/lang/lt-LT.json @@ -13,7 +13,7 @@ "label.all-time": "Visas laikotarpis", "label.analytics": "Analytics", "label.average": "Vidurkis", - "label.average-visit-time": "Vidutinė vizito trukmė", + "label.visit-duration": "Vidutinė vizito trukmė", "label.back": "Atgal", "label.before": "Prieš", "label.bounce-rate": "Atmetimo rodiklis", diff --git a/src/lang/mn-MN.json b/src/lang/mn-MN.json index bd001adde..340f4bcd7 100644 --- a/src/lang/mn-MN.json +++ b/src/lang/mn-MN.json @@ -13,7 +13,7 @@ "label.all-time": "Бүх цаг үеийн", "label.analytics": "Analytics", "label.average": "Дундаж", - "label.average-visit-time": "Зочилсон дундаж хугацаа", + "label.visit-duration": "Зочилсон дундаж хугацаа", "label.back": "Буцах", "label.before": "Өмнө", "label.bounce-rate": "Нэг хуудас үзээд гарсан", diff --git a/src/lang/ms-MY.json b/src/lang/ms-MY.json index 6fef99314..73124486c 100644 --- a/src/lang/ms-MY.json +++ b/src/lang/ms-MY.json @@ -13,7 +13,7 @@ "label.all-time": "All time", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Purata tempoh masa lawatan", + "label.visit-duration": "Purata tempoh masa lawatan", "label.back": "Kembali", "label.before": "Before", "label.bounce-rate": "Kadar lantunan", diff --git a/src/lang/my-MM.json b/src/lang/my-MM.json index bf8e0217e..8883c71e4 100644 --- a/src/lang/my-MM.json +++ b/src/lang/my-MM.json @@ -13,7 +13,7 @@ "label.all-time": "အချိန်အစမှအခုထိ", "label.analytics": "အန်နလစ်တစ်", "label.average": "ပျမ်းမျှ", - "label.average-visit-time": "ဝဘက်ဘ်ဆိုဒ်တွင် ပျမ်းမျှကုန်ဆုံးချိန်", + "label.visit-duration": "ဝဘက်ဘ်ဆိုဒ်တွင် ပျမ်းမျှကုန်ဆုံးချိန်", "label.back": "နောက်သို့", "label.before": "မတိုင်မီ", "label.bounce-rate": "Bounce နှုန်း", diff --git a/src/lang/nb-NO.json b/src/lang/nb-NO.json index a0fe6575f..491597236 100644 --- a/src/lang/nb-NO.json +++ b/src/lang/nb-NO.json @@ -13,7 +13,7 @@ "label.all-time": "Noensinne", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Gjennomsnittlig besøkstid", + "label.visit-duration": "Gjennomsnittlig besøkstid", "label.back": "Tilbake", "label.before": "Before", "label.bounce-rate": "Avvisningsfrekvens", diff --git a/src/lang/nl-NL.json b/src/lang/nl-NL.json index 4335345a7..1d11ed5a5 100644 --- a/src/lang/nl-NL.json +++ b/src/lang/nl-NL.json @@ -13,7 +13,7 @@ "label.all-time": "Onbeperkt", "label.analytics": "Analytics", "label.average": "Gemiddelde", - "label.average-visit-time": "Gemiddelde bezoektijd", + "label.visit-duration": "Gemiddelde bezoektijd", "label.back": "Terug", "label.before": "Voor", "label.bounce-rate": "Bouncepercentage", diff --git a/src/lang/pl-PL.json b/src/lang/pl-PL.json index 7eae7baaf..bd649cfd2 100644 --- a/src/lang/pl-PL.json +++ b/src/lang/pl-PL.json @@ -13,7 +13,7 @@ "label.all-time": "Cały czas", "label.analytics": "Analityka", "label.average": "Średnia", - "label.average-visit-time": "Średni czas wizyty", + "label.visit-duration": "Średni czas wizyty", "label.back": "Powrót", "label.before": "Przed", "label.bounce-rate": "Współczynnik odrzuceń", diff --git a/src/lang/pt-BR.json b/src/lang/pt-BR.json index 5c0425ea6..b84deb2d3 100644 --- a/src/lang/pt-BR.json +++ b/src/lang/pt-BR.json @@ -13,7 +13,7 @@ "label.all-time": "Todos os períodos", "label.analytics": "Análise", "label.average": "Média", - "label.average-visit-time": "Tempo médio de visita", + "label.visit-duration": "Tempo médio de visita", "label.back": "Voltar", "label.before": "Antes", "label.bounce-rate": "Taxa de rejeição", @@ -78,31 +78,31 @@ "label.greater-than": "Maior que", "label.greater-than-equals": "Maior ou igual a", "label.insights": "Insights", - "label.insights-description": "Explore seus dados em mais detalhes usando filtros", + "label.insights-description": "Explore seus dados em mais detalhes usando filtros", "label.is": "É igual a", "label.is-not": "Não é igual a", "label.is-not-set": "Não definido", "label.is-set": "Definido", "label.join": "Participar", "label.join-team": "Participar da equipe", - "label.language": "Idioma", + "label.language": "Idioma", "label.languages": "Idiomas", "label.laptop": "Notebook", "label.last-days": "Últimos {x} dias", "label.last-hours": "Últimas {x} horas", "label.last-months": "Últimos {x} meses", - "label.leave": "Sair", + "label.leave": "Sair", "label.leave-team": "Sair da equipe", "label.less-than": "Menor que", "label.less-than-equals": "Menor ou igual a", - "label.login": "Entrar", + "label.login": "Entrar", "label.logout": "Sair", "label.manage": "Gerenciar", "label.max": "Máximo", "label.member": "Membro", "label.members": "Membros", "label.min": "Mínimo", - "label.mobile": "Celular", + "label.mobile": "Celular", "label.more": "Mais", "label.my-account": "Minha conta", "label.my-websites": "Meus sites", @@ -114,15 +114,15 @@ "label.os": "Sistema operacional", "label.overview": "Visão geral", "label.owner": "Proprietário", - "label.page-of": "Página {current} de {total}", + "label.page-of": "Página {current} de {total}", "label.page-views": "Visualizações de página", - "label.pageTitle": "Título", + "label.pageTitle": "Título", "label.pages": "Páginas", - "label.password": "Senha", - "label.powered-by": "Desenvolvido por {name}", + "label.password": "Senha", + "label.powered-by": "Desenvolvido por {name}", "label.profile": "Perfil", "label.queries": "Consultas", - "label.query": "Consulta", + "label.query": "Consulta", "label.query-parameters": "Parâmetros da consulta", "label.realtime": "Tempo real", "label.referrer": "Referência", @@ -132,17 +132,17 @@ "label.region": "Estado", "label.regions": "Estados", "label.remove": "Remover", - "label.remove-member": "Remover membro", + "label.remove-member": "Remover membro", "label.reports": "Relatórios", "label.required": "Obrigatório", "label.reset": "Redefinir", "label.reset-website": "Redefinir dados", "label.retention": "Retenção", "label.retention-description": "Avalie a fidelidade dos seus usuários medindo a frequência com que eles retornam.", - "label.role": "Função", - "label.run-query": "Executar consulta", + "label.role": "Função", + "label.run-query": "Executar consulta", "label.save": "Salvar", - "label.screens": "Tamanhos de tela", + "label.screens": "Tamanhos de tela", "label.search": "Pesquisar", "label.select": "Selecionar", "label.select-date": "Selecionar data", @@ -150,7 +150,7 @@ "label.select-website": "Selecionar site", "label.sessions": "Sessões", "label.settings": "Configurações", - "label.share-url": "Link para compartilhar", + "label.share-url": "Link para compartilhar", "label.single-day": "Apenas um dia", "label.steps": "Etapas", "label.sum": "Soma", @@ -158,56 +158,56 @@ "label.team": "Equipe", "label.team-id": "ID da equipe", "label.team-member": "Membro da equipe", - "label.team-name": "Nome da equipe", - "label.team-owner": "Proprietário da equipe", - "label.team-view-only": "Apenas visualização da equipe", + "label.team-name": "Nome da equipe", + "label.team-owner": "Proprietário da equipe", + "label.team-view-only": "Apenas visualização da equipe", "label.team-websites": "Sites da equipe", "label.teams": "Equipes", "label.theme": "Tema", - "label.this-month": "Este mês", - "label.this-week": "Esta semana", + "label.this-month": "Este mês", + "label.this-week": "Esta semana", "label.this-year": "Este ano", "label.timezone": "Fuso horário", "label.title": "Título", - "label.today": "Hoje", + "label.today": "Hoje", "label.toggle-charts": "Alternar gráficos", "label.total": "Total", - "label.total-records": "Total de registros", + "label.total-records": "Total de registros", "label.tracking-code": "Código de rastreamento", "label.transfer": "Transferir", "label.transfer-website": "Transferir site", "label.true": "Sim", - "label.type": "Tipo", - "label.unique": "Únicos", + "label.type": "Tipo", + "label.unique": "Únicos", "label.unique-visitors": "Visitantes únicos", "label.unknown": "Desconhecido", "label.untitled": "Sem título", - "label.update": "Atualizar", - "label.url": "URL", + "label.update": "Atualizar", + "label.url": "URL", "label.urls": "URLs", "label.user": "Usuário", - "label.username": "Nome de usuário", + "label.username": "Nome de usuário", "label.users": "Usuários", "label.utm": "UTM", "label.utm-description": "Acompanhe suas campanhas de publicidade através de parâmetros UTM.", - "label.value": "Valor", + "label.value": "Valor", "label.view": "Visualizar", "label.view-details": "Ver mais", "label.view-only": "Somente visualização", "label.views": "Visualizações", - "label.views-per-visit": "Visualizações por visita", - "label.visitors": "Visitantes", + "label.views-per-visit": "Visualizações por visita", + "label.visitors": "Visitantes", "label.visits": "Visitas", "label.website": "Site", - "label.website-id": "ID do site", + "label.website-id": "ID do site", "label.websites": "Sites", "label.window": "Janela", - "label.yesterday": "Ontem", - "message.action-confirmation": "Digite {confirmation} na caixa abaixo para confirmar.", + "label.yesterday": "Ontem", + "message.action-confirmation": "Digite {confirmation} na caixa abaixo para confirmar.", "message.active-users": " Atualmente {x} usuários ativos", "message.confirm-delete": "Tem certeza de que deseja excluir {target}?", - "message.confirm-leave": "Tem certeza de que deseja sair de {target}?", - "message.confirm-remove": "Tem certeza que deseja remover {target}?", + "message.confirm-leave": "Tem certeza de que deseja sair de {target}?", + "message.confirm-remove": "Tem certeza que deseja remover {target}?", "message.confirm-reset": "Tem certeza que deseja redefinir os dados de {target}?", "message.delete-team-warning": "Excluir a equipe também excluirá todos os sites da equipe.", "message.delete-website-warning": "Todos os dados relacionados serão excluídos.", @@ -243,4 +243,4 @@ "message.viewed-page": "Página visualizada", "message.visitor-log": "Visitante de {country} usando o navegador {browser} em um {device} com sistema operacional {os}.", "message.visitors-dropped-off": "Visitantes abandonados" - } +} diff --git a/src/lang/pt-PT.json b/src/lang/pt-PT.json index 36966c983..02504a9a4 100644 --- a/src/lang/pt-PT.json +++ b/src/lang/pt-PT.json @@ -13,7 +13,7 @@ "label.all-time": "Todo o tempo", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Tempo médio de visita", + "label.visit-duration": "Tempo médio de visita", "label.back": "Voltar", "label.before": "Before", "label.bounce-rate": "Taxa de rejeição", diff --git a/src/lang/ro-RO.json b/src/lang/ro-RO.json index 296f51b77..e6c9cad45 100644 --- a/src/lang/ro-RO.json +++ b/src/lang/ro-RO.json @@ -13,7 +13,7 @@ "label.all-time": "Pentru tot timpul", "label.analytics": "Analytics", "label.average": "Mediu", - "label.average-visit-time": "Timp mediu de vizitare", + "label.visit-duration": "Timp mediu de vizitare", "label.back": "Înapoi", "label.before": "Înainte", "label.bounce-rate": "Rata de respingere", diff --git a/src/lang/ru-RU.json b/src/lang/ru-RU.json index 3d1d8e74f..25065a4fc 100644 --- a/src/lang/ru-RU.json +++ b/src/lang/ru-RU.json @@ -13,7 +13,7 @@ "label.all-time": "Все время", "label.analytics": "Аналитика", "label.average": "Average", - "label.average-visit-time": "Среднее время посещения", + "label.visit-duration": "Среднее время посещения", "label.back": "Назад", "label.before": "Before", "label.bounce-rate": "Отказы", diff --git a/src/lang/si-LK.json b/src/lang/si-LK.json index 6f672ab5c..087efa561 100644 --- a/src/lang/si-LK.json +++ b/src/lang/si-LK.json @@ -13,7 +13,7 @@ "label.all-time": "හැම වෙලාවෙම", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Average visit time", + "label.visit-duration": "Average visit time", "label.back": "ආපසු", "label.before": "Before", "label.bounce-rate": "Bounce rate", diff --git a/src/lang/sk-SK.json b/src/lang/sk-SK.json index d978281a1..10a547d12 100644 --- a/src/lang/sk-SK.json +++ b/src/lang/sk-SK.json @@ -13,7 +13,7 @@ "label.all-time": "All time", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Priemerný čas návštevy", + "label.visit-duration": "Priemerný čas návštevy", "label.back": "Späť", "label.before": "Before", "label.bounce-rate": "Okamžité opustenie", diff --git a/src/lang/sl-SI.json b/src/lang/sl-SI.json index 9ba94e3bc..0b3761f5b 100644 --- a/src/lang/sl-SI.json +++ b/src/lang/sl-SI.json @@ -13,7 +13,7 @@ "label.all-time": "Ves čas", "label.analytics": "Analitika", "label.average": "Povprečno", - "label.average-visit-time": "Povprečni čas obiska", + "label.visit-duration": "Povprečni čas obiska", "label.back": "Nazaj", "label.before": "Pred", "label.bounce-rate": "Odbojna stopnja", diff --git a/src/lang/sv-SE.json b/src/lang/sv-SE.json index 87e909423..8a72e3d68 100644 --- a/src/lang/sv-SE.json +++ b/src/lang/sv-SE.json @@ -13,7 +13,7 @@ "label.all-time": "Sedan början", "label.analytics": "Webbplats Analys", "label.average": "Genomsnitt", - "label.average-visit-time": "Genomsnittlig besökstid", + "label.visit-duration": "Genomsnittlig besökstid", "label.back": "Tillbaka", "label.before": "Före", "label.bounce-rate": "Avvisningsfrekvens", diff --git a/src/lang/ta-IN.json b/src/lang/ta-IN.json index 4e40c4902..770bc2721 100644 --- a/src/lang/ta-IN.json +++ b/src/lang/ta-IN.json @@ -13,7 +13,7 @@ "label.all-time": "All time", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "சராசரி வருகை நேரம்", + "label.visit-duration": "சராசரி வருகை நேரம்", "label.back": "பின்னால்", "label.before": "Before", "label.bounce-rate": "துள்ளல் விகிதம்", diff --git a/src/lang/th-TH.json b/src/lang/th-TH.json index a5ebeefb7..6f8b4a327 100644 --- a/src/lang/th-TH.json +++ b/src/lang/th-TH.json @@ -13,7 +13,7 @@ "label.all-time": "ทุกช่วงเวลา", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "ระยะเวลาเข้าชมเฉลี่ย", + "label.visit-duration": "ระยะเวลาเข้าชมเฉลี่ย", "label.back": "ย้อนกลับ", "label.before": "Before", "label.bounce-rate": "อัตราตีกลับ", diff --git a/src/lang/tr-TR.json b/src/lang/tr-TR.json index d4e46b634..285910524 100644 --- a/src/lang/tr-TR.json +++ b/src/lang/tr-TR.json @@ -13,7 +13,7 @@ "label.all-time": "Tüm zamanlar", "label.analytics": "Analitik", "label.average": "Ortalama", - "label.average-visit-time": "Ortalama ziyaret süresi", + "label.visit-duration": "Ortalama ziyaret süresi", "label.back": "Geri", "label.before": "Önce", "label.bounce-rate": "Tek sayfa ziyaret oranı", diff --git a/src/lang/uk-UA.json b/src/lang/uk-UA.json index 1042f3b46..3d393d300 100644 --- a/src/lang/uk-UA.json +++ b/src/lang/uk-UA.json @@ -13,7 +13,7 @@ "label.all-time": "Весь час", "label.analytics": "Аналітика", "label.average": "Середнє", - "label.average-visit-time": "Середній час візиту", + "label.visit-duration": "Середній час візиту", "label.back": "Назад", "label.before": "Раніше", "label.bounce-rate": "Показник відмов", diff --git a/src/lang/ur-PK.json b/src/lang/ur-PK.json index 862971f53..b7f018c73 100644 --- a/src/lang/ur-PK.json +++ b/src/lang/ur-PK.json @@ -13,7 +13,7 @@ "label.all-time": "تمام وقت", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "وزٹ کا اوسط وقت", + "label.visit-duration": "وزٹ کا اوسط وقت", "label.back": "پیچھے", "label.before": "Before", "label.bounce-rate": "اچھال کی شرح", diff --git a/src/lang/vi-VN.json b/src/lang/vi-VN.json index e9a47c616..dbf3bf95a 100644 --- a/src/lang/vi-VN.json +++ b/src/lang/vi-VN.json @@ -13,7 +13,7 @@ "label.all-time": "Toàn thời gian", "label.analytics": "Analytics", "label.average": "Average", - "label.average-visit-time": "Thời gian truy cập trung bình", + "label.visit-duration": "Thời gian truy cập trung bình", "label.back": "Quay về", "label.before": "Before", "label.bounce-rate": "Tỷ lệ thoát trang", diff --git a/src/lang/zh-CN.json b/src/lang/zh-CN.json index 4bf90fd16..62ab1aeae 100644 --- a/src/lang/zh-CN.json +++ b/src/lang/zh-CN.json @@ -13,7 +13,7 @@ "label.all-time": "所有时间段", "label.analytics": "分析", "label.average": "平均", - "label.average-visit-time": "平均访问时间", + "label.visit-duration": "平均访问时间", "label.back": "返回", "label.before": "之前", "label.bounce-rate": "跳出率", diff --git a/src/lang/zh-TW.json b/src/lang/zh-TW.json index 01c08b8b6..edad4a5c2 100644 --- a/src/lang/zh-TW.json +++ b/src/lang/zh-TW.json @@ -13,7 +13,7 @@ "label.all-time": "所有時間", "label.analytics": "分析", "label.average": "平均", - "label.average-visit-time": "平均造訪時間", + "label.visit-duration": "平均造訪時間", "label.back": "返回", "label.before": "之前", "label.bounce-rate": "跳出率", diff --git a/src/pages/api/websites/[websiteId]/stats.ts b/src/pages/api/websites/[websiteId]/stats.ts index 81a6d8354..9108e0628 100644 --- a/src/pages/api/websites/[websiteId]/stats.ts +++ b/src/pages/api/websites/[websiteId]/stats.ts @@ -22,6 +22,7 @@ export interface WebsiteStatsRequestQuery { country?: string; region?: string; city?: string; + compare?: string; } import * as yup from 'yup'; @@ -41,6 +42,7 @@ const schema = { country: yup.string(), region: yup.string(), city: yup.string(), + compare: yup.string(), }), }; @@ -77,7 +79,7 @@ export default async ( const stats = Object.keys(metrics[0]).reduce((obj, key) => { obj[key] = { value: Number(metrics[0][key]) || 0, - change: Number(metrics[0][key]) - Number(prevPeriod[0][key]) || 0, + prev: Number(prevPeriod[0][key]) || 0, }; return obj; }, {}); diff --git a/src/queries/analytics/eventData/getEventDataEvents.ts b/src/queries/analytics/eventData/getEventDataEvents.ts index 76ad3fce8..e5647debd 100644 --- a/src/queries/analytics/eventData/getEventDataEvents.ts +++ b/src/queries/analytics/eventData/getEventDataEvents.ts @@ -85,8 +85,8 @@ async function clickhouseQuery( limit 500 `, params, - ).then(a => { - return Object.values(a).map(a => { + ).then(result => { + return Object.values(result).map((a: any) => { return { eventName: a.eventName, fieldName: a.fieldName, @@ -113,8 +113,8 @@ async function clickhouseQuery( limit 500 `, params, - ).then(a => { - return Object.values(a).map(a => { + ).then(result => { + return Object.values(result).map((a: any) => { return { eventName: a.eventName, fieldName: a.fieldName, diff --git a/src/queries/analytics/eventData/getEventDataFields.ts b/src/queries/analytics/eventData/getEventDataFields.ts index 6ec3b35a8..f669ad39f 100644 --- a/src/queries/analytics/eventData/getEventDataFields.ts +++ b/src/queries/analytics/eventData/getEventDataFields.ts @@ -62,8 +62,8 @@ async function clickhouseQuery( limit 500 `, params, - ).then(a => { - return Object.values(a).map(a => { + ).then(result => { + return Object.values(result).map((a: any) => { return { fieldName: a.fieldName, dataType: Number(a.dataType), diff --git a/src/queries/analytics/eventData/getEventDataStats.ts b/src/queries/analytics/eventData/getEventDataStats.ts index 978f561bc..09bef107c 100644 --- a/src/queries/analytics/eventData/getEventDataStats.ts +++ b/src/queries/analytics/eventData/getEventDataStats.ts @@ -68,8 +68,8 @@ async function clickhouseQuery( ) as t `, params, - ).then(a => { - return Object.values(a).map(a => { + ).then(result => { + return Object.values(result).map((a: any) => { return { events: Number(a.events), fields: Number(a.fields), diff --git a/src/queries/analytics/eventData/getEventDataUsage.ts b/src/queries/analytics/eventData/getEventDataUsage.ts index 7866a6008..2a19f33e6 100644 --- a/src/queries/analytics/eventData/getEventDataUsage.ts +++ b/src/queries/analytics/eventData/getEventDataUsage.ts @@ -30,8 +30,8 @@ function clickhouseQuery( startDate, endDate, }, - ).then(a => { - return Object.values(a).map(a => { + ).then(result => { + return Object.values(result).map((a: any) => { return { websiteId: a.websiteId, count: Number(a.count) }; }); }); diff --git a/src/queries/analytics/getWebsiteStats.ts b/src/queries/analytics/getWebsiteStats.ts index bc006a2eb..6257e166c 100644 --- a/src/queries/analytics/getWebsiteStats.ts +++ b/src/queries/analytics/getWebsiteStats.ts @@ -92,13 +92,13 @@ async function clickhouseQuery( `, params, ).then(result => { - return Object.values(result).map(n => { + return Object.values(result).map((a: any) => { return { - pageviews: Number(n.pageviews), - visitors: Number(n.visitors), - visits: Number(n.visits), - bounces: Number(n.bounces), - totaltime: Number(n.totaltime), + pageviews: Number(a.pageviews), + visitors: Number(a.visitors), + visits: Number(a.visits), + bounces: Number(a.bounces), + totaltime: Number(a.totaltime), }; }); }); From 24af06f3aa9d763be3c4e3f6a7308a3bdf5f5bd3 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 23 May 2024 00:58:31 -0700 Subject: [PATCH 09/30] Metrics bar styling. --- src/app/(main)/websites/[websiteId]/WebsiteDetails.tsx | 2 +- src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx | 5 ++--- .../(main)/websites/[websiteId]/WebsiteMetricsBar.module.css | 5 ++--- src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx | 3 +++ .../websites/[websiteId]/compare/WebsiteComparePage.tsx | 2 ++ src/components/metrics/MetricCard.module.css | 4 ++++ src/components/metrics/MetricCard.tsx | 3 ++- 7 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/WebsiteDetails.tsx b/src/app/(main)/websites/[websiteId]/WebsiteDetails.tsx index 8c8ab2f22..1fd6992f6 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteDetails.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteDetails.tsx @@ -34,7 +34,7 @@ export default function WebsiteDetails({ websiteId }: { websiteId: string }) { <> - + {!website && } {website && ( diff --git a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx index 1d0c7a6a4..785f0f367 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx @@ -1,4 +1,3 @@ -import classNames from 'classnames'; import { Button, Icon, Icons, Popup, PopupTrigger, Text } from 'react-basics'; import PopupForm from 'app/(main)/reports/[reportId]/PopupForm'; import FilterSelectForm from 'app/(main)/reports/[reportId]/FilterSelectForm'; @@ -31,8 +30,8 @@ export function WebsiteFilterButton({ }; return ( - -
)} -
); } diff --git a/src/components/layout/Grid.module.css b/src/components/layout/Grid.module.css index f72a5f126..de99b7526 100644 --- a/src/components/layout/Grid.module.css +++ b/src/components/layout/Grid.module.css @@ -8,6 +8,10 @@ border-top: 1px solid var(--base300); } +.row.compare { + grid-template-columns: max-content 1fr 1fr; +} + .col { padding: 20px; min-height: 430px; diff --git a/src/components/layout/Grid.tsx b/src/components/layout/Grid.tsx index 2a34fdc4f..ec7f4fda1 100644 --- a/src/components/layout/Grid.tsx +++ b/src/components/layout/Grid.tsx @@ -1,6 +1,7 @@ import { CSSProperties } from 'react'; import classNames from 'classnames'; import { mapChildren } from 'react-basics'; +// eslint-disable-next-line css-modules/no-unused-class import styles from './Grid.module.css'; export interface GridProps { @@ -19,13 +20,13 @@ export function Grid({ className, style, children }: GridProps) { export function GridRow(props: { [x: string]: any; - columns?: 'one' | 'two' | 'three' | 'one-two' | 'two-one'; + columns?: 'one' | 'two' | 'three' | 'one-two' | 'two-one' | 'compare'; className?: string; children?: any; }) { const { columns = 'two', className, children, ...otherProps } = props; return ( -
+
{mapChildren(children, child => { return
{child}
; })} diff --git a/src/components/metrics/ListTable.tsx b/src/components/metrics/ListTable.tsx index 133905e13..14f2c9900 100644 --- a/src/components/metrics/ListTable.tsx +++ b/src/components/metrics/ListTable.tsx @@ -14,7 +14,7 @@ export interface ListTableProps { title?: string; metric?: string; className?: string; - renderLabel?: (row: any) => ReactNode; + renderLabel?: (row: any, index: number) => ReactNode; animate?: boolean; virtualize?: boolean; showPercentage?: boolean; @@ -34,13 +34,13 @@ export function ListTable({ }: ListTableProps) { const { formatMessage, labels } = useMessages(); - const getRow = row => { + const getRow = (row: { x: any; y: any; z: any }, index: number) => { const { x: label, y: value, z: percent } = row; return ( { - return
{getRow(data[index])}
; + return
{getRow(data[index], index)}
; }; return ( @@ -71,7 +71,7 @@ export function ListTable({ {Row} ) : ( - data.map(row => getRow(row)) + data.map(getRow) )}
@@ -97,9 +97,7 @@ const AnimatedRow = ({ label, value = 0, percent, animate, showPercentage = true {showPercentage && (
`${n}%`) }} /> - - {props.width.to(n => `${n?.toFixed?.(0)}%`)} - + {props.width.to(n => `${n?.toFixed?.(0)}%`)}
)}
diff --git a/src/components/metrics/MetricsTable.tsx b/src/components/metrics/MetricsTable.tsx index 857c136b2..6da0e289f 100644 --- a/src/components/metrics/MetricsTable.tsx +++ b/src/components/metrics/MetricsTable.tsx @@ -18,7 +18,6 @@ import styles from './MetricsTable.module.css'; export interface MetricsTableProps extends ListTableProps { websiteId: string; - domainName: string; type?: string; className?: string; dataFilter?: (data: any) => any; @@ -27,6 +26,7 @@ export interface MetricsTableProps extends ListTableProps { onDataLoad?: (data: any) => void; onSearch?: (search: string) => void; allowSearch?: boolean; + showMore?: boolean; children?: ReactNode; } @@ -39,6 +39,7 @@ export function MetricsTable({ onDataLoad, delay = null, allowSearch = false, + showMore = true, children, ...props }: MetricsTableProps) { @@ -98,7 +99,7 @@ export function MetricsTable({ )} {!data && isLoading && !isFetched && }
- {data && !error && limit && ( + {showMore && data && !error && limit && ( {formatMessage(labels.more)} diff --git a/src/components/metrics/PagesTable.tsx b/src/components/metrics/PagesTable.tsx index b0da80806..ca3f6be5c 100644 --- a/src/components/metrics/PagesTable.tsx +++ b/src/components/metrics/PagesTable.tsx @@ -4,18 +4,21 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable'; import { useMessages } from 'components/hooks'; import { useNavigation } from 'components/hooks'; import { emptyFilter } from 'lib/filters'; +import { useContext } from 'react'; +import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider'; export interface PagesTableProps extends MetricsTableProps { allowFilter?: boolean; } -export function PagesTable({ allowFilter, domainName, ...props }: PagesTableProps) { +export function PagesTable({ allowFilter, ...props }: PagesTableProps) { const { router, renderUrl, query: { view = 'url' }, } = useNavigation(); const { formatMessage, labels } = useMessages(); + const { domain } = useContext(WebsiteContext); const handleSelect = (key: any) => { router.push(renderUrl({ view: key }), { scroll: true }); @@ -39,9 +42,7 @@ export function PagesTable({ allowFilter, domainName, ...props }: PagesTableProp value={x} label={!x && formatMessage(labels.none)} externalUrl={ - view === 'url' - ? `${domainName.startsWith('http') ? domainName : `https://${domainName}`}${x}` - : null + view === 'url' ? `${domain.startsWith('http') ? domain : `https://${domain}`}${x}` : null } /> ); @@ -50,12 +51,11 @@ export function PagesTable({ allowFilter, domainName, ...props }: PagesTableProp return ( {allowFilter && } From b7a7d4de4d370d86d635fab9b9c621adce1978c7 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 26 May 2024 17:26:15 -0700 Subject: [PATCH 28/30] Added comparison tables. --- README.md | 24 ++-- package.json | 6 +- .../[websiteId]/WebsiteMetricsBar.tsx | 2 +- .../compare/WebsiteCompareTables.module.css | 7 + .../compare/WebsiteCompareTables.tsx | 68 ++++++---- .../hooks/queries/useWebsiteMetrics.ts | 8 +- src/components/messages.ts | 4 +- src/components/metrics/ChangeLabel.module.css | 31 +++++ src/components/metrics/ChangeLabel.tsx | 44 +++++++ src/components/metrics/ListTable.module.css | 6 +- src/components/metrics/ListTable.tsx | 6 +- src/components/metrics/MetricCard.module.css | 26 ---- src/components/metrics/MetricCard.tsx | 21 +-- src/components/metrics/MetricsTable.tsx | 4 +- src/components/metrics/PagesTable.tsx | 2 +- src/components/metrics/PageviewsChart.tsx | 4 +- src/pages/api/send.ts | 2 +- yarn.lock | 123 +++++++----------- 18 files changed, 220 insertions(+), 168 deletions(-) create mode 100644 src/components/metrics/ChangeLabel.module.css create mode 100644 src/components/metrics/ChangeLabel.tsx diff --git a/README.md b/README.md index 358fa392a..33a5aac39 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,25 @@

- Umami Logo + Umami Logo

Umami

-

Empowering insights. Preserving privacy.

-

Umami is a simple, fast, privacy-focused alternative to Google Analytics.

- GitHub Release + GitHub Release - MIT License + MIT License - Build Status + Build Status - Umami Demo + Umami Demo

@@ -128,20 +126,20 @@ docker compose up --force-recreate --- -## 📞 Contact +## 🛟 Support

- GitHub + GitHub - Twitter + Twitter - LinkedIn + LinkedIn - - Discord + + Discord

diff --git a/package.json b/package.json index be5bb5ac0..7f5c7c489 100644 --- a/package.json +++ b/package.json @@ -64,9 +64,9 @@ ".next/cache" ], "dependencies": { - "@clickhouse/client": "^1.0.1", + "@clickhouse/client": "^1.0.2", "@fontsource/inter": "^4.5.15", - "@prisma/client": "5.13.0", + "@prisma/client": "5.14.0", "@prisma/extension-read-replicas": "^0.3.0", "@react-spring/web": "^9.7.3", "@tanstack/react-query": "^5.28.6", @@ -102,7 +102,7 @@ "next-basics": "^0.39.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", - "prisma": "5.13.0", + "prisma": "5.14.0", "react": "^18.2.0", "react-basics": "^0.123.0", "react-beautiful-dnd": "^13.1.0", diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx index 90abc814a..5a6478482 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx @@ -70,7 +70,7 @@ export function WebsiteMetricsBar({ const items = [ { label: formatMessage(labels.previousPeriod), value: 'prev' }, - { label: formatMessage(labels.yearOverYear), value: 'yoy' }, + { label: formatMessage(labels.previousYear), value: 'yoy' }, ]; return ( diff --git a/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.module.css b/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.module.css index 4595f5738..c4821e886 100644 --- a/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.module.css +++ b/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.module.css @@ -4,4 +4,11 @@ .nav { width: 200px; + margin-top: 40px; +} + +.title { + color: var(--base800); + text-align: center; + font-weight: 700; } diff --git a/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx b/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx index f58197700..1b21103da 100644 --- a/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx +++ b/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx @@ -1,5 +1,6 @@ +import { useState } from 'react'; import SideNav from 'components/layout/SideNav'; -import { useMessages, useNavigation } from 'components/hooks'; +import { useDateRange, useMessages, useNavigation } from 'components/hooks'; import PagesTable from 'components/metrics/PagesTable'; import ReferrersTable from 'components/metrics/ReferrersTable'; import BrowsersTable from 'components/metrics/BrowsersTable'; @@ -13,11 +14,12 @@ import LanguagesTable from 'components/metrics/LanguagesTable'; import EventsTable from 'components/metrics/EventsTable'; import QueryParametersTable from 'components/metrics/QueryParametersTable'; import { Grid, GridRow } from 'components/layout/Grid'; -import styles from './WebsiteCompareTables.module.css'; -import { useContext, useState } from 'react'; import MetricsTable from 'components/metrics/MetricsTable'; -import FilterLink from 'components/common/FilterLink'; -import { WebsiteContext } from '../WebsiteProvider'; +import useStore from 'store/websites'; +import { getCompareDate } from 'lib/date'; +import { formatNumber } from 'lib/format'; +import ChangeLabel from 'components/metrics/ChangeLabel'; +import styles from './WebsiteCompareTables.module.css'; const views = { url: PagesTable, @@ -36,14 +38,15 @@ const views = { }; export function WebsiteCompareTables({ websiteId }: { websiteId: string }) { - const { domain } = useContext(WebsiteContext); const [data, setData] = useState([]); + const { dateRange } = useDateRange(websiteId); + const dateCompare = useStore(state => state[websiteId]?.dateCompare); const { formatMessage, labels } = useMessages(); const { renderUrl, query: { view }, } = useNavigation(); - const Component: typeof MetricsTable = views[view] || (() => null); + const Component: typeof MetricsTable = views[view || 'url'] || (() => null); const items = [ { @@ -108,27 +111,48 @@ export function WebsiteCompareTables({ websiteId }: { websiteId: string }) { }, ]; - const renderLabel = ({ x, y }, index) => { - return ( - - {y} : {data[index]?.y} ! - - ); + const renderChange = ({ x, y }) => { + const prev = data.find(d => d.x === x)?.y; + const value = y - prev; + const change = Math.abs(((y - prev) / prev) * 100); + + return !isNaN(change) && {formatNumber(change)}%; + }; + + const { startDate, endDate } = getCompareDate( + dateCompare, + dateRange.startDate, + dateRange.endDate, + ); + + const params = { + startAt: startDate.getTime(), + endAt: endDate.getTime(), }; return ( - - +
+
{formatMessage(labels.previous)}
+ +
+
+
{formatMessage(labels.current)}
+ +
); diff --git a/src/components/hooks/queries/useWebsiteMetrics.ts b/src/components/hooks/queries/useWebsiteMetrics.ts index dd201155b..088b31ac4 100644 --- a/src/components/hooks/queries/useWebsiteMetrics.ts +++ b/src/components/hooks/queries/useWebsiteMetrics.ts @@ -4,7 +4,7 @@ import { useFilterParams } from '../useFilterParams'; export function useWebsiteMetrics( websiteId: string, - query: { type: string; limit: number; search: string }, + queryParams: { type: string; limit: number; search: string; startAt?: number; endAt?: number }, options?: Omit void }, 'queryKey' | 'queryFn'>, ) { const { get, useQuery } = useApi(); @@ -16,17 +16,17 @@ export function useWebsiteMetrics( { websiteId, ...params, - ...query, + ...queryParams, }, ], queryFn: async () => { const filters = { ...params }; - filters[query.type] = undefined; + filters[queryParams.type] = undefined; const data = await get(`/websites/${websiteId}/metrics`, { ...filters, - ...query, + ...queryParams, }); options?.onDataLoad?.(data); diff --git a/src/components/messages.ts b/src/components/messages.ts index f4d986ff7..53e69401c 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -253,8 +253,10 @@ export const labels = defineMessages({ defaultMessage: 'Understand how users nagivate through your website.', }, compare: { id: 'label.compare', defaultMessage: 'Compare' }, + current: { id: 'label.current', defaultMessage: 'Current' }, + previous: { id: 'label.previous', defaultMessage: 'Previous' }, previousPeriod: { id: 'label.previous-period', defaultMessage: 'Previous period' }, - yearOverYear: { id: 'label.year-over-year', defaultMessage: 'Year over year' }, + previousYear: { id: 'label.previous-year', defaultMessage: 'Previous year' }, }); export const messages = defineMessages({ diff --git a/src/components/metrics/ChangeLabel.module.css b/src/components/metrics/ChangeLabel.module.css new file mode 100644 index 000000000..110f9a0da --- /dev/null +++ b/src/components/metrics/ChangeLabel.module.css @@ -0,0 +1,31 @@ +.label { + display: flex; + align-items: center; + gap: 5px; + font-size: 13px; + font-weight: 700; + padding: 0.1em 0.5em; + border-radius: 5px; + color: var(--base500); + align-self: flex-start; +} + +.positive { + color: var(--green700); + background: var(--green100); +} + +.negative { + color: var(--red700); + background: var(--red100); +} + +.neutral { + color: var(--base700); + background: var(--base100); +} + +.new { + color: var(--blue900); + background: var(--blue100); +} diff --git a/src/components/metrics/ChangeLabel.tsx b/src/components/metrics/ChangeLabel.tsx new file mode 100644 index 000000000..6eefc55ba --- /dev/null +++ b/src/components/metrics/ChangeLabel.tsx @@ -0,0 +1,44 @@ +import classNames from 'classnames'; +import { Icon, Icons } from 'react-basics'; +import { ReactNode } from 'react'; +import styles from './ChangeLabel.module.css'; + +export function ChangeLabel({ + value, + size, + reverseColors, + className, + children, +}: { + value: number; + size?: 'xs' | 'sm' | 'md' | 'lg'; + reverseColors?: boolean; + showPercentage?: boolean; + className?: string; + children?: ReactNode; +}) { + const positive = value * (reverseColors ? -1 : 1) >= 0; + const negative = value * (reverseColors ? -1 : 1) < 0; + const isNew = isNaN(value); + + return ( +
+ {!isNew && ( + + + + )} + {children || value} +
+ ); +} + +export default ChangeLabel; diff --git a/src/components/metrics/ListTable.module.css b/src/components/metrics/ListTable.module.css index 9c3d5cff6..405819b1c 100644 --- a/src/components/metrics/ListTable.module.css +++ b/src/components/metrics/ListTable.module.css @@ -72,9 +72,11 @@ } .value { - width: 50px; + display: flex; + align-items: center; + gap: 10px; text-align: end; - margin-inline-end: 10px; + margin-inline-end: 5px; font-weight: 600; } diff --git a/src/components/metrics/ListTable.tsx b/src/components/metrics/ListTable.tsx index 14f2c9900..59ded4917 100644 --- a/src/components/metrics/ListTable.tsx +++ b/src/components/metrics/ListTable.tsx @@ -15,6 +15,7 @@ export interface ListTableProps { metric?: string; className?: string; renderLabel?: (row: any, index: number) => ReactNode; + renderChange?: (row: any, index: number) => ReactNode; animate?: boolean; virtualize?: boolean; showPercentage?: boolean; @@ -27,6 +28,7 @@ export function ListTable({ metric, className, renderLabel, + renderChange, animate = true, virtualize = false, showPercentage = true, @@ -45,6 +47,7 @@ export function ListTable({ percent={percent} animate={animate && !virtualize} showPercentage={showPercentage} + change={renderChange ? renderChange(row, index) : null} /> ); }; @@ -78,7 +81,7 @@ export function ListTable({ ); } -const AnimatedRow = ({ label, value = 0, percent, animate, showPercentage = true }) => { +const AnimatedRow = ({ label, value = 0, percent, change, animate, showPercentage = true }) => { const props = useSpring({ width: percent, y: value, @@ -90,6 +93,7 @@ const AnimatedRow = ({ label, value = 0, percent, animate, showPercentage = true
{label}
+ {change} {props.y?.to(formatLongNumber)} diff --git a/src/components/metrics/MetricCard.module.css b/src/components/metrics/MetricCard.module.css index ee8f55b17..93e6c6d72 100644 --- a/src/components/metrics/MetricCard.module.css +++ b/src/components/metrics/MetricCard.module.css @@ -35,29 +35,3 @@ white-space: nowrap; color: var(--base800); } - -.change { - display: flex; - align-items: center; - gap: 5px; - font-size: 13px; - font-weight: 700; - padding: 0.1em 0.5em; - border-radius: 5px; - color: var(--base500); - align-self: flex-start; -} - -.change.positive { - color: var(--green700); - background: var(--green100); -} - -.change.negative { - color: var(--red700); - background: var(--red100); -} - -.hide { - visibility: hidden; -} diff --git a/src/components/metrics/MetricCard.tsx b/src/components/metrics/MetricCard.tsx index 907263afd..ecb4f2dcd 100644 --- a/src/components/metrics/MetricCard.tsx +++ b/src/components/metrics/MetricCard.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; -import { Icon, Icons } from 'react-basics'; import { useSpring, animated } from '@react-spring/web'; import { formatNumber } from 'lib/format'; +import ChangeLabel from 'components/metrics/ChangeLabel'; import styles from './MetricCard.module.css'; export interface MetricCardProps { @@ -31,30 +31,19 @@ export const MetricCard = ({ const props = useSpring({ x: Number(value) || 0, from: { x: 0 } }); const changeProps = useSpring({ x: Number(change) || 0, from: { x: 0 } }); const prevProps = useSpring({ x: Number(value - change) || 0, from: { x: 0 } }); - const positive = change * (reverseColors ? -1 : 1) >= 0; - const negative = change * (reverseColors ? -1 : 1) < 0; return (
{showLabel &&
{label}
} - + {props?.x?.to(x => format(x))} {showChange && ( -
- - - - + + {changeProps?.x?.to(x => format(Math.abs(x)))} -
+ )} {showPrevious && ( diff --git a/src/components/metrics/MetricsTable.tsx b/src/components/metrics/MetricsTable.tsx index 6da0e289f..4ca3ff522 100644 --- a/src/components/metrics/MetricsTable.tsx +++ b/src/components/metrics/MetricsTable.tsx @@ -27,6 +27,7 @@ export interface MetricsTableProps extends ListTableProps { onSearch?: (search: string) => void; allowSearch?: boolean; showMore?: boolean; + params?: { [key: string]: any }; children?: ReactNode; } @@ -40,6 +41,7 @@ export function MetricsTable({ delay = null, allowSearch = false, showMore = true, + params, children, ...props }: MetricsTableProps) { @@ -51,7 +53,7 @@ export function MetricsTable({ const { data, isLoading, isFetched, error } = useWebsiteMetrics( websiteId, - { type, limit, search }, + { type, limit, search, ...params }, { retryDelay: delay || DEFAULT_ANIMATION_DURATION, onDataLoad, diff --git a/src/components/metrics/PagesTable.tsx b/src/components/metrics/PagesTable.tsx index ca3f6be5c..d29952d4f 100644 --- a/src/components/metrics/PagesTable.tsx +++ b/src/components/metrics/PagesTable.tsx @@ -55,7 +55,7 @@ export function PagesTable({ allowFilter, ...props }: PagesTableProps) { type={view} metric={formatMessage(labels.views)} dataFilter={emptyFilter} - renderLabel={props.renderLabel || renderLink} + renderLabel={renderLink} > {allowFilter && } diff --git a/src/components/metrics/PageviewsChart.tsx b/src/components/metrics/PageviewsChart.tsx index 191b1490e..dabc2ce3b 100644 --- a/src/components/metrics/PageviewsChart.tsx +++ b/src/components/metrics/PageviewsChart.tsx @@ -46,7 +46,7 @@ export function PageviewsChart({ data, unit, isLoading, ...props }: PageviewsCha ? [ { type: 'line', - label: `${formatMessage(labels.visits)} (VS)`, + label: `${formatMessage(labels.visits)} (${formatMessage(labels.previous)})`, data: data.compare.pageviews, borderWidth: 2, backgroundColor: '#8601B0', @@ -55,7 +55,7 @@ export function PageviewsChart({ data, unit, isLoading, ...props }: PageviewsCha }, { type: 'line', - label: `${formatMessage(labels.visitors)} (VS)`, + label: `${formatMessage(labels.visitors)} (${formatMessage(labels.previous)})`, data: data.compare.sessions, borderWidth: 2, backgroundColor: '#f15bb5', diff --git a/src/pages/api/send.ts b/src/pages/api/send.ts index fe4a2abd2..11ba10d74 100644 --- a/src/pages/api/send.ts +++ b/src/pages/api/send.ts @@ -87,7 +87,7 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => { if (req.method === 'POST') { if (!process.env.DISABLE_BOT_CHECK && isbot(req.headers['user-agent'])) { - return ok(res); + return ok(res, { beep: 'boop' }); } await useValidate(schema, req, res); diff --git a/yarn.lock b/yarn.lock index 80b8e4542..4ba6684d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1206,17 +1206,17 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@clickhouse/client-common@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@clickhouse/client-common/-/client-common-1.0.1.tgz#c7dde5eafaad8189649373ecc23354c7a32847b3" - integrity sha512-3L6e0foP6VOktScoi6XWMjJyOpKCWgLUYgPVxP2c7gm6Kotq+iRmmmXtXTSg7B7uozcLZycTtPfIw2d80SYsYw== +"@clickhouse/client-common@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@clickhouse/client-common/-/client-common-1.0.2.tgz#0fe0a4b33101c08d85c1279e4d74b2a92d42d996" + integrity sha512-5oI2URFsXlzoysv5lAxoTUAnAHjXnaJ+1Jz3HUARR06Hkbr1sN0pGxfGwgjEd8E/lI4+UNdNEZicG2rlFnWSaA== -"@clickhouse/client@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@clickhouse/client/-/client-1.0.1.tgz#364db28d9ef9beaf19104f962c2b06090cb10468" - integrity sha512-fluUNnE2R7COJ6rn6DorYfi4D+AQn3t2qeBtEq37bQV3pD4EbKrBfKAwJ13e1lmMWdQ2B9bJUTMqGsRIDdWhJw== +"@clickhouse/client@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@clickhouse/client/-/client-1.0.2.tgz#7d9675e697ce697f1e6777f4c66ca6d3384e7325" + integrity sha512-PaK0GLjIrlCpXevrp9gliOrurna6MjMMFBgZhDh6Zup8IuJCjQru4LkNsWUl3hJ2nua6+Ygag14iB8ILbeaIjg== dependencies: - "@clickhouse/client-common" "1.0.1" + "@clickhouse/client-common" "1.0.2" "@colors/colors@1.5.0": version "1.5.0" @@ -2089,51 +2089,51 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@prisma/client@5.13.0": - version "5.13.0" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.13.0.tgz#b9f1d0983d714e982675201d8222a9ecb4bdad4a" - integrity sha512-uYdfpPncbZ/syJyiYBwGZS8Gt1PTNoErNYMuqHDa2r30rNSFtgTA/LXsSk55R7pdRTMi5pHkeP9B14K6nHmwkg== +"@prisma/client@5.14.0": + version "5.14.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.14.0.tgz#dadca5bb1137ddcebb454bbdaf89423823d3363f" + integrity sha512-akMSuyvLKeoU4LeyBAUdThP/uhVP3GuLygFE3MlYzaCb3/J8SfsYBE5PkaFuLuVpLyA6sFoW+16z/aPhNAESqg== -"@prisma/debug@5.13.0": - version "5.13.0" - resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.13.0.tgz#d88b0f6fafa0c216e20e284ed9fc30f1cbe45786" - integrity sha512-699iqlEvzyCj9ETrXhs8o8wQc/eVW+FigSsHpiskSFydhjVuwTJEfj/nIYqTaWFYuxiWQRfm3r01meuW97SZaQ== +"@prisma/debug@5.14.0": + version "5.14.0" + resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.14.0.tgz#1227c705893c38284f7c63d72441480ebaa12605" + integrity sha512-iq56qBZuFfX3fCxoxT8gBX33lQzomBU0qIUaEj1RebsKVz1ob/BVH1XSBwwwvRVtZEV1b7Fxx2eVu34Ge/mg3w== -"@prisma/engines-version@5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b": - version "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b.tgz#a72a4fb83ba1fd01ad45f795aa55168f60d34723" - integrity sha512-AyUuhahTINGn8auyqYdmxsN+qn0mw3eg+uhkp8zwknXYIqoT3bChG4RqNY/nfDkPvzWAPBa9mrDyBeOnWSgO6A== +"@prisma/engines-version@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48": + version "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48.tgz#019c3c75a5c3276e580685fe48cdbfd181176858" + integrity sha512-ip6pNkRo1UxWv+6toxNcYvItNYaqQjXdFNGJ+Nuk2eYtRoEdoF13wxo7/jsClJFFenMPVNVqXQDV0oveXnR1cA== -"@prisma/engines@5.13.0": - version "5.13.0" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.13.0.tgz#8994ebf7b4e35aee7746a8465ec22738379bcab6" - integrity sha512-hIFLm4H1boj6CBZx55P4xKby9jgDTeDG0Jj3iXtwaaHmlD5JmiDkZhh8+DYWkTGchu+rRF36AVROLnk0oaqhHw== +"@prisma/engines@5.14.0": + version "5.14.0" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.14.0.tgz#2ee91dd2220a726c27c906fbea788bbb3efdac6e" + integrity sha512-lgxkKZ6IEygVcw6IZZUlPIfLQ9hjSYAtHjZ5r64sCLDgVzsPFCi2XBBJgzPMkOQ5RHzUD4E/dVdpn9+ez8tk1A== dependencies: - "@prisma/debug" "5.13.0" - "@prisma/engines-version" "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b" - "@prisma/fetch-engine" "5.13.0" - "@prisma/get-platform" "5.13.0" + "@prisma/debug" "5.14.0" + "@prisma/engines-version" "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48" + "@prisma/fetch-engine" "5.14.0" + "@prisma/get-platform" "5.14.0" "@prisma/extension-read-replicas@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@prisma/extension-read-replicas/-/extension-read-replicas-0.3.0.tgz#2842a7c928f957c1dd58a6256104797596d43426" integrity sha512-F9+rSmYday6GT2qjhJtkZcBOpLO5LtpvFcMGqrBDHf+78LEdSuxfFjOxYlNuqk4B+th62yxpbhfpmB9/Mca14Q== -"@prisma/fetch-engine@5.13.0": - version "5.13.0" - resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.13.0.tgz#9b6945c7b38bb59e840f8905b20ea7a3d059ca55" - integrity sha512-Yh4W+t6YKyqgcSEB3odBXt7QyVSm0OQlBSldQF2SNXtmOgMX8D7PF/fvH6E6qBCpjB/yeJLy/FfwfFijoHI6sA== +"@prisma/fetch-engine@5.14.0": + version "5.14.0" + resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.14.0.tgz#45297c118d4ec3fea55129886edd5a429da1f6da" + integrity sha512-VrheA9y9DMURK5vu8OJoOgQpxOhas3qF0IBHJ8G/0X44k82kc8E0w98HCn2nhnbOOMwbWsJWXfLC2/F8n5u0gQ== dependencies: - "@prisma/debug" "5.13.0" - "@prisma/engines-version" "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b" - "@prisma/get-platform" "5.13.0" + "@prisma/debug" "5.14.0" + "@prisma/engines-version" "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48" + "@prisma/get-platform" "5.14.0" -"@prisma/get-platform@5.13.0": - version "5.13.0" - resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.13.0.tgz#99ef909a52b9d79b64d72d2d3d8210c4892b6572" - integrity sha512-B/WrQwYTzwr7qCLifQzYOmQhZcFmIFhR81xC45gweInSUn2hTEbfKUPd2keAog+y5WI5xLAFNJ3wkXplvSVkSw== +"@prisma/get-platform@5.14.0": + version "5.14.0" + resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.14.0.tgz#69112d3dde61905f59a65ed818f153e153ca40f0" + integrity sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw== dependencies: - "@prisma/debug" "5.13.0" + "@prisma/debug" "5.14.0" "@react-spring/animated@~9.7.3": version "9.7.3" @@ -8659,12 +8659,12 @@ pretty-format@^29.0.0, pretty-format@^29.7.0: ansi-styles "^5.0.0" react-is "^18.0.0" -prisma@5.13.0: - version "5.13.0" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.13.0.tgz#1f06e20ccfb6038ad68869e6eacd3b346f9d0851" - integrity sha512-kGtcJaElNRAdAGsCNykFSZ7dBKpL14Cbs+VaQ8cECxQlRPDjBlMHNFYeYt0SKovAVy2Y65JXQwB3A5+zIQwnTg== +prisma@5.14.0: + version "5.14.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.14.0.tgz#ffc4696a43b044b636c3303b7aa98c13c2ade4dd" + integrity sha512-gCNZco7y5XtjrnQYeDJTiVZmT/ncqCr5RY1/Cf8X2wgLRmyh9ayPAGBNziI4qEE4S6SxCH5omQLVo9lmURaJ/Q== dependencies: - "@prisma/engines" "5.13.0" + "@prisma/engines" "5.14.0" process@^0.11.10: version "0.11.10" @@ -9590,16 +9590,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9672,14 +9663,7 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10440,7 +10424,7 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -10458,15 +10442,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 8b304b7ca2c17cca2e797ccb288a965486fd6cbe Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 26 May 2024 21:30:03 -0700 Subject: [PATCH 29/30] Show percentages in metrics bar. --- .../[websiteId]/WebsiteMetricsBar.tsx | 16 ++++++++----- src/components/metrics/MetricCard.tsx | 23 +++++++++++-------- src/components/metrics/PageviewsChart.tsx | 2 +- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx index 5a6478482..b74482a47 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx @@ -3,7 +3,7 @@ import { useMessages, useSticky } from 'components/hooks'; import WebsiteDateFilter from 'components/input/WebsiteDateFilter'; import MetricCard from 'components/metrics/MetricCard'; import MetricsBar from 'components/metrics/MetricsBar'; -import { formatShortTime } from 'lib/format'; +import { formatShortTime, formatLongNumber } from 'lib/format'; import WebsiteFilterButton from './WebsiteFilterButton'; import useWebsiteStats from 'components/hooks/queries/useWebsiteStats'; import styles from './WebsiteMetricsBar.module.css'; @@ -37,16 +37,19 @@ export function WebsiteMetricsBar({ ...pageviews, label: formatMessage(labels.views), change: pageviews.value - pageviews.prev, + formatValue: formatLongNumber, }, { ...visits, label: formatMessage(labels.visits), change: visits.value - visits.prev, + formatValue: formatLongNumber, }, { ...visitors, label: formatMessage(labels.visitors), change: visitors.value - visitors.prev, + formatValue: formatLongNumber, }, { label: formatMessage(labels.bounceRate), @@ -55,7 +58,7 @@ export function WebsiteMetricsBar({ change: (Math.min(visitors.value, bounces.value) / visitors.value) * 100 - (Math.min(visitors.prev, bounces.prev) / visitors.prev) * 100, - format: n => Number(n).toFixed(0) + '%', + formatValue: n => Number(n).toFixed(0) + '%', reverseColors: true, }, { @@ -63,7 +66,8 @@ export function WebsiteMetricsBar({ value: totaltime.value / visits.value, prev: totaltime.prev / visits.prev, change: totaltime.value / visits.value - totaltime.prev / visits.prev, - format: n => `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`, + formatValue: n => + `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`, }, ] : []; @@ -78,12 +82,12 @@ export function WebsiteMetricsBar({ ref={ref} className={classNames(styles.container, { [styles.sticky]: sticky, - [styles.isSticky]: isSticky, + [styles.isSticky]: sticky && isSticky, })} >
- {metrics.map(({ label, value, prev, change, format, reverseColors }) => { + {metrics.map(({ label, value, prev, change, formatValue, reverseColors }) => { return ( { + const diff = value - change; + const pct = ((value - diff) / diff) * 100; const props = useSpring({ x: Number(value) || 0, from: { x: 0 } }); - const changeProps = useSpring({ x: Number(change) || 0, from: { x: 0 } }); - const prevProps = useSpring({ x: Number(value - change) || 0, from: { x: 0 } }); + const changeProps = useSpring({ x: Number(pct) || 0, from: { x: 0 } }); + const prevProps = useSpring({ x: Number(diff) || 0, from: { x: 0 } }); return (
{showLabel &&
{label}
} - - {props?.x?.to(x => format(x))} + + {props?.x?.to(x => formatValue(x))} {showChange && ( - - {changeProps?.x?.to(x => format(Math.abs(x)))} + + {changeProps?.x?.to(x => Math.abs(~~x))} + % )} {showPrevious && ( - - {prevProps?.x?.to(x => format(x))} + + {prevProps?.x?.to(x => formatValue(x))} )}
diff --git a/src/components/metrics/PageviewsChart.tsx b/src/components/metrics/PageviewsChart.tsx index dabc2ce3b..400b96ba3 100644 --- a/src/components/metrics/PageviewsChart.tsx +++ b/src/components/metrics/PageviewsChart.tsx @@ -46,7 +46,7 @@ export function PageviewsChart({ data, unit, isLoading, ...props }: PageviewsCha ? [ { type: 'line', - label: `${formatMessage(labels.visits)} (${formatMessage(labels.previous)})`, + label: `${formatMessage(labels.views)} (${formatMessage(labels.previous)})`, data: data.compare.pageviews, borderWidth: 2, backgroundColor: '#8601B0', From b8b1ff8fd426eadf5168eb4797785c0c769d9dc0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 26 May 2024 21:57:43 -0700 Subject: [PATCH 30/30] Remove journey report. --- src/app/(main)/reports/create/ReportTemplates.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/app/(main)/reports/create/ReportTemplates.tsx b/src/app/(main)/reports/create/ReportTemplates.tsx index 0777cc1f9..fdf5c5f52 100644 --- a/src/app/(main)/reports/create/ReportTemplates.tsx +++ b/src/app/(main)/reports/create/ReportTemplates.tsx @@ -6,7 +6,6 @@ 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'; @@ -45,12 +44,6 @@ 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 (