Reworked settings screens.

This commit is contained in:
Mike Cao 2025-05-03 00:31:37 -07:00
parent c1d301ffdc
commit 0a16ab38e4
58 changed files with 362 additions and 365 deletions

View file

@ -125,7 +125,7 @@ if (collectApiEndpoint) {
const redirects = [ const redirects = [
{ {
source: '/settings', source: '/settings',
destination: '/settings/websites', destination: '/settings/profile',
permanent: true, permanent: true,
}, },
{ {

View file

@ -138,6 +138,7 @@
"@rollup/plugin-node-resolve": "^15.2.0", "@rollup/plugin-node-resolve": "^15.2.0",
"@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@svgr/cli": "^8.1.0",
"@svgr/rollup": "^8.1.0", "@svgr/rollup": "^8.1.0",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",

48
pnpm-lock.yaml generated
View file

@ -210,6 +210,9 @@ importers:
'@rollup/plugin-terser': '@rollup/plugin-terser':
specifier: ^0.4.4 specifier: ^0.4.4
version: 0.4.4(rollup@3.29.5) version: 0.4.4(rollup@3.29.5)
'@svgr/cli':
specifier: ^8.1.0
version: 8.1.0(typescript@5.8.3)
'@svgr/rollup': '@svgr/rollup':
specifier: ^8.1.0 specifier: ^8.1.0
version: 8.1.0(rollup@3.29.5)(typescript@5.8.3) version: 8.1.0(rollup@3.29.5)(typescript@5.8.3)
@ -2711,6 +2714,11 @@ packages:
peerDependencies: peerDependencies:
'@babel/core': ^7.0.0-0 '@babel/core': ^7.0.0-0
'@svgr/cli@8.1.0':
resolution: {integrity: sha512-SnlaLspB610XFXvs3PmhzViHErsXp0yIy4ERyZlHDlO1ro2iYtHMWYk2mztdLD/lBjiA4ZXe4RePON3qU/Tc4A==}
engines: {node: '>=14'}
hasBin: true
'@svgr/core@8.1.0': '@svgr/core@8.1.0':
resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==} resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -2725,6 +2733,12 @@ packages:
peerDependencies: peerDependencies:
'@svgr/core': '*' '@svgr/core': '*'
'@svgr/plugin-prettier@8.1.0':
resolution: {integrity: sha512-o4/uFI8G64tAjBZ4E7gJfH+VP7Qi3T0+M4WnIsP91iFnGPqs5WvPDkpZALXPiyWEtzfYs1Rmwy1Zdfu8qoZuKw==}
engines: {node: '>=14'}
peerDependencies:
'@svgr/core': '*'
'@svgr/plugin-svgo@8.1.0': '@svgr/plugin-svgo@8.1.0':
resolution: {integrity: sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==} resolution: {integrity: sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -3581,6 +3595,10 @@ packages:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'} engines: {node: '>= 12'}
commander@9.5.0:
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
engines: {node: ^12.20.0 || >=14}
common-tags@1.8.2: common-tags@1.8.2:
resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
engines: {node: '>=4.0.0'} engines: {node: '>=4.0.0'}
@ -3788,6 +3806,10 @@ packages:
resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==}
engines: {node: '>=0.10'} engines: {node: '>=0.10'}
dashify@2.0.0:
resolution: {integrity: sha512-hpA5C/YrPjucXypHPPc0oJ1l9Hf6wWbiOL7Ik42cxnsUOhWiCB/fylKbKqqJalW9FgkNQCw16YO8uW9Hs0Iy1A==}
engines: {node: '>=4'}
data-uri-to-buffer@4.0.1: data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'} engines: {node: '>= 12'}
@ -10388,6 +10410,22 @@ snapshots:
'@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.26.10) '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.26.10)
'@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.26.10) '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.26.10)
'@svgr/cli@8.1.0(typescript@5.8.3)':
dependencies:
'@svgr/core': 8.1.0(typescript@5.8.3)
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3))
'@svgr/plugin-prettier': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3))
'@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3))(typescript@5.8.3)
camelcase: 6.3.0
chalk: 4.1.2
commander: 9.5.0
dashify: 2.0.0
glob: 8.1.0
snake-case: 3.0.4
transitivePeerDependencies:
- supports-color
- typescript
'@svgr/core@8.1.0(typescript@5.8.3)': '@svgr/core@8.1.0(typescript@5.8.3)':
dependencies: dependencies:
'@babel/core': 7.26.10 '@babel/core': 7.26.10
@ -10414,6 +10452,12 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@svgr/plugin-prettier@8.1.0(@svgr/core@8.1.0(typescript@5.8.3))':
dependencies:
'@svgr/core': 8.1.0(typescript@5.8.3)
deepmerge: 4.3.1
prettier: 2.8.8
'@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@5.8.3))(typescript@5.8.3)': '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@5.8.3))(typescript@5.8.3)':
dependencies: dependencies:
'@svgr/core': 8.1.0(typescript@5.8.3) '@svgr/core': 8.1.0(typescript@5.8.3)
@ -11400,6 +11444,8 @@ snapshots:
commander@8.3.0: {} commander@8.3.0: {}
commander@9.5.0: {}
common-tags@1.8.2: {} common-tags@1.8.2: {}
commondir@1.0.1: {} commondir@1.0.1: {}
@ -11690,6 +11736,8 @@ snapshots:
dependencies: dependencies:
assert-plus: 1.0.0 assert-plus: 1.0.0
dashify@2.0.0: {}
data-uri-to-buffer@4.0.1: {} data-uri-to-buffer@4.0.1: {}
data-view-buffer@1.0.2: data-view-buffer@1.0.2:

View file

