mirror of
https://github.com/cheeaun/phanpy.git
synced 2024-12-20 21:52:14 +03:00
152 lines
3.9 KiB
React
152 lines
3.9 KiB
React
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||
|
|
||
|
import useScroll from '../utils/useScroll';
|
||
|
import useTitle from '../utils/useTitle';
|
||
|
|
||
|
import Icon from './icon';
|
||
|
import Link from './link';
|
||
|
import Loader from './loader';
|
||
|
import Status from './status';
|
||
|
|
||
|
function Timeline({ title, id, emptyText, errorText, fetchItems = () => {} }) {
|
||
|
if (title) {
|
||
|
useTitle(title);
|
||
|
}
|
||
|
const [items, setItems] = useState([]);
|
||
|
const [uiState, setUIState] = useState('default');
|
||
|
const [showMore, setShowMore] = useState(false);
|
||
|
const scrollableRef = useRef(null);
|
||
|
const { nearReachEnd, reachStart } = useScroll({
|
||
|
scrollableElement: scrollableRef.current,
|
||
|
});
|
||
|
|
||
|
const loadItems = (firstLoad) => {
|
||
|
setUIState('loading');
|
||
|
(async () => {
|
||
|
try {
|
||
|
const { done, value } = await fetchItems(firstLoad);
|
||
|
if (value?.length) {
|
||
|
if (firstLoad) {
|
||
|
setItems(value);
|
||
|
} else {
|
||
|
setItems([...items, ...value]);
|
||
|
}
|
||
|
setShowMore(!done);
|
||
|
} else {
|
||
|
setShowMore(false);
|
||
|
}
|
||
|
setUIState('default');
|
||
|
} catch (e) {
|
||
|
console.error(e);
|
||
|
setUIState('error');
|
||
|
}
|
||
|
})();
|
||
|
};
|
||
|
|
||
|
useEffect(() => {
|
||
|
scrollableRef.current?.scrollTo({ top: 0 });
|
||
|
loadItems(true);
|
||
|
}, []);
|
||
|
|
||
|
useEffect(() => {
|
||
|
if (reachStart) {
|
||
|
loadItems(true);
|
||
|
}
|
||
|
}, [reachStart]);
|
||
|
|
||
|
useEffect(() => {
|
||
|
if (nearReachEnd && showMore) {
|
||
|
loadItems();
|
||
|
}
|
||
|
}, [nearReachEnd, showMore]);
|
||
|
|
||
|
return (
|
||
|
<div
|
||
|
id={`${id}-page`}
|
||
|
class="deck-container"
|
||
|
ref={scrollableRef}
|
||
|
tabIndex="-1"
|
||
|
>
|
||
|
<div class="timeline-deck deck">
|
||
|
<header
|
||
|
onClick={(e) => {
|
||
|
if (e.target === e.currentTarget) {
|
||
|
scrollableRef.current?.scrollTo({
|
||
|
top: 0,
|
||
|
behavior: 'smooth',
|
||
|
});
|
||
|
}
|
||
|
}}
|
||
|
>
|
||
|
<div class="header-side">
|
||
|
<Link to="/" class="button plain">
|
||
|
<Icon icon="home" size="l" />
|
||
|
</Link>
|
||
|
</div>
|
||
|
<h1>{title}</h1>
|
||
|
<div class="header-side">
|
||
|
<Loader hidden={uiState !== 'loading'} />
|
||
|
</div>
|
||
|
</header>
|
||
|
{!!items.length ? (
|
||
|
<>
|
||
|
<ul class="timeline">
|
||
|
{items.map((status) => (
|
||
|
<li key={`timeline-${status.id}`}>
|
||
|
<Link class="status-link" to={`/s/${status.id}`}>
|
||
|
<Status status={status} />
|
||
|
</Link>
|
||
|
</li>
|
||
|
))}
|
||
|
</ul>
|
||
|
{showMore && (
|
||
|
<button
|
||
|
type="button"
|
||
|
class="plain block"
|
||
|
disabled={uiState === 'loading'}
|
||
|
onClick={() => loadItems()}
|
||
|
style={{ marginBlockEnd: '6em' }}
|
||
|
>
|
||
|
{uiState === 'loading' ? (
|
||
|
<Loader abrupt />
|
||
|
) : (
|
||
|
<>Show more…</>
|
||
|
)}
|
||
|
</button>
|
||
|
)}
|
||
|
</>
|
||
|
) : uiState === 'loading' ? (
|
||
|
<ul class="timeline">
|
||
|
{Array.from({ length: 5 }).map((_, i) => (
|
||
|
<li key={i}>
|
||
|
<Status skeleton />
|
||
|
</li>
|
||
|
))}
|
||
|
</ul>
|
||
|
) : (
|
||
|
uiState !== 'loading' && <p class="ui-state">{emptyText}</p>
|
||
|
)}
|
||
|
{uiState === 'error' ? (
|
||
|
<p class="ui-state">
|
||
|
{errorText}
|
||
|
<br />
|
||
|
<br />
|
||
|
<button
|
||
|
class="button plain"
|
||
|
onClick={() => loadItems(!items.length)}
|
||
|
>
|
||
|
Try again
|
||
|
</button>
|
||
|
</p>
|
||
|
) : (
|
||
|
uiState !== 'loading' &&
|
||
|
!!items.length &&
|
||
|
!showMore && <p class="ui-state insignificant">The end.</p>
|
||
|
)}
|
||
|
</div>
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
export default Timeline;
|