mirror of
https://github.com/umami-software/umami.git
synced 2026-02-13 09:05:36 +01:00
Refactored website components. New layout.
This commit is contained in:
parent
6e41ba2e2c
commit
06f76dda13
35 changed files with 1159 additions and 987 deletions
|
|
@ -5,19 +5,22 @@ import { Column } from '@umami/react-zen';
|
|||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { WebsiteAddButton } from '@/app/(main)/settings/websites/WebsiteAddButton';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { PageBody } from '@/components/common/PageBody';
|
||||
|
||||
export function WebsitesPage() {
|
||||
const { teamId } = useNavigation();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<Column gap="6">
|
||||
<PageHeader title={formatMessage(labels.websites)}>
|
||||
<WebsiteAddButton teamId={teamId} />
|
||||
</PageHeader>
|
||||
<Panel>
|
||||
<WebsitesDataTable teamId={teamId} allowEdit={false} />
|
||||
</Panel>
|
||||
</Column>
|
||||
<PageBody>
|
||||
<Column gap="6">
|
||||
<PageHeader title={formatMessage(labels.websites)}>
|
||||
<WebsiteAddButton teamId={teamId} />
|
||||
</PageHeader>
|
||||
<Panel>
|
||||
<WebsitesDataTable teamId={teamId} allowEdit={false} />
|
||||
</Panel>
|
||||
</Column>
|
||||
</PageBody>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { useMemo } from 'react';
|
|||
import { firstBy } from 'thenby';
|
||||
import { WebsiteChart } from './WebsiteChart';
|
||||
import { useDashboard } from '@/store/dashboard';
|
||||
import { WebsiteHeader } from './WebsiteHeader';
|
||||
import { WebsiteControls } from './WebsiteControls';
|
||||
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { LinkButton } from '@/components/common/LinkButton';
|
||||
|
|
@ -33,7 +33,7 @@ export function WebsiteChartList({
|
|||
{ordered.map(({ id }, index) => {
|
||||
return index < limit ? (
|
||||
<div key={id}>
|
||||
<WebsiteHeader websiteId={id} showLinks={false}>
|
||||
<WebsiteControls websiteId={id} showLinks={false}>
|
||||
<LinkButton href={renderTeamUrl(`/websites/${id}`)} variant="primary">
|
||||
<Text>{formatMessage(labels.viewDetails)}</Text>
|
||||
<Icon>
|
||||
|
|
@ -42,7 +42,7 @@ export function WebsiteChartList({
|
|||
</Icon>
|
||||
</Icon>
|
||||
</LinkButton>
|
||||
</WebsiteHeader>
|
||||
</WebsiteControls>
|
||||
<WebsiteMetricsBar websiteId={id} showChange={true} />
|
||||
{showCharts && <WebsiteChart websiteId={id} />}
|
||||
</div>
|
||||
|
|
|
|||
24
src/app/(main)/websites/[websiteId]/WebsiteControls.tsx
Normal file
24
src/app/(main)/websites/[websiteId]/WebsiteControls.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { Column, Row } from '@umami/react-zen';
|
||||
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
|
||||
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
|
||||
import { FilterBar } from '@/components/input/FilterBar';
|
||||
|
||||
export function WebsiteControls({
|
||||
websiteId,
|
||||
showFilter = true,
|
||||
}: {
|
||||
websiteId: string;
|
||||
showFilter?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Column marginBottom="6" gap="3">
|
||||
<Row alignItems="center" justifyContent="space-between" gap="3" paddingY="3">
|
||||
{showFilter && <WebsiteFilterButton websiteId={websiteId} />}
|
||||
<Row alignItems="center" gap="3">
|
||||
<WebsiteDateFilter websiteId={websiteId} />
|
||||
</Row>
|
||||
</Row>
|
||||
<FilterBar />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { Button, Icon, Icons, DialogTrigger, Dialog, Modal, Text } from '@umami/react-zen';
|
||||
import { Button, Icon, DialogTrigger, Dialog, Modal, Text } from '@umami/react-zen';
|
||||
import { Lucide } from '@/components/icons';
|
||||
import { FilterEditForm } from '@/components/common/FilterEditForm';
|
||||
import { useMessages, useNavigation, useFilters } from '@/components/hooks';
|
||||
|
||||
|
|
@ -33,9 +34,9 @@ export function WebsiteFilterButton({
|
|||
<DialogTrigger>
|
||||
<Button variant="quiet">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
<Lucide.ListFilter />
|
||||
</Icon>
|
||||
{showText && <Text>{formatMessage(labels.filter)}</Text>}
|
||||
{showText && <Text weight="bold">{formatMessage(labels.filter)}</Text>}
|
||||
</Button>
|
||||
<Modal>
|
||||
<Dialog>
|
||||
|
|
|
|||
|
|
@ -1,39 +1,28 @@
|
|||
import { Column, Row, Heading } from '@umami/react-zen';
|
||||
import { Favicon } from '@/components/common/Favicon';
|
||||
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
|
||||
import { Button, Icon, Text, Row } from '@umami/react-zen';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { useWebsite } from '@/components/hooks/useWebsite';
|
||||
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
|
||||
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
|
||||
import { FilterBar } from '@/components/metrics/FilterBar';
|
||||
import { WebsiteMenu } from '@/app/(main)/websites/[websiteId]/WebsiteMenu';
|
||||
import { Lucide } from '@/components/icons';
|
||||
import { Favicon } from '@/components/common/Favicon';
|
||||
|
||||
export function WebsiteHeader({
|
||||
websiteId,
|
||||
showFilter = true,
|
||||
allowEdit = true,
|
||||
}: {
|
||||
websiteId: string;
|
||||
showFilter?: boolean;
|
||||
allowEdit?: boolean;
|
||||
}) {
|
||||
export function WebsiteHeader() {
|
||||
const website = useWebsite();
|
||||
const { name, domain } = website || {};
|
||||
|
||||
return (
|
||||
<Column marginY="6" gap="6">
|
||||
<Row alignItems="center" justifyContent="space-between" gap="3">
|
||||
<Row alignItems="center" gap="3">
|
||||
<Favicon domain={domain} />
|
||||
<Heading>{name}</Heading>
|
||||
</Row>
|
||||
<ActiveUsers websiteId={websiteId} />
|
||||
<Row alignItems="center" gap="3">
|
||||
{showFilter && <WebsiteFilterButton websiteId={websiteId} />}
|
||||
<WebsiteDateFilter websiteId={websiteId} />
|
||||
{allowEdit && <WebsiteMenu websiteId={websiteId} />}
|
||||
</Row>
|
||||
<PageHeader title={website.name} icon={<Favicon domain={website.domain} />}>
|
||||
<Row alignItems="center" gap>
|
||||
<Button>
|
||||
<Icon>
|
||||
<Lucide.Share />
|
||||
</Icon>
|
||||
<Text>Share</Text>
|
||||
</Button>
|
||||
<Button>
|
||||
<Icon>
|
||||
<Lucide.Edit />
|
||||
</Icon>
|
||||
<Text>Edit</Text>
|
||||
</Button>
|
||||
</Row>
|
||||
<FilterBar websiteId={websiteId} />
|
||||
</Column>
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,27 @@
|
|||
'use client';
|
||||
import { ReactNode } from 'react';
|
||||
import { Grid, Column, Box } from '@umami/react-zen';
|
||||
import { Grid, Column } from '@umami/react-zen';
|
||||
import { WebsiteProvider } from './WebsiteProvider';
|
||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
|
||||
import { PageBody } from '@/components/common/PageBody';
|
||||
import { WebsiteHeader } from '@/app/(main)/websites/[websiteId]/WebsiteHeader';
|
||||
import { WebsiteTabs } from '@/app/(main)/websites/[websiteId]/WebsiteTabs';
|
||||
|
||||
export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
|
||||
return (
|
||||
<WebsiteProvider websiteId={websiteId}>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<Grid columns="170px 1140px" justifyContent="center" gap>
|
||||
<Box position="sticky" top="20px" alignSelf="flex-start">
|
||||
<WebsiteTabs websiteId={websiteId} />
|
||||
</Box>
|
||||
<Column>{children}</Column>
|
||||
</Grid>
|
||||
<PageBody>
|
||||
<WebsiteHeader />
|
||||
<Grid columns="auto 1fr" justifyContent="center" gap width="100%">
|
||||
<Column position="sticky" top="0px" alignSelf="flex-start" width="200px" paddingTop="3">
|
||||
<WebsiteNav websiteId={websiteId} />
|
||||
</Column>
|
||||
<Column>
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
{children}
|
||||
</Column>
|
||||
</Grid>
|
||||
</PageBody>
|
||||
</WebsiteProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Icons } from '@/components/icons';
|
|||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import Link from 'next/link';
|
||||
|
||||
export function WebsiteTabs({ websiteId }: { websiteId: string }) {
|
||||
export function WebsiteNav({ websiteId }: { websiteId: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { pathname, renderTeamUrl } = useNavigation();
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ export function WebsiteTabs({ websiteId }: { websiteId: string }) {
|
|||
id: 'retention',
|
||||
label: formatMessage(labels.retention),
|
||||
icon: <Icons.Magnet />,
|
||||
path: '/funnels',
|
||||
path: '/retention',
|
||||
},
|
||||
{
|
||||
id: 'utm',
|
||||
|
|
@ -97,7 +97,7 @@ export function WebsiteTabs({ websiteId }: { websiteId: string }) {
|
|||
|
||||
return (
|
||||
<Link key={id} href={renderTeamUrl(`/websites/${websiteId}${path}`)}>
|
||||
<NavMenuItem highlightColor="5" isSelected={isSelected}>
|
||||
<NavMenuItem isSelected={isSelected}>
|
||||
<Row alignItems="center" gap>
|
||||
<Icon style={{ fill: 'currentcolor' }}>{icon}</Icon>
|
||||
<Text>{label}</Text>
|
||||
|
|
@ -8,5 +8,5 @@ export default async function ({ params }: { params: Promise<{ websiteId: string
|
|||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Event Data',
|
||||
title: 'Events',
|
||||
};
|
||||
|
|
|
|||
6
src/app/(main)/websites/[websiteId]/goals/GoalsPage.tsx
Normal file
6
src/app/(main)/websites/[websiteId]/goals/GoalsPage.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
'use client';
|
||||
import { Column } from '@umami/react-zen';
|
||||
|
||||
export function GoalsPage({ websiteId }: { websiteId: string }) {
|
||||
return <Column>Goals {websiteId}</Column>;
|
||||
}
|
||||
12
src/app/(main)/websites/[websiteId]/goals/page.tsx
Normal file
12
src/app/(main)/websites/[websiteId]/goals/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Metadata } from 'next';
|
||||
import { GoalsPage } from './GoalsPage';
|
||||
|
||||
export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
|
||||
const { websiteId } = await params;
|
||||
|
||||
return <GoalsPage websiteId={websiteId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Goals',
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Page } from '@/components/common/Page';
|
||||
import { PageBody } from '@/components/common/PageBody';
|
||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||
import { useApi, useMessages } from '@/components/hooks';
|
||||
import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder';
|
||||
|
|
@ -21,11 +21,11 @@ export function RealtimeHome() {
|
|||
}, [data, router]);
|
||||
|
||||
return (
|
||||
<Page isLoading={isLoading || data?.length > 0} error={error}>
|
||||
<PageBody isLoading={isLoading || data?.length > 0} error={error}>
|
||||
<SectionHeader title={formatMessage(labels.realtime)} />
|
||||
{data?.length === 0 && (
|
||||
<EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)} />
|
||||
)}
|
||||
</Page>
|
||||
</PageBody>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { firstBy } from 'thenby';
|
||||
import { Grid } from '@umami/react-zen';
|
||||
import { GridRow } from '@/components/common/GridRow';
|
||||
import { Page } from '@/components/common/Page';
|
||||
import { PageBody } from '@/components/common/PageBody';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { RealtimeChart } from '@/components/metrics/RealtimeChart';
|
||||
import { WorldMap } from '@/components/metrics/WorldMap';
|
||||
|
|
@ -17,7 +17,7 @@ export function WebsiteRealtimePage({ websiteId }: { websiteId: string }) {
|
|||
const { data, isLoading, error } = useRealtimeQuery(websiteId);
|
||||
|
||||
if (isLoading || error) {
|
||||
return <Page isLoading={isLoading} error={error} />;
|
||||
return <PageBody isLoading={isLoading} error={error} />;
|
||||
}
|
||||
|
||||
const countries = percentFilter(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
'use client';
|
||||
import { RetentionTable } from './RetentionTable';
|
||||
|
||||
export function RetentionPage({ websiteId }: { websiteId: string }) {
|
||||
return <RetentionTable websiteId={websiteId} />;
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder';
|
||||
import { useMessages, useLocale, useReport } from '@/components/hooks';
|
||||
import { formatDate } from '@/lib/date';
|
||||
|
||||
const DAYS = [1, 2, 3, 4, 5, 6, 7, 14, 21, 28];
|
||||
|
||||
export function RetentionTable({ days = DAYS }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { locale } = useLocale();
|
||||
const { report } = useReport();
|
||||
const { data } = report || {};
|
||||
|
||||
if (!data) {
|
||||
return <EmptyPlaceholder />;
|
||||
}
|
||||
|
||||
const rows = data.reduce((arr: any[], row: { date: any; visitors: any; day: any }) => {
|
||||
const { date, visitors, day } = row;
|
||||
if (day === 0) {
|
||||
return arr.concat({
|
||||
date,
|
||||
visitors,
|
||||
records: days
|
||||
.reduce((arr, day) => {
|
||||
arr[day] = data.find(x => x.date === date && x.day === day);
|
||||
return arr;
|
||||
}, [])
|
||||
.filter(n => n),
|
||||
});
|
||||
}
|
||||
return arr;
|
||||
}, []);
|
||||
|
||||
const totalDays = rows.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div>
|
||||
<div>{formatMessage(labels.date)}</div>
|
||||
<div>{formatMessage(labels.visitors)}</div>
|
||||
{days.map(n => (
|
||||
<div key={n}>
|
||||
{formatMessage(labels.day)} {n}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{rows.map(({ date, visitors, records }, rowIndex) => {
|
||||
return (
|
||||
<div key={rowIndex}>
|
||||
<div>{formatDate(date, 'PP', locale)}</div>
|
||||
<div>{visitors}</div>
|
||||
{days.map(day => {
|
||||
if (totalDays - rowIndex < day) {
|
||||
return null;
|
||||
}
|
||||
const percentage = records.filter(a => a.day === day)[0]?.percentage;
|
||||
return <div key={day}>{percentage ? `${Number(percentage).toFixed(2)}%` : ''}</div>;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
12
src/app/(main)/websites/[websiteId]/retention/page.tsx
Normal file
12
src/app/(main)/websites/[websiteId]/retention/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Metadata } from 'next';
|
||||
import { RetentionPage } from './RetentionPage';
|
||||
|
||||
export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
|
||||
const { websiteId } = await params;
|
||||
|
||||
return <RetentionPage websiteId={websiteId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Retention',
|
||||
};
|
||||
|
|
@ -15,7 +15,7 @@ export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean
|
|||
<DataColumn id="id" label={formatMessage(labels.session)} width="100px">
|
||||
{(row: any) => (
|
||||
<Link href={`sessions/${row.id}`} className={styles.link}>
|
||||
<Avatar key={row.id} seed={row.id} size={64} />
|
||||
<Avatar seed={row.id} size={64} />
|
||||
</Link>
|
||||
)}
|
||||
</DataColumn>
|
||||
|
|
|
|||
6
src/app/(main)/websites/[websiteId]/utm/UTMPage.tsx
Normal file
6
src/app/(main)/websites/[websiteId]/utm/UTMPage.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
'use client';
|
||||
import { Column } from '@umami/react-zen';
|
||||
|
||||
export function UTMPage({ websiteId }: { websiteId: string }) {
|
||||
return <Column>Goals {websiteId}</Column>;
|
||||
}
|
||||
12
src/app/(main)/websites/[websiteId]/utm/page.tsx
Normal file
12
src/app/(main)/websites/[websiteId]/utm/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Metadata } from 'next';
|
||||
import { UTMPage } from './UTMPage';
|
||||
|
||||
export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
|
||||
const { websiteId } = await params;
|
||||
|
||||
return <UTMPage websiteId={websiteId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'UTM Parameters',
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue