mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-22 09:57:00 +01:00
New components, convert hooks to components, bug fixes.
This commit is contained in:
parent
a2db27894f
commit
9d8a2406e1
1
assets/arrow-right.svg
Normal file
1
assets/arrow-right.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M216.464 36.465l-7.071 7.07c-4.686 4.686-4.686 12.284 0 16.971L387.887 239H12c-6.627 0-12 5.373-12 12v10c0 6.627 5.373 12 12 12h375.887L209.393 451.494c-4.686 4.686-4.686 12.284 0 16.971l7.071 7.07c4.686 4.686 12.284 4.686 16.97 0l211.051-211.05c4.686-4.686 4.686-12.284 0-16.971L233.434 36.465c-4.686-4.687-12.284-4.687-16.97 0z"/></svg>
|
After Width: | Height: | Size: 409 B |
13
components/Button.js
Normal file
13
components/Button.js
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Icon from './Icon';
|
||||
import styles from './Button.module.css';
|
||||
|
||||
export default function Button({ icon, children, className, onClick }) {
|
||||
return (
|
||||
<button type="button" className={classNames(styles.button, className)} onClick={onClick}>
|
||||
{icon && <Icon icon={icon} />}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
22
components/Button.module.css
Normal file
22
components/Button.module.css
Normal file
@ -0,0 +1,22 @@
|
||||
.button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #f5f5f5;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
border: 0;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: #eaeaea;
|
||||
}
|
||||
|
||||
.button svg {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
@ -1,13 +1,16 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
|
||||
function isInViewport(node) {
|
||||
return (
|
||||
window.pageYOffset < node.offsetTop + node.clientHeight ||
|
||||
window.pageXOffset < node.offsetLeft + node.clientWidth
|
||||
function isInViewport(element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return !(
|
||||
rect.bottom < 0 ||
|
||||
rect.right < 0 ||
|
||||
rect.left > window.innerWidth ||
|
||||
rect.top > window.innerHeight
|
||||
);
|
||||
}
|
||||
|
||||
export default function CheckVisible({ children }) {
|
||||
export default function CheckVisible({ className, children }) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const ref = useRef();
|
||||
|
||||
@ -30,5 +33,9 @@ export default function CheckVisible({ children }) {
|
||||
};
|
||||
}, [visible]);
|
||||
|
||||
return <div ref={ref}>{typeof children === 'function' ? children(visible) : children}</div>;
|
||||
return (
|
||||
<div ref={ref} className={className} data-visible={visible}>
|
||||
{typeof children === 'function' ? children(visible) : children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -23,10 +23,10 @@ export default function DropDown({ value, options = [], onChange, className }) {
|
||||
}
|
||||
}
|
||||
|
||||
document.body.addEventListener('click', hideMenu);
|
||||
document.addEventListener('click', hideMenu);
|
||||
|
||||
return () => {
|
||||
document.body.removeEventListener('click', hideMenu);
|
||||
document.removeEventListener('click', hideMenu);
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
|
7
components/Icon.js
Normal file
7
components/Icon.js
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Icon.module.css';
|
||||
|
||||
export default function Icon({ icon, className }) {
|
||||
return <div className={classNames(styles.icon, className)}>{icon}</div>;
|
||||
}
|
11
components/Icon.module.css
Normal file
11
components/Icon.module.css
Normal file
@ -0,0 +1,11 @@
|
||||
.icon {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.icon > svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
12
components/Link.js
Normal file
12
components/Link.js
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import NextLink from 'next/link';
|
||||
import styles from './Link.module.css';
|
||||
|
||||
export default function Link({ href, className, children }) {
|
||||
return (
|
||||
<NextLink href={href}>
|
||||
<a className={classNames(styles.link, className)}>{children}</a>
|
||||
</NextLink>
|
||||
);
|
||||
}
|
23
components/Link.module.css
Normal file
23
components/Link.module.css
Normal file
@ -0,0 +1,23 @@
|
||||
.link,
|
||||
.link:active,
|
||||
.link:visited {
|
||||
position: relative;
|
||||
color: #2c2c2c;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: #2680eb;
|
||||
opacity: 0.5;
|
||||
transition: width 100ms;
|
||||
}
|
||||
|
||||
.link:hover:before {
|
||||
width: 100%;
|
||||
transition: width 100ms;
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Button from './Button';
|
||||
import { getDateRange } from 'lib/date';
|
||||
import styles from './QuickButtons.module.css';
|
||||
|
||||
@ -17,13 +18,13 @@ export default function QuickButtons({ value, onChange }) {
|
||||
return (
|
||||
<div className={styles.buttons}>
|
||||
{Object.keys(options).map(key => (
|
||||
<div
|
||||
<Button
|
||||
key={key}
|
||||
className={classNames(styles.button, { [styles.active]: value === key })}
|
||||
className={classNames({ [styles.active]: value === key })}
|
||||
onClick={() => handleClick(key)}
|
||||
>
|
||||
{options[key]}
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
@ -6,23 +6,16 @@
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 12px;
|
||||
background: #f5f5f5;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: #eaeaea;
|
||||
.buttons button + button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.active {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 720px) {
|
||||
.buttons button:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -57,10 +57,12 @@ export default function RankingsChart({
|
||||
<div className={styles.title}>{title}</div>
|
||||
<div className={styles.heading}>{heading}</div>
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{rankings.map(({ x, y, z }) => (
|
||||
<Row key={x} label={x} value={y} percent={z} animate={visible} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CheckVisible>
|
||||
);
|
||||
|
@ -73,3 +73,15 @@
|
||||
background: #2680eb;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.body:empty:before {
|
||||
content: 'No data available';
|
||||
display: block;
|
||||
color: #b3b3b3;
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
49
components/StickyHeader.js
Normal file
49
components/StickyHeader.js
Normal file
@ -0,0 +1,49 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function StickyHeader({
|
||||
className,
|
||||
stickyClassName,
|
||||
stickyStyle,
|
||||
children,
|
||||
enabled = true,
|
||||
}) {
|
||||
const [sticky, setSticky] = useState(false);
|
||||
const ref = useRef();
|
||||
const offsetTop = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const checkPosition = () => {
|
||||
if (ref.current) {
|
||||
if (!offsetTop.current) {
|
||||
offsetTop.current = ref.current.offsetTop;
|
||||
}
|
||||
const state = window.pageYOffset > offsetTop.current;
|
||||
if (sticky !== state) {
|
||||
setSticky(state);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkPosition();
|
||||
|
||||
if (enabled) {
|
||||
window.addEventListener('scroll', checkPosition);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', checkPosition);
|
||||
};
|
||||
}, [sticky, enabled]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sticky={sticky}
|
||||
className={classNames(className, { [stickyClassName]: sticky })}
|
||||
{...(sticky && { style: stickyStyle })}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -5,7 +5,7 @@ import CheckVisible from './CheckVisible';
|
||||
import MetricsBar from './MetricsBar';
|
||||
import QuickButtons from './QuickButtons';
|
||||
import DateFilter from './DateFilter';
|
||||
import useSticky from './hooks/useSticky';
|
||||
import StickyHeader from './StickyHeader';
|
||||
import { get } from 'lib/web';
|
||||
import { getDateArray, getDateRange, getTimezone } from 'lib/date';
|
||||
import styles from './WebsiteChart.module.css';
|
||||
@ -13,13 +13,13 @@ import styles from './WebsiteChart.module.css';
|
||||
export default function WebsiteChart({
|
||||
websiteId,
|
||||
defaultDateRange = '7day',
|
||||
stickHeader = false,
|
||||
stickyHeader = false,
|
||||
onDataLoad = () => {},
|
||||
onDateChange = () => {},
|
||||
}) {
|
||||
const [data, setData] = useState();
|
||||
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
|
||||
const { startDate, endDate, unit, value } = dateRange;
|
||||
const [ref, sticky] = useSticky(stickHeader);
|
||||
const container = useRef();
|
||||
|
||||
const [pageviews, uniques] = useMemo(() => {
|
||||
@ -38,14 +38,15 @@ export default function WebsiteChart({
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
setData(
|
||||
await get(`/api/website/${websiteId}/pageviews`, {
|
||||
const data = await get(`/api/website/${websiteId}/pageviews`, {
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
unit,
|
||||
tz: getTimezone(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
setData(data);
|
||||
onDataLoad(data);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@ -54,10 +55,11 @@ export default function WebsiteChart({
|
||||
|
||||
return (
|
||||
<div ref={container}>
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames(styles.header, 'row', { [styles.sticky]: sticky })}
|
||||
style={{ width: sticky ? container.current.clientWidth : 'auto' }}
|
||||
<StickyHeader
|
||||
className={classNames(styles.header, 'row')}
|
||||
stickyClassName={styles.sticky}
|
||||
stickyStyle={{ width: container?.current?.clientWidth }}
|
||||
enabled={stickyHeader}
|
||||
>
|
||||
<MetricsBar
|
||||
className="col-12 col-md-9 col-lg-10"
|
||||
@ -70,12 +72,11 @@ export default function WebsiteChart({
|
||||
value={value}
|
||||
onChange={handleDateChange}
|
||||
/>
|
||||
</div>
|
||||
</StickyHeader>
|
||||
<div className="row">
|
||||
<CheckVisible>
|
||||
<CheckVisible className="col">
|
||||
{visible => (
|
||||
<PageviewsChart
|
||||
className="col"
|
||||
websiteId={websiteId}
|
||||
data={{ pageviews, uniques }}
|
||||
unit={unit}
|
||||
|
@ -3,7 +3,6 @@ import classNames from 'classnames';
|
||||
import WebsiteChart from './WebsiteChart';
|
||||
import RankingsChart from './RankingsChart';
|
||||
import WorldMap from './WorldMap';
|
||||
import CheckVisible from './CheckVisible';
|
||||
import { getDateRange } from 'lib/date';
|
||||
import { get } from 'lib/web';
|
||||
import { browserFilter, urlFilter, refFilter, deviceFilter, countryFilter } from 'lib/filters';
|
||||
@ -14,6 +13,7 @@ const sessionClasses = 'col-12 col-lg-4';
|
||||
|
||||
export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) {
|
||||
const [data, setData] = useState();
|
||||
const [chartLoaded, setChartLoaded] = useState(false);
|
||||
const [countryData, setCountryData] = useState();
|
||||
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
|
||||
const { startDate, endDate } = dateRange;
|
||||
@ -22,6 +22,10 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
|
||||
setData(await get(`/api/website/${websiteId}`));
|
||||
}
|
||||
|
||||
function handleDataLoad() {
|
||||
if (!chartLoaded) setTimeout(() => setChartLoaded(true), 300);
|
||||
}
|
||||
|
||||
function handleDateChange(values) {
|
||||
setTimeout(() => setDateRange(values), 300);
|
||||
}
|
||||
@ -41,9 +45,16 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
|
||||
<div className="row">
|
||||
<div className={classNames(styles.chart, 'col')}>
|
||||
<h2>{data.label}</h2>
|
||||
<WebsiteChart websiteId={websiteId} onDateChange={handleDateChange} stickHeader />
|
||||
<WebsiteChart
|
||||
websiteId={websiteId}
|
||||
onDataLoad={handleDataLoad}
|
||||
onDateChange={handleDateChange}
|
||||
stickyHeader
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{chartLoaded && (
|
||||
<>
|
||||
<div className={classNames(styles.row, 'row')}>
|
||||
<div className={pageviewClasses}>
|
||||
<RankingsChart
|
||||
@ -119,6 +130,8 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
.row {
|
||||
border-top: 1px solid #e1e1e1;
|
||||
min-height: 430px;
|
||||
}
|
||||
|
||||
.row > [class*='col-'] {
|
||||
|
@ -1,7 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { get } from 'lib/web';
|
||||
import Link from './Link';
|
||||
import WebsiteChart from './WebsiteChart';
|
||||
import Icon from './Icon';
|
||||
import { get } from 'lib/web';
|
||||
import Arrow from 'assets/arrow-right.svg';
|
||||
import styles from './WebsiteList.module.css';
|
||||
|
||||
export default function WebsiteList() {
|
||||
@ -16,18 +18,23 @@ export default function WebsiteList() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<>
|
||||
{data &&
|
||||
data.websites.map(({ website_id, label }) => (
|
||||
<div key={website_id}>
|
||||
<div key={website_id} className={styles.website}>
|
||||
<div className={styles.header}>
|
||||
<h2>
|
||||
<Link href={`/website/${website_id}/${label}`}>
|
||||
<a>{label}</a>
|
||||
<Link href={`/website/${website_id}/${label}`} className={styles.title}>
|
||||
{label}
|
||||
</Link>
|
||||
</h2>
|
||||
<Link href={`/website/${website_id}/${label}`} className={styles.details}>
|
||||
<Icon icon={<Arrow />} /> View details
|
||||
</Link>
|
||||
</div>
|
||||
<WebsiteChart key={website_id} title={label} websiteId={website_id} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,32 +1,29 @@
|
||||
.container > div {
|
||||
.website {
|
||||
padding-bottom: 30px;
|
||||
border-bottom: 1px solid #e1e1e1;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.container > div:last-child {
|
||||
.website:last-child {
|
||||
border-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.container a {
|
||||
position: relative;
|
||||
color: #2c2c2c;
|
||||
text-decoration: none;
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container a:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: #2680eb;
|
||||
opacity: 0.5;
|
||||
transition: width 100ms;
|
||||
.title {
|
||||
color: #2c2c2c !important;
|
||||
}
|
||||
|
||||
.container a:hover:before {
|
||||
width: 100%;
|
||||
transition: width 100ms;
|
||||
.details {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.details svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
@ -1,31 +0,0 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
|
||||
export default function useSticky(enabled) {
|
||||
const [node, setNode] = useState(null);
|
||||
const [sticky, setSticky] = useState(false);
|
||||
const offsetTop = useRef(0);
|
||||
|
||||
const ref = useCallback(node => {
|
||||
offsetTop.current = node?.offsetTop;
|
||||
setNode(node);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const checkPosition = () => {
|
||||
const state = window.pageYOffset > offsetTop.current;
|
||||
if (node && sticky !== state) {
|
||||
setSticky(state);
|
||||
}
|
||||
};
|
||||
|
||||
if (enabled) {
|
||||
window.addEventListener('scroll', checkPosition);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', checkPosition);
|
||||
};
|
||||
}, [node, sticky, enabled]);
|
||||
|
||||
return [ref, sticky];
|
||||
}
|
@ -9,6 +9,7 @@ body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
color: #2c2c2c;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
@ -25,6 +26,12 @@ body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
a,
|
||||
a:active,
|
||||
a:visited {
|
||||
@ -32,7 +39,7 @@ a:visited {
|
||||
}
|
||||
|
||||
header a {
|
||||
color: #000 !important;
|
||||
color: #2c2c2c !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@ -52,6 +59,7 @@ select {
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user