mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-01 10:22:30 +02:00
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:
parent
0374426ce3
commit
3f18c928bb
23 changed files with 1026 additions and 334 deletions
|
@ -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()
|
||||
|
|
|
@ -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",
|
||||
|
|
42
website/src/components/Svg/index.tsx
Normal file
42
website/src/components/Svg/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
54
website/src/components/Svg/styles.module.css
Normal file
54
website/src/components/Svg/styles.module.css
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
84
website/src/components/showcase/ShowcaseTagSelect/index.tsx
Normal file
84
website/src/components/showcase/ShowcaseTagSelect/index.tsx
Normal 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;
|
|
@ -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);
|
||||
}
|
143
website/src/components/showcase/ShowcaseTooltip/index.tsx
Normal file
143
website/src/components/showcase/ShowcaseTooltip/index.tsx
Normal 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}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
19
website/src/components/svgIcons/FavoriteIcon/index.tsx
Normal file
19
website/src/components/svgIcons/FavoriteIcon/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
22
yarn.lock
22
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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue