Skip to content

Commit

Permalink
[Gallery] Add option to filter plugins based on tags (facebook#6391)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sahejkm authored and 2wheeh committed Jul 17, 2024
1 parent 9bdaade commit 5217e4f
Show file tree
Hide file tree
Showing 7 changed files with 329 additions and 6 deletions.
25 changes: 21 additions & 4 deletions packages/lexical-website/src/components/Gallery/GalleryCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import clsx from 'clsx';
import React, {useEffect, useState} from 'react';

import Card from './Card';
import Filters from './components/Filters';
import SearchBar from './components/SearchBar';
import {Example, plugins} from './pluginList';
import styles from './styles.module.css';
import {Tag, TagList} from './tagList';
import {useFilteredExamples} from './utils';

function CardList({cards}: {cards: Array<Example>}) {
Expand All @@ -41,26 +43,41 @@ function GalleryCardsImpl() {
const [internGalleryCards, setInternGalleryCards] = useState<{
InternGalleryCards: () => Array<Example>;
} | null>(null);
const [internGalleryTags, setInternGalleryTags] = useState<{
InternGalleryTags: () => {[type in string]: Tag};
} | null>(null);

const pluginsCombined = plugins(customFields ?? {}).concat(
internGalleryCards != null ? internGalleryCards.InternGalleryCards() : [],
);

const tagList = {
...TagList,
...(internGalleryTags != null ? internGalleryTags.InternGalleryTags() : {}),
};

const filteredPlugins = useFilteredExamples(pluginsCombined);

useEffect(() => {
if (process.env.FB_INTERNAL) {
// @ts-ignore runtime dependency for intern builds
import('../../../../InternGalleryCards').then(setInternGalleryCards);
// @ts-ignore runtime dependency for intern builds
import('../../../../InternGalleryTags').then(setInternGalleryTags);
}
}, []);

return (
<section className="margin-top--lg margin-bottom--xl">
<div style={{display: 'flex', marginLeft: 'auto'}} className="container">
<SearchBar />
</div>
<CardList cards={filteredPlugins} />
<main className="margin-vert--lg">
<Filters filteredPlugins={filteredPlugins} tagList={tagList} />
<div
style={{display: 'flex', marginLeft: 'auto'}}
className="container">
<SearchBar />
</div>
<CardList cards={filteredPlugins} />
</main>
</section>
);
}
100 changes: 100 additions & 0 deletions packages/lexical-website/src/components/Gallery/components/Filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* Copyright (c) Meta Platforms, Inc. and 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 {CSSProperties, ReactNode} from 'react';

import Heading from '@theme/Heading';
import clsx from 'clsx';
import React from 'react';

import {Example} from '../pluginList';
import {Tag} from '../tagList';
import styles from './styles.module.css';
import TagSelect from './TagSelect';

function TagCircleIcon({color, style}: {color: string; style?: CSSProperties}) {
return (
<span
style={{
backgroundColor: color,
borderRadius: '50%',
height: 10,
width: 10,
...style,
}}
/>
);
}

function TagListItem({tag, tagKey}: {tag: Tag; tagKey: string}) {
const {title, description, color} = tag;
return (
<li className={styles.tagListItem}>
<TagSelect
tag={tagKey}
label={title}
description={description}
icon={
<TagCircleIcon
color={color}
style={{
backgroundColor: color,
marginLeft: 8,
}}
/>
}
/>
</li>
);
}

function TagList({allTags}: {allTags: {[type in string]: Tag}}) {
return (
<ul className={clsx('clean-list', styles.tagList)}>
{Object.keys(allTags).map((tag) => {
return <TagListItem key={tag} tag={allTags[tag]} tagKey={tag} />;
})}
</ul>
);
}

function HeadingText({filteredPlugins}: {filteredPlugins: Array<Example>}) {
return (
<div className={styles.headingText}>
<Heading as="h2">Filters</Heading>
<span>
{filteredPlugins.length === 1
? '1 exampe'
: `${filteredPlugins.length} examples`}
</span>
</div>
);
}

function HeadingRow({filteredPlugins}: {filteredPlugins: Array<Example>}) {
return (
<div className={clsx('margin-bottom--sm', styles.headingRow)}>
<HeadingText filteredPlugins={filteredPlugins} />
</div>
);
}

export default function Filters({
filteredPlugins,
tagList,
}: {
filteredPlugins: Array<Example>;
tagList: {[type in string]: Tag};
}): ReactNode {
return (
<section className="margin-top--l margin-bottom--lg container">
<HeadingRow filteredPlugins={filteredPlugins} />
<TagList allTags={tagList} />
</section>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Copyright (c) Meta Platforms, Inc. and 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 ComponentProps,
type ReactElement,
type ReactNode,
useCallback,
useId,
} from 'react';

import {useTags} from '../utils';
import styles from './styles.module.css';

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, setTags]);

return [isSelected, toggle] as const;
}

