Compare commits
24 Commits
main
...
ozaki/show
| Author | SHA1 | Date |
|---|---|---|
|
|
46c57d6cd5 | |
|
|
cdb7c07bdc | |
|
|
e147034d2e | |
|
|
c42c15eb32 | |
|
|
bed9bb3063 | |
|
|
30599c362e | |
|
|
022efa3653 | |
|
|
8bee535ffa | |
|
|
993efd235c | |
|
|
984518efaf | |
|
|
6e40a79b6f | |
|
|
49b67463bd | |
|
|
9790934ec4 | |
|
|
1c825e02d2 | |
|
|
dddb8cfc78 | |
|
|
c542c81cb8 | |
|
|
7daa9a1d04 | |
|
|
8d1b174917 | |
|
|
ec35998bde | |
|
|
7cf981c262 | |
|
|
a2516dc31a | |
|
|
cd3908b36f | |
|
|
f6b0d4622c | |
|
|
2b37b51b36 |
|
|
@ -0,0 +1,3 @@
|
|||
.tsbuildinfo*
|
||||
tsconfig*
|
||||
__tests__
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# `@docusaurus/plugin-content-showcase`
|
||||
|
||||
Showcase plugin for Docusaurus.
|
||||
|
||||
## Usage
|
||||
|
||||
See [plugin-content-showcase documentation](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-showcase).
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "@docusaurus/plugin-content-showcase",
|
||||
"version": "3.0.0",
|
||||
"description": "Showcase plugin for Docusaurus.",
|
||||
"main": "lib/index.js",
|
||||
"types": "src/plugin-content-showcase.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/facebook/docusaurus.git",
|
||||
"directory": "packages/docusaurus-plugin-content-showcase"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.0.0",
|
||||
"@docusaurus/types": "3.0.0",
|
||||
"@docusaurus/utils": "3.0.0",
|
||||
"@docusaurus/utils-validation": "3.0.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"tslib": "^2.6.0",
|
||||
"webpack": "^5.88.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
}
|
||||
}
|
||||
21
packages/docusaurus-plugin-content-showcase/src/__tests__/__fixtures__/website/docusaurus.config.js
generated
Normal file
21
packages/docusaurus-plugin-content-showcase/src/__tests__/__fixtures__/website/docusaurus.config.js
generated
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
title: 'My Site',
|
||||
tagline: 'The tagline of my site',
|
||||
url: 'https://your-docusaurus-site.example.com',
|
||||
baseUrl: '/',
|
||||
favicon: 'img/favicon.ico',
|
||||
markdown: {
|
||||
parseFrontMatter: async (params) => {
|
||||
const result = await params.defaultParseFrontMatter(params);
|
||||
result.frontMatter.custom_frontMatter = 'added by parseFrontMatter';
|
||||
return result;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
title: "Hello"
|
||||
description: "World"
|
||||
preview: github.com/ozakione.png
|
||||
website: "https://docusaurus.io/"
|
||||
source: "https://github.com/facebook/docusaurus"
|
||||
tags: ["opensource", "meta"]
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`docusaurus-plugin-content-showcase loads simple showcase 1`] = `
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"description": "World",
|
||||
"preview": "github.com/ozakione.png",
|
||||
"source": "https://github.com/facebook/docusaurus",
|
||||
"tags": [
|
||||
"opensource",
|
||||
"meta",
|
||||
],
|
||||
"title": "Hello",
|
||||
"website": "https://docusaurus.io/",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* 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 {escapeRegexp} from '@docusaurus/utils';
|
||||
import {validateShowcaseFrontMatter} from '../frontMatter';
|
||||
import type {ShowcaseFrontMatter} from '@docusaurus/plugin-content-showcase';
|
||||
|
||||
function testField(params: {
|
||||
prefix: string;
|
||||
validFrontMatters: ShowcaseFrontMatter[];
|
||||
convertibleFrontMatter?: [
|
||||
ConvertibleFrontMatter: {[key: string]: unknown},
|
||||
ConvertedFrontMatter: ShowcaseFrontMatter,
|
||||
][];
|
||||
invalidFrontMatters?: [
|
||||
InvalidFrontMatter: {[key: string]: unknown},
|
||||
ErrorMessage: string,
|
||||
][];
|
||||
}) {
|
||||
// eslint-disable-next-line jest/require-top-level-describe
|
||||
test(`[${params.prefix}] accept valid values`, () => {
|
||||
params.validFrontMatters.forEach((frontMatter) => {
|
||||
expect(validateShowcaseFrontMatter(frontMatter)).toEqual(frontMatter);
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/require-top-level-describe
|
||||
test(`[${params.prefix}] convert valid values`, () => {
|
||||
params.convertibleFrontMatter?.forEach(
|
||||
([convertibleFrontMatter, convertedFrontMatter]) => {
|
||||
expect(validateShowcaseFrontMatter(convertibleFrontMatter)).toEqual(
|
||||
convertedFrontMatter,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/require-top-level-describe
|
||||
test(`[${params.prefix}] throw error for values`, () => {
|
||||
params.invalidFrontMatters?.forEach(([frontMatter, message]) => {
|
||||
try {
|
||||
validateShowcaseFrontMatter(frontMatter);
|
||||
throw new Error(
|
||||
`Doc front matter is expected to be rejected, but was accepted successfully:\n ${JSON.stringify(
|
||||
frontMatter,
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect((err as Error).message).toMatch(
|
||||
new RegExp(escapeRegexp(message)),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('doc front matter schema', () => {
|
||||
it('accepts valid frontmatter', () => {
|
||||
const frontMatter: ShowcaseFrontMatter = {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
preview: 'preview',
|
||||
source: 'source',
|
||||
tags: [],
|
||||
website: 'website',
|
||||
};
|
||||
expect(validateShowcaseFrontMatter(frontMatter)).toEqual(frontMatter);
|
||||
});
|
||||
|
||||
it('reject invalid frontmatter', () => {
|
||||
const frontMatter = {};
|
||||
expect(() =>
|
||||
validateShowcaseFrontMatter(frontMatter),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`""title" is required. "description" is required. "preview" is required. "website" is required. "source" is required. "tags" is required"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateShowcaseFrontMatter full', () => {
|
||||
testField({
|
||||
prefix: 'valid full frontmatter',
|
||||
validFrontMatters: [
|
||||
{
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
preview: 'preview',
|
||||
source: 'source',
|
||||
tags: [],
|
||||
website: 'website',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* 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 path from 'path';
|
||||
import {loadContext} from '@docusaurus/core/src/server/site';
|
||||
import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
||||
|
||||
import pluginContentPages from '../index';
|
||||
import {validateOptions} from '../options';
|
||||
|
||||
describe('docusaurus-plugin-content-showcase', () => {
|
||||
it('loads simple showcase', async () => {
|
||||
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
||||
const context = await loadContext({siteDir});
|
||||
const plugin = pluginContentPages(
|
||||
context,
|
||||
validateOptions({
|
||||
validate: normalizePluginOptions,
|
||||
options: {
|
||||
path: 'src/showcase',
|
||||
},
|
||||
}),
|
||||
);
|
||||
const showcaseMetadata = await plugin.loadContent!();
|
||||
|
||||
expect(showcaseMetadata).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
* 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 {normalizePluginOptions} from '@docusaurus/utils-validation';
|
||||
import {validateOptions, DEFAULT_OPTIONS} from '../options';
|
||||
import type {Options} from '@docusaurus/plugin-content-showcase';
|
||||
|
||||
function testValidate(options: Options) {
|
||||
return validateOptions({validate: normalizePluginOptions, options});
|
||||
}
|
||||
const defaultOptions = {
|
||||
...DEFAULT_OPTIONS,
|
||||
id: 'default',
|
||||
};
|
||||
|
||||
describe('normalizeShowcasePluginOptions', () => {
|
||||
it('returns default options for undefined user options', () => {
|
||||
expect(testValidate({})).toEqual(defaultOptions);
|
||||
});
|
||||
|
||||
it('fills in default options for partially defined user options', () => {
|
||||
expect(testValidate({path: 'src/foo'})).toEqual({
|
||||
...defaultOptions,
|
||||
path: 'src/foo',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts correctly defined user options', () => {
|
||||
const userOptions = {
|
||||
path: 'src/showcase',
|
||||
routeBasePath: '/showcase',
|
||||
include: ['**/*.{yaml,yml}'],
|
||||
exclude: ['**/$*/'],
|
||||
};
|
||||
expect(testValidate(userOptions)).toEqual({
|
||||
...defaultOptions,
|
||||
...userOptions,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects bad path inputs', () => {
|
||||
expect(() => {
|
||||
testValidate({
|
||||
// @ts-expect-error: bad attribute
|
||||
path: 42,
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(`""path" must be a string"`);
|
||||
});
|
||||
|
||||
it('empty routeBasePath replace default path("/")', () => {
|
||||
expect(
|
||||
testValidate({
|
||||
routeBasePath: '',
|
||||
}),
|
||||
).toEqual({
|
||||
...defaultOptions,
|
||||
routeBasePath: '/',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts correctly defined tags file options', () => {
|
||||
const userOptions = {
|
||||
tags: '@site/showcase/tags.yaml',
|
||||
};
|
||||
expect(testValidate(userOptions)).toEqual({
|
||||
...defaultOptions,
|
||||
...userOptions,
|
||||
});
|
||||
});
|
||||
|
||||
it('reject badly defined tags file options', () => {
|
||||
const userOptions = {
|
||||
tags: 42,
|
||||
};
|
||||
expect(() =>
|
||||
testValidate(
|
||||
// @ts-expect-error: bad attributes
|
||||
userOptions,
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`""tags" must be one of [string, array]"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts correctly defined tags object options', () => {
|
||||
const userOptions = {
|
||||
tags: [
|
||||
{
|
||||
label: 'foo',
|
||||
description: {
|
||||
message: 'bar',
|
||||
id: 'baz',
|
||||
},
|
||||
color: 'red',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(testValidate(userOptions)).toEqual({
|
||||
...defaultOptions,
|
||||
...userOptions,
|
||||
});
|
||||
});
|
||||
|
||||
it('reject bedly defined tags object options', () => {
|
||||
const userOptions = {
|
||||
tags: [
|
||||
{
|
||||
label: 'foo',
|
||||
description: {
|
||||
message: 'bar',
|
||||
id: 'baz',
|
||||
},
|
||||
color: 42,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(() =>
|
||||
testValidate(
|
||||
// @ts-expect-error: bad attributes
|
||||
userOptions,
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(`""tags[0].color" must be a string"`);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* 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 {Joi, validateFrontMatter} from '@docusaurus/utils-validation';
|
||||
import type {ShowcaseFrontMatter} from '@docusaurus/plugin-content-showcase';
|
||||
|
||||
const showcaseFrontMatterSchema = Joi.object({
|
||||
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 validateShowcaseFrontMatter(frontMatter: {
|
||||
[key: string]: unknown;
|
||||
}): ShowcaseFrontMatter {
|
||||
return validateFrontMatter(frontMatter, showcaseFrontMatterSchema);
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
/**
|
||||
* 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 fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import {
|
||||
getFolderContainingFile,
|
||||
getPluginI18nPath,
|
||||
Globby,
|
||||
} from '@docusaurus/utils';
|
||||
import Yaml from 'js-yaml';
|
||||
|
||||
import {Joi} from '@docusaurus/utils-validation';
|
||||
import {validateShowcaseFrontMatter} from './frontMatter';
|
||||
import {tagSchema} from './options';
|
||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||
import type {
|
||||
PluginOptions,
|
||||
ShowcaseItem,
|
||||
TagOption,
|
||||
} from '@docusaurus/plugin-content-showcase';
|
||||
import type {ShowcaseContentPaths} from './types';
|
||||
|
||||
export function getContentPathList(
|
||||
contentPaths: ShowcaseContentPaths,
|
||||
): string[] {
|
||||
return [contentPaths.contentPathLocalized, contentPaths.contentPath];
|
||||
}
|
||||
|
||||
async function getTagsDefinition(
|
||||
filePath: string | TagOption[],
|
||||
): Promise<string[]> {
|
||||
if (Array.isArray(filePath)) {
|
||||
return filePath.map((tag) => tag.label);
|
||||
}
|
||||
|
||||
const rawYaml = await fs.readFile(filePath, 'utf-8');
|
||||
const unsafeYaml: any = Yaml.load(rawYaml);
|
||||
console.log('unsafeYaml:', unsafeYaml);
|
||||
|
||||
const transformedData = unsafeYaml.tags.map((item: any) => {
|
||||
const [label] = Object.keys(item); // Extract label from object key
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
const {description, color} = item[label]; // Extract description and color
|
||||
return {label, description, color}; // Create new object with transformed structure
|
||||
});
|
||||
console.log('transformedData:', transformedData);
|
||||
|
||||
const safeYaml = tagSchema.validate(transformedData);
|
||||
|
||||
if (safeYaml.error) {
|
||||
throw new Error(`Invalid tags.yaml file: ${safeYaml.error.message}`);
|
||||
}
|
||||
|
||||
const tagLabels = safeYaml.value.map((tag: any) => Object.keys(tag)[0]);
|
||||
return tagLabels;
|
||||
}
|
||||
|
||||
function createTagSchema(tags: string[]): Joi.Schema {
|
||||
return Joi.alternatives().try(
|
||||
Joi.string().valid(...tags), // Schema for single string
|
||||
Joi.array().items(Joi.string().valid(...tags)), // Schema for array of strings
|
||||
);
|
||||
}
|
||||
|
||||
function validateFrontMatterTags(
|
||||
frontMatterTags: string[],
|
||||
tagListSchema: Joi.Schema,
|
||||
): void {
|
||||
const result = tagListSchema.validate(frontMatterTags);
|
||||
if (result.error) {
|
||||
throw new Error(
|
||||
`Front matter contains invalid tags: ${result.error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function pluginContentShowcase(
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Plugin<ShowcaseItem | null> {
|
||||
const {siteDir, localizationDir} = context;
|
||||
|
||||
const contentPaths: ShowcaseContentPaths = {
|
||||
contentPath: path.resolve(siteDir, options.path),
|
||||
contentPathLocalized: getPluginI18nPath({
|
||||
localizationDir,
|
||||
pluginName: 'docusaurus-plugin-content-pages',
|
||||
pluginId: options.id,
|
||||
}),
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'docusaurus-plugin-content-showcase',
|
||||
|
||||
// todo doesn't work
|
||||
// getPathsToWatch() {
|
||||
// const {include} = options;
|
||||
// return getContentPathList(contentPaths).flatMap((contentPath) =>
|
||||
// include.map((pattern) => `${contentPath}/${pattern}`),
|
||||
// );
|
||||
// },
|
||||
|
||||
async loadContent(): Promise<ShowcaseItem | null> {
|
||||
const {include} = options;
|
||||
|
||||
if (!(await fs.pathExists(contentPaths.contentPath))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// const {baseUrl} = siteConfig;
|
||||
const showcaseFiles = await Globby(include, {
|
||||
cwd: contentPaths.contentPath,
|
||||
ignore: options.exclude,
|
||||
});
|
||||
|
||||
const filteredShowcaseFiles = showcaseFiles.filter(
|
||||
(source) => source !== 'tags.yaml',
|
||||
);
|
||||
|
||||
// todo refactor ugly
|
||||
const tagFilePath = path.join(
|
||||
await getFolderContainingFile(
|
||||
getContentPathList(contentPaths),
|
||||
'tags.yaml',
|
||||
),
|
||||
'tags.yaml',
|
||||
);
|
||||
|
||||
const tagList = await getTagsDefinition(tagFilePath);
|
||||
const createdTagSchema = createTagSchema(tagList);
|
||||
console.log('createdTagSchema:', createdTagSchema.describe());
|
||||
|
||||
async function processShowcaseSourceFile(relativeSource: string) {
|
||||
// Lookup in localized folder in priority
|
||||
const contentPath = await getFolderContainingFile(
|
||||
getContentPathList(contentPaths),
|
||||
relativeSource,
|
||||
);
|
||||
|
||||
const sourcePath = path.join(contentPath, relativeSource);
|
||||
|
||||
const rawYaml = await fs.readFile(sourcePath, 'utf-8');
|
||||
const unsafeYaml = Yaml.load(rawYaml) as {[key: string]: unknown};
|
||||
const yaml = validateShowcaseFrontMatter(unsafeYaml);
|
||||
|
||||
validateFrontMatterTags(yaml.tags, createdTagSchema);
|
||||
|
||||
return yaml;
|
||||
}
|
||||
|
||||
async function doProcessShowcaseSourceFile(relativeSource: string) {
|
||||
try {
|
||||
return await processShowcaseSourceFile(relativeSource);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Processing of page source file path=${relativeSource} failed.`,
|
||||
{cause: err as Error},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
items: await Promise.all(
|
||||
filteredShowcaseFiles.map(doProcessShowcaseSourceFile),
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
async contentLoaded({content, actions}) {
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {addRoute, createData} = actions;
|
||||
|
||||
const showcaseAllData = await createData(
|
||||
'showcaseAll.json',
|
||||
JSON.stringify(content.items),
|
||||
);
|
||||
|
||||
addRoute({
|
||||
path: '/showcaseAll',
|
||||
component: '@theme/Showcase',
|
||||
modules: {
|
||||
content: showcaseAllData,
|
||||
// img: '@site/src/showcase/website/ozaki/aot.jpg',
|
||||
},
|
||||
exact: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export {validateOptions} from './options';
|
||||
|
|
@ -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 type {LoaderContext} from 'webpack';
|
||||
|
||||
export default function markdownLoader(
|
||||
this: LoaderContext<undefined>,
|
||||
fileString: string,
|
||||
): void {
|
||||
const callback = this.async();
|
||||
|
||||
// const options = this.getOptions();
|
||||
|
||||
// TODO provide additional md processing here? like interlinking pages?
|
||||
// fileString = linkify(fileString)
|
||||
|
||||
return callback(null, fileString);
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* 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 {Joi, RouteBasePathSchema} from '@docusaurus/utils-validation';
|
||||
import {GlobExcludeDefault} from '@docusaurus/utils';
|
||||
import type {OptionValidationContext} from '@docusaurus/types';
|
||||
import type {PluginOptions, Options} from '@docusaurus/plugin-content-showcase';
|
||||
|
||||
export const DEFAULT_OPTIONS: PluginOptions = {
|
||||
id: 'showcase',
|
||||
path: 'showcase', // Path to data on filesystem, relative to site dir.
|
||||
routeBasePath: '/', // URL Route.
|
||||
include: ['**/*.{yml,yaml}'], // Extensions to include.
|
||||
exclude: GlobExcludeDefault,
|
||||
tags: '@site/showcase/tags.yaml',
|
||||
};
|
||||
|
||||
export const tagSchema = Joi.array().items(
|
||||
Joi.object({
|
||||
label: Joi.string().required(),
|
||||
description: Joi.object({
|
||||
message: Joi.string().required(),
|
||||
id: Joi.string().required(),
|
||||
}).required(),
|
||||
color: Joi.string().required(),
|
||||
}),
|
||||
);
|
||||
|
||||
const PluginOptionSchema = Joi.object<PluginOptions>({
|
||||
path: Joi.string().default(DEFAULT_OPTIONS.path),
|
||||
routeBasePath: RouteBasePathSchema.default(DEFAULT_OPTIONS.routeBasePath),
|
||||
include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include),
|
||||
exclude: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.exclude),
|
||||
id: Joi.string().default(DEFAULT_OPTIONS.id),
|
||||
tags: Joi.alternatives()
|
||||
.try(Joi.string().default(DEFAULT_OPTIONS.tags), tagSchema)
|
||||
.default(DEFAULT_OPTIONS.tags),
|
||||
});
|
||||
|
||||
export function validateOptions({
|
||||
validate,
|
||||
options,
|
||||
}: OptionValidationContext<Options, PluginOptions>): PluginOptions {
|
||||
const validatedOptions = validate(PluginOptionSchema, options);
|
||||
return validatedOptions;
|
||||
}
|
||||
60
packages/docusaurus-plugin-content-showcase/src/plugin-content-showcase.d.ts
vendored
Normal file
60
packages/docusaurus-plugin-content-showcase/src/plugin-content-showcase.d.ts
vendored
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare module '@docusaurus/plugin-content-showcase' {
|
||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||
|
||||
export type TagOption = {
|
||||
label: string;
|
||||
description: {
|
||||
message: string;
|
||||
id: string;
|
||||
};
|
||||
color: string;
|
||||
};
|
||||
|
||||
export type PluginOptions = {
|
||||
id?: string;
|
||||
path: string;
|
||||
routeBasePath: string;
|
||||
include: string[];
|
||||
exclude: string[];
|
||||
tags: string | TagOption[];
|
||||
};
|
||||
|
||||
type TagType =
|
||||
| 'favorite'
|
||||
| 'opensource'
|
||||
| 'product'
|
||||
| 'design'
|
||||
| 'i18n'
|
||||
| 'versioning'
|
||||
| 'large'
|
||||
| 'meta'
|
||||
| 'personal'
|
||||
| 'rtl';
|
||||
|
||||
export type ShowcaseFrontMatter = {
|
||||
readonly title: string;
|
||||
readonly description: string;
|
||||
readonly preview: string | null; // null = use our serverless screenshot service
|
||||
readonly website: string;
|
||||
readonly source: string | null;
|
||||
readonly tags: TagType[];
|
||||
};
|
||||
|
||||
export type ShowcaseItem = {
|
||||
items: ShowcaseFrontMatter[];
|
||||
};
|
||||
|
||||
export type Options = Partial<PluginOptions>;
|
||||
|
||||
export default function pluginContentShowcase(
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Promise<Plugin<ShowcaseItem | null>>;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
export type ShowcaseContentPaths = {
|
||||
contentPath: string;
|
||||
contentPathLocalized: string;
|
||||
};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"incremental": true,
|
||||
"tsBuildInfoFile": "./lib/.tsbuildinfo",
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["**/__tests__/**"]
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@
|
|||
"@docusaurus/plugin-content-blog": "3.0.0",
|
||||
"@docusaurus/plugin-content-docs": "3.0.0",
|
||||
"@docusaurus/plugin-content-pages": "3.0.0",
|
||||
"@docusaurus/plugin-content-showcase": "3.0.0",
|
||||
"@docusaurus/theme-common": "3.0.0",
|
||||
"@docusaurus/theme-translations": "3.0.0",
|
||||
"@docusaurus/types": "3.0.0",
|
||||
|
|
@ -41,6 +42,7 @@
|
|||
"postcss": "^8.4.26",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"react-popper": "^2.3.0",
|
||||
"react-router-dom": "^5.3.4",
|
||||
"rtlcss": "^4.1.0",
|
||||
"tslib": "^2.6.0",
|
||||
|
|
|
|||
|
|
@ -247,6 +247,96 @@ 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['website'][number];
|
||||
|
||||
export type Props = {
|
||||
content: User;
|
||||
};
|
||||
|
||||
export default function Showcase(props: Props): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/Showcase' {
|
||||
import type {ShowcaseItem} from '@docusaurus/plugin-content-showcase';
|
||||
|
||||
export type User = ShowcaseItem['website'][number];
|
||||
|
||||
export type Props = {
|
||||
content: User[];
|
||||
};
|
||||
|
||||
export default function Showcase(props: Props): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/Showcase/ShowcaseCard' {
|
||||
export type User = {
|
||||
title: string;
|
||||
description: string;
|
||||
preview: string | null; // null = use our serverless screenshot service
|
||||
website: string;
|
||||
source: string | null;
|
||||
tags: TagType[];
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
readonly user: User;
|
||||
}
|
||||
|
||||
export default function ShowcaseCard(props: Props): 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 ReactNode, type ReactElement} from 'react';
|
||||
|
||||
export interface Props extends ComponentProps<'input'> {
|
||||
icon: ReactElement<ComponentProps<'svg'>>;
|
||||
label: ReactNode;
|
||||
tag: TagType;
|
||||
}
|
||||
|
||||
export default function ShowcaseTagSelect(props: Props): JSX.Element;
|
||||
}
|
||||
declare module '@theme/Showcase/ShowcaseFilterToggle' {
|
||||
export type Operator = 'OR' | 'AND';
|
||||
|
||||
export default function ShowcaseFilterToggle(): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/Showcase/FavoriteIcon' {
|
||||
import {type ReactNode, type ComponentProps} from 'react';
|
||||
|
||||
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 type Props = Omit<SvgIconProps, 'children'>;
|
||||
|
||||
export default function FavoriteIcon(props: Props): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/BlogPostItem/Container' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* 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 from 'react';
|
||||
import clsx from 'clsx';
|
||||
import type {Props, SvgIconProps} 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;
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox={viewBox}
|
||||
color={colorAttr}
|
||||
aria-hidden
|
||||
className={clsx(styles.svgIcon, styles[color], styles[size], svgClass)}
|
||||
{...rest}>
|
||||
{children}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.svgIcon {
|
||||
user-select: none;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
display: inline-block;
|
||||
fill: currentColor;
|
||||
flex-shrink: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* font-size */
|
||||
.small {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.medium {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
/**
|
||||
* 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 from 'react';
|
||||
import clsx from 'clsx';
|
||||
import Link from '@docusaurus/Link';
|
||||
import Translate, {translate} from '@docusaurus/Translate';
|
||||
import FavoriteIcon from '@theme/Showcase/FavoriteIcon';
|
||||
import Heading from '@theme/Heading';
|
||||
import Tooltip from '@theme/Showcase/ShowcaseTooltip';
|
||||
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 = {
|
||||
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}>
|
||||
<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]}));
|
||||
|
||||
// Keep same order for all tags
|
||||
const tagObjectsSorted = sortBy(tagObjects, (tagObject) =>
|
||||
TagList.indexOf(tagObject.tag),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{tagObjectsSorted.map((tagObject, index) => {
|
||||
const id = `showcase_card_tag_${tagObject.tag}`;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={index}
|
||||
text={tagObject.description}
|
||||
anchorEl="#__docusaurus"
|
||||
id={id}>
|
||||
<TagComp key={index} {...tagObject} />
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getCardImage(user: User): string {
|
||||
return (
|
||||
user.preview ??
|
||||
`https://slorber-api-screenshot.netlify.app/${encodeURIComponent(
|
||||
user.website,
|
||||
)}/showcase`
|
||||
);
|
||||
}
|
||||
|
||||
function ShowcaseCard({user}: {user: User}) {
|
||||
const image = getCardImage(user);
|
||||
return (
|
||||
<li key={user.title} className="card shadow--md">
|
||||
<div className={clsx('card__image', styles.showcaseCardImage)}>
|
||||
<img src={image} alt={user.title} />
|
||||
</div>
|
||||
<div className="card__body">
|
||||
<div className={clsx(styles.showcaseCardHeader)}>
|
||||
<Heading as="h4" className={styles.showcaseCardTitle}>
|
||||
<Link href={user.website} className={styles.showcaseCardLink}>
|
||||
{user.title}
|
||||
</Link>
|
||||
</Heading>
|
||||
{user.tags.includes('favorite') && (
|
||||
<FavoriteIcon svgClass={styles.svgIconFavorite} size="small" />
|
||||
)}
|
||||
{user.source && (
|
||||
<Link
|
||||
href={user.source}
|
||||
className={clsx(
|
||||
'button button--secondary button--sm',
|
||||
styles.showcaseCardSrcBtn,
|
||||
)}>
|
||||
<Translate id="showcase.card.sourceLink">source</Translate>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<p className={styles.showcaseCardBody}>{user.description}</p>
|
||||
</div>
|
||||
<ul className={clsx('card__footer', styles.cardFooter)}>
|
||||
<ShowcaseCardTag tags={user.tags} />
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ShowcaseCard);
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
||||
.showcaseCardImage {
|
||||
overflow: hidden;
|
||||
height: 150px;
|
||||
border-bottom: 2px solid var(--ifm-color-emphasis-200);
|
||||
}
|
||||
|
||||
.showcaseCardHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.showcaseCardTitle {
|
||||
margin-bottom: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.showcaseCardTitle a {
|
||||
text-decoration: none;
|
||||
background: linear-gradient(
|
||||
var(--ifm-color-primary),
|
||||
var(--ifm-color-primary)
|
||||
)
|
||||
0% 100% / 0% 1px no-repeat;
|
||||
transition: background-size ease-out 200ms;
|
||||
}
|
||||
|
||||
.showcaseCardTitle a:not(:focus):hover {
|
||||
background-size: 100% 1px;
|
||||
}
|
||||
|
||||
.showcaseCardTitle,
|
||||
.showcaseCardHeader .svgIconFavorite {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.showcaseCardHeader .svgIconFavorite {
|
||||
color: var(--site-color-svg-icon-favorite);
|
||||
}
|
||||
|
||||
.showcaseCardSrcBtn {
|
||||
margin-left: 6px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.showcaseCardSrcBtn:focus-visible {
|
||||
background-color: var(--ifm-color-secondary-dark);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .showcaseCardSrcBtn {
|
||||
background-color: var(--ifm-color-emphasis-200) !important;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .showcaseCardSrcBtn:hover {
|
||||
background-color: var(--ifm-color-emphasis-300) !important;
|
||||
}
|
||||
|
||||
.showcaseCardBody {
|
||||
font-size: smaller;
|
||||
line-height: 1.66;
|
||||
}
|
||||
|
||||
.cardFooter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 0.675rem;
|
||||
border: 1px solid var(--ifm-color-secondary-darkest);
|
||||
cursor: default;
|
||||
margin-right: 6px;
|
||||
margin-bottom: 6px !important;
|
||||
border-radius: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tag .textLabel {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.tag .colorLabel {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
margin-left: 6px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* 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,57 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.checkboxLabel {
|
||||
--height: 25px;
|
||||
--width: 80px;
|
||||
--border: 2px;
|
||||
display: flex;
|
||||
width: var(--width);
|
||||
height: var(--height);
|
||||
position: relative;
|
||||
border-radius: var(--height);
|
||||
border: var(--border) solid var(--ifm-color-primary-darkest);
|
||||
cursor: pointer;
|
||||
justify-content: space-around;
|
||||
opacity: 0.75;
|
||||
transition: opacity var(--ifm-transition-fast)
|
||||
var(--ifm-transition-timing-default);
|
||||
box-shadow: var(--ifm-global-shadow-md);
|
||||
}
|
||||
|
||||
.checkboxLabel:hover {
|
||||
opacity: 1;
|
||||
box-shadow: var(--ifm-global-shadow-md),
|
||||
0 0 2px 1px var(--ifm-color-primary-dark);
|
||||
}
|
||||
|
||||
.checkboxLabel::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
inset: 0;
|
||||
width: calc(var(--width) / 2);
|
||||
height: 100%;
|
||||
border-radius: var(--height);
|
||||
background-color: var(--ifm-color-primary-darkest);
|
||||
transition: transform var(--ifm-transition-fast)
|
||||
var(--ifm-transition-timing-default);
|
||||
transform: translateX(calc(var(--width) / 2 - var(--border)));
|
||||
}
|
||||
|
||||
input:focus-visible ~ .checkboxLabel::after {
|
||||
outline: 2px solid currentColor;
|
||||
}
|
||||
|
||||
.checkboxLabel > * {
|
||||
font-size: 0.8rem;
|
||||
color: inherit;
|
||||
transition: opacity 150ms ease-in 50ms;
|
||||
}
|
||||
|
||||
input:checked ~ .checkboxLabel::after {
|
||||
transform: translateX(calc(-1 * var(--border)));
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
/**
|
||||
* 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, {
|
||||
useCallback,
|
||||
useState,
|
||||
useEffect,
|
||||
type ComponentProps,
|
||||
type ReactNode,
|
||||
type ReactElement,
|
||||
} from 'react';
|
||||
import {useHistory, useLocation} from '@docusaurus/router';
|
||||
|
||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
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(),
|
||||
});
|
||||
}, [tag, location, history]);
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={id}
|
||||
className="screen-reader-only"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
toggleTag();
|
||||
}
|
||||
}}
|
||||
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}
|
||||
{icon}
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.forwardRef(ShowcaseTagSelect);
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.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: var(--site-color-checkbox-checked-bg);
|
||||
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);
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
/**
|
||||
* 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, {useEffect, useState, useRef} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {usePopper} from 'react-popper';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
interface Props {
|
||||
anchorEl?: HTMLElement | string;
|
||||
id: string;
|
||||
text: string;
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
export default function Tooltip({
|
||||
children,
|
||||
id,
|
||||
anchorEl,
|
||||
text,
|
||||
}: Props): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLElement | null>(
|
||||
null,
|
||||
);
|
||||
const [popperElement, setPopperElement] = useState<HTMLElement | null>(null);
|
||||
const [arrowElement, setArrowElement] = useState<HTMLElement | null>(null);
|
||||
const [container, setContainer] = useState<Element | null>(null);
|
||||
const {styles: popperStyles, attributes} = usePopper(
|
||||
referenceElement,
|
||||
popperElement,
|
||||
{
|
||||
modifiers: [
|
||||
{
|
||||
name: 'arrow',
|
||||
options: {
|
||||
element: arrowElement,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 8],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const timeout = useRef<number | null>(null);
|
||||
const tooltipId = `${id}_tooltip`;
|
||||
|
||||
useEffect(() => {
|
||||
if (anchorEl) {
|
||||
if (typeof anchorEl === 'string') {
|
||||
setContainer(document.querySelector(anchorEl));
|
||||
} else {
|
||||
setContainer(anchorEl);
|
||||
}
|
||||
} else {
|
||||
setContainer(document.body);
|
||||
}
|
||||
}, [container, anchorEl]);
|
||||
|
||||
useEffect(() => {
|
||||
const showEvents = ['mouseenter', 'focus'];
|
||||
const hideEvents = ['mouseleave', 'blur'];
|
||||
|
||||
const handleOpen = () => {
|
||||
// There is no point in displaying an empty tooltip.
|
||||
if (text === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the title ahead of time to avoid displaying
|
||||
// two tooltips at the same time (native + this one).
|
||||
referenceElement?.removeAttribute('title');
|
||||
|
||||
timeout.current = window.setTimeout(() => {
|
||||
setOpen(true);
|
||||
}, 400);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
clearInterval(timeout.current!);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
if (referenceElement) {
|
||||
showEvents.forEach((event) => {
|
||||
referenceElement.addEventListener(event, handleOpen);
|
||||
});
|
||||
|
||||
hideEvents.forEach((event) => {
|
||||
referenceElement.addEventListener(event, handleClose);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (referenceElement) {
|
||||
showEvents.forEach((event) => {
|
||||
referenceElement.removeEventListener(event, handleOpen);
|
||||
});
|
||||
|
||||
hideEvents.forEach((event) => {
|
||||
referenceElement.removeEventListener(event, handleClose);
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [referenceElement, text]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{React.cloneElement(children, {
|
||||
ref: setReferenceElement,
|
||||
'aria-describedby': open ? tooltipId : undefined,
|
||||
})}
|
||||
{container
|
||||
? ReactDOM.createPortal(
|
||||
open && (
|
||||
<div
|
||||
id={tooltipId}
|
||||
role="tooltip"
|
||||
ref={setPopperElement}
|
||||
className={styles.tooltip}
|
||||
style={popperStyles.popper}
|
||||
{...attributes.popper}>
|
||||
{text}
|
||||
<span
|
||||
ref={setArrowElement}
|
||||
className={styles.tooltipArrow}
|
||||
style={popperStyles.arrow}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
container,
|
||||
)
|
||||
: container}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.tooltip {
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
color: var(--site-color-tooltip);
|
||||
background: var(--site-color-tooltip-background);
|
||||
font-size: 0.8rem;
|
||||
z-index: 500;
|
||||
line-height: 1.4;
|
||||
font-weight: 500;
|
||||
max-width: 300px;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.tooltipArrow {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.tooltipArrow,
|
||||
.tooltipArrow::before {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
.tooltipArrow::before {
|
||||
visibility: visible;
|
||||
content: '';
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.tooltip[data-popper-placement^='top'] > .tooltipArrow {
|
||||
bottom: -4px;
|
||||
}
|
||||
|
||||
.tooltip[data-popper-placement^='bottom'] > .tooltipArrow {
|
||||
top: -4px;
|
||||
}
|
||||
|
|
@ -0,0 +1,468 @@
|
|||
/**
|
||||
* 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, {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 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 ShowcaseTooltip from '@theme/Showcase/ShowcaseTooltip';
|
||||
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[];
|
||||
|
||||
const TITLE = translate({message: 'Docusaurus Site Showcase'});
|
||||
const DESCRIPTION = translate({
|
||||
message: 'List of websites people are building with Docusaurus',
|
||||
});
|
||||
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">
|
||||
<Heading as="h1">{TITLE}</Heading>
|
||||
<p>{DESCRIPTION}</p>
|
||||
<Link className="button button--primary" to={SUBMIT_URL}>
|
||||
<Translate id="showcase.header.button">
|
||||
🙏 Please add your site
|
||||
</Translate>
|
||||
</Link>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
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, description, color} = Tags[tag];
|
||||
const id = `showcase_checkbox_id_${tag}`;
|
||||
|
||||
return (
|
||||
<li key={i} className={styles.checkboxListItem}>
|
||||
<ShowcaseTooltip
|
||||
id={id}
|
||||
text={description}
|
||||
anchorEl="#__docusaurus">
|
||||
<ShowcaseTagSelect
|
||||
tag={tag}
|
||||
id={id}
|
||||
label={label}
|
||||
icon={
|
||||
tag === 'favorite' ? (
|
||||
<FavoriteIcon svgClass={styles.svgIconFavoriteXs} />
|
||||
) : (
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: '50%',
|
||||
marginLeft: 8,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</ShowcaseTooltip>
|
||||
</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">
|
||||
<div>{JSON.stringify(props)}</div>
|
||||
<main className="margin-vert--lg">
|
||||
<ShowcaseHeader />
|
||||
<ShowcaseFilters users={users} />
|
||||
<div
|
||||
style={{display: 'flex', marginLeft: 'auto'}}
|
||||
className="container">
|
||||
<SearchBar />
|
||||
</div>
|
||||
<ShowcaseCards users={users} />
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* 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;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -68,6 +68,7 @@ Datagit's
|
|||
dedup
|
||||
devto
|
||||
dingers
|
||||
dinooooooo
|
||||
Dmitry
|
||||
docsearch
|
||||
Docsify
|
||||
|
|
@ -105,6 +106,7 @@ Flightcontrol's
|
|||
Formik
|
||||
FOUC
|
||||
froms
|
||||
frontmatter
|
||||
funboxteam
|
||||
gabrielcsapo
|
||||
Gifs
|
||||
|
|
@ -228,7 +230,9 @@ orta
|
|||
Outerbounds
|
||||
outerbounds
|
||||
overrideable
|
||||
Ozaki
|
||||
ozaki
|
||||
ozakione
|
||||
O’Shannessy
|
||||
pageview
|
||||
Palenight
|
||||
|
|
|
|||
|
|
@ -239,6 +239,7 @@ export default async function createConfigAsync() {
|
|||
],
|
||||
themes: ['live-codeblock', ...dogfoodingThemeInstances],
|
||||
plugins: [
|
||||
'content-showcase',
|
||||
[
|
||||
'./src/plugins/changelog/index.js',
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
title: Clement;
|
||||
description: Description from frontmatter
|
||||
preview: https://github.com/ozakione.png
|
||||
website: https://github.com/ozakione
|
||||
source: source
|
||||
tags:
|
||||
- favorite
|
||||
- opensource
|
||||
---
|
||||
|
||||
# Hello
|
||||
|
||||
- some test
|
||||
|
||||
text
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
title: 'Dinosaur'
|
||||
description: 'Docusaurus dinooooooo'
|
||||
preview: https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/Grosser_Panda.JPG/2560px-Grosser_Panda.JPG
|
||||
website: 'https://agile-ts.org/'
|
||||
source: 'https://github.com/agile-ts/documentation'
|
||||
tags: ['opensource', 'design']
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
title: 'Ozaki'
|
||||
description: 'Ozaki website'
|
||||
preview: /img/aot.jpg
|
||||
website: 'https://agile-ts.org/'
|
||||
source: 'https://github.com/agile-ts/documentation'
|
||||
tags: ['opensource', 'design']
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
title: 'Seb'
|
||||
description: "Docusaurus maintainer's personal website"
|
||||
preview: require('./showcase/test.png')
|
||||
website: 'https://agile-ts.org/'
|
||||
source: 'https://github.com/agile-ts/documentation'
|
||||
tags: ['opensource', 'design']
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
tags:
|
||||
- favorite:
|
||||
label: 'Favorite'
|
||||
description:
|
||||
message: 'Our favorite Docusaurus sites that you must absolutely check out!'
|
||||
id: 'showcase.tag.favorite.description'
|
||||
color: '#e9669e'
|
||||
- opensource:
|
||||
label: 'Open-Source'
|
||||
description:
|
||||
message: 'Open-Source Docusaurus sites can be useful for inspiration!'
|
||||
id: 'showcase.tag.opensource.description'
|
||||
color: '#39ca30'
|
||||
- product:
|
||||
label: 'Product'
|
||||
description:
|
||||
message: 'Docusaurus sites associated to a commercial product!'
|
||||
id: 'showcase.tag.product.description'
|
||||
color: '#dfd545'
|
||||
- design:
|
||||
label: 'Design'
|
||||
description:
|
||||
message: 'Beautiful Docusaurus sites, polished and standing out from the initial template!'
|
||||
id: 'showcase.tag.design.description'
|
||||
color: '#a44fb7'
|
||||
- i18n:
|
||||
label: 'I18n'
|
||||
description:
|
||||
message: 'Translated Docusaurus sites using the internationalization support with more than 1 locale.'
|
||||
id: 'showcase.tag.i18n.description'
|
||||
color: '#127f82'
|
||||
- versioning:
|
||||
label: 'Versioning'
|
||||
description:
|
||||
message: 'Docusaurus sites using the versioning feature of the docs plugin to manage multiple versions.'
|
||||
id: 'showcase.tag.versioning.description'
|
||||
color: '#fe6829'
|
||||
- large:
|
||||
label: 'Large'
|
||||
description:
|
||||
message: 'Very large Docusaurus sites, including many more pages than the average!'
|
||||
id: 'showcase.tag.large.description'
|
||||
color: '#8c2f00'
|
||||
- meta:
|
||||
label: 'Meta'
|
||||
description:
|
||||
message: 'Docusaurus sites of Meta (formerly Facebook) projects'
|
||||
id: 'showcase.tag.meta.description'
|
||||
color: '#4267b2'
|
||||
- personal:
|
||||
label: 'Personal'
|
||||
description:
|
||||
message: 'Personal websites, blogs and digital gardens built with Docusaurus'
|
||||
id: 'showcase.tag.personal.description'
|
||||
color: '#14cfc3'
|
||||
- rtl:
|
||||
label: 'RTL Direction'
|
||||
description:
|
||||
message: 'Docusaurus sites using the right-to-left reading direction support.'
|
||||
id: 'showcase.tag.rtl.description'
|
||||
color: '#ffcfc3'
|
||||
Loading…
Reference in New Issue