diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 00000000..2dc5b675 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,19 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security + - enhancement + - bug +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/assets/external-link.svg b/assets/external-link.svg new file mode 100644 index 00000000..ed09306f --- /dev/null +++ b/assets/external-link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/logo.svg b/assets/logo.svg index eca6048b..c80f1668 100644 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -1 +1,2 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/assets/moon.svg b/assets/moon.svg new file mode 100644 index 00000000..6c8955ae --- /dev/null +++ b/assets/moon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/sun.svg b/assets/sun.svg new file mode 100644 index 00000000..ebc20eb2 --- /dev/null +++ b/assets/sun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/WebsiteDetails.js b/components/WebsiteDetails.js index 3a933638..e3a7e2c2 100644 --- a/components/WebsiteDetails.js +++ b/components/WebsiteDetails.js @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; +import { useRouter } from 'next/router'; import classNames from 'classnames'; import WebsiteChart from 'components/metrics/WebsiteChart'; import WorldMap from 'components/common/WorldMap'; @@ -19,12 +20,29 @@ import EventsChart from './metrics/EventsChart'; import useFetch from 'hooks/useFetch'; import Loading from 'components/common/Loading'; -export default function WebsiteDetails({ websiteId }) { - const { data } = useFetch(`/api/website/${websiteId}`); +const views = { + url: PagesTable, + referrer: ReferrersTable, + browser: BrowsersTable, + os: OSTable, + device: DevicesTable, + country: CountriesTable, + event: EventsTable, +}; + +export default function WebsiteDetails({ websiteId, token }) { + const router = useRouter(); + const { data } = useFetch(`/api/website/${websiteId}`, { token }); const [chartLoaded, setChartLoaded] = useState(false); const [countryData, setCountryData] = useState(); const [eventsData, setEventsData] = useState(); - const [expand, setExpand] = useState(); + const { + query: { id, view }, + basePath, + asPath, + } = router; + + const path = `${basePath}/${asPath.split('/')[1]}/${id.join('/')}`; const BackButton = () => ( diff --git a/components/common/Button.module.css b/components/common/Button.module.css index faae656b..324bbb22 100644 --- a/components/common/Button.module.css +++ b/components/common/Button.module.css @@ -3,6 +3,7 @@ justify-content: center; align-items: center; font-size: var(--font-size-normal); + color: var(--gray900); background: var(--gray100); padding: 8px 16px; border-radius: 4px; @@ -18,7 +19,7 @@ } .button:active { - color: initial; + color: var(--gray900); } .large { @@ -56,11 +57,11 @@ } .light { - background: var(--gray50); + background: transparent; } .light:hover { - background: var(--gray75); + background: inherit; } .button:disabled { diff --git a/components/common/ButtonGroup.module.css b/components/common/ButtonGroup.module.css index d18a8e9c..bc60f8d3 100644 --- a/components/common/ButtonGroup.module.css +++ b/components/common/ButtonGroup.module.css @@ -7,6 +7,7 @@ .group .button { border-radius: 0; + color: var(--gray800); background: var(--gray50); border-left: 1px solid var(--gray500); padding: 4px 8px; @@ -24,6 +25,7 @@ margin: 0; } -.selected { +.group .button.selected { + color: var(--gray900); font-weight: 600; } diff --git a/components/common/Calendar.js b/components/common/Calendar.js index d9c281d3..0414ff7f 100644 --- a/components/common/Calendar.js +++ b/components/common/Calendar.js @@ -70,34 +70,36 @@ export default function Calendar({ date, minDate, maxDate, onChange }) { : } size="small" /> - {!selectMonth && !selectYear && ( - - )} - {selectMonth && ( - - )} - {selectYear && ( - - )} +
+ {!selectMonth && !selectYear && ( + + )} + {selectMonth && ( + + )} + {selectYear && ( + + )} +
); } @@ -105,7 +107,7 @@ export default function Calendar({ date, minDate, maxDate, onChange }) { const DaySelector = ({ date, minDate, maxDate, locale, onSelect }) => { const startWeek = startOfWeek(date); const startMonth = startOfMonth(date); - const startDay = subDays(startMonth, startMonth.getDay() + 1); + const startDay = subDays(startMonth, startMonth.getDay()); const month = date.getMonth(); const year = date.getFullYear(); @@ -220,42 +222,46 @@ const YearSelector = ({ date, minDate, maxDate, onSelect }) => { return (
-
+
+ + + {chunk(years, 5).map((row, i) => ( + + {row.map((n, j) => ( + + ))} + + ))} + +
maxYear, + })} + onClick={() => (n < minYear || n > maxYear ? null : handleSelect(n))} + > + {n} +
+
+
+
); }; diff --git a/components/common/Calendar.module.css b/components/common/Calendar.module.css index ee5581bd..9751cf25 100644 --- a/components/common/Calendar.module.css +++ b/components/common/Calendar.module.css @@ -3,11 +3,12 @@ flex-direction: column; font-size: var(--font-size-small); flex: 1; - min-height: 285px; + min-height: 306px; } .calendar table { - flex: 1; + width: 100%; + border-spacing: 5px; } .calendar td { @@ -16,12 +17,14 @@ text-align: center; vertical-align: center; height: 40px; - min-width: 40px; + width: 40px; border-radius: 5px; + border: 1px solid transparent; } .calendar td:hover { - background: var(--gray100); + border: 1px solid var(--gray300); + background: var(--gray75); } .calendar td.faded { @@ -45,6 +48,7 @@ .calendar td.disabled:hover { cursor: default; background: var(--gray75); + border-color: transparent; } .calendar td.faded.disabled { @@ -60,18 +64,34 @@ font-size: var(--font-size-normal); } +.body { + display: flex; +} + .selector { cursor: pointer; } .pager { display: flex; + flex: 1; } .pager button { align-self: center; } +.middle { + flex: 1; +} + +.left, +.right { + display: flex; + justify-content: center; + align-items: center; +} + .left svg { transform: rotate(90deg); } @@ -83,3 +103,9 @@ .icon { margin-left: 10px; } + +@media only screen and (max-width: 992px) { + .calendar table { + max-width: calc(100vw - 30px); + } +} diff --git a/components/common/DateFilter.js b/components/common/DateFilter.js index c43cb860..fb76a081 100644 --- a/components/common/DateFilter.js +++ b/components/common/DateFilter.js @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { endOfYear } from 'date-fns'; +import { endOfYear, isSameDay } from 'date-fns'; import Modal from './Modal'; import DropDown from './DropDown'; import DatePickerForm from 'components/forms/DatePickerForm'; @@ -112,7 +112,8 @@ const CustomRange = ({ startDate, endDate, onClick }) => { return ( <> } className="mr-2" onClick={handleClick} /> - {`${dateFormat(startDate, 'd LLL y', locale)} — ${dateFormat(endDate, 'd LLL y', locale)}`} + {dateFormat(startDate, 'd LLL y', locale)} + {!isSameDay(startDate, endDate) && ` — ${dateFormat(endDate, 'd LLL y', locale)}`} ); }; diff --git a/components/common/DropDown.js b/components/common/DropDown.js index df559ef9..b6b2357b 100644 --- a/components/common/DropDown.js +++ b/components/common/DropDown.js @@ -15,6 +15,7 @@ export default function DropDown({ }) { const [showMenu, setShowMenu] = useState(false); const ref = useRef(); + const selectedOption = options.find(e => e.value === value); function handleShowMenu() { setShowMenu(state => !state); @@ -40,7 +41,13 @@ export default function DropDown({ } className={styles.icon} size="small" /> {showMenu && ( - + )} ); diff --git a/components/common/Dropdown.module.css b/components/common/Dropdown.module.css index 250e6c29..4b94f58f 100644 --- a/components/common/Dropdown.module.css +++ b/components/common/Dropdown.module.css @@ -20,5 +20,5 @@ } .icon { - padding-left: 10px; + padding-left: 20px; } diff --git a/components/common/LanguageButton.js b/components/common/LanguageButton.js deleted file mode 100644 index 0581a020..00000000 --- a/components/common/LanguageButton.js +++ /dev/null @@ -1,65 +0,0 @@ -import React, { useState, useRef } from 'react'; -import Head from 'next/head'; -import Globe from 'assets/globe.svg'; -import useDocumentClick from 'hooks/useDocumentClick'; -import Menu from './Menu'; -import Button from './Button'; -import { menuOptions } from 'lib/lang'; -import styles from './LanguageButton.module.css'; -import useLocale from '../../hooks/useLocale'; - -export default function LanguageButton({ menuPosition = 'bottom', menuAlign = 'left' }) { - const [showMenu, setShowMenu] = useState(false); - const [locale, setLocale] = useLocale(); - const ref = useRef(); - const selectedLocale = menuOptions.find(e => e.value === locale)?.display; - - function handleSelect(value) { - setLocale(value); - window.localStorage.setItem('locale', value); - setShowMenu(false); - } - - function toggleMenu() { - setShowMenu(state => !state); - } - - useDocumentClick(e => { - if (!ref.current.contains(e.target)) { - setShowMenu(false); - } - }); - - return ( - <> - - {locale === 'zh-CN' && ( - - )} - {locale === 'ja-JP' && ( - - )} - -
- - {showMenu && ( - - )} -
- - ); -} diff --git a/components/common/LanguageButton.module.css b/components/common/LanguageButton.module.css deleted file mode 100644 index 55464c4d..00000000 --- a/components/common/LanguageButton.module.css +++ /dev/null @@ -1,9 +0,0 @@ -.container { - display: flex; - position: relative; - cursor: pointer; -} - -.menu { - z-index: 100; -} diff --git a/components/common/Link.module.css b/components/common/Link.module.css index 54cebc0c..24d8f84c 100644 --- a/components/common/Link.module.css +++ b/components/common/Link.module.css @@ -1,23 +1,23 @@ -.link, -.link:active, -.link:visited { +a.link, +a.link:active, +a.link:visited { position: relative; - color: #2c2c2c; + color: var(--gray900); text-decoration: none; } -.link:before { +a.link:before { content: ''; position: absolute; bottom: -2px; width: 0; height: 2px; - background: #2680eb; + background: var(--primary400); opacity: 0.5; transition: width 100ms; } -.link:hover:before { +a.link:hover:before { width: 100%; transition: width 100ms; } diff --git a/components/common/Menu.js b/components/common/Menu.js index 283ee1fb..6421ba55 100644 --- a/components/common/Menu.js +++ b/components/common/Menu.js @@ -33,7 +33,8 @@ export default function Menu({
onSelect(value, e)} diff --git a/components/common/Menu.module.css b/components/common/Menu.module.css index 9bcd642f..369e37c8 100644 --- a/components/common/Menu.module.css +++ b/components/common/Menu.module.css @@ -8,14 +8,14 @@ .option { font-size: var(--font-size-small); font-weight: normal; - background: #fff; + background: var(--gray50); padding: 4px 16px; cursor: pointer; white-space: nowrap; } .option:hover { - background: #f5f5f5; + background: var(--gray100); } .float { @@ -44,3 +44,7 @@ .divider { border-top: 1px solid var(--gray300); } + +.selected { + font-weight: 600; +} diff --git a/components/common/MenuButton.js b/components/common/MenuButton.js new file mode 100644 index 00000000..62306570 --- /dev/null +++ b/components/common/MenuButton.js @@ -0,0 +1,58 @@ +import React, { useState, useRef } from 'react'; +import classNames from 'classnames'; +import Menu from 'components/common/Menu'; +import Button from 'components/common/Button'; +import useDocumentClick from 'hooks/useDocumentClick'; +import styles from './MenuButton.module.css'; + +export default function MenuButton({ + icon, + value, + options, + menuPosition = 'bottom', + menuAlign = 'right', + onSelect, + renderValue, +}) { + const [showMenu, setShowMenu] = useState(false); + const ref = useRef(); + const selectedOption = options.find(e => e.value === value); + + function handleSelect(value) { + onSelect(value); + setShowMenu(false); + } + + function toggleMenu() { + setShowMenu(state => !state); + } + + useDocumentClick(e => { + if (!ref.current.contains(e.target)) { + setShowMenu(false); + } + }); + + return ( +
+ + {showMenu && ( + + )} +
+ ); +} diff --git a/components/common/MenuButton.module.css b/components/common/MenuButton.module.css new file mode 100644 index 00000000..edf8dfc9 --- /dev/null +++ b/components/common/MenuButton.module.css @@ -0,0 +1,24 @@ +.container { + display: flex; + position: relative; + cursor: pointer; +} + +.button { + border: 1px solid transparent; + border-radius: 4px; +} + +.menu { + z-index: 100; +} + +.text { + font-size: var(--font-size-small); +} + +.open, +.open:hover { + background: var(--gray50); + border: 1px solid var(--gray500); +} diff --git a/components/common/Modal.module.css b/components/common/Modal.module.css index 3702e774..bf2491c7 100644 --- a/components/common/Modal.module.css +++ b/components/common/Modal.module.css @@ -16,8 +16,8 @@ right: 0; bottom: 0; margin: auto; - background: var(--gray900); - opacity: 0.1; + background: #000; + opacity: 0.5; } .content { diff --git a/components/common/NavMenu.js b/components/common/NavMenu.js new file mode 100644 index 00000000..6cbe7559 --- /dev/null +++ b/components/common/NavMenu.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { useRouter } from 'next/router'; +import classNames from 'classnames'; +import styles from './NavMenu.module.css'; + +export default function NavMenu({ options = [], className, onSelect = () => {} }) { + const router = useRouter(); + + return ( +
+ {options + .filter(({ hidden }) => !hidden) + .map(option => { + const { label, value, className: customClassName, render } = option; + + return render ? ( + render(option) + ) : ( +
onSelect(value, e)} + > + {label} +
+ ); + })} +
+ ); +} diff --git a/components/common/NavMenu.module.css b/components/common/NavMenu.module.css new file mode 100644 index 00000000..7be73973 --- /dev/null +++ b/components/common/NavMenu.module.css @@ -0,0 +1,22 @@ +.menu { + color: var(--gray800); + border: 1px solid var(--gray500); + border-radius: 4px; + overflow: hidden; + z-index: 2; +} + +.option { + padding: 8px 16px; + cursor: pointer; + border-radius: 4px; +} + +.option:hover { + background: var(--gray75); +} + +.selected { + color: var(--gray900); + font-weight: 600; +} diff --git a/components/common/RefreshButton.js b/components/common/RefreshButton.js index 60d0976e..af754a9c 100644 --- a/components/common/RefreshButton.js +++ b/components/common/RefreshButton.js @@ -1,15 +1,16 @@ import React, { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { FormattedMessage } from 'react-intl'; import { setDateRange } from 'redux/actions/websites'; import Button from './Button'; import Refresh from 'assets/redo.svg'; import Dots from 'assets/ellipsis-h.svg'; -import { useDateRange } from 'hooks/useDateRange'; +import useDateRange from 'hooks/useDateRange'; 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`]); @@ -24,5 +25,13 @@ export default function RefreshButton({ websiteId }) { setLoading(false); }, [completed]); - return - +
+
+
+ + umami + + ), + }} + /> +
{`v${version}`}
diff --git a/components/layout/Footer.module.css b/components/layout/Footer.module.css index 87008340..7c671d7e 100644 --- a/components/layout/Footer.module.css +++ b/components/layout/Footer.module.css @@ -5,11 +5,3 @@ font-size: var(--font-size-small); min-height: 100px; } - -.footer a { - text-decoration: none; -} - -.button { - margin: 0 5px; -} diff --git a/components/layout/FormLayout.module.css b/components/layout/FormLayout.module.css index 0b82ee7c..90e1b8c2 100644 --- a/components/layout/FormLayout.module.css +++ b/components/layout/FormLayout.module.css @@ -39,7 +39,7 @@ } .msg { - color: var(--gray50); + color: var(--msgColor); background: var(--red400); font-size: var(--font-size-small); padding: 4px 8px; diff --git a/components/layout/Header.js b/components/layout/Header.js index 7df55d0d..639db7ea 100644 --- a/components/layout/Header.js +++ b/components/layout/Header.js @@ -3,11 +3,12 @@ import { FormattedMessage } from 'react-intl'; import { useSelector } from 'react-redux'; import classNames from 'classnames'; import Link from 'components/common/Link'; -import UserButton from '../common/UserButton'; -import Icon from '../common/Icon'; +import Icon from 'components/common/Icon'; +import LanguageButton from 'components/settings/LanguageButton'; +import ThemeButton from 'components/settings/ThemeButton'; +import UserButton from 'components/settings/UserButton'; import Logo from 'assets/logo.svg'; import styles from './Header.module.css'; -import LanguageButton from '../common/LanguageButton'; export default function Header() { const user = useSelector(state => state.user); @@ -15,28 +16,29 @@ export default function Header() { return (
-
+
} size="large" className={styles.logo} /> umami
-
-
- {user ? ( - <> - - - - - - - - - - ) : ( - - )} +
+ {user && ( +
+ + + + + + +
+ )} +
+
+
+ + + {user && }
diff --git a/components/layout/Header.module.css b/components/layout/Header.module.css index 6853eeda..63d68ab9 100644 --- a/components/layout/Header.module.css +++ b/components/layout/Header.module.css @@ -13,18 +13,33 @@ .nav { display: flex; - justify-content: flex-end; + justify-content: center; align-items: center; font-size: var(--font-size-normal); font-weight: 600; } -.nav > * { +.nav a + a { margin-left: 40px; } -@media only screen and (max-width: 768px) { +.buttons { + display: flex; + justify-content: flex-end; +} + +@media only screen and (max-width: 992px) { .title { text-align: center; } + + .nav { + font-size: var(--font-size-large); + justify-content: center; + padding: 20px 0; + } + + .buttons { + justify-content: center; + } } diff --git a/components/layout/Layout.js b/components/layout/Layout.js index 021745cc..b16a0717 100644 --- a/components/layout/Layout.js +++ b/components/layout/Layout.js @@ -16,8 +16,8 @@ export default function Layout({ title, children, header = true, footer = true } {header &&
}
{children}
-
{footer &&