Updated navigation.

This commit is contained in:
Mike Cao 2023-03-21 21:28:36 -07:00
parent 611169c65f
commit fc2a8f3d9f
13 changed files with 156 additions and 123 deletions

View File

@ -1,14 +1,11 @@
import { Icon, Button, PopupTrigger, Popup, Tooltip, Text } from 'react-basics'; import { Icon, Button, PopupTrigger, Popup, Text } from 'react-basics';
import { useIntl } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { languages } from 'lib/lang'; import { languages } from 'lib/lang';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import Icons from 'components/icons'; import Icons from 'components/icons';
import { labels } from 'components/messages';
import styles from './LanguageButton.module.css'; import styles from './LanguageButton.module.css';
export default function LanguageButton({ tooltipPosition = 'top', menuPosition = 'right' }) { export default function LanguageButton() {
const { formatMessage } = useIntl();
const { locale, saveLocale } = useLocale(); const { locale, saveLocale } = useLocale();
const items = Object.keys(languages).map(key => ({ ...languages[key], value: key })); const items = Object.keys(languages).map(key => ({ ...languages[key], value: key }));
@ -18,14 +15,12 @@ export default function LanguageButton({ tooltipPosition = 'top', menuPosition =
return ( return (
<PopupTrigger> <PopupTrigger>
<Tooltip label={formatMessage(labels.language)} position={tooltipPosition}>
<Button variant="quiet"> <Button variant="quiet">
<Icon> <Icon>
<Icons.Globe /> <Icons.Globe />
</Icon> </Icon>
</Button> </Button>
</Tooltip> <Popup position="bottom" alignment="end">
<Popup position={menuPosition} alignment="end">
<div className={styles.menu}> <div className={styles.menu}>
{items.map(({ value, label }) => { {items.map(({ value, label }) => {
return ( return (

View File

@ -0,0 +1,53 @@
import { Icon, Button, PopupTrigger, Popup, Menu, Item, Text } from 'react-basics';
import { useRouter } from 'next/router';
import Icons from 'components/icons';
import useMessages from 'hooks/useMessages';
import useUser from 'hooks/useUser';
import styles from './ProfileButton.module.css';
export default function ProfileButton() {
const { formatMessage, labels } = useMessages();
const { user } = useUser();
const router = useRouter();
const handleSelect = key => {
if (key === 'profile') {
router.push('/settings/profile');
}
if (key === 'logout') {
router.push('/logout');
}
};
return (
<PopupTrigger>
<Button variant="quiet">
<Icon>
<Icons.Profile />
</Icon>
<Icon size="sm">
<Icons.ChevronDown />
</Icon>
</Button>
<Popup position="bottom" alignment="end">
<Menu variant="popup" onSelect={handleSelect} className={styles.menu}>
<Item key="user" className={styles.item}>
<Text>{user.username}</Text>
</Item>
<Item key="profile" className={styles.item} divider={true}>
<Icon>
<Icons.User />
</Icon>
<Text>{formatMessage(labels.profile)}</Text>
</Item>
<Item key="logout" className={styles.item}>
<Icon>
<Icons.Logout />
</Icon>
<Text>{formatMessage(labels.logout)}</Text>
</Item>
</Menu>
</Popup>
</PopupTrigger>
);
}

View File

@ -0,0 +1,9 @@
.menu {
width: 200px;
}
.item {
display: flex;
gap: 12px;
background: var(--base50);
}

View File

@ -1,14 +1,12 @@
import { useTransition, animated } from 'react-spring'; import { useTransition, animated } from 'react-spring';
import { Button, Icon, Tooltip } from 'react-basics'; import { Button, Icon } from 'react-basics';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useTheme from 'hooks/useTheme'; import useTheme from 'hooks/useTheme';
import Icons from 'components/icons'; import Icons from 'components/icons';
import { labels } from 'components/messages';
import styles from './ThemeButton.module.css'; import styles from './ThemeButton.module.css';
export default function ThemeButton({ tooltipPosition = 'top' }) { export default function ThemeButton() {
const [theme, setTheme] = useTheme(); const [theme, setTheme] = useTheme();
const { formatMessage } = useIntl();
const transitions = useTransition(theme, { const transitions = useTransition(theme, {
initial: { opacity: 1 }, initial: { opacity: 1 },
@ -28,7 +26,6 @@ export default function ThemeButton({ tooltipPosition = 'top' }) {
} }
return ( return (
<Tooltip label={formatMessage(labels.theme)} position={tooltipPosition}>
<Button variant="quiet" className={styles.button} onClick={handleClick}> <Button variant="quiet" className={styles.button} onClick={handleClick}>
{transitions((style, item) => ( {transitions((style, item) => (
<animated.div key={item} style={style}> <animated.div key={item} style={style}>
@ -36,6 +33,5 @@ export default function ThemeButton({ tooltipPosition = 'top' }) {
</animated.div> </animated.div>
))} ))}
</Button> </Button>
</Tooltip>
); );
} }

View File

@ -1,15 +1,21 @@
.layout { .layout {
display: grid; display: grid;
grid-template-rows: 1fr; grid-template-rows: max-content 1fr;
grid-template-columns: max-content 1fr; grid-template-columns: 1fr;
} }
.nav { .nav {
grid-row: 1 / 3; height: 60px;
width: 100vw;
z-index: 100;
grid-column: 1;
grid-row: 1 / 2;
} }
.body { .body {
grid-area: 1 / 2; grid-column: 1;
overflow: auto; grid-row: 2 / 3;
height: 100vh; min-height: 0;
max-height: calc(100vh - 60px);
overflow-y: auto;
} }

View File

@ -1,69 +1,50 @@
import { useState } from 'react'; import { useState } from 'react';
import { useIntl } from 'react-intl';
import { Icon, Text } from 'react-basics'; import { Icon, Text } from 'react-basics';
import Link from 'next/link';
import classNames from 'classnames'; import classNames from 'classnames';
import Icons from 'components/icons'; import Icons from 'components/icons';
import ThemeButton from 'components/input/ThemeButton'; import ThemeButton from 'components/input/ThemeButton';
import LanguageButton from 'components/input/LanguageButton'; import LanguageButton from 'components/input/LanguageButton';
import LogoutButton from 'components/input/LogoutButton'; import ProfileButton from 'components/input/ProfileButton';
import { labels } from 'components/messages';
import useUser from 'hooks/useUser';
import NavGroup from './NavGroup';
import styles from './NavBar.module.css'; import styles from './NavBar.module.css';
import useConfig from 'hooks/useConfig'; import useConfig from 'hooks/useConfig';
import useMessages from 'hooks/useMessages';
export default function NavBar() { export default function NavBar() {
const { user } = useUser();
const { cloudMode } = useConfig(); const { cloudMode } = useConfig();
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const [minimized, setMinimized] = useState(false); const [minimized, setMinimized] = useState(false);
const tooltipPosition = minimized ? 'right' : 'top';
const analytics = [ const links = [
{ label: formatMessage(labels.dashboard), url: '/dashboard', icon: <Icons.Dashboard /> }, { label: formatMessage(labels.dashboard), url: '/dashboard', icon: <Icons.Dashboard /> },
{ label: formatMessage(labels.realtime), url: '/realtime', icon: <Icons.Clock /> }, { label: formatMessage(labels.realtime), url: '/realtime', icon: <Icons.Clock /> },
]; !cloudMode && { label: formatMessage(labels.settings), url: '/settings', icon: <Icons.Gear /> },
const settings = [
!cloudMode && {
label: formatMessage(labels.websites),
url: '/settings/websites',
icon: <Icons.Globe />,
},
user?.isAdmin && {
label: formatMessage(labels.users),
url: '/settings/users',
icon: <Icons.User />,
},
!cloudMode && {
label: formatMessage(labels.teams),
url: '/settings/teams',
icon: <Icons.Users />,
},
{ label: formatMessage(labels.profile), url: '/settings/profile', icon: <Icons.Profile /> },
].filter(n => n); ].filter(n => n);
const handleMinimize = () => setMinimized(state => !state); const handleMinimize = () => setMinimized(state => !state);
return ( return (
<div className={classNames(styles.navbar, { [styles.minimized]: minimized })}> <div className={classNames(styles.navbar, { [styles.minimized]: minimized })}>
<div className={styles.header} onClick={handleMinimize}> <div className={styles.logo} onClick={handleMinimize}>
<Icon size="lg"> <Icon size="lg">
<Icons.Logo /> <Icons.Logo />
</Icon> </Icon>
<Text className={styles.text}>umami</Text> <Text className={styles.text}>umami</Text>
<Icon size="sm" rotate={minimized ? -90 : 90} className={styles.icon}>
<Icons.ChevronDown />
</Icon>
</div> </div>
<NavGroup title={formatMessage(labels.analytics)} items={analytics} minimized={minimized} /> <div className={styles.links}>
<NavGroup title={formatMessage(labels.settings)} items={settings} minimized={minimized} /> {links.map(({ url, icon, label }) => {
<div className={styles.footer}> return (
<div className={styles.buttons}> <Link key={url} href={url}>
<ThemeButton tooltipPosition={tooltipPosition} /> <Icon>{icon}</Icon>
<LanguageButton tooltipPosition={tooltipPosition} /> <Text>{label}</Text>
{!cloudMode && <LogoutButton tooltipPosition={tooltipPosition} />} </Link>
);
})}
</div> </div>
<div className={styles.actions}>
<ThemeButton />
<LanguageButton />
{!cloudMode && <ProfileButton />}
</div> </div>
</div> </div>
); );

View File

@ -1,62 +1,49 @@
.navbar { .navbar {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: row;
align-items: center; align-items: center;
height: 60px;
background: var(--base75); background: var(--base75);
height: 100%; border-bottom: 1px solid var(--base200);
width: 200px; padding: 0 20px;
border-right: 2px solid var(--base200);
} }
.header { .logo {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 10px; gap: 10px;
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
padding: 20px 0;
cursor: pointer; cursor: pointer;
min-width: 0;
} }
.header:hover .icon { .links {
visibility: visible;
}
.icon {
visibility: hidden;
position: absolute;
right: -10px;
border-radius: 100%;
color: var(--base50);
background: var(--base800);
height: 20px;
width: 20px;
}
.minimized.navbar {
width: 60px;
}
.minimized .text {
display: none;
}
.footer {
flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: row;
justify-content: flex-end; gap: 20px;
padding: 20px; padding: 0 40px;
flex: 1;
font-weight: 700;
} }
.buttons { .links a {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: 10px;
color: var(--font-color100);
} }
.minimized .buttons { .links a:hover {
flex-direction: column; color: var(--primary400);
}
.actions {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
min-width: 0;
} }

View File

@ -1,6 +1,6 @@
import classNames from 'classnames'; import classNames from 'classnames';
import styles from './Page.module.css';
import { Banner, Loading } from 'react-basics'; import { Banner, Loading } from 'react-basics';
import styles from './Page.module.css';
export default function Page({ className, error, loading, children }) { export default function Page({ className, error, loading, children }) {
if (error) { if (error) {

View File

@ -84,7 +84,7 @@ export default function WebsiteChart({
<StickyHeader <StickyHeader
stickyClassName={styles.sticky} stickyClassName={styles.sticky}
enabled={stickyHeader} enabled={stickyHeader}
scrollElement={document.getElementById(UI_LAYOUT_BODY) || document} scrollElement={document.getElementById(UI_LAYOUT_BODY)}
> >
<Row className={styles.header}> <Row className={styles.header}>
<Column> <Column>

View File

@ -1,15 +1,14 @@
import { Form, FormRow } from 'react-basics'; import { Form, FormRow } from 'react-basics';
import { useIntl } from 'react-intl';
import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting'; import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting';
import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting'; import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting';
import LanguageSetting from 'components/pages/settings/profile/LanguageSetting'; import LanguageSetting from 'components/pages/settings/profile/LanguageSetting';
import ThemeSetting from 'components/pages/settings/profile/ThemeSetting'; import ThemeSetting from 'components/pages/settings/profile/ThemeSetting';
import useUser from 'hooks/useUser'; import useUser from 'hooks/useUser';
import { labels } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function ProfileDetails() { export default function ProfileDetails() {
const { user } = useUser(); const { user } = useUser();
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
if (!user) { if (!user) {
return null; return null;
@ -20,7 +19,9 @@ export default function ProfileDetails() {
return ( return (
<Form> <Form>
<FormRow label={formatMessage(labels.username)}>{username}</FormRow> <FormRow label={formatMessage(labels.username)}>{username}</FormRow>
<FormRow label={formatMessage(labels.role)}>{role}</FormRow> <FormRow label={formatMessage(labels.role)}>
{formatMessage(labels[role] || labels.unknown)}
</FormRow>
<FormRow label={formatMessage(labels.defaultDateRange)}> <FormRow label={formatMessage(labels.defaultDateRange)}>
<DateRangeSetting /> <DateRangeSetting />
</FormRow> </FormRow>

8
hooks/useMessages.js Normal file
View File

@ -0,0 +1,8 @@
import { useIntl } from 'react-intl';
import { messages, labels } from 'components/messages';
export default function useMessages() {
const { formatMessage } = useIntl();
return { formatMessage, messages, labels };
}

View File

@ -29,7 +29,7 @@ export default function App({ Component, pageProps }) {
const Wrapper = ({ children }) => <span className={locale}>{children}</span>; const Wrapper = ({ children }) => <span className={locale}>{children}</span>;
if (!config || config.uiDisabled) { if (config?.uiDisabled) {
return null; return null;
} }

View File

@ -1,8 +1,3 @@
html {
overflow-x: hidden;
margin-right: calc(-1 * (100vw - 100%));
}
html, html,
body { body {
font-family: Inter, -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, font-family: Inter, -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans,
@ -20,6 +15,7 @@ body {
flex: 1; flex: 1;
color: var(--font-color100); color: var(--font-color100);
background: var(--base50); background: var(--base50);
overflow: hidden;
} }
*, *,
@ -64,7 +60,8 @@ svg {
#__next { #__next {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1;
width: 100%; width: 100%;
height: 100%; height: 100%;
flex: 1; overflow: hidden;
} }