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/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/Calendar.js b/components/common/Calendar.js index 9ad1be88..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 && ( + + )} +
); } @@ -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 b7e56df1..eb07431b 100644 --- a/components/common/Calendar.module.css +++ b/components/common/Calendar.module.css @@ -3,11 +3,11 @@ 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; } @@ -64,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); } 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/LanguageButton.js b/components/common/LanguageButton.js index 5565b45e..6fe7eb83 100644 --- a/components/common/LanguageButton.js +++ b/components/common/LanguageButton.js @@ -1,12 +1,13 @@ 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 { setItem } from 'lib/web'; +import useLocale from 'hooks/useLocale'; +import useDocumentClick from 'hooks/useDocumentClick'; +import Globe from 'assets/globe.svg'; import styles from './LanguageButton.module.css'; -import useLocale from '../../hooks/useLocale'; export default function LanguageButton({ menuPosition = 'bottom', menuAlign = 'left' }) { const [showMenu, setShowMenu] = useState(false); @@ -16,7 +17,7 @@ export default function LanguageButton({ menuPosition = 'bottom', menuAlign = 'l function handleSelect(value) { setLocale(value); - window.localStorage.setItem('locale', value); + setItem('umami.locale', value); setShowMenu(false); } diff --git a/components/common/Link.module.css b/components/common/Link.module.css index 54cebc0c..d6dc0536 100644 --- a/components/common/Link.module.css +++ b/components/common/Link.module.css @@ -1,12 +1,12 @@ -.link, -.link:active, -.link:visited { +a.link, +a.link:active, +a.link:visited { position: relative; color: #2c2c2c; text-decoration: none; } -.link:before { +a.link:before { content: ''; position: absolute; bottom: -2px; @@ -17,7 +17,7 @@ transition: width 100ms; } -.link:hover:before { +a.link:hover:before { width: 100%; transition: width 100ms; } 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..c5d6c9db --- /dev/null +++ b/components/common/NavMenu.module.css @@ -0,0 +1,20 @@ +.menu { + 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 { + font-weight: 600; +} diff --git a/components/common/RefreshButton.js b/components/common/RefreshButton.js index 60d0976e..1fa02f48 100644 --- a/components/common/RefreshButton.js +++ b/components/common/RefreshButton.js @@ -1,10 +1,11 @@ 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 }) { @@ -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/Header.js b/components/layout/Header.js index 7df55d0d..f275bda3 100644 --- a/components/layout/Header.js +++ b/components/layout/Header.js @@ -26,10 +26,10 @@ export default function Header() { {user ? ( <> - + - + diff --git a/components/layout/MenuLayout.js b/components/layout/MenuLayout.js index f28cc8a3..7183ad10 100644 --- a/components/layout/MenuLayout.js +++ b/components/layout/MenuLayout.js @@ -1,29 +1,37 @@ import React from 'react'; +import { useRouter } from 'next/router'; import classNames from 'classnames'; -import Menu from 'components/common/Menu'; +import NavMenu from 'components/common/NavMenu'; import styles from './MenuLayout.module.css'; export default function MenuLayout({ menu, selectedOption, - onMenuSelect, className, menuClassName, contentClassName, - optionClassName, children, + replace = false, }) { + const router = useRouter(); + + function handleSelect(url) { + if (replace) { + router.replace(url); + } else { + router.push(url); + } + } + return (
- -
+
{children}
diff --git a/components/layout/MenuLayout.module.css b/components/layout/MenuLayout.module.css index 1bdd7473..c0665d4c 100644 --- a/components/layout/MenuLayout.module.css +++ b/components/layout/MenuLayout.module.css @@ -10,25 +10,11 @@ } .container .content { + flex: 1; position: relative; border-left: 1px solid var(--gray300); padding-left: 30px; -} - -.option { - font-size: var(--font-size-normal); - padding: 8px 16px; - cursor: pointer; - margin-right: 30px; - border-radius: 4px; -} - -.option:hover { - background: var(--gray75); -} - -.selected { - font-weight: 600; + margin-left: 30px; } @media only screen and (max-width: 992px) { @@ -40,5 +26,6 @@ border-top: 1px solid var(--gray300); border-left: 0; padding-left: 0; + margin-left: 0; } } diff --git a/components/messages.js b/components/messages.js new file mode 100644 index 00000000..f6b589eb --- /dev/null +++ b/components/messages.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { defineMessages, FormattedMessage } from 'react-intl'; + +export const labels = defineMessages({ + unknown: { id: 'label.unknown', defaultMessage: 'Unknown' }, +}); + +export const devices = defineMessages({ + desktop: { id: 'metrics.device.desktop', defaultMessage: 'Desktop' }, + laptop: { id: 'metrics.device.laptop', defaultMessage: 'Laptop' }, + tablet: { id: 'metrics.device.tablet', defaultMessage: 'Tablet' }, + mobile: { id: 'metrics.device.mobile', defaultMessage: 'Mobile' }, +}); + +export function getDeviceMessage(device) { + return ; +} diff --git a/components/metrics/ActiveUsers.js b/components/metrics/ActiveUsers.js index 83ae00b8..3d7b7001 100644 --- a/components/metrics/ActiveUsers.js +++ b/components/metrics/ActiveUsers.js @@ -4,8 +4,8 @@ import useFetch from 'hooks/useFetch'; import styles from './ActiveUsers.module.css'; import { FormattedMessage } from 'react-intl'; -export default function ActiveUsers({ websiteId, className }) { - const { data } = useFetch(`/api/website/${websiteId}/active`, {}, { interval: 60000 }); +export default function ActiveUsers({ websiteId, token, className }) { + const { data } = useFetch(`/api/website/${websiteId}/active`, { token }, { interval: 60000 }); const count = useMemo(() => { return data?.[0]?.x || 0; }, [data]); @@ -20,7 +20,7 @@ export default function ActiveUsers({ websiteId, className }) {
diff --git a/components/metrics/BrowsersTable.js b/components/metrics/BrowsersTable.js index 36cfe82f..f092e62f 100644 --- a/components/metrics/BrowsersTable.js +++ b/components/metrics/BrowsersTable.js @@ -3,13 +3,14 @@ import { FormattedMessage } from 'react-intl'; import MetricsTable from './MetricsTable'; import { browserFilter } from 'lib/filters'; -export default function BrowsersTable({ websiteId, limit, onExpand }) { +export default function BrowsersTable({ websiteId, token, limit, onExpand }) { return ( } type="browser" metric={} websiteId={websiteId} + token={token} limit={limit} dataFilter={browserFilter} onExpand={onExpand} diff --git a/components/metrics/CountriesTable.js b/components/metrics/CountriesTable.js index e7857b6d..1f516653 100644 --- a/components/metrics/CountriesTable.js +++ b/components/metrics/CountriesTable.js @@ -3,13 +3,20 @@ import MetricsTable from './MetricsTable'; import { countryFilter, percentFilter } from 'lib/filters'; import { FormattedMessage } from 'react-intl'; -export default function CountriesTable({ websiteId, limit, onDataLoad = () => {}, onExpand }) { +export default function CountriesTable({ + websiteId, + token, + limit, + onDataLoad = () => {}, + onExpand, +}) { return ( } type="country" metric={} websiteId={websiteId} + token={token} limit={limit} dataFilter={countryFilter} onDataLoad={data => onDataLoad(percentFilter(data))} diff --git a/components/metrics/DevicesTable.js b/components/metrics/DevicesTable.js index 0f9c54e7..85d2bdfd 100644 --- a/components/metrics/DevicesTable.js +++ b/components/metrics/DevicesTable.js @@ -2,16 +2,19 @@ import React from 'react'; import MetricsTable from './MetricsTable'; import { deviceFilter } from 'lib/filters'; import { FormattedMessage } from 'react-intl'; +import { getDeviceMessage } from 'components/messages'; -export default function DevicesTable({ websiteId, limit, onExpand }) { +export default function DevicesTable({ websiteId, token, limit, onExpand }) { return ( } type="device" metric={} websiteId={websiteId} + token={token} limit={limit} dataFilter={deviceFilter} + renderLabel={({ x }) => getDeviceMessage(x)} onExpand={onExpand} /> ); diff --git a/components/metrics/EventsChart.js b/components/metrics/EventsChart.js index 1d8bb557..4a49d129 100644 --- a/components/metrics/EventsChart.js +++ b/components/metrics/EventsChart.js @@ -3,7 +3,7 @@ import tinycolor from 'tinycolor2'; import BarChart from './BarChart'; import { getTimezone, getDateArray, getDateLength } from 'lib/date'; import useFetch from 'hooks/useFetch'; -import { useDateRange } from 'hooks/useDateRange'; +import useDateRange from 'hooks/useDateRange'; const COLORS = [ '#2680eb', @@ -16,7 +16,7 @@ const COLORS = [ '#85d044', ]; -export default function EventsChart({ websiteId }) { +export default function EventsChart({ websiteId, token }) { const dateRange = useDateRange(websiteId); const { startDate, endDate, unit, modified } = dateRange; const { data } = useFetch( @@ -26,6 +26,7 @@ export default function EventsChart({ websiteId }) { end_at: +endDate, unit, tz: getTimezone(), + token, }, { update: [modified] }, ); diff --git a/components/metrics/EventsTable.js b/components/metrics/EventsTable.js index ccc13411..948b9f7a 100644 --- a/components/metrics/EventsTable.js +++ b/components/metrics/EventsTable.js @@ -3,13 +3,14 @@ import { FormattedMessage } from 'react-intl'; import MetricsTable from './MetricsTable'; import styles from './EventsTable.module.css'; -export default function EventsTable({ websiteId, limit, onExpand, onDataLoad }) { +export default function EventsTable({ websiteId, token, limit, onExpand, onDataLoad }) { return ( } type="event" metric={} websiteId={websiteId} + token={token} limit={limit} renderLabel={({ x }) =>