From 33ac0266378f5c6ac76121e65733eafebe4aba5a Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 19 Sep 2020 10:35:05 -0700 Subject: [PATCH] Added timezone and default date range settings. --- components/common/LanguageButton.js | 2 - components/common/RefreshButton.js | 2 +- components/metrics/EventsChart.js | 9 ++-- components/metrics/MetricsBar.js | 2 +- components/metrics/MetricsTable.js | 2 +- components/metrics/WebsiteChart.js | 17 +++---- components/settings/DateRangeSetting.js | 26 ++++++++++ .../settings/DateRangeSetting.module.css | 3 ++ components/settings/ProfileSettings.js | 34 ++++--------- .../settings/ProfileSettings.module.css | 2 +- components/settings/TimezoneSetting.js | 31 ++++++++++++ .../settings/TimezoneSetting.module.css | 8 +++ hooks/useDateRange.js | 49 +++++++++++++------ hooks/useForceUpdate.js | 9 ++++ hooks/useLocale.js | 3 ++ hooks/useTimezone.js | 17 +++++++ lang/de-DE.json | 1 + lang/en-US.json | 1 + lang/es-MX.json | 1 + lang/fr-FR.json | 1 + lang/ja-JP.json | 1 + lang/mn-MN.json | 1 + lang/nl-NL.json | 1 + lang/ru-RU.json | 1 + lang/tr-TR.json | 1 + lang/zh-CN.json | 1 + lib/constants.js | 5 ++ package.json | 3 +- redux/actions/app.js | 3 +- yarn.lock | 12 +++++ 30 files changed, 186 insertions(+), 63 deletions(-) create mode 100644 components/settings/DateRangeSetting.js create mode 100644 components/settings/DateRangeSetting.module.css create mode 100644 components/settings/TimezoneSetting.js create mode 100644 components/settings/TimezoneSetting.module.css create mode 100644 hooks/useForceUpdate.js create mode 100644 hooks/useTimezone.js diff --git a/components/common/LanguageButton.js b/components/common/LanguageButton.js index 6fe7eb83..fae90415 100644 --- a/components/common/LanguageButton.js +++ b/components/common/LanguageButton.js @@ -3,7 +3,6 @@ import Head from 'next/head'; import Menu from './Menu'; import Button from './Button'; import { menuOptions } from 'lib/lang'; -import { setItem } from 'lib/web'; import useLocale from 'hooks/useLocale'; import useDocumentClick from 'hooks/useDocumentClick'; import Globe from 'assets/globe.svg'; @@ -17,7 +16,6 @@ export default function LanguageButton({ menuPosition = 'bottom', menuAlign = 'l function handleSelect(value) { setLocale(value); - setItem('umami.locale', value); setShowMenu(false); } diff --git a/components/common/RefreshButton.js b/components/common/RefreshButton.js index 1fa02f48..af754a9c 100644 --- a/components/common/RefreshButton.js +++ b/components/common/RefreshButton.js @@ -10,7 +10,7 @@ import { getDateRange } from '../../lib/date'; export default function RefreshButton({ websiteId }) { const dispatch = useDispatch(); - const dateRange = useDateRange(websiteId); + const [dateRange] = useDateRange(websiteId); const [loading, setLoading] = useState(false); const completed = useSelector(state => state.queries[`/api/website/${websiteId}/metrics`]); diff --git a/components/metrics/EventsChart.js b/components/metrics/EventsChart.js index 4a49d129..48fb6f2b 100644 --- a/components/metrics/EventsChart.js +++ b/components/metrics/EventsChart.js @@ -1,9 +1,10 @@ import React, { useMemo } from 'react'; import tinycolor from 'tinycolor2'; import BarChart from './BarChart'; -import { getTimezone, getDateArray, getDateLength } from 'lib/date'; +import { getDateArray, getDateLength } from 'lib/date'; import useFetch from 'hooks/useFetch'; import useDateRange from 'hooks/useDateRange'; +import useTimezone from 'hooks/useTimezone'; const COLORS = [ '#2680eb', @@ -17,15 +18,17 @@ const COLORS = [ ]; export default function EventsChart({ websiteId, token }) { - const dateRange = useDateRange(websiteId); + const [dateRange] = useDateRange(websiteId); const { startDate, endDate, unit, modified } = dateRange; + const [timezone] = useTimezone(); + const { data } = useFetch( `/api/website/${websiteId}/events`, { start_at: +startDate, end_at: +endDate, unit, - tz: getTimezone(), + tz: timezone, token, }, { update: [modified] }, diff --git a/components/metrics/MetricsBar.js b/components/metrics/MetricsBar.js index 8d128c18..cad4c00e 100644 --- a/components/metrics/MetricsBar.js +++ b/components/metrics/MetricsBar.js @@ -9,7 +9,7 @@ import MetricCard from './MetricCard'; import styles from './MetricsBar.module.css'; export default function MetricsBar({ websiteId, token, className }) { - const dateRange = useDateRange(websiteId); + const [dateRange] = useDateRange(websiteId); const { startDate, endDate, modified } = dateRange; const { data } = useFetch( `/api/website/${websiteId}/metrics`, diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index 560c25f0..eed5fca2 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -29,7 +29,7 @@ export default function MetricsTable({ onDataLoad = () => {}, onExpand = () => {}, }) { - const dateRange = useDateRange(websiteId); + const [dateRange] = useDateRange(websiteId); const { startDate, endDate, modified } = dateRange; const { data } = useFetch( `/api/website/${websiteId}/rankings`, diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js index f2485888..5c6e8988 100644 --- a/components/metrics/WebsiteChart.js +++ b/components/metrics/WebsiteChart.js @@ -1,5 +1,4 @@ import React, { useMemo } from 'react'; -import { useDispatch } from 'react-redux'; import classNames from 'classnames'; import PageviewsChart from './PageviewsChart'; import MetricsBar from './MetricsBar'; @@ -8,8 +7,8 @@ import DateFilter from 'components/common/DateFilter'; import StickyHeader from 'components/helpers/StickyHeader'; import useFetch from 'hooks/useFetch'; import useDateRange from 'hooks/useDateRange'; -import { getDateArray, getDateLength, getTimezone } from 'lib/date'; -import { setDateRange } from 'redux/actions/websites'; +import useTimezone from 'hooks/useTimezone'; +import { getDateArray, getDateLength } from 'lib/date'; import styles from './WebsiteChart.module.css'; export default function WebsiteChart({ @@ -20,9 +19,9 @@ export default function WebsiteChart({ showLink = false, onDataLoad = () => {}, }) { - const dispatch = useDispatch(); - const dateRange = useDateRange(websiteId); + const [dateRange, setDateRange] = useDateRange(websiteId); const { startDate, endDate, unit, value, modified } = dateRange; + const [timezone] = useTimezone(); const { data } = useFetch( `/api/website/${websiteId}/pageviews`, @@ -30,7 +29,7 @@ export default function WebsiteChart({ start_at: +startDate, end_at: +endDate, unit, - tz: getTimezone(), + tz: timezone, token, }, { onDataLoad, update: [modified] }, @@ -46,10 +45,6 @@ export default function WebsiteChart({ return [[], []]; }, [data]); - function handleDateChange(values) { - dispatch(setDateRange(websiteId, values)); - } - return ( <> @@ -67,7 +62,7 @@ export default function WebsiteChart({ value={value} startDate={startDate} endDate={endDate} - onChange={handleDateChange} + onChange={setDateRange} /> diff --git a/components/settings/DateRangeSetting.js b/components/settings/DateRangeSetting.js new file mode 100644 index 00000000..625327b6 --- /dev/null +++ b/components/settings/DateRangeSetting.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import DateFilter from 'components/common/DateFilter'; +import Button from 'components/common/Button'; +import useDateRange from 'hooks/useDateRange'; +import { DEFAULT_DATE_RANGE } from 'lib/constants'; +import { getDateRange } from 'lib/date'; +import styles from './DateRangeSetting.module.css'; + +export default function DateRangeSetting() { + const [dateRange, setDateRange] = useDateRange(); + const { startDate, endDate, value } = dateRange; + + function handleReset() { + setDateRange(getDateRange(DEFAULT_DATE_RANGE)); + } + + return ( + <> + + + + ); +} diff --git a/components/settings/DateRangeSetting.module.css b/components/settings/DateRangeSetting.module.css new file mode 100644 index 00000000..230e7c97 --- /dev/null +++ b/components/settings/DateRangeSetting.module.css @@ -0,0 +1,3 @@ +.button { + margin-left: 10px; +} diff --git a/components/settings/ProfileSettings.js b/components/settings/ProfileSettings.js index 10386318..f28226c5 100644 --- a/components/settings/ProfileSettings.js +++ b/components/settings/ProfileSettings.js @@ -1,40 +1,27 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import PageHeader from 'components/layout/PageHeader'; import Button from 'components/common/Button'; import Modal from 'components/common/Modal'; import Toast from 'components/common/Toast'; import ChangePasswordForm from 'components/forms/ChangePasswordForm'; -import DateFilter from 'components/common/DateFilter'; +import TimezoneSetting from 'components/settings/TimezoneSetting'; import Dots from 'assets/ellipsis-h.svg'; -import { getTimezone } from 'lib/date'; -import { setItem } from 'lib/web'; -import useDateRange from 'hooks/useDateRange'; -import { setDateRange } from 'redux/actions/websites'; import styles from './ProfileSettings.module.css'; +import DateRangeSetting from './DateRangeSetting'; export default function ProfileSettings() { - const dispatch = useDispatch(); const user = useSelector(state => state.user); const [changePassword, setChangePassword] = useState(false); const [message, setMessage] = useState(); const { user_id } = user; - const timezone = getTimezone(); - const dateRange = useDateRange(0); - const { startDate, endDate, value } = dateRange; function handleSave() { setChangePassword(false); setMessage(); } - function handleDateChange(values) { - const { value } = values; - setItem(`umami.date-range`, value === 'custom' ? values : value); - dispatch(setDateRange(0, values)); - } - return ( <> @@ -47,7 +34,7 @@ export default function ProfileSettings() { -
+
@@ -55,17 +42,14 @@ export default function ProfileSettings() {
-
{timezone}
+
+ +
-
- +
+
{changePassword && ( diff --git a/components/settings/ProfileSettings.module.css b/components/settings/ProfileSettings.module.css index ea7711ac..fdd8252f 100644 --- a/components/settings/ProfileSettings.module.css +++ b/components/settings/ProfileSettings.module.css @@ -1,3 +1,3 @@ -.date { +.list dd { display: flex; } diff --git a/components/settings/TimezoneSetting.js b/components/settings/TimezoneSetting.js new file mode 100644 index 00000000..54751a45 --- /dev/null +++ b/components/settings/TimezoneSetting.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { listTimeZones } from 'timezone-support'; +import DropDown from '../common/DropDown'; +import Button from '../common/Button'; +import useTimezone from 'hooks/useTimezone'; +import { getTimezone } from 'lib/date'; +import styles from './TimezoneSetting.module.css'; + +export default function TimezoneSetting() { + const [timezone, saveTimezone] = useTimezone(); + const options = listTimeZones().map(n => ({ label: n, value: n })); + + function handleReset() { + saveTimezone(getTimezone()); + } + + return ( + <> + + + + ); +} diff --git a/components/settings/TimezoneSetting.module.css b/components/settings/TimezoneSetting.module.css new file mode 100644 index 00000000..9561111d --- /dev/null +++ b/components/settings/TimezoneSetting.module.css @@ -0,0 +1,8 @@ +.menu { + max-height: 300px; + overflow-y: auto; +} + +.button { + margin-left: 10px; +} diff --git a/hooks/useDateRange.js b/hooks/useDateRange.js index 13703d9f..77f892de 100644 --- a/hooks/useDateRange.js +++ b/hooks/useDateRange.js @@ -1,24 +1,41 @@ -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { parseISO } from 'date-fns'; import { getDateRange } from 'lib/date'; -import { getItem } from 'lib/web'; +import { getItem, setItem } from 'lib/web'; +import { setDateRange } from '../redux/actions/websites'; +import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants'; +import useForceUpdate from './useForceUpdate'; -export default function useDateRange(websiteId, defaultDateRange = '24hour') { - const globalDefault = getItem('umami.date-range'); +export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_RANGE) { + const dispatch = useDispatch(); + const dateRange = useSelector(state => state.websites[websiteId]?.dateRange); + const forceUpdate = useForceUpdate(); + + const globalDefault = getItem(DATE_RANGE_CONFIG); let globalDateRange; - if (typeof globalDefault === 'string') { - globalDateRange = getDateRange(globalDefault); - } else if (typeof globalDefault === 'object') { - globalDateRange = { - ...globalDefault, - startDate: parseISO(globalDefault.startDate), - endDate: parseISO(globalDefault.endDate), - }; + if (globalDefault) { + if (typeof globalDefault === 'string') { + globalDateRange = getDateRange(globalDefault); + } else if (typeof globalDefault === 'object') { + globalDateRange = { + ...globalDefault, + startDate: parseISO(globalDefault.startDate), + endDate: parseISO(globalDefault.endDate), + }; + } } - return useSelector( - state => - state.websites[websiteId]?.dateRange || globalDateRange || getDateRange(defaultDateRange), - ); + function saveDateRange(values) { + const { value } = values; + + if (websiteId) { + dispatch(setDateRange(websiteId, values)); + } else { + setItem(DATE_RANGE_CONFIG, value === 'custom' ? values : value); + forceUpdate(); + } + } + + return [dateRange || globalDateRange || getDateRange(defaultDateRange), saveDateRange]; } diff --git a/hooks/useForceUpdate.js b/hooks/useForceUpdate.js new file mode 100644 index 00000000..2b8d6101 --- /dev/null +++ b/hooks/useForceUpdate.js @@ -0,0 +1,9 @@ +import { useCallback, useState } from 'react'; + +export default function useForceUpdate() { + const [, update] = useState(Object.create(null)); + + return useCallback(() => { + update(Object.create(null)); + }, [update]); +} diff --git a/hooks/useLocale.js b/hooks/useLocale.js index 185c1f19..788b9b28 100644 --- a/hooks/useLocale.js +++ b/hooks/useLocale.js @@ -1,11 +1,14 @@ import { useDispatch, useSelector } from 'react-redux'; import { updateApp } from 'redux/actions/app'; +import { setItem } from 'lib/web'; +import { LOCALE_CONFIG } from 'lib/constants'; export default function useLocale() { const locale = useSelector(state => state.app.locale); const dispatch = useDispatch(); function setLocale(value) { + setItem(LOCALE_CONFIG, value); dispatch(updateApp({ locale: value })); } diff --git a/hooks/useTimezone.js b/hooks/useTimezone.js new file mode 100644 index 00000000..5de39f9a --- /dev/null +++ b/hooks/useTimezone.js @@ -0,0 +1,17 @@ +import { useState, useCallback } from 'react'; +import { getTimezone } from 'lib/date'; +import { getItem, setItem } from 'lib/web'; + +export default function useTimezone() { + const [timezone, setTimezone] = useState(getItem('umami.timezone') || getTimezone()); + + const saveTimezone = useCallback( + value => { + setItem('umami.timezone', value); + setTimezone(value); + }, + [setTimezone], + ); + + return [timezone, saveTimezone]; +} diff --git a/lang/de-DE.json b/lang/de-DE.json index 75d2b1ea..96ba0f99 100644 --- a/lang/de-DE.json +++ b/lang/de-DE.json @@ -11,6 +11,7 @@ "button.login": "Anmelden", "button.more": "Mehr", "button.refresh": "Aktualisieren", + "button.reset": "Reset", "button.save": "Speichern", "button.single-day": "Ein Tag", "button.view-details": "Details anzeigen", diff --git a/lang/en-US.json b/lang/en-US.json index a583e05c..54ee8cad 100644 --- a/lang/en-US.json +++ b/lang/en-US.json @@ -11,6 +11,7 @@ "button.login": "Login", "button.more": "More", "button.refresh": "Refresh", + "button.reset": "Reset", "button.save": "Save", "button.single-day": "Single day", "button.view-details": "View details", diff --git a/lang/es-MX.json b/lang/es-MX.json index 4a7cdd57..ed8cf1a2 100644 --- a/lang/es-MX.json +++ b/lang/es-MX.json @@ -11,6 +11,7 @@ "button.login": "Iniciar sesión", "button.more": "Más", "button.refresh": "Refresh", + "button.reset": "Reset", "button.save": "Guardar", "button.single-day": "Single day", "button.view-details": "Ver detalles", diff --git a/lang/fr-FR.json b/lang/fr-FR.json index 7536e839..8cfedb33 100644 --- a/lang/fr-FR.json +++ b/lang/fr-FR.json @@ -11,6 +11,7 @@ "button.login": "Connexion", "button.more": "Plus", "button.refresh": "Refresh", + "button.reset": "Reset", "button.save": "Sauvegarder", "button.single-day": "Single day", "button.view-details": "Voir les details", diff --git a/lang/ja-JP.json b/lang/ja-JP.json index a9318697..27d3ca34 100644 --- a/lang/ja-JP.json +++ b/lang/ja-JP.json @@ -11,6 +11,7 @@ "button.login": "ログイン", "button.more": "さらに表示", "button.refresh": "Refresh", + "button.reset": "Reset", "button.save": "保存", "button.single-day": "Single day", "button.view-details": "詳細表示", diff --git a/lang/mn-MN.json b/lang/mn-MN.json index 2a354fe4..0dd543bf 100644 --- a/lang/mn-MN.json +++ b/lang/mn-MN.json @@ -11,6 +11,7 @@ "button.login": "Нэвтрэх", "button.more": "Цааш", "button.refresh": "Refresh", + "button.reset": "Reset", "button.save": "Хадгалах", "button.single-day": "Single day", "button.view-details": "Дэлгэрүүлж харах", diff --git a/lang/nl-NL.json b/lang/nl-NL.json index e26c5266..2c49098c 100644 --- a/lang/nl-NL.json +++ b/lang/nl-NL.json @@ -11,6 +11,7 @@ "button.login": "Inloggen", "button.more": "Toon meer", "button.refresh": "Refresh", + "button.reset": "Reset", "button.save": "Opslaan", "button.single-day": "Single day", "button.view-details": "Meer details", diff --git a/lang/ru-RU.json b/lang/ru-RU.json index 0fd1b449..5cbedf0b 100644 --- a/lang/ru-RU.json +++ b/lang/ru-RU.json @@ -11,6 +11,7 @@ "button.login": "Войти", "button.more": "Больше", "button.refresh": "Refresh", + "button.reset": "Reset", "button.save": "Сохранить", "button.single-day": "Single day", "button.view-details": "Посмотреть детали", diff --git a/lang/tr-TR.json b/lang/tr-TR.json index 36c3b58c..6a93d1dd 100644 --- a/lang/tr-TR.json +++ b/lang/tr-TR.json @@ -11,6 +11,7 @@ "button.login": "Giriş Yap", "button.more": "Detaylı göster", "button.refresh": "Refresh", + "button.reset": "Reset", "button.save": "Kaydet", "button.single-day": "Single day", "button.view-details": "Detayı incele", diff --git a/lang/zh-CN.json b/lang/zh-CN.json index cbcc6f1c..9c6ddda4 100644 --- a/lang/zh-CN.json +++ b/lang/zh-CN.json @@ -11,6 +11,7 @@ "button.login": "登录", "button.more": "更多", "button.refresh": "刷新", + "button.reset": "Reset", "button.save": "保存", "button.single-day": "单日", "button.view-details": "查看更多", diff --git a/lib/constants.js b/lib/constants.js index e9641c68..0e2468ab 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -1,4 +1,9 @@ export const AUTH_COOKIE_NAME = 'umami.auth'; +export const LOCALE_CONFIG = 'umami.locale'; +export const TIMEZONE_CONFIG = 'umami.timezone'; +export const DATE_RANGE_CONFIG = 'umami.date-range'; + +export const DEFAULT_DATE_RANGE = '24hour'; export const POSTGRESQL = 'postgresql'; export const MYSQL = 'mysql'; diff --git a/package.json b/package.json index 37209cea..547537f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "0.41.0", + "version": "0.42.0", "description": "A simple, fast, website analytics alternative to Google Analytics. ", "author": "Mike Cao ", "license": "MIT", @@ -83,6 +83,7 @@ "redux-thunk": "^2.3.0", "request-ip": "^2.1.3", "thenby": "^1.3.4", + "timezone-support": "^2.0.2", "tinycolor2": "^1.4.1", "unfetch": "^4.1.0", "uuid": "^8.3.0" diff --git a/redux/actions/app.js b/redux/actions/app.js index 72636a74..cbb8285b 100644 --- a/redux/actions/app.js +++ b/redux/actions/app.js @@ -1,9 +1,10 @@ import { createSlice } from '@reduxjs/toolkit'; import { getItem } from 'lib/web'; +import { LOCALE_CONFIG } from 'lib/constants'; const app = createSlice({ name: 'app', - initialState: { locale: getItem('umami.locale') || 'en-US' }, + initialState: { locale: getItem(LOCALE_CONFIG) || 'en-US' }, reducers: { updateApp(state, action) { state = action.payload; diff --git a/yarn.lock b/yarn.lock index da2fa5cb..201c7e5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2699,6 +2699,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@2.20.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" + integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== + commander@^6.0.0, commander@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-6.1.0.tgz#f8d722b78103141006b66f4c7ba1e97315ba75bc" @@ -8608,6 +8613,13 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" +timezone-support@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/timezone-support/-/timezone-support-2.0.2.tgz#801d6924478b1b60f09b90699ce1127a6044cbe7" + integrity sha512-J/1PyHCX76vOPuJzCyHMQMH2wTjXCJ30R5EXaS/QTi+xYsL0thS0pubDrHCWnfG4zU1jpPJtctnBBRCOpcJZeQ== + dependencies: + commander "2.20.0" + tiny-lru@7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-7.0.6.tgz#b0c3cdede1e5882aa2d1ae21cb2ceccf2a331f24"