diff --git a/.gitignore b/.gitignore index 3fd77017..72ff2985 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ # production /build /public/umami.js +/lang-compiled # misc .DS_Store diff --git a/assets/globe.svg b/assets/globe.svg new file mode 100644 index 00000000..509eaba6 --- /dev/null +++ b/assets/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/WebsiteDetails.js b/components/WebsiteDetails.js index bd8025ee..af2a61a9 100644 --- a/components/WebsiteDetails.js +++ b/components/WebsiteDetails.js @@ -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)} > -
Back
+
+ +
); @@ -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: , + value: 'url', + component: PagesTable, + }, + { + label: , + value: 'referrer', + component: ReferrersTable, + }, + { + label: , + value: 'browser', + component: BrowsersTable, + }, + { + label: , + value: 'os', + component: OSTable, + }, + { + label: , + value: 'device', + component: DevicesTable, + }, + { + label: , + value: 'country', + component: CountriesTable, + }, + { + label: , + value: 'event', + component: EventsTable, + }, ]; const tableProps = { diff --git a/components/WebsiteList.js b/components/WebsiteList.js index 59573b96..f2a8c3db 100644 --- a/components/WebsiteList.js +++ b/components/WebsiteList.js @@ -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() { ))} {data.length === 0 && ( - + + } + > )} diff --git a/components/common/ButtonGroup.js b/components/common/ButtonGroup.js index 26707fb9..c91bb743 100644 --- a/components/common/ButtonGroup.js +++ b/components/common/ButtonGroup.js @@ -13,17 +13,20 @@ export default function ButtonGroup({ }) { return (
- {items.map(item => ( - - ))} + {items.map(item => { + const { label, value } = item; + return ( + + ); + })}
); } diff --git a/components/common/CopyButton.js b/components/common/CopyButton.js index 8d6b5db6..399da90d 100644 --- a/components/common/CopyButton.js +++ b/components/common/CopyButton.js @@ -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 = ( + +); 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(); window.getSelection().removeAllRanges(); } } diff --git a/components/common/DateFilter.js b/components/common/DateFilter.js index 7b7838dd..aaba8725 100644 --- a/components/common/DateFilter.js +++ b/components/common/DateFilter.js @@ -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: ( + + ), + value: '24hour', + }, + { + label: ( + + ), + value: '7day', + }, + { + label: ( + + ), + value: '30day', + }, + { + label: ( + + ), + value: '90day', + }, + { label: , value: '1day' }, + { label: , value: '1week' }, + { + label: , + value: '1month', + }, + { label: , value: '1year' }, ]; export default function DateFilter({ value, onChange, className }) { diff --git a/components/common/LanguageButton.js b/components/common/LanguageButton.js new file mode 100644 index 00000000..51903afe --- /dev/null +++ b/components/common/LanguageButton.js @@ -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 ( +
+ + {showMenu && ( + + )} +
+ ); +} diff --git a/components/common/LanguageButton.module.css b/components/common/LanguageButton.module.css new file mode 100644 index 00000000..b01f5d39 --- /dev/null +++ b/components/common/LanguageButton.module.css @@ -0,0 +1,5 @@ +.container { + display: flex; + position: relative; + cursor: pointer; +} diff --git a/components/common/Modal.module.css b/components/common/Modal.module.css index a164f1e8..9ca0d778 100644 --- a/components/common/Modal.module.css +++ b/components/common/Modal.module.css @@ -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); diff --git a/components/common/UserButton.js b/components/common/UserButton.js index aed05d9c..483082a8 100644 --- a/components/common/UserButton.js +++ b/components/common/UserButton.js @@ -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 {user.username} - + {user.username} }} + /> ), value: 'username', className: styles.username, }, - { label: 'Logout', value: 'logout' }, + { label: , value: 'logout' }, ]; function handleSelect(value) { diff --git a/components/forms/AccountEditForm.js b/components/forms/AccountEditForm.js index 96eea3df..16c6fd3f 100644 --- a/components/forms/AccountEditForm.js +++ b/components/forms/AccountEditForm.js @@ -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 = ; } if (!user_id && !password) { - errors.password = 'Required'; + errors.password = ; } 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 || ( + + ), + ); } }; @@ -50,20 +55,26 @@ export default function AccountEditForm({ values, onSave, onClose }) { {() => (
- + - + + - {message}
diff --git a/components/forms/ChangePasswordForm.js b/components/forms/ChangePasswordForm.js index f3e5927a..e2f225b7 100644 --- a/components/forms/ChangePasswordForm.js +++ b/components/forms/ChangePasswordForm.js @@ -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 = ; } if (!new_password) { - errors.new_password = 'Required'; + errors.new_password = ; } if (!confirm_password) { - errors.confirm_password = 'Required'; + errors.confirm_password = ; } else if (new_password !== confirm_password) { - errors.confirm_password = `Passwords don't match`; + errors.confirm_password = ( + + ); } 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 || ( + + ), + ); } }; @@ -56,25 +63,33 @@ export default function ChangePasswordForm({ values, onSave, onClose }) { {() => (
- + - + - + + - {message}
diff --git a/components/forms/DeleteForm.js b/components/forms/DeleteForm.js index 650e802f..1ba81626 100644 --- a/components/forms/DeleteForm.js +++ b/components/forms/DeleteForm.js @@ -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 ? ( + + ) : ( + + ); } return errors; @@ -28,7 +33,7 @@ export default function DeleteForm({ values, onSave, onClose }) { if (typeof response !== 'string') { onSave(); } else { - setMessage('Something went wrong'); + setMessage(); } }; @@ -42,11 +47,24 @@ export default function DeleteForm({ values, onSave, onClose }) { {() => (
- Are your sure you want to delete {values.name}? + {values.name} }} + /> +
+
+
-
All associated data will be deleted as well.

- Type DELETE in the box below to confirm. + DELETE }} + />

@@ -54,9 +72,11 @@ export default function DeleteForm({ values, onSave, onClose }) { + - {message}
diff --git a/components/forms/LoginForm.js b/components/forms/LoginForm.js index 78e0480e..a34a4474 100644 --- a/components/forms/LoginForm.js +++ b/components/forms/LoginForm.js @@ -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 = ; } if (!password) { - errors.password = 'Required'; + errors.password = ; } 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') ? ( + + ) : ( + response + ), + ); } }; @@ -54,18 +64,22 @@ export default function LoginForm() { } size="xlarge" className={styles.icon} />

umami

- + - + {message} diff --git a/components/forms/ShareUrlForm.js b/components/forms/ShareUrlForm.js index 16a9ca4b..ea162f67 100644 --- a/components/forms/ShareUrlForm.js +++ b/components/forms/ShareUrlForm.js @@ -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 (

- This is the publicly shared URL for {values.name}. + {values.name} }} + />