interface Props extends ComponentProps<'input'> {
tag: string;
label: string;
description: string;
icon: ReactElement<ComponentProps<'svg'>>;
}

export default function TagSelect({
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={styles.screenReaderOnly}
onKeyDown={(e) => {
if (e.key === 'Enter') {
toggle();
}
}}
{...rest}
/>
<label htmlFor={id} className={styles.checkboxLabel} title={description}>
{label}
{icon}
</label>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,95 @@
padding: 10px;
border: 1px solid gray;
}

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

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

.checkboxLabel:hover {
opacity: 1;
box-shadow: 0 0 2px 1px var(--ifm-color-secondary-darkest);
}

input[type='checkbox'] + .checkboxLabel {
display: flex;
align-items: center;
cursor: pointer;
line-height: 1.5;
border-radius: 4px;
padding: 0.275rem 0.8rem;
opacity: 0.85;
transition: opacity 200ms ease-out;
border: 2px solid var(--ifm-color-secondary-darkest);
}

input:focus-visible + .checkboxLabel {
outline: 2px solid currentColor;
}

input:checked + .checkboxLabel {
opacity: 0.9;
background-color: hsl(167deg 56% 73% / 25%);
border: 2px solid var(--ifm-color-primary-darkest);
}

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

.screenReaderOnly {
border: 0;
clip: rect(0 0 0 0);
clip-path: polygon(0 0, 0 0, 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
white-space: nowrap;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,21 @@ export type Example = {
uri?: string;
preview?: string;
renderPreview?: () => ReactNode;
tags: Array<string>;
};

export const plugins = (customFields: {
[key: string]: unknown;
}): Array<Example> => [
{
description: 'Learn how to create an editor with Emojis',
tags: ['opensource'],
title: 'EmojiPlugin',
uri: `${customFields.STACKBLITZ_PREFIX}examples/vanilla-js-plugin?embed=1&file=src%2Femoji-plugin%2FEmojiPlugin.ts&terminalHeight=0&ctl=0`,
},
{
description: 'Learn how to create an editor with Real Time Collaboration',
tags: ['opensource', 'favorite'],
title: 'Collab RichText',
uri: 'https://stackblitz.com/github/facebook/lexical/tree/fix/collab_example/examples/react-rich-collab?ctl=0&file=src%2Fmain.tsx&terminalHeight=0&embed=1',
},
Expand Down
27 changes: 27 additions & 0 deletions packages/lexical-website/src/components/Gallery/tagList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

export type Tag = {
color: string;
description: string;
title: string;
};

export const TagList: {[type in string]: Tag} = {
favorite: {
color: '#e9669e',
description:
'Our favorite Docusaurus sites that you must absolutely check out!',
title: 'Favorite',
},
opensource: {
color: '#39ca30',
description: 'Open-Source Lexical plugins for inspiration',
title: 'Open-Source',
},
};
Loading

0 comments on commit 5217e4f

Please sign in to comment.