diff --git a/next.config.ts b/next.config.ts
index 02851c82..b63abebb 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -130,7 +130,7 @@ const redirects = [
},
{
source: '/teams/:id',
- destination: '/teams/:id/dashboard',
+ destination: '/teams/:id/websites',
permanent: true,
},
{
diff --git a/package.json b/package.json
index 516732c5..7a06fdfa 100644
--- a/package.json
+++ b/package.json
@@ -21,9 +21,10 @@
"start-server": "node server.js",
"build-app": "next build",
"build-icons": "svgr ./src/assets --out-dir src/components/svg --typescript",
- "build-components": "rollup -c rollup.components.config.mjs",
- "build-tracker": "rollup -c rollup.tracker.config.mjs",
- "build-db": "npm-run-all copy-db-files build-db-client",
+ "build-components": "rollup -c rollup.components.config.js",
+ "build-tracker": "rollup -c rollup.tracker.config.js",
+ "build-prisma-client": "node scripts/build-prisma-client.js",
+ "build-db": "npm-run-all copy-db-files build-db-client build-prisma-client",
"build-lang": "npm-run-all format-lang compile-lang download-country-names download-language-names clean-lang",
"build-geo": "node scripts/build-geo.js",
"build-db-schema": "prisma db pull",
@@ -80,7 +81,7 @@
"@react-spring/web": "^9.7.3",
"@svgr/cli": "^8.1.0",
"@tanstack/react-query": "^5.28.6",
- "@umami/react-zen": "^0.133.0",
+ "@umami/react-zen": "^0.134.0",
"@umami/redis-client": "^0.27.0",
"bcryptjs": "^2.4.3",
"chalk": "^4.1.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 269c7c15..6d7a3926 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -45,8 +45,8 @@ importers:
specifier: ^5.28.6
version: 5.77.2(react@19.1.0)
'@umami/react-zen':
- specifier: ^0.133.0
- version: 0.133.0(@babel/core@7.27.1)(@types/react@19.1.5)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))
+ specifier: ^0.134.0
+ version: 0.134.0(@babel/core@7.27.1)(@types/react@19.1.5)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))
'@umami/redis-client':
specifier: ^0.27.0
version: 0.27.0
@@ -2585,8 +2585,8 @@ packages:
resolution: {integrity: sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@umami/react-zen@0.133.0':
- resolution: {integrity: sha512-AAhtYdmLwVZ4i5lzcr5mylc5IQIirlxEL0bMRktlFHbX73wiBrTnk2RrYjmRhCe2KVRkCw2EF8ORXQ7GFrfAOg==}
+ '@umami/react-zen@0.134.0':
+ resolution: {integrity: sha512-RBSD50mTw2YKY0Z73OSxVtjrMvIq3nGtWYtcZHPXl/4oYj3Ph0cKTKto14Jx2qs2kHm2DxcS3ND1FR1OrPEknw==}
'@umami/redis-client@0.27.0':
resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==}
@@ -9805,7 +9805,7 @@ snapshots:
'@typescript-eslint/types': 8.32.1
eslint-visitor-keys: 4.2.0
- '@umami/react-zen@0.133.0(@babel/core@7.27.1)(@types/react@19.1.5)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))':
+ '@umami/react-zen@0.134.0(@babel/core@7.27.1)(@types/react@19.1.5)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))':
dependencies:
'@fontsource/jetbrains-mono': 5.2.5
'@internationalized/date': 3.8.2
diff --git a/src/app/(main)/SideNav.tsx b/src/app/(main)/SideNav.tsx
index c6f99d96..cdc90cec 100644
--- a/src/app/(main)/SideNav.tsx
+++ b/src/app/(main)/SideNav.tsx
@@ -1,7 +1,6 @@
import Link from 'next/link';
import { Sidebar, SidebarHeader, SidebarSection, SidebarItem } from '@umami/react-zen';
import {
- Copy,
Globe,
LayoutDashboard,
Link as LinkIcon,
@@ -17,11 +16,6 @@ export function SideNav(props: any) {
const [isCollapsed] = useGlobalState('sidenav-collapsed');
const links = [
- {
- label: formatMessage(labels.dashboard),
- href: renderTeamUrl('/dashboard'),
- icon: ,
- },
{
label: formatMessage(labels.websites),
href: renderTeamUrl('/websites'),
diff --git a/src/app/(main)/dashboard/DashboardEdit.module.css b/src/app/(main)/dashboard/DashboardEdit.module.css
deleted file mode 100644
index 19266d17..00000000
--- a/src/app/(main)/dashboard/DashboardEdit.module.css
+++ /dev/null
@@ -1,57 +0,0 @@
-.buttons {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- gap: 10px;
-}
-
-.item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- width: 100%;
- padding: 20px;
- border-radius: 5px;
- border: 1px solid var(--base400);
- background: var(--base50);
- margin-bottom: 10px;
-}
-
-.text {
- position: relative;
-}
-
-.name {
- font-weight: 600;
- font-size: 16px;
-}
-
-.domain {
- font-size: 14px;
- color: var(--base700);
-}
-
-.dragActive {
- cursor: grab;
-}
-
-.dragActive:active {
- cursor: grabbing;
-}
-
-.header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 20px;
- gap: 20px;
-}
-
-.search {
- max-width: 360px;
-}
-
-.active {
- border-color: var(--base600);
- box-shadow: 4px 4px 4px var(--base100);
-}
diff --git a/src/app/(main)/dashboard/DashboardEdit.tsx b/src/app/(main)/dashboard/DashboardEdit.tsx
deleted file mode 100644
index 7dabdf64..00000000
--- a/src/app/(main)/dashboard/DashboardEdit.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import { useState, useMemo, useEffect } from 'react';
-import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
-import classNames from 'classnames';
-import { Button, Loading, Toggle, SearchField } from '@umami/react-zen';
-import { firstBy } from 'thenby';
-import { useDashboard, saveDashboard } from '@/store/dashboard';
-import { useMessages, useWebsites } from '@/components/hooks';
-import styles from './DashboardEdit.module.css';
-
-const DRAG_ID = 'dashboard-website-ordering';
-
-export function DashboardEdit({ teamId }: { teamId: string }) {
- const settings = useDashboard();
- const { websiteOrder, websiteActive, isEdited } = settings;
- const { formatMessage, labels } = useMessages();
- const [order, setOrder] = useState(websiteOrder || []);
- const [active, setActive] = useState(websiteActive || []);
- const [edited, setEdited] = useState(isEdited);
- const [websites, setWebsites] = useState([]);
- const [search, setSearch] = useState('');
-
- const {
- result,
- query: { isLoading },
- setParams,
- } = useWebsites({ teamId });
-
- useEffect(() => {
- if (result?.data) {
- setWebsites(prevWebsites => {
- const newWebsites = [...prevWebsites, ...result.data];
- if (newWebsites.length < result.count) {
- setParams(prevParams => ({ ...prevParams, page: prevParams.page + 1 }));
- }
- return newWebsites;
- });
- }
- }, [result]);
-
- const ordered = useMemo(() => {
- if (websites) {
- return websites
- .map((website: { id: any; name: string; domain: string }) => ({
- ...website,
- order: order.indexOf(website.id),
- }))
- .sort(firstBy('order'));
- }
- return [];
- }, [websites, order]);
-
- function handleWebsiteDrag({ destination, source }) {
- if (!destination || destination.index === source.index) return;
-
- const orderedWebsites = [...ordered];
- const [removed] = orderedWebsites.splice(source.index, 1);
- orderedWebsites.splice(destination.index, 0, removed);
-
- setOrder(orderedWebsites.map(website => website?.id || 0));
- setEdited(true);
- }
-
- function handleActiveWebsites(id: string) {
- setActive(prevActive =>
- prevActive.includes(id) ? prevActive.filter(a => a !== id) : [...prevActive, id],
- );
- setEdited(true);
- }
-
- function handleSave() {
- saveDashboard({
- editing: false,
- isEdited: edited,
- websiteOrder: order,
- websiteActive: active,
- });
- }
-
- function handleCancel() {
- saveDashboard({ editing: false, websiteOrder, websiteActive, isEdited });
- }
-
- function handleReset() {
- setOrder([]);
- setActive([]);
- setEdited(false);
- }
-
- if (isLoading) {
- return ;
- }
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
- {(provided, snapshot) => (
-
- {ordered.map(({ id, name, domain }, index) => {
- if (
- search &&
- !`${name.toLowerCase()}${domain.toLowerCase()}`.includes(search.toLowerCase())
- ) {
- return null;
- }
-
- return (
-
- {(provided, snapshot) => (
-
-
-
handleActiveWebsites(id)}
- />
-
- )}
-
- );
- })}
- {provided.placeholder}
-
- )}
-
-
-
- >
- );
-}
diff --git a/src/app/(main)/dashboard/DashboardPage.tsx b/src/app/(main)/dashboard/DashboardPage.tsx
deleted file mode 100644
index c7306087..00000000
--- a/src/app/(main)/dashboard/DashboardPage.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-'use client';
-import { Icon, Loading, Text } from '@umami/react-zen';
-import { SectionHeader } from '@/components/common/SectionHeader';
-import { Pager } from '@/components/common/Pager';
-import { WebsiteChartList } from '../websites/[websiteId]/WebsiteChartList';
-import { DashboardSettingsButton } from '@/app/(main)/dashboard/DashboardSettingsButton';
-import { DashboardEdit } from '@/app/(main)/dashboard/DashboardEdit';
-import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder';
-import { useMessages, useNavigation, useWebsites } from '@/components/hooks';
-import { Arrow } from '@/components/icons';
-import { useDashboard } from '@/store/dashboard';
-import { LinkButton } from '@/components/common/LinkButton';
-
-export function DashboardPage() {
- const { formatMessage, labels, messages } = useMessages();
- const { teamId, renderTeamUrl } = useNavigation();
- const { showCharts, editing, isEdited } = useDashboard();
- const pageSize = isEdited ? 200 : 10;
-
- const { result, query, params, setParams } = useWebsites({ teamId }, { pageSize });
- const { page } = params;
- const hasData = !!result?.data?.length;
-
- const handlePageChange = (page: number) => {
- setParams({ ...params, page });
- };
-
- if (query.isLoading) {
- return ;
- }
-
- return (
-
-
- {!editing && hasData && }
-
- {!hasData && (
-
-
-
-
-
- {formatMessage(messages.goToSettings)}
-
-
- )}
- {hasData && (
- <>
- {editing && }
- {!editing && (
- <>
-
-
- >
- )}
- >
- )}
-
- );
-}
diff --git a/src/app/(main)/dashboard/DashboardSettingsButton.module.css b/src/app/(main)/dashboard/DashboardSettingsButton.module.css
deleted file mode 100644
index 6e0d19c2..00000000
--- a/src/app/(main)/dashboard/DashboardSettingsButton.module.css
+++ /dev/null
@@ -1,5 +0,0 @@
-.buttonGroup {
- display: flex;
- place-items: center;
- gap: 10px;
-}
diff --git a/src/app/(main)/dashboard/DashboardSettingsButton.tsx b/src/app/(main)/dashboard/DashboardSettingsButton.tsx
deleted file mode 100644
index 12b7e913..00000000
--- a/src/app/(main)/dashboard/DashboardSettingsButton.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { Row, TooltipTrigger, Tooltip, Icon, Text, Button } from '@umami/react-zen';
-import { BarChart, Edit } from '@/components/icons';
-import { saveDashboard } from '@/store/dashboard';
-import { useMessages } from '@/components/hooks';
-
-export function DashboardSettingsButton() {
- const { formatMessage, labels } = useMessages();
-
- const handleToggleCharts = () => {
- saveDashboard(state => ({ showCharts: !state.showCharts }));
- };
-
- const handleEdit = () => {
- saveDashboard({ editing: true });
- };
-
- return (
-
-
-
- {formatMessage(labels.toggleCharts)}
-
-
-
- );
-}
diff --git a/src/app/(main)/dashboard/page.tsx b/src/app/(main)/dashboard/page.tsx
deleted file mode 100644
index 20ac3533..00000000
--- a/src/app/(main)/dashboard/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { DashboardPage } from './DashboardPage';
-import { Metadata } from 'next';
-
-export default function () {
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Dashboard',
-};
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx b/src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx
deleted file mode 100644
index 4a211151..00000000
--- a/src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { Text, Icon } from '@umami/react-zen';
-import { useMemo } from 'react';
-import { firstBy } from 'thenby';
-import { WebsiteChart } from './WebsiteChart';
-import { useDashboard } from '@/store/dashboard';
-import { WebsiteControls } from './WebsiteControls';
-import { WebsiteMetricsBar } from './WebsiteMetricsBar';
-import { useMessages, useNavigation } from '@/components/hooks';
-import { LinkButton } from '@/components/common/LinkButton';
-import { Arrow } from '@/components/icons';
-
-export function WebsiteChartList({
- websites,
- showCharts,
- limit,
-}: {
- websites: any[];
- showCharts?: boolean;
- limit?: number;
-}) {
- const { formatMessage, labels } = useMessages();
- const { websiteOrder, websiteActive } = useDashboard();
- const { renderTeamUrl } = useNavigation();
-
- const ordered = useMemo(() => {
- return websites
- .filter(website => (websiteActive.length ? websiteActive.includes(website.id) : true))
- .map(website => ({ ...website, order: websiteOrder.indexOf(website.id) || 0 }))
- .sort(firstBy('order'));
- }, [websites, websiteOrder, websiteActive]);
-
- return (
-
- {ordered.map(({ id }: { id: string }, index) => {
- return index < limit ? (
-
-
-
- {formatMessage(labels.viewDetails)}
-
-
-
-
-
-
-
-
- {showCharts &&
}
-
- ) : null;
- })}
-
- );
-}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx
index 48927023..557aaac4 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx
@@ -65,7 +65,7 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
},
{
id: 'journeys',
- label: formatMessage(labels.journey),
+ label: formatMessage(labels.journeys),
icon: ,
path: '/journeys',
},
diff --git a/src/app/(main)/websites/[websiteId]/funnels/Funnel.tsx b/src/app/(main)/websites/[websiteId]/funnels/Funnel.tsx
index a5f6bade..6281b5a2 100644
--- a/src/app/(main)/websites/[websiteId]/funnels/Funnel.tsx
+++ b/src/app/(main)/websites/[websiteId]/funnels/Funnel.tsx
@@ -1,4 +1,4 @@
-import { Grid, Column, Row, Text, Icon, ProgressBar, Dialog } from '@umami/react-zen';
+import { Grid, Column, Row, Text, Icon, ProgressBar, Dialog, Box } from '@umami/react-zen';
import { useMessages, useResultQuery } from '@/components/hooks';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { File, Lightning, User } from '@/components/icons';
@@ -13,7 +13,7 @@ type FunnelResult = {
visitors: number;
previous: number;
dropped: number;
- droppoff: number;
+ dropoff: number;
remaining: number;
};
@@ -53,25 +53,35 @@ export function Funnel({ id, name, type, parameters, websiteId, startDate, endDa
{data?.map(
(
- { type, value, visitors, previous, dropped, remaining }: FunnelResult,
+ { type, value, visitors, previous, dropped, dropoff, remaining }: FunnelResult,
index: number,
) => {
const isPage = type === 'page';
return (
-
+
-
+
{index + 1}
+ {index > 0 && (
+
+ )}
@@ -87,12 +97,16 @@ export function Funnel({ id, name, type, parameters, websiteId, startDate, endDa
{index > 0 && (
- {formatLongNumber(dropped)}
+
+ {formatLongNumber(dropped)}
+
)}
- {formatLongNumber(visitors)}
+
+ {`${formatLongNumber(visitors)} ${formatMessage(labels.visitors)}`}
+
@@ -102,9 +116,11 @@ export function Funnel({ id, name, type, parameters, websiteId, startDate, endDa
maxValue={previous || 1}
style={{ width: '100%' }}
/>
-
- {Math.round(remaining * 100)}%
-
+
+
+ {Math.round(remaining * 100)}%
+
+
diff --git a/src/app/(main)/websites/[websiteId]/journeys/Journey.tsx b/src/app/(main)/websites/[websiteId]/journeys/Journey.tsx
new file mode 100644
index 00000000..efc04ec6
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/journeys/Journey.tsx
@@ -0,0 +1,99 @@
+import { Grid, Row, Column, Text, Icon, Button, Dialog } from '@umami/react-zen';
+import { ReportEditButton } from '@/components/input/ReportEditButton';
+import { useMessages, useResultQuery } from '@/components/hooks';
+import { Arrow, Eye } from '@/components/icons';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { JourneyEditForm } from './JourneyEditForm';
+
+export interface JourneyProps {
+ id: string;
+ name: string;
+ type: string;
+ parameters: {
+ steps: string;
+ startStep: string;
+ endStep: string;
+ };
+ websiteId: string;
+ startDate: Date;
+ endDate: Date;
+}
+
+export type GoalData = { num: number; total: number };
+
+export function Journey({
+ id,
+ name,
+ type,
+ parameters,
+ websiteId,
+ startDate,
+ endDate,
+}: JourneyProps) {
+ const { formatMessage, labels } = useMessages();
+ const { data, error, isLoading } = useResultQuery(type, {
+ websiteId,
+ dateRange: {
+ startDate,
+ endDate,
+ },
+ parameters,
+ });
+
+ return (
+
+
+
+
+
+
+ {name}
+
+
+
+
+
+ {({ close }) => {
+ return (
+
+ );
+ }}
+
+
+
+
+
+ {formatMessage(labels.steps)}: {parameters?.steps}
+
+
+
+
+
+ {formatMessage(labels.startStep)}: {parameters?.startStep}
+
+
+
+
+
+ {formatMessage(labels.endStep)}: {parameters?.endStep || formatMessage(labels.none)}
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/journeys/JourneyAddButton.tsx b/src/app/(main)/websites/[websiteId]/journeys/JourneyAddButton.tsx
new file mode 100644
index 00000000..b75de0b2
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/journeys/JourneyAddButton.tsx
@@ -0,0 +1,28 @@
+import { Button, DialogTrigger, Dialog, Icon, Text, Modal } from '@umami/react-zen';
+import { useMessages } from '@/components/hooks';
+import { JourneyEditForm } from './JourneyEditForm';
+import { Plus } from '@/components/icons';
+
+export function JourneyAddButton({ websiteId }: { websiteId: string }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/journeys/JourneyEditForm.tsx b/src/app/(main)/websites/[websiteId]/journeys/JourneyEditForm.tsx
new file mode 100644
index 00000000..1a07d6b6
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/journeys/JourneyEditForm.tsx
@@ -0,0 +1,96 @@
+import {
+ Form,
+ FormField,
+ TextField,
+ FormButtons,
+ FormSubmitButton,
+ Button,
+ Select,
+ ListItem,
+ Loading,
+} from '@umami/react-zen';
+import { useApi, useMessages, useModified, useReportQuery } from '@/components/hooks';
+
+const JOURNEY_STEPS = ['3', '4', '5', '6', '7'];
+
+export function JourneyEditForm({
+ id,
+ websiteId,
+ onSave,
+ onClose,
+}: {
+ id?: string;
+ websiteId: string;
+ onSave?: () => void;
+ onClose?: () => void;
+}) {
+ const { formatMessage, labels } = useMessages();
+ const { touch } = useModified();
+ const { post, useMutation } = useApi();
+ const { data } = useReportQuery(id);
+ const { mutate, error, isPending } = useMutation({
+ mutationFn: (params: any) => post(`/reports${id ? `/${id}` : ''}`, params),
+ });
+
+ const handleSubmit = async ({ name, ...parameters }) => {
+ mutate(
+ { ...data, id, name, type: 'journey', websiteId, parameters },
+ {
+ onSuccess: async () => {
+ if (id) touch(`report:${id}`);
+ touch('reports:journey');
+ onSave?.();
+ onClose?.();
+ },
+ },
+ );
+ };
+
+ if (id && !data) {
+ return ;
+ }
+
+ const defaultValues = {
+ name: data?.name || '',
+ steps: data?.steps || '5',
+ startStep: data?.parameters?.startStep || '',
+ endStep: data?.parameters?.endStep || '',
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/journeys/JourneysPage.tsx b/src/app/(main)/websites/[websiteId]/journeys/JourneysPage.tsx
new file mode 100644
index 00000000..604f72ed
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/journeys/JourneysPage.tsx
@@ -0,0 +1,38 @@
+'use client';
+import { Grid, Loading } from '@umami/react-zen';
+import { SectionHeader } from '@/components/common/SectionHeader';
+import { Journey } from './Journey';
+import { JourneyAddButton } from './JourneyAddButton';
+import { WebsiteControls } from '../WebsiteControls';
+import { useDateRange, useReportsQuery } from '@/components/hooks';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { Panel } from '@/components/common/Panel';
+
+export function JourneysPage({ websiteId }: { websiteId: string }) {
+ const { result } = useReportsQuery({ websiteId, type: 'journey' });
+ const {
+ dateRange: { startDate, endDate },
+ } = useDateRange(websiteId);
+
+ if (!result) {
+ return ;
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+ {result?.data?.map((report: any) => (
+
+
+
+ ))}
+
+
+ >
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/journeys/page.tsx b/src/app/(main)/websites/[websiteId]/journeys/page.tsx
new file mode 100644
index 00000000..c055dacd
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/journeys/page.tsx
@@ -0,0 +1,12 @@
+import { Metadata } from 'next';
+import { JourneysPage } from './JourneysPage';
+
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
+ const { websiteId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Journeys',
+};
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 4977c99b..8249e184 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,5 +1,5 @@
import { redirect } from 'next/navigation';
export default function RootPage() {
- redirect('/dashboard');
+ redirect('/websites');
}
diff --git a/src/components/common/FilterEditForm.tsx b/src/components/common/FilterEditForm.tsx
index e630e0bf..c15e6ff0 100644
--- a/src/components/common/FilterEditForm.tsx
+++ b/src/components/common/FilterEditForm.tsx
@@ -1,7 +1,6 @@
import { useState, Key } from 'react';
-import { Grid, Row, Column, Label, List, ListItem, Button, Heading } from '@umami/react-zen';
+import { Grid, Row, Column, Label, List, ListItem, Button, Heading, Text } from '@umami/react-zen';
import { useFilters, useMessages } from '@/components/hooks';
-import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder';
import { FilterRecord } from '@/components/common/FilterRecord';
export interface FilterEditFormProps {
@@ -73,7 +72,7 @@ export function FilterEditForm({ data = [], onChange, onClose }: FilterEditFormP
/>
);
})}
- {!filters.length && }
+ {!filters.length && {formatMessage(labels.none)}}
diff --git a/src/components/input/TeamsButton.tsx b/src/components/input/TeamsButton.tsx
index 3487fd9f..c8151b12 100644
--- a/src/components/input/TeamsButton.tsx
+++ b/src/components/input/TeamsButton.tsx
@@ -32,7 +32,7 @@ export function TeamsButton({
const selectedKeys = new Set([teamId || user.id]);
const handleSelect = (id: Key) => {
- router.push(id === user.id ? '/dashboard' : `/teams/${id}/dashboard`);
+ router.push(id === user.id ? '/websites' : `/teams/${id}/websites`);
};
if (!result?.count) {
diff --git a/src/components/messages.ts b/src/components/messages.ts
index d2e9c2d0..91743807 100644
--- a/src/components/messages.ts
+++ b/src/components/messages.ts
@@ -282,6 +282,7 @@ export const labels = defineMessages({
defaultMessage: 'Track your goals for pageviews and events.',
},
journey: { id: 'label.journey', defaultMessage: 'Journey' },
+ journeys: { id: 'label.journeys', defaultMessage: 'Journeys' },
journeyDescription: {
id: 'label.journey-description',
defaultMessage: 'Understand how users navigate through your website.',
diff --git a/src/components/metrics/ChangeLabel.module.css b/src/components/metrics/ChangeLabel.module.css
index fb3112de..b3bc7433 100644
--- a/src/components/metrics/ChangeLabel.module.css
+++ b/src/components/metrics/ChangeLabel.module.css
@@ -2,7 +2,7 @@
display: flex;
align-items: center;
gap: 5px;
- font-size: 12px;
+ font-size: var(--font-size);
padding: 0.1em 0.5em;
border-radius: 5px;
color: var(--base500);
diff --git a/src/lib/schema.ts b/src/lib/schema.ts
index 5edf7a46..7402af8c 100644
--- a/src/lib/schema.ts
+++ b/src/lib/schema.ts
@@ -110,6 +110,15 @@ export const funnelReportSchema = z.object({
}),
});
+export const journeyReportSchema = z.object({
+ type: z.literal('journey'),
+ parameters: z.object({
+ steps: z.coerce.number().positive(),
+ startStep: z.string().optional(),
+ endStep: z.string().optional(),
+ }),
+});
+
export const reportBaseSchema = z.object({
websiteId: z.string().uuid(),
type: reportTypeParam,
@@ -120,6 +129,7 @@ export const reportBaseSchema = z.object({
export const reportTypeSchema = z.discriminatedUnion('type', [
goalReportSchema,
funnelReportSchema,
+ journeyReportSchema,
]);
export const reportSchema = z.intersection(reportBaseSchema, reportTypeSchema);