docusaurus/website/src/pages/showcase/index.tsx
Biplav Kumar Mazumdar dc7ae426ac
fix(website): fix showcase search input (#9260)
Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
2023-09-14 17:03:29 +02:00

339 lines
10 KiB
TypeScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {useState, useMemo, useEffect} from 'react';
import clsx from 'clsx';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import Translate, {translate} from '@docusaurus/Translate';
import {useHistory, useLocation} from '@docusaurus/router';
import {usePluralForm} from '@docusaurus/theme-common';
import Link from '@docusaurus/Link';
import Layout from '@theme/Layout';
import FavoriteIcon from '@site/src/components/svgIcons/FavoriteIcon';
import {
sortedUsers,
Tags,
TagList,
type User,
type TagType,
} from '@site/src/data/users';
import Heading from '@theme/Heading';
import ShowcaseTagSelect, {
readSearchTags,
} from './_components/ShowcaseTagSelect';
import ShowcaseFilterToggle, {
type Operator,
readOperator,
} from './_components/ShowcaseFilterToggle';
import ShowcaseCard from './_components/ShowcaseCard';
import ShowcaseTooltip from './_components/ShowcaseTooltip';
import styles from './styles.module.css';
const TITLE = translate({message: 'Docusaurus Site Showcase'});
const DESCRIPTION = translate({
message: 'List of websites people are building with Docusaurus',
});
const SUBMIT_URL = 'https://github.com/facebook/docusaurus/discussions/7826';
type UserState = {
scrollTopPosition: number;
focusedElementId: string | undefined;
};
function restoreUserState(userState: UserState | null) {
const {scrollTopPosition, focusedElementId} = userState ?? {
scrollTopPosition: 0,
focusedElementId: undefined,
};
// @ts-expect-error: if focusedElementId is undefined it returns null
document.getElementById(focusedElementId)?.focus();
window.scrollTo({top: scrollTopPosition});
}
export function prepareUserState(): UserState | undefined {
if (ExecutionEnvironment.canUseDOM) {
return {
scrollTopPosition: window.scrollY,
focusedElementId: document.activeElement?.id,
};
}
return undefined;
}
const SearchNameQueryKey = 'name';
function readSearchName(search: string) {
return new URLSearchParams(search).get(SearchNameQueryKey);
}
function filterUsers(
users: User[],
selectedTags: TagType[],
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) {
return users;
}
return users.filter((user) => {
if (user.tags.length === 0) {
return false;
}
if (operator === 'AND') {
return selectedTags.every((tag) => user.tags.includes(tag));
}
return selectedTags.some((tag) => user.tags.includes(tag));
});
}
function useFilteredUsers() {
const location = useLocation<UserState>();
const [operator, setOperator] = useState<Operator>('OR');
// On SSR / first mount (hydration) no tag is selected
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)
useEffect(() => {
setSelectedTags(readSearchTags(location.search));
setOperator(readOperator(location.search));
setSearchName(readSearchName(location.search));
restoreUserState(location.state);
}, [location]);
return useMemo(
() => filterUsers(sortedUsers, selectedTags, operator, searchName),
[selectedTags, operator, searchName],
);
}
function ShowcaseHeader() {
return (
<section className="margin-top--lg margin-bottom--lg text--center">
<Heading as="h1">{TITLE}</Heading>
<p>{DESCRIPTION}</p>
<Link className="button button--primary" to={SUBMIT_URL}>
<Translate id="showcase.header.button">
🙏 Please add your site
</Translate>
</Link>
</section>
);
}
function useSiteCountPlural() {
const {selectMessage} = usePluralForm();
return (sitesCount: number) =>
selectMessage(
sitesCount,
translate(
{
id: 'showcase.filters.resultCount',
description:
'Pluralized label for the number of sites found on the showcase. Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',
message: '1 site|{sitesCount} sites',
},
{sitesCount},
),
);
}
function ShowcaseFilters() {
const filteredUsers = useFilteredUsers();
const siteCountPlural = useSiteCountPlural();
return (
<section className="container margin-top--l margin-bottom--lg">
<div className={clsx('margin-bottom--sm', styles.filterCheckbox)}>
<div>
<Heading as="h2">
<Translate id="showcase.filters.title">Filters</Translate>
</Heading>
<span>{siteCountPlural(filteredUsers.length)}</span>
</div>
<ShowcaseFilterToggle />
</div>
<ul className={clsx('clean-list', styles.checkboxList)}>
{TagList.map((tag, i) => {
const {label, description, color} = Tags[tag];
const id = `showcase_checkbox_id_${tag}`;
return (
<li key={i} className={styles.checkboxListItem}>
<ShowcaseTooltip
id={id}
text={description}
anchorEl="#__docusaurus">
<ShowcaseTagSelect
tag={tag}
id={id}
label={label}
icon={
tag === 'favorite' ? (
<FavoriteIcon svgClass={styles.svgIconFavoriteXs} />
) : (
<span
style={{
backgroundColor: color,
width: 10,
height: 10,
borderRadius: '50%',
marginLeft: 8,
}}
/>
)
}
/>
</ShowcaseTooltip>
</li>
);
})}
</ul>
</section>
);
}
const favoriteUsers = sortedUsers.filter((user) =>
user.tags.includes('favorite'),
);
const otherUsers = sortedUsers.filter(
(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={translate({
message: 'Search for site name...',
id: 'showcase.searchBar.placeholder',
})}
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() {
const filteredUsers = useFilteredUsers();
if (filteredUsers.length === 0) {
return (
<section className="margin-top--lg margin-bottom--xl">
<div className="container padding-vert--md text--center">
<Heading as="h2">
<Translate id="showcase.usersList.noResult">No result</Translate>
</Heading>
</div>
</section>
);
}
return (
<section className="margin-top--lg margin-bottom--xl">
{filteredUsers.length === sortedUsers.length ? (
<>
<div className={styles.showcaseFavorite}>
<div className="container">
<div
className={clsx(
'margin-bottom--md',
styles.showcaseFavoriteHeader,
)}>
<Heading as="h2">
<Translate id="showcase.favoritesList.title">
Our favorites
</Translate>
</Heading>
<FavoriteIcon svgClass={styles.svgIconFavorite} />
</div>
<ul
className={clsx(
'container',
'clean-list',
styles.showcaseList,
)}>
{favoriteUsers.map((user) => (
<ShowcaseCard key={user.title} user={user} />
))}
</ul>
</div>
</div>
<div className="container margin-top--lg">
<Heading as="h2" className={styles.showcaseHeader}>
<Translate id="showcase.usersList.allUsers">All sites</Translate>
</Heading>
<ul className={clsx('clean-list', styles.showcaseList)}>
{otherUsers.map((user) => (
<ShowcaseCard key={user.title} user={user} />
))}
</ul>
</div>
</>
) : (
<div className="container">
<div
className={clsx('margin-bottom--md', styles.showcaseFavoriteHeader)}
/>
<ul className={clsx('clean-list', styles.showcaseList)}>
{filteredUsers.map((user) => (
<ShowcaseCard key={user.title} user={user} />
))}
</ul>
</div>
)}
</section>
);
}
export default function Showcase(): JSX.Element {
return (
<Layout title={TITLE} description={DESCRIPTION}>
<main className="margin-vert--lg">
<ShowcaseHeader />
<ShowcaseFilters />
<div
style={{display: 'flex', marginLeft: 'auto'}}
className="container">
<SearchBar />
</div>
<ShowcaseCards />
</main>
</Layout>
);
}