feat(website): redesign of showcase page (#5742)

* feat: redesign of showcase page

* redesign of showcase card

* improved card design

* create Tooltip component, Svg component

* Add popper.js to dependency

* fixed netlify deploy issues

* fixed netlify deploy issues

* fixed netlify deploy issues

* Make things work

* Relock

* Refactor

Signed-off-by: Josh-Cena <sidachen2003@gmail.com>

* Fix linter errors

* Make animation shorter

* Refactors

* Do not make entire link clickable

* fixed linting and netlify deploy issues

* enhanced styles and fix deploy issues

* Polishing

* improved contrast for selected tags

* Refactors

* Make each component standalone

* Fix operator on first render

* Color coding!

* fix SSR

* More elegant impl

* Do not show source if there is not one

* Fix

* custom on-focus styling for focusable elements with default outlinline && highlight filter toggle on focus.

* fix lint issues

* restore highlight coloring

* Use state instead of ref

Signed-off-by: Josh-Cena <sidachen2003@gmail.com>

* Visual seperator

* Refactors

Signed-off-by: Josh-Cena <sidachen2003@gmail.com>

* Minor fix with dev server

* Paletter improvement

Signed-off-by: Josh-Cena <sidachen2003@gmail.com>

Co-authored-by: Josh-Cena <sidachen2003@gmail.com>
This commit is contained in:
chima ilo 2021-11-18 15:22:26 +01:00 committed by GitHub
parent 0374426ce3
commit 3f18c928bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1026 additions and 334 deletions

View file

@ -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()

View file

@ -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",

View file

@ -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 (
<svg
viewBox={viewBox}
color={colorAttr}
aria-hidden
className={clsx(styles.svgIcon, styles[color], styles[size], svgClass)}
{...rest}>
{children}
</svg>
);
}

View file

@ -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;
}

View file

@ -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 (
<span
className={styles.tagIcon}
// TODO add a proper tooltip
title={`${label}: ${description}`}>
{icon}
</span>
);
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<HTMLLIElement, Props>(
({id, label, color, description}, ref) => (
<li
ref={ref}
aria-describedby={id}
className={styles.tag}
title={description}>
<span className={styles.textLabel}>{label.toLowerCase()}</span>
<span className={styles.colorLabel} style={{backgroundColor: color}} />
</li>
),
);
// 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) => (
<TagIcon key={index} {...tagObject} />
))}
{tagObjectsSorted.map((tagObject, index) => {
const id = `showcase_card_tag_${tagObject.tag}`;
return (
<Tooltip
key={index}
text={tagObject.description}
anchorEl="#__docusaurus"
id={id}>
<TagComp id={id} key={index} {...tagObject} />
</Tooltip>
);
})}
</>
);
}
const ShowcaseCard = memo(({user}: {user: User}) => (
<div key={user.title} className="col col--4 margin-bottom--lg">
<div className={clsx('card', styles.showcaseCard)}>
<div className={clsx('card__image', styles.showcaseCardImage)}>
<Image img={user.preview} alt={user.title} />
</div>
<div className="card__body">
<div className="avatar">
<div className="avatar__intro margin-left--none">
<div className={styles.titleIconsRow}>
<div className={styles.titleIconsRowTitle}>
<div className="avatar__name">{user.title}</div>
</div>
<div className={styles.titleIconsRowIcons}>
<ShowcaseCardTagIcons tags={user.tags} />
</div>
</div>
<small className="avatar__subtitle">{user.description}</small>
</div>
</div>
</div>
{(user.website || user.source) && (
<div className="card__footer">
<div className="button-group button-group--block">
{user.website && (
<a
className="button button--small button--secondary button--block"
href={user.website}
target="_blank"
rel="noreferrer noopener">
Website
</a>
)}
{user.source && (
<a
className="button button--small button--secondary button--block"
href={user.source}
target="_blank"
rel="noreferrer noopener">
Source
</a>
)}
</div>
</div>
)}
<li key={user.title} className="card shadow--md">
<div className={clsx('card__image', styles.showcaseCardImage)}>
<Image img={user.preview} alt={user.title} />
</div>
</div>
<div className="card__body">
<div className={clsx(styles.showcaseCardHeader)}>
<h4 className={styles.showcaseCardTitle}>
<Link
href={user.website}
tabIndex={0}
className={styles.showcaseCardLink}>
{user.title}
</Link>
</h4>
{user.tags.includes('favorite') && (
<FavoriteIcon svgClass={styles.svgIconFavorite} size="small" />
)}
{user.source && (
<Link
href={user.source}
tabIndex={0}
className={clsx(
'button button--secondary button--sm',
styles.showcaseCardSrcBtn,
)}>
source
</Link>
)}
</div>
<p className={styles.showcaseCardBody}>{user.description}</p>
</div>
<ul className={clsx('card__footer', styles.cardFooter)}>
<ShowcaseCardTag tags={user.tags} />
</ul>
</li>
));
export default ShowcaseCard;

