-
-
-
-
-
- {(user.website || user.source) && (
-
- )}
+
+
+
-
+
+
+
+
+ {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 && (
+
+ {text}
+
+
+ ),
+ 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 (
-
+
+ ๐ 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 (
+
+ );
+ }
+
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"