mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-14 21:10:34 +01:00
Fixed sticky header scrolling. Updated settings button.
This commit is contained in:
parent
5262d19c8b
commit
bb99b3eba5
@ -1,17 +1,16 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useMeasure } from 'react-basics';
|
||||||
import { useMeasure, useCombinedRefs } from 'react-basics';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import useSticky from 'hooks/useSticky';
|
import useSticky from 'hooks/useSticky';
|
||||||
import { UI_LAYOUT_BODY } from 'lib/constants';
|
|
||||||
|
|
||||||
export default function StickyHeader({
|
export default function StickyHeader({
|
||||||
className,
|
className,
|
||||||
stickyClassName,
|
stickyClassName,
|
||||||
stickyStyle,
|
stickyStyle,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
|
scrollElement,
|
||||||
children,
|
children,
|
||||||
}) {
|
}) {
|
||||||
const { ref: scrollRef, isSticky } = useSticky({ scrollElementId: UI_LAYOUT_BODY });
|
const { ref: scrollRef, isSticky } = useSticky({ scrollElement });
|
||||||
const { ref: measureRef, dimensions } = useMeasure();
|
const { ref: measureRef, dimensions } = useMeasure();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -7,7 +7,7 @@ import Icons from 'components/icons';
|
|||||||
import { labels } from 'components/messages';
|
import { labels } from 'components/messages';
|
||||||
import styles from './LanguageButton.module.css';
|
import styles from './LanguageButton.module.css';
|
||||||
|
|
||||||
export default function LanguageButton({ tooltipPosition = 'top' }) {
|
export default function LanguageButton({ tooltipPosition = 'top', menuPosition = 'right' }) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { locale, saveLocale } = useLocale();
|
const { locale, saveLocale } = useLocale();
|
||||||
const items = Object.keys(languages).map(key => ({ ...languages[key], value: key }));
|
const items = Object.keys(languages).map(key => ({ ...languages[key], value: key }));
|
||||||
@ -25,7 +25,7 @@ export default function LanguageButton({ tooltipPosition = 'top' }) {
|
|||||||
</Icon>
|
</Icon>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Popup position="right" alignment="end">
|
<Popup position={menuPosition} alignment="end">
|
||||||
<div className={styles.menu}>
|
<div className={styles.menu}>
|
||||||
{items.map(({ value, label }) => {
|
{items.map(({ value, label }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -1,49 +1,33 @@
|
|||||||
import { useRef, useState } from 'react';
|
import { useIntl } from 'react-intl';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { Button, Icon, Tooltip, PopupTrigger, Popup, Form, FormRow } from 'react-basics';
|
||||||
import TimezoneSetting from '../pages/settings/profile/TimezoneSetting';
|
import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting';
|
||||||
import DateRangeSetting from '../pages/settings/profile/DateRangeSetting';
|
import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting';
|
||||||
import { Button, Icon } from 'react-basics';
|
import Icons from 'components/icons';
|
||||||
|
import { labels } from 'components/messages';
|
||||||
import styles from './SettingsButton.module.css';
|
import styles from './SettingsButton.module.css';
|
||||||
import Gear from 'assets/gear.svg';
|
|
||||||
import useDocumentClick from '../../hooks/useDocumentClick';
|
|
||||||
|
|
||||||
export default function SettingsButton() {
|
export default function SettingsButton() {
|
||||||
const [show, setShow] = useState(false);
|
const { formatMessage } = useIntl();
|
||||||
const ref = useRef();
|
|
||||||
|
|
||||||
function handleClick() {
|
|
||||||
setShow(state => !state);
|
|
||||||
}
|
|
||||||
|
|
||||||
useDocumentClick(e => {
|
|
||||||
if (!ref.current?.contains(e.target)) {
|
|
||||||
setShow(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.button} ref={ref}>
|
<PopupTrigger>
|
||||||
<Button variant="light" onClick={handleClick}>
|
<Tooltip label={formatMessage(labels.settings)} position="bottom">
|
||||||
<Icon>
|
<Button variant="quiet">
|
||||||
<Gear />
|
<Icon>
|
||||||
</Icon>
|
<Icons.Gear />
|
||||||
</Button>
|
</Icon>
|
||||||
{show && (
|
</Button>
|
||||||
<div className={styles.panel}>
|
</Tooltip>
|
||||||
<dt>
|
<Popup className={styles.popup} position="bottom" alignment="end">
|
||||||
<FormattedMessage id="label.timezone" defaultMessage="Timezone" />
|
<Form>
|
||||||
</dt>
|
<FormRow label={formatMessage(labels.timezone)}>
|
||||||
<dd>
|
|
||||||
<TimezoneSetting />
|
<TimezoneSetting />
|
||||||
</dd>
|
</FormRow>
|
||||||
<dt>
|
<FormRow label={formatMessage(labels.defaultDateRange)}>
|
||||||
<FormattedMessage id="label.default-date-range" defaultMessage="Default date range" />
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
<DateRangeSetting />
|
<DateRangeSetting />
|
||||||
</dd>
|
</FormRow>
|
||||||
</div>
|
</Form>
|
||||||
)}
|
</Popup>
|
||||||
</div>
|
</PopupTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
.button {
|
.popup {
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
background: var(--base50);
|
background: var(--base50);
|
||||||
border: 1px solid var(--base500);
|
border: 1px solid var(--base500);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -14,7 +10,3 @@
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel dd {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useTransition, animated } from 'react-spring';
|
import { useTransition, animated } from 'react-spring';
|
||||||
import { Button, Icon, PopupTrigger, Tooltip } from 'react-basics';
|
import { Button, Icon, Tooltip } from 'react-basics';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import useTheme from 'hooks/useTheme';
|
import useTheme from 'hooks/useTheme';
|
||||||
import Icons from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
.button {
|
.button {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -1,38 +1,31 @@
|
|||||||
import { useRouter } from 'next/router';
|
import { Column, Icon, Row, Text } from 'react-basics';
|
||||||
import { Column, Row } from 'react-basics';
|
import Link from 'next/link';
|
||||||
import classNames from 'classnames';
|
|
||||||
import HamburgerButton from 'components/common/HamburgerButton';
|
|
||||||
import UpdateNotice from 'components/common/UpdateNotice';
|
|
||||||
import LanguageButton from 'components/input/LanguageButton';
|
import LanguageButton from 'components/input/LanguageButton';
|
||||||
import ThemeButton from 'components/input/ThemeButton';
|
import ThemeButton from 'components/input/ThemeButton';
|
||||||
import UserButton from 'components/input/UserButton';
|
|
||||||
import SettingsButton from 'components/input/SettingsButton';
|
import SettingsButton from 'components/input/SettingsButton';
|
||||||
import useConfig from 'hooks/useConfig';
|
import Icons from 'components/icons';
|
||||||
import useUser from 'hooks/useUser';
|
|
||||||
import styles from './Header.module.css';
|
import styles from './Header.module.css';
|
||||||
|
|
||||||
export default function Header({ className }) {
|
export default function Header() {
|
||||||
const { user } = useUser();
|
|
||||||
const { pathname } = useRouter();
|
|
||||||
const { updatesDisabled, adminDisabled } = useConfig();
|
|
||||||
const isSharePage = pathname.includes('/share/');
|
|
||||||
const allowUpdate = user?.isAdmin && !updatesDisabled && !isSharePage;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<header className={styles.header}>
|
||||||
{allowUpdate && <UpdateNotice />}
|
<Row>
|
||||||
<header className={classNames(styles.header, className)}>
|
<Column>
|
||||||
<Row>
|
<Link href="https://umami.is" target="_blank">
|
||||||
<Column className={styles.title}></Column>
|
<a className={styles.title}>
|
||||||
<HamburgerButton />
|
<Icon size="lg">
|
||||||
<Column className={styles.buttons}>
|
<Icons.Logo />
|
||||||
<ThemeButton />
|
</Icon>
|
||||||
<LanguageButton menuAlign="right" />
|
<Text>umami</Text>
|
||||||
<SettingsButton />
|
</a>
|
||||||
{user && !adminDisabled && <UserButton />}
|
</Link>
|
||||||
</Column>
|
</Column>
|
||||||
</Row>
|
<Column className={styles.buttons}>
|
||||||
</header>
|
<ThemeButton tooltipPosition="bottom" />
|
||||||
</>
|
<LanguageButton tooltipPosition="bottom" menuPosition="bottom" />
|
||||||
|
<SettingsButton />
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,42 +2,24 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50px;
|
padding: 30px 30px 0 30px;
|
||||||
border-bottom: 1px solid var(--base300);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
flex: 1;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
font-size: var(--font-size-lg);
|
font-size: var(--font-size-lg);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
display: flex;
|
color: var(--font-color100);
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links {
|
|
||||||
flex: 2;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-size: var(--font-size-md);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links a + a {
|
|
||||||
margin-left: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 992px) {
|
@media only screen and (max-width: 992px) {
|
||||||
|
13
components/layout/ShareLayout.js
Normal file
13
components/layout/ShareLayout.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Container } from 'react-basics';
|
||||||
|
import Header from './Header';
|
||||||
|
import Footer from './Footer';
|
||||||
|
|
||||||
|
export default function ShareLayout({ children }) {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Header />
|
||||||
|
{children}
|
||||||
|
<Footer />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
@ -18,6 +18,7 @@
|
|||||||
.label {
|
.label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
font-weight: 700;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { Button, Icon, Text, Row, Column, Flexbox } from 'react-basics';
|
import { Button, Icon, Text, Row, Column } from 'react-basics';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import PageviewsChart from './PageviewsChart';
|
import PageviewsChart from './PageviewsChart';
|
||||||
import MetricsBar from './MetricsBar';
|
import MetricsBar from './MetricsBar';
|
||||||
@ -14,8 +14,9 @@ import useApi from 'hooks/useApi';
|
|||||||
import useDateRange from 'hooks/useDateRange';
|
import useDateRange from 'hooks/useDateRange';
|
||||||
import useTimezone from 'hooks/useTimezone';
|
import useTimezone from 'hooks/useTimezone';
|
||||||
import usePageQuery from 'hooks/usePageQuery';
|
import usePageQuery from 'hooks/usePageQuery';
|
||||||
import { getDateArray, getDateLength, getDateRangeValues } from 'lib/date';
|
import { getDateArray, getDateLength } from 'lib/date';
|
||||||
import Icons from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
|
import { UI_LAYOUT_BODY } from 'lib/constants';
|
||||||
import { labels } from 'components/messages';
|
import { labels } from 'components/messages';
|
||||||
import styles from './WebsiteChart.module.css';
|
import styles from './WebsiteChart.module.css';
|
||||||
|
|
||||||
@ -82,7 +83,11 @@ export default function WebsiteChart({
|
|||||||
)}
|
)}
|
||||||
</WebsiteHeader>
|
</WebsiteHeader>
|
||||||
<FilterTags websiteId={websiteId} params={{ url, referrer, os, browser, device, country }} />
|
<FilterTags websiteId={websiteId} params={{ url, referrer, os, browser, device, country }} />
|
||||||
<StickyHeader stickyClassName={styles.sticky} enabled={stickyHeader}>
|
<StickyHeader
|
||||||
|
stickyClassName={styles.sticky}
|
||||||
|
enabled={stickyHeader}
|
||||||
|
scrollElement={document.getElementById(UI_LAYOUT_BODY) || document}
|
||||||
|
>
|
||||||
<Row className={styles.header}>
|
<Row className={styles.header}>
|
||||||
<Column>
|
<Column>
|
||||||
<MetricsBar websiteId={websiteId} />
|
<MetricsBar websiteId={websiteId} />
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Icons, Loading } from 'react-basics';
|
import { Loading } from 'react-basics';
|
||||||
import { useIntl } from 'react-intl';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import Page from 'components/layout/Page';
|
import Page from 'components/layout/Page';
|
||||||
import WebsiteChart from 'components/metrics/WebsiteChart';
|
import WebsiteChart from 'components/metrics/WebsiteChart';
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
import { useApi as nextUseApi } from 'next-basics';
|
|
||||||
import { getClientAuthToken } from 'lib/client';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import * as reactQuery from '@tanstack/react-query';
|
import * as reactQuery from '@tanstack/react-query';
|
||||||
|
import { useApi as nextUseApi } from 'next-basics';
|
||||||
|
import { getClientAuthToken } from 'lib/client';
|
||||||
|
import { SHARE_TOKEN_HEADER } from 'lib/constants';
|
||||||
|
import useStore from 'store/app';
|
||||||
|
|
||||||
|
const selector = state => state.shareToken;
|
||||||
|
|
||||||
export default function useApi() {
|
export default function useApi() {
|
||||||
const { basePath } = useRouter();
|
const { basePath } = useRouter();
|
||||||
|
const shareToken = useStore(selector);
|
||||||
|
|
||||||
const { get, post, put, del } = nextUseApi(getClientAuthToken(), basePath);
|
const { get, post, put, del } = nextUseApi(
|
||||||
|
{ authorization: `Bearer ${getClientAuthToken()}`, [SHARE_TOKEN_HEADER]: shareToken?.token },
|
||||||
|
basePath,
|
||||||
|
);
|
||||||
|
|
||||||
return { get, post, put, del, ...reactQuery };
|
return { get, post, put, del, ...reactQuery };
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,23 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
export default function useSticky({ scrollElementId, defaultSticky = false }) {
|
export default function useSticky({ scrollElement = document, defaultSticky = false }) {
|
||||||
const [isSticky, setIsSticky] = useState(defaultSticky);
|
const [isSticky, setIsSticky] = useState(defaultSticky);
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const initialTop = useRef(null);
|
const initialTop = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const element = scrollElementId ? document.getElementById(scrollElementId) : window;
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
setIsSticky(element.scrollTop > initialTop.current);
|
setIsSticky((scrollElement?.scrollTop ?? window.scrollY) > initialTop.current);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (initialTop.current === null) {
|
if (initialTop.current === null) {
|
||||||
initialTop.current = ref?.current?.offsetTop;
|
initialTop.current = ref?.current?.offsetTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
element.addEventListener('scroll', handleScroll);
|
scrollElement.addEventListener('scroll', handleScroll, true);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
element.removeEventListener('scroll', handleScroll);
|
scrollElement.removeEventListener('scroll', handleScroll, true);
|
||||||
};
|
};
|
||||||
}, [ref, setIsSticky]);
|
}, [ref, setIsSticky]);
|
||||||
|
|
||||||
|
10
lib/auth.ts
10
lib/auth.ts
@ -50,8 +50,12 @@ export function isValidToken(token, validation) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canViewWebsite({ user }: Auth, websiteId: string) {
|
export async function canViewWebsite({ user, shareToken }: Auth, websiteId: string) {
|
||||||
if (user.isAdmin) {
|
if (user?.isAdmin) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shareToken?.websiteId === websiteId) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +76,7 @@ export async function canCreateWebsite({ user }: Auth, teamId?: string) {
|
|||||||
if (teamId) {
|
if (teamId) {
|
||||||
const teamUser = await getTeamUser(teamId, user.id);
|
const teamUser = await getTeamUser(teamId, user.id);
|
||||||
|
|
||||||
return hasPermission(teamUser.role, PERMISSIONS.websiteCreate);
|
return hasPermission(teamUser?.role, PERMISSIONS.websiteCreate);
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasPermission(user.role, PERMISSIONS.websiteCreate);
|
return hasPermission(user.role, PERMISSIONS.websiteCreate);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import AppLayout from 'components/layout/AppLayout';
|
import ShareLayout from 'components/layout/ShareLayout';
|
||||||
import WebsiteDetails from 'components/pages/websites/WebsiteDetails';
|
import WebsiteDetails from 'components/pages/websites/WebsiteDetails';
|
||||||
import useShareToken from 'hooks/useShareToken';
|
import useShareToken from 'hooks/useShareToken';
|
||||||
|
|
||||||
@ -14,8 +14,8 @@ export default function SharePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout>
|
<ShareLayout>
|
||||||
<WebsiteDetails websiteId={shareToken.websiteId} />
|
<WebsiteDetails websiteId={shareToken.websiteId} />
|
||||||
</AppLayout>
|
</ShareLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user