mirror of
https://github.com/umami-software/umami.git
synced 2026-02-07 06:07:17 +01:00
Progress check-in.
This commit is contained in:
parent
30274a07fd
commit
54d5af5cbb
35 changed files with 540 additions and 405 deletions
51
components/input/LanguageButton.js
Normal file
51
components/input/LanguageButton.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { Icon, Button, PopupTrigger, Popup, Tooltip, Text } from 'react-basics';
|
||||
import { useIntl } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { languages } from 'lib/lang';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import Icons from 'components/icons';
|
||||
import { labels } from 'components/messages';
|
||||
import styles from './LanguageButton.module.css';
|
||||
|
||||
export default function LanguageButton({ tooltipPosition = 'top' }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const { locale, saveLocale } = useLocale();
|
||||
const items = Object.keys(languages).map(key => ({ ...languages[key], value: key }));
|
||||
|
||||
function handleSelect(value) {
|
||||
saveLocale(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<PopupTrigger>
|
||||
<PopupTrigger action="hover">
|
||||
<Button variant="quiet">
|
||||
<Icon>
|
||||
<Icons.Globe />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Tooltip position={tooltipPosition}>{formatMessage(labels.language)}</Tooltip>
|
||||
</PopupTrigger>
|
||||
<Popup position="right" alignment="end">
|
||||
<div className={styles.menu}>
|
||||
{items.map(({ value, label }) => {
|
||||
return (
|
||||
<div
|
||||
key={value}
|
||||
className={classNames(styles.item, { [styles.selected]: value === locale })}
|
||||
onClick={handleSelect.bind(null, value)}
|
||||
>
|
||||
<Text>{label}</Text>
|
||||
{value === locale && (
|
||||
<Icon className={styles.icon}>
|
||||
<Icons.Check />
|
||||
</Icon>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
);
|
||||
}
|
||||
35
components/input/LanguageButton.module.css
Normal file
35
components/input/LanguageButton.module.css
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
.menu {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
min-width: 600px;
|
||||
max-width: 100vw;
|
||||
padding: 10px;
|
||||
background: var(--base50);
|
||||
z-index: var(--z-index100);
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--border-color);
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: calc(100% / 3);
|
||||
border-radius: 5px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background: var(--base75);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selected {
|
||||
font-weight: 700;
|
||||
background: var(--blue100);
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--primary400);
|
||||
}
|
||||
22
components/input/LogoutButton.js
Normal file
22
components/input/LogoutButton.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { Button, Icon, Icons, PopupTrigger, Tooltip } from 'react-basics';
|
||||
import Link from 'next/link';
|
||||
import { labels } from 'components/messages';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
export default function LogoutButton({ tooltipPosition = 'top' }) {
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<Link href="/logout">
|
||||
<a>
|
||||
<PopupTrigger action="hover">
|
||||
<Button variant="quiet">
|
||||
<Icon>
|
||||
<Icons.Logout />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Tooltip position={tooltipPosition}>{formatMessage(labels.logout)}</Tooltip>
|
||||
</PopupTrigger>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
43
components/input/RefreshButton.js
Normal file
43
components/input/RefreshButton.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Button, Icon, Tooltip } from '../react-basics';
|
||||
import useStore from 'store/queries';
|
||||
import { setDateRange } from 'store/websites';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import Icons from 'components/icons';
|
||||
import { labels } from 'components/messages';
|
||||
|
||||
function RefreshButton({ websiteId }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const selector = useCallback(state => state[`/websites/${websiteId}/stats`], [websiteId]);
|
||||
const completed = useStore(selector);
|
||||
|
||||
function handleClick() {
|
||||
if (!loading && dateRange) {
|
||||
setLoading(true);
|
||||
if (/^\d+/.test(dateRange.value)) {
|
||||
setDateRange(websiteId, dateRange.value);
|
||||
} else {
|
||||
setDateRange(websiteId, dateRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(false);
|
||||
}, [completed]);
|
||||
|
||||
return (
|
||||
<Tooltip label={formatMessage(labels.refresh)}>
|
||||
<Button onClick={handleClick}>
|
||||
<Icon>
|
||||
<Icons.Refresh />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default RefreshButton;
|
||||
49
components/input/SettingsButton.js
Normal file
49
components/input/SettingsButton.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { useRef, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import TimezoneSetting from '../pages/settings/profile/TimezoneSetting';
|
||||
import DateRangeSetting from '../pages/settings/profile/DateRangeSetting';
|
||||
import { Button, Icon } from 'react-basics';
|
||||
import styles from './SettingsButton.module.css';
|
||||
import Gear from 'assets/gear.svg';
|
||||
import useDocumentClick from '../../hooks/useDocumentClick';
|
||||
|
||||
export default function SettingsButton() {
|
||||
const [show, setShow] = useState(false);
|
||||
const ref = useRef();
|
||||
|
||||
function handleClick() {
|
||||
setShow(state => !state);
|
||||
}
|
||||
|
||||
useDocumentClick(e => {
|
||||
if (!ref.current?.contains(e.target)) {
|
||||
setShow(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.button} ref={ref}>
|
||||
<Button variant="light" onClick={handleClick}>
|
||||
<Icon>
|
||||
<Gear />
|
||||
</Icon>
|
||||
</Button>
|
||||
{show && (
|
||||
<div className={styles.panel}>
|
||||
<dt>
|
||||
<FormattedMessage id="label.timezone" defaultMessage="Timezone" />
|
||||
</dt>
|
||||
<dd>
|
||||
<TimezoneSetting />
|
||||
</dd>
|
||||
<dt>
|
||||
<FormattedMessage id="label.default-date-range" defaultMessage="Default date range" />
|
||||
</dt>
|
||||
<dd>
|
||||
<DateRangeSetting />
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
components/input/SettingsButton.module.css
Normal file
20
components/input/SettingsButton.module.css
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
.button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--base50);
|
||||
border: 1px solid var(--base500);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
padding: 20px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.panel dd {
|
||||
display: flex;
|
||||
}
|
||||
41
components/input/ThemeButton.js
Normal file
41
components/input/ThemeButton.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { useTransition, animated } from 'react-spring';
|
||||
import { Button, Icon, PopupTrigger, Tooltip } from 'react-basics';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import Icons from 'components/icons';
|
||||
import { labels } from 'components/messages';
|
||||
import styles from './ThemeButton.module.css';
|
||||
|
||||
export default function ThemeButton({ tooltipPosition = 'top' }) {
|
||||
const [theme, setTheme] = useTheme();
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const transitions = useTransition(theme, {
|
||||
initial: { opacity: 1 },
|
||||
from: {
|
||||
opacity: 0,
|
||||
transform: `translateY(${theme === 'light' ? '20px' : '-20px'}) scale(0.5)`,
|
||||
},
|
||||
enter: { opacity: 1, transform: 'translateY(0px) scale(1.0)' },
|
||||
leave: {
|
||||
opacity: 0,
|
||||
transform: `translateY(${theme === 'light' ? '-20px' : '20px'}) scale(0.5)`,
|
||||
},
|
||||
});
|
||||
|
||||
function handleClick() {
|
||||
setTheme(theme === 'light' ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip label={formatMessage(labels.theme)} position={tooltipPosition}>
|
||||
<Button variant="quiet" className={styles.button} onClick={handleClick}>
|
||||
{transitions((style, item) => (
|
||||
<animated.div key={item} style={style}>
|
||||
<Icon className={styles.icon}>{item === 'light' ? <Icons.Sun /> : <Icons.Moon />}</Icon>
|
||||
</animated.div>
|
||||
))}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
15
components/input/ThemeButton.module.css
Normal file
15
components/input/ThemeButton.module.css
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
.button {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button > div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
}
|
||||
80
components/input/UserButton.js
Normal file
80
components/input/UserButton.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import useConfig from 'hooks/useConfig';
|
||||
import useUser from 'hooks/useUser';
|
||||
import { AUTH_TOKEN } from 'lib/constants';
|
||||
import { removeItem } from 'next-basics';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useRef, useState } from 'react';
|
||||
import { Button, Icon, Item, Menu, Popup, Text } from 'react-basics';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import useDocumentClick from 'hooks/useDocumentClick';
|
||||
import Profile from 'assets/profile.svg';
|
||||
import styles from './UserButton.module.css';
|
||||
|
||||
export default function UserButton() {
|
||||
const [show, setShow] = useState(false);
|
||||
const ref = useRef();
|
||||
const { user } = useUser();
|
||||
const router = useRouter();
|
||||
const { adminDisabled } = useConfig();
|
||||
|
||||
const menuOptions = [
|
||||
{
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id="label.logged-in-as"
|
||||
defaultMessage="Logged in as {username}"
|
||||
values={{ username: <b>{user.username}</b> }}
|
||||
/>
|
||||
),
|
||||
value: 'username',
|
||||
className: styles.username,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="label.profile" defaultMessage="Profile" />,
|
||||
value: 'profile',
|
||||
hidden: adminDisabled,
|
||||
divider: true,
|
||||
},
|
||||
{ label: <FormattedMessage id="label.logout" defaultMessage="Logout" />, value: 'logout' },
|
||||
];
|
||||
|
||||
function handleClick() {
|
||||
setShow(state => !state);
|
||||
}
|
||||
|
||||
function handleSelect(value) {
|
||||
if (value === 'logout') {
|
||||
removeItem(AUTH_TOKEN);
|
||||
router.push('/login');
|
||||
} else if (value === 'profile') {
|
||||
router.push('/profile');
|
||||
}
|
||||
}
|
||||
|
||||
useDocumentClick(e => {
|
||||
if (!ref.current?.contains(e.target)) {
|
||||
setShow(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.button} ref={ref}>
|
||||
<Button variant="light" onClick={handleClick}>
|
||||
<Icon className={styles.icon} size="large">
|
||||
<Profile />
|
||||
</Icon>
|
||||
</Button>
|
||||
{show && (
|
||||
<Popup className={styles.menu} position="bottom" gap={5}>
|
||||
<Menu items={menuOptions} onSelect={handleSelect}>
|
||||
{({ label, value }) => (
|
||||
<Item key={value}>
|
||||
<Text>{label}</Text>
|
||||
</Item>
|
||||
)}
|
||||
</Menu>
|
||||
</Popup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
components/input/UserButton.module.css
Normal file
25
components/input/UserButton.module.css
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
.button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.username {
|
||||
border-bottom: 1px solid var(--base500);
|
||||
}
|
||||
|
||||
.username:hover {
|
||||
background: var(--base50);
|
||||
}
|
||||
|
||||
.icon svg {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
left: -50%;
|
||||
background: var(--base50);
|
||||
border: 1px solid var(--base500);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
z-index: 100;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue