Updated menus, chart tooltips, styles.

This commit is contained in:
Mike Cao 2025-05-05 01:36:16 -07:00
parent 0a16ab38e4
commit 92b283486e
23 changed files with 179 additions and 208 deletions

View file

@ -79,7 +79,7 @@
"@prisma/extension-read-replicas": "^0.4.1",
"@react-spring/web": "^9.7.3",
"@tanstack/react-query": "^5.74.11",
"@umami/react-zen": "^0.89.0",
"@umami/react-zen": "^0.90.0",
"@umami/redis-client": "^0.27.0",
"bcryptjs": "^2.4.3",
"chalk": "^4.1.1",

10
pnpm-lock.yaml generated
View file

@ -39,8 +39,8 @@ importers:
specifier: ^5.74.11
version: 5.74.11(react@19.1.0)
'@umami/react-zen':
specifier: ^0.89.0
version: 0.89.0(@babel/core@7.26.10)(@types/react@19.1.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))
specifier: ^0.90.0
version: 0.90.0(@babel/core@7.26.10)(@types/react@19.1.2)(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
@ -2992,8 +2992,8 @@ packages:
resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==}
engines: {node: ^16.0.0 || >=18.0.0}
'@umami/react-zen@0.89.0':
resolution: {integrity: sha512-Lcvgh6Y4DKlUUDE84WowvxvJkgI4INW6lVM32L8+XUJVxBrEBa41RF7jF6KTgD6IizAwHtSouh4gVLzzBDmlCw==}
'@umami/react-zen@0.90.0':
resolution: {integrity: sha512-Hj0/GSQPUtiRwq1ri3nX+anWp5udNQmrKZcHOH/j1B3z4KL/AW+llYyXqP9loG1N+NgEzW66P791Pac8Wo7qpw==}
'@umami/redis-client@0.27.0':
resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==}
@ -10784,7 +10784,7 @@ snapshots:
'@typescript-eslint/types': 6.21.0
eslint-visitor-keys: 3.4.3
'@umami/react-zen@0.89.0(@babel/core@7.26.10)(@types/react@19.1.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))':
'@umami/react-zen@0.90.0(@babel/core@7.26.10)(@types/react@19.1.2)(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.0

View file

@ -5,6 +5,7 @@ import { TeamsButton } from '@/components/input/TeamsButton';
import type { RowProps } from '@umami/react-zen/Row';
import useGlobalState from '@/components/hooks/useGlobalState';
import { Lucide } from '@/components/icons';
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
export function MenuBar(props: RowProps) {
const [isCollapsed, setCollapsed] = useGlobalState('sidenav-collapsed');
@ -20,15 +21,19 @@ export function MenuBar(props: RowProps) {
backgroundColor="2"
border="bottom"
>
<Row>
<Row alignItems="center">
<Button onPress={() => setCollapsed(!isCollapsed)} variant="quiet">
<Icon>
<Lucide.PanelLeft />
</Icon>
</Button>
<TeamsButton />
<Icon>
<Lucide.Slash />
</Icon>
<WebsiteSelect />
</Row>
<Row justifyContent="flex-end">
<Row alignItems="center" justifyContent="flex-end">
<ThemeButton />
<LanguageButton />
<ProfileButton />

View file

@ -2,15 +2,15 @@
import { ProfileSettings } from './ProfileSettings';
import { useMessages } from '@/components/hooks';
import { SectionHeader } from '@/components/common/SectionHeader';
import { Column } from '@umami/react-zen';
export function ProfilePage() {
const { formatMessage, labels } = useMessages();
return (
<>
<Column gap>
<SectionHeader title={formatMessage(labels.profile)} />
<ProfileSettings />
</>
</Column>
);
}

View file

@ -28,12 +28,11 @@ export function TeamSettingsLayout({ children }: { children: ReactNode }) {
},
].filter(n => n);
const value = items.find(({ url }) => pathname.endsWith(url))?.id;
const value = items.find(({ url }) => pathname.includes(url))?.id;
return (
<Column gap="6">
<PageHeader title={formatMessage(labels.teamSettings)} />
<Column gap="6">
<Grid columns="200px 1fr">
<Column marginTop="6">

View file

@ -1,12 +0,0 @@
.footer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
font-size: var(--font-size-sm);
height: 100px;
}
.footer a {
color: var(--font-color100);
}

View file

@ -1,12 +1,12 @@
import { Row, Text } from '@umami/react-zen';
import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
import styles from './Footer.module.css';
export function Footer() {
return (
<footer className={styles.footer}>
<a href={HOMEPAGE_URL}>
<b>umami</b> {`v${CURRENT_VERSION}`}
<Row as="footer">
<a href={HOMEPAGE_URL} target="_blank">
<Text weight="bold">umami</Text> {`v${CURRENT_VERSION}`}
</a>
</footer>
</Row>
);
}

View file

@ -1,31 +0,0 @@
.header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
height: 100px;
}
.title {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
font-size: var(--font-size-lg);
font-weight: 700;
color: var(--font-color100) !important;
}
.buttons {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
}
@media only screen and (max-width: 992px) {
.header .buttons {
flex: 1;
}
}

View file

@ -1,27 +1,25 @@
import { ThemeButton } from '@umami/react-zen';
import { Icon, Text } from '@umami/react-zen';
import { Row, Icon, Text, ThemeButton } from '@umami/react-zen';
import Link from 'next/link';
import { LanguageButton } from '@/components/input/LanguageButton';
import { SettingsButton } from '@/components/input/SettingsButton';
import { Icons } from '@/components/icons';
import styles from './Header.module.css';
export function Header() {
return (
<header className={styles.header}>
<div>
<Link href="https://umami.is" target="_blank" className={styles.title}>
<Row as="header">
<Row gap>
<Link href="https://umami.is" target="_blank">
<Icon size="lg">
<Icons.Logo />
</Icon>
<Text>umami</Text>
</Link>
</div>
<div className={styles.buttons}>
</Row>
<Row alignItems="center" gap>
<ThemeButton />
<LanguageButton />
<SettingsButton />
</div>
</header>
</Row>
</Row>
);
}

View file

@ -1,5 +0,0 @@
.container {
flex: 1;
min-height: calc(100vh - 200px);
min-height: calc(100dvh - 200px);
}

View file

@ -1,11 +1,10 @@
'use client';
import { WebsiteDetailsPage } from '../../(main)/websites/[websiteId]/WebsiteDetailsPage';
import { WebsiteProvider } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
import { WebsiteDetailsPage } from '@/app/(main)/websites/[websiteId]/WebsiteDetailsPage';
import { useShareTokenQuery } from '@/components/hooks';
import { Page } from '@/components/common/Page';
import { Header } from './Header';
import { Footer } from './Footer';
import styles from './SharePage.module.css';
import { WebsiteProvider } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
export function SharePage({ shareId }) {
const { shareToken, isLoading } = useShareTokenQuery(shareId);
@ -15,14 +14,12 @@ export function SharePage({ shareId }) {
}
return (
<div className={styles.container}>
<Page>
<Header />
<WebsiteProvider websiteId={shareToken.websiteId}>
<WebsiteDetailsPage websiteId={shareToken.websiteId} />
</WebsiteProvider>
<Footer />
</Page>
</div>
<Page>
<Header />
<WebsiteProvider websiteId={shareToken.websiteId}>
<WebsiteDetailsPage websiteId={shareToken.websiteId} />
</WebsiteProvider>
<Footer />
</Page>
);
}

1
src/assets/security.svg Normal file
View file

@ -0,0 +1 @@
<svg id="Layer_1" height="512" viewBox="0 0 36 36" width="512" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="m18 34a1.07 1.07 0 0 1 -.48-.11l-4.87-2.43a13.79 13.79 0 0 1 -7.65-12.41v-12.14a1.07 1.07 0 0 1 1.05-1.07h3.47a7.45 7.45 0 0 0 4-1.19l3.87-2.48a1.07 1.07 0 0 1 1.15 0l3.87 2.48a7.45 7.45 0 0 0 4 1.19h3.47a1.07 1.07 0 0 1 1.12 1.07v12.14a13.79 13.79 0 0 1 -7.67 12.4l-4.87 2.43a1.07 1.07 0 0 1 -.46.12zm-10.88-26v11.05a11.67 11.67 0 0 0 6.49 10.49l4.39 2.2 4.39-2.2a11.67 11.67 0 0 0 6.49-10.49v-11.05h-2.4a9.57 9.57 0 0 1 -5.19-1.53l-3.29-2.14-3.29 2.12a9.57 9.57 0 0 1 -5.19 1.55z"/><path d="m18 18.8a4.8 4.8 0 1 1 4.8-4.8 4.81 4.81 0 0 1 -4.8 4.8zm0-7.47a2.67 2.67 0 1 0 2.67 2.67 2.67 2.67 0 0 0 -2.67-2.66z"/><path d="m24.4 24.67h-2.13a2.14 2.14 0 0 0 -2.13-2.13h-4.28a2.13 2.13 0 0 0 -2.13 2.13h-2.13a4.26 4.26 0 0 1 4.26-4.26h4.27a4.27 4.27 0 0 1 4.27 4.26z"/></svg>

