mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-10 06:42:31 +02:00
hooks bypass extreme wip
This commit is contained in:
parent
4f64b33e50
commit
0cfdd9d6fd
21 changed files with 644 additions and 992 deletions
|
@ -247,28 +247,18 @@ declare module '@theme/BlogPostItems' {
|
||||||
export default function BlogPostItem(props: Props): JSX.Element;
|
export default function BlogPostItem(props: Props): JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/ShowcaseDetails' {
|
declare module '@theme/Showcase' {
|
||||||
import type {ShowcaseItem} from '@docusaurus/plugin-content-showcase';
|
export default function Showcase(): JSX.Element;
|
||||||
|
|
||||||
export type User = ShowcaseItem;
|
|
||||||
|
|
||||||
export type Props = {
|
|
||||||
content: User;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Showcase(props: Props): JSX.Element;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/Showcase' {
|
declare module '@theme/Showcase/FavoriteIcon' {
|
||||||
import type {ShowcaseItem} from '@docusaurus/plugin-content-showcase';
|
export interface Props {
|
||||||
|
className?: string;
|
||||||
|
style?: ComponentProps<'svg'>['style'];
|
||||||
|
size: 'small' | 'medium' | 'large';
|
||||||
|
}
|
||||||
|
|
||||||
export type User = ShowcaseItem;
|
export default function FavoriteIcon(props: Props): JSX.Element;
|
||||||
|
|
||||||
export type Props = {
|
|
||||||
content: User[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Showcase(props: Props): JSX.Element;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/Showcase/ShowcaseCard' {
|
declare module '@theme/Showcase/ShowcaseCard' {
|
||||||
|
@ -287,13 +277,41 @@ declare module '@theme/Showcase/ShowcaseCard' {
|
||||||
|
|
||||||
export default function ShowcaseCard(props: Props): JSX.Element;
|
export default function ShowcaseCard(props: Props): JSX.Element;
|
||||||
}
|
}
|
||||||
declare module '@theme/Showcase/ShowcaseTagSelect' {
|
|
||||||
import {type ComponentProps, type ReactNode, type ReactElement} from 'react';
|
|
||||||
|
|
||||||
export interface Props extends ComponentProps<'input'> {
|
declare module '@theme/Showcase/ShowcaseCards' {
|
||||||
icon: ReactElement<ComponentProps<'svg'>>;
|
export default function ShowcaseCards(): JSX.Element;
|
||||||
label: ReactNode;
|
}
|
||||||
|
declare module '@theme/Showcase/ShowcaseTooltip' {
|
||||||
|
export interface Props {
|
||||||
|
anchorEl?: HTMLElement | string;
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
children: React.ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ShowcaseTooltip(props: Props): JSX.Element;
|
||||||
|
}
|
||||||
|
declare module '@theme/Showcase/ShowcaseTagSelect' {
|
||||||
|
import {type ComponentProps, type ReactElement} from 'react';
|
||||||
|
|
||||||
|
// TODO use from plugin content showcase
|
||||||
|
type TagType =
|
||||||
|
| 'favorite'
|
||||||
|
| 'opensource'
|
||||||
|
| 'product'
|
||||||
|
| 'design'
|
||||||
|
| 'i18n'
|
||||||
|
| 'versioning'
|
||||||
|
| 'large'
|
||||||
|
| 'meta'
|
||||||
|
| 'personal'
|
||||||
|
| 'rtl';
|
||||||
|
|
||||||
|
interface Props extends ComponentProps<'input'> {
|
||||||
tag: TagType;
|
tag: TagType;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
icon: ReactElement<ComponentProps<'svg'>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ShowcaseTagSelect(props: Props): JSX.Element;
|
export default function ShowcaseTagSelect(props: Props): JSX.Element;
|
||||||
|
@ -304,27 +322,32 @@ declare module '@theme/Showcase/ShowcaseFilterToggle' {
|
||||||
export default function ShowcaseFilterToggle(): JSX.Element;
|
export default function ShowcaseFilterToggle(): JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/Showcase/FavoriteIcon' {
|
declare module '@theme/Showcase/ShowcaseFilters' {
|
||||||
import {type ReactNode, type ComponentProps} from 'react';
|
// TODO use from plugin content showcase
|
||||||
|
export type TagType =
|
||||||
|
| 'favorite'
|
||||||
|
| 'opensource'
|
||||||
|
| 'product'
|
||||||
|
| 'design'
|
||||||
|
| 'i18n'
|
||||||
|
| 'versioning'
|
||||||
|
| 'large'
|
||||||
|
| 'meta'
|
||||||
|
| 'personal'
|
||||||
|
| 'rtl';
|
||||||
|
|
||||||
export type SvgIconProps = ComponentProps<'svg'> & {
|
export default function ShowcaseFilters(): JSX.Element;
|
||||||
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 type Props = Omit<SvgIconProps, 'children'>;
|
declare module '@theme/Showcase/OperatorButton' {
|
||||||
|
export default function OperatorButton(): JSX.Element;
|
||||||
|
}
|
||||||
|
declare module '@theme/Showcase/ClearAllButton' {
|
||||||
|
export default function ClearAllButton(): JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
export default function FavoriteIcon(props: Props): JSX.Element;
|
declare module '@theme/Showcase/ShowcaseSearchBar' {
|
||||||
|
export default function ShowcaseSearchBar(): JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/BlogPostItem/Container' {
|
declare module '@theme/BlogPostItem/Container' {
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {type ReactNode} from 'react';
|
||||||
|
import {useClearQueryString} from '@docusaurus/theme-common';
|
||||||
|
|
||||||
|
export default function ClearAllButton(): ReactNode {
|
||||||
|
const clearQueryString = useClearQueryString();
|
||||||
|
// TODO translate
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="button button--outline button--primary"
|
||||||
|
type="button"
|
||||||
|
onClick={() => clearQueryString()}>
|
||||||
|
Clear All
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
|
@ -7,36 +7,21 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type {Props, SvgIconProps} from '@theme/Showcase/FavoriteIcon';
|
|
||||||
|
import type {Props} from '@theme/Showcase/FavoriteIcon';
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
function Svg(props: SvgIconProps): JSX.Element {
|
export default function FavoriteIcon({
|
||||||
const {
|
size,
|
||||||
svgClass,
|
className,
|
||||||
colorAttr,
|
style,
|
||||||
children,
|
}: Props): React.ReactNode {
|
||||||
color = 'inherit',
|
|
||||||
size = 'medium',
|
|
||||||
viewBox = '0 0 24 24',
|
|
||||||
...rest
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
viewBox={viewBox}
|
viewBox="0 0 24 24"
|
||||||
color={colorAttr}
|
className={clsx(styles.svg, styles[size], className)}
|
||||||
aria-hidden
|
style={style}>
|
||||||
className={clsx(styles.svgIcon, styles[color], styles[size], svgClass)}
|
<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" />
|
||||||
{...rest}>
|
|
||||||
{children}
|
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FavoriteIcon(props: Props): 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,50 +5,23 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.svgIcon {
|
.svg {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
color: #e9669e;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
flex-shrink: 0;
|
|
||||||
color: inherit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* font-size */
|
|
||||||
.small {
|
.small {
|
||||||
font-size: 1.25rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.medium {
|
.medium {
|
||||||
font-size: 1.5rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.large {
|
.large {
|
||||||
font-size: 2.185rem;
|
font-size: 1.8rem;
|
||||||
}
|
|
||||||
|
|
||||||
/* 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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {useId} from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import {useOperator} from '../../_utils';
|
||||||
|
|
||||||
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
export default function OperatorButton(): JSX.Element {
|
||||||
|
const id = useId();
|
||||||
|
const [operator, toggleOperator] = useOperator();
|
||||||
|
// TODO add translations
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
type="checkbox"
|
||||||
|
className="screen-reader-only"
|
||||||
|
aria-label="Toggle between or and and for the tags you selected"
|
||||||
|
checked={operator === 'AND'}
|
||||||
|
onChange={toggleOperator}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
toggleOperator();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label htmlFor={id} className={clsx(styles.checkboxLabel, 'shadow--md')}>
|
||||||
|
{/* eslint-disable @docusaurus/no-untranslated-text */}
|
||||||
|
<span className={styles.checkboxLabelOr}>OR</span>
|
||||||
|
<span className={styles.checkboxLabelAnd}>AND</span>
|
||||||
|
{/* eslint-enable @docusaurus/no-untranslated-text */}
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -8,159 +8,31 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import Link from '@docusaurus/Link';
|
import Link from '@docusaurus/Link';
|
||||||
import Translate, {translate} from '@docusaurus/Translate';
|
import Translate from '@docusaurus/Translate';
|
||||||
import FavoriteIcon from '@theme/Showcase/FavoriteIcon';
|
// import Image from '@theme/IdealImage';
|
||||||
|
import {Tags, TagList, type TagType} from '@site/src/data/users';
|
||||||
|
import {type User} from '@theme/Showcase/ShowcaseCard';
|
||||||
|
import {sortBy} from '@site/src/utils/jsUtils';
|
||||||
import Heading from '@theme/Heading';
|
import Heading from '@theme/Heading';
|
||||||
|
import FavoriteIcon from '@theme/Showcase/FavoriteIcon';
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
const Tags: {[type in TagType]: Tag} = {
|
function TagItem({
|
||||||
favorite: {
|
label,
|
||||||
label: translate({message: 'Favorite'}),
|
description,
|
||||||
description: translate({
|
color,
|
||||||
message:
|
}: {
|
||||||
'Our favorite Docusaurus sites that you must absolutely check out!',
|
|
||||||
id: 'showcase.tag.favorite.description',
|
|
||||||
}),
|
|
||||||
color: '#e9669e',
|
|
||||||
},
|
|
||||||
|
|
||||||
opensource: {
|
|
||||||
label: translate({message: 'Open-Source'}),
|
|
||||||
description: translate({
|
|
||||||
message: 'Open-Source Docusaurus sites can be useful for inspiration!',
|
|
||||||
id: 'showcase.tag.opensource.description',
|
|
||||||
}),
|
|
||||||
color: '#39ca30',
|
|
||||||
},
|
|
||||||
|
|
||||||
product: {
|
|
||||||
label: translate({message: 'Product'}),
|
|
||||||
description: translate({
|
|
||||||
message: 'Docusaurus sites associated to a commercial product!',
|
|
||||||
id: 'showcase.tag.product.description',
|
|
||||||
}),
|
|
||||||
color: '#dfd545',
|
|
||||||
},
|
|
||||||
|
|
||||||
design: {
|
|
||||||
label: translate({message: 'Design'}),
|
|
||||||
description: translate({
|
|
||||||
message:
|
|
||||||
'Beautiful Docusaurus sites, polished and standing out from the initial template!',
|
|
||||||
id: 'showcase.tag.design.description',
|
|
||||||
}),
|
|
||||||
color: '#a44fb7',
|
|
||||||
},
|
|
||||||
|
|
||||||
i18n: {
|
|
||||||
label: translate({message: 'I18n'}),
|
|
||||||
description: translate({
|
|
||||||
message:
|
|
||||||
'Translated Docusaurus sites using the internationalization support with more than 1 locale.',
|
|
||||||
id: 'showcase.tag.i18n.description',
|
|
||||||
}),
|
|
||||||
color: '#127f82',
|
|
||||||
},
|
|
||||||
|
|
||||||
versioning: {
|
|
||||||
label: translate({message: 'Versioning'}),
|
|
||||||
description: translate({
|
|
||||||
message:
|
|
||||||
'Docusaurus sites using the versioning feature of the docs plugin to manage multiple versions.',
|
|
||||||
id: 'showcase.tag.versioning.description',
|
|
||||||
}),
|
|
||||||
color: '#fe6829',
|
|
||||||
},
|
|
||||||
|
|
||||||
large: {
|
|
||||||
label: translate({message: 'Large'}),
|
|
||||||
description: translate({
|
|
||||||
message:
|
|
||||||
'Very large Docusaurus sites, including many more pages than the average!',
|
|
||||||
id: 'showcase.tag.large.description',
|
|
||||||
}),
|
|
||||||
color: '#8c2f00',
|
|
||||||
},
|
|
||||||
|
|
||||||
meta: {
|
|
||||||
label: translate({message: 'Meta'}),
|
|
||||||
description: translate({
|
|
||||||
message: 'Docusaurus sites of Meta (formerly Facebook) projects',
|
|
||||||
id: 'showcase.tag.meta.description',
|
|
||||||
}),
|
|
||||||
color: '#4267b2', // Facebook blue
|
|
||||||
},
|
|
||||||
|
|
||||||
personal: {
|
|
||||||
label: translate({message: 'Personal'}),
|
|
||||||
description: translate({
|
|
||||||
message:
|
|
||||||
'Personal websites, blogs and digital gardens built with Docusaurus',
|
|
||||||
id: 'showcase.tag.personal.description',
|
|
||||||
}),
|
|
||||||
color: '#14cfc3',
|
|
||||||
},
|
|
||||||
|
|
||||||
rtl: {
|
|
||||||
label: translate({message: 'RTL Direction'}),
|
|
||||||
description: translate({
|
|
||||||
message:
|
|
||||||
'Docusaurus sites using the right-to-left reading direction support.',
|
|
||||||
id: 'showcase.tag.rtl.description',
|
|
||||||
}),
|
|
||||||
color: '#ffcfc3',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const TagList = Object.keys(Tags) as TagType[];
|
|
||||||
|
|
||||||
type TagType =
|
|
||||||
| 'favorite'
|
|
||||||
| 'opensource'
|
|
||||||
| 'product'
|
|
||||||
| 'design'
|
|
||||||
| 'i18n'
|
|
||||||
| 'versioning'
|
|
||||||
| 'large'
|
|
||||||
| 'meta'
|
|
||||||
| 'personal'
|
|
||||||
| 'rtl';
|
|
||||||
|
|
||||||
type User = {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
preview: string | null; // null = use our serverless screenshot service
|
|
||||||
website: string;
|
|
||||||
source: string | null;
|
|
||||||
tags: TagType[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type Tag = {
|
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
color: string;
|
color: string;
|
||||||
};
|
}) {
|
||||||
|
return (
|
||||||
function sortBy<T>(
|
<li className={styles.tag} title={description}>
|
||||||
array: T[],
|
|
||||||
getter: (item: T) => string | number | boolean,
|
|
||||||
): T[] {
|
|
||||||
const sortedArray = [...array];
|
|
||||||
sortedArray.sort((a, b) =>
|
|
||||||
// eslint-disable-next-line no-nested-ternary
|
|
||||||
getter(a) > getter(b) ? 1 : getter(b) > getter(a) ? -1 : 0,
|
|
||||||
);
|
|
||||||
return sortedArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TagComp = React.forwardRef<HTMLLIElement, Tag>(
|
|
||||||
({label, color, description}, ref) => (
|
|
||||||
<li ref={ref} className={styles.tag} title={description}>
|
|
||||||
<span className={styles.textLabel}>{label.toLowerCase()}</span>
|
<span className={styles.textLabel}>{label.toLowerCase()}</span>
|
||||||
<span className={styles.colorLabel} style={{backgroundColor: color}} />
|
<span className={styles.colorLabel} style={{backgroundColor: color}} />
|
||||||
</li>
|
</li>
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
function ShowcaseCardTag({tags}: {tags: TagType[]}) {
|
function ShowcaseCardTag({tags}: {tags: TagType[]}) {
|
||||||
const tagObjects = tags.map((tag) => ({tag, ...Tags[tag]}));
|
const tagObjects = tags.map((tag) => ({tag, ...Tags[tag]}));
|
||||||
|
@ -173,7 +45,7 @@ function ShowcaseCardTag({tags}: {tags: TagType[]}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{tagObjectsSorted.map((tagObject, index) => {
|
{tagObjectsSorted.map((tagObject, index) => {
|
||||||
return <TagComp key={index} {...tagObject} />;
|
return <TagItem key={index} {...tagObject} />;
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -182,6 +54,7 @@ function ShowcaseCardTag({tags}: {tags: TagType[]}) {
|
||||||
function getCardImage(user: User): string {
|
function getCardImage(user: User): string {
|
||||||
return (
|
return (
|
||||||
user.preview ??
|
user.preview ??
|
||||||
|
// TODO make it configurable
|
||||||
`https://slorber-api-screenshot.netlify.app/${encodeURIComponent(
|
`https://slorber-api-screenshot.netlify.app/${encodeURIComponent(
|
||||||
user.website,
|
user.website,
|
||||||
)}/showcase`
|
)}/showcase`
|
||||||
|
@ -193,6 +66,7 @@ function ShowcaseCard({user}: {user: User}) {
|
||||||
return (
|
return (
|
||||||
<li key={user.title} className="card shadow--md">
|
<li key={user.title} className="card shadow--md">
|
||||||
<div className={clsx('card__image', styles.showcaseCardImage)}>
|
<div className={clsx('card__image', styles.showcaseCardImage)}>
|
||||||
|
{/* TODO change back to ideal image */}
|
||||||
<img src={image} alt={user.title} />
|
<img src={image} alt={user.title} />
|
||||||
</div>
|
</div>
|
||||||
<div className="card__body">
|
<div className="card__body">
|
||||||
|
@ -203,7 +77,7 @@ function ShowcaseCard({user}: {user: User}) {
|
||||||
</Link>
|
</Link>
|
||||||
</Heading>
|
</Heading>
|
||||||
{user.tags.includes('favorite') && (
|
{user.tags.includes('favorite') && (
|
||||||
<FavoriteIcon svgClass={styles.svgIconFavorite} size="small" />
|
<FavoriteIcon size="medium" style={{marginRight: '0.25rem'}} />
|
||||||
)}
|
)}
|
||||||
{user.source && (
|
{user.source && (
|
||||||
<Link
|
<Link
|
||||||
|
|
|
@ -37,14 +37,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.showcaseCardTitle,
|
.showcaseCardTitle,
|
||||||
.showcaseCardHeader .svgIconFavorite {
|
.showcaseCardHeader {
|
||||||
margin-right: 0.25rem;
|
margin-right: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.showcaseCardHeader .svgIconFavorite {
|
|
||||||
color: var(--site-color-svg-icon-favorite);
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcaseCardSrcBtn {
|
.showcaseCardSrcBtn {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {ReactNode} from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import Translate from '@docusaurus/Translate';
|
||||||
|
import {sortedUsers} from '@site/src/data/users';
|
||||||
|
import Heading from '@theme/Heading';
|
||||||
|
import FavoriteIcon from '@theme/Showcase/FavoriteIcon';
|
||||||
|
import ShowcaseCard from '@theme/Showcase/ShowcaseCard';
|
||||||
|
import {type User} from '@theme/Showcase/ShowcaseCard';
|
||||||
|
import {useFilteredUsers} from '../../_utils';
|
||||||
|
|
||||||
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
const favoriteUsers = sortedUsers.filter((user) =>
|
||||||
|
user.tags.includes('favorite'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const otherUsers = sortedUsers.filter(
|
||||||
|
(user) => !user.tags.includes('favorite'),
|
||||||
|
);
|
||||||
|
|
||||||
|
function HeadingNoResult() {
|
||||||
|
return (
|
||||||
|
<Heading as="h2">
|
||||||
|
<Translate id="showcase.usersList.noResult">No result</Translate>
|
||||||
|
</Heading>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HeadingFavorites() {
|
||||||
|
return (
|
||||||
|
<Heading as="h2" className={styles.headingFavorites}>
|
||||||
|
<Translate id="showcase.favoritesList.title">Our favorites</Translate>
|
||||||
|
<FavoriteIcon size="large" style={{marginLeft: '1rem'}} />
|
||||||
|
</Heading>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HeadingAllSites() {
|
||||||
|
return (
|
||||||
|
<Heading as="h2">
|
||||||
|
<Translate id="showcase.usersList.allUsers">All sites</Translate>
|
||||||
|
</Heading>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardList({heading, items}: {heading?: ReactNode; items: User[]}) {
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
{heading}
|
||||||
|
<ul className={clsx('clean-list', styles.cardList)}>
|
||||||
|
{items.map((item) => (
|
||||||
|
<ShowcaseCard key={item.title} user={item} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NoResultSection() {
|
||||||
|
return (
|
||||||
|
<section className="margin-top--lg margin-bottom--xl">
|
||||||
|
<div className="container padding-vert--md text--center">
|
||||||
|
<HeadingNoResult />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ShowcaseCards(): JSX.Element {
|
||||||
|
const filteredUsers = useFilteredUsers();
|
||||||
|
|
||||||
|
if (filteredUsers.length === 0) {
|
||||||
|
return <NoResultSection />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="margin-top--lg margin-bottom--xl">
|
||||||
|
{filteredUsers.length === sortedUsers.length ? (
|
||||||
|
<>
|
||||||
|
<div className={styles.showcaseFavorite}>
|
||||||
|
<CardList heading={<HeadingFavorites />} items={favoriteUsers} />
|
||||||
|
</div>
|
||||||
|
<div className="margin-top--lg">
|
||||||
|
<CardList heading={<HeadingAllSites />} items={otherUsers} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<CardList items={filteredUsers} />
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.cardList {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcaseFavorite {
|
||||||
|
padding-top: 2rem;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
background-color: #f6fdfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] .showcaseFavorite {
|
||||||
|
background-color: #232525;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headingFavorites {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
|
@ -1,101 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, {useState, useEffect, useCallback} from 'react';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import {useHistory, useLocation} from '@docusaurus/router';
|
|
||||||
|
|
||||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
|
||||||
import type {Operator} from '@theme/Showcase/ShowcaseFilterToggle';
|
|
||||||
|
|
||||||
import styles from './styles.module.css';
|
|
||||||
|
|
||||||
const OperatorQueryKey = 'operator';
|
|
||||||
|
|
||||||
type UserState = {
|
|
||||||
scrollTopPosition: number;
|
|
||||||
focusedElementId: string | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
function prepareUserState(): UserState | undefined {
|
|
||||||
if (ExecutionEnvironment.canUseDOM) {
|
|
||||||
return {
|
|
||||||
scrollTopPosition: window.scrollY,
|
|
||||||
focusedElementId: document.activeElement?.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readOperator(search: string): Operator {
|
|
||||||
return (new URLSearchParams(search).get(OperatorQueryKey) ??
|
|
||||||
'OR') as Operator;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ShowcaseFilterToggle(): JSX.Element {
|
|
||||||
const id = 'showcase_filter_toggle';
|
|
||||||
const location = useLocation();
|
|
||||||
const history = useHistory();
|
|
||||||
const [operator, setOperator] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
setOperator(readOperator(location.search) === 'AND');
|
|
||||||
}, [location]);
|
|
||||||
const toggleOperator = useCallback(() => {
|
|
||||||
setOperator((o) => !o);
|
|
||||||
const searchParams = new URLSearchParams(location.search);
|
|
||||||
searchParams.delete(OperatorQueryKey);
|
|
||||||
if (!operator) {
|
|
||||||
searchParams.append(OperatorQueryKey, 'AND');
|
|
||||||
}
|
|
||||||
history.push({
|
|
||||||
...location,
|
|
||||||
search: searchParams.toString(),
|
|
||||||
state: prepareUserState(),
|
|
||||||
});
|
|
||||||
}, [operator, location, history]);
|
|
||||||
|
|
||||||
const ClearTag = () => {
|
|
||||||
history.push({
|
|
||||||
...location,
|
|
||||||
search: '',
|
|
||||||
state: prepareUserState(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="row" style={{alignItems: 'center'}}>
|
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
<label htmlFor={id} className={clsx(styles.checkboxLabel, 'shadow--md')}>
|
|
||||||
{/* eslint-disable @docusaurus/no-untranslated-text */}
|
|
||||||
<span className={styles.checkboxLabelOr}>OR</span>
|
|
||||||
<span className={styles.checkboxLabelAnd}>AND</span>
|
|
||||||
{/* eslint-enable @docusaurus/no-untranslated-text */}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{/* eslint-disable-next-line @docusaurus/no-untranslated-text */}
|
|
||||||
<button
|
|
||||||
className="button button--outline button--primary"
|
|
||||||
type="button"
|
|
||||||
onClick={() => ClearTag()}>
|
|
||||||
Clear All
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {ReactNode, CSSProperties} from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import Translate from '@docusaurus/Translate';
|
||||||
|
import FavoriteIcon from '@theme/Showcase/FavoriteIcon';
|
||||||
|
import {Tags, TagList} from '@site/src/data/users';
|
||||||
|
import Heading from '@theme/Heading';
|
||||||
|
import ShowcaseTagSelect from '@theme/Showcase/ShowcaseTagSelect';
|
||||||
|
import {type TagType} from '@theme/Showcase/ShowcaseFilters';
|
||||||
|
import OperatorButton from '@theme/Showcase/OperatorButton';
|
||||||
|
import ClearAllButton from '@theme/Showcase/ClearAllButton';
|
||||||
|
import {useFilteredUsers, useSiteCountPlural} from '../../_utils';
|
||||||
|
|
||||||
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
function TagCircleIcon({color, style}: {color: string; style?: CSSProperties}) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
borderRadius: '50%',
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShowcaseTagListItem({tag}: {tag: TagType}) {
|
||||||
|
const {label, description, color} = Tags[tag];
|
||||||
|
return (
|
||||||
|
<li className={styles.tagListItem}>
|
||||||
|
<ShowcaseTagSelect
|
||||||
|
tag={tag}
|
||||||
|
label={label}
|
||||||
|
description={description}
|
||||||
|
icon={
|
||||||
|
tag === 'favorite' ? (
|
||||||
|
<FavoriteIcon size="small" style={{marginLeft: 8}} />
|
||||||
|
) : (
|
||||||
|
<TagCircleIcon
|
||||||
|
color={color}
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
marginLeft: 8,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShowcaseTagList() {
|
||||||
|
return (
|
||||||
|
<ul className={clsx('clean-list', styles.tagList)}>
|
||||||
|
{TagList.map((tag) => {
|
||||||
|
return <ShowcaseTagListItem key={tag} tag={tag} />;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HeadingText() {
|
||||||
|
const filteredUsers = useFilteredUsers();
|
||||||
|
const siteCountPlural = useSiteCountPlural();
|
||||||
|
return (
|
||||||
|
<div className={styles.headingText}>
|
||||||
|
<Heading as="h2">
|
||||||
|
<Translate id="showcase.filters.title">Filters</Translate>
|
||||||
|
</Heading>
|
||||||
|
<span>{siteCountPlural(filteredUsers.length)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HeadingButtons() {
|
||||||
|
return (
|
||||||
|
<div className={styles.headingButtons} style={{alignItems: 'center'}}>
|
||||||
|
<OperatorButton />
|
||||||
|
<ClearAllButton />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HeadingRow() {
|
||||||
|
return (
|
||||||
|
<div className={clsx('margin-bottom--sm', styles.headingRow)}>
|
||||||
|
<HeadingText />
|
||||||
|
<HeadingButtons />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ShowcaseFilters(): ReactNode {
|
||||||
|
return (
|
||||||
|
<section className="container margin-top--l margin-bottom--lg">
|
||||||
|
<HeadingRow />
|
||||||
|
<ShowcaseTagList />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.headingRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headingText {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headingText > h2 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headingText > span {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headingButtons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headingButtons > * {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagList {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagListItem {
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagListItem:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {type ReactNode} from 'react';
|
||||||
|
import {translate} from '@docusaurus/Translate';
|
||||||
|
import {useSearchName} from '@site/src/pages/showcase/_utils';
|
||||||
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
export default function ShowcaseSearchBar(): ReactNode {
|
||||||
|
const [searchName, setSearchName] = useSearchName();
|
||||||
|
return (
|
||||||
|
<div className={styles.searchBar}>
|
||||||
|
<input
|
||||||
|
placeholder={translate({
|
||||||
|
message: 'Search for site name...',
|
||||||
|
id: 'showcase.searchBar.placeholder',
|
||||||
|
})}
|
||||||
|
value={searchName}
|
||||||
|
onInput={(e) => {
|
||||||
|
setSearchName(e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.searchBar {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchBar input {
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid gray;
|
||||||
|
}
|
|
@ -5,127 +5,54 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {
|
import React, {useCallback, type ReactNode, useId} from 'react';
|
||||||
useCallback,
|
import type {Props} from '@theme/Showcase/ShowcaseTagSelect';
|
||||||
useState,
|
import {useTags} from '../../_utils';
|
||||||
useEffect,
|
|
||||||
type ComponentProps,
|
|
||||||
type ReactNode,
|
|
||||||
type ReactElement,
|
|
||||||
} from 'react';
|
|
||||||
import {useHistory, useLocation} from '@docusaurus/router';
|
|
||||||
|
|
||||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
type UserState = {
|
function useTagState(tag: string) {
|
||||||
scrollTopPosition: number;
|
const [tags, setTags] = useTags();
|
||||||
focusedElementId: string | undefined;
|
const isSelected = tags.includes(tag);
|
||||||
};
|
const toggle = useCallback(() => {
|
||||||
|
setTags((list) => {
|
||||||
function prepareUserState(): UserState | undefined {
|
return list.includes(tag)
|
||||||
if (ExecutionEnvironment.canUseDOM) {
|
? list.filter((t) => t !== tag)
|
||||||
return {
|
: [...list, tag];
|
||||||
scrollTopPosition: window.scrollY,
|
|
||||||
focusedElementId: document.activeElement?.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleListItem<T>(list: T[], item: T): T[] {
|
|
||||||
const itemIndex = list.indexOf(item);
|
|
||||||
if (itemIndex === -1) {
|
|
||||||
return list.concat(item);
|
|
||||||
}
|
|
||||||
const newList = [...list];
|
|
||||||
newList.splice(itemIndex, 1);
|
|
||||||
return newList;
|
|
||||||
}
|
|
||||||
|
|
||||||
type TagType =
|
|
||||||
| 'favorite'
|
|
||||||
| 'opensource'
|
|
||||||
| 'product'
|
|
||||||
| 'design'
|
|
||||||
| 'i18n'
|
|
||||||
| 'versioning'
|
|
||||||
| 'large'
|
|
||||||
| 'meta'
|
|
||||||
| 'personal'
|
|
||||||
| 'rtl';
|
|
||||||
interface Props extends ComponentProps<'input'> {
|
|
||||||
icon: ReactElement<ComponentProps<'svg'>>;
|
|
||||||
label: ReactNode;
|
|
||||||
tag: TagType;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TagQueryStringKey = 'tags';
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
function ShowcaseTagSelect(
|
|
||||||
{id, icon, label, tag, ...rest}: Props,
|
|
||||||
ref: React.ForwardedRef<HTMLLabelElement>,
|
|
||||||
) {
|
|
||||||
const location = useLocation();
|
|
||||||
const history = useHistory();
|
|
||||||
const [selected, setSelected] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
const tags = readSearchTags(location.search);
|
|
||||||
setSelected(tags.includes(tag));
|
|
||||||
}, [tag, location]);
|
|
||||||
const toggleTag = useCallback(() => {
|
|
||||||
const tags = readSearchTags(location.search);
|
|
||||||
const newTags = toggleListItem(tags, tag);
|
|
||||||
const newSearch = replaceSearchTags(location.search, newTags);
|
|
||||||
history.push({
|
|
||||||
...location,
|
|
||||||
search: newSearch,
|
|
||||||
state: prepareUserState(),
|
|
||||||
});
|
});
|
||||||
}, [tag, location, history]);
|
}, [tag, setTags]);
|
||||||
|
|
||||||
|
return [isSelected, toggle] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ShowcaseTagSelect({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
tag,
|
||||||
|
...rest
|
||||||
|
}: Props): ReactNode {
|
||||||
|
const id = useId();
|
||||||
|
const [isSelected, toggle] = useTagState(tag);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id={id}
|
id={id}
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={toggle}
|
||||||
className="screen-reader-only"
|
className="screen-reader-only"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
toggleTag();
|
toggle();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onFocus={(e) => {
|
|
||||||
if (e.relatedTarget) {
|
|
||||||
e.target.nextElementSibling?.dispatchEvent(
|
|
||||||
new KeyboardEvent('focus'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={(e) => {
|
|
||||||
e.target.nextElementSibling?.dispatchEvent(new KeyboardEvent('blur'));
|
|
||||||
}}
|
|
||||||
onChange={toggleTag}
|
|
||||||
checked={selected}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
<label ref={ref} htmlFor={id} className={styles.checkboxLabel}>
|
<label htmlFor={id} className={styles.checkboxLabel} title={description}>
|
||||||
{label}
|
{label}
|
||||||
{icon}
|
{icon}
|
||||||
</label>
|
</label>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default React.forwardRef(ShowcaseTagSelect);
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ input:focus-visible + .checkboxLabel {
|
||||||
|
|
||||||
input:checked + .checkboxLabel {
|
input:checked + .checkboxLabel {
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
background-color: var(--site-color-checkbox-checked-bg);
|
background-color: hsl(167deg 56% 73% / 25%);
|
||||||
border: 2px solid var(--ifm-color-primary-darkest);
|
border: 2px solid var(--ifm-color-primary-darkest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,3 +36,7 @@ input:checked + .checkboxLabel:hover {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
box-shadow: 0 0 2px 1px var(--ifm-color-primary-dark);
|
box-shadow: 0 0 2px 1px var(--ifm-color-primary-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] input:checked + .checkboxLabel {
|
||||||
|
background-color: hsl(167deg 56% 73% / 10%);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
/**
|
||||||
|
* 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 {useCallback, useMemo} from 'react';
|
||||||
|
import {translate} from '@docusaurus/Translate';
|
||||||
|
import {
|
||||||
|
usePluralForm,
|
||||||
|
useQueryString,
|
||||||
|
useQueryStringList,
|
||||||
|
} from '@docusaurus/theme-common';
|
||||||
|
import type {TagType, User} from '@site/src/data/users';
|
||||||
|
import {sortedUsers} from '@site/src/data/users';
|
||||||
|
|
||||||
|
export function useSearchName() {
|
||||||
|
return useQueryString('name');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTags() {
|
||||||
|
return useQueryStringList('tags');
|
||||||
|
}
|
||||||
|
|
||||||
|
type Operator = 'OR' | 'AND';
|
||||||
|
|
||||||
|
export function useOperator() {
|
||||||
|
const [searchOperator, setSearchOperator] = useQueryString('operator');
|
||||||
|
const operator: Operator = searchOperator === 'AND' ? 'AND' : 'OR';
|
||||||
|
const toggleOperator = useCallback(() => {
|
||||||
|
const newOperator = operator === 'OR' ? 'AND' : null;
|
||||||
|
setSearchOperator(newOperator);
|
||||||
|
}, [operator, setSearchOperator]);
|
||||||
|
return [operator, toggleOperator] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterUsers({
|
||||||
|
users,
|
||||||
|
tags,
|
||||||
|
operator,
|
||||||
|
searchName,
|
||||||
|
}: {
|
||||||
|
users: User[];
|
||||||
|
tags: TagType[];
|
||||||
|
operator: Operator;
|
||||||
|
searchName: string | null;
|
||||||
|
}) {
|
||||||
|
if (searchName) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
users = users.filter((user) =>
|
||||||
|
user.title.toLowerCase().includes(searchName.toLowerCase()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (tags.length === 0) {
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
return users.filter((user) => {
|
||||||
|
if (user.tags.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (operator === 'AND') {
|
||||||
|
return tags.every((tag) => user.tags.includes(tag));
|
||||||
|
}
|
||||||
|
return tags.some((tag) => user.tags.includes(tag));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFilteredUsers() {
|
||||||
|
const [tags] = useTags();
|
||||||
|
const [searchName] = useSearchName();
|
||||||
|
const [operator] = useOperator();
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
filterUsers({
|
||||||
|
users: sortedUsers,
|
||||||
|
tags: tags as TagType[],
|
||||||
|
operator,
|
||||||
|
searchName,
|
||||||
|
}),
|
||||||
|
[tags, operator, searchName],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSiteCountPlural() {
|
||||||
|
const {selectMessage} = usePluralForm();
|
||||||
|
return (sitesCount: number) =>
|
||||||
|
selectMessage(
|
||||||
|
sitesCount,
|
||||||
|
translate(
|
||||||
|
{
|
||||||
|
id: 'showcase.filters.resultCount',
|
||||||
|
description:
|
||||||
|
'Pluralized label for the number of sites found on the showcase. Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',
|
||||||
|
message: '1 site|{sitesCount} sites',
|
||||||
|
},
|
||||||
|
{sitesCount},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -5,26 +5,16 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useEffect, useMemo, useState} from 'react';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import Link from '@docusaurus/Link';
|
|
||||||
import {useHistory, useLocation} from 'react-router-dom';
|
|
||||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
|
||||||
import Translate, {translate} from '@docusaurus/Translate';
|
import Translate, {translate} from '@docusaurus/Translate';
|
||||||
import {usePluralForm} from '@docusaurus/theme-common';
|
|
||||||
|
import Link from '@docusaurus/Link';
|
||||||
import {clientShowcase} from '@docusaurus/plugin-content-showcase/client';
|
import {clientShowcase} from '@docusaurus/plugin-content-showcase/client';
|
||||||
import type {User, Props} from '@theme/Showcase';
|
|
||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
import Heading from '@theme/Heading';
|
import Heading from '@theme/Heading';
|
||||||
import FavoriteIcon from '@theme/Showcase/FavoriteIcon';
|
|
||||||
import ShowcaseCard from '@theme/Showcase/ShowcaseCard';
|
|
||||||
import ShowcaseTagSelect from '@theme/Showcase/ShowcaseTagSelect';
|
|
||||||
import ShowcaseFilterToggle from '@theme/Showcase/ShowcaseFilterToggle';
|
|
||||||
import type {Operator} from '@theme/Showcase/ShowcaseFilterToggle';
|
|
||||||
import type {TagType} from '@docusaurus/plugin-content-showcase';
|
|
||||||
import styles from './styles.module.css';
|
|
||||||
|
|
||||||
type Users = User[];
|
import ShowcaseSearchBar from '@theme/Showcase/ShowcaseSearchBar';
|
||||||
|
import ShowcaseCards from '@theme/Showcase/ShowcaseCards';
|
||||||
|
import ShowcaseFilters from '@theme/Showcase/ShowcaseFilters';
|
||||||
|
|
||||||
const TITLE = translate({message: 'Docusaurus Site Showcase'});
|
const TITLE = translate({message: 'Docusaurus Site Showcase'});
|
||||||
const DESCRIPTION = translate({
|
const DESCRIPTION = translate({
|
||||||
|
@ -32,144 +22,6 @@ const DESCRIPTION = translate({
|
||||||
});
|
});
|
||||||
const SUBMIT_URL = 'https://github.com/facebook/docusaurus/discussions/7826';
|
const SUBMIT_URL = 'https://github.com/facebook/docusaurus/discussions/7826';
|
||||||
|
|
||||||
const OperatorQueryKey = 'operator';
|
|
||||||
|
|
||||||
function readOperator(search: string): Operator {
|
|
||||||
return (new URLSearchParams(search).get(OperatorQueryKey) ??
|
|
||||||
'OR') as Operator;
|
|
||||||
}
|
|
||||||
type UserState = {
|
|
||||||
scrollTopPosition: number;
|
|
||||||
focusedElementId: string | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Tag = {
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
color: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Tags: {[type in TagType]: Tag} = {
|
|
||||||
favorite: {
|
|
||||||
label: translate({message: 'Favorite'}),
|
|
||||||
description: translate({
|
|
||||||
message:
|
|
||||||
'Our favorite Docusaurus sites that you must absolutely check out!',
|
|
||||||
id: 'showcase.tag.favorite.description',
|
|
||||||
}),
|
|
||||||
color: '#e9669e',
|
|
||||||
},
|
|
||||||
|
|
||||||
opensource: {
|
|
||||||
label: translate({message: 'Open-Source'}),
|
|
||||||
description: translate({
|
|
||||||
message: 'Open-Source Docusaurus sites can be useful for inspiration!',
|
|
||||||
id: 'showcase.tag.opensource.description',
|
|
||||||
}),
|
|
||||||
color: '#39ca30',
|
|
||||||
},
|
|
||||||
|
|
||||||
product: {
|
|
||||||
label: translate({message: 'Product'}),
|
|
||||||
description: translate({
|
|
||||||
message: 'Docusaurus sites associated to a commercial product!',
|
|
||||||
id: 'showcase.tag.product.description',
|
|
||||||
}),
|
|
||||||
color: '#dfd545',
|
|
||||||
},
|
|
||||||
|
|
||||||
design: {
|
|
||||||
label: translate({message: 'Design'}),
|
|
||||||
description: translate({
|
|
||||||
message:
|
|
||||||
'Beautiful Docusaurus sites, polished and standing out from the initial template!',
|
|
||||||
id: 'showcase.tag.design.description',
|
|
||||||
}),
|
|
||||||
color: '#a44fb7',
|
|
||||||
},
|
|
||||||
|
|
||||||
i18n: {
|
|
||||||
label: translate({message: 'I18n'}),
|
|
||||||
description: translate({
|
|
||||||
message:
|
|
||||||
'Translated Docusaurus sites using the internationalization support with more than 1 locale.',
|
|
||||||
id: 'showcase.tag.i18n.description',
|
|
||||||
}),
|
|
||||||
color: '#127f82',
|
|
||||||
},
|
|
||||||
|
|
||||||
versioning: {
|
|
||||||
label: translate({message: 'Versioning'}),
|
|
||||||
description: translate({
|
|
||||||
message:
|
|
||||||
'Docusaurus sites using the versioning feature of the docs plugin to manage multiple versions.',
|
|
||||||
id: 'showcase.tag.versioning.description',
|
|
||||||
}),
|
|
||||||
color: '#fe6829',
|
|
||||||
},
|
|
||||||
|
|
||||||
large: {
|
|
||||||
label: translate({message: 'Large'}),
|
|
||||||
description: translate({
|
|
||||||
message:
|
|
||||||
'Very large Docusaurus sites, including many more pages than the average!',
|
|
||||||
id: 'showcase.tag.large.description',
|
|
||||||
}),
|
|
||||||
color: '#8c2f00',
|
|
||||||
},
|
|
||||||
|
|
||||||
meta: {
|
|
||||||
label: translate({message: 'Meta'}),
|
|
||||||
description: translate({
|
|
||||||
message: 'Docusaurus sites of Meta (formerly Facebook) projects',
|
|
||||||
id: 'showcase.tag.meta.description',
|
|
||||||
}),
|
|
||||||
color: '#4267b2', // Facebook blue
|
|
||||||
},
|
|
||||||
|
|
||||||
personal: {
|
|
||||||
label: translate({message: 'Personal'}),
|
|
||||||
description: translate({
|
|
||||||
message:
|
|
||||||
'Personal websites, blogs and digital gardens built with Docusaurus',
|
|
||||||
id: 'showcase.tag.personal.description',
|
|
||||||
}),
|
|
||||||
color: '#14cfc3',
|
|
||||||
},
|
|
||||||
|
|
||||||
rtl: {
|
|
||||||
label: translate({message: 'RTL Direction'}),
|
|
||||||
description: translate({
|
|
||||||
message:
|
|
||||||
'Docusaurus sites using the right-to-left reading direction support.',
|
|
||||||
id: 'showcase.tag.rtl.description',
|
|
||||||
}),
|
|
||||||
color: '#ffcfc3',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const TagList = Object.keys(Tags) as TagType[];
|
|
||||||
|
|
||||||
function sortBy<T>(
|
|
||||||
array: T[],
|
|
||||||
getter: (item: T) => string | number | boolean,
|
|
||||||
): T[] {
|
|
||||||
const sortedArray = [...array];
|
|
||||||
sortedArray.sort((a, b) =>
|
|
||||||
// eslint-disable-next-line no-nested-ternary
|
|
||||||
getter(a) > getter(b) ? 1 : getter(b) > getter(a) ? -1 : 0,
|
|
||||||
);
|
|
||||||
return sortedArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortUsers(users: Users): Users {
|
|
||||||
// Sort by site name
|
|
||||||
let result = sortBy(users, (user) => user.title.toLowerCase());
|
|
||||||
// Sort by favorite tag, favorites first
|
|
||||||
result = sortBy(result, (user) => (user.tags.includes('favorite') ? -1 : 1));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ShowcaseHeader() {
|
function ShowcaseHeader() {
|
||||||
return (
|
return (
|
||||||
<section className="margin-top--lg margin-bottom--lg text--center">
|
<section className="margin-top--lg margin-bottom--lg text--center">
|
||||||
|
@ -184,282 +36,20 @@ function ShowcaseHeader() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareUserState(): UserState | undefined {
|
export default function Showcase(): JSX.Element {
|
||||||
if (ExecutionEnvironment.canUseDOM) {
|
|
||||||
return {
|
|
||||||
scrollTopPosition: window.scrollY,
|
|
||||||
focusedElementId: document.activeElement?.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function restoreUserState(userState: UserState | null) {
|
|
||||||
const {scrollTopPosition, focusedElementId} = userState ?? {
|
|
||||||
scrollTopPosition: 0,
|
|
||||||
focusedElementId: undefined,
|
|
||||||
};
|
|
||||||
// @ts-expect-error: if focusedElementId is undefined it returns null
|
|
||||||
document.getElementById(focusedElementId)?.focus();
|
|
||||||
window.scrollTo({top: scrollTopPosition});
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterUsers(
|
|
||||||
users: Users,
|
|
||||||
selectedTags: TagType[],
|
|
||||||
operator: Operator,
|
|
||||||
searchName: string | null,
|
|
||||||
) {
|
|
||||||
if (searchName) {
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
users = users.filter((user) =>
|
|
||||||
user.title.toLowerCase().includes(searchName.toLowerCase()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (selectedTags.length === 0) {
|
|
||||||
return users;
|
|
||||||
}
|
|
||||||
return users.filter((user) => {
|
|
||||||
if (user.tags.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (operator === 'AND') {
|
|
||||||
return selectedTags.every((tag) => user.tags.includes(tag));
|
|
||||||
}
|
|
||||||
return selectedTags.some((tag) => user.tags.includes(tag));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const SearchNameQueryKey = 'name';
|
|
||||||
|
|
||||||
function readSearchName(search: string) {
|
|
||||||
return new URLSearchParams(search).get(SearchNameQueryKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
const TagQueryStringKey = 'tags';
|
|
||||||
|
|
||||||
function readSearchTags(search: string): TagType[] {
|
|
||||||
return new URLSearchParams(search).getAll(TagQueryStringKey) as TagType[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function useFilteredUsers(users: Users) {
|
|
||||||
const location = useLocation<UserState>();
|
|
||||||
const [operator, setOperator] = useState<Operator>('OR');
|
|
||||||
// On SSR / first mount (hydration) no tag is selected
|
|
||||||
const [selectedTags, setSelectedTags] = useState<TagType[]>([]);
|
|
||||||
const [searchName, setSearchName] = useState<string | null>(null);
|
|
||||||
// Sync tags from QS to state (delayed on purpose to avoid SSR/Client
|
|
||||||
// hydration mismatch)
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedTags(readSearchTags(location.search));
|
|
||||||
setOperator(readOperator(location.search));
|
|
||||||
setSearchName(readSearchName(location.search));
|
|
||||||
restoreUserState(location.state);
|
|
||||||
}, [location]);
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => filterUsers(sortUsers(users), selectedTags, operator, searchName),
|
|
||||||
[selectedTags, operator, searchName, users],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function useSiteCountPlural() {
|
|
||||||
const {selectMessage} = usePluralForm();
|
|
||||||
return (sitesCount: number) =>
|
|
||||||
selectMessage(
|
|
||||||
sitesCount,
|
|
||||||
translate(
|
|
||||||
{
|
|
||||||
id: 'showcase.filters.resultCount',
|
|
||||||
description:
|
|
||||||
'Pluralized label for the number of sites found on the showcase. Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',
|
|
||||||
message: '1 site|{sitesCount} sites',
|
|
||||||
},
|
|
||||||
{sitesCount},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ShowcaseFilters({users}: {users: Users}) {
|
|
||||||
const filteredUsers = useFilteredUsers(users);
|
|
||||||
const siteCountPlural = useSiteCountPlural();
|
|
||||||
return (
|
return (
|
||||||
<section className="container margin-top--l margin-bottom--lg">
|
<Layout title={TITLE} description={DESCRIPTION}>
|
||||||
<div className={clsx('margin-bottom--sm', styles.filterCheckbox)}>
|
{/* eslint-disable-next-line @docusaurus/prefer-docusaurus-heading, @docusaurus/no-untranslated-text */}
|
||||||
<div>
|
|
||||||
<Heading as="h2">
|
|
||||||
<Translate id="showcase.filters.title">Filters</Translate>
|
|
||||||
</Heading>
|
|
||||||
<span>{siteCountPlural(filteredUsers.length)}</span>
|
|
||||||
</div>
|
|
||||||
<ShowcaseFilterToggle />
|
|
||||||
</div>
|
|
||||||
<ul className={clsx('clean-list', styles.checkboxList)}>
|
|
||||||
{TagList.map((tag, i) => {
|
|
||||||
const {label, color} = Tags[tag];
|
|
||||||
const id = `showcase_checkbox_id_${tag}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={i} className={styles.checkboxListItem}>
|
|
||||||
<ShowcaseTagSelect
|
|
||||||
tag={tag}
|
|
||||||
id={id}
|
|
||||||
label={label}
|
|
||||||
// description={description} TODO
|
|
||||||
icon={
|
|
||||||
tag === 'favorite' ? (
|
|
||||||
<FavoriteIcon svgClass={styles.svgIconFavoriteXs} />
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
backgroundColor: color,
|
|
||||||
width: 10,
|
|
||||||
height: 10,
|
|
||||||
borderRadius: '50%',
|
|
||||||
marginLeft: 8,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SearchBar() {
|
|
||||||
const history = useHistory();
|
|
||||||
const location = useLocation();
|
|
||||||
const [value, setValue] = useState<string | null>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
setValue(readSearchName(location.search));
|
|
||||||
}, [location]);
|
|
||||||
return (
|
|
||||||
<div className={styles.searchContainer}>
|
|
||||||
<input
|
|
||||||
id="searchbar"
|
|
||||||
placeholder={translate({
|
|
||||||
message: 'Search for site name...',
|
|
||||||
id: 'showcase.searchBar.placeholder',
|
|
||||||
})}
|
|
||||||
value={value ?? undefined}
|
|
||||||
onInput={(e) => {
|
|
||||||
setValue(e.currentTarget.value);
|
|
||||||
const newSearch = new URLSearchParams(location.search);
|
|
||||||
newSearch.delete(SearchNameQueryKey);
|
|
||||||
if (e.currentTarget.value) {
|
|
||||||
newSearch.set(SearchNameQueryKey, e.currentTarget.value);
|
|
||||||
}
|
|
||||||
history.push({
|
|
||||||
...location,
|
|
||||||
search: newSearch.toString(),
|
|
||||||
state: prepareUserState(),
|
|
||||||
});
|
|
||||||
setTimeout(() => {
|
|
||||||
document.getElementById('searchbar')?.focus();
|
|
||||||
}, 0);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ShowcaseCards({users}: {users: Users}) {
|
|
||||||
const filteredUsers = useFilteredUsers(users);
|
|
||||||
|
|
||||||
if (filteredUsers.length === 0) {
|
|
||||||
return (
|
|
||||||
<section className="margin-top--lg margin-bottom--xl">
|
|
||||||
<div className="container padding-vert--md text--center">
|
|
||||||
<Heading as="h2">
|
|
||||||
<Translate id="showcase.usersList.noResult">No result</Translate>
|
|
||||||
</Heading>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const favoriteUsers = users.filter((user) => user.tags.includes('favorite'));
|
|
||||||
const otherUsers = users.filter((user) => !user.tags.includes('favorite'));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="margin-top--lg margin-bottom--xl">
|
|
||||||
{filteredUsers.length === sortUsers(users).length ? (
|
|
||||||
<>
|
|
||||||
<div className={styles.showcaseFavorite}>
|
|
||||||
<div className="container">
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'margin-bottom--md',
|
|
||||||
styles.showcaseFavoriteHeader,
|
|
||||||
)}>
|
|
||||||
<Heading as="h2">
|
|
||||||
<Translate id="showcase.favoritesList.title">
|
|
||||||
Our favorites
|
|
||||||
</Translate>
|
|
||||||
</Heading>
|
|
||||||
<FavoriteIcon svgClass={styles.svgIconFavorite} />
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
className={clsx(
|
|
||||||
'container',
|
|
||||||
'clean-list',
|
|
||||||
styles.showcaseList,
|
|
||||||
)}>
|
|
||||||
{favoriteUsers.map((user) => (
|
|
||||||
<ShowcaseCard key={user.title} user={user} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="container margin-top--lg">
|
|
||||||
<Heading as="h2" className={styles.showcaseHeader}>
|
|
||||||
<Translate id="showcase.usersList.allUsers">All sites</Translate>
|
|
||||||
</Heading>
|
|
||||||
<ul className={clsx('clean-list', styles.showcaseList)}>
|
|
||||||
{otherUsers.map((user) => (
|
|
||||||
<ShowcaseCard key={user.title} user={user} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="container">
|
|
||||||
<div
|
|
||||||
className={clsx('margin-bottom--md', styles.showcaseFavoriteHeader)}
|
|
||||||
/>
|
|
||||||
<ul className={clsx('clean-list', styles.showcaseList)}>
|
|
||||||
{filteredUsers.map((user) => (
|
|
||||||
<ShowcaseCard key={user.title} user={user} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Showcase(props: Props): JSX.Element {
|
|
||||||
const users = props.content;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout title="Showcase">
|
|
||||||
{/* eslint-disable-next-line @docusaurus/prefer-docusaurus-heading */}
|
|
||||||
<h1>Client showcase API: {clientShowcase}</h1>
|
<h1>Client showcase API: {clientShowcase}</h1>
|
||||||
<div>{JSON.stringify(props)}</div>
|
|
||||||
<main className="margin-vert--lg">
|
<main className="margin-vert--lg">
|
||||||
<ShowcaseHeader />
|
<ShowcaseHeader />
|
||||||
<ShowcaseFilters users={users} />
|
<ShowcaseFilters />
|
||||||
<div
|
<div
|
||||||
style={{display: 'flex', marginLeft: 'auto'}}
|
style={{display: 'flex', marginLeft: 'auto'}}
|
||||||
className="container">
|
className="container">
|
||||||
<SearchBar />
|
<ShowcaseSearchBar />
|
||||||
</div>
|
</div>
|
||||||
<ShowcaseCards users={users} />
|
<ShowcaseCards />
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,95 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.filterCheckbox {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filterCheckbox,
|
|
||||||
.checkboxList {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filterCheckbox > div:first-child {
|
|
||||||
display: flex;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filterCheckbox > div > * {
|
|
||||||
margin-bottom: 0;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkboxList {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkboxListItem {
|
|
||||||
user-select: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
height: 32px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkboxListItem:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchContainer {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchContainer input {
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 15px;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcaseList {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcaseFavorite {
|
|
||||||
padding-top: 2rem;
|
|
||||||
padding-bottom: 2rem;
|
|
||||||
background-color: var(--site-color-favorite-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcaseFavoriteHeader {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcaseFavoriteHeader > h2 {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcaseFavoriteHeader > svg {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svgIconFavoriteXs,
|
|
||||||
.svgIconFavorite {
|
|
||||||
color: var(--site-color-svg-icon-favorite);
|
|
||||||
}
|
|
||||||
|
|
||||||
.svgIconFavoriteXs {
|
|
||||||
margin-left: 0.625rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svgIconFavorite {
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
|
@ -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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type {Props} from '@theme/ShowcaseDetails';
|
|
||||||
import Layout from '@theme/Layout';
|
|
||||||
|
|
||||||
export default function Showcase(props: Props): JSX.Element {
|
|
||||||
const {content: MDXPageContent} = props;
|
|
||||||
const {title, description} = MDXPageContent;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout title="Showcase Details">
|
|
||||||
<div>Title {JSON.stringify(title)}</div>
|
|
||||||
<div>Description {JSON.stringify(description)}</div>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue