mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-22 18:00:17 +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';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
|
||||||
function isInViewport(node) {
|
function isInViewport(element) {
|
||||||
return (
|
const rect = element.getBoundingClientRect();
|
||||||
window.pageYOffset < node.offsetTop + node.clientHeight ||
|
return !(
|
||||||
window.pageXOffset < node.offsetLeft + node.clientWidth
|
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 [visible, setVisible] = useState(false);
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
|
|
||||||
@ -30,5 +33,9 @@ export default function CheckVisible({ children }) {
|
|||||||
};
|
};
|
||||||
}, [visible]);
|
}, [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 () => {
|
return () => {
|
||||||
document.body.removeEventListener('click', hideMenu);
|
document.removeEventListener('click', hideMenu);
|
||||||
};
|
};
|
||||||
}, [ref]);
|
}, [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 React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import Button from './Button';
|
||||||
import { getDateRange } from 'lib/date';
|
import { getDateRange } from 'lib/date';
|
||||||
import styles from './QuickButtons.module.css';
|
import styles from './QuickButtons.module.css';
|
||||||
|
|
||||||
@ -17,13 +18,13 @@ export default function QuickButtons({ value, onChange }) {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
{Object.keys(options).map(key => (
|
{Object.keys(options).map(key => (
|
||||||
<div
|
<Button
|
||||||
key={key}
|
key={key}
|
||||||
className={classNames(styles.button, { [styles.active]: value === key })}
|
className={classNames({ [styles.active]: value === key })}
|
||||||
onClick={() => handleClick(key)}
|
onClick={() => handleClick(key)}
|
||||||
>
|
>
|
||||||
{options[key]}
|
{options[key]}
|
||||||
</div>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -6,23 +6,16 @@
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.buttons button + button {
|
||||||
font-size: 12px;
|
margin-left: 10px;
|
||||||
background: #f5f5f5;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-right: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover {
|
|
||||||
background: #eaeaea;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.active {
|
.active {
|
||||||
font-weight: 600;
|
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.title}>{title}</div>
|
||||||
<div className={styles.heading}>{heading}</div>
|
<div className={styles.heading}>{heading}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.body}>
|
||||||
{rankings.map(({ x, y, z }) => (
|
{rankings.map(({ x, y, z }) => (
|
||||||
<Row key={x} label={x} value={y} percent={z} animate={visible} />
|
<Row key={x} label={x} value={y} percent={z} animate={visible} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</CheckVisible>
|
</CheckVisible>
|
||||||
);
|
);
|
||||||
|
@ -73,3 +73,15 @@
|
|||||||
background: #2680eb;
|
background: #2680eb;
|
||||||
z-index: -1;
|
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 MetricsBar from './MetricsBar';
|
||||||
import QuickButtons from './QuickButtons';
|
import QuickButtons from './QuickButtons';
|
||||||
import DateFilter from './DateFilter';
|
import DateFilter from './DateFilter';
|
||||||
import useSticky from './hooks/useSticky';
|
import StickyHeader from './StickyHeader';
|
||||||
import { get } from 'lib/web';
|
import { get } from 'lib/web';
|
||||||
import { getDateArray, getDateRange, getTimezone } from 'lib/date';
|
import { getDateArray, getDateRange, getTimezone } from 'lib/date';
|
||||||
import styles from './WebsiteChart.module.css';
|
import styles from './WebsiteChart.module.css';
|
||||||
@ -13,13 +13,13 @@ import styles from './WebsiteChart.module.css';
|
|||||||
export default function WebsiteChart({
|
export default function WebsiteChart({
|
||||||
websiteId,
|
websiteId,
|
||||||
defaultDateRange = '7day',
|
defaultDateRange = '7day',
|
||||||
stickHeader = false,
|
stickyHeader = false,
|
||||||
|
onDataLoad = () => {},
|
||||||
onDateChange = () => {},
|
onDateChange = () => {},
|
||||||
}) {
|
}) {
|
||||||
const [data, setData] = useState();
|
const [data, setData] = useState();
|
||||||
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
|
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
|
||||||
const { startDate, endDate, unit, value } = dateRange;
|
const { startDate, endDate, unit, value } = dateRange;
|
||||||
const [ref, sticky] = useSticky(stickHeader);
|
|
||||||
const container = useRef();
|
const container = useRef();
|
||||||
|
|
||||||
const [pageviews, uniques] = useMemo(() => {
|
const [pageviews, uniques] = useMemo(() => {
|
||||||
@ -38,14 +38,15 @@ export default function WebsiteChart({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
setData(
|
const data = await get(`/api/website/${websiteId}/pageviews`, {
|
||||||
await get(`/api/website/${websiteId}/pageviews`, {
|
|
||||||
start_at: +startDate,
|
start_at: +startDate,
|
||||||
end_at: +endDate,
|
end_at: +endDate,
|
||||||
unit,
|
unit,
|
||||||
tz: getTimezone(),
|
tz: getTimezone(),
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
|
setData(data);
|
||||||
|
onDataLoad(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -54,10 +55,11 @@ export default function WebsiteChart({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={container}>
|
<div ref={container}>
|
||||||
<div
|
<StickyHeader
|
||||||
ref={ref}
|
className={classNames(styles.header, 'row')}
|
||||||
className={classNames(styles.header, 'row', { [styles.sticky]: sticky })}
|
stickyClassName={styles.sticky}
|
||||||
style={{ width: sticky ? container.current.clientWidth : 'auto' }}
|
stickyStyle={{ width: container?.current?.clientWidth }}
|
||||||
|
enabled={stickyHeader}
|
||||||
>
|
>
|
||||||
<MetricsBar
|
<MetricsBar
|
||||||
className="col-12 col-md-9 col-lg-10"
|
className="col-12 col-md-9 col-lg-10"
|
||||||
@ -70,12 +72,11 @@ export default function WebsiteChart({
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={handleDateChange}
|
onChange={handleDateChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</StickyHeader>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<CheckVisible>
|
<CheckVisible className="col">
|
||||||
{visible => (
|
{visible => (
|
||||||
<PageviewsChart
|
<PageviewsChart
|
||||||
className="col"
|
|
||||||
websiteId={websiteId}
|
websiteId={websiteId}
|
||||||
data={{ pageviews, uniques }}
|
data={{ pageviews, uniques }}
|
||||||
unit={unit}
|
unit={unit}
|
||||||
|
@ -3,7 +3,6 @@ import classNames from 'classnames';
|
|||||||
import WebsiteChart from './WebsiteChart';
|
import WebsiteChart from './WebsiteChart';
|
||||||
import RankingsChart from './RankingsChart';
|
import RankingsChart from './RankingsChart';
|
||||||
import WorldMap from './WorldMap';
|
import WorldMap from './WorldMap';
|
||||||
import CheckVisible from './CheckVisible';
|
|
||||||
import { getDateRange } from 'lib/date';
|
import { getDateRange } from 'lib/date';
|
||||||
import { get } from 'lib/web';
|
import { get } from 'lib/web';
|
||||||
import { browserFilter, urlFilter, refFilter, deviceFilter, countryFilter } from 'lib/filters';
|
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' }) {
|
export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) {
|
||||||
const [data, setData] = useState();
|
const [data, setData] = useState();
|
||||||
|
const [chartLoaded, setChartLoaded] = useState(false);
|
||||||
const [countryData, setCountryData] = useState();
|
const [countryData, setCountryData] = useState();
|
||||||
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
|
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
|
||||||
const { startDate, endDate } = dateRange;
|
const { startDate, endDate } = dateRange;
|
||||||
@ -22,6 +22,10 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
|
|||||||
setData(await get(`/api/website/${websiteId}`));
|
setData(await get(`/api/website/${websiteId}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDataLoad() {
|
||||||
|
if (!chartLoaded) setTimeout(() => setChartLoaded(true), 300);
|
||||||
|
}
|
||||||
|
|
||||||
function handleDateChange(values) {
|
function handleDateChange(values) {
|
||||||
setTimeout(() => setDateRange(values), 300);
|
setTimeout(() => setDateRange(values), 300);
|
||||||
}
|
}
|
||||||
@ -41,9 +45,16 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
|
|||||||
<div className="row">
|
<div className="row">
|
||||||
<div className={classNames(styles.chart, 'col')}>
|
<div className={classNames(styles.chart, 'col')}>
|
||||||
<h2>{data.label}</h2>
|
<h2>{data.label}</h2>
|
||||||
<WebsiteChart websiteId={websiteId} onDateChange={handleDateChange} stickHeader />
|
<WebsiteChart
|
||||||
|
websiteId={websiteId}
|
||||||
|
onDataLoad={handleDataLoad}
|
||||||
|
onDateChange={handleDateChange}
|
||||||
|
stickyHeader
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{chartLoaded && (
|
||||||
|
<>
|
||||||
<div className={classNames(styles.row, 'row')}>
|
<div className={classNames(styles.row, 'row')}>
|
||||||
<div className={pageviewClasses}>
|
<div className={pageviewClasses}>
|
||||||
<RankingsChart
|
<RankingsChart
|
||||||
@ -119,6 +130,8 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
.row {
|
.row {
|
||||||
border-top: 1px solid #e1e1e1;
|
border-top: 1px solid #e1e1e1;
|
||||||
|
min-height: 430px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row > [class*='col-'] {
|
.row > [class*='col-'] {
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from './Link';
|
||||||
import { get } from 'lib/web';
|
|
||||||
import WebsiteChart from './WebsiteChart';
|
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';
|
import styles from './WebsiteList.module.css';
|
||||||
|
|
||||||
export default function WebsiteList() {
|
export default function WebsiteList() {
|
||||||
@ -16,18 +18,23 @@ export default function WebsiteList() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<>
|
||||||
{data &&
|
{data &&
|
||||||
data.websites.map(({ website_id, label }) => (
|
data.websites.map(({ website_id, label }) => (
|
||||||
<div key={website_id}>
|
<div key={website_id} className={styles.website}>
|
||||||
|
<div className={styles.header}>
|
||||||
<h2>
|
<h2>
|
||||||
<Link href={`/website/${website_id}/${label}`}>
|
<Link href={`/website/${website_id}/${label}`} className={styles.title}>
|
||||||
<a>{label}</a>
|
{label}
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</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} />
|
<WebsiteChart key={website_id} title={label} websiteId={website_id} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,29 @@
|
|||||||
.container > div {
|
.website {
|
||||||
padding-bottom: 30px;
|
padding-bottom: 30px;
|
||||||
border-bottom: 1px solid #e1e1e1;
|
border-bottom: 1px solid #e1e1e1;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container > div:last-child {
|
.website:last-child {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container a {
|
.header {
|
||||||
position: relative;
|
display: flex;
|
||||||
color: #2c2c2c;
|
justify-content: space-between;
|
||||||
text-decoration: none;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container a:before {
|
.title {
|
||||||
content: '';
|
color: #2c2c2c !important;
|
||||||
position: absolute;
|
|
||||||
bottom: -2px;
|
|
||||||
width: 0;
|
|
||||||
height: 2px;
|
|
||||||
background: #2680eb;
|
|
||||||
opacity: 0.5;
|
|
||||||
transition: width 100ms;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container a:hover:before {
|
.details {
|
||||||
width: 100%;
|
font-size: 14px;
|
||||||
transition: width 100ms;
|
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%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
color: #2c2c2c;
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,6 +26,12 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
a,
|
a,
|
||||||
a:active,
|
a:active,
|
||||||
a:visited {
|
a:visited {
|
||||||
@ -32,7 +39,7 @@ a:visited {
|
|||||||
}
|
}
|
||||||
|
|
||||||
header a {
|
header a {
|
||||||
color: #000 !important;
|
color: #2c2c2c !important;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,6 +59,7 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
|
flex: 1;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user