This commit is contained in:
ozakione 2024-03-24 22:02:40 +01:00
parent ec35998bde
commit 8d1b174917
13 changed files with 160 additions and 115 deletions

View file

@ -36,28 +36,33 @@ export default function pluginContentShowcase(
// }, // },
async loadContent(): Promise<Content> { async loadContent(): Promise<Content> {
const files = await fs.readdir( const files = await fs.readdir(path.join(siteDir, options.path));
path.join(siteDir, options.path, 'website'),
);
const yamlFiles = files.filter((file) => file.endsWith('.yaml')); const yamlFiles = files.filter((file) => file.endsWith('.yaml'));
const contentPromises = yamlFiles.map(async (file) => { const contentPromises = yamlFiles.map(async (file) => {
const yaml = await fs.readFile( const rawYaml = await fs.readFile(
path.join(siteDir, options.path, 'website', file), path.join(siteDir, options.path, file),
'utf-8', 'utf-8',
); );
const authors = Yaml.load(yaml); const yaml = Yaml.load(rawYaml);
const parsedAuthors = contentAuthorsSchema.validate(authors); const parsedYaml = contentAuthorsSchema.validate(yaml);
if (parsedAuthors.error) { if (parsedYaml.error) {
throw new Error(`Validation failed: ${parsedAuthors.error.message}`, { throw new Error(`Validation failed: ${parsedYaml.error.message}`, {
cause: parsedAuthors.error, cause: parsedYaml.error,
}); });
} }
const {title, description, preview, website, source, tags} =
parsedYaml.value;
return { return {
title: parsedAuthors.value.title, title,
author: parsedAuthors.value.author, // Assuming author is part of Content type description,
preview,
website,
source,
tags,
}; };
}); });
@ -71,7 +76,6 @@ export default function pluginContentShowcase(
if (!content) { if (!content) {
return; return;
} }
console.log('content:', content);
const {addRoute, createData} = actions; const {addRoute, createData} = actions;
@ -83,7 +87,7 @@ export default function pluginContentShowcase(
); );
addRoute({ addRoute({
path: `/${item.title}`, path: `/showcaseAll/${item.title}`,
component: '@theme/Showcase', component: '@theme/Showcase',
modules: { modules: {
content: dataAuthor, content: dataAuthor,
@ -92,6 +96,20 @@ export default function pluginContentShowcase(
}); });
}), }),
); );
const showcaseAllData = await createData(
'showcaseAll.json',
JSON.stringify(content.website),
);
addRoute({
path: '/showcaseAll',
component: '@theme/Showcase',
modules: {
content: showcaseAllData,
},
exact: true,
});
}, },
}; };
} }

View file

