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 = [
{
source: '/settings',
destination: '/settings/websites',
destination: '/settings/profile',
permanent: true,
},
{

View file

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

48
pnpm-lock.yaml generated
View file

@ -210,6 +210,9 @@ importers:
'@rollup/plugin-terser':
specifier: ^0.4.4
version: 0.4.4(rollup@3.29.5)
'@svgr/cli':
specifier: ^8.1.0
version: 8.1.0(typescript@5.8.3)
'@svgr/rollup':
specifier: ^8.1.0
version: 8.1.0(rollup@3.29.5)(typescript@5.8.3)
@ -2711,6 +2714,11 @@ packages:
peerDependencies:
'@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':
resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==}
engines: {node: '>=14'}
@ -2725,6 +2733,12 @@ packages:
peerDependencies:
'@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':
resolution: {integrity: sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==}
engines: {node: '>=14'}
@ -3581,6 +3595,10 @@ packages:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'}
commander@9.5.0:
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
engines: {node: ^12.20.0 || >=14}
common-tags@1.8.2:
resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
engines: {node: '>=4.0.0'}
@ -3788,6 +3806,10 @@ packages:
resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==}
engines: {node: '>=0.10'}
dashify@2.0.0:
resolution: {integrity: sha512-hpA5C/YrPjucXypHPPc0oJ1l9Hf6wWbiOL7Ik42cxnsUOhWiCB/fylKbKqqJalW9FgkNQCw16YO8uW9Hs0Iy1A==}
engines: {node: '>=4'}
data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
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-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)':
dependencies:
'@babel/core': 7.26.10
@ -10414,6 +10452,12 @@ snapshots:
transitivePeerDependencies:
- 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)':
dependencies:
'@svgr/core': 8.1.0(typescript@5.8.3)
@ -11400,6 +11444,8 @@ snapshots:
commander@8.3.0: {}
commander@9.5.0: {}
common-tags@1.8.2: {}
commondir@1.0.1: {}
@ -11690,6 +11736,8 @@ snapshots:
dependencies:
assert-plus: 1.0.0
dashify@2.0.0: {}
data-uri-to-buffer@4.0.1: {}
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 { PageHeader } from '@/components/common/PageHeader';
export function BoardsPage() {
return (
<Column>
<Heading>My Boards</Heading>
<PageHeader title="My Boards" />
<Link href="/teams/3a97e34a-7f9d-4de2-8754-ed81714b528d/boards/86d4095c-a2a8-4fc8-9521-103e858e2b41">
Board 1
</Link>

View file

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

View file

@ -1,6 +1,6 @@
'use client';
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 { WebsiteChartList } from '../websites/[websiteId]/WebsiteChartList';
import { DashboardSettingsButton } from '@/app/(main)/dashboard/DashboardSettingsButton';
@ -30,9 +30,9 @@ export function DashboardPage() {
return (
<section style={{ marginBottom: 60 }}>
<PageHeader title={formatMessage(labels.dashboard)}>
<SectionHeader title={formatMessage(labels.dashboard)}>
{!editing && hasData && <DashboardSettingsButton />}
</PageHeader>
</SectionHeader>
{!hasData && (
<EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)}>
<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 { useLoginQuery, useMessages, useNavigation } from '@/components/hooks';
import { LinkButton } from '@/components/common/LinkButton';
@ -11,7 +11,7 @@ export function ReportsHeader() {
const canEdit = user.role !== ROLES.viewOnly;
return (
<PageHeader title={formatMessage(labels.reports)}>
<SectionHeader title={formatMessage(labels.reports)}>
{canEdit && (
<LinkButton href={renderTeamUrl('/reports/create')} variant="primary">
<Icon>
@ -20,6 +20,6 @@ export function ReportsHeader() {
<Text>{formatMessage(labels.createReport)}</Text>
</LinkButton>
)}
</PageHeader>
</SectionHeader>
);
}

View file

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

View file

@ -1,31 +1,49 @@
'use client';
import { ReactNode } from 'react';
import { Grid, Column } from '@umami/react-zen';
import { useLoginQuery, useMessages } from '@/components/hooks';
import { SideBar } from '@/components/common/SideBar';
import { useLoginQuery, useMessages, useNavigation } from '@/components/hooks';
import { SideMenu } from '@/components/common/SideMenu';
import { PageHeader } from '@/components/common/PageHeader';
import { Panel } from '@/components/common/Panel';
export function SettingsLayout({ children }: { children: ReactNode }) {
const { user } = useLoginQuery();
const { formatMessage, labels } = useMessages();
const { pathname } = useNavigation();
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),
url: '/settings/websites',
},
{ key: 'teams', label: formatMessage(labels.teams), url: '/settings/teams' },
user.isAdmin && {
key: 'users',
id: 'users',
label: formatMessage(labels.users),
url: '/settings/users',
},
].filter(n => n);
const value = items.find(({ url }) => pathname.includes(url))?.id;
return (
<Grid>
<SideBar items={items} />
<Column>{children}</Column>
</Grid>
<Column gap="6">
<PageHeader title={formatMessage(labels.settings)} />
<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 { Button, Flexbox } from '@umami/react-zen';
import { Button, Row } from '@umami/react-zen';
import { useDateRange, useMessages } from '@/components/hooks';
import { DEFAULT_DATE_RANGE } from '@/lib/constants';
import { DateRange } from '@/lib/types';
import styles from './DateRangeSetting.module.css';
export function DateRangeSetting() {
const { formatMessage, labels } = useMessages();
@ -14,15 +13,14 @@ export function DateRangeSetting() {
const handleReset = () => saveDateRange(DEFAULT_DATE_RANGE);
return (
<Flexbox gap="3" width="300px">
<Row gap="3">
<DateFilter
className={styles.field}
value={value}
startDate={dateRange.startDate}
endDate={dateRange.endDate}
onChange={handleChange}
/>
<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 { PasswordEditForm } from '@/app/(main)/profile/PasswordEditForm';
import { PasswordEditForm } from './PasswordEditForm';
import { Icons } from '@/components/icons';
import { useMessages } from '@/components/hooks';
@ -14,7 +14,7 @@ export function PasswordChangeButton() {
return (
<DialogTrigger>
<Button>
<Icon>
<Icon fillColor="currentColor">
<Icons.Lock />
</Icon>
<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 { 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 { Row, Column, Label } from '@umami/react-zen';
import { useLoginQuery, useMessages } from '@/components/hooks';
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() {
const { user } = useLoginQuery();
@ -47,7 +47,9 @@ export function ProfileSettings() {
{!cloudMode && (
<Column>
<Label>{formatMessage(labels.password)}</Label>
<PasswordChangeButton />
<Row>
<PasswordChangeButton />
</Row>
</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 { useTimezone, useMessages } from '@/components/hooks';
import { getTimezone } from '@/lib/date';
import styles from './TimezoneSetting.module.css';
const timezones = Intl.supportedValuesOf('timeZone');
@ -25,7 +24,6 @@ export function TimezoneSetting() {
return (
<Row gap="3">
<Select
className={styles.dropdown}
selectedKey={timezone}
onChange={(value: any) => saveTimezone(value)}
allowSearch={true}

View file

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

View file

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

View file

@ -1,12 +1,13 @@
'use client';
import { TeamsDataTable } from './TeamsDataTable';
import { TeamsHeader } from './TeamsHeader';
import { Column } from '@umami/react-zen';
export function TeamsSettingsPage() {
return (
<>
<Column gap>
<TeamsHeader />
<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';
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() {
const { formatMessage, labels } = useMessages();
const handleSave = () => {};
return (
<>
<UsersHeader />
<Column gap>
<SectionHeader title={formatMessage(labels.users)}>
<UserAddButton onSave={handleSave} />
</SectionHeader>
<UsersDataTable />
</>
</Column>
);
}

View file

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

View file

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

View file

@ -1,17 +1,22 @@
'use client';
import { useLoginQuery } from '@/components/hooks';
import { useLoginQuery, useMessages } from '@/components/hooks';
import { WebsitesDataTable } from './WebsitesDataTable';
import { WebsitesHeader } from './WebsitesHeader';
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 }) {
const { user } = useLoginQuery();
const canCreate = user.role !== ROLES.viewOnly;
const { formatMessage, labels } = useMessages();
return (
<>
<WebsitesHeader allowCreate={canCreate} />
<Column gap>
<SectionHeader title={formatMessage(labels.websites)}>
{canCreate && <WebsiteAddButton teamId={teamId} />}
</SectionHeader>
<WebsitesDataTable teamId={teamId} />
</>
</Column>
);
}

View file

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

View file

@ -2,34 +2,48 @@
import { ReactNode } from 'react';
import { useMessages, useNavigation } from '@/components/hooks';
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 }) {
const { formatMessage, labels } = useMessages();
const { teamId } = useNavigation();
const { pathname, teamId } = useNavigation();
const items = [
{
key: 'team',
id: 'team',
label: formatMessage(labels.team),
url: `/teams/${teamId}/settings/team`,
},
{
key: 'websites',
id: 'websites',
label: formatMessage(labels.websites),
url: `/teams/${teamId}/settings/websites`,
},
{
key: 'members',
id: 'members',
label: formatMessage(labels.members),
url: `/teams/${teamId}/settings/members`,
},
].filter(n => n);
const value = items.find(({ url }) => pathname.endsWith(url))?.id;
return (
<Grid>
<SideBar items={items} />
<Column>{children}</Column>
</Grid>
<Column gap="6">
<PageHeader title={formatMessage(labels.teamSettings)} />
<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';
import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider';
import { TeamMembersDataTable } from './TeamMembersDataTable';
import { PageHeader } from '@/components/common/PageHeader';
import { SectionHeader } from '@/components/common/SectionHeader';
import { useLoginQuery, useMessages } from '@/components/hooks';
import { ROLES } from '@/lib/constants';
import { useContext } from 'react';
import { Column } from '@umami/react-zen';
export function TeamMembersPage({ teamId }: { teamId: string }) {
const team = useContext(TeamContext);
@ -18,9 +19,9 @@ export function TeamMembersPage({ teamId }: { teamId: string }) {
) && user.role !== ROLES.viewOnly;
return (
<>
<PageHeader title={formatMessage(labels.members)} />
<Column gap>
<SectionHeader title={formatMessage(labels.members)} />
<TeamMembersDataTable teamId={teamId} allowEdit={canEdit} />
</>
</Column>
);
}

View file

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

View file

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

View file

@ -1,15 +1,23 @@
'use client';
import { WebsitesHeader } from '@/app/(main)/settings/websites/WebsitesHeader';
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() {
const { teamId } = useNavigation();
const { formatMessage, labels } = useMessages();
return (
<>
<WebsitesHeader />
<WebsitesDataTable teamId={teamId} allowEdit={false} />
</>
<Column gap="6">
<PageHeader title={formatMessage(labels.websites)}>
<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 { 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 { ChangeLabel } from '@/components/metrics/ChangeLabel';
import { CitiesTable } from '@/components/metrics/CitiesTable';
@ -146,7 +146,7 @@ export function WebsiteCompareTables({ websiteId }: { websiteId: string }) {
return (
<Panel>
<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">
<Row alignItems="center" justifyContent="space-between">
<Heading size="1">{formatMessage(labels.previous)}</Heading>

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ import { useLocale } from '@/components/hooks';
export interface LinkButtonProps {
href: string;
target?: string;
scroll?: boolean;
variant?: any;
children?: ReactNode;
@ -12,8 +13,9 @@ export interface LinkButtonProps {
export function LinkButton({
href,
variant = 'quiet',
variant,
scroll = true,
target,
children,
...props
}: LinkButtonProps) {
@ -21,7 +23,7 @@ export function LinkButton({
return (
<Button {...props} variant={variant} asChild>
<Link href={href} dir={dir} scroll={scroll}>
<Link href={href} dir={dir} scroll={scroll} target={target}>
{children}
</Link>
</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;
}) {
return (
<Row justifyContent="space-between" alignItems="center" marginY="6">
<Row justifyContent="space-between" alignItems="center" paddingY="6" border="bottom">
<Row gap="3">
{icon && <Icon size="lg">{icon}</Icon>}
{title && <Heading size="2">{title}</Heading>}
{icon && <Icon>{icon}</Icon>}
{title && <Heading size="4">{title}</Heading>}
{description && <Text color="muted">{description}</Text>}
</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)}
onSelectionChange={handleChange}
renderValue={renderValue}
style={{ width: 'auto' }}
>
{options.map(({ label, value, divider }: any) => {
return (

View file

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

View file

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

View file

@ -79,6 +79,7 @@ export const labels = defineMessages({
realtime: { id: 'label.realtime', defaultMessage: 'Realtime' },
queries: { id: 'label.queries', defaultMessage: 'Queries' },
teams: { id: 'label.teams', defaultMessage: 'Teams' },
teamSettings: { id: 'label.team-settings', defaultMessage: 'Team settings' },
analytics: { id: 'label.analytics', defaultMessage: 'Analytics' },
login: { id: 'label.login', defaultMessage: 'Login' },
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 Redo } from './Redo';
export { default as Reports } from './Reports';
export { default as Security } from './Security';
export { default as Speaker } from './Speaker';
export { default as Sun } from './Sun';
export { default as Tag } from './Tag';