View file

@ -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;
}

View file

@ -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 (
<div className={clsx(className, styles.checkboxContainer)} title={title}>
<input type="checkbox" id={id} {...props} />
<label htmlFor={id}>{label}</label>
</div>
);
}
export default ShowcaseCheckbox;

View file

@ -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;
}

View file

@ -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 (
<div className="shadow--md">
<input
type="checkbox"
id={id}
className="screen-reader-only"
aria-label="Toggle between or and and for the tags you selected"
onChange={toggleOperator}
onKeyDown={(e) => {
if (e.key === 'Enter') {
toggleOperator();
}
}}
checked={operator}
/>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label htmlFor={id} className={styles.checkboxLabel}>
<span className={styles.checkboxLabelOr}>OR</span>
<span className={styles.checkboxLabelAnd}>AND</span>
<span className={styles.checkboxToggle} aria-hidden />
</label>
</div>
);
}

View file

@ -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);
}

View file

@ -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 (
<div className={styles.selectContainer}>
<label htmlFor={id}>{label}</label>
<select id={id} {...props}>
{props.children}
</select>
</div>
);
}
export default ShowcaseSelect;

View file

@ -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;
}

View file

@ -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<ComponentProps<'svg'>>;
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<HTMLLabelElement, Props>(
({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 (
<>
<input
type="checkbox"
id={id}
className="screen-reader-only"
onKeyDown={(e) => {
if (e.key === 'Enter') {
toggleTag();
}
}}
onChange={toggleTag}
checked={selected}
{...rest}
/>
<label
ref={ref}
htmlFor={id}
className={styles.checkboxLabel}
aria-describedby={id}>
{label}
{icon}
</label>
</>
);
},
);
export default ShowcaseTagSelect;

View file

@ -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);
}

View file

@ -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<HTMLElement>(null);
const [popperElement, setPopperElement] = useState<HTMLElement>(null);
const [arrowElement, setArrowElement] = useState<HTMLElement>(null);
const [container, setContainer] = useState<Element>(null);
const {styles: popperStyles, attributes} = usePopper(
referenceElement,
popperElement,
{
modifiers: [
{
name: 'arrow',
options: {
element: arrowElement,
},
},
{
name: 'offset',
options: {
offset: [0, 8],
},
},
],
},
);
const timeout = useRef<number>(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 && (
<div
id={id}
role="tooltip"
ref={setPopperElement}
className={styles.tooltip}
style={popperStyles.popper}
{...attributes.popper}>
{text}
<span
ref={setArrowElement}
className={styles.tooltipArrow}
style={popperStyles.arrow}
/>
</div>
),
container,
)
: container}
</>
);
}

View file

@ -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;
}

View file

@ -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<SvgIconProps, 'children'>,
): JSX.Element {
return (
<Svg {...props}>
<path d="M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z" />
</Svg>
);
}

View file

@ -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;
}

View file

@ -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<TagType, Tag> = {
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) {

View file

@ -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;
}

View file