@ -11,7 +11,7 @@ import type {PluginOptions, Options} from '@docusaurus/plugin-showcase';
export const DEFAULT_OPTIONS: PluginOptions = { export const DEFAULT_OPTIONS: PluginOptions = {
id: 'showcase', id: 'showcase',
path: 'src/showcase', // Path to data on filesystem, relative to site dir. path: 'src/showcase/website', // Path to data on filesystem, relative to site dir.
routeBasePath: '/', // URL Route. routeBasePath: '/', // URL Route.
}; };
@ -21,8 +21,12 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
}); });
export const contentAuthorsSchema = Joi.object({ export const contentAuthorsSchema = Joi.object({
author: Joi.string().required(),
title: Joi.string().required(), title: Joi.string().required(),
description: Joi.string().required(),
preview: Joi.string().required(),
website: Joi.string().required(),
source: Joi.string().required(),
tags: Joi.array().items(Joi.string()).required(),
}); });
export function validateOptions({ export function validateOptions({

View file

@ -18,10 +18,26 @@ declare module '@docusaurus/plugin-showcase' {
routeBasePath: string; routeBasePath: string;
}; };
export type TagType =
| 'favorite'
| 'opensource'
| 'product'
| 'design'
| 'i18n'
| 'versioning'
| 'large'
| 'meta'
| 'personal'
| 'rtl';
export type Content = { export type Content = {
website: { website: {
author: string;
title: string; title: string;
description: string;
preview: string | null; // null = use our serverless screenshot service
website: string;
source: string | null;
tags: TagType[];
}[]; }[];
}; };
@ -30,5 +46,5 @@ declare module '@docusaurus/plugin-showcase' {
export default function pluginShowcase( export default function pluginShowcase(
context: LoadContext, context: LoadContext,
options: PluginOptions, options: PluginOptions,
): Promise<Plugin<LoadedContent | null>>; ): Promise<Plugin<Content | null>>;
} }

View file

@ -26,6 +26,7 @@
"@docusaurus/plugin-content-blog": "3.0.0", "@docusaurus/plugin-content-blog": "3.0.0",
"@docusaurus/plugin-content-docs": "3.0.0", "@docusaurus/plugin-content-docs": "3.0.0",
"@docusaurus/plugin-content-pages": "3.0.0", "@docusaurus/plugin-content-pages": "3.0.0",
"@docusaurus/plugin-showcase": "3.0.0",
"@docusaurus/theme-common": "3.0.0", "@docusaurus/theme-common": "3.0.0",
"@docusaurus/theme-translations": "3.0.0", "@docusaurus/theme-translations": "3.0.0",
"@docusaurus/types": "3.0.0", "@docusaurus/types": "3.0.0",

View file

@ -248,14 +248,16 @@ declare module '@theme/BlogPostItems' {
} }
declare module '@theme/Showcase' { declare module '@theme/Showcase' {
export interface Props { import type {Content} from '@docusaurus/plugin-showcase';
content: {
[key: string]: string;
};
}
export function prepareUserState(): UserState | undefined; export function prepareUserState(): UserState | undefined;
export type User = Content['website'][number];
export type Props = {
content: User[];
};
export default function Showcase(props: Props): JSX.Element; export default function Showcase(props: Props): JSX.Element;
} }

View file

@ -9,12 +9,28 @@ import React, {useState, useEffect, useCallback} from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import {useHistory, useLocation} from '@docusaurus/router'; import {useHistory, useLocation} from '@docusaurus/router';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import type {Operator} from '@theme/Showcase/ShowcaseFilterToggle'; import type {Operator} from '@theme/Showcase/ShowcaseFilterToggle';
import {OperatorQueryKey} from '@theme/Showcase/ShowcaseFilterToggle'; import {OperatorQueryKey} from '@theme/Showcase/ShowcaseFilterToggle';
import {prepareUserState} from '@theme/Showcase';
import styles from './styles.module.css'; 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 readOperator(search: string): Operator { function readOperator(search: string): Operator {
return (new URLSearchParams(search).get(OperatorQueryKey) ?? return (new URLSearchParams(search).get(OperatorQueryKey) ??
'OR') as Operator; 'OR') as Operator;

View file

@ -15,9 +15,25 @@ import React, {
} from 'react'; } from 'react';
import {useHistory, useLocation} from '@docusaurus/router'; import {useHistory, useLocation} from '@docusaurus/router';
import {prepareUserState} from '@theme/Showcase'; import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import styles from './styles.module.css'; 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[] { function toggleListItem<T>(list: T[], item: T): T[] {
const itemIndex = list.indexOf(item); const itemIndex = list.indexOf(item);
if (itemIndex === -1) { if (itemIndex === -1) {

View file

@ -12,27 +12,32 @@ import {useHistory, useLocation} from 'react-router-dom';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import Translate, {translate} from '@docusaurus/Translate'; import Translate, {translate} from '@docusaurus/Translate';
import {usePluralForm} from '@docusaurus/theme-common'; import {usePluralForm} from '@docusaurus/theme-common';
import type {Props} from '@theme/Showcase'; 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 FavoriteIcon from '@theme/Showcase/FavoriteIcon';
import ShowcaseCard from '@theme/Showcase/ShowcaseCard'; import ShowcaseCard from '@theme/Showcase/ShowcaseCard';
import ShowcaseTooltip from '@theme/Showcase/ShowcaseTooltip'; import ShowcaseTooltip from '@theme/Showcase/ShowcaseTooltip';
import ShowcaseTagSelect, { import ShowcaseTagSelect from '@theme/Showcase/ShowcaseTagSelect';
readSearchTags, import ShowcaseFilterToggle from '@theme/Showcase/ShowcaseFilterToggle';
} from '@theme/Showcase/ShowcaseTagSelect';
import ShowcaseFilterToggle, {
readOperator,
} from '@theme/Showcase/ShowcaseFilterToggle';
import type {Operator} from '@theme/Showcase/ShowcaseFilterToggle'; import type {Operator} from '@theme/Showcase/ShowcaseFilterToggle';
import type {TagType} from '@docusaurus/plugin-showcase';
import styles from './styles.module.css'; import styles from './styles.module.css';
type Users = User[];
const TITLE = translate({message: 'Docusaurus Site Showcase'}); const TITLE = translate({message: 'Docusaurus Site Showcase'});
const DESCRIPTION = translate({ const DESCRIPTION = translate({
message: 'List of websites people are building with Docusaurus', message: 'List of websites people are building with Docusaurus',
}); });
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 = { type UserState = {
scrollTopPosition: number; scrollTopPosition: number;
focusedElementId: string | undefined; focusedElementId: string | undefined;
@ -145,17 +150,6 @@ const Tags: {[type in TagType]: Tag} = {
const TagList = Object.keys(Tags) as TagType[]; const TagList = Object.keys(Tags) as TagType[];
const Users: User[] = [
{
title: 'AgileTs',
description: 'Global State and Logic Framework for reactive Applications',
preview: 'https://github.com/ozakione.png',
website: 'https://agile-ts.org/',
source: 'https://github.com/agile-ts/documentation',
tags: ['opensource', 'design'],
},
];
function sortBy<T>( function sortBy<T>(
array: T[], array: T[],
getter: (item: T) => string | number | boolean, getter: (item: T) => string | number | boolean,
@ -168,17 +162,14 @@ function sortBy<T>(
return sortedArray; return sortedArray;
} }
function sortUsers() { function sortUsers(users: Users): Users {
let result = Users;
// Sort by site name // Sort by site name
result = sortBy(result, (user) => user.title.toLowerCase()); let result = sortBy(users, (user) => user.title.toLowerCase());
// Sort by favorite tag, favorites first // Sort by favorite tag, favorites first
result = sortBy(result, (user) => !user.tags.includes('favorite')); result = sortBy(result, (user) => (user.tags.includes('favorite') ? -1 : 1));
return result; return result;
} }
const sortedUsers = sortUsers();
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">
@ -213,35 +204,9 @@ function restoreUserState(userState: UserState | null) {
document.getElementById(focusedElementId)?.focus(); document.getElementById(focusedElementId)?.focus();
window.scrollTo({top: scrollTopPosition}); window.scrollTo({top: scrollTopPosition});
} }
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;
// description: string;
// color: string;
// };
function filterUsers( function filterUsers(
users: User[], users: Users,
selectedTags: TagType[], selectedTags: TagType[],
operator: Operator, operator: Operator,
searchName: string | null, searchName: string | null,
@ -266,7 +231,19 @@ function filterUsers(
}); });
} }
function useFilteredUsers() { 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 location = useLocation<UserState>();
const [operator, setOperator] = useState<Operator>('OR'); const [operator, setOperator] = useState<Operator>('OR');
// On SSR / first mount (hydration) no tag is selected // On SSR / first mount (hydration) no tag is selected
@ -282,8 +259,8 @@ function useFilteredUsers() {
}, [location]); }, [location]);
return useMemo( return useMemo(
() => filterUsers(sortedUsers, selectedTags, operator, searchName), () => filterUsers(sortUsers(users), selectedTags, operator, searchName),
[selectedTags, operator, searchName], [selectedTags, operator, searchName, users],
); );
} }
@ -304,8 +281,8 @@ function useSiteCountPlural() {
); );
} }
function ShowcaseFilters() { function ShowcaseFilters({users}: {users: Users}) {
const filteredUsers = useFilteredUsers(); const filteredUsers = useFilteredUsers(users);
const siteCountPlural = useSiteCountPlural(); const siteCountPlural = useSiteCountPlural();
return ( return (
<section className="container margin-top--l margin-bottom--lg"> <section className="container margin-top--l margin-bottom--lg">
@ -358,12 +335,6 @@ function ShowcaseFilters() {
); );
} }
const SearchNameQueryKey = 'name';
function readSearchName(search: string) {
return new URLSearchParams(search).get(SearchNameQueryKey);
}
function SearchBar() { function SearchBar() {
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
@ -401,15 +372,8 @@ function SearchBar() {
); );
} }
const favoriteUsers = sortedUsers.filter((user) => function ShowcaseCards({users}: {users: Users}) {
user.tags.includes('favorite'), const filteredUsers = useFilteredUsers(users);
);
const otherUsers = sortedUsers.filter(
(user) => !user.tags.includes('favorite'),
);
function ShowcaseCards() {
const filteredUsers = useFilteredUsers();
if (filteredUsers.length === 0) { if (filteredUsers.length === 0) {
return ( return (
@ -423,9 +387,12 @@ function ShowcaseCards() {
); );
} }
const favoriteUsers = users.filter((user) => user.tags.includes('favorite'));
const otherUsers = users.filter((user) => !user.tags.includes('favorite'));
return ( return (
<section className="margin-top--lg margin-bottom--xl"> <section className="margin-top--lg margin-bottom--xl">
{filteredUsers.length === sortedUsers.length ? ( {filteredUsers.length === sortUsers(users).length ? (
<> <>
<div className={styles.showcaseFavorite}> <div className={styles.showcaseFavorite}>
<div className="container"> <div className="container">
@ -481,18 +448,21 @@ function ShowcaseCards() {
} }
export default function Showcase(props: Props): JSX.Element { export default function Showcase(props: Props): JSX.Element {
// TODO remove temporary to test showcase specific page
const users = Array.isArray(props.content) ? props.content : [props.content];
return ( return (
<Layout title="Showcase"> <Layout title="Showcase">
<div>{JSON.stringify(props)}</div> <div>{JSON.stringify(props)}</div>
<main className="margin-vert--lg"> <main className="margin-vert--lg">
<ShowcaseHeader /> <ShowcaseHeader />
<ShowcaseFilters /> <ShowcaseFilters users={users} />
<div <div
style={{display: 'flex', marginLeft: 'auto'}} style={{display: 'flex', marginLeft: 'auto'}}
className="container"> className="container">
<SearchBar /> <SearchBar />
</div> </div>
<ShowcaseCards /> <ShowcaseCards users={users} />
</main> </main>
</Layout> </Layout>
); );

View file

@ -1,12 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* eslint-disable @docusaurus/no-untranslated-text */
import React from 'react';
export default function ShowcaseComponent(props) {
return <div>Your friends are : {props.content.author}</div>;
}

View file

@ -0,0 +1,6 @@
title: 'Dinosaur'
description: 'Docusaurus dino'
preview: require('./showcase/agilets.png')
website: 'https://agile-ts.org/'
source: 'https://github.com/agile-ts/documentation'
tags: ['opensource', 'design']

View file

@ -1,2 +0,0 @@
author: ozaki
title: ozaki

View file

@ -0,0 +1,6 @@
title: 'Ozaki'
description: 'Ozaki website'
preview: require('./showcase/agilets.png')
website: 'https://agile-ts.org/'
source: 'https://github.com/agile-ts/documentation'
tags: ['opensource', 'design']

View file

@ -1,2 +1,6 @@
author: seb title: 'Seb'
title: seb description: "Docusaurus maintainer's personal website"
preview: require('./showcase/agilets.png')
website: 'https://agile-ts.org/'
source: 'https://github.com/agile-ts/documentation'
tags: ['opensource', 'design']