This commit is contained in:
ozakione 2024-04-17 17:55:49 +02:00
parent 5b6626bcf4
commit 8c12b1c619
16 changed files with 126 additions and 193 deletions

View file

@ -19,27 +19,27 @@ import type {
ShowcaseItem,
} from '@docusaurus/plugin-content-showcase';
export function filterUsers({
users,
export function filterItems({
items,
tags,
operator,
searchName,
}: {
users: ShowcaseItem[];
items: ShowcaseItem[];
tags: TagType[];
operator: Operator;
searchName: string | undefined | null;
}): ShowcaseItem[] {
if (searchName) {
// eslint-disable-next-line no-param-reassign
users = users.filter((user) =>
items = items.filter((user) =>
user.title.toLowerCase().includes(searchName.toLowerCase()),
);
}
if (tags.length === 0) {
return users;
return items;
}
return users.filter((user) => {
return items.filter((user) => {
if (user.tags.length === 0) {
return false;
}
@ -71,19 +71,19 @@ export function useOperator(): [Operator, () => void] {
return [operator, toggleOperator];
}
export function useFilteredUsers(users: ShowcaseItem[]): ShowcaseItem[] {
export function useFilteredItems(items: ShowcaseItem[]): ShowcaseItem[] {
const [tags] = useTags();
const [searchName] = useSearchName() ?? [''];
const [operator] = useOperator();
return useMemo(
() =>
filterUsers({
users,
filterItems({
items,
tags: tags as TagType[],
operator,
searchName,
}),
[users, tags, operator, searchName],
[items, tags, operator, searchName],
);
}
@ -122,108 +122,7 @@ export type Tag = {
color: string;
};
export 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',
},
};
export const TagList = Object.keys(Tags) as TagType[];
export function sortUsers(params: ShowcaseItem[]): ShowcaseItem[] {
export function sortItems(params: ShowcaseItem[]): ShowcaseItem[] {
let result = params;
// Sort by site name
result = sortBy(result, (user) => user.title.toLowerCase());
@ -231,5 +130,3 @@ export function sortUsers(params: ShowcaseItem[]): ShowcaseItem[] {
result = sortBy(result, (user) => !user.tags.includes('favorite'));
return result;
}
// export const sortedUsers = sortUsers();

View file

@ -37,17 +37,17 @@ export default async function pluginContentShowcase(
contentPath: path.resolve(siteDir, sitePath),
contentPathLocalized: getPluginI18nPath({
localizationDir,
pluginName: 'docusaurus-plugin-content-pages',
pluginName: 'docusaurus-plugin-content-showcase',
pluginId: id,
}),
};
const tagList = await getTagsList({
const {tags: validatedTags, tagkeys} = await getTagsList({
configTags: tags,
configPath: contentPaths.contentPath,
});
const showcaseItemSchema = createShowcaseItemSchema(tagList);
const showcaseItemSchema = createShowcaseItemSchema(tagkeys);
return {
name: 'docusaurus-plugin-content-showcase',
@ -80,6 +80,7 @@ export default async function pluginContentShowcase(
await processContentLoaded({
content,
tags: validatedTags,
routeBasePath,
addRoute,
});

View file

@ -5,16 +5,21 @@
* LICENSE file in the root directory of this source tree.
*/
import type {ShowcaseItems} from '@docusaurus/plugin-content-showcase';
import type {
ShowcaseItems,
TagsOption,
} from '@docusaurus/plugin-content-showcase';
import type {PluginContentLoadedActions} from '@docusaurus/types';
export async function processContentLoaded({
content,
tags,
routeBasePath,
addRoute,
}: {
content: ShowcaseItems;
routeBasePath: string;
tags: TagsOption;
addRoute: PluginContentLoadedActions['addRoute'];
}): Promise<void> {
addRoute({
@ -22,6 +27,7 @@ export async function processContentLoaded({
component: '@theme/Showcase',
props: {
items: content.items,
tags,
},
exact: true,
});

View file

@ -17,6 +17,7 @@ export const DEFAULT_OPTIONS: PluginOptions = {
include: ['**/*.{yml,yaml}'],
// TODO exclude won't work if user pass a custom file name
exclude: [...GlobExcludeDefault, 'tags.*'],
screenshotApi: 'https://slorber-api-screenshot.netlify.app',
tags: 'tags.yml',
};
@ -28,6 +29,7 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
tags: Joi.alternatives()
.try(Joi.string().default(DEFAULT_OPTIONS.tags), tagSchema)
.default(DEFAULT_OPTIONS.tags),
screenshotApi: Joi.string().default(DEFAULT_OPTIONS.screenshotApi),
});
export function validateOptions({

View file

@ -32,7 +32,7 @@ declare module '@docusaurus/plugin-content-showcase' {
| 'rtl';
export type TagsOption = {
[tagName: string]: Tag;
[type in TagType]: Tag;
};
export type PluginOptions = {
@ -42,6 +42,7 @@ declare module '@docusaurus/plugin-content-showcase' {
include: string[];
exclude: string[];
tags: string | TagsOption;
screenshotApi: string;
};
export type ShowcaseItem = {

View file

@ -9,7 +9,10 @@ import fs from 'fs-extra';
import path from 'path';
import Yaml from 'js-yaml';
import {Joi} from '@docusaurus/utils-validation';
import type {PluginOptions} from '@docusaurus/plugin-content-showcase';
import type {
PluginOptions,
TagsOption,
} from '@docusaurus/plugin-content-showcase';
export const tagSchema = Joi.object().pattern(
Joi.string(),
@ -35,7 +38,7 @@ export async function getTagsList({
}: {
configTags: PluginOptions['tags'];
configPath: PluginOptions['path'];
}): Promise<string[]> {
}): Promise<{tagkeys: string[]; tags: TagsOption}> {
if (typeof configTags === 'object') {
const tags = tagSchema.validate(configTags);
if (tags.error) {
@ -44,7 +47,10 @@ export async function getTagsList({
{cause: tags},
);
}
return Object.keys(tags.value);
return {
tagkeys: Object.keys(tags.value),
tags: tags.value,
};
}
const tagsPath = path.resolve(configPath, configTags);
@ -61,8 +67,10 @@ export async function getTagsList({
);
}
const tagLabels = Object.keys(tags.value);
return tagLabels;
return {
tagkeys: Object.keys(tags.value),
tags: tags.value,
};
} catch (error) {
throw new Error(`Failed to read tags file for showcase`, {cause: error});
}

View file

@ -248,10 +248,14 @@ declare module '@theme/BlogPostItems' {
}
declare module '@theme/Showcase' {
import type {ShowcaseItem} from '@docusaurus/plugin-content-showcase';
import type {
ShowcaseItem,
TagsOption,
} from '@docusaurus/plugin-content-showcase';
export type Props = {
items: ShowcaseItem[];
tags: TagsOption;
};
export default function Showcase(props: Props): JSX.Element;

View file

@ -7,16 +7,20 @@
import React, {type ReactNode} from 'react';
import {useClearQueryString} from '@docusaurus/theme-common';
import Translate from '@docusaurus/Translate';
export default function ClearAllButton(): ReactNode {
const clearQueryString = useClearQueryString();
// TODO translate
return (
<button
className="button button--outline button--primary"
type="button"
onClick={() => clearQueryString()}>
<Translate
id="theme.Showcase.ClearAllButton.label"
description="The label for the Clear All button">
Clear All
</Translate>
</button>
);
}

View file

@ -7,7 +7,6 @@
import React from 'react';
import clsx from 'clsx';
import type {Props} from '@theme/Showcase/FavoriteIcon';
import styles from './styles.module.css';

View file

@ -8,13 +8,12 @@
import React, {useId} from 'react';
import clsx from 'clsx';
import {useOperator} from '@docusaurus/plugin-content-showcase/client';
import Translate from '@docusaurus/Translate';
import styles from './styles.module.css';
export default function OperatorButton(): JSX.Element {
const id = useId();
const [operator, toggleOperator] = useOperator();
// TODO add translations
return (
<>
<input
@ -30,11 +29,22 @@ export default function OperatorButton(): JSX.Element {
}
}}
/>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<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 */}
<span className={styles.checkboxLabelOr}>
<Translate
id="theme.Showcase.OrOperatorButton.label"
description="The label for the OR operator button">
OR
</Translate>
</span>
<span className={styles.checkboxLabelAnd}>
<Translate
id="theme.Showcase.AndOperatorButton.label"
description="The label for the AND operator button">
AND
</Translate>
</span>
</label>
</>
);

View file

@ -9,12 +9,8 @@ import React from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import Translate from '@docusaurus/Translate';
// import Image from '@theme/IdealImage';
import {
sortBy,
Tags,
TagList,
} from '@docusaurus/plugin-content-showcase/client';
import {sortBy} from '@docusaurus/plugin-content-showcase/client';
import {useShowcase} from '@docusaurus/theme-common/internal';
import Heading from '@theme/Heading';
import FavoriteIcon from '@theme/Showcase/FavoriteIcon';
import type {ShowcaseItem, TagType} from '@docusaurus/plugin-content-showcase';
@ -26,11 +22,14 @@ function TagItem({
color,
}: {
label: string;
description: string;
description: {
message: string;
id: string;
};
color: string;
}) {
return (
<li className={styles.tag} title={description}>
<li className={styles.tag} title={description.message}>
<span className={styles.textLabel}>{label.toLowerCase()}</span>
<span className={styles.colorLabel} style={{backgroundColor: color}} />
</li>
@ -38,6 +37,9 @@ function TagItem({
}
function ShowcaseCardTag({tags}: {tags: TagType[]}) {
const {tags: Tags} = useShowcase();
const TagList = Object.keys(Tags) as TagType[];
const tagObjects = tags.map((tag) => ({tag, ...Tags[tag]}));
// Keep same order for all tags
@ -54,23 +56,21 @@ function ShowcaseCardTag({tags}: {tags: TagType[]}) {
);
}
function getCardImage(user: ShowcaseItem): string {
function getCardImage(item: ShowcaseItem): string {
return (
user.preview ??
item.preview ??
// TODO make it configurable
`https://slorber-api-screenshot.netlify.app/${encodeURIComponent(
user.website,
item.website,
)}/showcase`
);
}
function ShowcaseCard({item}: {item: ShowcaseItem}) {
console.log('ShowcaseCard user:', item);
const image = getCardImage(item);
return (
<li key={item.title} className="card shadow--md">
<div className={clsx('card__image', styles.showcaseCardImage)}>
{/* TODO change back to ideal image */}
<img src={image} alt={item.title} />
</div>
<div className="card__body">

View file

@ -9,21 +9,20 @@ import type {ReactNode} from 'react';
import clsx from 'clsx';
import Translate from '@docusaurus/Translate';
import {
useFilteredUsers,
sortUsers,
useFilteredItems,
sortItems,
} from '@docusaurus/plugin-content-showcase/client';
import {useShowcase} from '@docusaurus/theme-common/internal';
import Heading from '@theme/Heading';
import FavoriteIcon from '@theme/Showcase/FavoriteIcon';
import ShowcaseCard from '@theme/Showcase/ShowcaseCard';
import type {ShowcaseItem} from '@docusaurus/plugin-content-showcase';
import styles from './styles.module.css';
function HeadingNoResult() {
return (
<Heading as="h2">
<Translate id="showcase.usersList.noResult">No result</Translate>
<Translate id="showcase.itemsList.noResult">No result</Translate>
</Heading>
);
}
@ -40,7 +39,7 @@ function HeadingFavorites() {
function HeadingAllSites() {
return (
<Heading as="h2">
<Translate id="showcase.usersList.allUsers">All sites</Translate>
<Translate id="showcase.itemsList.allItems">All sites</Translate>
</Heading>
);
}
@ -52,7 +51,6 @@ function CardList({
heading?: ReactNode;
items: ShowcaseItem[];
}) {
console.log('CardList items:', items);
return (
<div className="container">
{heading}
@ -76,40 +74,37 @@ function NoResultSection() {
}
export default function ShowcaseCards(): JSX.Element {
const users = useShowcase().items;
console.log('ShowcaseCards users:', users);
const {showcaseItems: items} = useShowcase();
const filteredUsers = useFilteredUsers(users);
const filteredItems = useFilteredItems(items);
if (filteredUsers.length === 0) {
if (filteredItems.length === 0) {
return <NoResultSection />;
}
const sortedUsers = sortUsers(users);
const sortedItems = sortItems(items);
const favoriteUsers = sortedUsers.filter((user: ShowcaseItem) =>
user.tags.includes('favorite'),
const favoriteItems = sortedItems.filter((item: ShowcaseItem) =>
item.tags.includes('favorite'),
);
console.log('favoriteUsers:', favoriteUsers);
const otherUsers = sortedUsers.filter(
(user: ShowcaseItem) => !user.tags.includes('favorite'),
const otherItems = sortedItems.filter(
(item: ShowcaseItem) => !item.tags.includes('favorite'),
);
console.log('otherUsers:', otherUsers);
return (
<section className="margin-top--lg margin-bottom--xl">
{filteredUsers.length === sortedUsers.length ? (
{filteredItems.length === sortedItems.length ? (
<>
<div className={styles.showcaseFavorite}>
<CardList heading={<HeadingFavorites />} items={favoriteUsers} />
<CardList heading={<HeadingFavorites />} items={favoriteItems} />
</div>
<div className="margin-top--lg">
<CardList heading={<HeadingAllSites />} items={otherUsers} />
<CardList heading={<HeadingAllSites />} items={otherItems} />
</div>
</>
) : (
<CardList items={filteredUsers} />
<CardList items={filteredItems} />
)}
</section>
);

View file

@ -9,10 +9,8 @@ import type {ReactNode, CSSProperties} from 'react';
import clsx from 'clsx';
import Translate from '@docusaurus/Translate';
import {
useFilteredUsers,
useFilteredItems,
useSiteCountPlural,
Tags,
TagList,
} from '@docusaurus/plugin-content-showcase/client';
import {useShowcase} from '@docusaurus/theme-common/internal';
import FavoriteIcon from '@theme/Showcase/FavoriteIcon';
@ -21,7 +19,6 @@ import ShowcaseTagSelect from '@theme/Showcase/ShowcaseTagSelect';
import OperatorButton from '@theme/Showcase/OperatorButton';
import ClearAllButton from '@theme/Showcase/ClearAllButton';
import type {TagType} from '@docusaurus/plugin-content-showcase';
import styles from './styles.module.css';
function TagCircleIcon({color, style}: {color: string; style?: CSSProperties}) {
@ -39,13 +36,14 @@ function TagCircleIcon({color, style}: {color: string; style?: CSSProperties}) {
}
function ShowcaseTagListItem({tag}: {tag: TagType}) {
const {label, description, color} = Tags[tag];
const {tags} = useShowcase();
const {label, description, color} = tags[tag];
return (
<li className={styles.tagListItem}>
<ShowcaseTagSelect
tag={tag}
label={label}
description={description}
description={description.message}
icon={
tag === 'favorite' ? (
<FavoriteIcon size="small" style={{marginLeft: 8}} />
@ -65,6 +63,8 @@ function ShowcaseTagListItem({tag}: {tag: TagType}) {
}
function ShowcaseTagList() {
const {tags} = useShowcase();
const TagList = Object.keys(tags) as TagType[];
return (
<ul className={clsx('clean-list', styles.tagList)}>
{TagList.map((tag) => {
@ -75,15 +75,15 @@ function ShowcaseTagList() {
}
function HeadingText() {
const users = useShowcase().items;
const filteredUsers = useFilteredUsers(users);
const {showcaseItems: items} = useShowcase();
const filteredItems = useFilteredItems(items);
const siteCountPlural = useSiteCountPlural();
return (
<div className={styles.headingText}>
<Heading as="h2">
<Translate id="showcase.filters.title">Filters</Translate>
</Heading>
<span>{siteCountPlural(filteredUsers.length)}</span>
<span>{siteCountPlural(filteredItems.length)}</span>
</div>
);
}

View file

@ -8,7 +8,6 @@
import React, {useCallback, type ReactNode, useId} from 'react';
import {useTags} from '@docusaurus/plugin-content-showcase/client';
import type {Props} from '@theme/Showcase/ShowcaseTagSelect';
import styles from './styles.module.css';
function useTagState(tag: string) {

View file

@ -6,12 +6,10 @@
*/
import Translate, {translate} from '@docusaurus/Translate';
import Link from '@docusaurus/Link';
import {ShowcaseProvider} from '@docusaurus/theme-common/internal';
import Layout from '@theme/Layout';
import Heading from '@theme/Heading';
import ShowcaseSearchBar from '@theme/Showcase/ShowcaseSearchBar';
import ShowcaseCards from '@theme/Showcase/ShowcaseCards';
import ShowcaseFilters from '@theme/Showcase/ShowcaseFilters';
@ -39,7 +37,7 @@ function ShowcaseHeader() {
export default function Showcase(props: Props): JSX.Element {
return (
<ShowcaseProvider content={{items: props.items}}>
<ShowcaseProvider content={props.items} tags={props.tags}>
<Layout title={TITLE} description={DESCRIPTION}>
<main className="margin-vert--lg">
<ShowcaseHeader />

View file

@ -7,31 +7,40 @@
import React, {useMemo, type ReactNode, useContext} from 'react';
import {ReactContextError} from '../utils/reactUtils';
import type {ShowcaseItems} from '@docusaurus/plugin-content-showcase';
import type {
ShowcaseItem,
TagsOption,
} from '@docusaurus/plugin-content-showcase';
const Context = React.createContext<ShowcaseItems | null>(null);
const Context = React.createContext<{
showcaseItems: ShowcaseItem[];
tags: TagsOption;
} | null>(null);
function useContextValue(content: ShowcaseItems): ShowcaseItems {
return useMemo(
() => ({
items: content.items,
}),
[content],
);
function useContextValue(
content: ShowcaseItem[],
tags: TagsOption,
): {showcaseItems: ShowcaseItem[]; tags: TagsOption} {
return useMemo(() => ({showcaseItems: content, tags}), [content, tags]);
}
export function ShowcaseProvider({
children,
content,
tags,
}: {
children: ReactNode;
content: ShowcaseItems;
content: ShowcaseItem[];
tags: TagsOption;
}): JSX.Element {
const contextValue = useContextValue(content);
const contextValue = useContextValue(content, tags);
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}
export function useShowcase(): ShowcaseItems {
export function useShowcase(): {
showcaseItems: ShowcaseItem[];
tags: TagsOption;
} {
const showcase = useContext(Context);
if (showcase === null) {
throw new ReactContextError('ShowcaseProvider');