@ -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<Operator>('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<TagType[]>([]);
@ -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 (
<div className="text--center">
<section className="margin-top--lg margin-bottom--xl text--center">
<h1>{TITLE}</h1>
<p>{DESCRIPTION}</p>
<p>
<a
className="button button--primary"
href={EDIT_URL}
target="_blank"
rel="noreferrer">
🙏 Add your site now!
</a>
</p>
</div>
<a
className={clsx('button button--primary', styles.showcaseCardSrcBtn)}
href={EDIT_URL}
target="_blank"
rel="noreferrer">
🙏 Add your site
</a>
</section>
);
}
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 (
<div className="margin-top--l margin-bottom--md container">
<div className="row">
{TagList.map((tag) => {
const {label, description, icon} = Tags[tag];
<section className="container margin-top--xl margin-bottom--lg">
<div className={clsx('margin-bottom--sm', styles.filterCheckbox)}>
<span>
<h3>Filter</h3>
<p>{`(${filteredUsers.length} site${
filteredUsers.length > 1 ? 's' : ''
})`}</p>
</span>
<ShowcaseFilterToggle />
</div>
<ul className={styles.checkboxList}>
{TagList.map((tag, i) => {
const {label, description, color} = Tags[tag];
const id = `showcase_checkbox_id_${tag};`;
return (
<div key={tag} className="col col--2">
<ShowcaseCheckbox
// TODO add a proper tooltip
title={`${label}: ${description}`}
name={tag}
label={
icon ? (
<>
{icon} {label}
</>
) : (
label
)
}
onChange={() => toggleTag(tag)}
checked={selectedTags.includes(tag)}
/>
</div>
<li key={i} className={styles.checkboxListItem}>
<Tooltip id={id} text={description} anchorEl="#__docusaurus">
<ShowcaseTagSelect
tag={tag}
id={id}
label={label}
icon={
tag === 'favorite' ? (
<FavoriteIcon svgClass={styles.svgIconFavoriteXs} />
) : (
<span
style={{
backgroundColor: color,
width: 10,
height: 10,
borderRadius: '50%',
marginLeft: 8,
}}
/>
)
}
/>
</Tooltip>
</li>
);
})}
<div className="col col--2">
<ShowcaseSelect
name="operator"
label="Filter: "
value={operator}
onChange={(e) => setOperator(e.target.value as Operator)}>
<option value="OR">OR</option>
<option value="AND">AND</option>
</ShowcaseSelect>
</div>
</div>
</div>
</ul>
</section>
);
}
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 (
<section className="margin-top--lg margin-bottom--xl">
<div className="container padding-vert--md text--center">
<h3>No result</h3>
</div>
</section>
);
}
return (
<section className="container margin-top--lg">
<h2>
{filteredUsers.length} site{filteredUsers.length > 1 ? 's' : ''}
</h2>
<div className="margin-top--lg">
{filteredUsers.length > 0 ? (
<div className="row">
<section className="margin-top--lg margin-bottom--xl">
{selectedTags.length === 0 ? (
<>
<div className={styles.showcaseFavorite}>
<div className="container">
<div
className={clsx(
'margin-bottom--md',
styles.showcaseFavoriteHeader,
)}>
<h3>Our favorites</h3>
<FavoriteIcon svgClass={styles.svgIconFavorite} />
</div>
<ul className={clsx('container', styles.showcaseList)}>
{favoriteUsers.map((user) => (
<ShowcaseCard key={user.title} user={user} />
))}
</ul>
</div>
</div>
<div className="container margin-top--xl">
<h3 className={styles.showcaseHeader}>All sites</h3>
<ul className={styles.showcaseList}>
{otherUsers.map((user) => (
<ShowcaseCard key={user.title} user={user} />
))}
</ul>
</div>
</>
) : (
<div className="container">
<ul className={styles.showcaseList}>
{filteredUsers.map((user) => (
<ShowcaseCard
key={user.title} // Title should be unique
user={user}
/>
<ShowcaseCard key={user.title} user={user} />
))}
</div>
) : (
<div className={clsx('padding-vert--md text--center')}>
<h3>No result</h3>
</div>
)}
</div>
</ul>
</div>
)}
</section>
);
}
function Showcase(): JSX.Element {
const {selectedTags, toggleTag} = useSelectedTags();
const [operator, setOperator] = useState<Operator>('OR');
const filteredUsers = useFilteredUsers(SortedUsers, selectedTags, operator);
return (
<Layout title={TITLE} description={DESCRIPTION}>
<main className="container margin-vert--lg">
<main className="margin-vert--lg">
<ShowcaseHeader />
<ShowcaseFilters
selectedTags={selectedTags}
toggleTag={toggleTag}
operator={operator}
setOperator={setOperator}
/>
<ShowcaseCards filteredUsers={filteredUsers} />
<ShowcaseFilters />
<ShowcaseCards />
</main>
</Layout>
);

View file

@ -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;
}

View file

@ -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"