Merge pull request #111 from mikecao/dev

v0.24.0
This commit is contained in:
Mike Cao 2020-09-07 20:46:21 -07:00 committed by GitHub
commit 8df3c21ada
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1566 additions and 248 deletions

1
.gitignore vendored
View File

@ -16,6 +16,7 @@
# production
/build
/public/umami.js
/lang-compiled
# misc
.DS_Store

1
assets/globe.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm179.3 160h-67.2c-6.7-36.5-17.5-68.8-31.2-94.7 42.9 19 77.7 52.7 98.4 94.7zM248 56c18.6 0 48.6 41.2 63.2 112H184.8C199.4 97.2 229.4 56 248 56zM48 256c0-13.7 1.4-27.1 4-40h77.7c-1 13.1-1.7 26.3-1.7 40s.7 26.9 1.7 40H52c-2.6-12.9-4-26.3-4-40zm20.7 88h67.2c6.7 36.5 17.5 68.8 31.2 94.7-42.9-19-77.7-52.7-98.4-94.7zm67.2-176H68.7c20.7-42 55.5-75.7 98.4-94.7-13.7 25.9-24.5 58.2-31.2 94.7zM248 456c-18.6 0-48.6-41.2-63.2-112h126.5c-14.7 70.8-44.7 112-63.3 112zm70.1-160H177.9c-1.1-12.8-1.9-26-1.9-40s.8-27.2 1.9-40h140.3c1.1 12.8 1.9 26 1.9 40s-.9 27.2-2 40zm10.8 142.7c13.7-25.9 24.4-58.2 31.2-94.7h67.2c-20.7 42-55.5 75.7-98.4 94.7zM366.3 296c1-13.1 1.7-26.3 1.7-40s-.7-26.9-1.7-40H444c2.6 12.9 4 26.3 4 40s-1.4 27.1-4 40h-77.7z"/></svg>

After

Width:  |  Height:  |  Size: 874 B

View File

@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import WebsiteChart from 'components/metrics/WebsiteChart';
import WorldMap from 'components/common/WorldMap';
@ -32,7 +33,9 @@ export default function WebsiteDetails({ websiteId }) {
size="xsmall"
onClick={() => setExpand(null)}
>
<div>Back</div>
<div>
<FormattedMessage id="button.back" defaultMessage="Back" />
</div>
</Button>
);
@ -40,13 +43,41 @@ export default function WebsiteDetails({ websiteId }) {
{
render: BackButton,
},
{ label: 'Pages', value: 'url', component: PagesTable },
{ label: 'Referrers', value: 'referrer', component: ReferrersTable },
{ label: 'Browsers', value: 'browser', component: BrowsersTable },
{ label: 'Operating system', value: 'os', component: OSTable },
{ label: 'Devices', value: 'device', component: DevicesTable },
{ label: 'Countries', value: 'country', component: CountriesTable },
{ label: 'Events', value: 'event', component: EventsTable },
{
label: <FormattedMessage id="metrics.pages" defaultMessage="Pages" />,
value: 'url',
component: PagesTable,
},
{
label: <FormattedMessage id="metrics.browsers" defaultMessage="Browsers" />,
value: 'referrer',
component: ReferrersTable,
},
{
label: <FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />,
value: 'browser',
component: BrowsersTable,
},
{
label: <FormattedMessage id="metrics.operating-system" defaultMessage="Operating system" />,
value: 'os',
component: OSTable,
},
{
label: <FormattedMessage id="metrics.devices" defaultMessage="Devices" />,
value: 'device',
component: DevicesTable,
},
{
label: <FormattedMessage id="metrics.countries" defaultMessage="Countries" />,
value: 'country',
component: CountriesTable,
},
{
label: <FormattedMessage id="metrics.events" defaultMessage="Events" />,
value: 'event',
component: EventsTable,
},
];
const tableProps = {

View File

@ -1,4 +1,5 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { useRouter } from 'next/router';
import WebsiteChart from 'components/metrics/WebsiteChart';
import Page from 'components/layout/Page';
@ -24,9 +25,21 @@ export default function WebsiteList() {
</div>
))}
{data.length === 0 && (
<EmptyPlaceholder msg={"You don't have any websites configured."}>
<EmptyPlaceholder
msg={
<FormattedMessage
id="placeholder.message.no-websites-configured"
defaultMessage="You don't have any websites configured."
/>
}
>
<Button icon={<Arrow />} size="medium" onClick={() => router.push('/settings')}>
<div>Go to settings</div>
<div>
<FormattedMessage
id="placeholder.message.go-to-settings"
defaultMessage="Go to settings"
/>
</div>
</Button>
</EmptyPlaceholder>
)}

View File

@ -13,17 +13,20 @@ export default function ButtonGroup({
}) {
return (
<div className={classNames(styles.group, className)}>
{items.map(item => (
<Button
key={item}
className={classNames(styles.button, { [styles.selected]: selectedItem === item })}
size={size}
icon={icon}
onClick={() => onClick(item)}
>
{item}
</Button>
))}
{items.map(item => {
const { label, value } = item;
return (
<Button
key={value}
className={classNames(styles.button, { [styles.selected]: selectedItem === value })}
size={size}
icon={icon}
onClick={() => onClick(value)}
>
{label}
</Button>
);
})}
</div>
);
}

View File

@ -1,7 +1,10 @@
import React, { useState } from 'react';
import Button from './Button';
import { FormattedMessage } from 'react-intl';
const defaultText = 'Copy to clipboard';
const defaultText = (
<FormattedMessage id="button.copy-to-clipboard" defaultMessage="Copy to clipboard" />
);
export default function CopyButton({ element, ...props }) {
const [text, setText] = useState(defaultText);
@ -10,7 +13,7 @@ export default function CopyButton({ element, ...props }) {
if (element?.current) {
element.current.select();
document.execCommand('copy');
setText('Copied!');
setText(<FormattedMessage id="message.copied" defaultMessage="Copied!" />);
window.getSelection().removeAllRanges();
}
}

View File

@ -1,16 +1,40 @@
import React from 'react';
import { getDateRange } from 'lib/date';
import DropDown from './DropDown';
import { FormattedMessage } from 'react-intl';
const filterOptions = [
{ label: 'Last 24 hours', value: '24hour' },
{ label: 'Last 7 days', value: '7day' },
{ label: 'Last 30 days', value: '30day' },
{ label: 'Last 90 days', value: '90day' },
{ label: 'Today', value: '1day' },
{ label: 'This week', value: '1week' },
{ label: 'This month', value: '1month' },
{ label: 'This year', value: '1year' },
{
label: (
<FormattedMessage id="label.last-hours" defaultMessage="Last {x} hours" values={{ x: 24 }} />
),
value: '24hour',
},
{
label: (
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 7 }} />
),
value: '7day',
},
{
label: (
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 30 }} />
),
value: '30day',
},
{
label: (
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 90 }} />
),
value: '90day',
},
{ label: <FormattedMessage id="label.today" defaultMessage="Today" />, value: '1day' },
{ label: <FormattedMessage id="label.this-week" defaultMessage="This week" />, value: '1week' },
{
label: <FormattedMessage id="label.this-month" defaultMessage="This month" />,
value: '1month',
},
{ label: <FormattedMessage id="label.this-year" defaultMessage="This year" />, value: '1year' },
];
export default function DateFilter({ value, onChange, className }) {

View File

@ -0,0 +1,45 @@
import React, { useState, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Globe from 'assets/globe.svg';
import useDocumentClick from 'hooks/useDocumentClick';
import { updateApp } from 'redux/actions/app';
import Menu from './Menu';
import Button from './Button';
import { menuOptions } from 'lib/lang';
import styles from './LanguageButton.module.css';
export default function LanguageButton({ menuPosition = 'bottom', menuAlign = 'left' }) {
const dispatch = useDispatch();
const [showMenu, setShowMenu] = useState(false);
const locale = useSelector(state => state.app.locale);
const ref = useRef();
const selectedLocale = menuOptions.find(e => e.value === locale)?.display;
function handleSelect(value) {
dispatch(updateApp({ locale: value }));
window.localStorage.setItem('locale', value);
setShowMenu(false);
}
useDocumentClick(e => {
if (!ref.current.contains(e.target)) {
setShowMenu(false);
}
});
return (
<div ref={ref} className={styles.container}>
<Button icon={<Globe />} onClick={() => setShowMenu(true)} size="small">
<div className={locale}>{selectedLocale}</div>
</Button>
{showMenu && (
<Menu
options={menuOptions}
onSelect={handleSelect}
float={menuPosition}
align={menuAlign}
/>
)}
</div>
);
}

View File

@ -0,0 +1,5 @@
.container {
display: flex;
position: relative;
cursor: pointer;
}

View File

@ -26,7 +26,7 @@
left: 50%;
transform: translate(-50%, -50%);
background: var(--gray50);
min-width: 200px;
min-width: 400px;
min-height: 100px;
z-index: 1;
border: 1px solid var(--gray300);

View File

@ -1,4 +1,5 @@
import React, { useState, useRef } from 'react';
import { FormattedMessage } from 'react-intl';
import { useSelector } from 'react-redux';
import { useRouter } from 'next/router';
import Menu from './Menu';
@ -17,14 +18,16 @@ export default function UserButton() {
const menuOptions = [
{
label: (
<>
Logged in as <b>{user.username}</b>
</>
<FormattedMessage
id="label.logged-in-as"
defaultMessage="Logged in as {username}"
values={{ username: <b>{user.username}</b> }}
/>
),
value: 'username',
className: styles.username,
},
{ label: 'Logout', value: 'logout' },
{ label: <FormattedMessage id="label.logout" defaultMessage="Logout" />, value: 'logout' },
];
function handleSelect(value) {

View File

@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { Formik, Form, Field } from 'formik';
import { post } from 'lib/web';
import Button from 'components/common/Button';
@ -18,10 +19,10 @@ const validate = ({ user_id, username, password }) => {
const errors = {};
if (!username) {
errors.username = 'Required';
errors.username = <FormattedMessage id="label.required" defaultMessage="Required" />;
}
if (!user_id && !password) {
errors.password = 'Required';
errors.password = <FormattedMessage id="label.required" defaultMessage="Required" />;
}
return errors;
@ -36,7 +37,11 @@ export default function AccountEditForm({ values, onSave, onClose }) {
if (typeof response !== 'string') {
onSave();
} else {
setMessage(response || 'Something went wrong');
setMessage(
response || (
<FormattedMessage id="message.failure" defaultMessage="Something went wrong." />
),
);
}
};
@ -50,20 +55,26 @@ export default function AccountEditForm({ values, onSave, onClose }) {
{() => (
<Form>
<FormRow>
<label htmlFor="username">Username</label>
<label htmlFor="username">
<FormattedMessage id="label.username" defaultMessage="Username" />
</label>
<Field name="username" type="text" />
<FormError name="username" />
</FormRow>
<FormRow>
<label htmlFor="password">Password</label>
<label htmlFor="password">
<FormattedMessage id="label.password" defaultMessage="Password" />
</label>
<Field name="password" type="password" />
<FormError name="password" />
</FormRow>
<FormButtons>
<Button type="submit" variant="action">
Save
<FormattedMessage id="button.save" defaultMessage="Save" />
</Button>
<Button onClick={onClose}>
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
</Button>
<Button onClick={onClose}>Cancel</Button>
</FormButtons>
<FormMessage>{message}</FormMessage>
</Form>

View File

@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { Formik, Form, Field } from 'formik';
import { post } from 'lib/web';
import Button from 'components/common/Button';
@ -19,15 +20,17 @@ const validate = ({ current_password, new_password, confirm_password }) => {
const errors = {};
if (!current_password) {
errors.current_password = 'Required';
errors.current_password = <FormattedMessage id="label.required" defaultMessage="Required" />;
}
if (!new_password) {
errors.new_password = 'Required';
errors.new_password = <FormattedMessage id="label.required" defaultMessage="Required" />;
}
if (!confirm_password) {
errors.confirm_password = 'Required';
errors.confirm_password = <FormattedMessage id="label.required" defaultMessage="Required" />;
} else if (new_password !== confirm_password) {
errors.confirm_password = `Passwords don't match`;
errors.confirm_password = (
<FormattedMessage id="label.passwords-dont-match" defaultMessage="Passwords don't match" />
);
}
return errors;
@ -42,7 +45,11 @@ export default function ChangePasswordForm({ values, onSave, onClose }) {
if (typeof response !== 'string') {
onSave();
} else {
setMessage(response || 'Something went wrong');
setMessage(
response || (
<FormattedMessage id="message.failure" defaultMessage="Something went wrong." />
),
);
}
};
@ -56,25 +63,33 @@ export default function ChangePasswordForm({ values, onSave, onClose }) {
{() => (
<Form>
<FormRow>
<label htmlFor="current_password">Current password</label>
<label htmlFor="current_password">
<FormattedMessage id="label.current-password" defaultMessage="Current password" />
</label>
<Field name="current_password" type="password" />
<FormError name="current_password" />
</FormRow>
<FormRow>
<label htmlFor="new_password">New password</label>
<label htmlFor="new_password">
<FormattedMessage id="label.new-password" defaultMessage="New password" />
</label>
<Field name="new_password" type="password" />
<FormError name="new_password" />
</FormRow>
<FormRow>
<label htmlFor="confirm_password">Confirm password</label>
<label htmlFor="confirm_password">
<FormattedMessage id="label.confirm-password" defaultMessage="Confirm password" />
</label>
<Field name="confirm_password" type="password" />
<FormError name="confirm_password" />
</FormRow>
<FormButtons>
<Button type="submit" variant="action">
Save
<FormattedMessage id="button.save" defaultMessage="Save" />
</Button>
<Button onClick={onClose}>
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
</Button>
<Button onClick={onClose}>Cancel</Button>
</FormButtons>
<FormMessage>{message}</FormMessage>
</Form>

View File

@ -8,12 +8,17 @@ import FormLayout, {
FormMessage,
FormRow,
} from 'components/layout/FormLayout';
import { FormattedMessage } from 'react-intl';
const validate = ({ confirmation }) => {
const errors = {};
if (confirmation !== 'DELETE') {
errors.confirmation = !confirmation ? 'Required' : 'Invalid';
errors.confirmation = !confirmation ? (
<FormattedMessage id="label.required" defaultMessage="Required" />
) : (
<FormattedMessage id="label.invalid" defaultMessage="Invalid" />
);
}
return errors;
@ -28,7 +33,7 @@ export default function DeleteForm({ values, onSave, onClose }) {
if (typeof response !== 'string') {
onSave();
} else {
setMessage('Something went wrong');
setMessage(<FormattedMessage id="message.failure" defaultMessage="Something went wrong." />);
}
};
@ -42,11 +47,24 @@ export default function DeleteForm({ values, onSave, onClose }) {
{() => (
<Form>
<div>
Are your sure you want to delete <b>{values.name}</b>?
<FormattedMessage
id="message.confirm-delete"
defaultMessage="Are your sure you want to delete {target}?"
values={{ target: <b>{values.name}</b> }}
/>
</div>
<div>
<FormattedMessage
id="message.delete-warning"
defaultMessage="All associated data will be deleted as well."
/>
</div>
<div>All associated data will be deleted as well.</div>
<p>
Type <b>DELETE</b> in the box below to confirm.
<FormattedMessage
id="message.type-delete"
defaultMessage="Type {delete} in the box below to confirm."
values={{ delete: <b>DELETE</b> }}
/>
</p>
<FormRow>
<Field name="confirmation" type="text" />
@ -54,9 +72,11 @@ export default function DeleteForm({ values, onSave, onClose }) {
</FormRow>
<FormButtons>
<Button type="submit" variant="danger">
Delete
<FormattedMessage id="button.delete" defaultMessage="Delete" />
</Button>
<Button onClick={onClose}>
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
</Button>
<Button onClick={onClose}>Cancel</Button>
</FormButtons>
<FormMessage>{message}</FormMessage>
</Form>

View File

@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { Formik, Form, Field } from 'formik';
import Router from 'next/router';
import { post } from 'lib/web';
@ -17,10 +18,10 @@ const validate = ({ username, password }) => {
const errors = {};
if (!username) {
errors.username = 'Required';
errors.username = <FormattedMessage id="label.required" defaultMessage="Required" />;
}
if (!password) {
errors.password = 'Required';
errors.password = <FormattedMessage id="label.required" defaultMessage="Required" />;
}
return errors;
@ -35,7 +36,16 @@ export default function LoginForm() {
if (typeof response !== 'string') {
await Router.push('/');
} else {
setMessage(response.startsWith('401') ? 'Incorrect username/password' : response);
setMessage(
response.startsWith('401') ? (
<FormattedMessage
id="message.incorrect-username-password"
defaultMessage="Incorrect username/password."
/>
) : (
response
),
);
}
};
@ -54,18 +64,22 @@ export default function LoginForm() {
<Icon icon={<Logo />} size="xlarge" className={styles.icon} />
<h1 className="center">umami</h1>
<FormRow>
<label htmlFor="username">Username</label>
<label htmlFor="username">
<FormattedMessage id="label.username" defaultMessage="Username" />
</label>
<Field name="username" type="text" />
<FormError name="username" />
</FormRow>
<FormRow>
<label htmlFor="password">Password</label>
<label htmlFor="password">
<FormattedMessage id="label.password" defaultMessage="Password" />
</label>
<Field name="password" type="password" />
<FormError name="password" />
</FormRow>
<FormButtons>
<Button type="submit" variant="action">
Login
<FormattedMessage id="button.login" defaultMessage="Login" />
</Button>
</FormButtons>
<FormMessage>{message}</FormMessage>

View File

@ -1,7 +1,8 @@
import React, { useRef } from 'react';
import { FormattedMessage } from 'react-intl';
import Button from 'components/common/Button';
import FormLayout, { FormButtons, FormRow } from 'components/layout/FormLayout';
import CopyButton from '../common/CopyButton';
import CopyButton from 'components/common/CopyButton';
export default function TrackingCodeForm({ values, onClose }) {
const ref = useRef();
@ -10,7 +11,11 @@ export default function TrackingCodeForm({ values, onClose }) {
return (
<FormLayout>
<p>
This is the publicly shared URL for <b>{values.name}</b>.
<FormattedMessage
id="message.share-url"
defaultMessage="This is the publicly shared URL for {target}."
values={{ target: <b>{values.name}</b> }}
/>
</p>
<FormRow>
<textarea
@ -24,7 +29,9 @@ export default function TrackingCodeForm({ values, onClose }) {
</FormRow>
<FormButtons>
<CopyButton type="submit" variant="action" element={ref} />
<Button onClick={onClose}>Cancel</Button>
<Button onClick={onClose}>
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
</Button>
</FormButtons>
</FormLayout>
);

View File

@ -1,4 +1,5 @@
import React, { useRef } from 'react';
import { FormattedMessage } from 'react-intl';
import Button from 'components/common/Button';
import FormLayout, { FormButtons, FormRow } from 'components/layout/FormLayout';
import CopyButton from '../common/CopyButton';
@ -9,8 +10,11 @@ export default function TrackingCodeForm({ values, onClose }) {
return (
<FormLayout>
<p>
To track stats for <b>{values.name}</b>, place the following code in the &lt;head&gt;
section of your website.
<FormattedMessage
id="message.track-stats"
defaultMessage="To track stats for {target}, place the following code in the {head} section of your website."
values={{ head: '<head>', target: <b>{values.name}</b> }}
/>
</p>
<FormRow>
<textarea
@ -24,7 +28,9 @@ export default function TrackingCodeForm({ values, onClose }) {
</FormRow>
<FormButtons>
<CopyButton type="submit" variant="action" element={ref} />
<Button onClick={onClose}>Cancel</Button>
<Button onClick={onClose}>
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
</Button>
</FormButtons>
</FormLayout>
);

View File

@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { Formik, Form, Field } from 'formik';
import { post } from 'lib/web';
import Button from 'components/common/Button';
@ -21,12 +22,12 @@ const validate = ({ name, domain }) => {
const errors = {};
if (!name) {
errors.name = 'Required';
errors.name = <FormattedMessage id="label.required" defaultMessage="Required" />;
}
if (!domain) {
errors.domain = 'Required';
errors.domain = <FormattedMessage id="label.required" defaultMessage="Required" />;
} else if (!DOMAIN_REGEX.test(domain)) {
errors.domain = 'Invalid domain';
errors.domain = <FormattedMessage id="label.invalid-domain" defaultMessage="Invalid domain" />;
}
return errors;
@ -41,7 +42,7 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
if (typeof response !== 'string') {
onSave();
} else {
setMessage('Something went wrong');
setMessage(<FormattedMessage id="message.failure" defaultMessage="Something went wrong." />);
}
};
@ -55,26 +56,42 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
{() => (
<Form>
<FormRow>
<label htmlFor="name">Name</label>
<label htmlFor="name">
<FormattedMessage id="label.name" defaultMessage="Name" />
</label>
<Field name="name" type="text" />
<FormError name="name" />
</FormRow>
<FormRow>
<label htmlFor="domain">Domain</label>
<label htmlFor="domain">
<FormattedMessage id="label.domain" defaultMessage="Domain" />
</label>
<Field name="domain" type="text" />
<FormError name="domain" />
</FormRow>
<FormRow>
<label></label>
<Field name="enable_share_url">
{({ field }) => <Checkbox {...field} label="Enable share URL" />}
{({ field }) => (
<Checkbox
{...field}
label={
<FormattedMessage
id="label.enable-share-url"
defaultMessage="Enable share URL"
/>
}
/>
)}
</Field>
</FormRow>
<FormButtons>
<Button type="submit" variant="action">
Save
<FormattedMessage id="button.save" defaultMessage="Save" />
</Button>
<Button onClick={onClose}>
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
</Button>
<Button onClick={onClose}>Cancel</Button>
</FormButtons>
<FormMessage>{message}</FormMessage>
</Form>

View File

@ -1,4 +1,5 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import Link from 'next/link';
import classNames from 'classnames';
import Button from 'components/common/Button';
@ -9,10 +10,10 @@ export default function Footer() {
return (
<footer className="container">
<div className={classNames(styles.footer, 'row justify-content-center')}>
<div>powered by</div>
<FormattedMessage id="footer.powered-by" defaultMessage="powered by" />
<Link href="https://umami.is">
<a>
<Button icon={<Logo />} size="small">
<Button className={styles.button} icon={<Logo />} size="small">
<b>umami</b>
</Button>
</a>

View File

@ -1,14 +1,15 @@
.footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: var(--font-size-small);
min-height: 100px;
}
.footer button {
margin-left: 10px;
}
.footer a {
text-decoration: none;
}
.button {
margin-left: 10px;
}

View File

@ -1,4 +1,5 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { useSelector } from 'react-redux';
import classNames from 'classnames';
import Link from 'components/common/Link';
@ -6,6 +7,7 @@ import UserButton from '../common/UserButton';
import Icon from '../common/Icon';
import Logo from 'assets/logo.svg';
import styles from './Header.module.css';
import LanguageButton from '../common/LanguageButton';
export default function Header() {
const user = useSelector(state => state.user);
@ -19,15 +21,24 @@ export default function Header() {
<Link href={user ? '/' : 'https://umami.is'}>umami</Link>
</div>
</div>
{user && (
<div className="col-12 col-md-6">
<div className={styles.nav}>
<Link href="/dashboard">Dashboard</Link>
<Link href="/settings">Settings</Link>
<UserButton />
</div>
<div className="col-12 col-md-6">
<div className={styles.nav}>
{user ? (
<>
<Link href="/dashboard">
<FormattedMessage id="header.nav.dashboard" defaultMessage="Dashboard" />
</Link>
<Link href="/settings">
<FormattedMessage id="header.nav.settings" defaultMessage="Settings" />
</Link>
<LanguageButton menuAlign="right" />
<UserButton />
</>
) : (
<LanguageButton menuAlign="right" />
)}
</div>
)}
</div>
</div>
</header>
);

View File

@ -13,6 +13,10 @@ export default function Layout({ title, children, header = true, footer = true }
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap"
rel="stylesheet"
/>
</Head>
{header && <Header />}
<main className="container">{children}</main>

View File

@ -1,8 +1,8 @@
import React, { useMemo } from 'react';
import { useSpring, animated } from 'react-spring';
import classNames from 'classnames';
import useFetch from 'hooks/useFetch';
import styles from './ActiveUsers.module.css';
import { FormattedMessage } from 'react-intl';
export default function ActiveUsers({ websiteId, className }) {
const { data } = useFetch(`/api/website/${websiteId}/active`, {}, { interval: 60000 });
@ -10,11 +10,6 @@ export default function ActiveUsers({ websiteId, className }) {
return data?.[0]?.x || 0;
}, [data]);
const props = useSpring({
x: count,
from: { x: 0 },
});
if (count === 0) {
return null;
}
@ -23,10 +18,13 @@ export default function ActiveUsers({ websiteId, className }) {
<div className={classNames(styles.container, className)}>
<div className={styles.dot} />
<div className={styles.text}>
<animated.div className={styles.value}>
{props.x.interpolate(x => x.toFixed(0))}
</animated.div>
<div>{`current visitor${count !== 1 ? 's' : ''}`}</div>
<div>
<FormattedMessage
id="active-users.message"
defaultMessage="{x} current {x, plural, one {visitor} other {visitors}}"
values={{ x: count }}
/>
</div>
</div>
</div>
);

View File

@ -3,8 +3,9 @@ import ReactTooltip from 'react-tooltip';
import classNames from 'classnames';
import ChartJS from 'chart.js';
import styles from './BarChart.module.css';
import { format } from 'date-fns';
import { formatLongNumber } from 'lib/format';
import { dateFormat } from 'lib/lang';
import { useSelector } from 'react-redux';
export default function BarChart({
chartId,
@ -21,6 +22,7 @@ export default function BarChart({
const canvas = useRef();
const chart = useRef();
const [tooltip, setTooltip] = useState({});
const locale = useSelector(state => state.app.locale);
function renderXLabel(label, index, values) {
const d = new Date(values[index].value);
@ -28,23 +30,23 @@ export default function BarChart({
switch (unit) {
case 'hour':
return format(d, 'ha');
return dateFormat(d, 'ha', locale);
case 'day':
if (records > 31) {
if (w <= 500) {
return index % 10 === 0 ? format(d, 'M/d') : '';
return index % 10 === 0 ? dateFormat(d, 'M/d', locale) : '';
}
return index % 5 === 0 ? format(d, 'M/d') : '';
return index % 5 === 0 ? dateFormat(d, 'M/d', locale) : '';
}
if (w <= 500) {
return index % 2 === 0 ? format(d, 'MMM d') : '';
return index % 2 === 0 ? dateFormat(d, 'MMM d', locale) : '';
}
return format(d, 'EEE M/d');
return dateFormat(d, 'EEE M/d', locale);
case 'month':
if (w <= 660) {
return format(d, 'MMM');
return dateFormat(d, 'MMM', locale);
}
return format(d, 'MMMM');
return dateFormat(d, 'MMMM', locale);
default:
return label;
}

View File

@ -1,13 +1,14 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable';
import { browserFilter } from 'lib/filters';
export default function BrowsersTable({ websiteId, limit, onExpand }) {
return (
<MetricsTable
title="Browsers"
title={<FormattedMessage id="metrics.browsers" defaultMessage="Browsers" />}
type="browser"
metric="Visitors"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId}
limit={limit}
dataFilter={browserFilter}

View File

@ -1,13 +1,14 @@
import React from 'react';
import MetricsTable from './MetricsTable';
import { countryFilter, percentFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl';
export default function CountriesTable({ websiteId, limit, onDataLoad = () => {}, onExpand }) {
return (
<MetricsTable
title="Countries"
title={<FormattedMessage id="metrics.countries" defaultMessage="Countries" />}
type="country"
metric="Visitors"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId}
limit={limit}
dataFilter={countryFilter}

View File

@ -1,13 +1,14 @@
import React from 'react';
import MetricsTable from './MetricsTable';
import { deviceFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl';
export default function DevicesTable({ websiteId, limit, onExpand }) {
return (
<MetricsTable
title="Devices"
title={<FormattedMessage id="metrics.devices" defaultMessage="Devices" />}
type="device"
metric="Visitors"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId}
limit={limit}
dataFilter={deviceFilter}

View File

@ -1,13 +1,14 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable';
import styles from './EventsTable.module.css';
export default function EventsTable({ websiteId, limit, onExpand, onDataLoad }) {
return (
<MetricsTable
title="Events"
title={<FormattedMessage id="metrics.events" defaultMessage="Events" />}
type="event"
metric="Actions"
metric={<FormattedMessage id="metrics.actions" defaultMessage="Actions" />}
websiteId={websiteId}
limit={limit}
renderLabel={({ x }) => <Label value={x} />}

View File

@ -1,11 +1,12 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import MetricCard from './MetricCard';
import Loading from 'components/common/Loading';
import useFetch from 'hooks/useFetch';
import { useDateRange } from 'hooks/useDateRange';
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
import MetricCard from './MetricCard';
import styles from './MetricsBar.module.css';
import { useDateRange } from '../../hooks/useDateRange';
export default function MetricsBar({ websiteId, className }) {
const dateRange = useDateRange(websiteId);
@ -36,15 +37,28 @@ export default function MetricsBar({ websiteId, className }) {
<Loading />
) : (
<>
<MetricCard label="Views" value={pageviews} format={formatFunc} />
<MetricCard label="Visitors" value={uniques} format={formatFunc} />
<MetricCard
label="Bounce rate"
label={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
value={pageviews}
format={formatFunc}
/>
<MetricCard
label={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
value={uniques}
format={formatFunc}
/>
<MetricCard
label={<FormattedMessage id="metrics.bounce-rate" defaultMessage="Bounce rate" />}
value={pageviews ? (bounces / pageviews) * 100 : 0}
format={n => Number(n).toFixed(0) + '%'}
/>
<MetricCard
label="Average visit time"
label={
<FormattedMessage
id="metrics.average-visit-time"
defaultMessage="Average visit time"
/>
}
value={totaltime && pageviews ? totaltime / (pageviews - bounces) : 0}
format={n => formatShortTime(n, ['m', 's'], ' ')}
/>

View File

@ -10,6 +10,7 @@ import { percentFilter } from 'lib/filters';
import { formatNumber, formatLongNumber } from 'lib/format';
import { useDateRange } from 'hooks/useDateRange';
import styles from './MetricsTable.module.css';
import { FormattedMessage } from 'react-intl';
export default function MetricsTable({
websiteId,
@ -97,7 +98,9 @@ export default function MetricsTable({
<div className={styles.footer}>
{limit && data.length > limit && (
<Button icon={<Arrow />} size="xsmall" onClick={() => onExpand(type)}>
<div>More</div>
<div>
<FormattedMessage id="button.more" defaultMessage="More" />
</div>
</Button>
)}
</div>

View File

@ -1,13 +1,14 @@
import React from 'react';
import MetricsTable from './MetricsTable';
import { osFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl';
export default function OSTable({ websiteId, limit, onExpand }) {
return (
<MetricsTable
title="Operating System"
title={<FormattedMessage id="metrics.operating-system" defaultMessage="Operating system" />}
type="os"
metric="Visitors"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId}
limit={limit}
dataFilter={osFilter}

View File

@ -1,34 +1,39 @@
import React, { useState } from 'react';
import MetricsTable from './MetricsTable';
import { FormattedMessage } from 'react-intl';
import ButtonGroup from 'components/common/ButtonGroup';
import { urlFilter } from 'lib/filters';
import ButtonGroup from '../common/ButtonGroup';
import { FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
import MetricsTable from './MetricsTable';
export default function PagesTable({ websiteId, websiteDomain, limit, onExpand }) {
const [filter, setFilter] = useState('Combined');
const [filter, setFilter] = useState(FILTER_COMBINED);
const buttons = [
{
label: <FormattedMessage id="metrics.filter.combined" defaultMessage="Combined" />,
value: FILTER_COMBINED,
},
{ label: <FormattedMessage id="metrics.filter.raw" defaultMessage="Raw" />, value: FILTER_RAW },
];
return (
<MetricsTable
title="Pages"
title={<FormattedMessage id="metrics.pages" defaultMessage="Pages" />}
type="url"
metric="Views"
headerComponent={limit ? null : <FilterButtons selected={filter} onClick={setFilter} />}
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
headerComponent={
limit ? null : <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />
}
websiteId={websiteId}
limit={limit}
dataFilter={urlFilter}
filterOptions={{ domain: websiteDomain, raw: filter === 'Raw' }}
filterOptions={{ domain: websiteDomain, raw: filter === FILTER_RAW }}
renderLabel={({ x }) => decodeURI(x)}
onExpand={onExpand}
/>
);
}
const FilterButtons = ({ selected, onClick }) => {
return (
<ButtonGroup
size="xsmall"
items={['Combined', 'Raw']}
selectedItem={selected}
onClick={onClick}
/>
);
const FilterButtons = ({ buttons, selected, onClick }) => {
return <ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} />;
};

View File

@ -1,8 +1,11 @@
import React from 'react';
import { useIntl } from 'react-intl';
import CheckVisible from 'components/helpers/CheckVisible';
import BarChart from './BarChart';
export default function PageviewsChart({ websiteId, data, unit, records, className }) {
const intl = useIntl();
const handleUpdate = chart => {
const {
data: { datasets },
@ -26,7 +29,10 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
chartId={websiteId}
datasets={[
{
label: 'unique visitors',
label: intl.formatMessage({
id: 'metrics.unique-visitors',
defaultMessage: 'Unique visitors',
}),
data: data.uniques,
lineTension: 0,
backgroundColor: 'rgb(38, 128, 235, 0.4)',
@ -34,7 +40,10 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
borderWidth: 1,
},
{
label: 'page views',
label: intl.formatMessage({
id: 'metrics.page-views',
defaultMessage: 'Page views',
}),
data: data.pageviews,
lineTension: 0,
backgroundColor: 'rgb(38, 128, 235, 0.2)',

View File

@ -3,18 +3,18 @@ import ButtonGroup from 'components/common/ButtonGroup';
import { getDateRange } from 'lib/date';
import styles from './QuickButtons.module.css';
const options = {
'24h': '24hour',
'7d': '7day',
'30d': '30day',
};
const options = [
{ label: '24h', value: '24hour' },
{ label: '7d', value: '7day' },
{ label: '30d', value: '30day' },
];
export default function QuickButtons({ value, onChange }) {
const selectedItem = Object.keys(options).find(key => options[key] === value);
const selectedItem = options.find(item => item.value === value)?.value;
function handleClick(selected) {
if (options[selected] !== value) {
onChange(getDateRange(options[selected]));
if (selected !== value) {
onChange(getDateRange(selected));
}
}
@ -22,7 +22,7 @@ export default function QuickButtons({ value, onChange }) {
<ButtonGroup
size="xsmall"
className={styles.buttons}
items={Object.keys(options)}
items={options}
selectedItem={selectedItem}
onClick={handleClick}
/>

View File

@ -1,10 +1,24 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable';
import { refFilter } from 'lib/filters';
import ButtonGroup from 'components/common/ButtonGroup';
import { FILTER_DOMAIN_ONLY, FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
export default function ReferrersTable({ websiteId, websiteDomain, limit, onExpand = () => {} }) {
const [filter, setFilter] = useState('Combined');
const [filter, setFilter] = useState(FILTER_COMBINED);
const buttons = [
{
label: <FormattedMessage id="metrics.filter.domain-only" defaultMessage="Domain only" />,
value: FILTER_DOMAIN_ONLY,
},
{
label: <FormattedMessage id="metrics.filter.combined" defaultMessage="Combined" />,
value: FILTER_COMBINED,
},
{ label: <FormattedMessage id="metrics.filter.raw" defaultMessage="Raw" />, value: FILTER_RAW },
];
const renderLink = ({ x: url }) => {
return url.startsWith('http') ? (
@ -18,18 +32,20 @@ export default function ReferrersTable({ websiteId, websiteDomain, limit, onExpa
return (
<MetricsTable
title="Referrers"
title={<FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />}
type="referrer"
metric="Views"
headerComponent={limit ? null : <FilterButtons selected={filter} onClick={setFilter} />}
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
headerComponent={
limit ? null : <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />
}
websiteId={websiteId}
websiteDomain={websiteDomain}
limit={limit}
dataFilter={refFilter}
filterOptions={{
domain: websiteDomain,
domainOnly: filter === 'Domain only',
raw: filter === 'Raw',
domainOnly: filter === FILTER_DOMAIN_ONLY,
raw: filter === FILTER_RAW,
}}
onExpand={onExpand}
renderLabel={renderLink}
@ -37,13 +53,6 @@ export default function ReferrersTable({ websiteId, websiteDomain, limit, onExpa
);
}
const FilterButtons = ({ selected, onClick }) => {
return (
<ButtonGroup
size="xsmall"
items={['Domain only', 'Combined', 'Raw']}
selectedItem={selected}
onClick={onClick}
/>
);
const FilterButtons = ({ buttons, selected, onClick }) => {
return <ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} />;
};

View File

@ -1,4 +1,5 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { useRouter } from 'next/router';
import PageHeader from 'components/layout/PageHeader';
import Button from 'components/common/Button';
@ -27,7 +28,9 @@ export default function WebsiteHeader({ websiteId, title, showLink = false }) {
}
size="small"
>
<div>View details</div>
<div>
<FormattedMessage id="button.view-details" defaultMessage="View details" />
</div>
</Button>
)}
</ButtonLayout>

View File

@ -15,6 +15,7 @@ import Trash from 'assets/trash.svg';
import Check from 'assets/check.svg';
import styles from './AccountSettings.module.css';
import Toast from '../common/Toast';
import { FormattedMessage } from 'react-intl';
export default function AccountSettings() {
const [addAccount, setAddAccount] = useState();
@ -30,19 +31,27 @@ export default function AccountSettings() {
row.username !== 'admin' ? (
<ButtonLayout>
<Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}>
<div>Edit</div>
<div>
<FormattedMessage id="button.edit" defaultMessage="Edit" />
</div>
</Button>
<Button icon={<Trash />} size="small" onClick={() => setDeleteAccount(row)}>
<div>Delete</div>
<div>
<FormattedMessage id="button.delete" defaultMessage="Delete" />
</div>
</Button>
</ButtonLayout>
) : null;
const columns = [
{ key: 'username', label: 'Username', className: 'col-6 col-md-4' },
{
key: 'username',
label: <FormattedMessage id="label.username" defaultMessage="Username" />,
className: 'col-6 col-md-4',
},
{
key: 'is_admin',
label: 'Administrator',
label: <FormattedMessage id="label.adminsitrator" defaultMessage="Administrator" />,
className: 'col-6 col-md-4',
render: Checkmark,
},
@ -54,7 +63,7 @@ export default function AccountSettings() {
function handleSave() {
setSaved(state => state + 1);
setMessage('Saved successfully.');
setMessage(<FormattedMessage id="message.save-success" defaultMessage="Saved successfully." />);
handleClose();
}
@ -73,12 +82,14 @@ export default function AccountSettings() {
<PageHeader>
<div>Accounts</div>
<Button icon={<Plus />} size="small" onClick={() => setAddAccount(true)}>
<div>Add account</div>
<div>
<FormattedMessage id="button.add-account" defaultMessage="Add account" />
</div>
</Button>
</PageHeader>
<Table columns={columns} rows={data} />
{editAccount && (
<Modal title="Edit account">
<Modal title={<FormattedMessage id="title.edit-account" defaultMessage="Edit account" />}>
<AccountEditForm
values={{ ...editAccount, password: '' }}
onSave={handleSave}
@ -87,12 +98,14 @@ export default function AccountSettings() {
</Modal>
)}
{addAccount && (
<Modal title="Add account">
<Modal title={<FormattedMessage id="title.add-account" defaultMessage="Add account" />}>
<AccountEditForm onSave={handleSave} onClose={handleClose} />
</Modal>
)}
{deleteAccount && (
<Modal title="Delete account">
<Modal
title={<FormattedMessage id="title.delete-account" defaultMessage="Delete account" />}
>
<DeleteForm
values={{ type: 'account', id: deleteAccount.user_id, name: deleteAccount.username }}
onSave={handleSave}

View File

@ -1,11 +1,12 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { useSelector } from 'react-redux';
import PageHeader from 'components/layout/PageHeader';
import Button from 'components/common/Button';
import ChangePasswordForm from '../forms/ChangePasswordForm';
import Modal from 'components/common/Modal';
import Toast from 'components/common/Toast';
import ChangePasswordForm from 'components/forms/ChangePasswordForm';
import Dots from 'assets/ellipsis-h.svg';
import Toast from '../common/Toast';
export default function ProfileSettings() {
const user = useSelector(state => state.user);
@ -15,19 +16,25 @@ export default function ProfileSettings() {
function handleSave() {
setChangePassword(false);
setMessage('Saved successfully.');
setMessage(<FormattedMessage id="message.save-success" defaultMessage="Saved successfully." />);
}
return (
<>
<PageHeader>
<div>Profile</div>
<div>
<FormattedMessage id="settings.profile" defaultMessage="Profile" />
</div>
<Button icon={<Dots />} size="small" onClick={() => setChangePassword(true)}>
<div>Change password</div>
<div>
<FormattedMessage id="button.change-password" defaultMessage="Change password" />
</div>
</Button>
</PageHeader>
<dl>
<dt>Username</dt>
<dt>
<FormattedMessage id="label.username" defaultMessage="Username" />
</dt>
<dd>{user.username}</dd>
</dl>
{changePassword && (

View File

@ -5,23 +5,35 @@ import WebsiteSettings from './WebsiteSettings';
import AccountSettings from './AccountSettings';
import ProfileSettings from './ProfileSettings';
import { useSelector } from 'react-redux';
import { FormattedMessage } from 'react-intl';
const WEBSITES = 1;
const ACCOUNTS = 2;
const PROFILE = 3;
export default function Settings() {
const user = useSelector(state => state.user);
const [option, setOption] = useState(1);
const [option, setOption] = useState(WEBSITES);
const menuOptions = [
{ label: 'Websites', value: 1 },
{ label: 'Accounts', value: 2, hidden: !user.is_admin },
{ label: 'Profile', value: 3 },
{
label: <FormattedMessage id="settings.websites" defaultMessage="Websites" />,
value: WEBSITES,
},
{
label: <FormattedMessage id="settings.accounts" defaultMessage="Accounts" />,
value: ACCOUNTS,
hidden: !user.is_admin,
},
{ label: <FormattedMessage id="settings.profile" defaultMessage="Profile" />, value: PROFILE },
];
return (
<Page>
<MenuLayout menu={menuOptions} selectedOption={option} onMenuSelect={setOption}>
{option === 1 && <WebsiteSettings />}
{option === 2 && <AccountSettings />}
{option === 3 && <ProfileSettings />}
{option === WEBSITES && <WebsiteSettings />}
{option === ACCOUNTS && <AccountSettings />}
{option === PROFILE && <ProfileSettings />}
</MenuLayout>
</Page>
);

View File

@ -1,23 +1,24 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import Table from 'components/common/Table';
import Button from 'components/common/Button';
import PageHeader from 'components/layout/PageHeader';
import Modal from 'components/common/Modal';
import WebsiteEditForm from '../forms/WebsiteEditForm';
import DeleteForm from '../forms/DeleteForm';
import TrackingCodeForm from '../forms/TrackingCodeForm';
import ShareUrlForm from '../forms/ShareUrlForm';
import WebsiteEditForm from 'components/forms/WebsiteEditForm';
import DeleteForm from 'components/forms/DeleteForm';
import TrackingCodeForm from 'components/forms/TrackingCodeForm';
import ShareUrlForm from 'components/forms/ShareUrlForm';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import ButtonLayout from 'components/layout/ButtonLayout';
import Toast from 'components/common/Toast';
import Pen from 'assets/pen.svg';
import Trash from 'assets/trash.svg';
import Plus from 'assets/plus.svg';
import Code from 'assets/code.svg';
import Link from 'assets/link.svg';
import useFetch from 'hooks/useFetch';
import styles from './WebsiteSettings.module.css';
import useFetch from '../../hooks/useFetch';
import Toast from '../common/Toast';
export default function WebsiteSettings() {
const [editWebsite, setEditWebsite] = useState();
@ -35,7 +36,7 @@ export default function WebsiteSettings() {
<Button
icon={<Link />}
size="small"
tooltip="Share URL"
tooltip={<FormattedMessage id="tooltip.get-share-url" defaultMessage="Get share URL" />}
tooltipId={`button-share-${row.website_id}`}
onClick={() => setShowUrl(row)}
/>
@ -43,22 +44,36 @@ export default function WebsiteSettings() {
<Button
icon={<Code />}
size="small"
tooltip="Get tracking code"
tooltip={
<FormattedMessage id="tooltip.get-tracking-code" defaultMessage="Get tracking code" />
}
tooltipId={`button-code-${row.website_id}`}
onClick={() => setShowCode(row)}
/>
<Button icon={<Pen />} size="small" onClick={() => setEditWebsite(row)}>
<div>Edit</div>
<div>
<FormattedMessage id="button.edit" defaultMessage="Edit" />
</div>
</Button>
<Button icon={<Trash />} size="small" onClick={() => setDeleteWebsite(row)}>
<div>Delete</div>
<div>
<FormattedMessage id="button.delete" defaultMessage="Delete" />
</div>
</Button>
</ButtonLayout>
);
const columns = [
{ key: 'name', label: 'Name', className: 'col-6 col-md-4' },
{ key: 'domain', label: 'Domain', className: 'col-6 col-md-4' },
{
key: 'name',
label: <FormattedMessage id="label.name" defaultMessage="Name" />,
className: 'col-6 col-md-4',
},
{
key: 'domain',
label: <FormattedMessage id="label.domain" defaultMessage="Domain" />,
className: 'col-6 col-md-4',
},
{
key: 'action',
className: classNames(styles.buttons, 'col-12 col-md-4 pt-2 pt-md-0'),
@ -68,7 +83,7 @@ export default function WebsiteSettings() {
function handleSave() {
setSaved(state => state + 1);
setMessage('Saved successfully.');
setMessage(<FormattedMessage id="message.save-success" defaultMessage="Saved successfully." />);
handleClose();
}
@ -85,9 +100,18 @@ export default function WebsiteSettings() {
}
const empty = (
<EmptyPlaceholder msg={"You don't have any websites configured."}>
<EmptyPlaceholder
msg={
<FormattedMessage
id="placeholder.message.no-websites-configured"
defaultMessage="You don't have any websites configured."
/>
}
>
<Button icon={<Plus />} size="medium" onClick={() => setAddWebsite(true)}>
<div>Add website</div>
<div>
<FormattedMessage id="button.add-website" defaultMessage="Add website" />
</div>
</Button>
</EmptyPlaceholder>
);
@ -95,24 +119,30 @@ export default function WebsiteSettings() {
return (
<>
<PageHeader>
<div>Websites</div>
<div>
<FormattedMessage id="settings.websites" defaultMessage="Websites" />
</div>
<Button icon={<Plus />} size="small" onClick={() => setAddWebsite(true)}>
<div>Add website</div>
<div>
<FormattedMessage id="button.add-website" defaultMessage="Add website" />
</div>
</Button>
</PageHeader>
<Table columns={columns} rows={data} empty={empty} />
{editWebsite && (
<Modal title="Edit website">
<Modal title={<FormattedMessage id="title.edit-website" defaultMessage="Edit website" />}>
<WebsiteEditForm values={editWebsite} onSave={handleSave} onClose={handleClose} />
</Modal>
)}
{addWebsite && (
<Modal title="Add website">
<Modal title={<FormattedMessage id="title.add-website" defaultMessage="Add website" />}>
<WebsiteEditForm onSave={handleSave} onClose={handleClose} />
</Modal>
)}
{deleteWebsite && (
<Modal title="Delete website">
<Modal
title={<FormattedMessage id="title.delete-website" defaultMessage="Delete website" />}
>
<DeleteForm
values={{ type: 'website', id: deleteWebsite.website_id, name: deleteWebsite.name }}
onSave={handleSave}
@ -121,12 +151,12 @@ export default function WebsiteSettings() {
</Modal>
)}
{showCode && (
<Modal title="Tracking code">
<Modal title={<FormattedMessage id="title.tracking-code" defaultMessage="Tracking code" />}>
<TrackingCodeForm values={showCode} onClose={handleClose} />
</Modal>
)}
{showUrl && (
<Modal title="Share URL">
<Modal title={<FormattedMessage id="title.share-url" defaultMessage="Share URL" />}>
<ShareUrlForm values={showUrl} onClose={handleClose} />
</Modal>
)}

236
lang/en.json Normal file
View File

@ -0,0 +1,236 @@
{
"active-users.message": {
"defaultMessage": "{x} current {x, plural, one {visitor} other {visitors}}"
},
"button.add-account": {
"defaultMessage": "Add account"
},
"button.add-website": {
"defaultMessage": "Add website"
},
"button.back": {
"defaultMessage": "Back"
},
"button.cancel": {
"defaultMessage": "Cancel"
},
"button.change-password": {
"defaultMessage": "Change password"
},
"button.copy-to-clipboard": {
"defaultMessage": "Copy to clipboard"
},
"button.delete": {
"defaultMessage": "Delete"
},
"button.edit": {
"defaultMessage": "Edit"
},
"button.login": {
"defaultMessage": "Login"
},
"button.more": {
"defaultMessage": "More"
},
"button.save": {
"defaultMessage": "Save"
},
"button.view-details": {
"defaultMessage": "View details"
},
"footer.powered-by": {
"defaultMessage": "powered by"
},
"header.nav.dashboard": {
"defaultMessage": "Dashboard"
},
"header.nav.settings": {
"defaultMessage": "Settings"
},
"label.adminsitrator": {
"defaultMessage": "Administrator"
},
"label.confirm-password": {
"defaultMessage": "Confirm password"
},
"label.current-password": {
"defaultMessage": "Current password"
},
"label.domain": {
"defaultMessage": "Domain"
},
"label.enable-share-url": {
"defaultMessage": "Enable share URL"
},
"label.invalid": {
"defaultMessage": "Invalid"
},
"label.invalid-domain": {
"defaultMessage": "Invalid domain"
},
"label.last-days": {
"defaultMessage": "Last {x} days"
},
"label.last-hours": {
"defaultMessage": "Last {x} hours"
},
"label.logged-in-as": {
"defaultMessage": "Logged in as {username}"
},
"label.logout": {
"defaultMessage": "Logout"
},
"label.name": {
"defaultMessage": "Name"
},
"label.new-password": {
"defaultMessage": "New password"
},
"label.password": {
"defaultMessage": "Password"
},
"label.passwords-dont-match": {
"defaultMessage": "Passwords don't match"
},
"label.required": {
"defaultMessage": "Required"
},
"label.this-month": {
"defaultMessage": "This month"
},
"label.this-week": {
"defaultMessage": "This week"
},
"label.this-year": {
"defaultMessage": "This year"
},
"label.today": {
"defaultMessage": "Today"
},
"label.username": {
"defaultMessage": "Username"
},
"message.confirm-delete": {
"defaultMessage": "Are your sure you want to delete {target}?"
},
"message.copied": {
"defaultMessage": "Copied!"
},
"message.delete-warning": {
"defaultMessage": "All associated data will be deleted as well."
},
"message.failure": {
"defaultMessage": "Something went wrong."
},
"message.incorrect-username-password": {
"defaultMessage": "Incorrect username/password."
},
"message.save-success": {
"defaultMessage": "Saved successfully."
},
"message.share-url": {
"defaultMessage": "This is the publicly shared URL for {target}."
},
"message.track-stats": {
"defaultMessage": "To track stats for {target}, place the following code in the {head} section of your website."
},
"message.type-delete": {
"defaultMessage": "Type {delete} in the box below to confirm."
},
"metrics.actions": {
"defaultMessage": "Actions"
},
"metrics.average-visit-time": {
"defaultMessage": "Average visit time"
},
"metrics.bounce-rate": {
"defaultMessage": "Bounce rate"
},
"metrics.browsers": {
"defaultMessage": "Browsers"
},
"metrics.countries": {
"defaultMessage": "Countries"
},
"metrics.devices": {
"defaultMessage": "Devices"
},
"metrics.events": {
"defaultMessage": "Events"
},
"metrics.filter.combined": {
"defaultMessage": "Combined"
},
"metrics.filter.domain-only": {
"defaultMessage": "Domain only"
},
"metrics.filter.raw": {
"defaultMessage": "Raw"
},
"metrics.operating-system": {
"defaultMessage": "Operating system"
},
"metrics.page-views": {
"defaultMessage": "Page views"
},
"metrics.pages": {
"defaultMessage": "Pages"
},
"metrics.referrers": {
"defaultMessage": "Referrers"
},
"metrics.unique-visitors": {
"defaultMessage": "Unique visitors"
},
"metrics.views": {
"defaultMessage": "Views"
},
"metrics.visitors": {
"defaultMessage": "Visitors"
},
"placeholder.message.go-to-settings": {
"defaultMessage": "Go to settings"
},
"placeholder.message.no-websites-configured": {
"defaultMessage": "You don't have any websites configured."
},
"settings.accounts": {
"defaultMessage": "Accounts"
},
"settings.profile": {
"defaultMessage": "Profile"
},
"settings.websites": {
"defaultMessage": "Websites"
},
"title.add-account": {
"defaultMessage": "Add account"
},
"title.add-website": {
"defaultMessage": "Add website"
},
"title.delete-account": {
"defaultMessage": "Delete account"
},
"title.delete-website": {
"defaultMessage": "Delete website"
},
"title.edit-account": {
"defaultMessage": "Edit account"
},
"title.edit-website": {
"defaultMessage": "Edit website"
},
"title.share-url": {
"defaultMessage": "Share URL"
},
"title.tracking-code": {
"defaultMessage": "Tracking code"
},
"tooltip.get-share-url": {
"defaultMessage": "Get share URL"
},
"tooltip.get-tracking-code": {
"defaultMessage": "Get tracking code"
}
}

236
lang/zh-CN.json Normal file
View File

@ -0,0 +1,236 @@
{
"active-users.message": {
"defaultMessage": "当前在线 {x} 人"
},
"button.add-account": {
"defaultMessage": "添加账户"
},
"button.add-website": {
"defaultMessage": "添加网站"
},
"button.back": {
"defaultMessage": "返回"
},
"button.cancel": {
"defaultMessage": "取消"
},
"button.change-password": {
"defaultMessage": "更新密码"
},
"button.copy-to-clipboard": {
"defaultMessage": "复制"
},
"button.delete": {
"defaultMessage": "删除"
},
"button.edit": {
"defaultMessage": "编辑"
},
"button.login": {
"defaultMessage": "登录"
},
"button.more": {
"defaultMessage": "更多"
},
"button.save": {
"defaultMessage": "保存"
},
"button.view-details": {
"defaultMessage": "查看更多"
},
"footer.powered-by": {
"defaultMessage": "运行"
},
"header.nav.dashboard": {
"defaultMessage": "仪表板"
},
"header.nav.settings": {
"defaultMessage": "设置"
},
"label.adminsitrator": {
"defaultMessage": "管理员"
},
"label.confirm-password": {
"defaultMessage": "确认密码"
},
"label.current-password": {
"defaultMessage": "目前密码"
},
"label.domain": {
"defaultMessage": "域名"
},
"label.enable-share-url": {
"defaultMessage": "激活共享链接"
},
"label.invalid": {
"defaultMessage": "输入无效"
},
"label.invalid-domain": {
"defaultMessage": "无效域名"
},
"label.last-days": {
"defaultMessage": "最近 {x} 天"
},
"label.last-hours": {
"defaultMessage": "最近 {x} 小时"
},
"label.logged-in-as": {
"defaultMessage": "登录名: {username}"
},
"label.logout": {
"defaultMessage": "退出"
},
"label.name": {
"defaultMessage": "名字"
},
"label.new-password": {
"defaultMessage": "新密码"
},
"label.password": {
"defaultMessage": "密码"
},
"label.passwords-dont-match": {
"defaultMessage": "密码不一致"
},
"label.required": {
"defaultMessage": "必填"
},
"label.this-month": {
"defaultMessage": "本月"
},
"label.this-week": {
"defaultMessage": "本周"
},
"label.this-year": {
"defaultMessage": "今年"
},
"label.today": {
"defaultMessage": "今天"
},
"label.username": {
"defaultMessage": "用户名"
},
"message.confirm-delete": {
"defaultMessage": "你确定要删除{target}吗?"
},
"message.copied": {
"defaultMessage": "复制成功!"
},
"message.delete-warning": {
"defaultMessage": "所有相关数据将会被删除."
},
"message.failure": {
"defaultMessage": "出现错误."
},
"message.incorrect-username-password": {
"defaultMessage": "用户名密码不正确."
},
"message.save-success": {
"defaultMessage": "成功保存."
},
"message.share-url": {
"defaultMessage": "这是 {target} 的共享链接."
},
"message.track-stats": {
"defaultMessage": "把以下代码放到你的网站的{head}部分来收集{target}的数据."
},
"message.type-delete": {
"defaultMessage": "在下面空格输入{delete}确认"
},
"metrics.actions": {
"defaultMessage": "用户行为"
},
"metrics.average-visit-time": {
"defaultMessage": "平均访问时间"
},
"metrics.bounce-rate": {
"defaultMessage": "跳出率"
},
"metrics.browsers": {
"defaultMessage": "浏览器"
},
"metrics.countries": {
"defaultMessage": "国家"
},
"metrics.devices": {
"defaultMessage": "设备"
},
"metrics.events": {
"defaultMessage": "行为类别"
},
"metrics.filter.combined": {
"defaultMessage": "总和"
},
"metrics.filter.domain-only": {
"defaultMessage": "只看域名"
},
"metrics.filter.raw": {
"defaultMessage": "原始"
},
"metrics.operating-system": {
"defaultMessage": "操作系统"
},
"metrics.page-views": {
"defaultMessage": "页面流量"
},
"metrics.pages": {
"defaultMessage": "网页"
},
"metrics.referrers": {
"defaultMessage": "指入域名"
},
"metrics.unique-visitors": {
"defaultMessage": "独立访客"
},
"metrics.views": {
"defaultMessage": "页面流量"
},
"metrics.visitors": {
"defaultMessage": "独立访客"
},
"placeholder.message.go-to-settings": {
"defaultMessage": "去设置"
},
"placeholder.message.no-websites-configured": {
"defaultMessage": "你还没有设置任何网站."
},
"settings.accounts": {
"defaultMessage": "账户"
},
"settings.profile": {
"defaultMessage": "个人资料"
},
"settings.websites": {
"defaultMessage": "网站"
},
"title.add-account": {
"defaultMessage": "添加账户"
},
"title.add-website": {
"defaultMessage": "添加网站"
},
"title.delete-account": {
"defaultMessage": "删除账户"
},
"title.delete-website": {
"defaultMessage": "删除网站"
},
"title.edit-account": {
"defaultMessage": "编辑账户"
},
"title.edit-website": {
"defaultMessage": "编辑网站"
},
"title.share-url": {
"defaultMessage": "共享链接"
},
"title.tracking-code": {
"defaultMessage": "跟踪代码"
},
"tooltip.get-share-url": {
"defaultMessage": "获得共享链接"
},
"tooltip.get-tracking-code": {
"defaultMessage": "获得跟踪代码"
}
}

View File

@ -19,6 +19,10 @@ export const POSTGRESQL_DATE_FORMATS = {
year: 'YYYY-01-01',
};
export const FILTER_DOMAIN_ONLY = 0;
export const FILTER_COMBINED = 1;
export const FILTER_RAW = 2;
export const DOMAIN_REGEX = /((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}/;
export const DESKTOP_SCREEN_WIDTH = 1920;

23
lib/lang.js Normal file
View File

@ -0,0 +1,23 @@
import enMessages from 'lang-compiled/en.json';
import zhCNMessages from 'lang-compiled/zh-CN.json';
import { format } from 'date-fns';
import { enUS, zhCN } from 'date-fns/locale';
export const messages = {
en: enMessages,
'zh-CN': zhCNMessages,
};
export const dateLocales = {
en: enUS,
'zh-CN': zhCN,
};
export const menuOptions = [
{ label: 'English', value: 'en', display: 'EN' },
{ label: '中文 (Chinese Simplified)', value: 'zh-CN', display: '中文' },
];
export function dateFormat(date, str, locale) {
return format(date, str, { locale: dateLocales[locale] || enUS });
}

View File

@ -4,10 +4,15 @@ import { subMinutes } from 'date-fns';
import { MYSQL, POSTGRESQL, MYSQL_DATE_FORMATS, POSTGRESQL_DATE_FORMATS } from 'lib/constants';
export function getDatabase() {
return (
const type =
process.env.DATABASE_TYPE ||
(process.env.DATABASE_URL && process.env.DATABASE_URL.split(':')[0])
);
(process.env.DATABASE_URL && process.env.DATABASE_URL.split(':')[0]);
if (type === 'postgres') {
return 'postgresql';
}
return type;
}
export function getDateQuery(db, field, unit, timezone) {

View File

@ -1,6 +1,6 @@
{
"name": "umami",
"version": "0.22.0",
"version": "0.24.0",
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
"author": "Mike Cao <mike@mikecao.com>",
"license": "MIT",
@ -11,7 +11,7 @@
},
"scripts": {
"dev": "next dev",
"build": "npm-run-all build-tracker copy-db-schema build-db-client build-app",
"build": "npm-run-all build-tracker compile-lang copy-db-schema build-db-client build-app",
"start": "next start",
"build-app": "next build",
"build-tracker": "rollup -c rollup.tracker.config.js",
@ -21,7 +21,9 @@
"build-mysql-schema": "dotenv prisma introspect -- --schema=./prisma/schema.mysql.prisma",
"build-mysql-client": "dotenv prisma generate -- --schema=./prisma/schema.mysql.prisma",
"build-postgresql-schema": "dotenv prisma introspect -- --schema=./prisma/schema.postgresql.prisma",
"build-postgresql-client": "dotenv prisma generate -- --schema=./prisma/schema.postgresql.prisma"
"build-postgresql-client": "dotenv prisma generate -- --schema=./prisma/schema.postgresql.prisma",
"extract-lang": "formatjs extract {pages,components}/**/*.js --out-file lang/en.json",
"compile-lang": "formatjs compile-folder --ast lang lang-compiled"
},
"lint-staged": {
"**/*.js": [
@ -61,6 +63,7 @@
"promise-polyfill": "^8.1.3",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-intl": "^5.8.0",
"react-redux": "^7.2.1",
"react-simple-maps": "^2.1.2",
"react-spring": "^8.0.27",
@ -75,6 +78,7 @@
"uuid": "^8.3.0"
},
"devDependencies": {
"@formatjs/cli": "^2.9.0",
"@prisma/cli": "2.6.2",
"@rollup/plugin-buble": "^0.21.3",
"@rollup/plugin-node-resolve": "^9.0.0",
@ -87,6 +91,7 @@
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.20.6",
"eslint-plugin-react-hooks": "^4.1.0",
"extract-react-intl-messages": "^4.1.1",
"husky": "^4.2.5",
"lint-staged": "^10.3.0",
"npm-run-all": "^4.1.5",

View File

@ -1,16 +1,41 @@
import React from 'react';
import { Provider } from 'react-redux';
import React, { useEffect } from 'react';
import { IntlProvider } from 'react-intl';
import { Provider, useDispatch, useSelector } from 'react-redux';
import { useStore } from 'redux/store';
import { updateApp } from 'redux/actions/app';
import { messages } from 'lib/lang';
import 'styles/variables.css';
import 'styles/bootstrap-grid.css';
import 'styles/index.css';
const Intl = ({ children }) => {
const dispatch = useDispatch();
const locale = useSelector(state => state.app.locale);
const Wrapper = ({ children }) => <span className={locale}>{children}</span>;
useEffect(() => {
const saved = localStorage.getItem('locale');
if (saved) {
dispatch(updateApp({ locale: saved }));
}
});
return (
<IntlProvider locale={locale} messages={messages[locale]} textComponent={Wrapper}>
{children}
</IntlProvider>
);
};
export default function App({ Component, pageProps }) {
const store = useStore();
return (
<Provider store={store}>
<Component {...pageProps} />
<Intl>
<Component {...pageProps} />
</Intl>
</Provider>
);
}

16
redux/actions/app.js Normal file
View File

@ -0,0 +1,16 @@
import { createSlice } from '@reduxjs/toolkit';
const app = createSlice({
name: 'app',
initialState: { locale: 'en' },
reducers: {
updateApp(state, action) {
state = action.payload;
return state;
},
},
});
export const { updateApp } = app.actions;
export default app.reducer;

View File

@ -1,6 +1,7 @@
import { combineReducers } from 'redux';
import app from './actions/app';
import user from './actions/user';
import websites from './actions/websites';
import queries from './actions/queries';
export default combineReducers({ user, websites, queries });
export default combineReducers({ app, user, websites, queries });

View File

@ -2,14 +2,25 @@ require('dotenv').config();
const fs = require('fs');
const path = require('path');
const databaseType =
process.env.DATABASE_TYPE || (process.env.DATABASE_URL && process.env.DATABASE_URL.split(':')[0]);
function getDatabase() {
const type =
process.env.DATABASE_TYPE ||
(process.env.DATABASE_URL && process.env.DATABASE_URL.split(':')[0]);
if (type === 'postgres') {
return 'postgresql';
}
return type;
}
const databaseType = getDatabase();
if (!databaseType || !['mysql', 'postgresql'].includes(databaseType)) {
throw new Error('Missing or invalid database');
}
console.log(`Database schema detected: ${databaseType}`);
console.log(`Database type detected: ${databaseType}`);
const src = path.resolve(__dirname, `../prisma/schema.${databaseType}.prisma`);
const dest = path.resolve(__dirname, '../prisma/schema.prisma');

View File

@ -13,6 +13,11 @@ body {
background: var(--gray75);
}
.zh-CN {
font-family: 'Noto Sans SC', sans-serif !important;
font-size: 110%;
}
*,
*:before,
*:after {

441
yarn.lock
View File

@ -344,7 +344,7 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
"@babel/parser@^7.10.4", "@babel/parser@^7.11.5", "@babel/parser@^7.7.7":
"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.11.5", "@babel/parser@^7.7.7":
version "7.11.5"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037"
integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==
@ -1025,7 +1025,7 @@
globals "^11.1.0"
lodash "^4.17.19"
"@babel/types@7.11.5", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.4.4", "@babel/types@^7.7.4", "@babel/types@^7.9.5":
"@babel/types@7.11.5", "@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.3.0", "@babel/types@^7.4.4", "@babel/types@^7.7.4", "@babel/types@^7.9.5":
version "7.11.5"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d"
integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==
@ -1064,6 +1064,80 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
"@formatjs/cli@^2.9.0":
version "2.9.0"
resolved "https://registry.yarnpkg.com/@formatjs/cli/-/cli-2.9.0.tgz#52cceffb133cd1307650af26e878785aa4370a0c"
integrity sha512-HSpu0qrpPGaja+V7bHc61ZyzHfV+OzGVmB2DmamjZSaI0S5o7gs0skY40o4chIVNsYT/X9RD9G77pOIEHDWmUA==
dependencies:
"@formatjs/ts-transformer" "^2.9.0"
"@types/json-stable-stringify" "^1.0.32"
"@types/lodash" "^4.14.150"
"@types/loud-rejection" "^2.0.0"
"@types/node" "14"
chalk "^4.0.0"
commander "5.1.0"
fast-glob "^3.2.4"
fs-extra "^9.0.0"
intl-messageformat-parser "^6.0.5"
json-stable-stringify "^1.0.1"
lodash "^4.17.15"
loud-rejection "^2.2.0"
typescript "^4.0"
"@formatjs/ecma402-abstract@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.2.0.tgz#5b03ba4931436070ad926d1b2e89bf07edc5ea5b"
integrity sha512-jc1bZHhIE1YI0HnZIZcdlKpF4wle2pkgQpzXHDoyy4bUqzBSvDqktnF26hOkyA04KD4wqd61gkuTvRrHMmroAg==
"@formatjs/intl-displaynames@^3.3.6":
version "3.3.6"
resolved "https://registry.yarnpkg.com/@formatjs/intl-displaynames/-/intl-displaynames-3.3.6.tgz#2b5c938ea1cd38e859f2d716ea317feccbbd8896"
integrity sha512-yrTDL3U0MR10vp17noLI2JuNiHq/Fp1P8/mW/t1gCMOpw38FY4bFTOV68FWxSZwzsy/yETqXHjPUTUbpLtEO/Q==
dependencies:
"@formatjs/ecma402-abstract" "^1.2.0"
"@formatjs/intl-listformat@^4.2.5":
version "4.2.5"
resolved "https://registry.yarnpkg.com/@formatjs/intl-listformat/-/intl-listformat-4.2.5.tgz#2a39223c5fda3f865d56cea80d7459f5bd9828a8"
integrity sha512-mcH/CdRH58ao3caZzIdAA32vZM5woxTszIieRjhY2qHxCorVzBPXFYCGTVCO9rtKVFlkMR/pyzaqH3Y1gNiRmw==
dependencies:
"@formatjs/ecma402-abstract" "^1.2.0"
"@formatjs/intl-numberformat@^5.5.2":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@formatjs/intl-numberformat/-/intl-numberformat-5.6.0.tgz#87bd1e56246fba2c7af58f73930cbe379dd0aef8"
integrity sha512-MfYcqX1LE2N4P9eVtQXI/L6APlXgjexCj0b7GxJfK+icrwbA0XINSPGTt96kUxO5hf/tDu0MxJXnt9gwMKm/EA==
dependencies:
"@formatjs/ecma402-abstract" "^1.2.0"
"@formatjs/intl-relativetimeformat@^7.2.5":
version "7.2.5"
resolved "https://registry.yarnpkg.com/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-7.2.5.tgz#3101a8262bd7fb329d7bd555135f67a36c5e58df"
integrity sha512-KTf0zTP7YbrVAPPJMnZNYRrNvEwuNwqOVNcfz0cQwewjE2ImxPW+03zdRHkwDt92WbRv6T0EDRBpgC2Dxaip6Q==
dependencies:
"@formatjs/ecma402-abstract" "^1.2.0"
"@formatjs/intl@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-1.3.0.tgz#843d79ced6908c2ca25abf65ccee52bc72da6b85"
integrity sha512-wjzzA7CALsYDjDOdpmGGsMYUblp9LcPtxdjjdZyd8s4xQ5lZZUWrJxqzInkax89TWeGTprHGYh31qPpYbjsRRQ==
dependencies:
"@formatjs/ecma402-abstract" "^1.2.0"
"@formatjs/intl-displaynames" "^3.3.6"
"@formatjs/intl-listformat" "^4.2.5"
"@formatjs/intl-relativetimeformat" "^7.2.5"
fast-memoize "^2.5.2"
intl-messageformat "^9.3.6"
intl-messageformat-parser "^6.0.5"
"@formatjs/ts-transformer@^2.6.0", "@formatjs/ts-transformer@^2.9.0":
version "2.9.0"
resolved "https://registry.yarnpkg.com/@formatjs/ts-transformer/-/ts-transformer-2.9.0.tgz#582f8c54bc5888044f3e848163411dabca130aff"
integrity sha512-H6skH+McG2OoUL3nc6Eas/5IunM8hk7uDU5Ak/qtNtvsehOH8g030LaFaMxw28BWq2vBipGAPlyyz/KTvO8fPw==
dependencies:
intl-messageformat-parser "^6.0.5"
typescript "^4.0"
"@next/react-dev-overlay@9.5.3":
version "9.5.3"
resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-9.5.3.tgz#3275301f08045ecc709e3273031973a1f5e81427"
@ -1289,6 +1363,39 @@
"@svgr/plugin-svgo" "^5.4.0"
loader-utils "^2.0.0"
"@types/babel__core@^7.1.7":
version "7.1.9"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.9.tgz#77e59d438522a6fb898fa43dc3455c6e72f3963d"
integrity sha512-sY2RsIJ5rpER1u3/aQ8OFSI7qGIy8o1NEEbgb2UaJcvOtXOMpd39ko723NBpjQFg9SIX7TXtjejZVGeIMLhoOw==
dependencies:
"@babel/parser" "^7.1.0"
"@babel/types" "^7.0.0"
"@types/babel__generator" "*"
"@types/babel__template" "*"
"@types/babel__traverse" "*"
"@types/babel__generator@*":
version "7.6.1"
resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.1.tgz#4901767b397e8711aeb99df8d396d7ba7b7f0e04"
integrity sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew==
dependencies:
"@babel/types" "^7.0.0"
"@types/babel__template@*":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.2.tgz#4ff63d6b52eddac1de7b975a5223ed32ecea9307"
integrity sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==
dependencies:
"@babel/parser" "^7.1.0"
"@babel/types" "^7.0.0"
"@types/babel__traverse@*":
version "7.0.13"
resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.13.tgz#1874914be974a492e1b4cb00585cabb274e8ba18"
integrity sha512-i+zS7t6/s9cdQvbqKDARrcbrPvtJGlbYsMkazo03nTAK3RX9FNrLllXys22uiTGJapPOTZTQ35nHh4ISph4SLQ==
dependencies:
"@babel/types" "^7.3.0"
"@types/buble@^0.19.2":
version "0.19.2"
resolved "https://registry.yarnpkg.com/@types/buble/-/buble-0.19.2.tgz#a4289d20b175b3c206aaad80caabdabe3ecdfdd1"
@ -1311,17 +1418,49 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
"@types/fs-extra@^9.0.1":
version "9.0.1"
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.1.tgz#91c8fc4c51f6d5dbe44c2ca9ab09310bd00c7918"
integrity sha512-B42Sxuaz09MhC3DDeW5kubRcQ5by4iuVQ0cRRWM2lggLzAa/KVom0Aft/208NgMvNQQZ86s5rVcqDdn/SH0/mg==
dependencies:
"@types/node" "*"
"@types/hoist-non-react-statics@^3.3.1":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
dependencies:
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5":
version "7.0.6"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
"@types/json-stable-stringify@^1.0.32":
version "1.0.32"
resolved "https://registry.yarnpkg.com/@types/json-stable-stringify/-/json-stable-stringify-1.0.32.tgz#121f6917c4389db3923640b2e68de5fa64dda88e"
integrity sha512-q9Q6+eUEGwQkv4Sbst3J4PNgDOvpuVuKj79Hl/qnmBMEIPzB5QoFRUtjcgcg2xNUZyYUGXBk5wYIBKHt0A+Mxw==
"@types/lodash@^4.14.150":
version "4.14.161"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.161.tgz#a21ca0777dabc6e4f44f3d07f37b765f54188b18"
integrity sha512-EP6O3Jkr7bXvZZSZYlsgt5DIjiGr0dXP1/jVEwVLTFgg0d+3lWVQkRavYVQszV7dYUwvg0B8R0MBDpcmXg7XIA==
"@types/loud-rejection@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/loud-rejection/-/loud-rejection-2.0.0.tgz#271bb21c63f51776e1156604cda3b21a2d3f60f3"
integrity sha512-oTHISsIybJGoh3b3Ay/10csbAd2k0su7G7DGrE1QWciC+IdydPm0WMw1+Gr9YMYjPiJ5poB3g5Ev73IlLoavLw==
dependencies:
loud-rejection "*"
"@types/minimist@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6"
integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=
"@types/node@*":
"@types/node@*", "@types/node@14":
version "14.6.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.4.tgz#a145cc0bb14ef9c4777361b7bbafa5cf8e3acb5a"
integrity sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ==
@ -1336,11 +1475,24 @@
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
"@types/prop-types@*":
version "15.7.3"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
"@types/q@^1.5.1":
version "1.5.4"
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
"@types/react@*":
version "16.9.49"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.49.tgz#09db021cf8089aba0cdb12a49f8021a69cce4872"
integrity sha512-DtLFjSj0OYAdVLBbyjhuV9CdGVHCkHn2R+xr3XkBvK2rS1Y1tkc14XSGjYgm5Fjjr90AxH9tiSzc1pCFMGO06g==
dependencies:
"@types/prop-types" "*"
csstype "^3.0.2"
"@types/resolve@1.17.1":
version "1.17.1"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
@ -1348,6 +1500,13 @@
dependencies:
"@types/node" "*"
"@types/schema-utils@^2.4.0":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@types/schema-utils/-/schema-utils-2.4.0.tgz#9983012045d541dcee053e685a27c9c87c840fcd"
integrity sha512-454hrj5gz/FXcUE20ygfEiN4DxZ1sprUo0V1gqIqkNZ/CzoEzAZEll2uxMsuyz6BYjiQan4Aa65xbTemfzW9hQ==
dependencies:
schema-utils "*"
"@types/unist@^2.0.0", "@types/unist@^2.0.2":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
@ -1755,6 +1914,11 @@ arr-union@^3.1.0:
resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
array-find-index@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=
array-includes@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348"
@ -1838,6 +2002,11 @@ async-each@^1.0.1:
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
at-least-node@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
atob@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
@ -1863,6 +2032,22 @@ babel-plugin-dynamic-import-node@^2.3.3:
dependencies:
object.assign "^4.1.0"
babel-plugin-react-intl@^7.0.0:
version "7.9.4"
resolved "https://registry.yarnpkg.com/babel-plugin-react-intl/-/babel-plugin-react-intl-7.9.4.tgz#1fc9ab50470d41b934df50d8f436578ee1732cb0"
integrity sha512-cMKrHEXrw43yT4M89Wbgq8A8N8lffSquj1Piwov/HVukR7jwOw8gf9btXNsQhT27ccyqEwy+M286JQYy0jby2g==
dependencies:
"@babel/core" "^7.9.0"
"@babel/helper-plugin-utils" "^7.8.3"
"@babel/types" "^7.9.5"
"@formatjs/ts-transformer" "^2.6.0"
"@types/babel__core" "^7.1.7"
"@types/fs-extra" "^9.0.1"
"@types/schema-utils" "^2.4.0"
fs-extra "^9.0.0"
intl-messageformat-parser "^5.3.7"
schema-utils "^2.6.6"
babel-plugin-syntax-jsx@6.18.0:
version "6.18.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
@ -2501,6 +2686,11 @@ commander@2, commander@^2.20.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
commander@^6.0.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-6.1.0.tgz#f8d722b78103141006b66f4c7ba1e97315ba75bc"
@ -2867,6 +3057,18 @@ csso@^4.0.2:
dependencies:
css-tree "1.0.0-alpha.39"
csstype@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.3.tgz#2b410bbeba38ba9633353aff34b05d9755d065f8"
integrity sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag==
currently-unhandled@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
integrity sha1-mI3zP+qxke95mmE2nddsF635V+o=
dependencies:
array-find-index "^1.0.1"
cyclist@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
@ -3083,6 +3285,11 @@ detect-browser@^5.1.1:
resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.1.1.tgz#a800db91d3fd60d0861669f5984f1be9ffbe009c"
integrity sha512-5n2aWI57qC3kZaK4j2zYsG6L1LrxgLptGCNhMQgdKhVn6cSdcq43pp6xHPfTHG3TYM6myF4tIPWiZtfdVDgb9w==
detect-indent@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.0.0.tgz#0abd0f549f69fc6659a254fe96786186b6f528fd"
integrity sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==
detect-libc@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
@ -3707,6 +3914,27 @@ extglob@^2.0.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
extract-react-intl-messages@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/extract-react-intl-messages/-/extract-react-intl-messages-4.1.1.tgz#cd01d99053bb053ecc8410ccdccb9ac56daae91c"
integrity sha512-dPogci5X7HVtV7VbUxajH/1YgfNRaW2VtEiVidZ/31Tq8314uzOtzVMNo0IrAPD2E+H1wHoPiu/j565TZsyIZg==
dependencies:
"@babel/core" "^7.9.0"
babel-plugin-react-intl "^7.0.0"
flat "^5.0.0"
glob "^7.1.6"
js-yaml "^3.13.1"
load-json-file "^6.2.0"
lodash.merge "^4.6.2"
lodash.mergewith "^4.6.2"
lodash.pick "^4.4.0"
meow "^6.1.0"
mkdirp "^1.0.3"
pify "^5.0.0"
read-babelrc-up "^1.1.0"
sort-keys "^4.0.0"
write-json-file "^4.3.0"
fast-deep-equal@^3.1.1:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@ -3739,6 +3967,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
fast-memoize@^2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.5.2.tgz#79e3bb6a4ec867ea40ba0e7146816f6cdce9b57e"
integrity sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==
fastest-levenshtein@^1.0.12:
version "1.0.12"
resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2"
@ -3841,6 +4074,11 @@ flat-cache@^2.0.1:
rimraf "2.6.3"
write "1.0.3"
flat@^5.0.0:
version "5.0.2"
resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241"
integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
flatted@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
@ -3905,6 +4143,16 @@ from2@^2.1.0:
inherits "^2.0.1"
readable-stream "^2.0.0"
fs-extra@^9.0.0:
version "9.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc"
integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==
dependencies:
at-least-node "^1.0.0"
graceful-fs "^4.2.0"
jsonfile "^6.0.1"
universalify "^1.0.0"
fs-minipass@^1.2.5:
version "1.2.7"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7"
@ -4095,7 +4343,7 @@ gonzales-pe@^4.3.0:
dependencies:
minimist "^1.2.5"
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.2.2:
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2:
version "4.2.4"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
@ -4201,7 +4449,7 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.3.0:
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@ -4409,6 +4657,28 @@ internal-slot@^1.0.2:
has "^1.0.3"
side-channel "^1.0.2"
intl-messageformat-parser@^5.3.7:
version "5.5.1"
resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-5.5.1.tgz#f09a692755813e6220081e3374df3fb1698bd0c6"
integrity sha512-TvB3LqF2VtP6yI6HXlRT5TxX98HKha6hCcrg9dwlPwNaedVNuQA9KgBdtWKgiyakyCTYHQ+KJeFEstNKfZr64w==
dependencies:
"@formatjs/intl-numberformat" "^5.5.2"
intl-messageformat-parser@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-6.0.5.tgz#098b052ac2714101b4da06fd45d68199d3abd131"
integrity sha512-4aO/RTUtzWiV/naqif4ubwz8P7THOxhraN6XmQpgXj4mdGjtPNO2j3vKlEDgAvv4BEi12R/JCHfLf7SUyfPKog==
dependencies:
"@formatjs/ecma402-abstract" "^1.2.0"
intl-messageformat@^9.3.6:
version "9.3.6"
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-9.3.6.tgz#6b15bca5ebbd81808cf703423c34fb789cf1da8e"
integrity sha512-ZmaPVtB1i0Ao64sI+kCl+uAqlHGn1KyHHPYw2W/cd4q00ACDBpdeqeD3y4tQnMXMGZriwbSn90dJ+bvSkQr1dA==
dependencies:
fast-memoize "^2.5.2"
intl-messageformat-parser "^6.0.5"
invariant@^2.2.2, invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
@ -4779,6 +5049,13 @@ json-stable-stringify-without-jsonify@^1.0.1:
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
json-stable-stringify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=
dependencies:
jsonify "~0.0.0"
json5@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
@ -4793,6 +5070,20 @@ json5@^2.1.0, json5@^2.1.2:
dependencies:
minimist "^1.2.5"
jsonfile@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179"
integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==
dependencies:
universalify "^1.0.0"
optionalDependencies:
graceful-fs "^4.1.6"
jsonify@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
jsx-ast-utils@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz#1114a4c1209481db06c690c2b4f488cc665f657e"
@ -4925,6 +5216,16 @@ load-json-file@^4.0.0:
pify "^3.0.0"
strip-bom "^3.0.0"
load-json-file@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-6.2.0.tgz#5c7770b42cafa97074ca2848707c61662f4251a1"
integrity sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==
dependencies:
graceful-fs "^4.1.15"
parse-json "^5.0.0"
strip-bom "^4.0.0"
type-fest "^0.6.0"
loader-runner@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357"
@ -4982,11 +5283,21 @@ lodash._reinterpolate@^3.0.0:
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
lodash.merge@^4.6.0:
lodash.merge@^4.6.0, lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash.mergewith@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
lodash.pick@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
@ -5054,6 +5365,14 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
loud-rejection@*, loud-rejection@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-2.2.0.tgz#4255eb6e9c74045b0edc021fa7397ab655a8517c"
integrity sha512-S0FayMXku80toa5sZ6Ro4C+s+EtFDCsyJNG/AzFMfX3AxD5Si4dZsgzm/kKnbOxHl5Cv8jBlno8+3XYIh2pNjQ==
dependencies:
currently-unhandled "^0.4.1"
signal-exit "^3.0.2"
lru-cache@6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@ -5090,7 +5409,7 @@ make-dir@^2.0.0:
pify "^4.0.1"
semver "^5.6.0"
make-dir@^3.0.2:
make-dir@^3.0.0, make-dir@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
@ -5195,6 +5514,23 @@ memorystream@^0.3.1:
resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI=
meow@^6.1.0:
version "6.1.1"
resolved "https://registry.yarnpkg.com/meow/-/meow-6.1.1.tgz#1ad64c4b76b2a24dfb2f635fddcadf320d251467"
integrity sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==
dependencies:
"@types/minimist" "^1.2.0"
camelcase-keys "^6.2.2"
decamelize-keys "^1.1.0"
hard-rejection "^2.1.0"
minimist-options "^4.0.2"
normalize-package-data "^2.5.0"
read-pkg-up "^7.0.1"
redent "^3.0.0"
trim-newlines "^3.0.0"
type-fest "^0.13.1"
yargs-parser "^18.1.3"
meow@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/meow/-/meow-7.1.1.tgz#7c01595e3d337fcb0ec4e8eed1666ea95903d306"
@ -5293,7 +5629,7 @@ minimatch@^3.0.4:
dependencies:
brace-expansion "^1.1.7"
minimist-options@4.1.0:
minimist-options@4.1.0, minimist-options@^4.0.2:
version "4.1.0"
resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==
@ -6114,6 +6450,11 @@ pify@^4.0.1:
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
pify@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f"
integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==
pkg-dir@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
@ -6781,6 +7122,23 @@ react-fast-compare@^2.0.1:
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-intl@^5.8.0:
version "5.8.0"
resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-5.8.0.tgz#4d365ee992b35cdb81576abd2fb06e4d78a8e461"
integrity sha512-03FHg9u9gW+fc9zyVQS0WwZc3AkIzwRVE73O6FJx10ZCJ5XDDHWzgNCK6H65rX0Hq9+Hw9m7IJiU6YIvV3xLFw==
dependencies:
"@formatjs/ecma402-abstract" "^1.2.0"
"@formatjs/intl" "^1.3.0"
"@formatjs/intl-displaynames" "^3.3.6"
"@formatjs/intl-listformat" "^4.2.5"
"@formatjs/intl-relativetimeformat" "^7.2.5"
"@types/hoist-non-react-statics" "^3.3.1"
fast-memoize "^2.5.2"
hoist-non-react-statics "^3.3.2"
intl-messageformat "^9.3.6"
intl-messageformat-parser "^6.0.5"
shallow-equal "^1.2.1"
react-is@16.13.1, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.9.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@ -6845,6 +7203,14 @@ react@^16.13.1:
object-assign "^4.1.1"
prop-types "^15.6.2"
read-babelrc-up@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/read-babelrc-up/-/read-babelrc-up-1.1.0.tgz#10fd5baaf6ca03eaba6748fa65ddae25bca61e70"
integrity sha512-fcl0JeI85Ss3//kfC3z2rsG2VxSiHl1bJgpjQWrne2YuQEewZpAgAjb17A6q/Q3ozWeZsUSroiIBVsnjmOU8vw==
dependencies:
find-up "^4.1.0"
json5 "^2.1.2"
read-cache@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
@ -7318,6 +7684,15 @@ scheduler@^0.19.1:
loose-envify "^1.1.0"
object-assign "^4.1.1"
schema-utils@*, schema-utils@^2.6.1, schema-utils@^2.6.6:
version "2.7.1"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
dependencies:
"@types/json-schema" "^7.0.5"
ajv "^6.12.4"
ajv-keywords "^3.5.2"
schema-utils@2.6.6:
version "2.6.6"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.6.tgz#299fe6bd4a3365dc23d99fd446caff8f1d6c330c"
@ -7335,15 +7710,6 @@ schema-utils@^1.0.0:
ajv-errors "^1.0.0"
ajv-keywords "^3.1.0"
schema-utils@^2.6.1, schema-utils@^2.6.6:
version "2.7.1"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
dependencies:
"@types/json-schema" "^7.0.5"
ajv "^6.12.4"
ajv-keywords "^3.5.2"
semver-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
@ -7426,6 +7792,11 @@ shallow-clone@^3.0.0:
dependencies:
kind-of "^6.0.2"
shallow-equal@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da"
integrity sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@ -7530,6 +7901,13 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0"
use "^3.1.0"
sort-keys@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-4.0.0.tgz#56dc5e256637bfe3fec8db0dc57c08b1a2be22d6"
integrity sha512-hlJLzrn/VN49uyNkZ8+9b+0q9DjmmYcYOnbMQtpkLrYpPwRApDPZfmqbUfJnAA3sb/nRib+nDot7Zi/1ER1fuA==
dependencies:
is-plain-obj "^2.0.0"
source-list-map@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
@ -7857,6 +8235,11 @@ strip-bom@^3.0.0:
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
strip-bom@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
strip-final-newline@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
@ -8356,6 +8739,11 @@ typescript@^3.9.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa"
integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==
typescript@^4.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2"
integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==
unfetch@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.1.0.tgz#6ec2dd0de887e58a4dee83a050ded80ffc4137db"
@ -8476,6 +8864,11 @@ unist-util-visit@^2.0.0:
unist-util-is "^4.0.0"
unist-util-visit-parents "^3.0.0"
universalify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d"
integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==
unquote@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544"
@ -8763,7 +9156,7 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
write-file-atomic@^3.0.3:
write-file-atomic@^3.0.0, write-file-atomic@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
@ -8773,6 +9166,18 @@ write-file-atomic@^3.0.3:
signal-exit "^3.0.2"
typedarray-to-buffer "^3.1.5"
write-json-file@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-4.3.0.tgz#908493d6fd23225344af324016e4ca8f702dd12d"
integrity sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ==
dependencies:
detect-indent "^6.0.0"
graceful-fs "^4.1.15"
is-plain-obj "^2.0.0"
make-dir "^3.0.0"
sort-keys "^4.0.0"
write-file-atomic "^3.0.0"
write@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"