After

Width:  |  Height:  |  Size: 899 B

View file

@ -1,9 +1,24 @@
import { useMemo, useState } from 'react';
import { useTheme } from '@umami/react-zen';
import { BarChartTooltip } from '@/components/charts/BarChartTooltip';
import { ChartTooltip } from '@/components/charts/ChartTooltip';
import { Chart, ChartProps } from '@/components/charts/Chart';
import { useLocale } from '@/components/hooks';
import { renderNumberLabels } from '@/lib/charts';
import { getThemeColors } from '@/lib/colors';
import { formatDate } from '@/lib/date';
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
const dateFormats = {
millisecond: 'T',
second: 'pp',
minute: 'p',
hour: 'p - PP',
day: 'PPPP',
week: 'PPPP',
month: 'LLLL yyyy',
quarter: 'qqq',
year: 'yyyy',
};
export interface BarChartProps extends ChartProps {
unit: string;
@ -18,22 +33,23 @@ export interface BarChartProps extends ChartProps {
isAllTime?: boolean;
}
export function BarChart(props: BarChartProps) {
export function BarChart({
renderXLabel,
renderYLabel,
unit,
XAxisType = 'time',
YAxisType = 'linear',
stacked = false,
minDate,
maxDate,
currency,
isAllTime,
...props
}: BarChartProps) {
const [tooltip, setTooltip] = useState(null);
const { theme } = useTheme();
const { locale } = useLocale();
const { colors } = getThemeColors(theme);
const {
renderXLabel,
renderYLabel,
unit,
XAxisType = 'time',
YAxisType = 'linear',
stacked = false,
minDate,
maxDate,
currency,
isAllTime,
} = props;
const options: any = useMemo(() => {
return {
@ -80,9 +96,23 @@ export function BarChart(props: BarChartProps) {
}, [colors, unit, stacked, renderXLabel, renderYLabel]);
const handleTooltip = ({ tooltip }: { tooltip: any }) => {
const { opacity } = tooltip;
const { opacity, labelColors, dataPoints } = tooltip;
setTooltip(opacity ? tooltip : null);
if (opacity) {
setTooltip({
title: formatDate(
new Date(dataPoints[0].raw?.d || dataPoints[0].raw?.x || dataPoints[0].raw),
dateFormats[unit],
locale,
),
color: labelColors?.[0]?.backgroundColor,
value: currency
? formatLongCurrency(dataPoints[0].raw.y, currency)
: `${formatLongNumber(dataPoints[0].raw.y)} ${dataPoints[0].dataset.label}`,
});
} else {
setTooltip(null);
}
};
return (
@ -92,9 +122,9 @@ export function BarChart(props: BarChartProps) {
type="bar"
chartOptions={options}
onTooltip={handleTooltip}
style={{ height: 400 }}
height="400px"
/>
{tooltip && <BarChartTooltip tooltip={tooltip} unit={unit} currency={currency} />}
{tooltip && <ChartTooltip {...tooltip} />}
</>
);
}

View file

@ -1,38 +0,0 @@
import { useLocale } from '@/components/hooks';
import { formatDate } from '@/lib/date';
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
import { Column, Row, StatusLight, FloatingTooltip } from '@umami/react-zen';
const formats = {
millisecond: 'T',
second: 'pp',
minute: 'p',
hour: 'p - PP',
day: 'PPPP',
week: 'PPPP',
month: 'LLLL yyyy',
quarter: 'qqq',
year: 'yyyy',
};
export function BarChartTooltip({ tooltip, unit, currency }) {
const { locale } = useLocale();
const { labelColors, dataPoints } = tooltip;
return (
<FloatingTooltip>
<Column gap="3" fontSize="1">
<Row alignItems="center">
{formatDate(new Date(dataPoints[0].raw.d || dataPoints[0].raw.x), formats[unit], locale)}
</Row>
<Row alignItems="center">
<StatusLight color={labelColors?.[0]?.backgroundColor}>
{currency
? formatLongCurrency(dataPoints[0].raw.y, currency)
: `${formatLongNumber(dataPoints[0].raw.y)} ${dataPoints[0].dataset.label}`}
</StatusLight>
</Row>
</Column>
</FloatingTooltip>
);
}

View file

@ -1,27 +1,31 @@
import { Chart, ChartProps } from '@/components/charts/Chart';
import { useState } from 'react';
import { StatusLight } from '@umami/react-zen';
import { formatLongNumber } from '@/lib/format';
import { ChartTooltip } from '@/components/charts/ChartTooltip';
export interface BubbleChartProps extends ChartProps {
type?: 'bubble';
}
export function BubbleChart(props: BubbleChartProps) {
export function BubbleChart({ type = 'bubble', ...props }: BubbleChartProps) {
const [tooltip, setTooltip] = useState(null);
const { type = 'bubble' } = props;
const handleTooltip = ({ tooltip }) => {
const { labelColors, dataPoints } = tooltip;
const { opacity, labelColors, title, dataPoints } = tooltip;
setTooltip(
tooltip.opacity ? (
<StatusLight color={labelColors?.[0]?.backgroundColor}>
{formatLongNumber(dataPoints?.[0]?.raw)} {dataPoints?.[0]?.label}
</StatusLight>
) : null,
opacity
? {
color: labelColors?.[0]?.backgroundColor,
value: `${title}: ${dataPoints[0].raw}`,
}
: null,
);
};
return <Chart {...props} type={type} tooltip={tooltip} onTooltip={handleTooltip} />;
return (
<>
<Chart {...props} type={type} onTooltip={handleTooltip} />
{tooltip && <ChartTooltip {...tooltip} />}
</>
);
}

View file

@ -1,10 +1,11 @@
import { useState, useRef, useEffect, useMemo, HTMLAttributes } from 'react';
import { Loading } from '@umami/react-zen';
import { useState, useRef, useEffect, useMemo } from 'react';
import { Loading, Box, Column } from '@umami/react-zen';
import ChartJS, { LegendItem, ChartOptions } from 'chart.js/auto';
import { Legend } from '@/components/metrics/Legend';
import { DEFAULT_ANIMATION_DURATION } from '@/lib/constants';
import type { BoxProps } from '@umami/react-zen/Box';
export interface ChartProps extends HTMLAttributes<HTMLDivElement> {
export interface ChartProps extends BoxProps {
type?: 'bar' | 'bubble' | 'doughnut' | 'pie' | 'line' | 'polarArea' | 'radar' | 'scatter';
data?: object;
isLoading?: boolean;
@ -138,12 +139,12 @@ export function Chart({
}, [data, options]);
return (
<>
<div {...props}>
<Column gap="6">
<Box {...props}>
{isLoading && <Loading position="page" icon="dots" />}
<canvas ref={canvas} />
</div>
</Box>
<Legend items={legendItems} onClick={handleLegendClick} />
</>
</Column>
);
}

View file

@ -0,0 +1,23 @@
import { ReactNode } from 'react';
import { Column, Row, StatusLight, FloatingTooltip } from '@umami/react-zen';
export function ChartTooltip({
title,
color,
value,
}: {
title?: string;
color?: string;
value?: ReactNode;
}) {
return (
<FloatingTooltip>
<Column gap="3" fontSize="1">
{title && <Row alignItems="center">{title}</Row>}
<Row alignItems="center">
<StatusLight color={color}>{value}</StatusLight>
</Row>
</Column>
</FloatingTooltip>
);
}

View file

@ -1,27 +1,31 @@
import { Chart, ChartProps } from '@/components/charts/Chart';
import { useState } from 'react';
import { StatusLight } from '@umami/react-zen';
import { formatLongNumber } from '@/lib/format';
import { ChartTooltip } from '@/components/charts/ChartTooltip';
export interface PieChartProps extends ChartProps {
type?: 'doughnut' | 'pie';
}
export function PieChart(props: PieChartProps) {
export function PieChart({ type = 'pie', ...props }: PieChartProps) {
const [tooltip, setTooltip] = useState(null);
const { type = 'pie' } = props;
const handleTooltip = ({ tooltip }) => {
const { labelColors, dataPoints } = tooltip;
const { opacity, labelColors, title, dataPoints } = tooltip;
setTooltip(
tooltip.opacity ? (
<StatusLight color={labelColors?.[0]?.backgroundColor}>
{formatLongNumber(dataPoints?.[0]?.raw)} {dataPoints?.[0]?.label}
</StatusLight>
) : null,
opacity
? {
color: labelColors?.[0]?.backgroundColor,
value: `${title}: ${dataPoints[0].raw}`,
}
: null,
);
};
return <Chart {...props} type={type} tooltip={tooltip} onTooltip={handleTooltip} />;
return (
<>
<Chart {...props} type={type} onTooltip={handleTooltip} />
{tooltip && <ChartTooltip {...tooltip} />}
</>
);
}

View file

@ -1,11 +1,11 @@
import { Text, List, ListItem } from '@umami/react-zen';
export interface MenuNavProps {
export interface SideMenuProps {
items: { id: string; label: string; url: string }[];
selectedKey?: string;
}
export function SideMenu({ items, selectedKey }: MenuNavProps) {
export function SideMenu({ items, selectedKey }: SideMenuProps) {
return (
<List>
{items.map(({ id, label, url }) => {

View file

@ -22,6 +22,7 @@ export function DateFilter({
value,
onChange,
showAllTime = false,
...props
}: DateFilterProps) {
const { formatMessage, labels } = useMessages();
const [showPicker, setShowPicker] = useState(false);
@ -102,6 +103,7 @@ export function DateFilter({
return (
<>
<Select
{...props}
selectedKey={value}
placeholder={formatMessage(labels.selectDate)}
onSelectionChange={handleChange}

View file

@ -1,21 +0,0 @@
.legend {
display: flex;
justify-content: center;
flex-wrap: wrap;
padding: 20px 0;
}
.label {
display: flex;
align-items: center;
font-size: var(--font-size-sm);
cursor: pointer;
}
.label + .label {
margin-inline-start: 20px;
}
.hidden {
color: var(--base400);
}

View file

@ -1,8 +1,6 @@
import { StatusLight } from '@umami/react-zen';
import { Row, StatusLight, Text } from '@umami/react-zen';
import { colord } from 'colord';
import classNames from 'classnames';
import { LegendItem } from 'chart.js/auto';
import styles from './Legend.module.css';
export function Legend({
items = [],
@ -16,21 +14,21 @@ export function Legend({
}
return (
<div className={styles.legend}>
<Row gap wrap="wrap" justifyContent="center">
{items.map(item => {
const { text, fillStyle, hidden } = item;
const color = colord(fillStyle);
return (
<div
key={text}
className={classNames(styles.label, { [styles.hidden]: hidden })}
onClick={() => onClick(item)}
>
<StatusLight color={color.alpha(color.alpha() + 0.2).toHex()}>{text}</StatusLight>
</div>
<Row key={text} onClick={() => onClick(item)}>
<StatusLight color={color.alpha(color.alpha() + 0.2).toHex()}>
<Text size="1" color={hidden ? 'disabled' : undefined} wrap="nowrap">
{text}
</Text>
</StatusLight>
</Row>
);
})}
</div>
</Row>
);
}

View file

@ -0,0 +1,16 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const SvgSecurity = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={512}
height={512}
data-name="Layer 1"
viewBox="0 0 36 36"
{...props}
>
<path d="M18 34a1.1 1.1 0 0 1-.48-.11l-4.87-2.43A13.79 13.79 0 0 1 5 19.05V6.91a1.07 1.07 0 0 1 1.05-1.07h3.47a7.45 7.45 0 0 0 4-1.19l3.87-2.48a1.07 1.07 0 0 1 1.15 0l3.87 2.48a7.45 7.45 0 0 0 4 1.19h3.47A1.07 1.07 0 0 1 31 6.91v12.14a13.79 13.79 0 0 1-7.67 12.4l-4.87 2.43A1.1 1.1 0 0 1 18 34M7.12 8v11.05a11.67 11.67 0 0 0 6.49 10.49l4.39 2.2 4.39-2.2a11.67 11.67 0 0 0 6.49-10.49V8h-2.4a9.57 9.57 0 0 1-5.19-1.53L18 4.33l-3.29 2.12A9.57 9.57 0 0 1 9.52 8z" />
<path d="M18 18.8a4.8 4.8 0 1 1 4.8-4.8 4.81 4.81 0 0 1-4.8 4.8m0-7.47A2.67 2.67 0 1 0 20.67 14 2.67 2.67 0 0 0 18 11.34zM24.4 24.67h-2.13a2.14 2.14 0 0 0-2.13-2.13h-4.28a2.13 2.13 0 0 0-2.13 2.13H11.6a4.26 4.26 0 0 1 4.26-4.26h4.27a4.27 4.27 0 0 1 4.27 4.26" />
</svg>
);
export default SvgSecurity;