diff --git a/packages/docusaurus/src/choosePort.ts b/packages/docusaurus/src/choosePort.ts index 8dc4f8df09..7e430d9909 100644 --- a/packages/docusaurus/src/choosePort.ts +++ b/packages/docusaurus/src/choosePort.ts @@ -92,6 +92,7 @@ export default async function choosePort( new Promise((resolve) => { if (port === defaultPort) { resolve(port); + return; } const message = process.platform !== 'win32' && defaultPort < 1024 && !isRoot() diff --git a/website/package.json b/website/package.json index 32806d4cbc..f6be84f510 100644 --- a/website/package.json +++ b/website/package.json @@ -39,6 +39,7 @@ "@docusaurus/remark-plugin-npm2yarn": "2.0.0-beta.9", "@docusaurus/theme-live-codeblock": "2.0.0-beta.9", "@docusaurus/utils": "2.0.0-beta.9", + "@popperjs/core": "^2.10.2", "clsx": "^1.1.1", "color": "^4.0.1", "esbuild-loader": "2.13.1", @@ -47,6 +48,7 @@ "npm-to-yarn": "^1.0.0-2", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-popper": "^2.2.5", "rehype-katex": "^4.0.0", "remark-math": "^3.0.1", "workbox-routing": "^5.0.0", diff --git a/website/src/components/Svg/index.tsx b/website/src/components/Svg/index.tsx new file mode 100644 index 0000000000..8b607a4c29 --- /dev/null +++ b/website/src/components/Svg/index.tsx @@ -0,0 +1,42 @@ +/** + * 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, {ReactNode, ComponentProps} from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.css'; + +export interface SvgIconProps extends ComponentProps<'svg'> { + viewBox?: string; + size?: 'inherit' | 'small' | 'medium' | 'large'; + color?: 'inherit' | 'primary' | 'secondary' | 'success' | 'error' | 'warning'; + svgClass?: string; // Class attribute on the child + colorAttr?: string; // Applies a color attribute to the SVG element. + children: ReactNode; // Node passed into the SVG element. +} + +export default function Svg(props: SvgIconProps): JSX.Element { + const { + svgClass, + colorAttr, + children, + color = 'inherit', + size = 'medium', + viewBox = '0 0 24 24', + ...rest + } = props; + + return ( + + {children} + + ); +} diff --git a/website/src/components/Svg/styles.module.css b/website/src/components/Svg/styles.module.css new file mode 100644 index 0000000000..db2d48e3ea --- /dev/null +++ b/website/src/components/Svg/styles.module.css @@ -0,0 +1,54 @@ +/** + * 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. + */ + +.svgIcon { + user-select: none; + width: 1em; + height: 1em; + display: inline-block; + fill: currentColor; + flex-shrink: 0; + color: inherit; +} + +/* font-size */ +.small { + font-size: 1.25rem; +} + +.medium { + font-size: 1.5rem; +} + +.large { + font-size: 2.185rem; +} + +/* colors */ +.primary { + color: var(--ifm-color-primary); +} + +.secondary { + color: var(--ifm-color-secondary); +} + +.success { + color: var(--ifm-color-success); +} + +.error { + color: var(--ifm-color-error); +} + +.warning { + color: var(--ifm-color-warning); +} + +.inherit { + color: inherit; +} diff --git a/website/src/components/showcase/ShowcaseCard/index.tsx b/website/src/components/showcase/ShowcaseCard/index.tsx index 3b4a812d13..724a0df7e9 100644 --- a/website/src/components/showcase/ShowcaseCard/index.tsx +++ b/website/src/components/showcase/ShowcaseCard/index.tsx @@ -6,90 +6,96 @@ */ import React, {memo} from 'react'; - -import styles from './styles.module.css'; import clsx from 'clsx'; import Image from '@theme/IdealImage'; -import {Tags, TagList, TagType, User, Tag} from '../../../data/users'; -import {sortBy} from '../../../utils/jsUtils'; +import Link from '@docusaurus/Link'; -function TagIcon({label, description, icon}: Tag) { - return ( - - {icon} - - ); +import styles from './styles.module.css'; +import FavoriteIcon from '@site/src/components/svgIcons/FavoriteIcon'; +import Tooltip from '@site/src/components/showcase/ShowcaseTooltip'; +import {Tags, TagList, TagType, User, Tag} from '@site/src/data/users'; +import {sortBy} from '@site/src/utils/jsUtils'; + +interface Props extends Tag { + id: string; } -function ShowcaseCardTagIcons({tags}: {tags: TagType[]}) { - const tagObjects = tags - .map((tag) => ({tag, ...Tags[tag]})) - .filter((tagObject) => !!tagObject.icon); +const TagComp = React.forwardRef( + ({id, label, color, description}, ref) => ( +
  • + {label.toLowerCase()} + +
  • + ), +); - // Keep same order of icons for all tags +function ShowcaseCardTag({tags}: {tags: TagType[]}) { + const tagObjects = tags.map((tag) => ({tag, ...Tags[tag]})); + + // Keep same order for all tags const tagObjectsSorted = sortBy(tagObjects, (tagObject) => TagList.indexOf(tagObject.tag), ); return ( <> - {tagObjectsSorted.map((tagObject, index) => ( - - ))} + {tagObjectsSorted.map((tagObject, index) => { + const id = `showcase_card_tag_${tagObject.tag}`; + + return ( + + + + ); + })} ); } const ShowcaseCard = memo(({user}: {user: User}) => ( -
    -
    -
    - {user.title} -
    -
    -
    -
    -
    -
    -
    {user.title}
    -
    -
    - -
    -
    - {user.description} -
    -
    -
    - {(user.website || user.source) && ( -
    -
    - {user.website && ( - - Website - - )} - {user.source && ( - - Source - - )} -
    -
    - )} +
  • +
    + {user.title}
    -
  • +
    +
    +

    + + {user.title} + +

    + {user.tags.includes('favorite') && ( + + )} + {user.source && ( + + source + + )} +
    +

    {user.description}

    +
    +
      + +
    + )); export default ShowcaseCard; diff --git a/website/src/components/showcase/ShowcaseCard/styles.module.css b/website/src/components/showcase/ShowcaseCard/styles.module.css index 2495278b25..4f6c4dbb3f 100644 --- a/website/src/components/showcase/ShowcaseCard/styles.module.css +++ b/website/src/components/showcase/ShowcaseCard/styles.module.css @@ -5,31 +5,125 @@ * LICENSE file in the root directory of this source tree. */ -.showcaseCard { - height: 100%; -} - .showcaseCardImage { - max-height: 175px; overflow: hidden; + max-height: 150px; + border-bottom: 4px solid var(--ifm-color-primary); } -.titleIconsRow { +.showcaseCardHeader { display: flex; - flex-direction: row; - flex-wrap: nowrap; + align-items: center; + margin-bottom: 12px; } -.titleIconsRowTitle { - flex: 1; +.showcaseCardTitle { + margin-bottom: 0; + flex: 1 1 auto; } -.titleIconsRowIcons { +.showcaseCardTitle a { + text-decoration: none; + position: relative; +} + +.showcaseCardTitle a::after { + display: block; + content: ''; + position: absolute; + width: 0; + height: 1px; + transition: width ease-out 200ms; + background-color: var(--ifm-color-primary); + bottom: 0; + left: 0; +} + +.showcaseCardTitle a:hover::after, +.showcaseCardTitle a:focus-visible::after { + width: 100%; +} + +.showcaseCardTitle, +.showcaseCardHeader .svgIconFavorite { + margin-right: 0.25rem; +} + +.showcaseCardHeader .svgIconFavorite { + color: var(--site-color-svgIcon-favorite); +} + +.showcaseCardSrcBtn { + margin-left: 6px; + padding-left: 12px; + padding-right: 12px; + z-index: 1; flex-grow: 0; flex-shrink: 0; + border: 0; } -.tagIcon { - margin: 0.2rem; - user-select: none; +html[data-theme='dark'] .showcaseCardSrcBtn { + background-color: var(--ifm-color-emphasis-200) !important; + color: inherit; +} + +html[data-theme='dark'] .showcaseCardSrcBtn:hover { + background-color: var(--ifm-color-emphasis-300) !important; +} + +.showcaseCardSrcBtn:focus, +.showcaseCardTitle a:focus { + outline: none; +} + +.showcaseCardSrcBtn:focus-visible { + background-color: var(--ifm-color-secondary-dark); +} + +.showcaseCardBody { + font-size: smaller; + line-height: 1.66; +} + +.cardFooter { + list-style: none; + display: flex; + flex-wrap: wrap; + align-items: center; +} + +.tag { + font-size: 0.675rem; + border: 1px solid var(--ifm-color-secondary-darkest); + white-space: nowrap; + margin-right: 6px; + margin-bottom: 6px !important; + border-radius: 12px; + display: inline-flex; + align-items: center; + justify-content: center; + height: 20px; + cursor: default; + outline: 0px; + text-decoration: none; + z-index: 5; + padding: 0px; + vertical-align: middle; + box-sizing: border-box; + background-color: transparent; +} + +.tag .textLabel { + overflow: hidden; + margin-left: 8px; + white-space: nowrap; +} + +.tag .colorLabel { + width: 7px; + height: 7px; + border-radius: 50%; + margin-left: 6px; + margin-right: 6px; } diff --git a/website/src/components/showcase/ShowcaseCheckbox/index.tsx b/website/src/components/showcase/ShowcaseCheckbox/index.tsx deleted file mode 100644 index 1359931230..0000000000 --- a/website/src/components/showcase/ShowcaseCheckbox/index.tsx +++ /dev/null @@ -1,32 +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, {ComponentProps, ReactNode} from 'react'; -import clsx from 'clsx'; - -import styles from './styles.module.css'; - -interface Props extends ComponentProps<'input'> { - label: ReactNode; -} - -function ShowcaseCheckbox({ - title, - className, - label, - ...props -}: Props): JSX.Element { - const id = `showcase_checkbox_id_${props.name};`; - return ( -
    - - -
    - ); -} - -export default ShowcaseCheckbox; diff --git a/website/src/components/showcase/ShowcaseCheckbox/styles.module.css b/website/src/components/showcase/ShowcaseCheckbox/styles.module.css deleted file mode 100644 index 98d742ff2b..0000000000 --- a/website/src/components/showcase/ShowcaseCheckbox/styles.module.css +++ /dev/null @@ -1,21 +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. - */ - -.checkboxContainer { - display: inline; - padding: 5px; - user-select: none; -} - -.checkboxContainer, .checkboxContainer > * { - cursor: pointer; -} - -.checkboxContainer label { - margin-left: 0.5rem; - text-overflow: ellipsis; -} diff --git a/website/src/components/showcase/ShowcaseFilterToggle/index.tsx b/website/src/components/showcase/ShowcaseFilterToggle/index.tsx new file mode 100644 index 0000000000..a2f7dc9c94 --- /dev/null +++ b/website/src/components/showcase/ShowcaseFilterToggle/index.tsx @@ -0,0 +1,61 @@ +/** + * 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 {useHistory, useLocation} from '@docusaurus/router'; + +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); + searchParams.append(OperatorQueryKey, operator ? 'OR' : 'AND'); + history.push({...location, search: searchParams.toString()}); + }, [operator, location, history]); + + return ( +
    + { + if (e.key === 'Enter') { + toggleOperator(); + } + }} + checked={operator} + /> + {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + +
    + ); +} diff --git a/website/src/components/showcase/ShowcaseFilterToggle/styles.module.css b/website/src/components/showcase/ShowcaseFilterToggle/styles.module.css new file mode 100644 index 0000000000..c8df01a656 --- /dev/null +++ b/website/src/components/showcase/ShowcaseFilterToggle/styles.module.css @@ -0,0 +1,57 @@ +/** + * 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. + */ + +.checkboxLabel { + display: flex; + width: 80px; + height: 25px; + position: relative; + border-radius: 25px; + border: 2px solid var(--ifm-color-primary-darkest); + overflow: hidden; + background-color: transparent; + cursor: pointer; + justify-content: space-around; + align-items: center; + opacity: 0.75; + transition: opacity 200ms ease-out; +} + +input:focus ~ .checkboxLabel, +input:focus-visible ~ .checkboxLabel, +.checkboxLabel:hover { + opacity: 1; + box-shadow: 0px 0px 2px 1px var(--ifm-color-primary-dark); +} + +.checkboxLabel > * { + font-size: 0.8rem; + color: inherit; + transition: opacity 150ms ease-in 50ms; +} + +.checkboxToggle { + position: absolute; + content: ''; + top: -2px; + left: -2px; + width: 40px; + height: 25px; + border-radius: 20px; + background-color: var(--ifm-color-primary-darkest); + box-sizing: border-box; + border: 0.04em solid var(--ifm-color-primary-darkest); + transition-property: transform; + transition-duration: 200ms; + transition-timing-function: ease-out; + transition-delay: 150ms; + transform: translateX(38px); +} + +input:checked ~ .checkboxLabel > .checkboxToggle { + transform: translateX(0); +} diff --git a/website/src/components/showcase/ShowcaseSelect/index.tsx b/website/src/components/showcase/ShowcaseSelect/index.tsx deleted file mode 100644 index c685a21291..0000000000 --- a/website/src/components/showcase/ShowcaseSelect/index.tsx +++ /dev/null @@ -1,28 +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, {ComponentProps} from 'react'; - -import styles from './styles.module.css'; - -interface Props extends ComponentProps<'select'> { - label: string; -} - -function ShowcaseSelect({label, ...props}: Props): JSX.Element { - const id = `showcase_select_id_${props.name};`; - return ( -
    - - -
    - ); -} - -export default ShowcaseSelect; diff --git a/website/src/components/showcase/ShowcaseSelect/styles.module.css b/website/src/components/showcase/ShowcaseSelect/styles.module.css deleted file mode 100644 index 0257a0ef91..0000000000 --- a/website/src/components/showcase/ShowcaseSelect/styles.module.css +++ /dev/null @@ -1,12 +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. - */ - -.selectContainer { - display: inline; - padding: 5px; - user-select: none; -} diff --git a/website/src/components/showcase/ShowcaseTagSelect/index.tsx b/website/src/components/showcase/ShowcaseTagSelect/index.tsx new file mode 100644 index 0000000000..781d0271e5 --- /dev/null +++ b/website/src/components/showcase/ShowcaseTagSelect/index.tsx @@ -0,0 +1,84 @@ +/** + * 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, { + ComponentProps, + ReactNode, + ReactElement, + useCallback, + useState, + useEffect, +} from 'react'; +import {useHistory, useLocation} from '@docusaurus/router'; +import {toggleListItem} from '@site/src/utils/jsUtils'; +import type {TagType} from '@site/src/data/users'; + +import styles from './styles.module.css'; + +interface Props extends ComponentProps<'input'> { + icon: ReactElement>; + label: ReactNode; + tag: TagType; +} + +const TagQueryStringKey = 'tags'; + +export function readSearchTags(search: string): TagType[] { + return new URLSearchParams(search).getAll(TagQueryStringKey) as TagType[]; +} + +function replaceSearchTags(search: string, newTags: TagType[]) { + const searchParams = new URLSearchParams(search); + searchParams.delete(TagQueryStringKey); + newTags.forEach((tag) => searchParams.append(TagQueryStringKey, tag)); + return searchParams.toString(); +} + +const ShowcaseTagSelect = React.forwardRef( + ({id, icon, label, tag, ...rest}, ref) => { + 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}); + }, [tag, location, history]); + return ( + <> + { + if (e.key === 'Enter') { + toggleTag(); + } + }} + onChange={toggleTag} + checked={selected} + {...rest} + /> + + + ); + }, +); + +export default ShowcaseTagSelect; diff --git a/website/src/components/showcase/ShowcaseTagSelect/styles.module.css b/website/src/components/showcase/ShowcaseTagSelect/styles.module.css new file mode 100644 index 0000000000..cde5af7376 --- /dev/null +++ b/website/src/components/showcase/ShowcaseTagSelect/styles.module.css @@ -0,0 +1,46 @@ +/** + * 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. + */ + +input[type='checkbox'] + .checkboxLabel { + display: flex; + align-items: center; + cursor: pointer; + overflow: hidden; + line-height: 1.5; + margin: 0; + border-radius: 4px; + text-overflow: ellipsis; + padding: 0.275rem 0.8rem; + white-space: nowrap; + opacity: 0.85; + transition: opacity 200ms ease-out; + border: 2px solid var(--ifm-color-secondary-darkest); + background-color: inherit; +} + +input:focus + .checkboxLabel, +input:focus-visible + .checkboxLabel, +.checkboxLabel:hover { + opacity: 1; + outline: 0; + box-shadow: 0px 0px 2px 1px var(--ifm-color-secondary-darkest); +} + +input:checked + .checkboxLabel { + opacity: 0.9; + transition: opacity 200ms ease-out; + background-color: var(--site-color-checkbox-checked-bg); + border: 2px solid var(--ifm-color-primary-darkest); +} + +input:checked:focus + .checkboxLabel, +input:checked:focus-visible + .checkboxLabel, +input:checked + .checkboxLabel:hover { + outline: 0; + opacity: 0.75; + box-shadow: 0px 0px 2px 1px var(--ifm-color-primary-dark); +} diff --git a/website/src/components/showcase/ShowcaseTooltip/index.tsx b/website/src/components/showcase/ShowcaseTooltip/index.tsx new file mode 100644 index 0000000000..b41470ac31 --- /dev/null +++ b/website/src/components/showcase/ShowcaseTooltip/index.tsx @@ -0,0 +1,143 @@ +/** + * 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, {useEffect, useState, useRef} from 'react'; +import ReactDOM from 'react-dom'; +import {usePopper} from 'react-popper'; +import styles from './styles.module.css'; + +interface Props { + anchorEl?: HTMLElement | string; + id: string; + text: string; + delay?: number; + children: React.ReactElement; +} + +export default function Tooltip({ + children, + id, + anchorEl, + text, + delay, +}: Props): JSX.Element { + const [open, setOpen] = useState(false); + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + const [arrowElement, setArrowElement] = useState(null); + const [container, setContainer] = useState(null); + const {styles: popperStyles, attributes} = usePopper( + referenceElement, + popperElement, + { + modifiers: [ + { + name: 'arrow', + options: { + element: arrowElement, + }, + }, + { + name: 'offset', + options: { + offset: [0, 8], + }, + }, + ], + }, + ); + + const timeout = useRef(null); + + useEffect(() => { + if (anchorEl) { + if (typeof anchorEl === 'string') { + setContainer(document.querySelector(anchorEl)); + } else { + setContainer(anchorEl); + } + } else { + setContainer(document.body); + } + }, [container, anchorEl]); + + useEffect(() => { + const showEvents = ['mouseenter', 'focus']; + const hideEvents = ['mouseleave', 'blur']; + + const handleOpen = () => { + // There is no point in displaying an empty tooltip. + if (text === '') { + return; + } + + // Remove the title ahead of time to avoid displaying + // two tooltips at the same time (native + this one). + referenceElement.removeAttribute('title'); + + timeout.current = window.setTimeout(() => { + setOpen(true); + }, delay || 400); + }; + + const handleClose = () => { + clearInterval(timeout.current); + setOpen(false); + }; + + if (referenceElement) { + showEvents.forEach((event) => { + referenceElement.addEventListener(event, handleOpen); + }); + + hideEvents.forEach((event) => { + referenceElement.addEventListener(event, handleClose); + }); + } + + return () => { + if (referenceElement) { + showEvents.forEach((event) => { + referenceElement.removeEventListener(event, handleOpen); + }); + + hideEvents.forEach((event) => { + referenceElement.removeEventListener(event, handleClose); + }); + } + }; + }, [referenceElement, text, delay]); + + return ( + <> + {React.cloneElement(children, { + ref: setReferenceElement, + })} + {container + ? ReactDOM.createPortal( + open && ( + + ), + container, + ) + : container} + + ); +} diff --git a/website/src/components/showcase/ShowcaseTooltip/styles.module.css b/website/src/components/showcase/ShowcaseTooltip/styles.module.css new file mode 100644 index 0000000000..3fe242d056 --- /dev/null +++ b/website/src/components/showcase/ShowcaseTooltip/styles.module.css @@ -0,0 +1,46 @@ +/** + * 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. + */ + +.tooltip { + border-radius: 4px; + padding: 4px 8px; + color: var(--site-color-tooltip); + background: var(--site-color-tooltip-background); + font-size: 0.8rem; + z-index: 500; + line-height: 1.4; + font-weight: 500; + max-width: 300px; + opacity: 0.92; + white-space: normal; +} + +.tooltipArrow { + visibility: hidden; +} + +.tooltipArrow, +.tooltipArrow::before { + position: absolute; + width: 8px; + height: 8px; + background: inherit; +} + +.tooltipArrow::before { + visibility: visible; + content: ''; + transform: rotate(45deg); +} + +.tooltip[data-popper-placement^='top'] > .tooltipArrow { + bottom: -4px; +} + +.tooltip[data-popper-placement^='bottom'] > .tooltipArrow { + top: -4px; +} diff --git a/website/src/components/svgIcons/FavoriteIcon/index.tsx b/website/src/components/svgIcons/FavoriteIcon/index.tsx new file mode 100644 index 0000000000..34105f3219 --- /dev/null +++ b/website/src/components/svgIcons/FavoriteIcon/index.tsx @@ -0,0 +1,19 @@ +/** + * 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, {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 cd96243272..9d2236c418 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -28,11 +28,18 @@ 73% ); - --ifm-color-feedback-background: #fff; + --site-color-feedback-background: #fff; + --site-color-favorite-background: #f6fdfd; + --site-color-tooltip: #fff; + --site-color-tooltip-background: #353738; + --site-color-svgIcon-favorite: #e9669e; + --site-color-checkbox-checked-bg: hsl(167deg 56% 73% / 25%); } html[data-theme='dark'] { - --ifm-color-feedback-background: #f0f8ff; + --site-color-feedback-background: #f0f8ff; + --site-color-favorite-background: #1d1e1e; + --site-color-checkbox-checked-bg: hsl(167deg 56% 73% / 10%); } .docusaurus-highlight-code-line { @@ -150,3 +157,17 @@ div[class^='announcementBar_'] { .red > a { color: red; } + +.screen-reader-only { + border: 0; + clip: rect(0 0 0 0); + -webkit-clip-path: polygon(0px 0px, 0px 0px, 0px 0px); + clip-path: polygon(0px 0px, 0px 0px, 0px 0px); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + white-space: nowrap; +} diff --git a/website/src/data/users.tsx b/website/src/data/users.tsx index 4fca8af9e9..1894ee62f3 100644 --- a/website/src/data/users.tsx +++ b/website/src/data/users.tsx @@ -7,8 +7,7 @@ /* eslint-disable global-require */ -import React from 'react'; -import {difference, sortBy} from '../utils/jsUtils'; +import {difference, sortBy} from '@site/src/utils/jsUtils'; /* * ADD YOUR SITE TO THE DOCUSAURUS SHOWCASE: @@ -44,7 +43,7 @@ import {difference, sortBy} from '../utils/jsUtils'; export type Tag = { label: string; description: string; - icon: JSX.Element; + color: string; }; export type TagType = @@ -54,7 +53,6 @@ export type TagType = | 'design' | 'i18n' | 'versioning' - | 'multiInstance' | 'large' | 'facebook' | 'personal' @@ -65,7 +63,7 @@ export type User = { description: string; preview: any; website: string; - source: string; + source: string | null; tags: TagType[]; }; @@ -78,76 +76,69 @@ export const Tags: Record = { label: 'Favorite', description: 'Our favorite Docusaurus sites that you must absolutely check-out!', - icon: <>โค๏ธ, + color: '#e9669e', }, // For open-source sites, a link to the source code is required opensource: { label: 'Open-Source', description: 'Open-Source Docusaurus sites can be useful for inspiration!', - icon: <>๐Ÿ‘จโ€๐Ÿ’ป, + color: '#39ca30', }, product: { label: 'Product', description: 'Docusaurus sites associated to a commercial product!', - icon: <>๐Ÿ’ต, + color: '#dfd545', }, design: { label: 'Design', description: 'Beautiful Docusaurus sites, polished and standing out from the initial template!', - icon: <>๐Ÿ’…, + color: '#a44fb7', }, i18n: { label: 'I18n', description: 'Translated Docusaurus sites using the internationalization support with more than 1 locale.', - icon: <>๐Ÿณ๏ธ, + color: '#127f82', }, versioning: { label: 'Versioning', description: 'Docusaurus sites using the versioning feature of the docs plugin to manage multiple versions.', - icon: <>๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ, - }, - // Sites using multi-instance plugins - multiInstance: { - label: 'Multi-Instance', - description: - 'Docusaurus sites using multiple instances of the same plugin on the same site.', - icon: <>๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ, + color: '#fe6829', }, // Large Docusaurus sites, with a lot of content (> 200 pages, excluding versions) large: { - label: 'Large site', + label: 'Large', description: 'Very large Docusaurus sites, including much more pages than the average!', - icon: <>๐Ÿ’ช, + color: '#8c2f00', }, facebook: { - label: 'Facebook sites', + label: 'Facebook', description: 'Docusaurus sites of Facebook projects', - icon: <>๐Ÿ‘ฅ, + color: '#4267b2', // Facebook blue }, personal: { - label: 'Personal sites', + label: 'Personal', description: 'Personal websites, blogs and digital gardens built with Docusaurus', - icon: <>๐Ÿ™‹, + color: '#14cfc3', }, rtl: { label: 'RTL Direction', description: 'Docusaurus sites using the right-to-left reading direction support.', - icon: <>โ†ช๏ธ, + color: '#ffcfc3', }, }; @@ -1586,7 +1577,7 @@ function sortUsers() { return result; } -export const SortedUsers = sortUsers(); +export const sortedUsers = sortUsers(); // Fail-fast on common errors function ensureUserValid(user: User) { diff --git a/website/src/featureRequests/styles.module.css b/website/src/featureRequests/styles.module.css index f82da5028a..c647730d84 100644 --- a/website/src/featureRequests/styles.module.css +++ b/website/src/featureRequests/styles.module.css @@ -8,6 +8,6 @@ .main { padding: var(--ifm-spacing-horizontal); border-radius: 4px; - background: var(--ifm-color-feedback-background); + background: var(--site-color-feedback-background); min-height: 500px; } diff --git a/website/src/pages/showcase/index.tsx b/website/src/pages/showcase/index.tsx index c0297ff1b1..3bbe3571aa 100644 --- a/website/src/pages/showcase/index.tsx +++ b/website/src/pages/showcase/index.tsx @@ -5,20 +5,26 @@ * LICENSE file in the root directory of this source tree. */ -import React, {useState, useMemo, useCallback, useEffect} from 'react'; +import React, {useState, useMemo, useEffect} from 'react'; import Layout from '@theme/Layout'; -import ShowcaseCheckbox from '@site/src/components/showcase/ShowcaseCheckbox'; -import ShowcaseSelect from '@site/src/components/showcase/ShowcaseSelect'; -import ShowcaseCard from '@site/src/components/showcase/ShowcaseCard'; import clsx from 'clsx'; -import {useHistory, useLocation} from '@docusaurus/router'; +import FavoriteIcon from '@site/src/components/svgIcons/FavoriteIcon'; +import ShowcaseTagSelect, { + readSearchTags, +} from '@site/src/components/showcase/ShowcaseTagSelect'; +import ShowcaseFilterToggle, { + Operator, + readOperator, +} from '@site/src/components/showcase/ShowcaseFilterToggle'; +import ShowcaseCard from '@site/src/components/showcase/ShowcaseCard'; +import {sortedUsers, Tags, TagList, User, TagType} from '@site/src/data/users'; +import Tooltip from '@site/src/components/showcase/ShowcaseTooltip'; -import {toggleListItem} from '../../utils/jsUtils'; -import {SortedUsers, Tags, TagList, User, TagType} from '../../data/users'; +import {useLocation} from '@docusaurus/router'; -type Operator = 'OR' | 'AND'; +import styles from './styles.module.css'; const TITLE = 'Docusaurus Site Showcase'; const DESCRIPTION = 'List of websites people are building with Docusaurus'; @@ -45,34 +51,22 @@ function filterUsers( }); } -function useFilteredUsers( - users: User[], - selectedTags: TagType[], - operator: Operator, -) { +function useFilteredUsers() { + const selectedTags = useSelectedTags(); + const location = useLocation(); + const [operator, setOperator] = useState('OR'); + useEffect(() => { + setOperator(readOperator(location.search)); + }, [location]); return useMemo( - () => filterUsers(users, selectedTags, operator), - [users, selectedTags, operator], + () => filterUsers(sortedUsers, selectedTags, operator), + [selectedTags, operator], ); } -const TagQueryStringKey = 'tags'; - -function readSearchTags(search: string) { - return new URLSearchParams(search).getAll(TagQueryStringKey) as TagType[]; -} - -function replaceSearchTags(search: string, newTags: TagType[]) { - const searchParams = new URLSearchParams(search); - searchParams.delete(TagQueryStringKey); - newTags.forEach((tag) => searchParams.append(TagQueryStringKey, tag)); - return searchParams.toString(); -} - function useSelectedTags() { // The search query-string is the source of truth! const location = useLocation(); - const {push} = useHistory(); // On SSR / first mount (hydration) no tag is selected const [selectedTags, setSelectedTags] = useState([]); @@ -81,136 +75,148 @@ function useSelectedTags() { useEffect(() => { const tags = readSearchTags(location.search); setSelectedTags(tags); - }, [location, setSelectedTags]); + }, [location]); - // Update the QS value - const toggleTag = useCallback( - (tag: TagType) => { - const tags = readSearchTags(location.search); - const newTags = toggleListItem(tags, tag); - const newSearch = replaceSearchTags(location.search, newTags); - push({...location, search: newSearch}); - // no need to call setSelectedTags, useEffect will do the sync - }, - [location, push], - ); - - return {selectedTags, toggleTag}; + return selectedTags; } function ShowcaseHeader() { return ( -
    +

    {TITLE}

    {DESCRIPTION}

    -

    - - ๐Ÿ™ Add your site now! - -

    -
    + + ๐Ÿ™ Add your site + + ); } -interface Props { - selectedTags: TagType[]; - toggleTag: (tag: TagType) => void; - operator: Operator; - setOperator: (op: Operator) => void; -} - -function ShowcaseFilters({ - selectedTags, - toggleTag, - operator, - setOperator, -}: Props) { +function ShowcaseFilters() { + const filteredUsers = useFilteredUsers(); return ( -
    -
    - {TagList.map((tag) => { - const {label, description, icon} = Tags[tag]; +
    +
    + +

    Filter

    +

    {`(${filteredUsers.length} site${ + filteredUsers.length > 1 ? 's' : '' + })`}

    +
    + +
    +
      + {TagList.map((tag, i) => { + const {label, description, color} = Tags[tag]; + const id = `showcase_checkbox_id_${tag};`; + return ( -
      - - {icon} {label} - - ) : ( - label - ) - } - onChange={() => toggleTag(tag)} - checked={selectedTags.includes(tag)} - /> -
      +
    • + + + ) : ( + + ) + } + /> + +
    • ); })} -
      - setOperator(e.target.value as Operator)}> - - - -
      -
    -
    + + ); } -function ShowcaseCards({filteredUsers}: {filteredUsers: User[]}) { +const favoriteUsers = sortedUsers.filter((user) => + user.tags.includes('favorite'), +); +const otherUsers = sortedUsers.filter( + (user) => !user.tags.includes('favorite'), +); + +function ShowcaseCards() { + const selectedTags = useSelectedTags(); + const filteredUsers = useFilteredUsers(); + + if (filteredUsers.length === 0) { + return ( +
    +
    +

    No result

    +
    +
    + ); + } + return ( -
    -

    - {filteredUsers.length} site{filteredUsers.length > 1 ? 's' : ''} -

    -
    - {filteredUsers.length > 0 ? ( -
    +
    + {selectedTags.length === 0 ? ( + <> +
    +
    +
    +

    Our favorites

    + +
    +
      + {favoriteUsers.map((user) => ( + + ))} +
    +
    +
    +
    +

    All sites

    +
      + {otherUsers.map((user) => ( + + ))} +
    +
    + + ) : ( +
    +
      {filteredUsers.map((user) => ( - + ))} -
    - ) : ( -
    -

    No result

    -
    - )} -
    + +
    + )}
    ); } function Showcase(): JSX.Element { - const {selectedTags, toggleTag} = useSelectedTags(); - const [operator, setOperator] = useState('OR'); - const filteredUsers = useFilteredUsers(SortedUsers, selectedTags, operator); return ( -
    +
    - - + +
    ); diff --git a/website/src/pages/showcase/styles.module.css b/website/src/pages/showcase/styles.module.css index b5c0e33b4a..00f2793ef1 100644 --- a/website/src/pages/showcase/styles.module.css +++ b/website/src/pages/showcase/styles.module.css @@ -4,3 +4,95 @@ * 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 > span { + display: flex; + flex: 1 1 auto; +} + +.filterCheckbox > span > * { + margin-bottom: 0; + margin-right: 8px; +} + +.checkboxList { + flex-wrap: wrap; +} + +.checkboxList, +.showcaseList { + padding: 0px; + list-style: none; +} + +.checkboxListItem { + position: relative; + background-color: transparent; + user-select: none; + white-space: nowrap; + display: inline-flex; + align-items: center; + justify-content: center; + height: 32px; + outline: 0px; + text-decoration: none; + padding: 0px; + font-size: 0.8rem; + vertical-align: middle; + box-sizing: border-box; +} + +.checkboxListItem { + margin-top: 0.5rem; + margin-right: 0.5rem; +} + +.checkboxListItem:last-child { + margin-right: 0; +} + +.showcaseList { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 24px; + position: relative; +} + +.showcaseFavorite { + padding-top: 3rem; + padding-bottom: 3rem; + background-color: var(--site-color-favorite-background); +} + +.showcaseFavoriteHeader { + display: flex; + align-items: center; +} + +.showcaseFavoriteHeader > h3 { + margin-bottom: 0; +} + +.svgIconFavoriteXs, +.svgIconFavorite { + color: var(--site-color-svgIcon-favorite); +} + +.svgIconFavoriteXs { + margin-left: 0.625rem; + font-size: 1rem; +} + +.svgIconFavorite { + margin-left: 1rem; +} diff --git a/yarn.lock b/yarn.lock index 370e8ec7f9..5a7a877887 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3592,6 +3592,11 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== +"@popperjs/core@^2.10.2": + version "2.10.2" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590" + integrity sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ== + "@rollup/plugin-babel@^5.2.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz#9cb1c5146ddd6a4968ad96f209c50c62f92f9879" @@ -16833,7 +16838,7 @@ react-error-overlay@^6.0.9: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew== -react-fast-compare@^3.1.1: +react-fast-compare@^3.0.1, react-fast-compare@^3.1.1: version "3.2.0" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== @@ -16893,6 +16898,14 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1: dependencies: "@babel/runtime" "^7.10.3" +react-popper@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.5.tgz#1214ef3cec86330a171671a4fbcbeeb65ee58e96" + integrity sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw== + dependencies: + react-fast-compare "^3.0.1" + warning "^4.0.2" + react-router-config@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988" @@ -20317,6 +20330,13 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.12" +warning@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + watchpack@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.2.0.tgz#47d78f5415fe550ecd740f99fe2882323a58b1ce"