mirror of
https://github.com/umami-software/umami.git
synced 2026-02-05 13:17:19 +01:00
Moved code into src folder. Added build for component library.
This commit is contained in:
parent
7a7233ead4
commit
ede658771e
490 changed files with 749 additions and 442 deletions
145
src/components/input/DateFilter.js
Normal file
145
src/components/input/DateFilter.js
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import { useState } from 'react';
|
||||
import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics';
|
||||
import { endOfYear, isSameDay } from 'date-fns';
|
||||
import DatePickerForm from 'components/metrics/DatePickerForm';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import { formatDate } from 'lib/date';
|
||||
import Icons from 'components/icons';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
|
||||
export function DateFilter({
|
||||
value,
|
||||
startDate,
|
||||
endDate,
|
||||
className,
|
||||
onChange,
|
||||
showAllTime = false,
|
||||
alignment = 'end',
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
|
||||
const options = [
|
||||
{ label: formatMessage(labels.today), value: '1day' },
|
||||
{
|
||||
label: formatMessage(labels.lastHours, { x: 24 }),
|
||||
value: '24hour',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.yesterday),
|
||||
value: '-1day',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.thisWeek),
|
||||
value: '1week',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.lastDays, { x: 7 }),
|
||||
value: '7day',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.thisMonth),
|
||||
value: '1month',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.lastDays, { x: 30 }),
|
||||
value: '30day',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.lastDays, { x: 90 }),
|
||||
value: '90day',
|
||||
},
|
||||
{ label: formatMessage(labels.thisYear), value: '1year' },
|
||||
showAllTime && {
|
||||
label: formatMessage(labels.allTime),
|
||||
value: 'all',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.customRange),
|
||||
value: 'custom',
|
||||
divider: true,
|
||||
},
|
||||
].filter(n => n);
|
||||
|
||||
const renderValue = value => {
|
||||
return value.startsWith('range') ? (
|
||||
<CustomRange startDate={startDate} endDate={endDate} onClick={() => handleChange('custom')} />
|
||||
) : (
|
||||
options.find(e => e.value === value).label
|
||||
);
|
||||
};
|
||||
|
||||
const handleChange = value => {
|
||||
if (value === 'custom') {
|
||||
setShowPicker(true);
|
||||
return;
|
||||
}
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
const handlePickerChange = value => {
|
||||
setShowPicker(false);
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
const handleClose = () => setShowPicker(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
className={className}
|
||||
items={options}
|
||||
renderValue={renderValue}
|
||||
value={value}
|
||||
alignment={alignment}
|
||||
placeholder={formatMessage(labels.selectDate)}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{({ label, value, divider }) => (
|
||||
<Item key={value} divider={divider}>
|
||||
{label}
|
||||
</Item>
|
||||
)}
|
||||
</Dropdown>
|
||||
{showPicker && (
|
||||
<Modal onClose={handleClose}>
|
||||
<DatePickerForm
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
minDate={new Date(2000, 0, 1)}
|
||||
maxDate={endOfYear(new Date())}
|
||||
onChange={handlePickerChange}
|
||||
onClose={() => setShowPicker(false)}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const CustomRange = ({ startDate, endDate, onClick }) => {
|
||||
const { locale } = useLocale();
|
||||
|
||||
function handleClick(e) {
|
||||
e.stopPropagation();
|
||||
|
||||
onClick();
|
||||
}
|
||||
|
||||
return (
|
||||
<Flexbox gap={10} alignItems="center" wrap="nowrap">
|
||||
<Icon className="mr-2" onClick={handleClick}>
|
||||
<Icons.Calendar />
|
||||
</Icon>
|
||||
<Text>
|
||||
{formatDate(startDate, 'd LLL y', locale)}
|
||||
{!isSameDay(startDate, endDate) && ` — ${formatDate(endDate, 'd LLL y', locale)}`}
|
||||
</Text>
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateFilter;
|
||||
53
src/components/input/LanguageButton.js
Normal file
53
src/components/input/LanguageButton.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { Icon, Button, PopupTrigger, Popup, Text } from 'react-basics';
|
||||
import classNames from 'classnames';
|
||||
import { languages } from 'lib/lang';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import Icons from 'components/icons';
|
||||
import styles from './LanguageButton.module.css';
|
||||
|
||||
export function LanguageButton() {
|
||||
const { locale, saveLocale, dir } = useLocale();
|
||||
const items = Object.keys(languages).map(key => ({ ...languages[key], value: key }));
|
||||
|
||||
function handleSelect(value, close, e) {
|
||||
e.stopPropagation();
|
||||
saveLocale(value);
|
||||
close();
|
||||
}
|
||||
|
||||
return (
|
||||
<PopupTrigger>
|
||||
<Button variant="quiet">
|
||||
<Icon>
|
||||
<Icons.Globe />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Popup position="bottom" alignment={dir === 'rtl' ? 'start' : 'end'}>
|
||||
{close => {
|
||||
return (
|
||||
<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, close)}
|
||||
>
|
||||
<Text>{label}</Text>
|
||||
{value === locale && (
|
||||
<Icon className={styles.icon}>
|
||||
<Icons.Check />
|
||||
</Icon>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export default LanguageButton;
|
||||
34
src/components/input/LanguageButton.module.css
Normal file
34
src/components/input/LanguageButton.module.css
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
.menu {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
min-width: 640px;
|
||||
padding: 10px;
|
||||
background: var(--base50);
|
||||
z-index: var(--z-index-popup);
|
||||
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);
|
||||
}
|
||||
20
src/components/input/LogoutButton.js
Normal file
20
src/components/input/LogoutButton.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { Button, Icon, Icons, TooltipPopup } from 'react-basics';
|
||||
import Link from 'next/link';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
|
||||
export function LogoutButton({ tooltipPosition = 'top' }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
return (
|
||||
<Link href="/src/pages/logout">
|
||||
<TooltipPopup label={formatMessage(labels.logout)} position={tooltipPosition}>
|
||||
<Button variant="quiet">
|
||||
<Icon>
|
||||
<Icons.Logout />
|
||||
</Icon>
|
||||
</Button>
|
||||
</TooltipPopup>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogoutButton;
|
||||
71
src/components/input/MonthSelect.js
Normal file
71
src/components/input/MonthSelect.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { useRef } from 'react';
|
||||
import {
|
||||
Text,
|
||||
Icon,
|
||||
CalendarMonthSelect,
|
||||
CalendarYearSelect,
|
||||
Button,
|
||||
PopupTrigger,
|
||||
Popup,
|
||||
} from 'react-basics';
|
||||
import { startOfMonth, endOfMonth } from 'date-fns';
|
||||
import Icons from 'components/icons';
|
||||
import { useLocale } from 'components/hooks';
|
||||
import { formatDate } from 'lib/date';
|
||||
import { getDateLocale } from 'lib/lang';
|
||||
import styles from './MonthSelect.module.css';
|
||||
|
||||
export function MonthSelect({ date = new Date(), onChange }) {
|
||||
const { locale } = useLocale();
|
||||
const month = formatDate(date, 'MMMM', locale);
|
||||
const year = date.getFullYear();
|
||||
const ref = useRef();
|
||||
|
||||
const handleChange = (close, date) => {
|
||||
onChange(`range:${startOfMonth(date).getTime()}:${endOfMonth(date).getTime()}`);
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={ref} className={styles.container}>
|
||||
<PopupTrigger>
|
||||
<Button className={styles.input} variant="quiet">
|
||||
<Text>{month}</Text>
|
||||
<Icon size="sm">
|
||||
<Icons.ChevronDown />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Popup className={styles.popup} alignment="start">
|
||||
{close => (
|
||||
<CalendarMonthSelect
|
||||
date={date}
|
||||
locale={getDateLocale(locale)}
|
||||
onSelect={handleChange.bind(null, close)}
|
||||
/>
|
||||
)}
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
<PopupTrigger>
|
||||
<Button className={styles.input} variant="quiet">
|
||||
<Text>{year}</Text>
|
||||
<Icon size="sm">
|
||||
<Icons.ChevronDown />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Popup className={styles.popup} alignment="start">
|
||||
{close => (
|
||||
<CalendarYearSelect
|
||||
date={date}
|
||||
locale={getDateLocale(locale)}
|
||||
onSelect={handleChange.bind(null, close)}
|
||||
/>
|
||||
)}
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MonthSelect;
|
||||
22
src/components/input/MonthSelect.module.css
Normal file
22
src/components/input/MonthSelect.module.css
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--base400);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.popup {
|
||||
border: 1px solid var(--base400);
|
||||
background: var(--base50);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 20px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
61
src/components/input/ProfileButton.js
Normal file
61
src/components/input/ProfileButton.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { Icon, Button, PopupTrigger, Popup, Menu, Item, Text } from 'react-basics';
|
||||
import { useRouter } from 'next/router';
|
||||
import Icons from 'components/icons';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import useConfig from 'components/hooks/useConfig';
|
||||
import styles from './ProfileButton.module.css';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
|
||||
export function ProfileButton() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
const { cloudMode } = useConfig();
|
||||
const router = useRouter();
|
||||
const { dir } = useLocale();
|
||||
|
||||
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={dir === 'rtl' ? 'start' : '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>
|
||||
{!cloudMode && (
|
||||
<Item key="logout" className={styles.item}>
|
||||
<Icon>
|
||||
<Icons.Logout />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.logout)}</Text>
|
||||
</Item>
|
||||
)}
|
||||
</Menu>
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProfileButton;
|
||||
10
src/components/input/ProfileButton.module.css
Normal file
10
src/components/input/ProfileButton.module.css
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
.menu {
|
||||
width: 200px;
|
||||
z-index: var(--z-index-popup);
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
background: var(--base50);
|
||||
}
|
||||
28
src/components/input/RefreshButton.js
Normal file
28
src/components/input/RefreshButton.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { LoadingButton, Icon, TooltipPopup } from 'react-basics';
|
||||
import { setWebsiteDateRange } from 'store/websites';
|
||||
import useDateRange from 'components/hooks/useDateRange';
|
||||
import Icons from 'components/icons';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
|
||||
export function RefreshButton({ websiteId, isLoading }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
|
||||
function handleClick() {
|
||||
if (!isLoading && dateRange) {
|
||||
setWebsiteDateRange(websiteId, dateRange);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipPopup label={formatMessage(labels.refresh)}>
|
||||
<LoadingButton loading={isLoading} onClick={handleClick}>
|
||||
<Icon>
|
||||
<Icons.Refresh />
|
||||
</Icon>
|
||||
</LoadingButton>
|
||||
</TooltipPopup>
|
||||
);
|
||||
}
|
||||
|
||||
export default RefreshButton;
|
||||
37
src/components/input/SettingsButton.js
Normal file
37
src/components/input/SettingsButton.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { Button, Icon, PopupTrigger, Popup, Form, FormRow } from 'react-basics';
|
||||
import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting';
|
||||
import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting';
|
||||
import Icons from 'components/icons';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import styles from './SettingsButton.module.css';
|
||||
|
||||
export function SettingsButton() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<PopupTrigger>
|
||||
<Button variant="quiet">
|
||||
<Icon>
|
||||
<Icons.Gear />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Popup
|
||||
className={styles.popup}
|
||||
position="bottom"
|
||||
alignment="end"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<Form>
|
||||
<FormRow label={formatMessage(labels.timezone)}>
|
||||
<TimezoneSetting />
|
||||
</FormRow>
|
||||
<FormRow label={formatMessage(labels.defaultDateRange)}>
|
||||
<DateRangeSetting />
|
||||
</FormRow>
|
||||
</Form>
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsButton;
|
||||
11
src/components/input/SettingsButton.module.css
Normal file
11
src/components/input/SettingsButton.module.css
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
.popup {
|
||||
background: var(--base50);
|
||||
border: 1px solid var(--base500);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
38
src/components/input/ThemeButton.js
Normal file
38
src/components/input/ThemeButton.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { useTransition, animated } from 'react-spring';
|
||||
import { Button, Icon } from 'react-basics';
|
||||
import useTheme from 'components/hooks/useTheme';
|
||||
import Icons from 'components/icons';
|
||||
import styles from './ThemeButton.module.css';
|
||||
|
||||
export function ThemeButton() {
|
||||
const { theme, saveTheme } = useTheme();
|
||||
|
||||
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() {
|
||||
saveTheme(theme === 'light' ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export default ThemeButton;
|
||||
14
src/components/input/ThemeButton.module.css
Normal file
14
src/components/input/ThemeButton.module.css
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
.button {
|
||||
width: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button > div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
}
|
||||
25
src/components/input/WebsiteDateFilter.js
Normal file
25
src/components/input/WebsiteDateFilter.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import useDateRange from 'components/hooks/useDateRange';
|
||||
import DateFilter from './DateFilter';
|
||||
import styles from './WebsiteDateFilter.module.css';
|
||||
|
||||
export function WebsiteDateFilter({ websiteId }) {
|
||||
const [dateRange, setDateRange] = useDateRange(websiteId);
|
||||
const { value, startDate, endDate } = dateRange;
|
||||
|
||||
const handleChange = async value => {
|
||||
setDateRange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<DateFilter
|
||||
className={styles.dropdown}
|
||||
value={value}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onChange={handleChange}
|
||||
showAllTime={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebsiteDateFilter;
|
||||
3
src/components/input/WebsiteDateFilter.module.css
Normal file
3
src/components/input/WebsiteDateFilter.module.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.dropdown {
|
||||
min-width: 200px;
|
||||
}
|
||||
28
src/components/input/WebsiteSelect.js
Normal file
28
src/components/input/WebsiteSelect.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { Dropdown, Item } from 'react-basics';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
|
||||
export function WebsiteSelect({ websiteId, onSelect }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { get, useQuery } = useApi();
|
||||
const { data } = useQuery(['websites:me'], () => get('/me/websites'));
|
||||
|
||||
const renderValue = value => {
|
||||
return data?.data?.find(({ id }) => id === value)?.name;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
items={data?.data}
|
||||
value={websiteId}
|
||||
renderValue={renderValue}
|
||||
onChange={onSelect}
|
||||
alignment="end"
|
||||
placeholder={formatMessage(labels.selectWebsite)}
|
||||
>
|
||||
{({ id, name }) => <Item key={id}>{name}</Item>}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebsiteSelect;
|
||||
Loading…
Add table
Add a link
Reference in a new issue