diff --git a/components/common/DropDown.js b/components/common/DropDown.js index b6b2357b..d240fbd6 100644 --- a/components/common/DropDown.js +++ b/components/common/DropDown.js @@ -37,7 +37,7 @@ export default function DropDown({ return (
- {options.find(e => e.value === value)?.label || value} +
{options.find(e => e.value === value)?.label || value}
} className={styles.icon} size="small" />
{showMenu && ( diff --git a/components/common/Dropdown.module.css b/components/common/Dropdown.module.css index 4b94f58f..9738b007 100644 --- a/components/common/Dropdown.module.css +++ b/components/common/Dropdown.module.css @@ -19,6 +19,10 @@ min-width: 160px; } +.text { + flex: 1; +} + .icon { padding-left: 20px; } diff --git a/components/common/UpdateNotice.js b/components/common/UpdateNotice.js new file mode 100644 index 00000000..27ec562a --- /dev/null +++ b/components/common/UpdateNotice.js @@ -0,0 +1,47 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import useVersion from 'hooks/useVersion'; +import styles from './UpdateNotice.module.css'; +import ButtonLayout from '../layout/ButtonLayout'; +import Button from './Button'; +import useForceUpdate from '../../hooks/useForceUpdate'; + +export default function UpdateNotice() { + const forceUpdte = useForceUpdate(); + const { hasUpdate, latest, updateCheck } = useVersion(); + + function handleViewClick() { + location.href = 'https://github.com/mikecao/umami/releases'; + updateCheck(); + forceUpdte(); + } + + function handleDismissClick() { + updateCheck(); + forceUpdte(); + } + + if (!hasUpdate) { + return null; + } + + return ( +
+
+ +
+ + + + +
+ ); +} diff --git a/components/common/UpdateNotice.module.css b/components/common/UpdateNotice.module.css new file mode 100644 index 00000000..52a97c3b --- /dev/null +++ b/components/common/UpdateNotice.module.css @@ -0,0 +1,13 @@ +.notice { + display: flex; + justify-content: center; + align-items: center; + padding-top: 10px; + font-size: var(--font-size-small); + font-weight: 600; +} + +.message { + text-align: center; + margin-right: 20px; +} diff --git a/components/common/WorldMap.js b/components/common/WorldMap.js index f6eec8d6..a24f7400 100644 --- a/components/common/WorldMap.js +++ b/components/common/WorldMap.js @@ -6,6 +6,8 @@ import tinycolor from 'tinycolor2'; import useTheme from 'hooks/useTheme'; import { THEME_COLORS } from 'lib/constants'; import styles from './WorldMap.module.css'; +import useCountryNames from 'hooks/useCountryNames'; +import useLocale from 'hooks/useLocale'; const geoUrl = '/world-110m.json'; @@ -21,6 +23,8 @@ export default function WorldMap({ data, className }) { }), [theme], ); + const [locale] = useLocale(); + const countryNames = useCountryNames(locale); function getFillColor(code) { if (code === 'AQ') return; @@ -39,10 +43,10 @@ export default function WorldMap({ data, className }) { return code === 'AQ' ? 0 : 1; } - function handleHover({ ISO_A2: code, NAME: name }) { + function handleHover(code) { if (code === 'AQ') return; const country = data?.find(({ x }) => x === code); - setTooltip(`${name}: ${country?.y || 0} visitors`); + setTooltip(`${countryNames[code]}: ${country?.y || 0} visitors`); } return ( @@ -70,7 +74,7 @@ export default function WorldMap({ data, className }) { hover: { outline: 'none', fill: colors.hoverColor }, pressed: { outline: 'none' }, }} - onMouseOver={() => handleHover(geo.properties)} + onMouseOver={() => handleHover(code)} onMouseOut={() => setTooltip(null)} /> ); diff --git a/components/forms/AccountEditForm.js b/components/forms/AccountEditForm.js index 16c6fd3f..ccce3463 100644 --- a/components/forms/AccountEditForm.js +++ b/components/forms/AccountEditForm.js @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Formik, Form, Field } from 'formik'; +import { useRouter } from 'next/router'; import { post } from 'lib/web'; import Button from 'components/common/Button'; import FormLayout, { @@ -29,18 +30,17 @@ const validate = ({ user_id, username, password }) => { }; export default function AccountEditForm({ values, onSave, onClose }) { + const { basePath } = useRouter(); const [message, setMessage] = useState(); const handleSubmit = async values => { - const response = await post(`/api/account`, values); + const { ok, data } = await post(`${basePath}/api/account`, values); - if (typeof response !== 'string') { + if (ok) { onSave(); } else { setMessage( - response || ( - - ), + data || , ); } }; diff --git a/components/forms/ChangePasswordForm.js b/components/forms/ChangePasswordForm.js index e2f225b7..c41b6e6b 100644 --- a/components/forms/ChangePasswordForm.js +++ b/components/forms/ChangePasswordForm.js @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; +import { useRouter } from 'next/router'; import { Formik, Form, Field } from 'formik'; import { post } from 'lib/web'; import Button from 'components/common/Button'; @@ -37,18 +38,17 @@ const validate = ({ current_password, new_password, confirm_password }) => { }; export default function ChangePasswordForm({ values, onSave, onClose }) { + const { basePath } = useRouter(); const [message, setMessage] = useState(); const handleSubmit = async values => { - const response = await post(`/api/account/password`, values); + const { ok, data } = await post(`${basePath}/api/account/password`, values); - if (typeof response !== 'string') { + if (ok) { onSave(); } else { setMessage( - response || ( - - ), + data || , ); } }; diff --git a/components/forms/DeleteForm.js b/components/forms/DeleteForm.js index f53b286f..689cd1fc 100644 --- a/components/forms/DeleteForm.js +++ b/components/forms/DeleteForm.js @@ -1,4 +1,6 @@ import React, { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { useRouter } from 'next/router'; import { Formik, Form, Field } from 'formik'; import { del } from 'lib/web'; import Button from 'components/common/Button'; @@ -8,7 +10,6 @@ import FormLayout, { FormMessage, FormRow, } from 'components/layout/FormLayout'; -import { FormattedMessage } from 'react-intl'; const CONFIRMATION_WORD = 'DELETE'; @@ -27,15 +28,18 @@ const validate = ({ confirmation }) => { }; export default function DeleteForm({ values, onSave, onClose }) { + const { basePath } = useRouter(); const [message, setMessage] = useState(); const handleSubmit = async ({ type, id }) => { - const response = await del(`/api/${type}/${id}`); + const { ok, data } = await del(`${basePath}/api/${type}/${id}`); - if (typeof response !== 'string') { + if (ok) { onSave(); } else { - setMessage(); + setMessage( + data || , + ); } }; diff --git a/components/forms/LoginForm.js b/components/forms/LoginForm.js index a34a4474..3866f240 100644 --- a/components/forms/LoginForm.js +++ b/components/forms/LoginForm.js @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Formik, Form, Field } from 'formik'; -import Router from 'next/router'; +import { useRouter } from 'next/router'; import { post } from 'lib/web'; import Button from 'components/common/Button'; import FormLayout, { @@ -28,22 +28,26 @@ const validate = ({ username, password }) => { }; export default function LoginForm() { + const router = useRouter(); const [message, setMessage] = useState(); const handleSubmit = async ({ username, password }) => { - const response = await post('/api/auth/login', { username, password }); + const { ok, status, data } = await post(`${router.basePath}/api/auth/login`, { + username, + password, + }); - if (typeof response !== 'string') { - await Router.push('/'); + if (ok) { + return router.push('/'); } else { setMessage( - response.startsWith('401') ? ( + status === 401 ? ( ) : ( - response + data ), ); } diff --git a/components/forms/WebsiteEditForm.js b/components/forms/WebsiteEditForm.js index 7d405e52..3432c2a4 100644 --- a/components/forms/WebsiteEditForm.js +++ b/components/forms/WebsiteEditForm.js @@ -11,6 +11,7 @@ import FormLayout, { } from 'components/layout/FormLayout'; import Checkbox from 'components/common/Checkbox'; import { DOMAIN_REGEX } from 'lib/constants'; +import { useRouter } from 'next/router'; const initialValues = { name: '', @@ -34,15 +35,18 @@ const validate = ({ name, domain }) => { }; export default function WebsiteEditForm({ values, onSave, onClose }) { + const { basePath } = useRouter(); const [message, setMessage] = useState(); const handleSubmit = async values => { - const response = await post(`/api/website`, values); + const { ok, data } = await post(`${basePath}/api/website`, values); - if (typeof response !== 'string') { + if (ok) { onSave(); } else { - setMessage(); + setMessage( + data || , + ); } }; diff --git a/components/layout/Header.js b/components/layout/Header.js index 639db7ea..c48fdd11 100644 --- a/components/layout/Header.js +++ b/components/layout/Header.js @@ -6,6 +6,7 @@ import Link from 'components/common/Link'; import Icon from 'components/common/Icon'; import LanguageButton from 'components/settings/LanguageButton'; import ThemeButton from 'components/settings/ThemeButton'; +import UpdateNotice from 'components/common/UpdateNotice'; import UserButton from 'components/settings/UserButton'; import Logo from 'assets/logo.svg'; import styles from './Header.module.css'; @@ -15,6 +16,7 @@ export default function Header() { return (
+ {user?.is_admin && }
diff --git a/components/layout/Page.js b/components/layout/Page.js index c9a928c5..28492ddf 100644 --- a/components/layout/Page.js +++ b/components/layout/Page.js @@ -1,6 +1,7 @@ import React from 'react'; +import classNames from 'classnames'; import styles from './Page.module.css'; -export default function Page({ children }) { - return
{children}
; +export default function Page({ className, children }) { + return
{children}
; } diff --git a/components/layout/PageHeader.module.css b/components/layout/PageHeader.module.css index 74f7d1a2..263bd5b7 100644 --- a/components/layout/PageHeader.module.css +++ b/components/layout/PageHeader.module.css @@ -4,4 +4,5 @@ align-items: center; align-content: center; min-height: 80px; + align-self: stretch; } diff --git a/components/metrics/CountriesTable.js b/components/metrics/CountriesTable.js index 58548d06..d562b464 100644 --- a/components/metrics/CountriesTable.js +++ b/components/metrics/CountriesTable.js @@ -1,9 +1,18 @@ import React from 'react'; import MetricsTable from './MetricsTable'; -import { countryFilter, percentFilter } from 'lib/filters'; +import { percentFilter } from 'lib/filters'; import { FormattedMessage } from 'react-intl'; +import useCountryNames from 'hooks/useCountryNames'; +import useLocale from 'hooks/useLocale'; export default function CountriesTable({ websiteId, token, limit, onDataLoad = () => {} }) { + const [locale] = useLocale(); + const countryNames = useCountryNames(locale); + + function renderLabel({ x }) { + return
{countryNames[x]}
; + } + return ( } @@ -12,8 +21,8 @@ export default function CountriesTable({ websiteId, token, limit, onDataLoad = ( websiteId={websiteId} token={token} limit={limit} - dataFilter={countryFilter} onDataLoad={data => onDataLoad(percentFilter(data))} + renderLabel={renderLabel} /> ); } diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js index 99c03951..ea86ad3e 100644 --- a/components/metrics/WebsiteChart.js +++ b/components/metrics/WebsiteChart.js @@ -59,7 +59,7 @@ export default function WebsiteChart({ } return ( - <> +
- +
); } diff --git a/components/metrics/WebsiteChart.module.css b/components/metrics/WebsiteChart.module.css index 29f94670..0e947aea 100644 --- a/components/metrics/WebsiteChart.module.css +++ b/components/metrics/WebsiteChart.module.css @@ -1,6 +1,7 @@ .container { display: flex; flex-direction: column; + align-self: stretch; } .title { diff --git a/components/settings/Settings.js b/components/pages/Settings.js similarity index 85% rename from components/settings/Settings.js rename to components/pages/Settings.js index be9feb35..35d039df 100644 --- a/components/settings/Settings.js +++ b/components/pages/Settings.js @@ -2,9 +2,9 @@ import React, { useState } from 'react'; import { useRouter } from 'next/router'; import Page from 'components/layout/Page'; import MenuLayout from 'components/layout/MenuLayout'; -import WebsiteSettings from './WebsiteSettings'; -import AccountSettings from './AccountSettings'; -import ProfileSettings from './ProfileSettings'; +import WebsiteSettings from '../settings/WebsiteSettings'; +import AccountSettings from '../settings/AccountSettings'; +import ProfileSettings from '../settings/ProfileSettings'; import { useSelector } from 'react-redux'; import { FormattedMessage } from 'react-intl'; @@ -26,7 +26,7 @@ export default function Settings() { { label: , value: ACCOUNTS, - hidden: !user.is_admin, + hidden: !user?.is_admin, }, { label: , diff --git a/components/pages/Test.module.css b/components/pages/Test.module.css new file mode 100644 index 00000000..78977c55 --- /dev/null +++ b/components/pages/Test.module.css @@ -0,0 +1,5 @@ +.test { + border: 1px solid var(--gray200); + border-radius: 5px; + padding: 0 20px 20px 20px; +} diff --git a/components/pages/TestConsole.js b/components/pages/TestConsole.js new file mode 100644 index 00000000..f6fa8a23 --- /dev/null +++ b/components/pages/TestConsole.js @@ -0,0 +1,94 @@ +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; +import classNames from 'classnames'; +import Head from 'next/head'; +import Link from 'next/link'; +import Page from '../layout/Page'; +import PageHeader from '../layout/PageHeader'; +import useFetch from '../../hooks/useFetch'; +import DropDown from '../common/DropDown'; +import styles from './Test.module.css'; +import WebsiteChart from '../metrics/WebsiteChart'; +import EventsChart from '../metrics/EventsChart'; +import Button from '../common/Button'; +import EmptyPlaceholder from '../common/EmptyPlaceholder'; + +export default function TestConsole() { + const user = useSelector(state => state.user); + const [website, setWebsite] = useState(); + const { data } = useFetch('/api/websites'); + + if (!data || !user?.is_admin) { + return null; + } + + const options = data.map(({ name, website_id }) => ({ label: name, value: website_id })); + const selectedValue = options.find(({ value }) => value === website?.website_id)?.value; + + function handleSelect(value) { + setWebsite(data.find(({ website_id }) => website_id === value)); + } + + function handleClick() { + window.umami('event (default)'); + window.umami.trackView('/page-view', 'https://www.google.com'); + window.umami.trackEvent('event (custom)', 'custom-type'); + } + + return ( + + + {typeof window !== 'undefined' && website && ( +