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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 { .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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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