feat(website): search in showcase (#6333)

* feat(website): search in showcase

* fix SSR
This commit is contained in:
Joshua Chen 2022-01-15 11:38:57 +08:00 committed by GitHub
parent 95955ecfb0
commit 9c4187a5b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 79 additions and 21 deletions

View file

@ -34,7 +34,9 @@ export default function ShowcaseFilterToggle(): JSX.Element {
setOperator((o) => !o); setOperator((o) => !o);
const searchParams = new URLSearchParams(location.search); const searchParams = new URLSearchParams(location.search);
searchParams.delete(OperatorQueryKey); searchParams.delete(OperatorQueryKey);
searchParams.append(OperatorQueryKey, operator ? 'OR' : 'AND'); if (!operator) {
searchParams.append(OperatorQueryKey, operator ? 'OR' : 'AND');
}
history.push({ history.push({
...location, ...location,
search: searchParams.toString(), search: searchParams.toString(),

View file

@ -29,7 +29,7 @@ import {
import ShowcaseTooltip from './_components/ShowcaseTooltip'; import ShowcaseTooltip from './_components/ShowcaseTooltip';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import {useLocation} from '@docusaurus/router'; import {useHistory, useLocation} from '@docusaurus/router';
import styles from './styles.module.css'; import styles from './styles.module.css';
@ -64,11 +64,24 @@ export function prepareUserState(): UserState | undefined {
return undefined; return undefined;
} }
const SearchNameQueryKey = 'name';
function readSearchName(search: string) {
return new URLSearchParams(search).get(SearchNameQueryKey);
}
function filterUsers( function filterUsers(
users: User[], users: User[],
selectedTags: TagType[], selectedTags: TagType[],
operator: Operator, operator: Operator,
searchName: string | null,
) { ) {
if (searchName) {
// eslint-disable-next-line no-param-reassign
users = users.filter((user) =>
user.title.toLowerCase().includes(searchName.toLowerCase()),
);
}
if (selectedTags.length === 0) { if (selectedTags.length === 0) {
return users; return users;
} }
@ -85,33 +98,23 @@ function filterUsers(
} }
function useFilteredUsers() { function useFilteredUsers() {
const selectedTags = useSelectedTags();
const location = useLocation<UserState>(); const location = useLocation<UserState>();
const [operator, setOperator] = useState<Operator>('OR'); const [operator, setOperator] = useState<Operator>('OR');
useEffect(() => {
setOperator(readOperator(location.search));
restoreUserState(location.state);
}, [location]);
return useMemo(
() => filterUsers(sortedUsers, selectedTags, operator),
[selectedTags, operator],
);
}
function useSelectedTags() {
// The search query-string is the source of truth!
const location = useLocation<UserState>();
// On SSR / first mount (hydration) no tag is selected // On SSR / first mount (hydration) no tag is selected
const [selectedTags, setSelectedTags] = useState<TagType[]>([]); const [selectedTags, setSelectedTags] = useState<TagType[]>([]);
const [searchName, setSearchName] = useState<string | null>(null);
// Sync tags from QS to state (delayed on purpose to avoid SSR/Client hydration mismatch) // Sync tags from QS to state (delayed on purpose to avoid SSR/Client hydration mismatch)
useEffect(() => { useEffect(() => {
setSelectedTags(readSearchTags(location.search)); setSelectedTags(readSearchTags(location.search));
setOperator(readOperator(location.search));
setSearchName(readSearchName(location.search));
restoreUserState(location.state); restoreUserState(location.state);
}, [location]); }, [location]);
return selectedTags; return useMemo(
() => filterUsers(sortedUsers, selectedTags, operator, searchName),
[selectedTags, operator, searchName],
);
} }
function ShowcaseHeader() { function ShowcaseHeader() {
@ -190,8 +193,41 @@ const otherUsers = sortedUsers.filter(
(user) => !user.tags.includes('favorite'), (user) => !user.tags.includes('favorite'),
); );
function SearchBar() {
const history = useHistory();
const location = useLocation();
const [value, setValue] = useState<string | null>(null);
useEffect(() => {
setValue(readSearchName(location.search));
}, [location]);
return (
<div className={styles.searchContainer}>
<input
id="searchbar"
placeholder="Search for site name..."
value={value ?? undefined}
onInput={(e) => {
setValue(e.currentTarget.value);
const newSearch = new URLSearchParams(location.search);
newSearch.delete(SearchNameQueryKey);
if (e.currentTarget.value) {
newSearch.set(SearchNameQueryKey, e.currentTarget.value);
}
history.push({
...location,
search: newSearch.toString(),
state: prepareUserState(),
});
setTimeout(() => {
document.getElementById('searchbar')?.focus();
}, 0);
}}
/>
</div>
);
}
function ShowcaseCards() { function ShowcaseCards() {
const selectedTags = useSelectedTags();
const filteredUsers = useFilteredUsers(); const filteredUsers = useFilteredUsers();
if (filteredUsers.length === 0) { if (filteredUsers.length === 0) {
@ -199,6 +235,7 @@ function ShowcaseCards() {
<section className="margin-top--lg margin-bottom--xl"> <section className="margin-top--lg margin-bottom--xl">
<div className="container padding-vert--md text--center"> <div className="container padding-vert--md text--center">
<h2>No result</h2> <h2>No result</h2>
<SearchBar />
</div> </div>
</section> </section>
); );
@ -206,7 +243,7 @@ function ShowcaseCards() {
return ( return (
<section className="margin-top--lg margin-bottom--xl"> <section className="margin-top--lg margin-bottom--xl">
{selectedTags.length === 0 ? ( {filteredUsers.length === sortedUsers.length ? (
<> <>
<div className={styles.showcaseFavorite}> <div className={styles.showcaseFavorite}>
<div className="container"> <div className="container">
@ -217,6 +254,7 @@ function ShowcaseCards() {
)}> )}>
<h2>Our favorites</h2> <h2>Our favorites</h2>
<FavoriteIcon svgClass={styles.svgIconFavorite} /> <FavoriteIcon svgClass={styles.svgIconFavorite} />
<SearchBar />
</div> </div>
<ul className={clsx('container', styles.showcaseList)}> <ul className={clsx('container', styles.showcaseList)}>
{favoriteUsers.map((user) => ( {favoriteUsers.map((user) => (
@ -236,6 +274,13 @@ function ShowcaseCards() {
</> </>
) : ( ) : (
<div className="container"> <div className="container">
<div
className={clsx(
'margin-bottom--md',
styles.showcaseFavoriteHeader,
)}>
<SearchBar />
</div>
<ul className={styles.showcaseList}> <ul className={styles.showcaseList}>
{filteredUsers.map((user) => ( {filteredUsers.map((user) => (
<ShowcaseCard key={user.title} user={user} /> <ShowcaseCard key={user.title} user={user} />

View file

@ -52,6 +52,17 @@
margin-right: 0; margin-right: 0;
} }
.searchContainer {
margin-left: auto;
}
.searchContainer input {
height: 30px;
border-radius: 15px;
padding: 10px;
border: 1px solid gray;
}
.showcaseList { .showcaseList {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));