From 5a22be5efa28b3f152b9e06ab3786ee8557dc971 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 10 Sep 2020 23:55:29 -0700 Subject: [PATCH 1/9] Allow admins to view account websites. --- assets/list-ul.svg | 1 + components/WebsiteDetails.js | 1 + components/WebsiteList.js | 4 ++-- components/metrics/BarChart.js | 6 ------ components/settings/AccountSettings.js | 17 ++++++++++++++--- lang/de-DE.json | 3 +++ lang/en.json | 6 ++++++ lang/nl-NL.json | 3 +++ lang/ru-RU.json | 3 +++ lang/tr-TR.json | 3 +++ lang/zh-CN.json | 3 +++ package.json | 2 +- pages/404.js | 5 ++++- pages/account.js | 18 ------------------ pages/api/{account.js => account/index.js} | 0 pages/api/website/{[id]/index.js => [id].js} | 0 pages/api/{website.js => website/index.js} | 0 pages/api/websites.js | 11 ++++++++--- pages/{dashboard.js => dashboard/[[...id]].js} | 6 +++++- 19 files changed, 57 insertions(+), 35 deletions(-) create mode 100644 assets/list-ul.svg delete mode 100644 pages/account.js rename pages/api/{account.js => account/index.js} (100%) rename pages/api/website/{[id]/index.js => [id].js} (100%) rename pages/api/{website.js => website/index.js} (100%) rename pages/{dashboard.js => dashboard/[[...id]].js} (67%) diff --git a/assets/list-ul.svg b/assets/list-ul.svg new file mode 100644 index 0000000000..5e63212638 --- /dev/null +++ b/assets/list-ul.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/WebsiteDetails.js b/components/WebsiteDetails.js index 91217d78ea..3ff87de969 100644 --- a/components/WebsiteDetails.js +++ b/components/WebsiteDetails.js @@ -28,6 +28,7 @@ export default function WebsiteDetails({ websiteId }) { const BackButton = () => ( - {showMenu && ( - - )} - + <> + + {locale === 'zh-CN' && ( + + )} + {locale === 'jp-JP' && ( + + )} + +
+ + {showMenu && ( + + )} +
+ ); } diff --git a/components/layout/Layout.js b/components/layout/Layout.js index c961ade9b5..021745cc9c 100644 --- a/components/layout/Layout.js +++ b/components/layout/Layout.js @@ -13,10 +13,6 @@ 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" /> - {header &&
}
{children}
From 8e3286179a460b96f625212cbb7816caa021579a Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 11 Sep 2020 13:49:43 -0700 Subject: [PATCH 3/9] API security updates. --- pages/api/account/[id].js | 20 ++++------ pages/api/account/password.js | 8 +++- pages/api/website/[id]/active.js | 17 ++++++--- pages/api/website/[id]/events.js | 27 +++++++++----- pages/api/website/[id]/metrics.js | 29 +++++++++------ pages/api/website/[id]/pageviews.js | 33 ++++++++++------- pages/api/website/[id]/rankings.js | 57 ++++++++++++++++------------- 7 files changed, 113 insertions(+), 78 deletions(-) diff --git a/pages/api/account/[id].js b/pages/api/account/[id].js index b8fe22458c..c87f948b1f 100644 --- a/pages/api/account/[id].js +++ b/pages/api/account/[id].js @@ -9,24 +9,20 @@ export default async (req, res) => { const { id } = req.query; const user_id = +id; - if (req.method === 'GET') { - if (is_admin) { - const account = await getAccountById(user_id); + if (is_admin) { + return unauthorized(res); + } - return ok(res, account); - } + if (req.method === 'GET') { + const account = await getAccountById(user_id); - return unauthorized(res); + return ok(res, account); } if (req.method === 'DELETE') { - if (is_admin) { - await deleteAccount(user_id); - - return ok(res); - } + await deleteAccount(user_id); - return unauthorized(res); + return ok(res); } return methodNotAllowed(res); diff --git a/pages/api/account/password.js b/pages/api/account/password.js index 32f87960ba..c9c955fa47 100644 --- a/pages/api/account/password.js +++ b/pages/api/account/password.js @@ -1,14 +1,18 @@ import { getAccountById, updateAccount } from 'lib/queries'; import { useAuth } from 'lib/middleware'; -import { badRequest, methodNotAllowed, ok } from 'lib/response'; +import { badRequest, methodNotAllowed, ok, unauthorized } from 'lib/response'; import { checkPassword, hashPassword } from 'lib/crypto'; export default async (req, res) => { await useAuth(req, res); - const { user_id } = req.auth; + const { user_id, is_admin } = req.auth; const { current_password, new_password } = req.body; + if (is_admin) { + return unauthorized(res); + } + if (req.method === 'POST') { const account = await getAccountById(user_id); const valid = await checkPassword(current_password, account.password); diff --git a/pages/api/website/[id]/active.js b/pages/api/website/[id]/active.js index 06731408cc..492074a9e7 100644 --- a/pages/api/website/[id]/active.js +++ b/pages/api/website/[id]/active.js @@ -1,11 +1,18 @@ import { getActiveVisitors } from 'lib/queries'; -import { ok } from 'lib/response'; +import { methodNotAllowed, ok } from 'lib/response'; +import { useAuth } from 'lib/middleware'; export default async (req, res) => { - const { id } = req.query; - const website_id = +id; + await useAuth(req, res); - const result = await getActiveVisitors(website_id); + if (req.method === 'GET') { + const { id } = req.query; + const website_id = +id; - return ok(res, result); + const result = await getActiveVisitors(website_id); + + return ok(res, result); + } + + return methodNotAllowed(res); }; diff --git a/pages/api/website/[id]/events.js b/pages/api/website/[id]/events.js index d7ead27c52..c230acd1c6 100644 --- a/pages/api/website/[id]/events.js +++ b/pages/api/website/[id]/events.js @@ -1,21 +1,28 @@ import moment from 'moment-timezone'; import { getEvents } from 'lib/queries'; -import { ok, badRequest } from 'lib/response'; +import { ok, badRequest, methodNotAllowed } from 'lib/response'; +import { useAuth } from 'lib/middleware'; const unitTypes = ['month', 'hour', 'day']; export default async (req, res) => { - const { id, start_at, end_at, unit, tz } = req.query; + await useAuth(req, res); - if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) { - return badRequest(res); - } + if (req.method === 'GET') { + const { id, start_at, end_at, unit, tz } = req.query; + + if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) { + return badRequest(res); + } - const websiteId = +id; - const startDate = new Date(+start_at); - const endDate = new Date(+end_at); + const websiteId = +id; + const startDate = new Date(+start_at); + const endDate = new Date(+end_at); - const events = await getEvents(websiteId, startDate, endDate, tz, unit); + const events = await getEvents(websiteId, startDate, endDate, tz, unit); + + return ok(res, events); + } - return ok(res, events); + return methodNotAllowed(res); }; diff --git a/pages/api/website/[id]/metrics.js b/pages/api/website/[id]/metrics.js index 4b0d71e118..70a9d1e637 100644 --- a/pages/api/website/[id]/metrics.js +++ b/pages/api/website/[id]/metrics.js @@ -1,18 +1,25 @@ import { getMetrics } from 'lib/queries'; -import { ok } from 'lib/response'; +import { methodNotAllowed, ok } from 'lib/response'; +import { useAuth } from 'lib/middleware'; export default async (req, res) => { - const { id, start_at, end_at } = req.query; - const websiteId = +id; - const startDate = new Date(+start_at); - const endDate = new Date(+end_at); + await useAuth(req, res); - const metrics = await getMetrics(websiteId, startDate, endDate); + if (req.method === 'GET') { + const { id, start_at, end_at } = req.query; + const websiteId = +id; + const startDate = new Date(+start_at); + const endDate = new Date(+end_at); - const stats = Object.keys(metrics[0]).reduce((obj, key) => { - obj[key] = Number(metrics[0][key]) || 0; - return obj; - }, {}); + const metrics = await getMetrics(websiteId, startDate, endDate); - return ok(res, stats); + const stats = Object.keys(metrics[0]).reduce((obj, key) => { + obj[key] = Number(metrics[0][key]) || 0; + return obj; + }, {}); + + return ok(res, stats); + } + + return methodNotAllowed(res); }; diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/website/[id]/pageviews.js index c90e1c6b59..ff3b731ee1 100644 --- a/pages/api/website/[id]/pageviews.js +++ b/pages/api/website/[id]/pageviews.js @@ -1,24 +1,31 @@ import moment from 'moment-timezone'; import { getPageviews } from 'lib/queries'; -import { ok, badRequest } from 'lib/response'; +import { ok, badRequest, methodNotAllowed } from 'lib/response'; +import { useAuth } from 'lib/middleware'; const unitTypes = ['month', 'hour', 'day']; export default async (req, res) => { - const { id, start_at, end_at, unit, tz } = req.query; + await useAuth(req, res); - if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) { - return badRequest(res); - } + if (req.method === 'GET') { + const { id, start_at, end_at, unit, tz } = req.query; + + if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) { + return badRequest(res); + } - const websiteId = +id; - const startDate = new Date(+start_at); - const endDate = new Date(+end_at); + const websiteId = +id; + const startDate = new Date(+start_at); + const endDate = new Date(+end_at); - const [pageviews, uniques] = await Promise.all([ - getPageviews(websiteId, startDate, endDate, tz, unit, '*'), - getPageviews(websiteId, startDate, endDate, tz, unit, 'distinct session_id'), - ]); + const [pageviews, uniques] = await Promise.all([ + getPageviews(websiteId, startDate, endDate, tz, unit, '*'), + getPageviews(websiteId, startDate, endDate, tz, unit, 'distinct session_id'), + ]); + + return ok(res, { pageviews, uniques }); + } - return ok(res, { pageviews, uniques }); + return methodNotAllowed(res); }; diff --git a/pages/api/website/[id]/rankings.js b/pages/api/website/[id]/rankings.js index 4e613d0d92..158a4bc28c 100644 --- a/pages/api/website/[id]/rankings.js +++ b/pages/api/website/[id]/rankings.js @@ -1,6 +1,7 @@ import { getRankings } from 'lib/queries'; -import { ok, badRequest } from 'lib/response'; -import { DOMAIN_REGEX } from '../../../../lib/constants'; +import { ok, badRequest, methodNotAllowed } from 'lib/response'; +import { DOMAIN_REGEX } from 'lib/constants'; +import { useAuth } from 'lib/middleware'; const sessionColumns = ['browser', 'os', 'device', 'country']; const pageviewColumns = ['url', 'referrer']; @@ -25,29 +26,35 @@ function getColumn(type) { } export default async (req, res) => { - const { id, type, start_at, end_at, domain } = req.query; - const websiteId = +id; - const startDate = new Date(+start_at); - const endDate = new Date(+end_at); - - if ( - type !== 'event' && - !sessionColumns.includes(type) && - !pageviewColumns.includes(type) && - domain && - DOMAIN_REGEX.test(domain) - ) { - return badRequest(res); + await useAuth(req, res); + + if (req.method === 'GET') { + const { id, type, start_at, end_at, domain } = req.query; + const websiteId = +id; + const startDate = new Date(+start_at); + const endDate = new Date(+end_at); + + if ( + type !== 'event' && + !sessionColumns.includes(type) && + !pageviewColumns.includes(type) && + domain && + DOMAIN_REGEX.test(domain) + ) { + return badRequest(res); + } + + const rankings = await getRankings( + websiteId, + startDate, + endDate, + getColumn(type), + getTable(type), + domain, + ); + + return ok(res, rankings); } - const rankings = await getRankings( - websiteId, - startDate, - endDate, - getColumn(type), - getTable(type), - domain, - ); - - return ok(res, rankings); + return methodNotAllowed(res); }; From 7a8ab94bbabbee375eea835e41bef131d2bd2e87 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 12 Sep 2020 09:59:19 -0700 Subject: [PATCH 4/9] Added page not found string. --- README.md | 1 - lang/de-DE.json | 1 + lang/en-US.json | 1 + lang/ja-JP.json | 1 + lang/nl-NL.json | 1 + lang/ru-RU.json | 1 + lang/tr-TR.json | 1 + lang/zh-CN.json | 1 + 8 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a241d409fd..397dcdbfa0 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,6 @@ Or with MySQL support: docker pull ghcr.io/mikecao/umami:mysql-latest ``` - ## Getting updates To get the latest features, simply do a pull, install any new dependencies, and rebuild: diff --git a/lang/de-DE.json b/lang/de-DE.json index 2dbc9b430f..4b2c1f1603 100644 --- a/lang/de-DE.json +++ b/lang/de-DE.json @@ -42,6 +42,7 @@ "message.failure": "Es it ein Fehler aufgetreten.", "message.incorrect-username-password": "Falsches Passwort oder Benutzername.", "message.no-data-available": "Keine Daten vorhanden.", + "message.page-not-found": "Seite nicht gefunden.", "message.save-success": "Erfolgreich gespeichert.", "message.share-url": "Dies ist der öffentliche URL zum Teilen für {target}.", "message.track-stats": "Um die Statistiken für {target} zu übermitteln, platzieren Sie bitte den folgenden Quelltext im {head} ihrer Homepage.", diff --git a/lang/en-US.json b/lang/en-US.json index df557819c7..e9ebdf6b6d 100644 --- a/lang/en-US.json +++ b/lang/en-US.json @@ -42,6 +42,7 @@ "message.failure": "Something went wrong.", "message.incorrect-username-password": "Incorrect username/password.", "message.no-data-available": "No data available.", + "message.page-not-found": "Page not found.", "message.save-success": "Saved successfully.", "message.share-url": "This is the publicly shared URL for {target}.", "message.track-stats": "To track stats for {target}, place the following code in the {head} section of your website.", diff --git a/lang/ja-JP.json b/lang/ja-JP.json index 7fe9095fb1..7d28642833 100644 --- a/lang/ja-JP.json +++ b/lang/ja-JP.json @@ -42,6 +42,7 @@ "message.failure": "問題が発生しました。", "message.incorrect-username-password": "ユーザー名/パスワードが正しくありません。", "message.no-data-available": "データがありません。", + "message.page-not-found": "ページが見つかりません。", "message.save-success": "正常に保存されました。", "message.share-url": "これは {target} の共有リンクです。", "message.track-stats": "{target}のアクセス解析を開始するには、次のコードをWebサイトの{head}セクションへ追加してください。", diff --git a/lang/nl-NL.json b/lang/nl-NL.json index d48696efd1..5e02d4938f 100644 --- a/lang/nl-NL.json +++ b/lang/nl-NL.json @@ -42,6 +42,7 @@ "message.failure": "Er is iets misgegaan.", "message.incorrect-username-password": "Incorrecte gebruikersnaam/wachtwoord.", "message.no-data-available": "Geen gegevens beschikbaar.", + "message.page-not-found": "Pagina niet gevonden.", "message.save-success": "Opslaan succesvol.", "message.share-url": "Met deze URL kan {target} openbaar gedeeld worden.", "message.track-stats": "Om statistieken voor {target} bij te houden, plaats je de volgende code in het {head} gedeelte van je website.", diff --git a/lang/ru-RU.json b/lang/ru-RU.json index 36329add87..527cfb8d56 100644 --- a/lang/ru-RU.json +++ b/lang/ru-RU.json @@ -42,6 +42,7 @@ "message.failure": "Что-то пошло не так.", "message.incorrect-username-password": "Неверное имя пользователя/пароль.", "message.no-data-available": "Нет данных.", + "message.page-not-found": "Страница не найдена.", "message.save-success": "Успешно сохранено.", "message.share-url": "Это публичная ссылка для {target}.", "message.track-stats": "Чтобы отслеживать статистику для {target}, поместите следующий код в раздел {head} вашего сайта.", diff --git a/lang/tr-TR.json b/lang/tr-TR.json index bac433c57a..20a1c44104 100644 --- a/lang/tr-TR.json +++ b/lang/tr-TR.json @@ -42,6 +42,7 @@ "message.failure": "Bir şeyler ters gitti!", "message.incorrect-username-password": "Hatalı kullanıcı adı ya da parola.", "message.no-data-available": "Henüz hiç veri yok.", + "message.page-not-found": "Sayfa bulunamadı.", "message.save-success": "Başarıyla kaydedildi.", "message.share-url": "{target} için kullanılabilir anonim paylaşım adresidir.", "message.track-stats": "{target} alanı adı istatistiklerini takip etmek için, aşağıdaki kodu web sitenizin {head} bloğuna yerleştirin.", diff --git a/lang/zh-CN.json b/lang/zh-CN.json index b8290d8ce3..c050e6a6b9 100644 --- a/lang/zh-CN.json +++ b/lang/zh-CN.json @@ -42,6 +42,7 @@ "message.failure": "出现错误.", "message.incorrect-username-password": "用户名密码不正确.", "message.no-data-available": "无可用数据.", + "message.page-not-found": "网页未找到.", "message.save-success": "成功保存.", "message.share-url": "这是 {target} 的共享链接.", "message.track-stats": "把以下代码放到你的网站的{head}部分来收集{target}的数据.", From 4e103152b2fdbf50ff9e3b59a2811d419ce4a540 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 13 Sep 2020 01:26:54 -0700 Subject: [PATCH 5/9] Custom date range selection. --- components/common/Button.js | 5 + components/common/Button.module.css | 27 ++- components/common/Calendar.js | 258 +++++++++++++++++++++ components/common/Calendar.module.css | 84 +++++++ components/common/DateFilter.js | 54 ++++- components/common/DropDown.js | 7 +- components/common/Dropdown.module.css | 13 +- components/common/Icon.js | 3 +- components/common/LanguageButton.js | 2 +- components/common/Modal.module.css | 3 +- components/forms/DatePickerForm.js | 41 ++++ components/forms/DatePickerForm.module.css | 25 ++ components/forms/DeleteForm.js | 14 +- components/metrics/WebsiteChart.js | 8 +- lib/array.js | 11 + lib/date.js | 15 +- lib/lang.js | 4 +- package.json | 2 +- styles/index.css | 9 +- 19 files changed, 545 insertions(+), 40 deletions(-) create mode 100644 components/common/Calendar.js create mode 100644 components/common/Calendar.module.css create mode 100644 components/forms/DatePickerForm.js create mode 100644 components/forms/DatePickerForm.module.css create mode 100644 lib/array.js diff --git a/components/common/Button.js b/components/common/Button.js index 2bdbee5bfa..d72fb66292 100644 --- a/components/common/Button.js +++ b/components/common/Button.js @@ -13,6 +13,8 @@ export default function Button({ className, tooltip, tooltipId, + disabled = false, + onClick = () => {}, ...props }) { return ( @@ -27,7 +29,10 @@ export default function Button({ [styles.xsmall]: size === 'xsmall', [styles.action]: variant === 'action', [styles.danger]: variant === 'danger', + [styles.disabled]: disabled, })} + disabled={disabled} + onClick={!disabled ? onClick : null} {...props} > {icon && } diff --git a/components/common/Button.module.css b/components/common/Button.module.css index 4bf5e05a65..99c7168f31 100644 --- a/components/common/Button.module.css +++ b/components/common/Button.module.css @@ -14,7 +14,7 @@ } .button:hover { - background: #eaeaea; + background: var(--gray200); } .button:active { @@ -38,19 +38,32 @@ } .action { - color: var(--gray50) !important; - background: var(--gray900) !important; + color: var(--gray50); + background: var(--gray900); } .action:hover { - background: var(--gray800) !important; + background: var(--gray800); } .danger { - color: var(--gray50) !important; - background: var(--red500) !important; + color: var(--gray50); + background: var(--red500); } .danger:hover { - background: var(--red400) !important; + background: var(--red400); +} + +.button:disabled { + color: var(--gray500); + background: var(--gray75); +} + +.button:disabled:active { + color: var(--gray500); +} + +.button:disabled:hover { + background: var(--gray75); } diff --git a/components/common/Calendar.js b/components/common/Calendar.js new file mode 100644 index 0000000000..8ecf76a89f --- /dev/null +++ b/components/common/Calendar.js @@ -0,0 +1,258 @@ +import React, { useState } from 'react'; +import classNames from 'classnames'; +import { + startOfWeek, + startOfMonth, + startOfYear, + endOfMonth, + addDays, + subDays, + addYears, + subYears, + addMonths, + setMonth, + setYear, + isSameDay, + isBefore, + isAfter, +} from 'date-fns'; +import Button from './Button'; +import useLocale from 'hooks/useLocale'; +import { dateFormat } from 'lib/lang'; +import { chunk } from 'lib/array'; +import Chevron from 'assets/chevron-down.svg'; +import Cross from 'assets/times.svg'; +import styles from './Calendar.module.css'; +import Icon from './Icon'; + +export default function Calendar({ date, minDate, maxDate, onChange }) { + const [locale] = useLocale(); + const [selectMonth, setSelectMonth] = useState(false); + const [selectYear, setSelectYear] = useState(false); + + const month = dateFormat(date, 'MMMM', locale); + const year = date.getFullYear(); + + function toggleMonthSelect() { + setSelectYear(false); + setSelectMonth(state => !state); + } + + function toggleYearSelect() { + setSelectMonth(false); + setSelectYear(state => !state); + } + + function handleChange(value) { + setSelectMonth(false); + setSelectYear(false); + if (value) { + onChange(value); + } + } + + return ( +
+
+
{date.getDate()}
+
+ {month} + : } size="small" /> +
+
+ {year} + : } size="small" /> +
+
+ {!selectMonth && !selectYear && ( + + )} + {selectMonth && ( + + )} + {selectYear && ( + + )} +
+ ); +} + +const DaySelector = ({ date, minDate, maxDate, locale, onSelect }) => { + const startWeek = startOfWeek(date); + const startMonth = startOfMonth(date); + const startDay = subDays(startMonth, startMonth.getDay() + 1); + const month = date.getMonth(); + const year = date.getFullYear(); + + const daysOfWeek = []; + for (let i = 0; i < 7; i++) { + daysOfWeek.push(addDays(startWeek, i)); + } + + const days = []; + for (let i = 0; i < 35; i++) { + days.push(addDays(startDay, i)); + } + + return ( + + + + {daysOfWeek.map((day, i) => ( + + ))} + + + + {chunk(days, 7).map((week, i) => ( + + {week.map((day, j) => { + const disabled = isBefore(day, minDate) || isAfter(day, maxDate); + return ( + + ); + })} + + ))} + +
+ {dateFormat(day, 'EEE', locale)} +
onSelect(day) : null} + > + {day.getDate()} +
+ ); +}; + +const MonthSelector = ({ date, minDate, maxDate, locale, onSelect }) => { + const start = startOfYear(date); + const months = []; + for (let i = 0; i < 12; i++) { + months.push(endOfMonth(addMonths(start, i))); + } + + function handleSelect(value) { + onSelect(setMonth(date, value)); + } + + return ( + + + {chunk(months, 3).map((row, i) => ( + + {row.map((month, j) => { + const disabled = isBefore(month, minDate) || isAfter(month, maxDate); + return ( + + ); + })} + + ))} + +
handleSelect(month.getMonth()) : null} + > + {dateFormat(month, 'MMMM', locale)} +
+ ); +}; + +const YearSelector = ({ date, minDate, maxDate, onSelect }) => { + const [currentDate, setCurrentDate] = useState(date); + const year = date.getFullYear(); + const currentYear = currentDate.getFullYear(); + const minYear = minDate.getFullYear(); + const maxYear = maxDate.getFullYear(); + const years = []; + for (let i = 0; i < 15; i++) { + years.push(currentYear - 7 + i); + } + + function handleSelect(value) { + onSelect(setYear(date, value)); + } + + function handlePrevClick() { + setCurrentDate(state => subYears(state, 15)); + } + + function handleNextClick() { + setCurrentDate(state => addYears(state, 15)); + } + + return ( +
+
+ ); +}; diff --git a/components/common/Calendar.module.css b/components/common/Calendar.module.css new file mode 100644 index 0000000000..ccfde683b7 --- /dev/null +++ b/components/common/Calendar.module.css @@ -0,0 +1,84 @@ +.calendar { + display: flex; + flex-direction: column; + font-size: var(--font-size-small); + flex: 1; +} + +.calendar table { + flex: 1; +} + +.calendar td { + color: var(--gray800); + cursor: pointer; + text-align: center; + vertical-align: center; + height: 40px; + min-width: 40px; + border-radius: 5px; +} + +.calendar td:hover { + background: var(--gray100); +} + +.calendar td.faded { + color: var(--gray500); +} + +.calendar td.selected { + font-weight: 600; + border: 1px solid var(--gray600); +} + +.calendar td.selected:hover { + background: transparent; +} + +.calendar td.disabled { + color: var(--gray300); + background: var(--gray75); +} + +.calendar td.disabled:hover { + cursor: default; + background: var(--gray75); +} + +.calendar td.faded.disabled { + color: var(--gray200); +} + +.header { + display: flex; + justify-content: space-evenly; + align-items: center; + font-weight: 700; + line-height: 40px; + font-size: var(--font-size-normal); +} + +.selector { + cursor: pointer; +} + +.pager { + display: flex; +} + +.pager button { + align-self: center; +} + +.left svg { + transform: rotate(90deg); +} + +.right svg { + transform: rotate(-90deg); +} + +.icon { + margin-left: 10px; +} diff --git a/components/common/DateFilter.js b/components/common/DateFilter.js index aaba8725f0..a7ac6372ca 100644 --- a/components/common/DateFilter.js +++ b/components/common/DateFilter.js @@ -1,7 +1,12 @@ -import React from 'react'; -import { getDateRange } from 'lib/date'; -import DropDown from './DropDown'; +import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; +import { endOfYear } from 'date-fns'; +import Modal from './Modal'; +import DropDown from './DropDown'; +import DatePickerForm from 'components/forms/DatePickerForm'; +import useLocale from 'hooks/useLocale'; +import { getDateRange } from 'lib/date'; +import { dateFormat } from 'lib/lang'; const filterOptions = [ { @@ -35,14 +40,53 @@ const filterOptions = [ value: '1month', }, { label: , value: '1year' }, + { + label: , + value: 'custom', + }, ]; -export default function DateFilter({ value, onChange, className }) { +export default function DateFilter({ value, startDate, endDate, onChange, className }) { + const [locale] = useLocale(); + const [showPicker, setShowPicker] = useState(false); + const displayValue = + value === 'custom' + ? `${dateFormat(startDate, 'd LLL y', locale)} — ${dateFormat(endDate, 'd LLL y', locale)}` + : value; + function handleChange(value) { + if (value === 'custom') { + setShowPicker(true); + return; + } onChange(getDateRange(value)); } + function handlePickerChange(value) { + setShowPicker(false); + onChange(value); + } + return ( - + <> + + {showPicker && ( + + setShowPicker(false)} + /> + + )} + ); } diff --git a/components/common/DropDown.js b/components/common/DropDown.js index 8f1ca4f974..03fa8bb616 100644 --- a/components/common/DropDown.js +++ b/components/common/DropDown.js @@ -23,9 +23,8 @@ export default function DropDown({ function handleSelect(selected, e) { e.stopPropagation(); setShowMenu(false); - if (selected !== value) { - onChange(selected); - } + + onChange(selected); } useDocumentClick(e => { @@ -37,7 +36,7 @@ export default function DropDown({ return (
- {options.find(e => e.value === value)?.label} +
{options.find(e => e.value === value)?.label || value}
} size="small" />
{showMenu && ( diff --git a/components/common/Dropdown.module.css b/components/common/Dropdown.module.css index ec63552f8d..958305e7a9 100644 --- a/components/common/Dropdown.module.css +++ b/components/common/Dropdown.module.css @@ -1,16 +1,15 @@ .dropdown { + flex: 1; position: relative; - font-size: var(--font-size-small); - min-width: 140px; + border: 1px solid var(--gray500); + border-radius: 4px; + cursor: pointer; } .value { display: flex; justify-content: space-between; - white-space: nowrap; - position: relative; + font-size: var(--font-size-small); + min-width: 140px; padding: 4px 16px; - border: 1px solid var(--gray500); - border-radius: 4px; - cursor: pointer; } diff --git a/components/common/Icon.js b/components/common/Icon.js index 1b6fd1d9b5..8a794f61f9 100644 --- a/components/common/Icon.js +++ b/components/common/Icon.js @@ -2,7 +2,7 @@ import React from 'react'; import classNames from 'classnames'; import styles from './Icon.module.css'; -export default function Icon({ icon, className, size = 'medium' }) { +export default function Icon({ icon, className, size = 'medium', ...props }) { return (
{icon}
diff --git a/components/common/LanguageButton.js b/components/common/LanguageButton.js index 74c55ba327..714ae7f64d 100644 --- a/components/common/LanguageButton.js +++ b/components/common/LanguageButton.js @@ -35,7 +35,7 @@ export default function LanguageButton({ menuPosition = 'bottom', menuAlign = 'l rel="stylesheet" /> )} - {locale === 'jp-JP' && ( + {locale === 'ja-JP' && ( +
+ + +
+ + + + +
+ ); +} diff --git a/components/forms/DatePickerForm.module.css b/components/forms/DatePickerForm.module.css new file mode 100644 index 0000000000..dcedc17a42 --- /dev/null +++ b/components/forms/DatePickerForm.module.css @@ -0,0 +1,25 @@ +.container { + display: flex; + flex-direction: column; + width: 800px; + max-width: 100vw; +} + +.calendars { + display: flex; +} + +.calendars > div:first-child { + padding-right: 20px; + border-right: 1px solid var(--gray300); +} + +.calendars > div:last-child { + padding-left: 20px; +} + +@media only screen and (max-width: 768px) { + .calendars { + flex-direction: column; + } +} diff --git a/components/forms/DeleteForm.js b/components/forms/DeleteForm.js index 1ba8162626..f53b286f2e 100644 --- a/components/forms/DeleteForm.js +++ b/components/forms/DeleteForm.js @@ -10,10 +10,12 @@ import FormLayout, { } from 'components/layout/FormLayout'; import { FormattedMessage } from 'react-intl'; +const CONFIRMATION_WORD = 'DELETE'; + const validate = ({ confirmation }) => { const errors = {}; - if (confirmation !== 'DELETE') { + if (confirmation !== CONFIRMATION_WORD) { errors.confirmation = !confirmation ? ( ) : ( @@ -44,7 +46,7 @@ export default function DeleteForm({ values, onSave, onClose }) { validate={validate} onSubmit={handleSubmit} > - {() => ( + {props => (
DELETE }} + values={{ delete: {CONFIRMATION_WORD} }} />

@@ -71,7 +73,11 @@ export default function DeleteForm({ values, onSave, onClose }) { -
diff --git a/lib/array.js b/lib/array.js new file mode 100644 index 0000000000..5b9aa1f2ca --- /dev/null +++ b/lib/array.js @@ -0,0 +1,11 @@ +export function chunk(arr, size) { + const chunks = []; + + let index = 0; + while (index < arr.length) { + chunks.push(arr.slice(index, size + index)); + index += size; + } + + return chunks; +} diff --git a/lib/date.js b/lib/date.js index 13c2e55f86..c8d092d4e8 100644 --- a/lib/date.js +++ b/lib/date.js @@ -18,7 +18,7 @@ import { endOfYear, differenceInHours, differenceInCalendarDays, - differenceInMonths, + differenceInCalendarMonths, } from 'date-fns'; export function getTimezone() { @@ -85,10 +85,21 @@ export function getDateRange(value) { } } +export function getDateRangeValues(startDate, endDate) { + if (differenceInHours(endDate, startDate) <= 48) { + return { startDate: startOfHour(startDate), endDate: endOfHour(endDate), unit: 'hour' }; + } else if (differenceInCalendarDays(endDate, startDate) <= 90) { + return { startDate: startOfDay(startDate), endDate: endOfDay(endDate), unit: 'day' }; + } else if (differenceInCalendarMonths(endDate, startDate) <= 12) { + return { startDate: startOfMonth(startDate), endDate: endOfMonth(endDate), unit: 'month' }; + } + return { startDate: startOfYear(startDate), endDate: endOfYear(endDate), unit: 'year' }; +} + const dateFuncs = { hour: [differenceInHours, addHours, startOfHour], day: [differenceInCalendarDays, addDays, startOfDay], - month: [differenceInMonths, addMonths, startOfMonth], + month: [differenceInCalendarMonths, addMonths, startOfMonth], }; export function getDateArray(data, startDate, endDate, unit) { diff --git a/lib/lang.js b/lib/lang.js index 0bff776d32..bbc257afad 100644 --- a/lib/lang.js +++ b/lib/lang.js @@ -9,7 +9,7 @@ import deDEMessages from 'lang-compiled/de-DE.json'; import jaMessages from 'lang-compiled/ja-JP.json'; export const messages = { - en: enMessages, + 'en-US': enMessages, 'nl-NL': nlMessages, 'zh-CN': zhCNMessages, 'de-DE': deDEMessages, @@ -19,7 +19,7 @@ export const messages = { }; export const dateLocales = { - en: enUS, + 'en-US': enUS, 'nl-NL': nl, 'zh-CN': zhCN, 'de-DE': de, diff --git a/package.json b/package.json index 67418849c0..e87212c3ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "0.29.0", + "version": "0.30.0", "description": "A simple, fast, website analytics alternative to Google Analytics. ", "author": "Mike Cao ", "license": "MIT", diff --git a/styles/index.css b/styles/index.css index c8e1709ea5..5435364f63 100644 --- a/styles/index.css +++ b/styles/index.css @@ -15,7 +15,10 @@ body { .zh-CN { font-family: 'Noto Sans SC', sans-serif !important; - font-size: 110%; +} + +.ja-JP { + font-family: 'Noto Sans JP', sans-serif !important; } *, @@ -40,6 +43,10 @@ h6 { height: 100%; } +#__modals { + z-index: 10; +} + button, input, select { From a0cb27846367360a12f5ec316cc590dc299f262a Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 13 Sep 2020 01:38:14 -0700 Subject: [PATCH 6/9] Fix display of years. --- lib/date.js | 5 ++++- package.json | 2 +- pages/api/website/[id]/pageviews.js | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/date.js b/lib/date.js index c8d092d4e8..e7e280362c 100644 --- a/lib/date.js +++ b/lib/date.js @@ -4,6 +4,7 @@ import { addHours, addDays, addMonths, + addYears, subHours, subDays, startOfHour, @@ -19,6 +20,7 @@ import { differenceInHours, differenceInCalendarDays, differenceInCalendarMonths, + differenceInCalendarYears, } from 'date-fns'; export function getTimezone() { @@ -90,7 +92,7 @@ export function getDateRangeValues(startDate, endDate) { return { startDate: startOfHour(startDate), endDate: endOfHour(endDate), unit: 'hour' }; } else if (differenceInCalendarDays(endDate, startDate) <= 90) { return { startDate: startOfDay(startDate), endDate: endOfDay(endDate), unit: 'day' }; - } else if (differenceInCalendarMonths(endDate, startDate) <= 12) { + } else if (differenceInCalendarMonths(endDate, startDate) <= 24) { return { startDate: startOfMonth(startDate), endDate: endOfMonth(endDate), unit: 'month' }; } return { startDate: startOfYear(startDate), endDate: endOfYear(endDate), unit: 'year' }; @@ -100,6 +102,7 @@ const dateFuncs = { hour: [differenceInHours, addHours, startOfHour], day: [differenceInCalendarDays, addDays, startOfDay], month: [differenceInCalendarMonths, addMonths, startOfMonth], + year: [differenceInCalendarYears, addYears, startOfYear], }; export function getDateArray(data, startDate, endDate, unit) { diff --git a/package.json b/package.json index e87212c3ae..f9541179a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "0.30.0", + "version": "0.31.0", "description": "A simple, fast, website analytics alternative to Google Analytics. ", "author": "Mike Cao ", "license": "MIT", diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/website/[id]/pageviews.js index ff3b731ee1..398e52e446 100644 --- a/pages/api/website/[id]/pageviews.js +++ b/pages/api/website/[id]/pageviews.js @@ -3,7 +3,7 @@ import { getPageviews } from 'lib/queries'; import { ok, badRequest, methodNotAllowed } from 'lib/response'; import { useAuth } from 'lib/middleware'; -const unitTypes = ['month', 'hour', 'day']; +const unitTypes = ['year', 'month', 'hour', 'day']; export default async (req, res) => { await useAuth(req, res); From f59594e4cd04f833c88f8f2ff642707e9958adf7 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 13 Sep 2020 11:33:57 -0700 Subject: [PATCH 7/9] Calendar updates. Responsive CSS updates. --- assets/calendar-alt.svg | 1 + components/common/Button.js | 1 + components/common/Button.module.css | 13 ++++++ components/common/Calendar.js | 11 ++++-- components/common/Calendar.module.css | 5 ++- components/common/DateFilter.js | 46 +++++++++++++++++----- components/common/DropDown.js | 4 +- components/common/Dropdown.module.css | 13 +++++- components/common/LanguageButton.js | 6 ++- components/common/Menu.js | 3 +- components/common/Menu.module.css | 4 ++ components/metrics/MetricCard.module.css | 3 +- components/metrics/MetricsBar.module.css | 3 ++ components/metrics/WebsiteChart.js | 19 +++++---- components/metrics/WebsiteChart.module.css | 12 ++++++ lib/date.js | 21 +++++----- lib/lang.js | 2 +- pages/api/website/[id]/events.js | 2 +- 18 files changed, 127 insertions(+), 42 deletions(-) create mode 100644 assets/calendar-alt.svg diff --git a/assets/calendar-alt.svg b/assets/calendar-alt.svg new file mode 100644 index 0000000000..230c4e66ac --- /dev/null +++ b/assets/calendar-alt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/common/Button.js b/components/common/Button.js index d72fb66292..b973b36e6d 100644 --- a/components/common/Button.js +++ b/components/common/Button.js @@ -29,6 +29,7 @@ export default function Button({ [styles.xsmall]: size === 'xsmall', [styles.action]: variant === 'action', [styles.danger]: variant === 'danger', + [styles.light]: variant === 'light', [styles.disabled]: disabled, })} disabled={disabled} diff --git a/components/common/Button.module.css b/components/common/Button.module.css index 99c7168f31..faae656bb7 100644 --- a/components/common/Button.module.css +++ b/components/common/Button.module.css @@ -55,7 +55,16 @@ background: var(--red400); } +.light { + background: var(--gray50); +} + +.light:hover { + background: var(--gray75); +} + .button:disabled { + cursor: default; color: var(--gray500); background: var(--gray75); } @@ -67,3 +76,7 @@ .button:disabled:hover { background: var(--gray75); } + +.button.light:disabled { + background: var(--gray50); +} diff --git a/components/common/Calendar.js b/components/common/Calendar.js index 8ecf76a89f..d9c281d394 100644 --- a/components/common/Calendar.js +++ b/components/common/Calendar.js @@ -160,7 +160,7 @@ const MonthSelector = ({ date, minDate, maxDate, locale, onSelect }) => { const start = startOfYear(date); const months = []; for (let i = 0; i < 12; i++) { - months.push(endOfMonth(addMonths(start, i))); + months.push(addMonths(start, i)); } function handleSelect(value) { @@ -173,7 +173,8 @@ const MonthSelector = ({ date, minDate, maxDate, locale, onSelect }) => { {chunk(months, 3).map((row, i) => ( {row.map((month, j) => { - const disabled = isBefore(month, minDate) || isAfter(month, maxDate); + const disabled = + isBefore(endOfMonth(month), minDate) || isAfter(startOfMonth(month), maxDate); return ( {
); diff --git a/components/common/Calendar.module.css b/components/common/Calendar.module.css index ccfde683b7..ee5581bd46 100644 --- a/components/common/Calendar.module.css +++ b/components/common/Calendar.module.css @@ -3,6 +3,7 @@ flex-direction: column; font-size: var(--font-size-small); flex: 1; + min-height: 285px; } .calendar table { @@ -37,7 +38,7 @@ } .calendar td.disabled { - color: var(--gray300); + color: var(--gray400); background: var(--gray75); } @@ -47,7 +48,7 @@ } .calendar td.faded.disabled { - color: var(--gray200); + background: var(--gray100); } .header { diff --git a/components/common/DateFilter.js b/components/common/DateFilter.js index a7ac6372ca..c43cb860c8 100644 --- a/components/common/DateFilter.js +++ b/components/common/DateFilter.js @@ -7,20 +7,33 @@ import DatePickerForm from 'components/forms/DatePickerForm'; import useLocale from 'hooks/useLocale'; import { getDateRange } from 'lib/date'; import { dateFormat } from 'lib/lang'; +import Calendar from 'assets/calendar-alt.svg'; +import Icon from './Icon'; const filterOptions = [ + { label: , value: '1day' }, { label: ( ), value: '24hour', }, + { + label: , + value: '1week', + divider: true, + }, { label: ( ), value: '7day', }, + { + label: , + value: '1month', + divider: true, + }, { label: ( @@ -33,26 +46,22 @@ const filterOptions = [ ), value: '90day', }, - { label: , value: '1day' }, - { label: , value: '1week' }, - { - label: , - value: '1month', - }, { label: , value: '1year' }, { label: , value: 'custom', + divider: true, }, ]; export default function DateFilter({ value, startDate, endDate, onChange, className }) { - const [locale] = useLocale(); const [showPicker, setShowPicker] = useState(false); const displayValue = - value === 'custom' - ? `${dateFormat(startDate, 'd LLL y', locale)} — ${dateFormat(endDate, 'd LLL y', locale)}` - : value; + value === 'custom' ? ( + handleChange('custom')} /> + ) : ( + value + ); function handleChange(value) { if (value === 'custom') { @@ -90,3 +99,20 @@ export default function DateFilter({ value, startDate, endDate, onChange, classN ); } + +const CustomRange = ({ startDate, endDate, onClick }) => { + const [locale] = useLocale(); + + function handleClick(e) { + e.stopPropagation(); + + onClick(); + } + + return ( + <> + } className="mr-2" onClick={handleClick} /> + {`${dateFormat(startDate, 'd LLL y', locale)} — ${dateFormat(endDate, 'd LLL y', locale)}`} + + ); +}; diff --git a/components/common/DropDown.js b/components/common/DropDown.js index 03fa8bb616..df559ef99f 100644 --- a/components/common/DropDown.js +++ b/components/common/DropDown.js @@ -36,8 +36,8 @@ export default function DropDown({ return (
-
{options.find(e => e.value === value)?.label || value}
- } size="small" /> + {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 958305e7a9..250e6c293d 100644 --- a/components/common/Dropdown.module.css +++ b/components/common/Dropdown.module.css @@ -1,15 +1,24 @@ .dropdown { - flex: 1; position: relative; + display: flex; + justify-content: space-between; + align-items: center; border: 1px solid var(--gray500); border-radius: 4px; cursor: pointer; } .value { + flex: 1; display: flex; justify-content: space-between; font-size: var(--font-size-small); - min-width: 140px; + flex-wrap: nowrap; + white-space: nowrap; padding: 4px 16px; + min-width: 160px; +} + +.icon { + padding-left: 10px; } diff --git a/components/common/LanguageButton.js b/components/common/LanguageButton.js index 714ae7f64d..0581a020a0 100644 --- a/components/common/LanguageButton.js +++ b/components/common/LanguageButton.js @@ -20,6 +20,10 @@ export default function LanguageButton({ menuPosition = 'bottom', menuAlign = 'l setShowMenu(false); } + function toggleMenu() { + setShowMenu(state => !state); + } + useDocumentClick(e => { if (!ref.current.contains(e.target)) { setShowMenu(false); @@ -43,7 +47,7 @@ export default function LanguageButton({ menuPosition = 'bottom', menuAlign = 'l )}
- {showMenu && ( diff --git a/components/common/Menu.js b/components/common/Menu.js index 4cb5a88525..283ee1fb3c 100644 --- a/components/common/Menu.js +++ b/components/common/Menu.js @@ -25,7 +25,7 @@ export default function Menu({ {options .filter(({ hidden }) => !hidden) .map(option => { - const { label, value, className: customClassName, render } = option; + const { label, value, className: customClassName, render, divider } = option; return render ? ( render(option) @@ -34,6 +34,7 @@ export default function Menu({ key={value} className={classNames(styles.option, optionClassName, customClassName, { [selectedClassName]: selectedOption === value, + [styles.divider]: divider, })} onClick={e => onSelect(value, e)} > diff --git a/components/common/Menu.module.css b/components/common/Menu.module.css index be394b71c2..9bcd642f0d 100644 --- a/components/common/Menu.module.css +++ b/components/common/Menu.module.css @@ -40,3 +40,7 @@ .right { right: 0; } + +.divider { + border-top: 1px solid var(--gray300); +} diff --git a/components/metrics/MetricCard.module.css b/components/metrics/MetricCard.module.css index ed868f99a6..9c0d9e4613 100644 --- a/components/metrics/MetricCard.module.css +++ b/components/metrics/MetricCard.module.css @@ -3,7 +3,7 @@ flex-direction: column; justify-content: center; min-width: 120px; - margin-right: 20px; + padding-right: 20px; } .value { @@ -16,4 +16,5 @@ .label { font-size: var(--font-size-normal); + white-space: nowrap; } diff --git a/components/metrics/MetricsBar.module.css b/components/metrics/MetricsBar.module.css index 4046634ef7..5f5cb1a7f4 100644 --- a/components/metrics/MetricsBar.module.css +++ b/components/metrics/MetricsBar.module.css @@ -4,6 +4,9 @@ } @media only screen and (max-width: 992px) { + .bar { + justify-content: space-between; + } .bar > div:last-child { display: none; } diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js index 6b319a893f..8cf01a5a88 100644 --- a/components/metrics/WebsiteChart.js +++ b/components/metrics/WebsiteChart.js @@ -57,14 +57,17 @@ export default function WebsiteChart({ stickyClassName={styles.sticky} enabled={stickyHeader} > - - +
+ +
+
+ +
diff --git a/components/metrics/WebsiteChart.module.css b/components/metrics/WebsiteChart.module.css index 3bd9db1931..ea0fcaee05 100644 --- a/components/metrics/WebsiteChart.module.css +++ b/components/metrics/WebsiteChart.module.css @@ -29,3 +29,15 @@ border-bottom: 1px solid var(--gray300); z-index: 3; } + +.filter { + display: flex; + justify-content: flex-end; + align-items: center; +} + +@media only screen and (max-width: 992px) { + .filter { + display: block; + } +} diff --git a/lib/date.js b/lib/date.js index e7e280362c..363e68dd0a 100644 --- a/lib/date.js +++ b/lib/date.js @@ -88,14 +88,16 @@ export function getDateRange(value) { } export function getDateRangeValues(startDate, endDate) { - if (differenceInHours(endDate, startDate) <= 48) { - return { startDate: startOfHour(startDate), endDate: endOfHour(endDate), unit: 'hour' }; + let unit = 'year'; + if (differenceInHours(endDate, startDate) <= 72) { + unit = 'hour'; } else if (differenceInCalendarDays(endDate, startDate) <= 90) { - return { startDate: startOfDay(startDate), endDate: endOfDay(endDate), unit: 'day' }; + unit = 'day'; } else if (differenceInCalendarMonths(endDate, startDate) <= 24) { - return { startDate: startOfMonth(startDate), endDate: endOfMonth(endDate), unit: 'month' }; + unit = 'month'; } - return { startDate: startOfYear(startDate), endDate: endOfYear(endDate), unit: 'year' }; + + return { startDate: startOfDay(startDate), endDate: endOfDay(endDate), unit }; } const dateFuncs = { @@ -112,11 +114,12 @@ export function getDateArray(data, startDate, endDate, unit) { function findData(t) { const x = data.find(e => { - if (unit === 'day') { - const [year, month, day] = e.t.split('-'); - return normalize(new Date(year, month - 1, day)).getTime() === t.getTime(); + if (unit === 'hour') { + return normalize(new Date(e.t)).getTime() === t.getTime(); } - return normalize(new Date(e.t)).getTime() === t.getTime(); + + const [year, month, day] = e.t.split('-'); + return normalize(new Date(year, month - 1, day)).getTime() === t.getTime(); }); return x?.y || 0; diff --git a/lib/lang.js b/lib/lang.js index 3a91f118fb..f66aa0ac24 100644 --- a/lib/lang.js +++ b/lib/lang.js @@ -37,9 +37,9 @@ export const menuOptions = [ { label: 'Nederlands (Dutch)', value: 'nl-NL', display: 'NL' }, { label: 'Deutsch (German)', value: 'de-DE', display: 'DE' }, { label: '日本語 (Japanese)', value: 'ja-JP', display: '日本語' }, + { label: 'Español (Mexicano)', value: 'es-MX', display: 'ES' }, { label: 'Русский (Russian)', value: 'ru-RU', display: 'РУ' }, { label: 'Turkish', value: 'tr-TR', display: 'TR' }, - { label: 'Español (Mexicano)', value: 'es-MX', display: 'ES' }, ]; export function dateFormat(date, str, locale) { diff --git a/pages/api/website/[id]/events.js b/pages/api/website/[id]/events.js index c230acd1c6..143c745a26 100644 --- a/pages/api/website/[id]/events.js +++ b/pages/api/website/[id]/events.js @@ -3,7 +3,7 @@ import { getEvents } from 'lib/queries'; import { ok, badRequest, methodNotAllowed } from 'lib/response'; import { useAuth } from 'lib/middleware'; -const unitTypes = ['month', 'hour', 'day']; +const unitTypes = ['year', 'month', 'hour', 'day']; export default async (req, res) => { await useAuth(req, res); From 0d4a2e2a0ea84e446180a16d4464fe7ca4289805 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 13 Sep 2020 19:01:02 -0700 Subject: [PATCH 8/9] Update locale files and scripts. --- lang/de-DE.json | 2 ++ lang/en-US.json | 2 ++ lang/es-MX.json | 3 +++ lang/ja-JP.json | 2 ++ lang/nl-NL.json | 2 ++ lang/ru-RU.json | 2 ++ lang/tr-TR.json | 2 ++ lang/zh-CN.json | 2 ++ lib/lang.js | 12 ++++++------ package.json | 6 +++--- scripts/format-lang.js | 2 +- scripts/merge-lang.js | 12 ++++++------ 12 files changed, 33 insertions(+), 16 deletions(-) diff --git a/lang/de-DE.json b/lang/de-DE.json index 0128943eca..df6adaa14f 100644 --- a/lang/de-DE.json +++ b/lang/de-DE.json @@ -12,12 +12,14 @@ "button.more": "Mehr", "button.save": "Speichern", "button.view-details": "Details anzeigen", + "button.websites": "Webseiten", "footer.powered-by": "Powered by", "header.nav.dashboard": "Übersicht", "header.nav.settings": "Einstellungen", "label.administrator": "Administrator", "label.confirm-password": "Passwort wiederholen", "label.current-password": "Derzeitiges Passwort", + "label.custom-range": "Custom range", "label.domain": "Domain", "label.enable-share-url": "Freigabe-URL aktivieren", "label.invalid": "Ungültig", diff --git a/lang/en-US.json b/lang/en-US.json index e9ebdf6b6d..e24b93477c 100644 --- a/lang/en-US.json +++ b/lang/en-US.json @@ -12,12 +12,14 @@ "button.more": "More", "button.save": "Save", "button.view-details": "View details", + "button.websites": "Websites", "footer.powered-by": "Powered by", "header.nav.dashboard": "Dashboard", "header.nav.settings": "Settings", "label.administrator": "Administrator", "label.confirm-password": "Confirm password", "label.current-password": "Current password", + "label.custom-range": "Custom range", "label.domain": "Domain", "label.enable-share-url": "Enable share URL", "label.invalid": "Invalid", diff --git a/lang/es-MX.json b/lang/es-MX.json index 783992d872..e089ef7fcc 100644 --- a/lang/es-MX.json +++ b/lang/es-MX.json @@ -12,12 +12,14 @@ "button.more": "Más", "button.save": "Guardar", "button.view-details": "Ver detalles", + "button.websites": "Sitios", "footer.powered-by": "Desarrollado con", "header.nav.dashboard": "Panel de control", "header.nav.settings": "Configuraciones", "label.administrator": "Administrador", "label.confirm-password": "Confirmar contraseña", "label.current-password": "Contraseña actual", + "label.custom-range": "Custom range", "label.domain": "Dominio", "label.enable-share-url": "Habilitar compartir URL", "label.invalid": "Inválido", @@ -42,6 +44,7 @@ "message.failure": "Algo falló.", "message.incorrect-username-password": "Nombre de usuario o contraseña incorrectos.", "message.no-data-available": "Sin información disponible.", + "message.page-not-found": "Page not found", "message.save-success": "Guardado exitosamente.", "message.share-url": "Esta es la URL compartida públicamente para {target}.", "message.track-stats": "Para registrar estadísticas para {target}, copia el siguiente código dentro de la etiqueta {head} de tu sitio.", diff --git a/lang/ja-JP.json b/lang/ja-JP.json index 7d28642833..cdeec4c896 100644 --- a/lang/ja-JP.json +++ b/lang/ja-JP.json @@ -12,12 +12,14 @@ "button.more": "さらに表示", "button.save": "保存", "button.view-details": "詳細表示", + "button.websites": "Webサイト", "footer.powered-by": "Powered by", "header.nav.dashboard": "ダッシュボード", "header.nav.settings": "設定", "label.administrator": "管理者", "label.confirm-password": "パスワード(確認)", "label.current-password": "現在のパスワード", + "label.custom-range": "Custom range", "label.domain": "ドメイン", "label.enable-share-url": "共有リンクを有効にする", "label.invalid": "無効", diff --git a/lang/nl-NL.json b/lang/nl-NL.json index 5e02d4938f..8d97ca436c 100644 --- a/lang/nl-NL.json +++ b/lang/nl-NL.json @@ -12,12 +12,14 @@ "button.more": "Toon meer", "button.save": "Opslaan", "button.view-details": "Meer details", + "button.websites": "Websites", "footer.powered-by": "mogelijk gemaakt door", "header.nav.dashboard": "Dashboard", "header.nav.settings": "Instellingen", "label.administrator": "Administrator", "label.confirm-password": "Wachtwoord bevestigen", "label.current-password": "Huidig wachtwoord", + "label.custom-range": "Custom range", "label.domain": "Domein", "label.enable-share-url": "Sta delen via openbare URL toe", "label.invalid": "Ongeldig", diff --git a/lang/ru-RU.json b/lang/ru-RU.json index a173c970b3..5c6e9df7c3 100644 --- a/lang/ru-RU.json +++ b/lang/ru-RU.json @@ -12,12 +12,14 @@ "button.more": "Больше", "button.save": "Сохранить", "button.view-details": "Посмотреть детали", + "button.websites": "Сайты", "footer.powered-by": "на движке", "header.nav.dashboard": "Информационная панель", "header.nav.settings": "Настройки", "label.administrator": "Администратор", "label.confirm-password": "Подтвердить пароль", "label.current-password": "Текущий пароль", + "label.custom-range": "Custom range", "label.domain": "Домен", "label.enable-share-url": "Разрешить делиться ссылкой", "label.invalid": "Некорректный", diff --git a/lang/tr-TR.json b/lang/tr-TR.json index 20a1c44104..8e9f100caa 100644 --- a/lang/tr-TR.json +++ b/lang/tr-TR.json @@ -12,12 +12,14 @@ "button.more": "Detaylı göster", "button.save": "Kaydet", "button.view-details": "Detayı incele", + "button.websites": "Web siteleri", "footer.powered-by": "Sağlayıcı:", "header.nav.dashboard": "Kontrol Paneli", "header.nav.settings": "Ayarlar", "label.administrator": "Yönetici", "label.confirm-password": "Parolayı onayla", "label.current-password": "Mevcut parola", + "label.custom-range": "Custom range", "label.domain": "Alan adı", "label.enable-share-url": "Anonim paylaşım URL'i aktif", "label.invalid": "Geçeriz", diff --git a/lang/zh-CN.json b/lang/zh-CN.json index c050e6a6b9..1f21eb0f8c 100644 --- a/lang/zh-CN.json +++ b/lang/zh-CN.json @@ -12,12 +12,14 @@ "button.more": "更多", "button.save": "保存", "button.view-details": "查看更多", + "button.websites": "网站", "footer.powered-by": "运行", "header.nav.dashboard": "仪表板", "header.nav.settings": "设置", "label.administrator": "管理员", "label.confirm-password": "确认密码", "label.current-password": "目前密码", + "label.custom-range": "Custom range", "label.domain": "域名", "label.enable-share-url": "激活共享链接", "label.invalid": "输入无效", diff --git a/lib/lang.js b/lib/lang.js index f66aa0ac24..3f097a2eb2 100644 --- a/lib/lang.js +++ b/lib/lang.js @@ -33,12 +33,12 @@ export const dateLocales = { export const menuOptions = [ { label: 'English', value: 'en', display: 'EN' }, - { label: '中文 (Chinese Simplified)', value: 'zh-CN', display: '中文' }, - { label: 'Nederlands (Dutch)', value: 'nl-NL', display: 'NL' }, - { label: 'Deutsch (German)', value: 'de-DE', display: 'DE' }, - { label: '日本語 (Japanese)', value: 'ja-JP', display: '日本語' }, - { label: 'Español (Mexicano)', value: 'es-MX', display: 'ES' }, - { label: 'Русский (Russian)', value: 'ru-RU', display: 'РУ' }, + { label: '中文', value: 'zh-CN', display: 'CN' }, + { label: 'Deutsch', value: 'de-DE', display: 'DE' }, + { label: 'Español', value: 'es-MX', display: 'ES' }, + { label: '日本語', value: 'ja-JP', display: 'JP' }, + { label: 'Nederlands', value: 'nl-NL', display: 'NL' }, + { label: 'Русский', value: 'ru-RU', display: 'RU' }, { label: 'Turkish', value: 'tr-TR', display: 'TR' }, ]; diff --git a/package.json b/package.json index f9541179a6..25c50c7ddd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "0.31.0", + "version": "0.32.0", "description": "A simple, fast, website analytics alternative to Google Analytics. ", "author": "Mike Cao ", "license": "MIT", @@ -23,10 +23,10 @@ "build-postgresql-schema": "dotenv prisma introspect -- --schema=./prisma/schema.postgresql.prisma", "build-postgresql-client": "dotenv prisma generate -- --schema=./prisma/schema.postgresql.prisma", "build-lang": "npm-run-all format-lang compile-lang", - "extract-lang": "formatjs extract {pages,components}/**/*.js --out-file lang/en-US.json", + "extract-lang": "formatjs extract {pages,components}/**/*.js --out-file build/messages.json", "merge-lang": "node scripts/merge-lang.js", "format-lang": "node scripts/format-lang.js", - "compile-lang": "formatjs compile-folder --ast lang-formatted lang-compiled" + "compile-lang": "formatjs compile-folder --ast build lang-compiled" }, "lint-staged": { "**/*.js": [ diff --git a/scripts/format-lang.js b/scripts/format-lang.js index f26c024999..5b03239ea4 100644 --- a/scripts/format-lang.js +++ b/scripts/format-lang.js @@ -3,7 +3,7 @@ const path = require('path'); const prettier = require('prettier'); const src = path.resolve(__dirname, '../lang'); -const dest = path.resolve(__dirname, '../lang-formatted'); +const dest = path.resolve(__dirname, '../build'); const files = fs.readdirSync(src); if (!fs.existsSync(dest)) { diff --git a/scripts/merge-lang.js b/scripts/merge-lang.js index 885bf75973..f1b86df988 100644 --- a/scripts/merge-lang.js +++ b/scripts/merge-lang.js @@ -1,11 +1,11 @@ const fs = require('fs'); const path = require('path'); const prettier = require('prettier'); -const root = require('../lang/en-US.json'); +const messages = require('../build/messages.json'); -const dir = path.resolve(__dirname, '../lang'); -const files = fs.readdirSync(dir); -const keys = Object.keys(root).sort(); +const dest = path.resolve(__dirname, '../lang'); +const files = fs.readdirSync(dest); +const keys = Object.keys(messages).sort(); files.forEach(file => { const lang = require(`../lang/${file}`); @@ -15,7 +15,7 @@ files.forEach(file => { const merged = keys.reduce((obj, key) => { const message = lang[key]; - obj[key] = message || root[key]; + obj[key] = message || messages[key].defaultMessage; if (!message) { console.log(`* Added key ${key}`); @@ -26,5 +26,5 @@ files.forEach(file => { const json = prettier.format(JSON.stringify(merged), { parser: 'json' }); - fs.writeFileSync(path.resolve(dir, file), json); + fs.writeFileSync(path.resolve(dest, file), json); }); From 38ec91c48eabebc83a7c5310188b276a42292898 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 13 Sep 2020 20:09:18 -0700 Subject: [PATCH 9/9] Update chart tooltip. --- components/metrics/BarChart.js | 15 ++++++++++++--- components/metrics/MetricCard.module.css | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/components/metrics/BarChart.js b/components/metrics/BarChart.js index 96eeaf37e7..1c66504dd2 100644 --- a/components/metrics/BarChart.js +++ b/components/metrics/BarChart.js @@ -44,9 +44,9 @@ export default function BarChart({ return dateFormat(d, 'EEE M/d', locale); case 'month': if (w <= 660) { - return dateFormat(d, 'MMM', locale); + return index % 2 === 0 ? dateFormat(d, 'MMM', locale) : ''; } - return dateFormat(d, 'MMMM', locale); + return dateFormat(d, 'MMM', locale); default: return label; } @@ -65,7 +65,7 @@ export default function BarChart({ const [label, value] = body[0].lines[0].split(':'); setTooltip({ - title: dateFormat(new Date(+title[0]), 'EEE MMMM d yyyy', locale), + title: dateFormat(new Date(+title[0]), getTooltipFormat(unit), locale), value, label, labelColor: labelColors[0].backgroundColor, @@ -73,6 +73,15 @@ export default function BarChart({ } } + function getTooltipFormat(unit) { + switch (unit) { + case 'hour': + return 'EEE ha — MMM d yyyy'; + default: + return 'EEE MMMM d yyyy'; + } + } + function createChart() { const options = { animation: { diff --git a/components/metrics/MetricCard.module.css b/components/metrics/MetricCard.module.css index 9c0d9e4613..3f3405eeb9 100644 --- a/components/metrics/MetricCard.module.css +++ b/components/metrics/MetricCard.module.css @@ -2,7 +2,7 @@ display: flex; flex-direction: column; justify-content: center; - min-width: 120px; + min-width: 140px; padding-right: 20px; }