More refactoring and clean-up.

This commit is contained in:
Mike Cao 2023-02-10 03:26:57 -08:00
parent f062cdbed2
commit c815e7cd51
16 changed files with 101 additions and 134 deletions

View File

@ -1,24 +1,11 @@
import PropTypes from 'prop-types'; import { ButtonGroup, Button, Flexbox } from 'react-basics';
import ButtonLayout from 'components/layout/ButtonLayout';
import { ButtonGroup } from 'react-basics';
function FilterButtons({ buttons, selected, onClick }) { export default function FilterButtons({ items, selected, onSelect }) {
return ( return (
<ButtonLayout> <Flexbox justifyContent="center">
<ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} /> <ButtonGroup items={items} selectedKey={selected} onSelect={onSelect}>
</ButtonLayout> {({ key, label }) => <Button key={key}>{label}</Button>}
</ButtonGroup>
</Flexbox>
); );
} }
FilterButtons.propTypes = {
buttons: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.node,
value: PropTypes.any.isRequired,
}),
),
selected: PropTypes.any,
onClick: PropTypes.func,
};
export default FilterButtons;

View File

@ -1,23 +1,32 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
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({ className, stickyClassName, stickyStyle, children }) { export default function StickyHeader({
const { ref, isSticky } = useSticky(); className,
const initialWidth = useRef(0); stickyClassName,
stickyStyle,
useEffect(() => { enabled = true,
initialWidth.current = ref.current.clientWidth; children,
}, [ref]); }) {
const { ref: scrollRef, isSticky } = useSticky({ scrollElementId: UI_LAYOUT_BODY });
const { ref: measureRef, dimensions } = useMeasure();
return ( return (
<div <div
ref={ref} ref={measureRef}
data-sticky={isSticky} data-sticky={enabled && isSticky}
className={classNames(className, { [stickyClassName]: isSticky })} style={enabled && isSticky ? { height: dimensions.height } : null}
style={isSticky ? { ...stickyStyle, width: initialWidth.current } : null} >
<div
ref={scrollRef}
className={classNames(className, { [stickyClassName]: enabled && isSticky })}
style={enabled && isSticky ? { ...stickyStyle, width: dimensions.width } : null}
> >
{children} {children}
</div> </div>
</div>
); );
} }

View File

