phanpy/src/pages/search.jsx

369 lines
12 KiB
React
Raw Normal View History

import './search.css';
2023-09-04 09:49:39 +03:00
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import { InView } from 'react-intersection-observer';
import { useParams, useSearchParams } from 'react-router-dom';
2023-02-12 14:29:03 +03:00
import AccountBlock from '../components/account-block';
import Icon from '../components/icon';
import Link from '../components/link';
2023-02-11 11:27:52 +03:00
import Loader from '../components/loader';
2023-04-26 08:09:44 +03:00
import NavMenu from '../components/nav-menu';
2023-09-04 09:49:39 +03:00
import SearchForm from '../components/search-form';
import Status from '../components/status';
import { api } from '../utils/api';
2023-02-11 12:57:26 +03:00
import useTitle from '../utils/useTitle';
const SHORT_LIMIT = 5;
const LIMIT = 40;
2023-02-18 15:48:24 +03:00
function Search(props) {
const params = useParams();
const { masto, instance, authenticated } = api({
instance: params.instance,
});
2023-08-14 06:22:42 +03:00
const [uiState, setUIState] = useState('default');
const [searchParams] = useSearchParams();
const searchFormRef = useRef();
2023-02-18 15:48:24 +03:00
const q = props?.query || searchParams.get('q');
const type = props?.type || searchParams.get('type');
useTitle(
q
? `Search: ${q}${
type
? ` (${
{
statuses: 'Posts',
accounts: 'Accounts',
hashtags: 'Hashtags',
}[type]
})`
: ''
}`
: 'Search',
`/search`,
);
2023-02-11 12:57:26 +03:00
const [showMore, setShowMore] = useState(false);
const offsetRef = useRef(0);
useEffect(() => {
offsetRef.current = 0;
}, [q, type]);
const scrollableRef = useRef();
useLayoutEffect(() => {
scrollableRef.current?.scrollTo?.(0, 0);
}, [q, type]);
const [statusResults, setStatusResults] = useState([]);
const [accountResults, setAccountResults] = useState([]);
2023-02-10 19:05:18 +03:00
const [hashtagResults, setHashtagResults] = useState([]);
useEffect(() => {
setStatusResults([]);
setAccountResults([]);
setHashtagResults([]);
}, [q]);
const setTypeResultsFunc = {
statuses: setStatusResults,
accounts: setAccountResults,
hashtags: setHashtagResults,
};
function loadResults(firstLoad) {
2023-08-14 06:22:42 +03:00
setUIState('loading');
if (firstLoad && !type) {
setStatusResults(statusResults.slice(0, SHORT_LIMIT));
setAccountResults(accountResults.slice(0, SHORT_LIMIT));
setHashtagResults(hashtagResults.slice(0, SHORT_LIMIT));
}
(async () => {
const params = {
q,
resolve: authenticated,
limit: SHORT_LIMIT,
};
if (type) {
params.limit = LIMIT;
params.type = type;
params.offset = offsetRef.current;
}
try {
const results = await masto.v2.search(params);
console.log(results);
if (type) {
if (firstLoad) {
setTypeResultsFunc[type](results[type]);
const length = results[type]?.length;
offsetRef.current = LIMIT;
setShowMore(!!length);
} else {
setTypeResultsFunc[type]((prev) => [...prev, ...results[type]]);
const length = results[type]?.length;
offsetRef.current = offsetRef.current + LIMIT;
setShowMore(!!length);
}
} else {
setStatusResults(results.statuses);
setAccountResults(results.accounts);
setHashtagResults(results.hashtags);
offsetRef.current = 0;
setShowMore(false);
}
2023-08-14 06:22:42 +03:00
setUIState('default');
} catch (err) {
console.error(err);
2023-08-14 06:22:42 +03:00
setUIState('error');
}
})();
}
useEffect(() => {
if (q) {
searchFormRef.current?.setValue?.(q);
loadResults(true);
2023-09-04 09:49:39 +03:00
} else {
searchFormRef.current?.focus?.();
}
}, [q, type, instance]);
2023-09-04 09:49:39 +03:00
useHotkeys(
'/',
(e) => {
searchFormRef.current?.focus?.();
},
{
preventDefault: true,
},
);
return (
<div id="search-page" class="deck-container" ref={scrollableRef}>
<div class="timeline-deck deck">
<header>
<div class="header-grid">
<div class="header-side">
2023-04-26 08:09:44 +03:00
<NavMenu />
</div>
<SearchForm ref={searchFormRef} />
<div class="header-side">&nbsp;</div>
</div>
</header>
<main>
{!!q && (
<div class="filter-bar">
2023-09-04 12:01:06 +03:00
{!!type && (
<Link to={`/search${q ? `?q=${encodeURIComponent(q)}` : ''}`}>
All
</Link>
)}
{[
{
label: 'Accounts',
type: 'accounts',
2023-09-04 12:01:06 +03:00
to: `/search?q=${encodeURIComponent(q)}&type=accounts`,
},
{
label: 'Hashtags',
type: 'hashtags',
2023-09-04 12:01:06 +03:00
to: `/search?q=${encodeURIComponent(q)}&type=hashtags`,
},
{
label: 'Posts',
type: 'statuses',
2023-09-04 12:01:06 +03:00
to: `/search?q=${encodeURIComponent(q)}&type=statuses`,
},
]
.sort((a, b) => {
if (a.type === type) return -1;
if (b.type === type) return 1;
return 0;
})
.map((link) => (
<Link to={link.to}>{link.label}</Link>
))}
</div>
)}
{!!q ? (
2023-02-10 19:05:18 +03:00
<>
{(!type || type === 'accounts') && (
<>
{type !== 'accounts' && (
<h2 class="timeline-header">Accounts</h2>
)}
{accountResults.length > 0 ? (
<>
<ul class="timeline flat accounts-list">
{accountResults.map((account) => (
<li key={account.id}>
<AccountBlock
account={account}
instance={instance}
showStats
/>
</li>
))}
</ul>
{type !== 'accounts' && (
<div class="ui-state">
<Link
class="plain button"
to={`/search?q=${q}&type=accounts`}
>
See more accounts <Icon icon="arrow-right" />
</Link>
</div>
)}
</>
) : (
!type &&
(uiState === 'loading' ? (
<p class="ui-state">
<Loader abrupt />
</p>
) : (
<p class="ui-state">No accounts found.</p>
))
)}
</>
2023-02-10 19:05:18 +03:00
)}
{(!type || type === 'hashtags') && (
<>
{type !== 'hashtags' && (
<h2 class="timeline-header">Hashtags</h2>
)}
{hashtagResults.length > 0 ? (
<>
<ul class="link-list hashtag-list">
{hashtagResults.map((hashtag) => (
<li key={hashtag.name}>
<Link
to={
instance
? `/${instance}/t/${hashtag.name}`
: `/t/${hashtag.name}`
}
>
<Icon icon="hashtag" />
<span>{hashtag.name}</span>
</Link>
</li>
))}
</ul>
{type !== 'hashtags' && (
<div class="ui-state">
<Link
class="plain button"
to={`/search?q=${q}&type=hashtags`}
>
See more hashtags <Icon icon="arrow-right" />
</Link>
</div>
)}
</>
) : (
!type &&
(uiState === 'loading' ? (
<p class="ui-state">
<Loader abrupt />
</p>
) : (
<p class="ui-state">No hashtags found.</p>
))
)}
</>
2023-02-10 19:05:18 +03:00
)}
{(!type || type === 'statuses') && (
<>
{type !== 'statuses' && (
<h2 class="timeline-header">Posts</h2>
)}
{statusResults.length > 0 ? (
<>
<ul class="timeline">
{statusResults.map((status) => (
<li key={status.id}>
<Link
class="status-link"
to={
instance
? `/${instance}/s/${status.id}`
: `/s/${status.id}`
}
>
<Status status={status} />
</Link>
</li>
))}
</ul>
{type !== 'statuses' && (
<div class="ui-state">
<Link
class="plain button"
to={`/search?q=${q}&type=statuses`}
>
See more posts <Icon icon="arrow-right" />
</Link>
</div>
)}
</>
) : (
!type &&
(uiState === 'loading' ? (
<p class="ui-state">
<Loader abrupt />
</p>
) : (
<p class="ui-state">No posts found.</p>
))
)}
</>
2023-02-10 19:05:18 +03:00
)}
{!!type &&
(uiState === 'default' ? (
showMore ? (
<InView
onChange={(inView) => {
if (inView) {
loadResults();
}
}}
>
<button
type="button"
class="plain block"
onClick={() => loadResults()}
style={{ marginBlockEnd: '6em' }}
>
Show more&hellip;
</button>
</InView>
) : (
<p class="ui-state insignificant">The end.</p>
)
) : (
uiState === 'loading' && (
<p class="ui-state">
<Loader abrupt />
</p>
)
))}
2023-02-10 19:05:18 +03:00
</>
2023-02-11 11:27:52 +03:00
) : uiState === 'loading' ? (
<p class="ui-state">
<Loader abrupt />
</p>
2023-02-10 19:05:18 +03:00
) : (
2023-02-11 12:57:44 +03:00
<p class="ui-state">
Enter your search term or paste a URL above to get started.
</p>
)}
</main>
</div>
</div>
);
}
export default Search;