From 66fd67f97d19b7e73e8dc62882813352cfd6957e Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 5 Apr 2024 17:11:58 +0200 Subject: [PATCH 01/18] commit --- .../src/pages/showcase/_components/ShowcaseCard/index.tsx | 1 + .../showcase/_components/ShowcaseFilterToggle/index.tsx | 4 ++-- website/src/pages/showcase/_utils.tsx | 6 ++++++ 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 website/src/pages/showcase/_utils.tsx diff --git a/website/src/pages/showcase/_components/ShowcaseCard/index.tsx b/website/src/pages/showcase/_components/ShowcaseCard/index.tsx index 5bf6935e12e7..64fc033dae6a 100644 --- a/website/src/pages/showcase/_components/ShowcaseCard/index.tsx +++ b/website/src/pages/showcase/_components/ShowcaseCard/index.tsx @@ -62,6 +62,7 @@ function ShowcaseCardTag({tags}: {tags: TagType[]}) { function getCardImage(user: User): string { return ( user.preview ?? + // TODO make it configurable `https://slorber-api-screenshot.netlify.app/${encodeURIComponent( user.website, )}/showcase` diff --git a/website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx b/website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx index 78c1126c61a6..77282594958f 100644 --- a/website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx +++ b/website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx @@ -18,8 +18,8 @@ export type Operator = 'OR' | 'AND'; export const OperatorQueryKey = 'operator'; export function readOperator(search: string): Operator { - return (new URLSearchParams(search).get(OperatorQueryKey) ?? - 'OR') as Operator; + const qsOperator = new URLSearchParams(search).get(OperatorQueryKey) ?? 'OR'; + return qsOperator === 'AND' ? 'AND' : 'OR'; } export default function ShowcaseFilterToggle(): JSX.Element { diff --git a/website/src/pages/showcase/_utils.tsx b/website/src/pages/showcase/_utils.tsx new file mode 100644 index 000000000000..b5c0e33b4a5b --- /dev/null +++ b/website/src/pages/showcase/_utils.tsx @@ -0,0 +1,6 @@ +/** + * 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. + */ From cf584baba193aa5f9c4802dd17c6e4c455a4beff Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 5 Apr 2024 17:36:25 +0200 Subject: [PATCH 02/18] commit --- .../_components/ClearAllButton/index.tsx | 31 ++++++++++ .../index.tsx | 59 ++++++++++--------- .../styles.module.css | 0 website/src/pages/showcase/index.tsx | 11 +++- 4 files changed, 69 insertions(+), 32 deletions(-) create mode 100644 website/src/pages/showcase/_components/ClearAllButton/index.tsx rename website/src/pages/showcase/_components/{ShowcaseFilterToggle => OperatorButton}/index.tsx (63%) rename website/src/pages/showcase/_components/{ShowcaseFilterToggle => OperatorButton}/styles.module.css (100%) diff --git a/website/src/pages/showcase/_components/ClearAllButton/index.tsx b/website/src/pages/showcase/_components/ClearAllButton/index.tsx new file mode 100644 index 000000000000..d065d6155d66 --- /dev/null +++ b/website/src/pages/showcase/_components/ClearAllButton/index.tsx @@ -0,0 +1,31 @@ +/** + * 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 React from 'react'; +import {useHistory} from '@docusaurus/router'; +import {prepareUserState} from '../../index'; + +export default function ClearAllButton() { + const history = useHistory(); + const clearAll = () => { + history.push({ + ...history.location, + search: '', + state: prepareUserState(), + }); + }; + + // TODO translate + return ( + + ); +} diff --git a/website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx b/website/src/pages/showcase/_components/OperatorButton/index.tsx similarity index 63% rename from website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx rename to website/src/pages/showcase/_components/OperatorButton/index.tsx index 77282594958f..528b4219c88c 100644 --- a/website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx +++ b/website/src/pages/showcase/_components/OperatorButton/index.tsx @@ -15,45 +15,53 @@ import styles from './styles.module.css'; export type Operator = 'OR' | 'AND'; +const DefaultOperator: Operator = 'OR'; + export const OperatorQueryKey = 'operator'; export function readOperator(search: string): Operator { - const qsOperator = new URLSearchParams(search).get(OperatorQueryKey) ?? 'OR'; + const qsOperator = + new URLSearchParams(search).get(OperatorQueryKey) ?? DefaultOperator; return qsOperator === 'AND' ? 'AND' : 'OR'; } -export default function ShowcaseFilterToggle(): JSX.Element { - const id = 'showcase_filter_toggle'; +function setSearchOperator(search: string, operator: Operator): string { + const searchParams = new URLSearchParams(search); + searchParams.delete(OperatorQueryKey); + if (!operator) { + searchParams.append(OperatorQueryKey, 'AND'); + } + return searchParams.toString(); +} + +function useOperator() { const location = useLocation(); const history = useHistory(); - const [operator, setOperator] = useState(false); + + const [operator, setOperator] = useState(DefaultOperator); useEffect(() => { - setOperator(readOperator(location.search) === 'AND'); + setOperator(readOperator(location.search)); }, [location]); + const toggleOperator = useCallback(() => { - setOperator((o) => !o); - const searchParams = new URLSearchParams(location.search); - searchParams.delete(OperatorQueryKey); - if (!operator) { - searchParams.append(OperatorQueryKey, 'AND'); - } + const newOperator = operator === 'AND' ? 'OR' : 'AND'; + setOperator(newOperator); + const newSearch = setSearchOperator(location.search, newOperator); history.push({ ...location, - search: searchParams.toString(), + search: newSearch, state: prepareUserState(), }); }, [operator, location, history]); - const ClearTag = () => { - history.push({ - ...location, - search: '', - state: prepareUserState(), - }); - }; + return {operator, toggleOperator}; +} +export default function OperatorButton() { + const id = 'showcase_filter_toggle'; + const {operator, toggleOperator} = useOperator(); return ( -
+ <> - - -
+ ); } diff --git a/website/src/pages/showcase/_components/ShowcaseFilterToggle/styles.module.css b/website/src/pages/showcase/_components/OperatorButton/styles.module.css similarity index 100% rename from website/src/pages/showcase/_components/ShowcaseFilterToggle/styles.module.css rename to website/src/pages/showcase/_components/OperatorButton/styles.module.css diff --git a/website/src/pages/showcase/index.tsx b/website/src/pages/showcase/index.tsx index 9d3437e65603..568231b42ea6 100644 --- a/website/src/pages/showcase/index.tsx +++ b/website/src/pages/showcase/index.tsx @@ -26,10 +26,12 @@ import Heading from '@theme/Heading'; import ShowcaseTagSelect, { readSearchTags, } from './_components/ShowcaseTagSelect'; -import ShowcaseFilterToggle, { +import OperatorButton, { type Operator, readOperator, -} from './_components/ShowcaseFilterToggle'; +} from './_components/OperatorButton'; +import ClearAllButton from './_components/ClearAllButton'; + import ShowcaseCard from './_components/ShowcaseCard'; import ShowcaseTooltip from './_components/ShowcaseTooltip'; @@ -163,7 +165,10 @@ function ShowcaseFilters() { {siteCountPlural(filteredUsers.length)} - +
+ + +
    {TagList.map((tag, i) => { From d776303666b3d04170f0acf9a01ae39ee11de2ae Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 5 Apr 2024 17:48:19 +0200 Subject: [PATCH 03/18] commit --- .../_components/ClearAllButton/index.tsx | 2 +- .../_components/OperatorButton/index.tsx | 4 +- .../_components/ShowcaseTagSelect/index.tsx | 2 +- website/src/pages/showcase/_utils.tsx | 118 +++++++++++++++ website/src/pages/showcase/index.tsx | 137 +++--------------- 5 files changed, 139 insertions(+), 124 deletions(-) diff --git a/website/src/pages/showcase/_components/ClearAllButton/index.tsx b/website/src/pages/showcase/_components/ClearAllButton/index.tsx index d065d6155d66..a7378f0c453b 100644 --- a/website/src/pages/showcase/_components/ClearAllButton/index.tsx +++ b/website/src/pages/showcase/_components/ClearAllButton/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import {useHistory} from '@docusaurus/router'; -import {prepareUserState} from '../../index'; +import {prepareUserState} from '../../_utils'; export default function ClearAllButton() { const history = useHistory(); diff --git a/website/src/pages/showcase/_components/OperatorButton/index.tsx b/website/src/pages/showcase/_components/OperatorButton/index.tsx index 528b4219c88c..8db89cfdec7f 100644 --- a/website/src/pages/showcase/_components/OperatorButton/index.tsx +++ b/website/src/pages/showcase/_components/OperatorButton/index.tsx @@ -9,13 +9,13 @@ import React, {useState, useEffect, useCallback} from 'react'; import clsx from 'clsx'; import {useHistory, useLocation} from '@docusaurus/router'; -import {prepareUserState} from '../../index'; +import {prepareUserState} from '../../_utils'; import styles from './styles.module.css'; export type Operator = 'OR' | 'AND'; -const DefaultOperator: Operator = 'OR'; +export const DefaultOperator: Operator = 'OR'; export const OperatorQueryKey = 'operator'; diff --git a/website/src/pages/showcase/_components/ShowcaseTagSelect/index.tsx b/website/src/pages/showcase/_components/ShowcaseTagSelect/index.tsx index 7495cf42eb58..003cc938bfdc 100644 --- a/website/src/pages/showcase/_components/ShowcaseTagSelect/index.tsx +++ b/website/src/pages/showcase/_components/ShowcaseTagSelect/index.tsx @@ -17,7 +17,7 @@ import {useHistory, useLocation} from '@docusaurus/router'; import {toggleListItem} from '@site/src/utils/jsUtils'; import type {TagType} from '@site/src/data/users'; -import {prepareUserState} from '../../index'; +import {prepareUserState} from '../../_utils'; import styles from './styles.module.css'; interface Props extends ComponentProps<'input'> { diff --git a/website/src/pages/showcase/_utils.tsx b/website/src/pages/showcase/_utils.tsx index b5c0e33b4a5b..2db6b58a90c8 100644 --- a/website/src/pages/showcase/_utils.tsx +++ b/website/src/pages/showcase/_utils.tsx @@ -4,3 +4,121 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ +import {useEffect, useMemo, useState} from 'react'; +import {useLocation} from '@docusaurus/router'; +import {translate} from '@docusaurus/Translate'; +import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; +import {usePluralForm} from '@docusaurus/theme-common'; +import type {TagType, User} from '@site/src/data/users'; +import {sortedUsers} from '@site/src/data/users'; +import type {Operator} from '@site/src/pages/showcase/_components/OperatorButton'; +import { + DefaultOperator, + readOperator, +} from '@site/src/pages/showcase/_components/OperatorButton'; +import {readSearchTags} from '@site/src/pages/showcase/_components/ShowcaseTagSelect'; + +type UserState = { + scrollTopPosition: number; + focusedElementId: string | undefined; +}; + +export 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'; + +export function readSearchName(search: string) { + return new URLSearchParams(search).get(SearchNameQueryKey); +} + +export function setSearchName(search: string, value: string): string { + const newSearch = new URLSearchParams(search); + newSearch.delete(SearchNameQueryKey); + if (value) { + newSearch.set(SearchNameQueryKey, value); + } + return newSearch.toString(); +} + +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)); + }); +} + +export function useFilteredUsers() { + const location = useLocation(); + const [operator, setOperator] = useState(DefaultOperator); + // On SSR / first mount (hydration) no tag is selected + const [selectedTags, setSelectedTags] = useState([]); + const [name, setName] = useState(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)); + setName(readSearchName(location.search)); + restoreUserState(location.state); + }, [location]); + + return useMemo( + () => filterUsers(sortedUsers, selectedTags, operator, name), + [selectedTags, operator, name], + ); +} + +export 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}, + ), + ); +} diff --git a/website/src/pages/showcase/index.tsx b/website/src/pages/showcase/index.tsx index 568231b42ea6..f226a8ac6f37 100644 --- a/website/src/pages/showcase/index.tsx +++ b/website/src/pages/showcase/index.tsx @@ -5,36 +5,30 @@ * LICENSE file in the root directory of this source tree. */ -import {useState, useMemo, useEffect} from 'react'; +import {useState, 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 {sortedUsers, Tags, TagList} from '@site/src/data/users'; import Heading from '@theme/Heading'; -import ShowcaseTagSelect, { - readSearchTags, -} from './_components/ShowcaseTagSelect'; -import OperatorButton, { - type Operator, - readOperator, -} from './_components/OperatorButton'; +import ShowcaseTagSelect from './_components/ShowcaseTagSelect'; +import OperatorButton from './_components/OperatorButton'; import ClearAllButton from './_components/ClearAllButton'; import ShowcaseCard from './_components/ShowcaseCard'; import ShowcaseTooltip from './_components/ShowcaseTooltip'; +import { + prepareUserState, + readSearchName, + setSearchName, + useFilteredUsers, + useSiteCountPlural, +} from './_utils'; import styles from './styles.module.css'; const TITLE = translate({message: 'Docusaurus Site Showcase'}); @@ -43,85 +37,6 @@ const DESCRIPTION = translate({ }); 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(); - const [operator, setOperator] = useState('OR'); - // On SSR / first mount (hydration) no tag is selected - const [selectedTags, setSelectedTags] = useState([]); - const [searchName, setSearchName] = useState(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 (
    @@ -136,23 +51,6 @@ function ShowcaseHeader() { ); } -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(); @@ -234,17 +132,16 @@ function SearchBar() { })} 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); - } + const name = e.currentTarget.value; + setValue(name); + const newSearch = setSearchName(location.search, name); history.push({ ...location, - search: newSearch.toString(), + search: newSearch, state: prepareUserState(), }); + + // TODO ??? setTimeout(() => { document.getElementById('searchbar')?.focus(); }, 0); From 7e981e6df1e1819f249909ba327e01c5279b01ae Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 5 Apr 2024 18:37:01 +0200 Subject: [PATCH 04/18] commit --- .../svgIcons/FavoriteIcon/index.tsx | 19 ---- website/src/css/custom.css | 1 - .../_components/FavoriteIcon/index.tsx | 32 +++++++ .../FavoriteIcon/styles.module.css | 27 ++++++ .../_components/ShowcaseCard/index.tsx | 4 +- .../ShowcaseCard/styles.module.css | 6 +- .../_components/ShowcaseCards/index.tsx | 96 +++++++++++++++++++ .../ShowcaseCards/styles.module.css | 32 +++++++ website/src/pages/showcase/index.tsx | 92 ++---------------- website/src/pages/showcase/styles.module.css | 14 --- 10 files changed, 198 insertions(+), 125 deletions(-) delete mode 100644 website/src/components/svgIcons/FavoriteIcon/index.tsx create mode 100644 website/src/pages/showcase/_components/FavoriteIcon/index.tsx create mode 100644 website/src/pages/showcase/_components/FavoriteIcon/styles.module.css create mode 100644 website/src/pages/showcase/_components/ShowcaseCards/index.tsx create mode 100644 website/src/pages/showcase/_components/ShowcaseCards/styles.module.css diff --git a/website/src/components/svgIcons/FavoriteIcon/index.tsx b/website/src/components/svgIcons/FavoriteIcon/index.tsx deleted file mode 100644 index 129166a8abb7..000000000000 --- a/website/src/components/svgIcons/FavoriteIcon/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/** - * 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 React from 'react'; -import Svg, {type SvgIconProps} from '@site/src/components/Svg'; - -export default function FavoriteIcon( - props: Omit, -): JSX.Element { - return ( - - - - ); -} diff --git a/website/src/css/custom.css b/website/src/css/custom.css index d775ed2db137..463429630bf6 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -17,7 +17,6 @@ --site-color-favorite-background: #f6fdfd; --site-color-tooltip: #fff; --site-color-tooltip-background: #353738; - --site-color-svg-icon-favorite: #e9669e; --site-color-checkbox-checked-bg: hsl(167deg 56% 73% / 25%); --site-color-feedback-background: #f0f8ff; --docusaurus-highlighted-code-line-bg: rgb(0 0 0 / 10%); diff --git a/website/src/pages/showcase/_components/FavoriteIcon/index.tsx b/website/src/pages/showcase/_components/FavoriteIcon/index.tsx new file mode 100644 index 000000000000..cf0c6ab2ad35 --- /dev/null +++ b/website/src/pages/showcase/_components/FavoriteIcon/index.tsx @@ -0,0 +1,32 @@ +/** + * 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 React, {type ComponentProps} from 'react'; +import clsx from 'clsx'; + +import styles from './styles.module.css'; + +interface Props { + className?: string; + style?: ComponentProps<'svg'>['style']; + size: 'small' | 'medium' | 'large'; +} + +export default function FavoriteIcon({ + size, + className, + style, +}: Props): React.ReactNode { + return ( + + + + ); +} diff --git a/website/src/pages/showcase/_components/FavoriteIcon/styles.module.css b/website/src/pages/showcase/_components/FavoriteIcon/styles.module.css new file mode 100644 index 000000000000..4eda3471db71 --- /dev/null +++ b/website/src/pages/showcase/_components/FavoriteIcon/styles.module.css @@ -0,0 +1,27 @@ +/** + * 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. + */ + +.svg { + user-select: none; + color: #e9669e; + width: 1em; + height: 1em; + display: inline-block; + fill: currentColor; +} + +.small { + font-size: 1rem; +} + +.medium { + font-size: 1.25rem; +} + +.large { + font-size: 1.8rem; +} diff --git a/website/src/pages/showcase/_components/ShowcaseCard/index.tsx b/website/src/pages/showcase/_components/ShowcaseCard/index.tsx index 64fc033dae6a..4fe23c08c313 100644 --- a/website/src/pages/showcase/_components/ShowcaseCard/index.tsx +++ b/website/src/pages/showcase/_components/ShowcaseCard/index.tsx @@ -10,7 +10,6 @@ import clsx from 'clsx'; import Link from '@docusaurus/Link'; import Translate from '@docusaurus/Translate'; import Image from '@theme/IdealImage'; -import FavoriteIcon from '@site/src/components/svgIcons/FavoriteIcon'; import { Tags, TagList, @@ -20,6 +19,7 @@ import { } from '@site/src/data/users'; import {sortBy} from '@site/src/utils/jsUtils'; import Heading from '@theme/Heading'; +import FavoriteIcon from '../FavoriteIcon'; import Tooltip from '../ShowcaseTooltip'; import styles from './styles.module.css'; @@ -84,7 +84,7 @@ function ShowcaseCard({user}: {user: User}) { {user.tags.includes('favorite') && ( - + )} {user.source && ( + user.tags.includes('favorite'), +); + +const otherUsers = sortedUsers.filter( + (user) => !user.tags.includes('favorite'), +); + +export default function ShowcaseCards() { + const filteredUsers = useFilteredUsers(); + + if (filteredUsers.length === 0) { + return ( +
    +
    + + No result + +
    +
    + ); + } + + return ( +
    + {filteredUsers.length === sortedUsers.length ? ( + <> +
    +
    +
    + + + Our favorites + + + +
    +
      + {favoriteUsers.map((user) => ( + + ))} +
    +
    +
    +
    + + All sites + +
      + {otherUsers.map((user) => ( + + ))} +
    +
    + + ) : ( +
    +
    +
      + {filteredUsers.map((user) => ( + + ))} +
    +
    + )} +
    + ); +} diff --git a/website/src/pages/showcase/_components/ShowcaseCards/styles.module.css b/website/src/pages/showcase/_components/ShowcaseCards/styles.module.css new file mode 100644 index 000000000000..4af8962009a9 --- /dev/null +++ b/website/src/pages/showcase/_components/ShowcaseCards/styles.module.css @@ -0,0 +1,32 @@ +/** + * 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. + */ + +.showcaseList { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 24px; +} + +.showcaseFavorite { + padding-top: 2rem; + padding-bottom: 2rem; + background-color: var(--site-color-favorite-background); +} + +.showcaseFavoriteHeader { + display: flex; + align-items: center; +} + +.showcaseFavoriteHeader > h2 { + margin-bottom: 0; +} + +.showcaseFavoriteHeader > svg { + width: 30px; + height: 30px; +} diff --git a/website/src/pages/showcase/index.tsx b/website/src/pages/showcase/index.tsx index f226a8ac6f37..5540b4ad6a7a 100644 --- a/website/src/pages/showcase/index.tsx +++ b/website/src/pages/showcase/index.tsx @@ -12,14 +12,14 @@ import {useHistory, useLocation} from '@docusaurus/router'; import Link from '@docusaurus/Link'; import Layout from '@theme/Layout'; -import FavoriteIcon from '@site/src/components/svgIcons/FavoriteIcon'; -import {sortedUsers, Tags, TagList} from '@site/src/data/users'; +import FavoriteIcon from '@site/src/pages/showcase/_components/FavoriteIcon'; +import {Tags, TagList} from '@site/src/data/users'; import Heading from '@theme/Heading'; import ShowcaseTagSelect from './_components/ShowcaseTagSelect'; import OperatorButton from './_components/OperatorButton'; import ClearAllButton from './_components/ClearAllButton'; -import ShowcaseCard from './_components/ShowcaseCard'; +import ShowcaseCards from './_components/ShowcaseCards'; import ShowcaseTooltip from './_components/ShowcaseTooltip'; import { @@ -29,6 +29,7 @@ import { useFilteredUsers, useSiteCountPlural, } from './_utils'; + import styles from './styles.module.css'; const TITLE = translate({message: 'Docusaurus Site Showcase'}); @@ -72,7 +73,6 @@ function ShowcaseFilters() { {TagList.map((tag, i) => { const {label, description, color} = Tags[tag]; const id = `showcase_checkbox_id_${tag}`; - return (
  • + ) : ( - user.tags.includes('favorite'), -); -const otherUsers = sortedUsers.filter( - (user) => !user.tags.includes('favorite'), -); - function SearchBar() { const history = useHistory(); const location = useLocation(); @@ -151,78 +147,6 @@ function SearchBar() { ); } -function ShowcaseCards() { - const filteredUsers = useFilteredUsers(); - - if (filteredUsers.length === 0) { - return ( -
    -
    - - No result - -
    -
    - ); - } - - return ( -
    - {filteredUsers.length === sortedUsers.length ? ( - <> -
    -
    -
    - - - Our favorites - - - -
    -
      - {favoriteUsers.map((user) => ( - - ))} -
    -
    -
    -
    - - All sites - -
      - {otherUsers.map((user) => ( - - ))} -
    -
    - - ) : ( -
    -
    -
      - {filteredUsers.map((user) => ( - - ))} -
    -
    - )} -
    - ); -} - export default function Showcase(): JSX.Element { return ( diff --git a/website/src/pages/showcase/styles.module.css b/website/src/pages/showcase/styles.module.css index d9cfd94b0428..5f4295c48479 100644 --- a/website/src/pages/showcase/styles.module.css +++ b/website/src/pages/showcase/styles.module.css @@ -79,17 +79,3 @@ width: 30px; height: 30px; } - -.svgIconFavoriteXs, -.svgIconFavorite { - color: var(--site-color-svg-icon-favorite); -} - -.svgIconFavoriteXs { - margin-left: 0.625rem; - font-size: 1rem; -} - -.svgIconFavorite { - margin-left: 1rem; -} From 3ba90f2d5ed1f33a3a9862c24756c4e54e5f6d1e Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 5 Apr 2024 19:02:01 +0200 Subject: [PATCH 05/18] commit --- .../_components/ShowcaseCards/index.tsx | 104 +++++++++--------- .../ShowcaseCards/styles.module.css | 13 +-- website/src/pages/showcase/styles.module.css | 26 ----- 3 files changed, 55 insertions(+), 88 deletions(-) diff --git a/website/src/pages/showcase/_components/ShowcaseCards/index.tsx b/website/src/pages/showcase/_components/ShowcaseCards/index.tsx index 31935b50039c..8fcf68cd2f0f 100644 --- a/website/src/pages/showcase/_components/ShowcaseCards/index.tsx +++ b/website/src/pages/showcase/_components/ShowcaseCards/index.tsx @@ -7,7 +7,7 @@ import clsx from 'clsx'; import Translate from '@docusaurus/Translate'; -import {sortedUsers} from '@site/src/data/users'; +import {sortedUsers, type User} from '@site/src/data/users'; import Heading from '@theme/Heading'; import FavoriteIcon from '../FavoriteIcon'; import ShowcaseCard from '../ShowcaseCard'; @@ -23,72 +23,74 @@ const otherUsers = sortedUsers.filter( (user) => !user.tags.includes('favorite'), ); +function HeadingNoResult() { + return ( + + No result + + ); +} + +function HeadingFavorites() { + return ( + + Our favorites + + + ); +} + +function HeadingAllSites() { + return ( + + All sites + + ); +} + +function CardList({items}: {items: User[]}) { + return ( +
      + {items.map((item) => ( + + ))} +
    + ); +} + +function NoResultSection() { + return ( +
    +
    + +
    +
    + ); +} + export default function ShowcaseCards() { const filteredUsers = useFilteredUsers(); if (filteredUsers.length === 0) { - return ( -
    -
    - - No result - -
    -
    - ); + return ; } return (
    {filteredUsers.length === sortedUsers.length ? ( <> -
    -
    -
    - - - Our favorites - - - -
    -
      - {favoriteUsers.map((user) => ( - - ))} -
    -
    +
    + +
    - - All sites - -
      - {otherUsers.map((user) => ( - - ))} -
    + +
    ) : (
    -
    -
      - {filteredUsers.map((user) => ( - - ))} -
    +
    )}
    diff --git a/website/src/pages/showcase/_components/ShowcaseCards/styles.module.css b/website/src/pages/showcase/_components/ShowcaseCards/styles.module.css index 4af8962009a9..b3cb35f09387 100644 --- a/website/src/pages/showcase/_components/ShowcaseCards/styles.module.css +++ b/website/src/pages/showcase/_components/ShowcaseCards/styles.module.css @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -.showcaseList { +.cardList { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 24px; @@ -17,16 +17,7 @@ background-color: var(--site-color-favorite-background); } -.showcaseFavoriteHeader { +.headingFavorites { display: flex; align-items: center; } - -.showcaseFavoriteHeader > h2 { - margin-bottom: 0; -} - -.showcaseFavoriteHeader > svg { - width: 30px; - height: 30px; -} diff --git a/website/src/pages/showcase/styles.module.css b/website/src/pages/showcase/styles.module.css index 5f4295c48479..30290313e1fe 100644 --- a/website/src/pages/showcase/styles.module.css +++ b/website/src/pages/showcase/styles.module.css @@ -53,29 +53,3 @@ padding: 10px; border: 1px solid gray; } - -.showcaseList { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 24px; -} - -.showcaseFavorite { - padding-top: 2rem; - padding-bottom: 2rem; - background-color: var(--site-color-favorite-background); -} - -.showcaseFavoriteHeader { - display: flex; - align-items: center; -} - -.showcaseFavoriteHeader > h2 { - margin-bottom: 0; -} - -.showcaseFavoriteHeader > svg { - width: 30px; - height: 30px; -} From 2361fd4ca93397b98791a3a793727a90a7337ef3 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 5 Apr 2024 19:50:28 +0200 Subject: [PATCH 06/18] commit --- .../_components/ShowcaseFilters/index.tsx | 113 ++++++++++++++++++ .../ShowcaseFilters/styles.module.css | 53 ++++++++ website/src/pages/showcase/index.tsx | 76 +----------- 3 files changed, 168 insertions(+), 74 deletions(-) create mode 100644 website/src/pages/showcase/_components/ShowcaseFilters/index.tsx create mode 100644 website/src/pages/showcase/_components/ShowcaseFilters/styles.module.css diff --git a/website/src/pages/showcase/_components/ShowcaseFilters/index.tsx b/website/src/pages/showcase/_components/ShowcaseFilters/index.tsx new file mode 100644 index 000000000000..d4be07ad1ba1 --- /dev/null +++ b/website/src/pages/showcase/_components/ShowcaseFilters/index.tsx @@ -0,0 +1,113 @@ +/** + * 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 type {ReactNode, CSSProperties} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import FavoriteIcon from '@site/src/pages/showcase/_components/FavoriteIcon'; +import {Tags, TagList, type TagType} from '@site/src/data/users'; +import Heading from '@theme/Heading'; +import ShowcaseTagSelect from '../ShowcaseTagSelect'; +import OperatorButton from '../OperatorButton'; +import ClearAllButton from '../ClearAllButton'; +import ShowcaseTooltip from '../ShowcaseTooltip'; +import {useFilteredUsers, useSiteCountPlural} from '../../_utils'; + +import styles from './styles.module.css'; + +function TagCircleIcon({color, style}: {color: string; style?: CSSProperties}) { + return ( + + ); +} + +function ShowcaseTagListItem({tag}: {tag: TagType}) { + const {label, description, color} = Tags[tag]; + const id = `showcase_checkbox_id_${tag}`; + return ( +
  • + + + ) : ( + + ) + } + /> + +
  • + ); +} + +function ShowcaseTagList() { + return ( +
      + {TagList.map((tag) => { + return ; + })} +
    + ); +} + +function HeadingText() { + const filteredUsers = useFilteredUsers(); + const siteCountPlural = useSiteCountPlural(); + return ( +
    + + Filters + + {siteCountPlural(filteredUsers.length)} +
    + ); +} + +function HeadingButtons() { + return ( +
    + + +
    + ); +} + +function HeadingRow() { + return ( +
    + + +
    + ); +} + +export default function ShowcaseFilters(): ReactNode { + return ( +
    + + +
    + ); +} diff --git a/website/src/pages/showcase/_components/ShowcaseFilters/styles.module.css b/website/src/pages/showcase/_components/ShowcaseFilters/styles.module.css new file mode 100644 index 000000000000..627b3322a12c --- /dev/null +++ b/website/src/pages/showcase/_components/ShowcaseFilters/styles.module.css @@ -0,0 +1,53 @@ +/** + * 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. + */ + +.headingRow { + display: flex; + align-items: center; + justify-content: space-between; +} + +.headingText { + display: flex; + align-items: baseline; +} + +.headingText > h2 { + margin-bottom: 0; +} + +.headingText > span { + margin-left: 8px; +} + +.headingButtons { + display: flex; + align-items: center; +} + +.headingButtons > * { + margin-left: 8px; +} + +.tagList { + display: flex; + align-items: center; + flex-wrap: wrap; +} + +.tagListItem { + user-select: none; + white-space: nowrap; + height: 32px; + font-size: 0.8rem; + margin-top: 0.5rem; + margin-right: 0.5rem; +} + +.tagListItem:last-child { + margin-right: 0; +} diff --git a/website/src/pages/showcase/index.tsx b/website/src/pages/showcase/index.tsx index 5540b4ad6a7a..984b413f0a0a 100644 --- a/website/src/pages/showcase/index.tsx +++ b/website/src/pages/showcase/index.tsx @@ -6,29 +6,16 @@ */ import {useState, useEffect} from 'react'; -import clsx from 'clsx'; import Translate, {translate} from '@docusaurus/Translate'; import {useHistory, useLocation} from '@docusaurus/router'; import Link from '@docusaurus/Link'; import Layout from '@theme/Layout'; -import FavoriteIcon from '@site/src/pages/showcase/_components/FavoriteIcon'; -import {Tags, TagList} from '@site/src/data/users'; import Heading from '@theme/Heading'; -import ShowcaseTagSelect from './_components/ShowcaseTagSelect'; -import OperatorButton from './_components/OperatorButton'; -import ClearAllButton from './_components/ClearAllButton'; import ShowcaseCards from './_components/ShowcaseCards'; -import ShowcaseTooltip from './_components/ShowcaseTooltip'; - -import { - prepareUserState, - readSearchName, - setSearchName, - useFilteredUsers, - useSiteCountPlural, -} from './_utils'; +import ShowcaseFilters from './_components/ShowcaseFilters'; +import {prepareUserState, readSearchName, setSearchName} from './_utils'; import styles from './styles.module.css'; @@ -52,65 +39,6 @@ function ShowcaseHeader() { ); } -function ShowcaseFilters() { - const filteredUsers = useFilteredUsers(); - const siteCountPlural = useSiteCountPlural(); - return ( -
    -
    -
    - - Filters - - {siteCountPlural(filteredUsers.length)} -
    -
    - - -
    -
    -
      - {TagList.map((tag, i) => { - const {label, description, color} = Tags[tag]; - const id = `showcase_checkbox_id_${tag}`; - return ( -
    • - - - ) : ( - - ) - } - /> - -
    • - ); - })} -
    -
    - ); -} - function SearchBar() { const history = useHistory(); const location = useLocation(); From abb52e01357376ee5ded63bf53f7cfd55bbb513d Mon Sep 17 00:00:00 2001 From: sebastien Date: Sun, 7 Apr 2024 11:28:06 +0200 Subject: [PATCH 07/18] extract ShowcaseSearchBar --- .../_components/ShowcaseSearchBar/index.tsx | 49 +++++++++++++++++ .../ShowcaseSearchBar/styles.module.css | 17 ++++++ website/src/pages/showcase/index.tsx | 44 +-------------- website/src/pages/showcase/styles.module.css | 55 ------------------- 4 files changed, 68 insertions(+), 97 deletions(-) create mode 100644 website/src/pages/showcase/_components/ShowcaseSearchBar/index.tsx create mode 100644 website/src/pages/showcase/_components/ShowcaseSearchBar/styles.module.css delete mode 100644 website/src/pages/showcase/styles.module.css diff --git a/website/src/pages/showcase/_components/ShowcaseSearchBar/index.tsx b/website/src/pages/showcase/_components/ShowcaseSearchBar/index.tsx new file mode 100644 index 000000000000..0ba42a67f1e1 --- /dev/null +++ b/website/src/pages/showcase/_components/ShowcaseSearchBar/index.tsx @@ -0,0 +1,49 @@ +/** + * 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, useEffect, type ReactNode} from 'react'; +import {translate} from '@docusaurus/Translate'; +import {useHistory, useLocation} from '@docusaurus/router'; +import {prepareUserState, readSearchName, setSearchName} from '../../_utils'; + +import styles from './styles.module.css'; + +export default function ShowcaseSearchBar(): ReactNode { + const history = useHistory(); + const location = useLocation(); + const [value, setValue] = useState(null); + useEffect(() => { + setValue(readSearchName(location.search)); + }, [location]); + return ( +
    + { + const name = e.currentTarget.value; + setValue(name); + const newSearch = setSearchName(location.search, name); + history.push({ + ...location, + search: newSearch, + state: prepareUserState(), + }); + + // TODO ??? + setTimeout(() => { + document.getElementById('searchbar')?.focus(); + }, 0); + }} + /> +
    + ); +} diff --git a/website/src/pages/showcase/_components/ShowcaseSearchBar/styles.module.css b/website/src/pages/showcase/_components/ShowcaseSearchBar/styles.module.css new file mode 100644 index 000000000000..22e60d479b14 --- /dev/null +++ b/website/src/pages/showcase/_components/ShowcaseSearchBar/styles.module.css @@ -0,0 +1,17 @@ +/** + * 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. + */ + +.searchBar { + margin-left: auto; +} + +.searchBar input { + height: 30px; + border-radius: 15px; + padding: 10px; + border: 1px solid gray; +} diff --git a/website/src/pages/showcase/index.tsx b/website/src/pages/showcase/index.tsx index 984b413f0a0a..471342f12088 100644 --- a/website/src/pages/showcase/index.tsx +++ b/website/src/pages/showcase/index.tsx @@ -5,19 +5,15 @@ * LICENSE file in the root directory of this source tree. */ -import {useState, useEffect} from 'react'; import Translate, {translate} from '@docusaurus/Translate'; -import {useHistory, useLocation} from '@docusaurus/router'; import Link from '@docusaurus/Link'; import Layout from '@theme/Layout'; import Heading from '@theme/Heading'; +import ShowcaseSearchBar from '@site/src/pages/showcase/_components/ShowcaseSearchBar'; import ShowcaseCards from './_components/ShowcaseCards'; import ShowcaseFilters from './_components/ShowcaseFilters'; -import {prepareUserState, readSearchName, setSearchName} from './_utils'; - -import styles from './styles.module.css'; const TITLE = translate({message: 'Docusaurus Site Showcase'}); const DESCRIPTION = translate({ @@ -39,42 +35,6 @@ function ShowcaseHeader() { ); } -function SearchBar() { - const history = useHistory(); - const location = useLocation(); - const [value, setValue] = useState(null); - useEffect(() => { - setValue(readSearchName(location.search)); - }, [location]); - return ( -
    - { - const name = e.currentTarget.value; - setValue(name); - const newSearch = setSearchName(location.search, name); - history.push({ - ...location, - search: newSearch, - state: prepareUserState(), - }); - - // TODO ??? - setTimeout(() => { - document.getElementById('searchbar')?.focus(); - }, 0); - }} - /> -
    - ); -} - export default function Showcase(): JSX.Element { return ( @@ -84,7 +44,7 @@ export default function Showcase(): JSX.Element {
    - +
    diff --git a/website/src/pages/showcase/styles.module.css b/website/src/pages/showcase/styles.module.css deleted file mode 100644 index 30290313e1fe..000000000000 --- a/website/src/pages/showcase/styles.module.css +++ /dev/null @@ -1,55 +0,0 @@ -/** - * 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. - */ - -.filterCheckbox { - justify-content: space-between; -} - -.filterCheckbox, -.checkboxList { - display: flex; - align-items: center; -} - -.filterCheckbox > div:first-child { - display: flex; - flex: 1 1 auto; - align-items: center; -} - -.filterCheckbox > div > * { - margin-bottom: 0; - margin-right: 8px; -} - -.checkboxList { - flex-wrap: wrap; -} - -.checkboxListItem { - user-select: none; - white-space: nowrap; - height: 32px; - font-size: 0.8rem; - margin-top: 0.5rem; - margin-right: 0.5rem; -} - -.checkboxListItem:last-child { - margin-right: 0; -} - -.searchContainer { - margin-left: auto; -} - -.searchContainer input { - height: 30px; - border-radius: 15px; - padding: 10px; - border: 1px solid gray; -} From a2578ed49539ca7e60e9fcbd9df50208c6baf071 Mon Sep 17 00:00:00 2001 From: sebastien Date: Tue, 9 Apr 2024 18:39:40 +0200 Subject: [PATCH 08/18] cleanup, we don't need userState / focus management thing --- packages/docusaurus-theme-common/src/index.ts | 2 ++ .../_components/ClearAllButton/index.tsx | 2 -- .../_components/OperatorButton/index.tsx | 3 -- .../_components/ShowcaseSearchBar/index.tsx | 17 ++++------- .../_components/ShowcaseTagSelect/index.tsx | 2 -- website/src/pages/showcase/_utils.tsx | 30 +------------------ 6 files changed, 8 insertions(+), 48 deletions(-) diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts index b39a078c1bc0..a18bc69e5bc8 100644 --- a/packages/docusaurus-theme-common/src/index.ts +++ b/packages/docusaurus-theme-common/src/index.ts @@ -98,6 +98,8 @@ export {useDocsPreferredVersion} from './contexts/docsPreferredVersion'; export {processAdmonitionProps} from './utils/admonitionUtils'; +export {useQueryString} from './utils/historyUtils'; + export { SkipToContentFallbackId, SkipToContentLink, diff --git a/website/src/pages/showcase/_components/ClearAllButton/index.tsx b/website/src/pages/showcase/_components/ClearAllButton/index.tsx index a7378f0c453b..af3ae9b32fd8 100644 --- a/website/src/pages/showcase/_components/ClearAllButton/index.tsx +++ b/website/src/pages/showcase/_components/ClearAllButton/index.tsx @@ -7,7 +7,6 @@ import React from 'react'; import {useHistory} from '@docusaurus/router'; -import {prepareUserState} from '../../_utils'; export default function ClearAllButton() { const history = useHistory(); @@ -15,7 +14,6 @@ export default function ClearAllButton() { history.push({ ...history.location, search: '', - state: prepareUserState(), }); }; diff --git a/website/src/pages/showcase/_components/OperatorButton/index.tsx b/website/src/pages/showcase/_components/OperatorButton/index.tsx index 8db89cfdec7f..15351b6bd471 100644 --- a/website/src/pages/showcase/_components/OperatorButton/index.tsx +++ b/website/src/pages/showcase/_components/OperatorButton/index.tsx @@ -9,8 +9,6 @@ import React, {useState, useEffect, useCallback} from 'react'; import clsx from 'clsx'; import {useHistory, useLocation} from '@docusaurus/router'; -import {prepareUserState} from '../../_utils'; - import styles from './styles.module.css'; export type Operator = 'OR' | 'AND'; @@ -50,7 +48,6 @@ function useOperator() { history.push({ ...location, search: newSearch, - state: prepareUserState(), }); }, [operator, location, history]); diff --git a/website/src/pages/showcase/_components/ShowcaseSearchBar/index.tsx b/website/src/pages/showcase/_components/ShowcaseSearchBar/index.tsx index 0ba42a67f1e1..d1278f5a328e 100644 --- a/website/src/pages/showcase/_components/ShowcaseSearchBar/index.tsx +++ b/website/src/pages/showcase/_components/ShowcaseSearchBar/index.tsx @@ -8,40 +8,33 @@ import {useState, useEffect, type ReactNode} from 'react'; import {translate} from '@docusaurus/Translate'; import {useHistory, useLocation} from '@docusaurus/router'; -import {prepareUserState, readSearchName, setSearchName} from '../../_utils'; +import {readSearchName, setSearchName} from '../../_utils'; import styles from './styles.module.css'; export default function ShowcaseSearchBar(): ReactNode { const history = useHistory(); const location = useLocation(); - const [value, setValue] = useState(null); + const [value, setValue] = useState(''); useEffect(() => { - setValue(readSearchName(location.search)); + setValue(readSearchName(location.search) ?? ''); }, [location]); return (
    { const name = e.currentTarget.value; setValue(name); const newSearch = setSearchName(location.search, name); - history.push({ + history.replace({ ...location, search: newSearch, - state: prepareUserState(), }); - - // TODO ??? - setTimeout(() => { - document.getElementById('searchbar')?.focus(); - }, 0); }} />
    diff --git a/website/src/pages/showcase/_components/ShowcaseTagSelect/index.tsx b/website/src/pages/showcase/_components/ShowcaseTagSelect/index.tsx index 003cc938bfdc..a57f70e4104d 100644 --- a/website/src/pages/showcase/_components/ShowcaseTagSelect/index.tsx +++ b/website/src/pages/showcase/_components/ShowcaseTagSelect/index.tsx @@ -17,7 +17,6 @@ import {useHistory, useLocation} from '@docusaurus/router'; import {toggleListItem} from '@site/src/utils/jsUtils'; import type {TagType} from '@site/src/data/users'; -import {prepareUserState} from '../../_utils'; import styles from './styles.module.css'; interface Props extends ComponentProps<'input'> { @@ -57,7 +56,6 @@ function ShowcaseTagSelect( history.push({ ...location, search: newSearch, - state: prepareUserState(), }); }, [tag, location, history]); return ( diff --git a/website/src/pages/showcase/_utils.tsx b/website/src/pages/showcase/_utils.tsx index 2db6b58a90c8..63598668014a 100644 --- a/website/src/pages/showcase/_utils.tsx +++ b/website/src/pages/showcase/_utils.tsx @@ -7,7 +7,6 @@ import {useEffect, useMemo, useState} from 'react'; import {useLocation} from '@docusaurus/router'; import {translate} from '@docusaurus/Translate'; -import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; import {usePluralForm} from '@docusaurus/theme-common'; import type {TagType, User} from '@site/src/data/users'; import {sortedUsers} from '@site/src/data/users'; @@ -18,32 +17,6 @@ import { } from '@site/src/pages/showcase/_components/OperatorButton'; import {readSearchTags} from '@site/src/pages/showcase/_components/ShowcaseTagSelect'; -type UserState = { - scrollTopPosition: number; - focusedElementId: string | undefined; -}; - -export 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'; export function readSearchName(search: string) { @@ -86,7 +59,7 @@ function filterUsers( } export function useFilteredUsers() { - const location = useLocation(); + const location = useLocation(); const [operator, setOperator] = useState(DefaultOperator); // On SSR / first mount (hydration) no tag is selected const [selectedTags, setSelectedTags] = useState([]); @@ -97,7 +70,6 @@ export function useFilteredUsers() { setSelectedTags(readSearchTags(location.search)); setOperator(readOperator(location.search)); setName(readSearchName(location.search)); - restoreUserState(location.state); }, [location]); return useMemo( From 4f1d8725754103695bd01697d1a24d36378e5e7f Mon Sep 17 00:00:00 2001 From: sebastien Date: Tue, 9 Apr 2024 19:19:48 +0200 Subject: [PATCH 09/18] refactor showcase to use querystring utils --- packages/docusaurus-theme-common/src/index.ts | 7 +- .../src/utils/historyUtils.ts | 76 ++++++++++++++----- .../_components/ClearAllButton/index.tsx | 17 ++--- .../_components/ShowcaseSearchBar/index.tsx | 24 ++---- website/src/pages/showcase/_utils.tsx | 24 ++---- 5 files changed, 81 insertions(+), 67 deletions(-) diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts index a18bc69e5bc8..6859089b5a38 100644 --- a/packages/docusaurus-theme-common/src/index.ts +++ b/packages/docusaurus-theme-common/src/index.ts @@ -98,7 +98,12 @@ export {useDocsPreferredVersion} from './contexts/docsPreferredVersion'; export {processAdmonitionProps} from './utils/admonitionUtils'; -export {useQueryString} from './utils/historyUtils'; +export { + useHistorySelector, + useQueryString, + useQueryStringList, + useClearQueryString, +} from './utils/historyUtils'; export { SkipToContentFallbackId, diff --git a/packages/docusaurus-theme-common/src/utils/historyUtils.ts b/packages/docusaurus-theme-common/src/utils/historyUtils.ts index 2d896bd25107..6f4af5c2c0ce 100644 --- a/packages/docusaurus-theme-common/src/utils/historyUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/historyUtils.ts @@ -74,26 +74,24 @@ export function useQueryStringValue(key: string | null): string | null { }); } -export function useQueryStringKeySetter(): ( +function useQueryStringUpdater( key: string, - newValue: string | null, - options?: {push: boolean}, -) => void { +): (newValue: string | null, options?: {push: boolean}) => void { const history = useHistory(); return useCallback( - (key, newValue, options) => { + (newValue, options) => { const searchParams = new URLSearchParams(history.location.search); if (newValue) { searchParams.set(key, newValue); } else { searchParams.delete(key); } - const updaterFn = options?.push ? history.push : history.replace; - updaterFn({ + const updateHistory = options?.push ? history.push : history.replace; + updateHistory({ search: searchParams.toString(), }); }, - [history], + [key, history], ); } @@ -101,14 +99,56 @@ export function useQueryString( key: string, ): [string, (newValue: string, options?: {push: boolean}) => void] { const value = useQueryStringValue(key) ?? ''; - const setQueryString = useQueryStringKeySetter(); - return [ - value, - useCallback( - (newValue: string, options) => { - setQueryString(key, newValue, options); - }, - [setQueryString, key], - ), - ]; + const update = useQueryStringUpdater(key); + return [value, update]; +} + +function useQueryStringListValues(key: string): string[] { + return useHistorySelector((history) => { + return new URLSearchParams(history.location.search).getAll(key); + }); +} + +type ListUpdate = string[] | ((oldValues: string[]) => string[]); +type ListUpdateFunction = ( + update: ListUpdate, + options?: {push: boolean}, +) => void; + +function useQueryStringListUpdater(key: string): ListUpdateFunction { + const history = useHistory(); + const setValues: ListUpdateFunction = useCallback( + (update, options) => { + const searchParams = new URLSearchParams(history.location.search); + const newValues = Array.isArray(update) + ? update + : update(searchParams.getAll(key)); + searchParams.delete(key); + newValues.forEach((v) => searchParams.append(key, v)); + + const updateHistory = options?.push ? history.push : history.replace; + updateHistory({ + search: searchParams.toString(), + }); + }, + [history, key], + ); + return setValues; +} + +export function useQueryStringList( + key: string, +): [string[], ListUpdateFunction] { + const values = useQueryStringListValues(key); + const setValues = useQueryStringListUpdater(key); + return [values, setValues]; +} + +export function useClearQueryString(): () => void { + const history = useHistory(); + return useCallback(() => { + history.push({ + search: undefined, + }); + }, [history]); } diff --git a/website/src/pages/showcase/_components/ClearAllButton/index.tsx b/website/src/pages/showcase/_components/ClearAllButton/index.tsx index af3ae9b32fd8..372706062ee3 100644 --- a/website/src/pages/showcase/_components/ClearAllButton/index.tsx +++ b/website/src/pages/showcase/_components/ClearAllButton/index.tsx @@ -5,24 +5,17 @@ * LICENSE file in the root directory of this source tree. */ -import React from 'react'; -import {useHistory} from '@docusaurus/router'; - -export default function ClearAllButton() { - const history = useHistory(); - const clearAll = () => { - history.push({ - ...history.location, - search: '', - }); - }; +import React, {type ReactNode} from 'react'; +import {useClearQueryString} from '@docusaurus/theme-common'; +export default function ClearAllButton(): ReactNode { + const clearQueryString = useClearQueryString(); // TODO translate return ( ); diff --git a/website/src/pages/showcase/_components/ShowcaseSearchBar/index.tsx b/website/src/pages/showcase/_components/ShowcaseSearchBar/index.tsx index d1278f5a328e..e6a6f881612a 100644 --- a/website/src/pages/showcase/_components/ShowcaseSearchBar/index.tsx +++ b/website/src/pages/showcase/_components/ShowcaseSearchBar/index.tsx @@ -5,20 +5,14 @@ * LICENSE file in the root directory of this source tree. */ -import {useState, useEffect, type ReactNode} from 'react'; +import {type ReactNode} from 'react'; import {translate} from '@docusaurus/Translate'; -import {useHistory, useLocation} from '@docusaurus/router'; -import {readSearchName, setSearchName} from '../../_utils'; - +import {useQueryStringSearchName} from '@site/src/pages/showcase/_utils'; import styles from './styles.module.css'; export default function ShowcaseSearchBar(): ReactNode { - const history = useHistory(); - const location = useLocation(); - const [value, setValue] = useState(''); - useEffect(() => { - setValue(readSearchName(location.search) ?? ''); - }, [location]); + // TODO need to optimize these slow QS updates + const [searchName, setSearchName] = useQueryStringSearchName(); return (
    { - const name = e.currentTarget.value; - setValue(name); - const newSearch = setSearchName(location.search, name); - history.replace({ - ...location, - search: newSearch, - }); + setSearchName(e.currentTarget.value); }} />
    diff --git a/website/src/pages/showcase/_utils.tsx b/website/src/pages/showcase/_utils.tsx index 63598668014a..93f1a1ba657c 100644 --- a/website/src/pages/showcase/_utils.tsx +++ b/website/src/pages/showcase/_utils.tsx @@ -7,7 +7,7 @@ import {useEffect, useMemo, useState} from 'react'; import {useLocation} from '@docusaurus/router'; import {translate} from '@docusaurus/Translate'; -import {usePluralForm} from '@docusaurus/theme-common'; +import {usePluralForm, useQueryString} from '@docusaurus/theme-common'; import type {TagType, User} from '@site/src/data/users'; import {sortedUsers} from '@site/src/data/users'; import type {Operator} from '@site/src/pages/showcase/_components/OperatorButton'; @@ -17,19 +17,8 @@ import { } from '@site/src/pages/showcase/_components/OperatorButton'; import {readSearchTags} from '@site/src/pages/showcase/_components/ShowcaseTagSelect'; -const SearchNameQueryKey = 'name'; - -export function readSearchName(search: string) { - return new URLSearchParams(search).get(SearchNameQueryKey); -} - -export function setSearchName(search: string, value: string): string { - const newSearch = new URLSearchParams(search); - newSearch.delete(SearchNameQueryKey); - if (value) { - newSearch.set(SearchNameQueryKey, value); - } - return newSearch.toString(); +export function useQueryStringSearchName() { + return useQueryString('name'); } function filterUsers( @@ -63,18 +52,17 @@ export function useFilteredUsers() { const [operator, setOperator] = useState(DefaultOperator); // On SSR / first mount (hydration) no tag is selected const [selectedTags, setSelectedTags] = useState([]); - const [name, setName] = useState(null); + const [searchName] = useQueryStringSearchName(); // Sync tags from QS to state (delayed on purpose to avoid SSR/Client // hydration mismatch) useEffect(() => { setSelectedTags(readSearchTags(location.search)); setOperator(readOperator(location.search)); - setName(readSearchName(location.search)); }, [location]); return useMemo( - () => filterUsers(sortedUsers, selectedTags, operator, name), - [selectedTags, operator, name], + () => filterUsers(sortedUsers, selectedTags, operator, searchName), + [selectedTags, operator, searchName], ); } From ad51195080a92ca0deab2134935b4e9133311c5f Mon Sep 17 00:00:00 2001 From: sebastien Date: Tue, 9 Apr 2024 19:58:10 +0200 Subject: [PATCH 10/18] Use query string for operator --- .../src/utils/historyUtils.ts | 2 +- .../_components/OperatorButton/index.tsx | 56 +++---------------- website/src/pages/showcase/_utils.tsx | 24 +++++--- 3 files changed, 24 insertions(+), 58 deletions(-) diff --git a/packages/docusaurus-theme-common/src/utils/historyUtils.ts b/packages/docusaurus-theme-common/src/utils/historyUtils.ts index 6f4af5c2c0ce..38f733ed729b 100644 --- a/packages/docusaurus-theme-common/src/utils/historyUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/historyUtils.ts @@ -97,7 +97,7 @@ function useQueryStringUpdater( export function useQueryString( key: string, -): [string, (newValue: string, options?: {push: boolean}) => void] { +): [string, (newValue: string | null, options?: {push: boolean}) => void] { const value = useQueryStringValue(key) ?? ''; const update = useQueryStringUpdater(key); return [value, update]; diff --git a/website/src/pages/showcase/_components/OperatorButton/index.tsx b/website/src/pages/showcase/_components/OperatorButton/index.tsx index 15351b6bd471..b03e3893ea30 100644 --- a/website/src/pages/showcase/_components/OperatorButton/index.tsx +++ b/website/src/pages/showcase/_components/OperatorButton/index.tsx @@ -5,72 +5,30 @@ * LICENSE file in the root directory of this source tree. */ -import React, {useState, useEffect, useCallback} from 'react'; +import React, {useId} from 'react'; import clsx from 'clsx'; -import {useHistory, useLocation} from '@docusaurus/router'; +import {DefaultOperator, useOperator} from '../../_utils'; import styles from './styles.module.css'; -export type Operator = 'OR' | 'AND'; - -export const DefaultOperator: Operator = 'OR'; - -export const OperatorQueryKey = 'operator'; - -export function readOperator(search: string): Operator { - const qsOperator = - new URLSearchParams(search).get(OperatorQueryKey) ?? DefaultOperator; - return qsOperator === 'AND' ? 'AND' : 'OR'; -} - -function setSearchOperator(search: string, operator: Operator): string { - const searchParams = new URLSearchParams(search); - searchParams.delete(OperatorQueryKey); - if (!operator) { - searchParams.append(OperatorQueryKey, 'AND'); - } - return searchParams.toString(); -} - -function useOperator() { - const location = useLocation(); - const history = useHistory(); - - const [operator, setOperator] = useState(DefaultOperator); - useEffect(() => { - setOperator(readOperator(location.search)); - }, [location]); - - const toggleOperator = useCallback(() => { - const newOperator = operator === 'AND' ? 'OR' : 'AND'; - setOperator(newOperator); - const newSearch = setSearchOperator(location.search, newOperator); - history.push({ - ...location, - search: newSearch, - }); - }, [operator, location, history]); - - return {operator, toggleOperator}; -} - export default function OperatorButton() { - const id = 'showcase_filter_toggle'; - const {operator, toggleOperator} = useOperator(); + const id = useId(); + const [operator, toggleOperator] = useOperator(); + // TODO add translations return ( <> { if (e.key === 'Enter') { toggleOperator(); } }} - checked={operator !== DefaultOperator} />