From 964a4e458e8cf2e441e4f724215b439791042d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Wed, 10 Apr 2024 10:42:27 +0200 Subject: [PATCH] refactor(website): refactor showcase components (#10023) --- packages/docusaurus-theme-common/src/index.ts | 7 + .../src/utils/historyUtils.ts | 85 +++-- project-words.txt | 1 - website/package.json | 2 - .../svgIcons/FavoriteIcon/index.tsx | 19 -- website/src/css/custom.css | 7 - .../_components/ClearAllButton/index.tsx | 22 ++ .../_components/FavoriteIcon/index.tsx | 32 ++ .../FavoriteIcon/styles.module.css | 27 ++ .../_components/OperatorButton/index.tsx | 41 +++ .../styles.module.css | 0 .../_components/ShowcaseCard/index.tsx | 44 ++- .../ShowcaseCard/styles.module.css | 6 +- .../_components/ShowcaseCards/index.tsx | 98 ++++++ .../ShowcaseCards/styles.module.css | 27 ++ .../ShowcaseFilterToggle/index.tsx | 85 ----- .../_components/ShowcaseFilters/index.tsx | 109 +++++++ .../ShowcaseFilters/styles.module.css | 53 ++++ .../_components/ShowcaseSearchBar/index.tsx | 29 ++ .../ShowcaseSearchBar/styles.module.css | 17 + .../_components/ShowcaseTagSelect/index.tsx | 87 ++---- .../ShowcaseTagSelect/styles.module.css | 6 +- .../_components/ShowcaseTooltip/index.tsx | 145 --------- .../ShowcaseTooltip/styles.module.css | 45 --- website/src/pages/showcase/_utils.tsx | 99 ++++++ website/src/pages/showcase/index.tsx | 294 +----------------- website/src/pages/showcase/styles.module.css | 95 ------ yarn.lock | 63 ++-- 28 files changed, 723 insertions(+), 822 deletions(-) delete mode 100644 website/src/components/svgIcons/FavoriteIcon/index.tsx create mode 100644 website/src/pages/showcase/_components/ClearAllButton/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/OperatorButton/index.tsx rename website/src/pages/showcase/_components/{ShowcaseFilterToggle => OperatorButton}/styles.module.css (100%) create mode 100644 website/src/pages/showcase/_components/ShowcaseCards/index.tsx create mode 100644 website/src/pages/showcase/_components/ShowcaseCards/styles.module.css delete mode 100644 website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx create mode 100644 website/src/pages/showcase/_components/ShowcaseFilters/index.tsx create mode 100644 website/src/pages/showcase/_components/ShowcaseFilters/styles.module.css 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/_components/ShowcaseTooltip/index.tsx delete mode 100644 website/src/pages/showcase/_components/ShowcaseTooltip/styles.module.css create mode 100644 website/src/pages/showcase/_utils.tsx delete mode 100644 website/src/pages/showcase/styles.module.css diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts index b39a078c1bc0..6859089b5a38 100644 --- a/packages/docusaurus-theme-common/src/index.ts +++ b/packages/docusaurus-theme-common/src/index.ts @@ -98,6 +98,13 @@ export {useDocsPreferredVersion} from './contexts/docsPreferredVersion'; export {processAdmonitionProps} from './utils/admonitionUtils'; +export { + useHistorySelector, + useQueryString, + useQueryStringList, + useClearQueryString, +} from './utils/historyUtils'; + export { SkipToContentFallbackId, SkipToContentLink, diff --git a/packages/docusaurus-theme-common/src/utils/historyUtils.ts b/packages/docusaurus-theme-common/src/utils/historyUtils.ts index 2d896bd25107..4695896a1ecd 100644 --- a/packages/docusaurus-theme-common/src/utils/historyUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/historyUtils.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {useCallback, useEffect, useSyncExternalStore} from 'react'; +import {useCallback, useEffect, useMemo, useSyncExternalStore} from 'react'; import {useHistory} from '@docusaurus/router'; import {useEvent} from './reactUtils'; @@ -74,41 +74,86 @@ 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], ); } 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 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[] { + // Unfortunately we can't just use searchParams.getAll(key) in the selector + // It would create a new array every time and lead to an infinite loop... + // The selector has to return a primitive/string value to avoid that... + const arrayJsonString = useHistorySelector((history) => { + const values = new URLSearchParams(history.location.search).getAll(key); + return JSON.stringify(values); + }); + return useMemo(() => JSON.parse(arrayJsonString), [arrayJsonString]); +} + +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.replace({ + search: undefined, + }); + }, [history]); } diff --git a/project-words.txt b/project-words.txt index 7066441d2dfe..b46635c951ec 100644 --- a/project-words.txt +++ b/project-words.txt @@ -301,7 +301,6 @@ rtcts rtlcss saurus Scaleway -searchbar Sebastien sebastien sebastienlorber diff --git a/website/package.json b/website/package.json index 3156aa68d0d1..30dab58aa5eb 100644 --- a/website/package.json +++ b/website/package.json @@ -49,7 +49,6 @@ "@docusaurus/theme-mermaid": "3.2.1", "@docusaurus/utils": "3.2.1", "@docusaurus/utils-common": "3.2.1", - "@popperjs/core": "^2.11.8", "@swc/core": "1.2.197", "clsx": "^2.0.0", "color": "^4.2.3", @@ -61,7 +60,6 @@ "react-dom": "^18.0.0", "react-lite-youtube-embed": "^2.3.52", "react-medium-image-zoom": "^5.1.6", - "react-popper": "^2.3.0", "rehype-katex": "^7.0.0", "remark-math": "^6.0.0", "swc-loader": "^0.2.3", 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..b48b672f6116 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -14,11 +14,6 @@ */ --site-primary-hue-saturation: 167 68%; --site-primary-hue-saturation-light: 167 56%; /* do we really need this extra one? */ - --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%); /* Use a darker color to ensure contrast, ideally we don't need important */ @@ -28,8 +23,6 @@ html[data-theme='dark'] { --site-color-feedback-background: #2a2929; - --site-color-favorite-background: #1d1e1e; - --site-color-checkbox-checked-bg: hsl(167deg 56% 73% / 10%); --docusaurus-highlighted-code-line-bg: rgb(66 66 66 / 35%); } 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..372706062ee3 --- /dev/null +++ b/website/src/pages/showcase/_components/ClearAllButton/index.tsx @@ -0,0 +1,22 @@ +/** + * 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 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/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/OperatorButton/index.tsx b/website/src/pages/showcase/_components/OperatorButton/index.tsx new file mode 100644 index 000000000000..927844af917e --- /dev/null +++ b/website/src/pages/showcase/_components/OperatorButton/index.tsx @@ -0,0 +1,41 @@ +/** + * 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, {useId} from 'react'; +import clsx from 'clsx'; +import {useOperator} from '../../_utils'; + +import styles from './styles.module.css'; + +export default function OperatorButton() { + const id = useId(); + const [operator, toggleOperator] = useOperator(); + // TODO add translations + return ( + <> + { + if (e.key === 'Enter') { + toggleOperator(); + } + }} + /> + + + ); +} 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/_components/ShowcaseCard/index.tsx b/website/src/pages/showcase/_components/ShowcaseCard/index.tsx index 5bf6935e12e7..336698958cd0 100644 --- a/website/src/pages/showcase/_components/ShowcaseCard/index.tsx +++ b/website/src/pages/showcase/_components/ShowcaseCard/index.tsx @@ -10,27 +10,28 @@ 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, - type TagType, - type User, - type Tag, -} from '@site/src/data/users'; +import {Tags, TagList, type TagType, type User} from '@site/src/data/users'; import {sortBy} from '@site/src/utils/jsUtils'; import Heading from '@theme/Heading'; -import Tooltip from '../ShowcaseTooltip'; +import FavoriteIcon from '../FavoriteIcon'; import styles from './styles.module.css'; -const TagComp = React.forwardRef( - ({label, color, description}, ref) => ( -
  • +function TagItem({ + label, + description, + color, +}: { + label: string; + description: string; + color: string; +}) { + return ( +
  • {label.toLowerCase()}
  • - ), -); + ); +} function ShowcaseCardTag({tags}: {tags: TagType[]}) { const tagObjects = tags.map((tag) => ({tag, ...Tags[tag]})); @@ -43,17 +44,7 @@ function ShowcaseCardTag({tags}: {tags: TagType[]}) { return ( <> {tagObjectsSorted.map((tagObject, index) => { - const id = `showcase_card_tag_${tagObject.tag}`; - - return ( - - - - ); + return ; })} ); @@ -62,6 +53,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` @@ -83,7 +75,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'), +); + +function HeadingNoResult() { + return ( + + No result + + ); +} + +function HeadingFavorites() { + return ( + + Our favorites + + + ); +} + +function HeadingAllSites() { + return ( + + All sites + + ); +} + +function CardList({heading, items}: {heading?: ReactNode; items: User[]}) { + return ( +
    + {heading} +
      + {items.map((item) => ( + + ))} +
    +
    + ); +} + +function NoResultSection() { + return ( +
    +
    + +
    +
    + ); +} + +export default function ShowcaseCards() { + const filteredUsers = useFilteredUsers(); + + if (filteredUsers.length === 0) { + return ; + } + + return ( +
    + {filteredUsers.length === sortedUsers.length ? ( + <> +
    + } items={favoriteUsers} /> +
    +
    + } items={otherUsers} /> +
    + + ) : ( + + )} +
    + ); +} 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..b1512e62d7ca --- /dev/null +++ b/website/src/pages/showcase/_components/ShowcaseCards/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. + */ + +.cardList { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 24px; +} + +.showcaseFavorite { + padding-top: 2rem; + padding-bottom: 2rem; + background-color: #f6fdfd; +} + +html[data-theme='dark'] .showcaseFavorite { + background-color: #232525; +} + +.headingFavorites { + display: flex; + align-items: center; +} diff --git a/website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx b/website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx deleted file mode 100644 index 78c1126c61a6..000000000000 --- a/website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx +++ /dev/null @@ -1,85 +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, {useState, useEffect, useCallback} from 'react'; -import clsx from 'clsx'; -import {useHistory, useLocation} from '@docusaurus/router'; - -import {prepareUserState} from '../../index'; - -import styles from './styles.module.css'; - -export type Operator = 'OR' | 'AND'; - -export const OperatorQueryKey = 'operator'; - -export function readOperator(search: string): Operator { - return (new URLSearchParams(search).get(OperatorQueryKey) ?? - 'OR') as Operator; -} - -export default function ShowcaseFilterToggle(): JSX.Element { - const id = 'showcase_filter_toggle'; - const location = useLocation(); - const history = useHistory(); - const [operator, setOperator] = useState(false); - useEffect(() => { - setOperator(readOperator(location.search) === 'AND'); - }, [location]); - const toggleOperator = useCallback(() => { - setOperator((o) => !o); - const searchParams = new URLSearchParams(location.search); - searchParams.delete(OperatorQueryKey); - if (!operator) { - searchParams.append(OperatorQueryKey, 'AND'); - } - history.push({ - ...location, - search: searchParams.toString(), - state: prepareUserState(), - }); - }, [operator, location, history]); - - const ClearTag = () => { - history.push({ - ...location, - search: '', - state: prepareUserState(), - }); - }; - - return ( -
    - { - if (e.key === 'Enter') { - toggleOperator(); - } - }} - checked={operator} - /> - - - -
    - ); -} 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..a65c02decdc4 --- /dev/null +++ b/website/src/pages/showcase/_components/ShowcaseFilters/index.tsx @@ -0,0 +1,109 @@ +/** + * 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 {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]; + 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/_components/ShowcaseSearchBar/index.tsx b/website/src/pages/showcase/_components/ShowcaseSearchBar/index.tsx new file mode 100644 index 000000000000..87b62b8ce39b --- /dev/null +++ b/website/src/pages/showcase/_components/ShowcaseSearchBar/index.tsx @@ -0,0 +1,29 @@ +/** + * 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} from 'react'; +import {translate} from '@docusaurus/Translate'; +import {useSearchName} from '@site/src/pages/showcase/_utils'; +import styles from './styles.module.css'; + +export default function ShowcaseSearchBar(): ReactNode { + const [searchName, setSearchName] = useSearchName(); + return ( +
    + { + setSearchName(e.currentTarget.value); + }} + /> +
    + ); +} 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/_components/ShowcaseTagSelect/index.tsx b/website/src/pages/showcase/_components/ShowcaseTagSelect/index.tsx index 7495cf42eb58..4a1e22820c52 100644 --- a/website/src/pages/showcase/_components/ShowcaseTagSelect/index.tsx +++ b/website/src/pages/showcase/_components/ShowcaseTagSelect/index.tsx @@ -7,90 +7,65 @@ import React, { useCallback, - useState, - useEffect, type ComponentProps, type ReactNode, type ReactElement, + useId, } from 'react'; -import {useHistory, useLocation} from '@docusaurus/router'; -import {toggleListItem} from '@site/src/utils/jsUtils'; import type {TagType} from '@site/src/data/users'; +import {useTags} from '../../_utils'; -import {prepareUserState} from '../../index'; import styles from './styles.module.css'; -interface Props extends ComponentProps<'input'> { - icon: ReactElement>; - label: ReactNode; - tag: TagType; -} - -const TagQueryStringKey = 'tags'; +function useTagState(tag: string) { + const [tags, setTags] = useTags(); + const isSelected = tags.includes(tag); + const toggle = useCallback(() => { + setTags((list) => { + return list.includes(tag) + ? list.filter((t) => t !== tag) + : [...list, tag]; + }); + }, [tag, setTags]); -export function readSearchTags(search: string): TagType[] { - return new URLSearchParams(search).getAll(TagQueryStringKey) as TagType[]; + return [isSelected, toggle] as const; } -function replaceSearchTags(search: string, newTags: TagType[]) { - const searchParams = new URLSearchParams(search); - searchParams.delete(TagQueryStringKey); - newTags.forEach((tag) => searchParams.append(TagQueryStringKey, tag)); - return searchParams.toString(); +interface Props extends ComponentProps<'input'> { + tag: TagType; + label: string; + description: string; + icon: ReactElement>; } -function ShowcaseTagSelect( - {id, icon, label, tag, ...rest}: Props, - ref: React.ForwardedRef, -) { - const location = useLocation(); - const history = useHistory(); - const [selected, setSelected] = useState(false); - useEffect(() => { - const tags = readSearchTags(location.search); - setSelected(tags.includes(tag)); - }, [tag, location]); - const toggleTag = useCallback(() => { - const tags = readSearchTags(location.search); - const newTags = toggleListItem(tags, tag); - const newSearch = replaceSearchTags(location.search, newTags); - history.push({ - ...location, - search: newSearch, - state: prepareUserState(), - }); - }, [tag, location, history]); +export default function ShowcaseTagSelect({ + icon, + label, + description, + tag, + ...rest +}: Props): ReactNode { + const id = useId(); + const [isSelected, toggle] = useTagState(tag); return ( <> { if (e.key === 'Enter') { - toggleTag(); + toggle(); } }} - onFocus={(e) => { - if (e.relatedTarget) { - e.target.nextElementSibling?.dispatchEvent( - new KeyboardEvent('focus'), - ); - } - }} - onBlur={(e) => { - e.target.nextElementSibling?.dispatchEvent(new KeyboardEvent('blur')); - }} - onChange={toggleTag} - checked={selected} {...rest} /> -