@ -1,10 +1,11 @@
import { Column, Heading } from '@umami/react-zen'; import { Column } from '@umami/react-zen';
import Link from 'next/link'; import Link from 'next/link';
import { PageHeader } from '@/components/common/PageHeader';
export function BoardsPage() { export function BoardsPage() {
return ( return (
<Column> <Column>
<Heading>My Boards</Heading> <PageHeader title="My Boards" />
<Link href="/teams/3a97e34a-7f9d-4de2-8754-ed81714b528d/boards/86d4095c-a2a8-4fc8-9521-103e858e2b41"> <Link href="/teams/3a97e34a-7f9d-4de2-8754-ed81714b528d/boards/86d4095c-a2a8-4fc8-9521-103e858e2b41">
Board 1 Board 1
</Link> </Link>

View file

@ -4,7 +4,7 @@ import Link from 'next/link';
import Script from 'next/script'; import Script from 'next/script';
import { WebsiteSelect } from '@/components/input/WebsiteSelect'; import { WebsiteSelect } from '@/components/input/WebsiteSelect';
import { Page } from '@/components/common/Page'; import { Page } from '@/components/common/Page';
import { PageHeader } from '@/components/common/PageHeader'; import { SectionHeader } from '@/components/common/SectionHeader';
import { EventsChart } from '@/components/metrics/EventsChart'; import { EventsChart } from '@/components/metrics/EventsChart';
import { WebsiteChart } from '../websites/[websiteId]/WebsiteChart'; import { WebsiteChart } from '../websites/[websiteId]/WebsiteChart';
import { useApi, useNavigation } from '@/components/hooks'; import { useApi, useNavigation } from '@/components/hooks';
@ -118,9 +118,9 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
return ( return (
<Page isLoading={isLoading} error={error}> <Page isLoading={isLoading} error={error}>
<PageHeader title="Test console"> <SectionHeader title="Test console">
<WebsiteSelect websiteId={website?.id} onSelect={handleChange} /> <WebsiteSelect websiteId={website?.id} onSelect={handleChange} />
</PageHeader> </SectionHeader>
{website && ( {website && (
<div className={styles.container}> <div className={styles.container}>
<Script <Script

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import { Icon, Icons, Loading, Text } from '@umami/react-zen'; import { Icon, Icons, Loading, Text } from '@umami/react-zen';
import { PageHeader } from '@/components/common/PageHeader'; import { SectionHeader } from '@/components/common/SectionHeader';
import { Pager } from '@/components/common/Pager'; import { Pager } from '@/components/common/Pager';
import { WebsiteChartList } from '../websites/[websiteId]/WebsiteChartList'; import { WebsiteChartList } from '../websites/[websiteId]/WebsiteChartList';
import { DashboardSettingsButton } from '@/app/(main)/dashboard/DashboardSettingsButton'; import { DashboardSettingsButton } from '@/app/(main)/dashboard/DashboardSettingsButton';
@ -30,9 +30,9 @@ export function DashboardPage() {
return ( return (
<section style={{ marginBottom: 60 }}> <section style={{ marginBottom: 60 }}>
<PageHeader title={formatMessage(labels.dashboard)}> <SectionHeader title={formatMessage(labels.dashboard)}>
{!editing && hasData && <DashboardSettingsButton />} {!editing && hasData && <DashboardSettingsButton />}
</PageHeader> </SectionHeader>
{!hasData && ( {!hasData && (
<EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)}> <EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)}>
<LinkButton href={renderTeamUrl('/settings')}> <LinkButton href={renderTeamUrl('/settings')}>

View file

@ -1,3 +0,0 @@
.field {
width: 200px;
}

View file

@ -1,8 +0,0 @@
import { PageHeader } from '@/components/common/PageHeader';
import { useMessages } from '@/components/hooks';
export function ProfileHeader() {
const { formatMessage, labels } = useMessages();
return <PageHeader title={formatMessage(labels.profile)}></PageHeader>;
}

View file

@ -1,9 +0,0 @@
.container {
margin: 0 auto;
}
@media screen and (max-width: 768px) {
.container {
margin: 0;
}
}

View file

@ -1,13 +0,0 @@
'use client';
import { ProfileHeader } from './ProfileHeader';
import { ProfileSettings } from './ProfileSettings';
import styles from './ProfilePage.module.css';
export function ProfilePage() {
return (
<div className={styles.container}>
<ProfileHeader />
<ProfileSettings />
</div>
);
}

View file

@ -1,8 +0,0 @@
.buttons {
display: flex;
gap: 10px;
}
.active {
border: 2px solid var(--primary-color);
}

View file

@ -1,29 +0,0 @@
import classNames from 'classnames';
import { Button, Icon, useTheme } from '@umami/react-zen';
import { Icons } from '@/components/icons';
import styles from './ThemeSetting.module.css';
export function ThemeSetting() {
const { theme, setTheme } = useTheme();
return (
<div className={styles.buttons}>
<Button
className={classNames({ [styles.active]: theme === 'light' })}
onPress={() => setTheme('light')}
>
<Icon>
<Icons.Sun />
</Icon>
</Button>
<Button
className={classNames({ [styles.active]: theme === 'dark' })}
onPress={() => setTheme('dark')}
>
<Icon>
<Icons.Moon />
</Icon>
</Button>
</div>
);
}

View file

@ -1,8 +0,0 @@
.dropdown {
width: 200px;
}
div.menu {
max-height: 300px;
width: 300px;
}

View file

@ -1,4 +1,4 @@
import { PageHeader } from '@/components/common/PageHeader'; import { SectionHeader } from '@/components/common/SectionHeader';
import { Icon, Icons, Text } from '@umami/react-zen'; import { Icon, Icons, Text } from '@umami/react-zen';
import { useLoginQuery, useMessages, useNavigation } from '@/components/hooks'; import { useLoginQuery, useMessages, useNavigation } from '@/components/hooks';
import { LinkButton } from '@/components/common/LinkButton'; import { LinkButton } from '@/components/common/LinkButton';
@ -11,7 +11,7 @@ export function ReportsHeader() {
const canEdit = user.role !== ROLES.viewOnly; const canEdit = user.role !== ROLES.viewOnly;
return ( return (
<PageHeader title={formatMessage(labels.reports)}> <SectionHeader title={formatMessage(labels.reports)}>
{canEdit && ( {canEdit && (
<LinkButton href={renderTeamUrl('/reports/create')} variant="primary"> <LinkButton href={renderTeamUrl('/reports/create')} variant="primary">
<Icon> <Icon>
@ -20,6 +20,6 @@ export function ReportsHeader() {
<Text>{formatMessage(labels.createReport)}</Text> <Text>{formatMessage(labels.createReport)}</Text>
</LinkButton> </LinkButton>
)} )}
</PageHeader> </SectionHeader>
); );
} }

View file

@ -1,7 +1,7 @@
import { Icon, Text, Row, Column, Grid } from '@umami/react-zen'; import { Icon, Text, Row, Column, Grid } from '@umami/react-zen';
import { useMessages, useNavigation } from '@/components/hooks'; import { useMessages, useNavigation } from '@/components/hooks';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { PageHeader } from '@/components/common/PageHeader'; import { SectionHeader } from '@/components/common/SectionHeader';
import { LinkButton } from '@/components/common/LinkButton'; import { LinkButton } from '@/components/common/LinkButton';
export function ReportTemplates({ showHeader = true }: { showHeader?: boolean }) { export function ReportTemplates({ showHeader = true }: { showHeader?: boolean }) {
@ -61,7 +61,7 @@ export function ReportTemplates({ showHeader = true }: { showHeader?: boolean })
return ( return (
<> <>
{showHeader && <PageHeader title={formatMessage(labels.reports)} />} {showHeader && <SectionHeader title={formatMessage(labels.reports)} />}
<Grid columns="repeat(3, minmax(200px, 1fr))" gap="3"> <Grid columns="repeat(3, minmax(200px, 1fr))" gap="3">
{reports.map(({ title, description, url, icon }) => { {reports.map(({ title, description, url, icon }) => {
return ( return (

View file

@ -1,31 +1,49 @@
'use client'; 'use client';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Grid, Column } from '@umami/react-zen'; import { Grid, Column } from '@umami/react-zen';
import { useLoginQuery, useMessages } from '@/components/hooks'; import { useLoginQuery, useMessages, useNavigation } from '@/components/hooks';
import { SideBar } from '@/components/common/SideBar'; import { SideMenu } from '@/components/common/SideMenu';
import { PageHeader } from '@/components/common/PageHeader';
import { Panel } from '@/components/common/Panel';
export function SettingsLayout({ children }: { children: ReactNode }) { export function SettingsLayout({ children }: { children: ReactNode }) {
const { user } = useLoginQuery(); const { user } = useLoginQuery();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { pathname } = useNavigation();
const items = [ const items = [
{ {
key: 'websites', id: 'profile',
label: formatMessage(labels.profile),
url: '/settings/profile',
},
{ id: 'teams', label: formatMessage(labels.teams), url: '/settings/teams' },
user.isAdmin && {
id: 'websites',
label: formatMessage(labels.websites), label: formatMessage(labels.websites),
url: '/settings/websites', url: '/settings/websites',
}, },
{ key: 'teams', label: formatMessage(labels.teams), url: '/settings/teams' },
user.isAdmin && { user.isAdmin && {
key: 'users', id: 'users',
label: formatMessage(labels.users), label: formatMessage(labels.users),
url: '/settings/users', url: '/settings/users',
}, },
].filter(n => n); ].filter(n => n);
const value = items.find(({ url }) => pathname.includes(url))?.id;
return ( return (
<Grid> <Column gap="6">
<SideBar items={items} /> <PageHeader title={formatMessage(labels.settings)} />
<Column>{children}</Column>
</Grid> <Grid columns="160px 1fr" gap="6">
<Column marginTop="6">
<SideMenu items={items} selectedKey={value} />
</Column>
<Column>
<Panel>{children}</Panel>
</Column>
</Grid>
</Column>
); );
} }

View file

@ -1,9 +1,8 @@
import { DateFilter } from '@/components/input/DateFilter'; import { DateFilter } from '@/components/input/DateFilter';
import { Button, Flexbox } from '@umami/react-zen'; import { Button, Row } from '@umami/react-zen';
import { useDateRange, useMessages } from '@/components/hooks'; import { useDateRange, useMessages } from '@/components/hooks';
import { DEFAULT_DATE_RANGE } from '@/lib/constants'; import { DEFAULT_DATE_RANGE } from '@/lib/constants';
import { DateRange } from '@/lib/types'; import { DateRange } from '@/lib/types';
import styles from './DateRangeSetting.module.css';
export function DateRangeSetting() { export function DateRangeSetting() {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
@ -14,15 +13,14 @@ export function DateRangeSetting() {
const handleReset = () => saveDateRange(DEFAULT_DATE_RANGE); const handleReset = () => saveDateRange(DEFAULT_DATE_RANGE);
return ( return (
<Flexbox gap="3" width="300px"> <Row gap="3">
<DateFilter <DateFilter
className={styles.field}
value={value} value={value}
startDate={dateRange.startDate} startDate={dateRange.startDate}
endDate={dateRange.endDate} endDate={dateRange.endDate}
onChange={handleChange} onChange={handleChange}
/> />
<Button onPress={handleReset}>{formatMessage(labels.reset)}</Button> <Button onPress={handleReset}>{formatMessage(labels.reset)}</Button>
</Flexbox> </Row>
); );
} }

View file

@ -1,5 +1,5 @@
import { Button, Icon, Text, useToast, DialogTrigger, Dialog, Modal } from '@umami/react-zen'; import { Button, Icon, Text, useToast, DialogTrigger, Dialog, Modal } from '@umami/react-zen';
import { PasswordEditForm } from '@/app/(main)/profile/PasswordEditForm'; import { PasswordEditForm } from './PasswordEditForm';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
@ -14,7 +14,7 @@ export function PasswordChangeButton() {
return ( return (
<DialogTrigger> <DialogTrigger>
<Button> <Button>
<Icon> <Icon fillColor="currentColor">
<Icons.Lock /> <Icons.Lock />
</Icon> </Icon>
<Text>{formatMessage(labels.changePassword)}</Text> <Text>{formatMessage(labels.changePassword)}</Text>

View file

@ -0,0 +1,8 @@
import { SectionHeader } from '@/components/common/SectionHeader';
import { useMessages } from '@/components/hooks';
export function ProfileHeader() {
const { formatMessage, labels } = useMessages();
return <SectionHeader title={formatMessage(labels.profile)}></SectionHeader>;
}

View file

@ -0,0 +1,16 @@
'use client';
import { ProfileSettings } from './ProfileSettings';
import { useMessages } from '@/components/hooks';
import { SectionHeader } from '@/components/common/SectionHeader';
export function ProfilePage() {
const { formatMessage, labels } = useMessages();
return (
<>
<SectionHeader title={formatMessage(labels.profile)} />
<ProfileSettings />
</>
);
}

View file

@ -1,11 +1,11 @@
import { Column, Label } from '@umami/react-zen'; import { Row, Column, Label } from '@umami/react-zen';
import { TimezoneSetting } from '@/app/(main)/profile/TimezoneSetting';
import { DateRangeSetting } from '@/app/(main)/profile/DateRangeSetting';
import { LanguageSetting } from '@/app/(main)/profile/LanguageSetting';
import { ThemeSetting } from '@/app/(main)/profile/ThemeSetting';
import { PasswordChangeButton } from './PasswordChangeButton';
import { useLoginQuery, useMessages } from '@/components/hooks'; import { useLoginQuery, useMessages } from '@/components/hooks';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
import { TimezoneSetting } from './TimezoneSetting';
import { DateRangeSetting } from './DateRangeSetting';
import { LanguageSetting } from './LanguageSetting';
import { ThemeSetting } from './ThemeSetting';
import { PasswordChangeButton } from './PasswordChangeButton';
export function ProfileSettings() { export function ProfileSettings() {
const { user } = useLoginQuery(); const { user } = useLoginQuery();
@ -47,7 +47,9 @@ export function ProfileSettings() {
{!cloudMode && ( {!cloudMode && (
<Column> <Column>
<Label>{formatMessage(labels.password)}</Label> <Label>{formatMessage(labels.password)}</Label>
<PasswordChangeButton /> <Row>
<PasswordChangeButton />
</Row>
</Column> </Column>
)} )}

View file

@ -0,0 +1,24 @@
import { Row, Button, Icon, useTheme } from '@umami/react-zen';
import { Icons } from '@/components/icons';
export function ThemeSetting() {
const { theme, setTheme } = useTheme();
return (
<Row gap>
<Button
variant={theme === 'light' ? 'primary' : 'secondary'}
onPress={() => setTheme('light')}
>
<Icon fillColor="currentColor">
<Icons.Sun />
</Icon>
</Button>
<Button variant={theme === 'dark' ? 'primary' : 'secondary'} onPress={() => setTheme('dark')}>
<Icon fillColor="currentColor">
<Icons.Moon />
</Icon>
</Button>
</Row>
);
}

View file

@ -2,7 +2,6 @@ import { useState } from 'react';
import { Row, Select, ListItem, Button } from '@umami/react-zen'; import { Row, Select, ListItem, Button } from '@umami/react-zen';
import { useTimezone, useMessages } from '@/components/hooks'; import { useTimezone, useMessages } from '@/components/hooks';
import { getTimezone } from '@/lib/date'; import { getTimezone } from '@/lib/date';
import styles from './TimezoneSetting.module.css';
const timezones = Intl.supportedValuesOf('timeZone'); const timezones = Intl.supportedValuesOf('timeZone');
@ -25,7 +24,6 @@ export function TimezoneSetting() {
return ( return (
<Row gap="3"> <Row gap="3">
<Select <Select
className={styles.dropdown}
selectedKey={timezone} selectedKey={timezone}
onChange={(value: any) => saveTimezone(value)} onChange={(value: any) => saveTimezone(value)}
allowSearch={true} allowSearch={true}

View file

@ -1,5 +1,5 @@
import { Row } from '@umami/react-zen'; import { Row } from '@umami/react-zen';
import { PageHeader } from '@/components/common/PageHeader'; import { SectionHeader } from '@/components/common/SectionHeader';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
import { useLoginQuery, useMessages } from '@/components/hooks'; import { useLoginQuery, useMessages } from '@/components/hooks';
import { TeamsJoinButton } from './TeamsJoinButton'; import { TeamsJoinButton } from './TeamsJoinButton';
@ -11,11 +11,11 @@ export function TeamsHeader({ allowCreate = true }: { allowCreate?: boolean }) {
const cloudMode = !!process.env.cloudMode; const cloudMode = !!process.env.cloudMode;
return ( return (
<PageHeader title={formatMessage(labels.teams)}> <SectionHeader title={formatMessage(labels.teams)}>
<Row gap="3"> <Row gap="3">
{!cloudMode && <TeamsJoinButton />} {!cloudMode && <TeamsJoinButton />}
{allowCreate && user.role !== ROLES.viewOnly && <TeamsAddButton />} {allowCreate && user.role !== ROLES.viewOnly && <TeamsAddButton />}
</Row> </Row>
</PageHeader> </SectionHeader>
); );
} }

View file

@ -16,7 +16,7 @@ export function TeamsJoinButton() {
return ( return (
<DialogTrigger> <DialogTrigger>
<Button variant="secondary"> <Button variant="secondary">
<Icon> <Icon fillColor="currentColor">
<Icons.AddUser /> <Icons.AddUser />
</Icon> </Icon>
<Text>{formatMessage(labels.joinTeam)}</Text> <Text>{formatMessage(labels.joinTeam)}</Text>

View file

@ -1,12 +1,13 @@
'use client'; 'use client';
import { TeamsDataTable } from './TeamsDataTable'; import { TeamsDataTable } from './TeamsDataTable';
import { TeamsHeader } from './TeamsHeader'; import { TeamsHeader } from './TeamsHeader';
import { Column } from '@umami/react-zen';
export function TeamsSettingsPage() { export function TeamsSettingsPage() {
return ( return (
<> <Column gap>
<TeamsHeader /> <TeamsHeader />
<TeamsDataTable /> <TeamsDataTable />
</> </Column>
); );
} }

View file

@ -1,13 +0,0 @@
import { PageHeader } from '@/components/common/PageHeader';
import { useMessages } from '@/components/hooks';
import { UserAddButton } from './UserAddButton';
export function UsersHeader({ onAdd }: { onAdd?: () => void }) {
const { formatMessage, labels } = useMessages();
return (
<PageHeader title={formatMessage(labels.users)}>
<UserAddButton onSave={onAdd} />
</PageHeader>
);
}

View file

@ -1,12 +1,21 @@
'use client'; 'use client';
import { UsersDataTable } from './UsersDataTable'; import { UsersDataTable } from './UsersDataTable';
import { UsersHeader } from './UsersHeader'; import { Column } from '@umami/react-zen';
import { SectionHeader } from '@/components/common/SectionHeader';
import { UserAddButton } from '@/app/(main)/settings/users/UserAddButton';
import { useMessages } from '@/components/hooks';
export function UsersSettingsPage() { export function UsersSettingsPage() {
const { formatMessage, labels } = useMessages();
const handleSave = () => {};
return ( return (
<> <Column gap>
<UsersHeader /> <SectionHeader title={formatMessage(labels.users)}>
<UserAddButton onSave={handleSave} />
</SectionHeader>
<UsersDataTable /> <UsersDataTable />
</> </Column>
); );
} }

View file

@ -2,7 +2,7 @@ import { useContext } from 'react';
import { Tabs, Tab, TabList, TabPanel } from '@umami/react-zen'; import { Tabs, Tab, TabList, TabPanel } from '@umami/react-zen';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { UserEditForm } from './UserEditForm'; import { UserEditForm } from './UserEditForm';
import { PageHeader } from '@/components/common/PageHeader'; import { SectionHeader } from '@/components/common/SectionHeader';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { UserWebsites } from './UserWebsites'; import { UserWebsites } from './UserWebsites';
import { UserContext } from './UserProvider'; import { UserContext } from './UserProvider';
@ -13,7 +13,7 @@ export function UserSettings({ userId }: { userId: string }) {
return ( return (
<> <>
<PageHeader title={user?.username} icon={<Icons.User />} /> <SectionHeader title={user?.username} icon={<Icons.User />} />
<Tabs> <Tabs>
<TabList> <TabList>
<Tab id="details">{formatMessage(labels.details)}</Tab> <Tab id="details">{formatMessage(labels.details)}</Tab>

View file

@ -1,6 +1,6 @@
import { useMessages, useNavigation } from '@/components/hooks'; import { useMessages, useNavigation } from '@/components/hooks';
import { PageHeader } from '@/components/common/PageHeader';
import { WebsiteAddButton } from './WebsiteAddButton'; import { WebsiteAddButton } from './WebsiteAddButton';
import { PageHeader } from '@/components/common/PageHeader';
export interface WebsitesHeaderProps { export interface WebsitesHeaderProps {
allowCreate?: boolean; allowCreate?: boolean;

View file

@ -1,17 +1,22 @@
'use client'; 'use client';
import { useLoginQuery } from '@/components/hooks'; import { useLoginQuery, useMessages } from '@/components/hooks';
import { WebsitesDataTable } from './WebsitesDataTable'; import { WebsitesDataTable } from './WebsitesDataTable';
import { WebsitesHeader } from './WebsitesHeader';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
import { WebsiteAddButton } from '@/app/(main)/settings/websites/WebsiteAddButton';
import { SectionHeader } from '@/components/common/SectionHeader';
import { Column } from '@umami/react-zen';
export function WebsitesSettingsPage({ teamId }: { teamId: string }) { export function WebsitesSettingsPage({ teamId }: { teamId: string }) {
const { user } = useLoginQuery(); const { user } = useLoginQuery();
const canCreate = user.role !== ROLES.viewOnly; const canCreate = user.role !== ROLES.viewOnly;
const { formatMessage, labels } = useMessages();
return ( return (
<> <Column gap>
<WebsitesHeader allowCreate={canCreate} /> <SectionHeader title={formatMessage(labels.websites)}>
{canCreate && <WebsiteAddButton teamId={teamId} />}
</SectionHeader>
<WebsitesDataTable teamId={teamId} /> <WebsitesDataTable teamId={teamId} />
</> </Column>
); );
} }

View file

@ -1,14 +1,14 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { Button, Icon, Tabs, TabList, Tab, TabPanel, Text } from '@umami/react-zen'; import { Icon, Tabs, TabList, Tab, TabPanel, Text } from '@umami/react-zen';
import Link from 'next/link';
import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { PageHeader } from '@/components/common/PageHeader'; import { SectionHeader } from '@/components/common/SectionHeader';
import { ShareUrl } from './ShareUrl'; import { ShareUrl } from './ShareUrl';
import { TrackingCode } from './TrackingCode'; import { TrackingCode } from './TrackingCode';
import { WebsiteData } from './WebsiteData'; import { WebsiteData } from './WebsiteData';
import { WebsiteEditForm } from './WebsiteEditForm'; import { WebsiteEditForm } from './WebsiteEditForm';
import { LinkButton } from '@/components/common/LinkButton';
export function WebsiteSettings({ export function WebsiteSettings({
websiteId, websiteId,
@ -22,16 +22,18 @@ export function WebsiteSettings({
return ( return (
<> <>
<PageHeader title={website?.name} icon={<Icons.Globe />}> <SectionHeader title={website?.name} icon={<Icons.Globe />}>
<Link href={`/websites/${websiteId}`} target={openExternal ? '_blank' : null}> <LinkButton
<Button variant="primary"> variant="primary"
<Icon> href={`/websites/${websiteId}`}
<Icons.Arrow /> target={openExternal ? '_blank' : null}
</Icon> >
<Text>{formatMessage(labels.view)}</Text> <Icon>
</Button> <Icons.Arrow />
</Link> </Icon>
</PageHeader> <Text>{formatMessage(labels.view)}</Text>
</LinkButton>
</SectionHeader>
<Tabs> <Tabs>
<TabList> <TabList>
<Tab id="details">{formatMessage(labels.details)}</Tab> <Tab id="details">{formatMessage(labels.details)}</Tab>

View file

@ -2,34 +2,48 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { useMessages, useNavigation } from '@/components/hooks'; import { useMessages, useNavigation } from '@/components/hooks';
import { Grid, Column } from '@umami/react-zen'; import { Grid, Column } from '@umami/react-zen';
import { SideBar } from '@/components/common/SideBar'; import { SideMenu } from '@/components/common/SideMenu';
import { Panel } from '@/components/common/Panel';
import { PageHeader } from '@/components/common/PageHeader';
export function TeamSettingsLayout({ children }: { children: ReactNode }) { export function TeamSettingsLayout({ children }: { children: ReactNode }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { teamId } = useNavigation(); const { pathname, teamId } = useNavigation();
const items = [ const items = [
{ {
key: 'team', id: 'team',
label: formatMessage(labels.team), label: formatMessage(labels.team),
url: `/teams/${teamId}/settings/team`, url: `/teams/${teamId}/settings/team`,
}, },
{ {
key: 'websites', id: 'websites',
label: formatMessage(labels.websites), label: formatMessage(labels.websites),
url: `/teams/${teamId}/settings/websites`, url: `/teams/${teamId}/settings/websites`,
}, },
{ {
key: 'members', id: 'members',
label: formatMessage(labels.members), label: formatMessage(labels.members),
url: `/teams/${teamId}/settings/members`, url: `/teams/${teamId}/settings/members`,
}, },
].filter(n => n); ].filter(n => n);
const value = items.find(({ url }) => pathname.endsWith(url))?.id;
return ( return (
<Grid> <Column gap="6">
<SideBar items={items} /> <PageHeader title={formatMessage(labels.teamSettings)} />
<Column>{children}</Column>
</Grid> <Column gap="6">
<Grid columns="200px 1fr">
<Column marginTop="6">
<SideMenu items={items} selectedKey={value} />
</Column>
<Column>
<Panel>{children}</Panel>
</Column>
</Grid>
</Column>
</Column>
); );
} }

View file

@ -1,10 +1,11 @@
'use client'; 'use client';
import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider'; import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider';
import { TeamMembersDataTable } from './TeamMembersDataTable'; import { TeamMembersDataTable } from './TeamMembersDataTable';
import { PageHeader } from '@/components/common/PageHeader'; import { SectionHeader } from '@/components/common/SectionHeader';
import { useLoginQuery, useMessages } from '@/components/hooks'; import { useLoginQuery, useMessages } from '@/components/hooks';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
import { useContext } from 'react'; import { useContext } from 'react';
import { Column } from '@umami/react-zen';
export function TeamMembersPage({ teamId }: { teamId: string }) { export function TeamMembersPage({ teamId }: { teamId: string }) {
const team = useContext(TeamContext); const team = useContext(TeamContext);
@ -18,9 +19,9 @@ export function TeamMembersPage({ teamId }: { teamId: string }) {
) && user.role !== ROLES.viewOnly; ) && user.role !== ROLES.viewOnly;
return ( return (
<> <Column gap>
<PageHeader title={formatMessage(labels.members)} /> <SectionHeader title={formatMessage(labels.members)} />
<TeamMembersDataTable teamId={teamId} allowEdit={canEdit} /> <TeamMembersDataTable teamId={teamId} allowEdit={canEdit} />
</> </Column>
); );
} }

View file

@ -1,14 +1,12 @@
import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider'; import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider';
import { useLoginQuery, useMessages } from '@/components/hooks'; import { useLoginQuery, useMessages } from '@/components/hooks';
import { Icons } from '@/components/icons'; import { SectionHeader } from '@/components/common/SectionHeader';
import { PageHeader } from '@/components/common/PageHeader';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import { Column, Tabs, TabList, Tab, TabPanel } from '@umami/react-zen'; import { Column, Tabs, TabList, Tab, TabPanel } from '@umami/react-zen';
import { TeamLeaveButton } from '@/app/(main)/settings/teams/TeamLeaveButton'; import { TeamLeaveButton } from '@/app/(main)/settings/teams/TeamLeaveButton';
import { TeamManage } from './TeamManage'; import { TeamManage } from './TeamManage';
import { TeamEditForm } from './TeamEditForm'; import { TeamEditForm } from './TeamEditForm';
import { Panel } from '@/components/common/Panel';
export function TeamDetails({ teamId }: { teamId: string }) { export function TeamDetails({ teamId }: { teamId: string }) {
const team = useContext(TeamContext); const team = useContext(TeamContext);
@ -27,24 +25,22 @@ export function TeamDetails({ teamId }: { teamId: string }) {
) && user.role !== ROLES.viewOnly; ) && user.role !== ROLES.viewOnly;
return ( return (
<Column> <Column gap>
<PageHeader title={team?.name} icon={<Icons.Users />}> <SectionHeader title={team?.name}>
{!isTeamOwner && <TeamLeaveButton teamId={team.id} teamName={team.name} />} {!isTeamOwner && <TeamLeaveButton teamId={team.id} teamName={team.name} />}
</PageHeader> </SectionHeader>
<Panel> <Tabs selectedKey={tab} onSelectionChange={(value: any) => setTab(value)}>
<Tabs selectedKey={tab} onSelectionChange={(value: any) => setTab(value)}> <TabList>
<TabList> <Tab id="details">{formatMessage(labels.details)}</Tab>
<Tab id="details">{formatMessage(labels.details)}</Tab> {isTeamOwner && <Tab id="manage">{formatMessage(labels.manage)}</Tab>}
{isTeamOwner && <Tab id="manage">{formatMessage(labels.manage)}</Tab>} </TabList>
</TabList> <TabPanel id="details">
<TabPanel id="details"> <TeamEditForm teamId={teamId} allowEdit={canEdit} />
<TeamEditForm teamId={teamId} allowEdit={canEdit} /> </TabPanel>
</TabPanel> <TabPanel id="manage">
<TabPanel id="manage"> <TeamManage teamId={teamId} />
<TeamManage teamId={teamId} /> </TabPanel>
</TabPanel> </Tabs>
</Tabs>
</Panel>
</Column> </Column>
); );
} }

View file

@ -2,10 +2,11 @@
import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider'; import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider';
import { WebsiteAddButton } from '@/app/(main)/settings/websites/WebsiteAddButton'; import { WebsiteAddButton } from '@/app/(main)/settings/websites/WebsiteAddButton';
import { useLoginQuery, useMessages } from '@/components/hooks'; import { useLoginQuery, useMessages } from '@/components/hooks';
import { PageHeader } from '@/components/common/PageHeader'; import { SectionHeader } from '@/components/common/SectionHeader';
import { TeamWebsitesDataTable } from './TeamWebsitesDataTable'; import { TeamWebsitesDataTable } from './TeamWebsitesDataTable';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
import { useContext } from 'react'; import { useContext } from 'react';
import { Column } from '@umami/react-zen';
export function TeamWebsitesPage({ teamId }: { teamId: string }) { export function TeamWebsitesPage({ teamId }: { teamId: string }) {
const team = useContext(TeamContext); const team = useContext(TeamContext);
@ -18,11 +19,11 @@ export function TeamWebsitesPage({ teamId }: { teamId: string }) {
) && user.role !== ROLES.viewOnly; ) && user.role !== ROLES.viewOnly;
return ( return (
<> <Column gap>
<PageHeader title={formatMessage(labels.websites)}> <SectionHeader title={formatMessage(labels.websites)}>
{canEdit && <WebsiteAddButton teamId={teamId} />} {canEdit && <WebsiteAddButton teamId={teamId} />}
</PageHeader> </SectionHeader>
<TeamWebsitesDataTable teamId={teamId} allowEdit={canEdit} /> <TeamWebsitesDataTable teamId={teamId} allowEdit={canEdit} />
</> </Column>
); );
} }

View file

@ -1,15 +1,23 @@
'use client'; 'use client';
import { WebsitesHeader } from '@/app/(main)/settings/websites/WebsitesHeader';
import { WebsitesDataTable } from '@/app/(main)/settings/websites/WebsitesDataTable'; import { WebsitesDataTable } from '@/app/(main)/settings/websites/WebsitesDataTable';
import { useNavigation } from '@/components/hooks'; import { useMessages, useNavigation } from '@/components/hooks';
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';
export function WebsitesPage() { export function WebsitesPage() {
const { teamId } = useNavigation(); const { teamId } = useNavigation();
const { formatMessage, labels } = useMessages();
return ( return (
<> <Column gap="6">
<WebsitesHeader /> <PageHeader title={formatMessage(labels.websites)}>
<WebsitesDataTable teamId={teamId} allowEdit={false} /> <WebsiteAddButton teamId={teamId} />
</> </PageHeader>
<Panel>
<WebsitesDataTable teamId={teamId} allowEdit={false} />
</Panel>
</Column>
); );
} }

View file

@ -1,6 +1,6 @@
import { Grid, Heading, Column, Row } from '@umami/react-zen'; import { Grid, Heading, Column, Row } from '@umami/react-zen';
import { useDateRange, useMessages, useNavigation } from '@/components/hooks'; import { useDateRange, useMessages, useNavigation } from '@/components/hooks';
import { SideBar } from '@/components/common/SideBar'; import { SideMenu } from '@/components/common/SideMenu';
import { BrowsersTable } from '@/components/metrics/BrowsersTable'; import { BrowsersTable } from '@/components/metrics/BrowsersTable';
import { ChangeLabel } from '@/components/metrics/ChangeLabel'; import { ChangeLabel } from '@/components/metrics/ChangeLabel';
import { CitiesTable } from '@/components/metrics/CitiesTable'; import { CitiesTable } from '@/components/metrics/CitiesTable';
@ -146,7 +146,7 @@ export function WebsiteCompareTables({ websiteId }: { websiteId: string }) {
return ( return (
<Panel> <Panel>
<Grid columns={{ xs: '1fr', lg: '200px 1fr 1fr' }} gap="6"> <Grid columns={{ xs: '1fr', lg: '200px 1fr 1fr' }} gap="6">
<SideBar items={items} selectedKey={view} /> <SideMenu items={items} selectedKey={view} />
<Column border="left" paddingLeft="6"> <Column border="left" paddingLeft="6">
<Row alignItems="center" justifyContent="space-between"> <Row alignItems="center" justifyContent="space-between">
<Heading size="1">{formatMessage(labels.previous)}</Heading> <Heading size="1">{formatMessage(labels.previous)}</Heading>

View file

@ -1,7 +1,7 @@
import { Icon, Icons, Text, Grid, Column } from '@umami/react-zen'; import { Icon, Icons, Text, Grid, Column } from '@umami/react-zen';
import { LinkButton } from '@/components/common/LinkButton'; import { LinkButton } from '@/components/common/LinkButton';
import { useMessages, useNavigation } from '@/components/hooks'; import { useMessages, useNavigation } from '@/components/hooks';
import { SideBar } from '@/components/common/SideBar'; import { SideMenu } from '@/components/common/SideMenu';
import { BrowsersTable } from '@/components/metrics/BrowsersTable'; import { BrowsersTable } from '@/components/metrics/BrowsersTable';
import { CitiesTable } from '@/components/metrics/CitiesTable'; import { CitiesTable } from '@/components/metrics/CitiesTable';
import { CountriesTable } from '@/components/metrics/CountriesTable'; import { CountriesTable } from '@/components/metrics/CountriesTable';
@ -17,7 +17,6 @@ import { RegionsTable } from '@/components/metrics/RegionsTable';
import { ScreenTable } from '@/components/metrics/ScreenTable'; import { ScreenTable } from '@/components/metrics/ScreenTable';
import { TagsTable } from '@/components/metrics/TagsTable'; import { TagsTable } from '@/components/metrics/TagsTable';
import { ChannelsTable } from '@/components/metrics/ChannelsTable'; import { ChannelsTable } from '@/components/metrics/ChannelsTable';
import { Panel } from '@/components/common/Panel';
const views = { const views = {
url: PagesTable, url: PagesTable,
@ -56,77 +55,77 @@ export function WebsiteExpandedView({
const items = [ const items = [
{ {
key: 'url', id: 'url',
label: formatMessage(labels.pages), label: formatMessage(labels.pages),
url: renderUrl({ view: 'url' }), url: renderUrl({ view: 'url' }),
}, },
{ {
key: 'referrer', id: 'referrer',
label: formatMessage(labels.referrers), label: formatMessage(labels.referrers),
url: renderUrl({ view: 'referrer' }), url: renderUrl({ view: 'referrer' }),
}, },
{ {
key: 'channel', id: 'channel',
label: formatMessage(labels.channels), label: formatMessage(labels.channels),
url: renderUrl({ view: 'channel' }), url: renderUrl({ view: 'channel' }),
}, },
{ {
key: 'browser', id: 'browser',
label: formatMessage(labels.browsers), label: formatMessage(labels.browsers),
url: renderUrl({ view: 'browser' }), url: renderUrl({ view: 'browser' }),
}, },
{ {
key: 'os', id: 'os',
label: formatMessage(labels.os), label: formatMessage(labels.os),
url: renderUrl({ view: 'os' }), url: renderUrl({ view: 'os' }),
}, },
{ {
key: 'device', id: 'device',
label: formatMessage(labels.devices), label: formatMessage(labels.devices),
url: renderUrl({ view: 'device' }), url: renderUrl({ view: 'device' }),
}, },
{ {
key: 'country', id: 'country',
label: formatMessage(labels.countries), label: formatMessage(labels.countries),
url: renderUrl({ view: 'country' }), url: renderUrl({ view: 'country' }),
}, },
{ {
key: 'region', id: 'region',
label: formatMessage(labels.regions), label: formatMessage(labels.regions),
url: renderUrl({ view: 'region' }), url: renderUrl({ view: 'region' }),
}, },
{ {
key: 'city', id: 'city',
label: formatMessage(labels.cities), label: formatMessage(labels.cities),
url: renderUrl({ view: 'city' }), url: renderUrl({ view: 'city' }),
}, },
{ {
key: 'language', id: 'language',
label: formatMessage(labels.languages), label: formatMessage(labels.languages),
url: renderUrl({ view: 'language' }), url: renderUrl({ view: 'language' }),
}, },
{ {
key: 'screen', id: 'screen',
label: formatMessage(labels.screens), label: formatMessage(labels.screens),
url: renderUrl({ view: 'screen' }), url: renderUrl({ view: 'screen' }),
}, },
{ {
key: 'event', id: 'event',
label: formatMessage(labels.events), label: formatMessage(labels.events),
url: renderUrl({ view: 'event' }), url: renderUrl({ view: 'event' }),
}, },
{ {
key: 'query', id: 'query',
label: formatMessage(labels.queryParameters), label: formatMessage(labels.queryParameters),
url: renderUrl({ view: 'query' }), url: renderUrl({ view: 'query' }),
}, },
{ {
key: 'host', id: 'host',
label: formatMessage(labels.hosts), label: formatMessage(labels.hosts),
url: renderUrl({ view: 'host' }), url: renderUrl({ view: 'host' }),
}, },
{ {
key: 'tag', id: 'tag',
label: formatMessage(labels.tags), label: formatMessage(labels.tags),
url: renderUrl({ view: 'tag' }), url: renderUrl({ view: 'tag' }),
}, },
@ -143,20 +142,18 @@ export function WebsiteExpandedView({
</Icon> </Icon>
<Text>{formatMessage(labels.back)}</Text> <Text>{formatMessage(labels.back)}</Text>
</LinkButton> </LinkButton>
<SideBar items={items} selectedKey={view} /> <SideMenu items={items} selectedKey={view} />
</Column> </Column>
<Column> <Column>
<Panel> <DetailsComponent
<DetailsComponent websiteId={websiteId}
websiteId={websiteId} domainName={domainName}
domainName={domainName} animate={false}
animate={false} virtualize={true}
virtualize={true} itemCount={25}
itemCount={25} allowFilter={true}
allowFilter={true} allowSearch={true}
allowSearch={true} />
/>
</Panel>
</Column> </Column>
</Grid> </Grid>
); );

View file

@ -1,7 +1,7 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Page } from '@/components/common/Page'; import { Page } from '@/components/common/Page';
import { PageHeader } from '@/components/common/PageHeader'; import { SectionHeader } from '@/components/common/SectionHeader';
import { useApi, useMessages } from '@/components/hooks'; import { useApi, useMessages } from '@/components/hooks';
import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder'; import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder';
@ -22,7 +22,7 @@ export function RealtimeHome() {
return ( return (
<Page isLoading={isLoading || data?.length > 0} error={error}> <Page isLoading={isLoading || data?.length > 0} error={error}>
<PageHeader title={formatMessage(labels.realtime)} /> <SectionHeader title={formatMessage(labels.realtime)} />
{data?.length === 0 && ( {data?.length === 0 && (
<EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)} /> <EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)} />
)} )}

View file

@ -36,19 +36,19 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
const buttons = [ const buttons = [
{ {
label: formatMessage(labels.all), label: formatMessage(labels.all),
key: TYPE_ALL, id: TYPE_ALL,
}, },
{ {
label: formatMessage(labels.views), label: formatMessage(labels.views),
key: TYPE_PAGEVIEW, id: TYPE_PAGEVIEW,
}, },
{ {
label: formatMessage(labels.visitors), label: formatMessage(labels.visitors),
key: TYPE_SESSION, id: TYPE_SESSION,
}, },
{ {
label: formatMessage(labels.events), label: formatMessage(labels.events),
key: TYPE_EVENT, id: TYPE_EVENT,
}, },
]; ];
@ -160,7 +160,7 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
<div className={styles.table}> <div className={styles.table}>
<div className={styles.actions}> <div className={styles.actions}>
<SearchField className={styles.search} value={search} onSearch={setSearch} /> <SearchField className={styles.search} value={search} onSearch={setSearch} />
<FilterButtons items={buttons} selectedKey={filter} onSelect={setFilter} /> <FilterButtons items={buttons} value={filter} onChange={setFilter} />
</div> </div>
<div className={styles.header}>{formatMessage(labels.activity)}</div> <div className={styles.header}>{formatMessage(labels.activity)}</div>
<div className={styles.body}> <div className={styles.body}>

View file

@ -46,9 +46,9 @@ export function DataGrid({
}; };
return ( return (
<> <Column gap="4">
{allowSearch && (hasData || search) && ( {allowSearch && (hasData || search) && (
<Row width="280px" alignItems="center" marginBottom="6"> <Row width="280px" alignItems="center">
<SearchField <SearchField
value={search} value={search}
onSearch={handleSearch} onSearch={handleSearch}
@ -71,6 +71,6 @@ export function DataGrid({
</Row> </Row>
)} )}
</LoadingPanel> </LoadingPanel>
</> </Column>
); );
} }

View file

@ -5,6 +5,7 @@ import { useLocale } from '@/components/hooks';
export interface LinkButtonProps { export interface LinkButtonProps {
href: string; href: string;
target?: string;
scroll?: boolean; scroll?: boolean;
variant?: any; variant?: any;
children?: ReactNode; children?: ReactNode;
@ -12,8 +13,9 @@ export interface LinkButtonProps {
export function LinkButton({ export function LinkButton({
href, href,
variant = 'quiet', variant,
scroll = true, scroll = true,
target,
children, children,
...props ...props
}: LinkButtonProps) { }: LinkButtonProps) {
@ -21,7 +23,7 @@ export function LinkButton({
return ( return (
<Button {...props} variant={variant} asChild> <Button {...props} variant={variant} asChild>
<Link href={href} dir={dir} scroll={scroll}> <Link href={href} dir={dir} scroll={scroll} target={target}>
{children} {children}
</Link> </Link>
</Button> </Button>

View file

@ -1,10 +0,0 @@
.page {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
width: 100%;
max-width: 1320px;
margin: 0 auto;
padding: 0 20px;
}

View file

@ -1,48 +0,0 @@
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
align-content: center;
align-self: stretch;
flex-wrap: wrap;
height: 100px;
}
.header a {
color: var(--base600);
}
.header a:hover {
color: var(--base900);
}
.title {
display: flex;
align-items: center;
font-size: 24px;
font-weight: 700;
gap: 20px;
height: 60px;
flex: 1;
}
.breadcrumb {
padding-top: 20px;
}
.icon {
color: var(--base700);
margin-inline-end: 1rem;
}
.actions {
display: flex;
justify-content: flex-end;
}
@media only screen and (max-width: 992px) {
.header {
margin-bottom: 10px;
}
}

View file

@ -15,10 +15,10 @@ export function PageHeader({
children?: ReactNode; children?: ReactNode;
}) { }) {
return ( return (
<Row justifyContent="space-between" alignItems="center" marginY="6"> <Row justifyContent="space-between" alignItems="center" paddingY="6" border="bottom">
<Row gap="3"> <Row gap="3">
{icon && <Icon size="lg">{icon}</Icon>} {icon && <Icon>{icon}</Icon>}
{title && <Heading size="2">{title}</Heading>} {title && <Heading size="4">{title}</Heading>}
{description && <Text color="muted">{description}</Text>} {description && <Text color="muted">{description}</Text>}
</Row> </Row>
<Row justifyContent="flex-end">{children}</Row> <Row justifyContent="flex-end">{children}</Row>

View file

@ -1,32 +0,0 @@
.pager {
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: center;
}
.nav {
display: flex;
align-items: center;
justify-content: center;
}
.text {
font-size: var(--font-size-md);
margin: 0 16px;
justify-content: center;
}
.count {
color: var(--base600);
font-weight: 700;
}
@media only screen and (max-width: 992px) {
.pager {
grid-template-columns: repeat(2, 1fr);
}
.nav {
justify-content: flex-end;
}
}

View file

@ -0,0 +1,27 @@
import { ReactNode } from 'react';
import { Heading, Icon, Row, Text } from '@umami/react-zen';
export function SectionHeader({
title,
description,
icon,
children,
}: {
title: string;
description?: string;
icon?: ReactNode;
allowEdit?: boolean;
className?: string;
children?: ReactNode;
}) {
return (
<Row justifyContent="space-between" alignItems="center" height="60px">
<Row gap="3" alignItems="center">
{icon && <Icon>{icon}</Icon>}
{title && <Heading size="3">{title}</Heading>}
{description && <Text color="muted">{description}</Text>}
</Row>
<Row justifyContent="flex-end">{children}</Row>
</Row>
);
}

View file

@ -1,20 +0,0 @@
import { Text, List, ListItem } from '@umami/react-zen';
export interface MenuNavProps {
items: any[];
selectedKey?: string;
}
export function SideBar({ items, selectedKey }: MenuNavProps) {
return (
<List>
{items.map(({ key, label, url }) => {
return (
<ListItem key={key} href={url}>
<Text weight={key === selectedKey ? 'bold' : 'regular'}>{label}</Text>
</ListItem>
);
})}
</List>
);
}

View file

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

View file

@ -106,7 +106,6 @@ export function DateFilter({
placeholder={formatMessage(labels.selectDate)} placeholder={formatMessage(labels.selectDate)}
onSelectionChange={handleChange} onSelectionChange={handleChange}
renderValue={renderValue} renderValue={renderValue}
style={{ width: 'auto' }}
> >
{options.map(({ label, value, divider }: any) => { {options.map(({ label, value, divider }: any) => {
return ( return (

View file

@ -22,7 +22,7 @@ export function ProfileButton() {
const handleSelect = (key: Key) => { const handleSelect = (key: Key) => {
if (key === 'profile') { if (key === 'profile') {
router.push('/profile'); router.push('/settings/profile');
} }
if (key === 'logout') { if (key === 'logout') {
router.push('/logout'); router.push('/logout');

View file

@ -1,6 +1,6 @@
import { Button, Icon, DialogTrigger, Popover, Column, Label } from '@umami/react-zen'; import { Button, Icon, DialogTrigger, Popover, Column, Label } from '@umami/react-zen';
import { TimezoneSetting } from '@/app/(main)/profile/TimezoneSetting'; import { TimezoneSetting } from '@/app/(main)/settings/profile/TimezoneSetting';
import { DateRangeSetting } from '@/app/(main)/profile/DateRangeSetting'; import { DateRangeSetting } from '@/app/(main)/settings/profile/DateRangeSetting';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';

View file

@ -79,6 +79,7 @@ export const labels = defineMessages({
realtime: { id: 'label.realtime', defaultMessage: 'Realtime' }, realtime: { id: 'label.realtime', defaultMessage: 'Realtime' },
queries: { id: 'label.queries', defaultMessage: 'Queries' }, queries: { id: 'label.queries', defaultMessage: 'Queries' },
teams: { id: 'label.teams', defaultMessage: 'Teams' }, teams: { id: 'label.teams', defaultMessage: 'Teams' },
teamSettings: { id: 'label.team-settings', defaultMessage: 'Team settings' },
analytics: { id: 'label.analytics', defaultMessage: 'Analytics' }, analytics: { id: 'label.analytics', defaultMessage: 'Analytics' },
login: { id: 'label.login', defaultMessage: 'Login' }, login: { id: 'label.login', defaultMessage: 'Login' },
logout: { id: 'label.logout', defaultMessage: 'Logout' }, logout: { id: 'label.logout', defaultMessage: 'Logout' },

View file

@ -32,6 +32,7 @@ export { default as Profile } from './Profile';
export { default as Pushpin } from './Pushpin'; export { default as Pushpin } from './Pushpin';
export { default as Redo } from './Redo'; export { default as Redo } from './Redo';
export { default as Reports } from './Reports'; export { default as Reports } from './Reports';
export { default as Security } from './Security';
export { default as Speaker } from './Speaker'; export { default as Speaker } from './Speaker';
export { default as Sun } from './Sun'; export { default as Sun } from './Sun';
export { default as Tag } from './Tag'; export { default as Tag } from './Tag';