mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-15 09:45:04 +01:00
More deletes. Fixed sticky header.
This commit is contained in:
parent
87bbaa7f1d
commit
45c13da262
@ -1,28 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Icon.module.css';
|
||||
|
||||
function Icon({ icon, className, size = 'medium', ...props }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.icon, className, {
|
||||
[styles.xlarge]: size === 'xlarge',
|
||||
[styles.large]: size === 'large',
|
||||
[styles.medium]: size === 'medium',
|
||||
[styles.small]: size === 'small',
|
||||
[styles.xsmall]: size === 'xsmall',
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Icon.propTypes = {
|
||||
className: PropTypes.string,
|
||||
icon: PropTypes.node.isRequired,
|
||||
size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']),
|
||||
};
|
||||
|
||||
export default Icon;
|
@ -1,35 +0,0 @@
|
||||
.icon {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.icon svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.xlarge > svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.large > svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.medium > svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.small > svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.xsmall > svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import NextLink from 'next/link';
|
||||
import { Icon } from 'react-basics';
|
||||
import styles from './Link.module.css';
|
||||
|
||||
function Link({ className, icon, children, size, iconRight, onClick, ...props }) {
|
||||
return (
|
||||
<NextLink {...props}>
|
||||
<a
|
||||
className={classNames(styles.link, className, {
|
||||
[styles.large]: size === 'large',
|
||||
[styles.small]: size === 'small',
|
||||
[styles.xsmall]: size === 'xsmall',
|
||||
[styles.iconRight]: iconRight,
|
||||
})}
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon && <Icon className={styles.icon} icon={icon} size={size} />}
|
||||
{children}
|
||||
</a>
|
||||
</NextLink>
|
||||
);
|
||||
}
|
||||
|
||||
Link.propTypes = {
|
||||
className: PropTypes.string,
|
||||
icon: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
size: PropTypes.oneOf(['large', 'small', 'xsmall']),
|
||||
iconRight: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Link;
|
@ -1,42 +0,0 @@
|
||||
a.link,
|
||||
a.link:active,
|
||||
a.link:visited {
|
||||
position: relative;
|
||||
color: var(--base900);
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
a.link span {
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
a.link:hover span {
|
||||
border-bottom: 2px solid var(--primary400);
|
||||
}
|
||||
|
||||
a.link.large {
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
a.link.small {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
a.link.xsmall {
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
a.link .icon + * {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
a.link.iconRight .icon {
|
||||
order: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
a.link.iconRight .icon + * {
|
||||
margin: 0;
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Menu.module.css';
|
||||
|
||||
function Menu({
|
||||
options = [],
|
||||
selectedOption,
|
||||
className,
|
||||
float,
|
||||
align = 'left',
|
||||
optionClassName,
|
||||
selectedClassName,
|
||||
onSelect = () => {},
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.menu, className, {
|
||||
[styles.float]: float,
|
||||
[styles.top]: float === 'top',
|
||||
[styles.bottom]: float === 'bottom',
|
||||
[styles.left]: align === 'left',
|
||||
[styles.right]: align === 'right',
|
||||
})}
|
||||
>
|
||||
{options
|
||||
.filter(({ hidden }) => !hidden)
|
||||
.map(option => {
|
||||
const { label, value, className: customClassName, render, divider } = option;
|
||||
|
||||
return render ? (
|
||||
render(option)
|
||||
) : (
|
||||
<div
|
||||
key={value}
|
||||
className={classNames(styles.option, optionClassName, customClassName, {
|
||||
[selectedClassName]: selectedOption === option,
|
||||
[styles.selected]: selectedOption === option,
|
||||
[styles.divider]: divider,
|
||||
})}
|
||||
onClick={e => onSelect(value, e)}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Menu.propTypes = {
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
render: PropTypes.func,
|
||||
divider: PropTypes.bool,
|
||||
}),
|
||||
),
|
||||
selectedOption: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
float: PropTypes.oneOf(['top', 'bottom']),
|
||||
align: PropTypes.oneOf(['left', 'right']),
|
||||
optionClassName: PropTypes.string,
|
||||
selectedClassName: PropTypes.string,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Menu;
|
@ -1,49 +0,0 @@
|
||||
.menu {
|
||||
background: var(--base50);
|
||||
border: 1px solid var(--base500);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.option {
|
||||
background: var(--base50);
|
||||
padding: 4px 16px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background: var(--base100);
|
||||
}
|
||||
|
||||
.float {
|
||||
position: absolute;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.top {
|
||||
bottom: 100%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
top: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-top: 1px solid var(--base300);
|
||||
}
|
||||
|
||||
.selected {
|
||||
font-weight: 600;
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Menu from 'components/common/Menu';
|
||||
import useDocumentClick from 'hooks/useDocumentClick';
|
||||
import styles from './MenuButton.module.css';
|
||||
import { Button } from 'react-basics';
|
||||
|
||||
function MenuButton({
|
||||
children,
|
||||
value,
|
||||
options,
|
||||
buttonClassName,
|
||||
buttonVariant,
|
||||
menuClassName,
|
||||
menuPosition = 'bottom',
|
||||
menuAlign = 'right',
|
||||
onSelect,
|
||||
renderValue,
|
||||
hideLabel,
|
||||
}) {
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const ref = useRef();
|
||||
const selectedOption = options.find(e => e.value === value);
|
||||
|
||||
function handleSelect(value) {
|
||||
onSelect(value);
|
||||
setShowMenu(false);
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
setShowMenu(state => !state);
|
||||
}
|
||||
|
||||
useDocumentClick(e => {
|
||||
if (!ref.current?.contains(e.target)) {
|
||||
setShowMenu(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.container} ref={ref}>
|
||||
<Button
|
||||
className={classNames(styles.button, buttonClassName, { [styles.open]: showMenu })}
|
||||
onClick={toggleMenu}
|
||||
variant={buttonVariant}
|
||||
>
|
||||
{!hideLabel && (
|
||||
<div className={styles.text}>{renderValue ? renderValue(selectedOption) : value}</div>
|
||||
)}
|
||||
{children}
|
||||
</Button>
|
||||
{showMenu && (
|
||||
<Menu
|
||||
className={menuClassName}
|
||||
options={options}
|
||||
selectedOption={selectedOption}
|
||||
onSelect={handleSelect}
|
||||
float={menuPosition}
|
||||
align={menuAlign}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MenuButton.propTypes = {
|
||||
icon: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
render: PropTypes.func,
|
||||
divider: PropTypes.bool,
|
||||
}),
|
||||
),
|
||||
buttonClassName: PropTypes.string,
|
||||
menuClassName: PropTypes.string,
|
||||
menuPosition: PropTypes.oneOf(['top', 'bottom']),
|
||||
menuAlign: PropTypes.oneOf(['left', 'right']),
|
||||
onSelect: PropTypes.func,
|
||||
renderValue: PropTypes.func,
|
||||
};
|
||||
|
||||
export default MenuButton;
|
@ -1,20 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.open,
|
||||
.open:hover {
|
||||
background: var(--base50);
|
||||
border: 1px solid var(--base500);
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useRouter } from 'next/router';
|
||||
import classNames from 'classnames';
|
||||
import styles from './NavMenu.module.css';
|
||||
|
||||
function NavMenu({ options = [], className, onSelect = () => {} }) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.menu, className)}>
|
||||
{options
|
||||
.filter(({ hidden }) => !hidden)
|
||||
.map(option => {
|
||||
const { label, value, className: customClassName, render } = option;
|
||||
|
||||
return render ? (
|
||||
render(option)
|
||||
) : (
|
||||
<div
|
||||
key={value}
|
||||
className={classNames(styles.option, customClassName, {
|
||||
[styles.selected]: router.asPath === value,
|
||||
})}
|
||||
onClick={e => onSelect(value, e)}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NavMenu.propTypes = {
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
render: PropTypes.func,
|
||||
}),
|
||||
),
|
||||
className: PropTypes.string,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
export default NavMenu;
|
@ -1,22 +0,0 @@
|
||||
.menu {
|
||||
color: var(--base800);
|
||||
border: 1px solid var(--base500);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.option {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background: var(--base75);
|
||||
}
|
||||
|
||||
.selected {
|
||||
color: var(--base900);
|
||||
font-weight: 600;
|
||||
}
|
@ -1,46 +1,15 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import useSticky from 'hooks/useSticky';
|
||||
|
||||
export default function StickyHeader({
|
||||
className,
|
||||
stickyClassName,
|
||||
stickyStyle,
|
||||
children,
|
||||
enabled = true,
|
||||
}) {
|
||||
const [sticky, setSticky] = useState(false);
|
||||
const ref = useRef();
|
||||
const top = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const checkPosition = () => {
|
||||
if (ref.current) {
|
||||
if (!top.current) {
|
||||
top.current = ref.current.offsetTop + ref.current.offsetHeight;
|
||||
}
|
||||
const state = window.pageYOffset > top.current;
|
||||
if (sticky !== state) {
|
||||
setSticky(state);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (enabled) {
|
||||
checkPosition();
|
||||
window.addEventListener('scroll', checkPosition);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', checkPosition);
|
||||
};
|
||||
}, [sticky, enabled]);
|
||||
export default function StickyHeader({ className, stickyClassName, stickyStyle, children }) {
|
||||
const { ref, isSticky } = useSticky();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sticky={sticky}
|
||||
className={classNames(className, { [stickyClassName]: sticky })}
|
||||
style={sticky ? { ...stickyStyle, width: ref?.current?.clientWidth } : null}
|
||||
data-sticky={isSticky}
|
||||
className={classNames(className, { [stickyClassName]: isSticky })}
|
||||
style={isSticky ? { ...stickyStyle, width: ref?.current?.clientWidth } : null}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
@ -2,12 +2,12 @@
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
grid-template-columns: max-content 1fr;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav {
|
||||
grid-row: 1 / 3;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.body {
|
||||
|
@ -3,7 +3,6 @@ import { useRouter } from 'next/router';
|
||||
import Script from 'next/script';
|
||||
import classNames from 'classnames';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
import Link from 'components/common/Link';
|
||||
import { CURRENT_VERSION, HOMEPAGE_URL, REPO_URL } from 'lib/constants';
|
||||
import styles from './Footer.module.css';
|
||||
|
||||
@ -22,15 +21,15 @@ export default function Footer({ className }) {
|
||||
<div>
|
||||
{formatMessage(messages.poweredBy, {
|
||||
name: (
|
||||
<Link href={HOMEPAGE_URL}>
|
||||
<a href={HOMEPAGE_URL}>
|
||||
<b>umami</b>
|
||||
</Link>
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
</Column>
|
||||
<Column className={styles.version}>
|
||||
<Link href={REPO_URL}>{`v${CURRENT_VERSION}`}</Link>
|
||||
<a href={REPO_URL}>{`v${CURRENT_VERSION}`}</a>
|
||||
</Column>
|
||||
</Row>
|
||||
{!pathname.includes('/share/') && <Script src={`/telemetry.js`} />}
|
||||
|
@ -1,30 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import styles from './GridLayout.module.css';
|
||||
|
||||
export default function GridLayout({ className, children }) {
|
||||
return <div className={classNames(styles.grid, className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export const GridRow = ({ className, children }) => {
|
||||
return <div className={classNames(styles.row, className, 'row')}>{children}</div>;
|
||||
};
|
||||
|
||||
export const GridColumn = ({ xs, sm, md, lg, xl, className, children }) => {
|
||||
const classes = [];
|
||||
|
||||
classes.push(xs ? `col-${xs}` : 'col-12');
|
||||
|
||||
if (sm) {
|
||||
classes.push(`col-sm-${sm}`);
|
||||
}
|
||||
if (md) {
|
||||
classes.push(`col-md-${md}`);
|
||||
}
|
||||
if (lg) {
|
||||
classes.push(`col-lg-${lg}`);
|
||||
}
|
||||
if (xl) {
|
||||
classes.push(`col-lg-${xl}`);
|
||||
}
|
||||
return <div className={classNames(styles.col, classes, className)}>{children}</div>;
|
||||
};
|
@ -1,18 +0,0 @@
|
||||
import { Row, cloneChildren } from 'react-basics';
|
||||
import styles from './GridRow.module.css';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function GridRow(props) {
|
||||
const { children, className, ...rowProps } = props;
|
||||
return (
|
||||
<Row {...rowProps} className={className}>
|
||||
{breakpoint =>
|
||||
cloneChildren(children, () => {
|
||||
return {
|
||||
className: classNames(styles.column, styles[breakpoint]),
|
||||
};
|
||||
})
|
||||
}
|
||||
</Row>
|
||||
);
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
.column {
|
||||
padding: 20px;
|
||||
border-top: 1px solid var(--base200);
|
||||
border-left: 1px solid var(--base200);
|
||||
}
|
||||
|
||||
.column:first-child {
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.column:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.column.xs,
|
||||
.column.sm,
|
||||
.column.md {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
}
|
@ -6,10 +6,9 @@
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: var(--font-size-2xl);
|
||||
line-height: 40px;
|
||||
min-height: 40px;
|
||||
font-weight: 600;
|
||||
min-height: 36px;
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Loading, Icons } from 'react-basics';
|
||||
import { Loading, Icon, Text, Button } from 'react-basics';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import Link from 'next/link';
|
||||
import firstBy from 'thenby';
|
||||
import classNames from 'classnames';
|
||||
import Link from 'components/common/Link';
|
||||
import useApi from 'hooks/useApi';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
@ -11,6 +11,7 @@ import usePageQuery from 'hooks/usePageQuery';
|
||||
import ErrorMessage from 'components/common/ErrorMessage';
|
||||
import DataTable from './DataTable';
|
||||
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||
import Icons from 'components/icons';
|
||||
import styles from './MetricsTable.module.css';
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -78,14 +79,15 @@ export default function MetricsTable({
|
||||
{data && !error && <DataTable {...props} data={filteredData} className={className} />}
|
||||
<div className={styles.footer}>
|
||||
{data && !error && limit && (
|
||||
<Link
|
||||
icon={<Icons.ArrowRight />}
|
||||
href={router.pathname}
|
||||
as={resolve({ view: type })}
|
||||
size="small"
|
||||
iconRight
|
||||
>
|
||||
{formatMessage(messages.more)}
|
||||
<Link href={router.pathname} as={resolve({ view: type })}>
|
||||
<a>
|
||||
<Button variant="quiet">
|
||||
<Text>{formatMessage(messages.more)}</Text>
|
||||
<Icon size="sm">
|
||||
<Icons.ArrowRight />
|
||||
</Icon>
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
@ -39,7 +39,7 @@ export default function WebsiteChart({
|
||||
const { get, useQuery } = useApi();
|
||||
|
||||
const { data, isLoading, error } = useQuery(
|
||||
['websites:pageviews', { websiteId, modified, url, referrer, os, browser, device, country }],
|
||||
['websites:pageviews', websiteId, modified, url, referrer, os, browser, device, country],
|
||||
() =>
|
||||
get(`/websites/${websiteId}/pageviews`, {
|
||||
startAt: +startDate,
|
||||
@ -82,10 +82,6 @@ export default function WebsiteChart({
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading icon="dots" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} title={title} domain={domain}>
|
||||
|
@ -40,7 +40,7 @@ export default function WebsiteDetails({ websiteId }) {
|
||||
domain={data?.domain}
|
||||
onDataLoad={handleDataLoad}
|
||||
showLink={false}
|
||||
stickyHeader
|
||||
stickyHeader={true}
|
||||
/>
|
||||
{!chartLoaded && <Loading icon="dots" />}
|
||||
{chartLoaded && (
|
||||
|
@ -9,6 +9,7 @@ import WorldMap from 'components/common/WorldMap';
|
||||
import CountriesTable from 'components/metrics/CountriesTable';
|
||||
import EventsTable from 'components/metrics/EventsTable';
|
||||
import EventsChart from 'components/metrics/EventsChart';
|
||||
import styles from './WebsiteTableView.module.css';
|
||||
|
||||
export default function WebsiteTableView({ websiteId }) {
|
||||
const [countryData, setCountryData] = useState();
|
||||
@ -19,38 +20,38 @@ export default function WebsiteTableView({ websiteId }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row>
|
||||
<Column variant="two">
|
||||
<Row className={styles.row}>
|
||||
<Column className={styles.col} variant="two">
|
||||
<PagesTable {...tableProps} />
|
||||
</Column>
|
||||
<Column variant="two">
|
||||
<Column className={styles.col} variant="two">
|
||||
<ReferrersTable {...tableProps} />
|
||||
</Column>
|
||||
</Row>
|
||||
<Row>
|
||||
<Column variant="three">
|
||||
<Row className={styles.row}>
|
||||
<Column className={styles.col} variant="three">
|
||||
<BrowsersTable {...tableProps} />
|
||||
</Column>
|
||||
<Column variant="three">
|
||||
<Column className={styles.col} variant="three">
|
||||
<OSTable {...tableProps} />
|
||||
</Column>
|
||||
<Column variant="three">
|
||||
<Column className={styles.col} variant="three">
|
||||
<DevicesTable {...tableProps} />
|
||||
</Column>
|
||||
</Row>
|
||||
<Row>
|
||||
<Column xs={12} sm={12} md={12} defaultSize={8}>
|
||||
<Row className={styles.row}>
|
||||
<Column className={styles.col} xs={12} sm={12} md={12} defaultSize={8}>
|
||||
<WorldMap data={countryData} />
|
||||
</Column>
|
||||
<Column xs={12} sm={12} md={12} defaultSize={4}>
|
||||
<Column className={styles.col} xs={12} sm={12} md={12} defaultSize={4}>
|
||||
<CountriesTable {...tableProps} onDataLoad={setCountryData} />
|
||||
</Column>
|
||||
</Row>
|
||||
<Row>
|
||||
<Column xs={12} md={12} lg={4} defaultSize={4}>
|
||||
<Row className={styles.row}>
|
||||
<Column className={styles.col} xs={12} md={12} lg={4} defaultSize={4}>
|
||||
<EventsTable {...tableProps} />
|
||||
</Column>
|
||||
<Column xs={12} md={12} lg={8} defaultSize={8}>
|
||||
<Column className={styles.col} xs={12} md={12} lg={8} defaultSize={8}>
|
||||
<EventsChart websiteId={websiteId} />
|
||||
</Column>
|
||||
</Row>
|
||||
|
@ -1,8 +1,3 @@
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
27
hooks/useSticky.js
Normal file
27
hooks/useSticky.js
Normal file
@ -0,0 +1,27 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
export default function useSticky(defaultSticky = false) {
|
||||
const [isSticky, setIsSticky] = useState(defaultSticky);
|
||||
const ref = useRef(null);
|
||||
const initialTop = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (window.pageYOffset > initialTop.current) {
|
||||
setIsSticky(true);
|
||||
} else {
|
||||
setIsSticky(false);
|
||||
}
|
||||
};
|
||||
|
||||
initialTop.current = ref.current.offsetTop;
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { ref, isSticky };
|
||||
}
|
Loading…
Reference in New Issue
Block a user