mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-06 12:52: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;
|
||||
}
|
||||
|
||||
declare module '@theme/ShowcaseDetails' {
|
||||
import type {ShowcaseItem} from '@docusaurus/plugin-content-showcase';
|
||||
|
||||
export type User = ShowcaseItem;
|
||||
|
||||
export type Props = {
|
||||
content: User;
|
||||
};
|
||||
|
||||
export default function Showcase(props: Props): JSX.Element;
|
||||
declare module '@theme/Showcase' {
|
||||
export default function Showcase(): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/Showcase' {
|
||||
import type {ShowcaseItem} from '@docusaurus/plugin-content-showcase';
|
||||
declare module '@theme/Showcase/FavoriteIcon' {
|
||||
export interface Props {
|
||||
className?: string;
|
||||
style?: ComponentProps<'svg'>['style'];
|
||||
size: 'small' | 'medium' | 'large';
|
||||
}
|
||||
|
||||
export type User = ShowcaseItem;
|
||||
|
||||
export type Props = {
|
||||
content: User[];
|
||||
};
|
||||
|
||||
export default function Showcase(props: Props): JSX.Element;
|
||||
export default function FavoriteIcon(props: Props): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/Showcase/ShowcaseCard' {
|
||||
|
@ -287,13 +277,41 @@ declare module '@theme/Showcase/ShowcaseCard' {
|
|||
|
||||
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'> {
|
||||
icon: ReactElement<ComponentProps<'svg'>>;
|
||||
label: ReactNode;
|
||||
declare module '@theme/Showcase/ShowcaseCards' {
|
||||
export default function ShowcaseCards(): JSX.Element;
|
||||
}
|
||||
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;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: ReactElement<ComponentProps<'svg'>>;
|
||||
}
|
||||
|
||||
export default function ShowcaseTagSelect(props: Props): JSX.Element;
|
||||
|
@ -304,27 +322,32 @@ declare module '@theme/Showcase/ShowcaseFilterToggle' {
|
|||
export default function ShowcaseFilterToggle(): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/Showcase/FavoriteIcon' {
|
||||
import {type ReactNode, type ComponentProps} from 'react';
|
||||
declare module '@theme/Showcase/ShowcaseFilters' {
|
||||
// TODO use from plugin content showcase
|
||||
export type TagType =
|
||||
| 'favorite'
|
||||
| 'opensource'
|
||||
| 'product'
|
||||
| 'design'
|
||||
| 'i18n'
|
||||
| 'versioning'
|
||||
| 'large'
|
||||
| 'meta'
|
||||
| 'personal'
|
||||
| 'rtl';
|
||||
|
||||
export type SvgIconProps = 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 ShowcaseFilters(): JSX.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' {
|
||||
|
|
|
@ -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 clsx from 'clsx';
|
||||
import type {Props, SvgIconProps} from '@theme/Showcase/FavoriteIcon';
|
||||
|
||||
import type {Props} from '@theme/Showcase/FavoriteIcon';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
function Svg(props: SvgIconProps): JSX.Element {
|
||||
const {
|
||||
svgClass,
|
||||
colorAttr,
|
||||
children,
|
||||
color = 'inherit',
|
||||
size = 'medium',
|
||||
viewBox = '0 0 24 24',
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
export default function FavoriteIcon({
|
||||
size,
|
||||
className,
|
||||
style,
|
||||
}: Props): React.ReactNode {
|
||||
return (
|
||||
<svg
|
||||
viewBox={viewBox}
|
||||
color={colorAttr}
|
||||
aria-hidden
|
||||
className={clsx(styles.svgIcon, styles[color], styles[size], svgClass)}
|
||||
{...rest}>
|
||||
{children}
|
||||
viewBox="0 0 24 24"
|
||||
className={clsx(styles.svg, styles[size], className)}
|
||||
style={style}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
.svgIcon {
|
||||
.svg {
|
||||
user-select: none;
|
||||
color: #e9669e;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
display: inline-block;
|
||||
fill: currentColor;
|
||||
flex-shrink: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* font-size */
|
||||
.small {
|
||||
font-size: 1.25rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.medium {
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
|
|
@ -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 clsx from 'clsx';
|
||||
import Link from '@docusaurus/Link';
|
||||
import Translate, {translate} from '@docusaurus/Translate';
|
||||
import FavoriteIcon from '@theme/Showcase/FavoriteIcon';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
// 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 FavoriteIcon from '@theme/Showcase/FavoriteIcon';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
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[];
|
||||
|
||||
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 = {
|
||||
function TagItem({
|
||||
label,
|
||||
description,
|
||||
color,
|
||||
}: {
|
||||
label: string;
|
||||
description: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const TagComp = React.forwardRef<HTMLLIElement, Tag>(
|
||||
({label, color, description}, ref) => (
|
||||
<li ref={ref} className={styles.tag} title={description}>
|
||||
}) {
|
||||
return (
|
||||
<li className={styles.tag} title={description}>
|
||||
<span className={styles.textLabel}>{label.toLowerCase()}</span>
|
||||
<span className={styles.colorLabel} style={{backgroundColor: color}} />
|
||||
</li>
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
function ShowcaseCardTag({tags}: {tags: TagType[]}) {
|
||||
const tagObjects = tags.map((tag) => ({tag, ...Tags[tag]}));
|
||||
|
@ -173,7 +45,7 @@ function ShowcaseCardTag({tags}: {tags: TagType[]}) {
|
|||
return (
|
||||
<>
|
||||
{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 {
|
||||
return (
|
||||
user.preview ??
|
||||
// TODO make it configurable
|
||||
`https://slorber-api-screenshot.netlify.app/${encodeURIComponent(
|
||||
user.website,
|
||||
)}/showcase`
|
||||
|
@ -193,6 +66,7 @@ function ShowcaseCard({user}: {user: User}) {
|
|||
return (
|
||||
<li key={user.title} className="card shadow--md">
|
||||
<div className={clsx('card__image', styles.showcaseCardImage)}>
|
||||
{/* TODO change back to ideal image */}
|
||||
<img src={image} alt={user.title} />
|
||||
</div>
|
||||
<div className="card__body">
|
||||
|
@ -203,7 +77,7 @@ function ShowcaseCard({user}: {user: User}) {
|
|||
</Link>
|
||||
</Heading>
|
||||
{user.tags.includes('favorite') && (
|
||||
<FavoriteIcon svgClass={styles.svgIconFavorite} size="small" />
|
||||
<FavoriteIcon size="medium" style={{marginRight: '0.25rem'}} />
|
||||
)}
|
||||
{user.source && (
|
||||
<Link
|
||||
|
|
|
@ -37,14 +37,10 @@
|
|||
}
|
||||
|
||||
.showcaseCardTitle,
|
||||
.showcaseCardHeader .svgIconFavorite {
|
||||
.showcaseCardHeader {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.showcaseCardHeader .svgIconFavorite {
|
||||
color: var(--site-color-svg-icon-favorite);
|
||||
}
|
||||
|
||||
.showcaseCardSrcBtn {
|
||||
margin-left: 6px;
|
||||
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.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
useCallback,
|
||||
useState,
|
||||
useEffect,
|
||||
type ComponentProps,
|
||||
type ReactNode,
|
||||
type ReactElement,
|
||||
} from 'react';
|
||||
import {useHistory, useLocation} from '@docusaurus/router';
|
||||
import React, {useCallback, type ReactNode, useId} from 'react';
|
||||
import type {Props} from '@theme/Showcase/ShowcaseTagSelect';
|
||||
import {useTags} from '../../_utils';
|
||||
|
||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
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 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(),
|
||||
function useTagState(tag: string) {
|
||||
const [tags, setTags] = useTags();
|
||||
const isSelected = tags.includes(tag);
|
||||
const toggle = useCallback(() => {
|
||||
setTags((list) => {
|
||||
return list.includes(tag)
|
||||
? list.filter((t) => t !== tag)
|
||||
: [...list, tag];
|
||||
});
|
||||
}, [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 (
|
||||
<>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={id}
|
||||
checked={isSelected}
|
||||
onChange={toggle}
|
||||
className="screen-reader-only"
|
||||
onKeyDown={(e) => {
|
||||
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}
|
||||
/>
|
||||
<label ref={ref} htmlFor={id} className={styles.checkboxLabel}>
|
||||
<label htmlFor={id} className={styles.checkboxLabel} title={description}>
|
||||
{label}
|
||||
{icon}
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.forwardRef(ShowcaseTagSelect);
|
||||
|
|
|
@ -28,7 +28,7 @@ input:focus-visible + .checkboxLabel {
|
|||
|
||||
input:checked + .checkboxLabel {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -36,3 +36,7 @@ input:checked + .checkboxLabel:hover {
|
|||
opacity: 0.75;
|
||||
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.
|
||||
*/
|
||||
|
||||
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 {usePluralForm} from '@docusaurus/theme-common';
|
||||
|
||||
import Link from '@docusaurus/Link';
|
||||
import {clientShowcase} from '@docusaurus/plugin-content-showcase/client';
|
||||
import type {User, Props} from '@theme/Showcase';
|
||||
import Layout from '@theme/Layout';
|
||||
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 DESCRIPTION = translate({
|
||||
|
@ -32,144 +22,6 @@ const DESCRIPTION = translate({
|
|||
});
|
||||
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() {
|
||||
return (
|
||||
<section className="margin-top--lg margin-bottom--lg text--center">
|
||||
|
@ -184,282 +36,20 @@ function ShowcaseHeader() {
|
|||
);
|
||||
}
|
||||
|
||||
function prepareUserState(): UserState | undefined {
|
||||
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();
|
||||
export default function Showcase(): JSX.Element {
|
||||
return (
|
||||
<section className="container margin-top--l margin-bottom--lg">
|
||||
<div className={clsx('margin-bottom--sm', styles.filterCheckbox)}>
|
||||
<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 */}
|
||||
<Layout title={TITLE} description={DESCRIPTION}>
|
||||
{/* eslint-disable-next-line @docusaurus/prefer-docusaurus-heading, @docusaurus/no-untranslated-text */}
|
||||
<h1>Client showcase API: {clientShowcase}</h1>
|
||||
<div>{JSON.stringify(props)}</div>
|
||||
<main className="margin-vert--lg">
|
||||
<ShowcaseHeader />
|
||||
<ShowcaseFilters users={users} />
|
||||
<ShowcaseFilters />
|
||||
<div
|
||||
style={{display: 'flex', marginLeft: 'auto'}}
|
||||
className="container">
|
||||
<SearchBar />
|
||||
<ShowcaseSearchBar />
|
||||
</div>
|
||||
<ShowcaseCards users={users} />
|
||||
<ShowcaseCards />
|
||||
</main>
|
||||
</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