More deletes. Fixed sticky header.

This commit is contained in:
Mike Cao 2023-02-08 23:14:11 -08:00
parent 87bbaa7f1d
commit 45c13da262
23 changed files with 69 additions and 582 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -40,7 +40,7 @@ export default function WebsiteDetails({ websiteId }) {
domain={data?.domain}
onDataLoad={handleDataLoad}
showLink={false}
stickyHeader
stickyHeader={true}
/>
{!chartLoaded && <Loading icon="dots" />}
{chartLoaded && (

View File

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

View File

@ -1,8 +1,3 @@
.grid {
display: flex;
flex-direction: column;
}
.col {
display: flex;
flex-direction: column;

27
hooks/useSticky.js Normal file
View 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 };
}