Updated grid layouts. Fixed chart tooltip.

This commit is contained in:
Mike Cao 2025-04-06 08:13:55 -07:00
parent 96c2c32d14
commit 1d24e23a34
9 changed files with 64 additions and 79 deletions

View file

@ -78,7 +78,7 @@
"@react-spring/web": "^9.7.5", "@react-spring/web": "^9.7.5",
"@tanstack/react-query": "^5.68.0", "@tanstack/react-query": "^5.68.0",
"@umami/prisma-client": "^0.16.0", "@umami/prisma-client": "^0.16.0",
"@umami/react-zen": "^0.77.0", "@umami/react-zen": "^0.79.0",
"@umami/redis-client": "^0.27.0", "@umami/redis-client": "^0.27.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"chalk": "^4.1.2", "chalk": "^4.1.2",

22
pnpm-lock.yaml generated
View file

@ -45,8 +45,8 @@ importers:
specifier: ^0.16.0 specifier: ^0.16.0
version: 0.16.0(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2))(@prisma/extension-read-replicas@0.4.0(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2))) version: 0.16.0(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2))(@prisma/extension-read-replicas@0.4.0(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2)))
'@umami/react-zen': '@umami/react-zen':
specifier: ^0.77.0 specifier: ^0.79.0
version: 0.77.0(@babel/core@7.26.9)(@types/react@19.0.10)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.0.0)) version: 0.79.0(@babel/core@7.26.9)(@types/react@19.0.10)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.0.0))
'@umami/redis-client': '@umami/redis-client':
specifier: ^0.27.0 specifier: ^0.27.0
version: 0.27.0 version: 0.27.0
@ -2961,8 +2961,8 @@ packages:
'@prisma/client': ^4.8.0 '@prisma/client': ^4.8.0
'@prisma/extension-read-replicas': ^0.3.0 '@prisma/extension-read-replicas': ^0.3.0
'@umami/react-zen@0.77.0': '@umami/react-zen@0.79.0':
resolution: {integrity: sha512-tdoPdCMfPOhBMEiGXZ62zfTG8qjto7Ii+mWXNX2qdPjZ+SP5AB8wnQpqIGiXoUZUnP41iWw0GS97sTZq3xeEBQ==} resolution: {integrity: sha512-qumZSV/dWvtq7iR7QwxEO5emE/jzgI5uPP5Y1E9S+MMGnJRhD5gQ1TvURf+jYAlTuiPubLysu6U3nKYmPNJxUg==}
'@umami/redis-client@0.27.0': '@umami/redis-client@0.27.0':
resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==} resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==}
@ -3329,8 +3329,8 @@ packages:
caniuse-lite@1.0.30001702: caniuse-lite@1.0.30001702:
resolution: {integrity: sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==} resolution: {integrity: sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==}
caniuse-lite@1.0.30001709: caniuse-lite@1.0.30001712:
resolution: {integrity: sha512-NgL3vUTnDrPCZ3zTahp4fsugQ4dc7EKTSzwQDPEel6DMoMnfH2jhry9n2Zm8onbSR+f/QtKHFOA+iAQu4kbtWA==} resolution: {integrity: sha512-MBqPpGYYdQ7/hfKiet9SCI+nmN5/hp4ZzveOJubl5DTAMa5oggjAuoi0Z4onBpKPFI2ePGnQuQIzF3VxDjDJig==}
caseless@0.12.0: caseless@0.12.0:
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
@ -10646,7 +10646,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@umami/react-zen@0.77.0(@babel/core@7.26.9)(@types/react@19.0.10)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.0.0))': '@umami/react-zen@0.79.0(@babel/core@7.26.9)(@types/react@19.0.10)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.0.0))':
dependencies: dependencies:
'@fontsource/jetbrains-mono': 5.2.5 '@fontsource/jetbrains-mono': 5.2.5
'@react-aria/focus': 3.20.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-aria/focus': 3.20.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@ -11120,13 +11120,13 @@ snapshots:
caniuse-api@3.0.0: caniuse-api@3.0.0:
dependencies: dependencies:
browserslist: 4.24.4 browserslist: 4.24.4
caniuse-lite: 1.0.30001709 caniuse-lite: 1.0.30001712
lodash.memoize: 4.1.2 lodash.memoize: 4.1.2
lodash.uniq: 4.5.0 lodash.uniq: 4.5.0
caniuse-lite@1.0.30001702: {} caniuse-lite@1.0.30001702: {}
caniuse-lite@1.0.30001709: {} caniuse-lite@1.0.30001712: {}
caseless@0.12.0: {} caseless@0.12.0: {}
@ -13667,7 +13667,7 @@ snapshots:
'@swc/counter': 0.1.3 '@swc/counter': 0.1.3
'@swc/helpers': 0.5.15 '@swc/helpers': 0.5.15
busboy: 1.6.0 busboy: 1.6.0
caniuse-lite: 1.0.30001709 caniuse-lite: 1.0.30001712
postcss: 8.4.31 postcss: 8.4.31
react: 19.0.0 react: 19.0.0
react-dom: 19.0.0(react@19.0.0) react-dom: 19.0.0(react@19.0.0)
@ -13692,7 +13692,7 @@ snapshots:
'@swc/counter': 0.1.3 '@swc/counter': 0.1.3
'@swc/helpers': 0.5.15 '@swc/helpers': 0.5.15
busboy: 1.6.0 busboy: 1.6.0
caniuse-lite: 1.0.30001709 caniuse-lite: 1.0.30001712
postcss: 8.4.31 postcss: 8.4.31
react: 19.1.0 react: 19.1.0
react-dom: 19.1.0(react@19.1.0) react-dom: 19.1.0(react@19.1.0)

View file

@ -10,15 +10,17 @@ import { WebsiteMenu } from '@/app/(main)/websites/[websiteId]/WebsiteMenu';
import { WebsiteCompareBar } from '@/app/(main)/websites/[websiteId]/WebsiteCompareBar'; import { WebsiteCompareBar } from '@/app/(main)/websites/[websiteId]/WebsiteCompareBar';
export function WebsiteHeader({ export function WebsiteHeader({
websiteId,
showFilter = true, showFilter = true,
allowEdit = true, allowEdit = true,
}: { }: {
websiteId: string;
showFilter?: boolean; showFilter?: boolean;
allowEdit?: boolean; allowEdit?: boolean;
compareMode?: boolean; compareMode?: boolean;
}) { }) {
const website = useWebsite(); const website = useWebsite();
const { id: websiteId, name, domain } = website || {}; const { name, domain } = website || {};
return ( return (
<Column marginY="6" gap="6"> <Column marginY="6" gap="6">

View file

@ -1,4 +1,5 @@
import { Grid } from '@umami/react-zen'; import { Grid } from '@umami/react-zen';
import { GridRow } from '@/components/common/GridRow';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
import { PagesTable } from '@/components/metrics/PagesTable'; import { PagesTable } from '@/components/metrics/PagesTable';
import { ReferrersTable } from '@/components/metrics/ReferrersTable'; import { ReferrersTable } from '@/components/metrics/ReferrersTable';
@ -7,57 +8,37 @@ import { OSTable } from '@/components/metrics/OSTable';
import { DevicesTable } from '@/components/metrics/DevicesTable'; import { DevicesTable } from '@/components/metrics/DevicesTable';
import { WorldMap } from '@/components/metrics/WorldMap'; import { WorldMap } from '@/components/metrics/WorldMap';
import { CountriesTable } from '@/components/metrics/CountriesTable'; import { CountriesTable } from '@/components/metrics/CountriesTable';
import { EventsTable } from '@/components/metrics/EventsTable';
import { EventsChart } from '@/components/metrics/EventsChart';
import { usePathname } from 'next/navigation';
export function WebsiteTableView({ websiteId }: { websiteId: string }) { export function WebsiteTableView({ websiteId }: { websiteId: string }) {
const pathname = usePathname();
const tableProps = {
websiteId,
limit: 10,
};
const isSharePage = pathname.includes('/share/');
return ( return (
<Grid gap="3"> <Grid gap="3">
<Grid gap="3" columns="repeat(auto-fill, minmax(500px, 1fr))"> <GridRow layout="two">
<Panel> <Panel>
<PagesTable {...tableProps} /> <PagesTable websiteId={websiteId} limit={10} />
</Panel> </Panel>
<Panel> <Panel>
<ReferrersTable {...tableProps} /> <ReferrersTable websiteId={websiteId} limit={10} />
</Panel> </Panel>
</Grid> </GridRow>
<Grid gap="3" columns="repeat(auto-fill, minmax(400px, 1fr))"> <GridRow layout="three">
<Panel> <Panel>
<BrowsersTable {...tableProps} /> <BrowsersTable websiteId={websiteId} limit={10} />
</Panel> </Panel>
<Panel> <Panel>
<OSTable {...tableProps} /> <OSTable websiteId={websiteId} limit={10} />
</Panel> </Panel>
<Panel> <Panel>
<DevicesTable {...tableProps} /> <DevicesTable websiteId={websiteId} limit={10} />
</Panel> </Panel>
</Grid> </GridRow>
<Grid gap="3" columns="2fr 1fr"> <GridRow layout="two-one">
<Panel padding="0"> <Panel padding="0" gridColumn="span 2">
<WorldMap websiteId={websiteId} /> <WorldMap websiteId={websiteId} />
</Panel> </Panel>
<Panel> <Panel>
<CountriesTable {...tableProps} /> <CountriesTable websiteId={websiteId} limit={10} />
</Panel> </Panel>
</Grid> </GridRow>
{isSharePage && (
<Grid gap="3">
<Panel>
<EventsTable {...tableProps} />
</Panel>
<Panel>
<EventsChart websiteId={websiteId} />
</Panel>
</Grid>
)}
</Grid> </Grid>
); );
} }

View file

@ -82,19 +82,19 @@ export function BarChart(props: BarChartProps) {
const handleTooltip = ({ tooltip }: { tooltip: any }) => { const handleTooltip = ({ tooltip }: { tooltip: any }) => {
const { opacity } = tooltip; const { opacity } = tooltip;
setTooltip( setTooltip(opacity ? tooltip : null);
opacity ? <BarChartTooltip tooltip={tooltip} unit={unit} currency={currency} /> : null,
);
}; };
return ( return (
<Chart <>
{...props} <Chart
type="bar" {...props}
chartOptions={options} type="bar"
tooltip={tooltip} chartOptions={options}
onTooltip={handleTooltip} onTooltip={handleTooltip}
style={{ height: 400 }} style={{ height: 400 }}
/> />
{tooltip && <BarChartTooltip tooltip={tooltip} unit={unit} currency={currency} />}
</>
); );
} }

View file

@ -1,4 +1,4 @@
import { useState, useRef, useEffect, useMemo, ReactNode, HTMLAttributes } from 'react'; import { useState, useRef, useEffect, useMemo, HTMLAttributes } from 'react';
import { Loading } from '@umami/react-zen'; import { Loading } from '@umami/react-zen';
import ChartJS, { LegendItem, ChartOptions } from 'chart.js/auto'; import ChartJS, { LegendItem, ChartOptions } from 'chart.js/auto';
import { Legend } from '@/components/metrics/Legend'; import { Legend } from '@/components/metrics/Legend';
@ -14,7 +14,6 @@ export interface ChartProps extends HTMLAttributes<HTMLDivElement> {
onUpdate?: (chart: any) => void; onUpdate?: (chart: any) => void;
onTooltip?: (model: any) => void; onTooltip?: (model: any) => void;
chartOptions?: ChartOptions; chartOptions?: ChartOptions;
tooltip?: ReactNode;
} }
export function Chart({ export function Chart({
@ -27,7 +26,6 @@ export function Chart({
onUpdate, onUpdate,
onTooltip, onTooltip,
chartOptions, chartOptions,
tooltip,
...props ...props
}: ChartProps) { }: ChartProps) {
const canvas = useRef(null); const canvas = useRef(null);
@ -53,6 +51,7 @@ export function Chart({
}, },
tooltip: { tooltip: {
enabled: false, enabled: false,
intersect: true,
external: onTooltip, external: onTooltip,
}, },
}, },
@ -139,7 +138,6 @@ export function Chart({
<canvas ref={canvas} /> <canvas ref={canvas} />
</div> </div>
<Legend items={legendItems} onClick={handleLegendClick} /> <Legend items={legendItems} onClick={handleLegendClick} />
{tooltip}
</> </>
); );
} }

View file

@ -2,10 +2,20 @@ import { Grid } from '@umami/react-zen';
const LAYOUTS = { const LAYOUTS = {
one: { columns: '1fr' }, one: { columns: '1fr' },
two: { columns: { xs: '1fr', sm: '1fr', md: '1fr 1fr', lg: '1fr 1fr' } }, two: {
three: { columns: { xs: '1fr', sm: '1fr', md: '1fr 1fr 1fr', lg: '1fr 2fr' } }, columns: {
'one-two': { columns: { xs: '1fr', sm: '1fr', md: '1fr 2fr', lg: '1fr 2fr' } }, xs: '1fr',
'two-one': { columns: { xs: '1fr', sm: '1fr', md: '2fr 1fr', lg: '2fr 1fr', xl: '2fr 1fr' } }, md: 'repeat(auto-fill, minmax(600px, 1fr))',
},
},
three: {
columns: {
xs: '1fr',
md: 'repeat(auto-fill, minmax(400px, 1fr))',
},
},
'one-two': { columns: { xs: '1fr', lg: 'repeat(3, 1fr)' } },
'two-one': { columns: { xs: '1fr', lg: 'repeat(3, 1fr)' } },
}; };
export function GridRow(props: { export function GridRow(props: {

View file

@ -13,9 +13,9 @@ export function TypeIcon({
return ( return (
<Row gap="3" alignItems="center"> <Row gap="3" alignItems="center">
<img <img
src={`${process.env.basePath || ''}/images/${type}/${value src={`${process.env.basePath || ''}/images/${type}/${
?.replaceAll(' ', '-') value?.replaceAll(' ', '-').toLowerCase() || 'xx'
.toLowerCase()}.png`} }.png`}
onError={e => { onError={e => {
e.currentTarget.src = `${process.env.basePath || ''}/images/${type}/unknown.png`; e.currentTarget.src = `${process.env.basePath || ''}/images/${type}/unknown.png`;
}} }}

View file

@ -1,4 +1,4 @@
import { useState } from 'react'; import { Key } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { import {
Text, Text,
@ -30,16 +30,10 @@ export function TeamsButton({
const { teamId } = useNavigation(); const { teamId } = useNavigation();
const router = useRouter(); const router = useRouter();
const team = result?.data?.find(({ id }) => id === teamId); const team = result?.data?.find(({ id }) => id === teamId);
const [selectedKeys, setSelectedKeys] = useState<any>(new Set([teamId || user.id])); const selectedKeys = new Set([teamId || user.id]);
const handleSelect = (keys: Set<string>) => { const handleSelect = (id: Key) => {
if (keys.size > 0) { router.push(id === user.id ? '/dashboard' : `/teams/${id}/dashboard`);
const [id] = [...keys];
router.push(id === user.id ? '/dashboard' : `/teams/${id}/dashboard`);
setSelectedKeys(keys);
}
}; };
if (!result?.count) { if (!result?.count) {
@ -63,7 +57,7 @@ export function TeamsButton({
selectionMode="single" selectionMode="single"
selectedKeys={selectedKeys} selectedKeys={selectedKeys}
autoFocus="last" autoFocus="last"
onSelectionChange={keys => handleSelect(keys as Set<string>)} onAction={handleSelect}
> >
<MenuSection title={formatMessage(labels.myAccount)}> <MenuSection title={formatMessage(labels.myAccount)}>
<MenuItem id={user.id}> <MenuItem id={user.id}>