mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-24 02:06:19 +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, useCombinedRefs } from 'react-basics';
|
||||
import { useMeasure } from 'react-basics';
|
||||
import classNames from 'classnames';
|
||||
import useSticky from 'hooks/useSticky';
|
||||
import { UI_LAYOUT_BODY } from 'lib/constants';
|
||||
|
||||
export default function StickyHeader({
|
||||
className,
|
||||
stickyClassName,
|
||||
stickyStyle,
|
||||
enabled = true,
|
||||
scrollElement,
|
||||
children,
|
||||
}) {
|
||||
const { ref: scrollRef, isSticky } = useSticky({ scrollElementId: UI_LAYOUT_BODY });
|
||||
const { ref: scrollRef, isSticky } = useSticky({ scrollElement });
|
||||
const { ref: measureRef, dimensions } = useMeasure();
|
||||
|
||||
return (
|
||||
|
@ -7,7 +7,7 @@ import Icons from 'components/icons';
|
||||
import { labels } from 'components/messages';
|
||||
import styles from './LanguageButton.module.css';
|
||||
|
||||
export default function LanguageButton({ tooltipPosition = 'top' }) {
|
||||
export default function LanguageButton({ tooltipPosition = 'top', menuPosition = 'right' }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const { locale, saveLocale } = useLocale();
|
||||
const items = Object.keys(languages).map(key => ({ ...languages[key], value: key }));
|
||||
@ -25,7 +25,7 @@ export default function LanguageButton({ tooltipPosition = 'top' }) {
|
||||
</Icon>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Popup position="right" alignment="end">
|
||||
<Popup position={menuPosition} alignment="end">
|
||||
<div className={styles.menu}>
|
||||
{items.map(({ value, label }) => {
|
||||
return (
|
||||
|
@ -1,49 +1,33 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import TimezoneSetting from '../pages/settings/profile/TimezoneSetting';
|
||||
import DateRangeSetting from '../pages/settings/profile/DateRangeSetting';
|
||||
import { Button, Icon } from 'react-basics';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Button, Icon, Tooltip, PopupTrigger, Popup, Form, FormRow } from 'react-basics';
|
||||
import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting';
|
||||
import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting';
|
||||
import Icons from 'components/icons';
|
||||
import { labels } from 'components/messages';
|
||||
import styles from './SettingsButton.module.css';
|
||||
import Gear from 'assets/gear.svg';
|
||||
import useDocumentClick from '../../hooks/useDocumentClick';
|
||||
|
||||
export default function SettingsButton() {
|
||||
const [show, setShow] = useState(false);
|
||||
const ref = useRef();
|
||||
|
||||
function handleClick() {
|
||||
setShow(state => !state);
|
||||
}
|
||||
|
||||
useDocumentClick(e => {
|
||||
if (!ref.current?.contains(e.target)) {
|
||||
setShow(false);
|
||||
}
|
||||
});
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div className={styles.button} ref={ref}>
|
||||
<Button variant="light" onClick={handleClick}>
|
||||
<Icon>
|
||||
<Gear />
|
||||
</Icon>
|
||||
</Button>
|
||||
{show && (
|
||||
<div className={styles.panel}>
|
||||
<dt>
|
||||
<FormattedMessage id="label.timezone" defaultMessage="Timezone" />
|
||||
</dt>
|
||||
<dd>
|
||||
<PopupTrigger>
|
||||
<Tooltip label={formatMessage(labels.settings)} position="bottom">
|
||||
<Button variant="quiet">
|
||||
<Icon>
|
||||
<Icons.Gear />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Popup className={styles.popup} position="bottom" alignment="end">
|
||||
<Form>
|
||||
<FormRow label={formatMessage(labels.timezone)}>
|
||||
<TimezoneSetting />
|
||||
</dd>
|
||||
<dt>
|
||||
<FormattedMessage id="label.default-date-range" defaultMessage="Default date range" />
|
||||
</dt>
|
||||
<dd>
|
||||
</FormRow>
|
||||
<FormRow label={formatMessage(labels.defaultDateRange)}>
|
||||
<DateRangeSetting />
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</FormRow>
|
||||
</Form>
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,4 @@
|
||||
.button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.panel {
|
||||
.popup {
|
||||
background: var(--base50);
|
||||
border: 1px solid var(--base500);
|
||||
border-radius: 4px;
|
||||
@ -14,7 +10,3 @@
|
||||
padding: 20px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.panel dd {
|
||||
display: flex;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 useTheme from 'hooks/useTheme';
|
||||
import Icons from 'components/icons';
|
||||
|
@ -1,6 +1,5 @@
|
||||
.button {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
@ -1,38 +1,31 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import { Column, Row } from 'react-basics';
|
||||
import classNames from 'classnames';
|
||||
import HamburgerButton from 'components/common/HamburgerButton';
|
||||
import UpdateNotice from 'components/common/UpdateNotice';
|
||||
import { Column, Icon, Row, Text } from 'react-basics';
|
||||
import Link from 'next/link';
|
||||
import LanguageButton from 'components/input/LanguageButton';
|
||||
import ThemeButton from 'components/input/ThemeButton';
|
||||
import UserButton from 'components/input/UserButton';
|
||||
import SettingsButton from 'components/input/SettingsButton';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
import useUser from 'hooks/useUser';
|
||||
import Icons from 'components/icons';
|
||||
import styles from './Header.module.css';
|
||||
|
||||
export default function Header({ className }) {
|
||||
const { user } = useUser();
|
||||
const { pathname } = useRouter();
|
||||
const { updatesDisabled, adminDisabled } = useConfig();
|
||||
const isSharePage = pathname.includes('/share/');
|
||||
const allowUpdate = user?.isAdmin && !updatesDisabled && !isSharePage;
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<>
|
||||
{allowUpdate && <UpdateNotice />}
|
||||
<header className={classNames(styles.header, className)}>
|
||||
<Row>
|
||||
<Column className={styles.title}></Column>
|
||||
<HamburgerButton />
|
||||
<Column className={styles.buttons}>
|
||||
<ThemeButton />
|
||||
<LanguageButton menuAlign="right" />
|
||||
<SettingsButton />
|
||||
{user && !adminDisabled && <UserButton />}
|
||||
</Column>
|
||||
</Row>
|
||||
</header>
|
||||
</>
|
||||
<header className={styles.header}>
|
||||
<Row>
|
||||
<Column>
|
||||
<Link href="https://umami.is" target="_blank">
|
||||
<a className={styles.title}>
|
||||
<Icon size="lg">
|
||||
<Icons.Logo />
|
||||
</Icon>
|
||||
<Text>umami</Text>
|
||||
</a>
|
||||
</Link>
|
||||
</Column>
|
||||
<Column className={styles.buttons}>
|
||||
<ThemeButton tooltipPosition="bottom" />
|
||||
<LanguageButton tooltipPosition="bottom" menuPosition="bottom" />
|
||||
<SettingsButton />
|
||||
</Column>
|
||||
</Row>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
@ -2,42 +2,24 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
border-bottom: 1px solid var(--base300);
|
||||
padding: 30px 30px 0 30px;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
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;
|
||||
color: var(--font-color100);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
gap: 5px;
|
||||
white-space: nowrap;
|
||||
min-height: 30px;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
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 PageviewsChart from './PageviewsChart';
|
||||
import MetricsBar from './MetricsBar';
|
||||
@ -14,8 +14,9 @@ import useApi from 'hooks/useApi';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import useTimezone from 'hooks/useTimezone';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import { getDateArray, getDateLength, getDateRangeValues } from 'lib/date';
|
||||
import { getDateArray, getDateLength } from 'lib/date';
|
||||
import Icons from 'components/icons';
|
||||
import { UI_LAYOUT_BODY } from 'lib/constants';
|
||||
import { labels } from 'components/messages';
|
||||
import styles from './WebsiteChart.module.css';
|
||||
|
||||
@ -82,7 +83,11 @@ export default function WebsiteChart({
|
||||
)}
|
||||
</WebsiteHeader>
|
||||
<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}>
|
||||
<Column>
|
||||
<MetricsBar websiteId={websiteId} />
|
||||
|
@ -1,8 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { Icons, Loading } from 'react-basics';
|
||||
import { useIntl } from 'react-intl';
|
||||
import Link from 'next/link';
|
||||
import classNames from 'classnames';
|
||||
import { Loading } from 'react-basics';
|
||||
import Page from 'components/layout/Page';
|
||||
import WebsiteChart from 'components/metrics/WebsiteChart';
|
||||
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 * 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() {
|
||||
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 };
|
||||
}
|
||||
|
@ -1,25 +1,23 @@
|
||||
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 ref = useRef(null);
|
||||
const initialTop = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const element = scrollElementId ? document.getElementById(scrollElementId) : window;
|
||||
|
||||
const handleScroll = () => {
|
||||
setIsSticky(element.scrollTop > initialTop.current);
|
||||
setIsSticky((scrollElement?.scrollTop ?? window.scrollY) > initialTop.current);
|
||||
};
|
||||
|
||||
if (initialTop.current === null) {
|
||||
initialTop.current = ref?.current?.offsetTop;
|
||||
}
|
||||
|
||||
element.addEventListener('scroll', handleScroll);
|
||||
scrollElement.addEventListener('scroll', handleScroll, true);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener('scroll', handleScroll);
|
||||
scrollElement.removeEventListener('scroll', handleScroll, true);
|
||||
};
|
||||
}, [ref, setIsSticky]);
|
||||
|
||||
|
10
lib/auth.ts
10
lib/auth.ts
@ -50,8 +50,12 @@ export function isValidToken(token, validation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function canViewWebsite({ user }: Auth, websiteId: string) {
|
||||
if (user.isAdmin) {
|
||||
export async function canViewWebsite({ user, shareToken }: Auth, websiteId: string) {
|
||||
if (user?.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (shareToken?.websiteId === websiteId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -72,7 +76,7 @@ export async function canCreateWebsite({ user }: Auth, teamId?: string) {
|
||||
if (teamId) {
|
||||
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);
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 useShareToken from 'hooks/useShareToken';
|
||||
|
||||
@ -14,8 +14,8 @@ export default function SharePage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<ShareLayout>
|
||||
<WebsiteDetails websiteId={shareToken.websiteId} />
|
||||
</AppLayout>
|
||||
</ShareLayout>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user