mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-14 21:10:34 +01:00
Updated navigation.
This commit is contained in:
parent
611169c65f
commit
fc2a8f3d9f
@ -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 (
|
||||||
|
53
components/input/ProfileButton.js
Normal file
53
components/input/ProfileButton.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
9
components/input/ProfileButton.module.css
Normal file
9
components/input/ProfileButton.module.css
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.menu {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
background: var(--base50);
|
||||||
|
}
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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>
|
||||||
|
@ -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
8
hooks/useMessages.js
Normal 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 };
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user