Fixed sticky header scrolling. Updated settings button.

This commit is contained in:
Mike Cao 2023-03-03 12:37:26 -08:00
parent 5262d19c8b
commit bb99b3eba5
16 changed files with 109 additions and 134 deletions

View File

@ -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 (

View File

@ -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 (

View File

@ -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>
);
}

View File

@ -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;
}

View File

@ -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';

View File

@ -1,6 +1,5 @@
.button {
width: 50px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;

View File

@ -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>
);
}

View File

@ -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) {

View 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>
);
}

View File

@ -18,6 +18,7 @@
.label {
display: flex;
align-items: center;
font-weight: 700;
gap: 5px;
white-space: nowrap;
min-height: 30px;

View File

@ -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} />

View File

@ -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';

View File

@ -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 };
}

View File

@ -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]);

View File

@ -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);

View File

@ -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>
);
}