@ -2,6 +2,7 @@ import { Container } from 'react-basics';
import Head from 'next/head'; import Head from 'next/head';
import NavBar from 'components/layout/NavBar'; import NavBar from 'components/layout/NavBar';
import useRequireLogin from 'hooks/useRequireLogin'; import useRequireLogin from 'hooks/useRequireLogin';
import { UI_LAYOUT_BODY } from 'lib/constants';
import styles from './AppLayout.module.css'; import styles from './AppLayout.module.css';
export default function AppLayout({ title, children }) { export default function AppLayout({ title, children }) {
@ -19,7 +20,7 @@ export default function AppLayout({ title, children }) {
<div className={styles.nav}> <div className={styles.nav}>
<NavBar /> <NavBar />
</div> </div>
<div className={styles.body} id="layout-body"> <div className={styles.body} id={UI_LAYOUT_BODY}>
<Container> <Container>
<main>{children}</main> <main>{children}</main>
</Container> </Container>

View File

@ -1,16 +0,0 @@
import classNames from 'classnames';
import styles from './ButtonLayout.module.css';
export default function ButtonLayout({ className, children, align = 'center' }) {
return (
<div
className={classNames(styles.buttons, className, {
[styles.left]: align === 'left',
[styles.center]: align === 'center',
[styles.right]: align === 'right',
})}
>
{children}
</div>
);
}

View File

@ -1,21 +0,0 @@
.buttons {
display: flex;
align-items: center;
width: 100%;
}
.buttons button + * {
margin-left: 10px;
}
.center {
justify-content: center;
}
.left {
justify-content: flex-start;
}
.right {
justify-content: flex-end;
}

View File

@ -2,7 +2,7 @@
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 30px;
background: var(--base50); background: var(--base50);
position: relative; position: relative;
padding: 30px;
} }

View File

@ -79,6 +79,10 @@ export const labels = defineMessages({
query: { id: 'label.query-parameters', defaultMessage: 'Query parameters' }, query: { id: 'label.query-parameters', defaultMessage: 'Query parameters' },
back: { id: 'label.back', defaultMessage: 'Back' }, back: { id: 'label.back', defaultMessage: 'Back' },
visitors: { id: 'label.visitors', defaultMessage: 'Visitors' }, visitors: { id: 'label.visitors', defaultMessage: 'Visitors' },
filterCombined: { id: 'label.filter-combined', defaultMessage: 'Combined' },
filterRaw: { id: 'label.filter-raw', defaultMessage: 'Raw' },
views: { id: 'label.views', defaultMessage: 'View' },
none: { id: 'label.none', defaultMessage: 'None' },
}); });
export const messages = defineMessages({ export const messages = defineMessages({

View File

@ -1,31 +1,25 @@
import { useState } from 'react'; import { useState } from 'react';
import { useIntl, defineMessage } from 'react-intl'; import { useIntl } from 'react-intl';
import FilterLink from 'components/common/FilterLink'; import FilterLink from 'components/common/FilterLink';
import FilterButtons from 'components/common/FilterButtons'; import FilterButtons from 'components/common/FilterButtons';
import { urlFilter } from 'lib/filters'; import { urlFilter } from 'lib/filters';
import { labels } from 'components/messages';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
export const FILTER_COMBINED = 0; export const FILTER_COMBINED = 0;
export const FILTER_RAW = 1; export const FILTER_RAW = 1;
const messages = defineMessage({
combined: { id: 'metrics.filter.combined', defaultMessage: 'Combined' },
raw: { id: 'metrics.filter.raw', defaultMessage: 'Raw' },
pages: { id: 'metrics.pages', defaultMessage: 'Pages' },
views: { id: 'metrics.views', defaultMessage: 'View' },
});
export default function PagesTable({ websiteId, showFilters, ...props }) { export default function PagesTable({ websiteId, showFilters, ...props }) {
const [filter, setFilter] = useState(FILTER_COMBINED); const [filter, setFilter] = useState(FILTER_COMBINED);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const buttons = [ const buttons = [
{ {
label: formatMessage(messages.combined), label: formatMessage(labels.filterCombined),
value: FILTER_COMBINED, value: FILTER_COMBINED,
}, },
{ {
label: formatMessage(messages.raw), label: formatMessage(labels.filterRaw),
value: FILTER_RAW, value: FILTER_RAW,
}, },
]; ];
@ -38,9 +32,9 @@ export default function PagesTable({ websiteId, showFilters, ...props }) {
<> <>
{showFilters && <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />} {showFilters && <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />}
<MetricsTable <MetricsTable
title={formatMessage(messages.pages)} title={formatMessage(labels.pages)}
type="url" type="url"
metric={formatMessage(messages.views)} metric={formatMessage(labels.views)}
websiteId={websiteId} websiteId={websiteId}
dataFilter={filter !== FILTER_RAW ? urlFilter : null} dataFilter={filter !== FILTER_RAW ? urlFilter : null}
renderLabel={renderLink} renderLabel={renderLink}

View File

@ -1,50 +1,42 @@
import { useState } from 'react'; import { useState } from 'react';
import { useIntl, defineMessages } from 'react-intl'; import { useIntl } from 'react-intl';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import FilterButtons from 'components/common/FilterButtons'; import FilterButtons from 'components/common/FilterButtons';
import FilterLink from 'components/common/FilterLink'; import FilterLink from 'components/common/FilterLink';
import { refFilter } from 'lib/filters'; import { refFilter } from 'lib/filters';
import { labels } from 'components/messages';
const FILTER_COMBINED = 0; const FILTER_COMBINED = 0;
const FILTER_RAW = 1; const FILTER_RAW = 1;
const messages = defineMessages({
combined: { id: 'metrics.filter.combined', defaultMessage: 'Combined' },
raw: { id: 'metrics.filter.raw', defaultMessage: 'Raw' },
referrers: { id: 'metrics.referrers', defaultMessage: 'Referrers' },
views: { id: 'metrics.views', defaultMessage: 'Views' },
none: { id: 'label.none', defaultMessage: 'None' },
});
export default function ReferrersTable({ websiteId, showFilters, ...props }) { export default function ReferrersTable({ websiteId, showFilters, ...props }) {
const [filter, setFilter] = useState(FILTER_COMBINED); const [filter, setFilter] = useState(FILTER_COMBINED);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const none = formatMessage(messages.none);
const buttons = [ const items = [
{ {
label: formatMessage(messages.combined), label: formatMessage(labels.filterCombined),
value: FILTER_COMBINED, value: FILTER_COMBINED,
}, },
{ label: formatMessage(messages.raw), value: FILTER_RAW }, { label: formatMessage(labels.filterRaw), value: FILTER_RAW },
]; ];
const renderLink = ({ w: link, x: referrer }) => { const renderLink = ({ w: link, x: referrer }) => {
return referrer ? ( return referrer ? (
<FilterLink id="referrer" value={referrer} externalUrl={link} /> <FilterLink id="referrer" value={referrer} externalUrl={link} />
) : ( ) : (
`(${none})` `(${formatMessage(labels.none)})`
); );
}; };
return ( return (
<> <>
{showFilters && <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />} {showFilters && <FilterButtons items={items} selected={filter} onSelect={setFilter} />}
<MetricsTable <MetricsTable
{...props} {...props}
title={formatMessage(messages.referrers)} title={formatMessage(labels.referrers)}
type="referrer" type="referrer"
metric={formatMessage(messages.views)} metric={formatMessage(labels.views)}
websiteId={websiteId} websiteId={websiteId}
dataFilter={filter !== FILTER_RAW ? refFilter : null} dataFilter={filter !== FILTER_RAW ? refFilter : null}
renderLabel={renderLink} renderLabel={renderLink}

View File

@ -97,16 +97,12 @@ export default function WebsiteChart({
</a> </a>
</Link> </Link>
)} )}
</WebsiteHeader>
<StickyHeader
className={styles.metrics}
stickyClassName={styles.sticky}
enabled={stickyHeader}
>
<FilterTags <FilterTags
params={{ url, referrer, os, browser, device, country }} params={{ url, referrer, os, browser, device, country }}
onClick={handleCloseFilter} onClick={handleCloseFilter}
/> />
</WebsiteHeader>
<StickyHeader stickyClassName={styles.sticky} enabled={stickyHeader}>
<Row className={styles.header}> <Row className={styles.header}>
<Column xs={12} sm={12} md={12} defaultSize={10}> <Column xs={12} sm={12} md={12} defaultSize={10}>
<MetricsBar websiteId={websiteId} /> <MetricsBar websiteId={websiteId} />

View File

@ -17,15 +17,12 @@
} }
.header { .header {
min-height: 90px;
margin-bottom: 20px;
}
.metrics {
position: relative; position: relative;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
min-height: 90px;
margin-bottom: 20px;
} }
.sticky { .sticky {
@ -35,6 +32,7 @@
border-bottom: 1px solid var(--base300); border-bottom: 1px solid var(--base300);
z-index: 3; z-index: 3;
width: inherit; width: inherit;
padding-top: 20px;
} }
.filter { .filter {

View File

@ -1,8 +1,8 @@
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import MenuButton from 'components/common/MenuButton'; import { Menu, Icon, Text, PopupTrigger, Popup, Item, Button } from 'react-basics';
import Gear from 'assets/gear.svg'; import Icons from 'components/icons';
import { labels } from 'components/messages';
import { saveDashboard } from 'store/dashboard'; import { saveDashboard } from 'store/dashboard';
import { Icon } from 'react-basics';
const messages = defineMessages({ const messages = defineMessages({
toggleCharts: { id: 'message.toggle-charts', defaultMessage: 'Toggle charts' }, toggleCharts: { id: 'message.toggle-charts', defaultMessage: 'Toggle charts' },
@ -33,10 +33,18 @@ export default function DashboardSettingsButton() {
} }
return ( return (
<MenuButton options={menuOptions} onSelect={handleSelect} hideLabel> <PopupTrigger>
<Button>
<Icon> <Icon>
<Gear /> <Icons.Edit />
</Icon> </Icon>
</MenuButton> <Text>{formatMessage(labels.edit)}</Text>
</Button>
<Popup alignment="end">
<Menu variant="popup" items={menuOptions} onSelect={handleSelect}>
{({ label, value }) => <Item key={value}>{label}</Item>}
</Menu>
</Popup>
</PopupTrigger>
); );
} }

View File

@ -1,4 +1,5 @@
import { Row, Column, Menu, Item, Icon, Button } from 'react-basics'; import { Row, Column, Menu, Item, Icon, Button } from 'react-basics';
import { useIntl } from 'react-intl';
import Link from 'next/link'; import Link from 'next/link';
import BrowsersTable from 'components/metrics/BrowsersTable'; import BrowsersTable from 'components/metrics/BrowsersTable';
import CountriesTable from 'components/metrics/CountriesTable'; import CountriesTable from 'components/metrics/CountriesTable';
@ -13,7 +14,7 @@ import EventsTable from 'components/metrics/EventsTable';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import Icons from 'components/icons'; import Icons from 'components/icons';
import { labels } from 'components/messages'; import { labels } from 'components/messages';
import { useIntl } from 'react-intl'; import styles from './WebsiteMenuView.module.css';
const views = { const views = {
url: PagesTable, url: PagesTable,
@ -81,8 +82,8 @@ export default function WebsiteMenuView({ websiteId, websiteDomain }) {
const DetailsComponent = views[view]; const DetailsComponent = views[view];
return ( return (
<Row> <Row className={styles.row}>
<Column> <Column defaultSize={3} className={styles.col}>
<Button> <Button>
<Icon rotate={180}> <Icon rotate={180}>
<Icons.ArrowRight /> <Icons.ArrowRight />
@ -91,15 +92,15 @@ export default function WebsiteMenuView({ websiteId, websiteDomain }) {
</Button> </Button>
<Menu items={items}> <Menu items={items}>
{({ value, label }) => ( {({ value, label }) => (
<Item key={value}>
<Link href={resolve()}> <Link href={resolve()}>
<a> <a>{label}</a>
<Item key={value}>{label}</Item>
</a>
</Link> </Link>
</Item>
)} )}
</Menu> </Menu>
</Column> </Column>
<Column> <Column defaultSize={9} className={styles.col}>
<DetailsComponent <DetailsComponent
websiteId={websiteId} websiteId={websiteId}
websiteDomain={websiteDomain} websiteDomain={websiteDomain}

View File

@ -0,0 +1,13 @@
.row {
border-top: 1px solid var(--base300);
}
.col {
border-left: 1px solid var(--base300);
padding: 30px;
}
.col:first-child {
padding-left: 0;
border-left: 0;
}

View File

@ -1,20 +1,19 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
export default function useSticky( export default function useSticky({ scrollElementId, defaultSticky = false }) {
element = document.getElementById('layout-body'),
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(element.scrollTop > initialTop.current);
}; };
if (initialTop.current === null) { if (initialTop.current === null) {
initialTop.current = ref.current.offsetTop; initialTop.current = ref?.current?.offsetTop;
} }
element.addEventListener('scroll', handleScroll); element.addEventListener('scroll', handleScroll);
@ -22,7 +21,7 @@ export default function useSticky(
return () => { return () => {
element.removeEventListener('scroll', handleScroll); element.removeEventListener('scroll', handleScroll);
}; };
}, [setIsSticky]); }, [ref, setIsSticky]);
return { ref, isSticky }; return { ref, isSticky };
} }

View File

@ -23,6 +23,8 @@ export const DEFAULT_WEBSITE_LIMIT = 10;
export const REALTIME_RANGE = 30; export const REALTIME_RANGE = 30;
export const REALTIME_INTERVAL = 3000; export const REALTIME_INTERVAL = 3000;
export const UI_LAYOUT_BODY = 'ui-layout-body';
export const EVENT_TYPE = { export const EVENT_TYPE = {
pageView: 1, pageView: 1,
customEvent: 2, customEvent: 2,