mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-31 18:07:00 +02:00
feat(website): new plugin to load CHANGELOG and render as blog (#6414)
* feat(website): new plugin to load CHANGELOG and render as blog * use CJS * move footer links * better design * fixes * correctly order posts * add authors * Add axios * Update styles * oops * oops * add expand button * back to index page link * fix styles * add feed options * fix * fix * Add fallback * fix * fixes
This commit is contained in:
parent
5c447b1ca3
commit
f6ff6474bc
24 changed files with 845 additions and 48 deletions
158
website/src/plugins/changelog/index.js
Normal file
158
website/src/plugins/changelog/index.js
Normal file
|
@ -0,0 +1,158 @@
|
|||
/**
|
||||
* 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 import/no-extraneous-dependencies */
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
const pluginContentBlog = require('@docusaurus/plugin-content-blog');
|
||||
const {aliasedSitePath, docuHash, normalizeUrl} = require('@docusaurus/utils');
|
||||
const syncAvatars = require('./syncAvatars');
|
||||
|
||||
/**
|
||||
* Multiple versions may be published on the same day, causing the order to be
|
||||
* the reverse. Therefore, our publish time has a "fake hour" to order them.
|
||||
*/
|
||||
const publishTimes = new Set();
|
||||
/** @type {Record<string, {name: string, url: string, alias: string, imageURL: string}>} */
|
||||
const authorsMap = {};
|
||||
|
||||
/**
|
||||
* @param {string} section
|
||||
*/
|
||||
function processSection(section) {
|
||||
const title = section
|
||||
.match(/\n## .*/)?.[0]
|
||||
.trim()
|
||||
.replace('## ', '');
|
||||
if (!title) {
|
||||
return null;
|
||||
}
|
||||
const content = section
|
||||
.replace(/\n## .*/, '')
|
||||
.trim()
|
||||
.replace('running_woman', 'running');
|
||||
|
||||
let authors = content.match(/## Committers: \d+.*/ms);
|
||||
if (authors) {
|
||||
authors = authors[0]
|
||||
.match(/- .*/g)
|
||||
.map(
|
||||
(line) =>
|
||||
line.match(
|
||||
/- (?:(?<name>.*?) \()?\[@(?<alias>.*)\]\((?<url>.*?)\)\)?/,
|
||||
).groups,
|
||||
)
|
||||
.map((author) => ({
|
||||
...author,
|
||||
name: author.name ?? author.alias,
|
||||
imageURL: `./img/${author.alias}.png`,
|
||||
}))
|
||||
.sort((a, b) => a.url.localeCompare(b.url));
|
||||
|
||||
authors.forEach((author) => {
|
||||
authorsMap[author.alias] = author;
|
||||
});
|
||||
}
|
||||
let hour = 20;
|
||||
const date = title.match(/ \((.*)\)/)[1];
|
||||
while (publishTimes.has(`${date}T${hour}:00`)) {
|
||||
hour -= 1;
|
||||
}
|
||||
publishTimes.add(`${date}T${hour}:00`);
|
||||
|
||||
return {
|
||||
title: title.replace(/ \(.*\)/, ''),
|
||||
content: `---
|
||||
date: ${`${date}T${hour}:00`}${
|
||||
authors
|
||||
? `
|
||||
authors:
|
||||
${authors.map((author) => ` - '${author.alias}'`).join('\n')}`
|
||||
: ''
|
||||
}
|
||||
---
|
||||
|
||||
# ${title.replace(/ \(.*\)/, '')}
|
||||
|
||||
<!-- truncate -->
|
||||
|
||||
${content.replace(/####/g, '##')}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@docusaurus/types').LoadContext} context
|
||||
* @returns {import('@docusaurus/types').Plugin}
|
||||
*/
|
||||
async function ChangelogPlugin(context, options) {
|
||||
const generateDir = path.join(context.siteDir, 'changelog/source');
|
||||
const blogPlugin = await pluginContentBlog.default(context, {
|
||||
...options,
|
||||
path: generateDir,
|
||||
id: 'changelog',
|
||||
blogListComponent: '@theme/ChangelogList',
|
||||
blogPostComponent: '@theme/ChangelogPage',
|
||||
});
|
||||
const changelogPath = path.join(__dirname, '../../../../CHANGELOG.md');
|
||||
return {
|
||||
...blogPlugin,
|
||||
name: 'changelog-plugin',
|
||||
async loadContent() {
|
||||
const fileContent = await fs.readFile(changelogPath, 'utf-8');
|
||||
const sections = fileContent
|
||||
.split(/(?=\n## )/ms)
|
||||
.map(processSection)
|
||||
.filter(Boolean);
|
||||
await Promise.all(
|
||||
sections.map((section) =>
|
||||
fs.outputFile(
|
||||
path.join(generateDir, `${section.title}.md`),
|
||||
section.content,
|
||||
),
|
||||
),
|
||||
);
|
||||
await syncAvatars(authorsMap, generateDir);
|
||||
const content = await blogPlugin.loadContent();
|
||||
content.blogPosts.forEach((post, index) => {
|
||||
const pageIndex = Math.floor(index / options.postsPerPage);
|
||||
post.metadata.listPageLink = normalizeUrl([
|
||||
context.baseUrl,
|
||||
options.routeBasePath,
|
||||
pageIndex === 0 ? '/' : `/page/${pageIndex + 1}`,
|
||||
]);
|
||||
});
|
||||
return content;
|
||||
},
|
||||
configureWebpack(...args) {
|
||||
const config = blogPlugin.configureWebpack(...args);
|
||||
const pluginDataDirRoot = path.join(
|
||||
context.generatedFilesDir,
|
||||
'changelog-plugin',
|
||||
'default',
|
||||
);
|
||||
// Redirect the metadata path to our folder
|
||||
config.module.rules[0].use[1].options.metadataPath = (mdxPath) => {
|
||||
// Note that metadataPath must be the same/in-sync as
|
||||
// the path from createData for each MDX.
|
||||
const aliasedPath = aliasedSitePath(mdxPath, context.siteDir);
|
||||
return path.join(pluginDataDirRoot, `${docuHash(aliasedPath)}.json`);
|
||||
};
|
||||
return config;
|
||||
},
|
||||
getThemePath() {
|
||||
return path.join(__dirname, './theme');
|
||||
},
|
||||
getPathsToWatch() {
|
||||
// Don't watch the generated dir
|
||||
return [changelogPath];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ChangelogPlugin.validateOptions = pluginContentBlog.validateOptions;
|
||||
|
||||
module.exports = ChangelogPlugin;
|
88
website/src/plugins/changelog/syncAvatars.js
Normal file
88
website/src/plugins/changelog/syncAvatars.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* 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 import/no-extraneous-dependencies */
|
||||
|
||||
// @ts-check
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
// const axios = require('axios').default;
|
||||
|
||||
// TODO not sure how the syncing should be done at all... for now it always
|
||||
// pretends the limit is reached. We should only fetch a portion of the avatars
|
||||
// at a time. But seems avatars.githubusercontent.com API doesn't like HTTP requests?
|
||||
/**
|
||||
* @param {string} username
|
||||
* @param {Record<string, number>} lastUpdateCache
|
||||
* @param {Record<string, {imageURL: string; url: string}>} authorsMap
|
||||
* @returns true if saved successfully (including not found); false if limited reached
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async function fetchImage(username, lastUpdateCache, authorsMap) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to keep track of all committers that are in the changelog, and fetch
|
||||
* their avatars beforehand. This prevents sending too many requests to GitHub
|
||||
* every time one visits a page. Avatar refreshing is done incrementally across
|
||||
* each build.
|
||||
*
|
||||
* This method mutates the authorsMap. If the avatar fails to be fetched, the
|
||||
* imageURL is re-written with the github remote URL. The authors map is then
|
||||
* written to FS.
|
||||
*
|
||||
* @param {Record<string, {name: string, url: string, alias: string, imageURL: string}>} authorsMap
|
||||
* @param {string} generateDir
|
||||
*/
|
||||
async function syncAvatars(authorsMap, generateDir) {
|
||||
const imagePath = path.join(generateDir, 'img');
|
||||
const lastUpdateCachePath = path.join(imagePath, 'lastUpdate.json');
|
||||
const authorsPath = path.join(generateDir, 'authors.json');
|
||||
if (!(await fs.pathExists(lastUpdateCachePath))) {
|
||||
await fs.outputFile(lastUpdateCachePath, '{}');
|
||||
}
|
||||
/**
|
||||
* Records the last time an avatar was successfully updated.
|
||||
* If an entry doesn't exist, the file won't exist either.
|
||||
* @type {Record<string, number>}
|
||||
*/
|
||||
const lastUpdateCache = await fs.readJSON(lastUpdateCachePath);
|
||||
let limitReached = false;
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const username of Object.keys(authorsMap)) {
|
||||
if (!limitReached && !lastUpdateCache[username]) {
|
||||
if (!(await fetchImage(username, lastUpdateCache, authorsMap))) {
|
||||
limitReached = true;
|
||||
}
|
||||
}
|
||||
if (limitReached) {
|
||||
authorsMap[username].imageURL = `https://github.com/${username}.png`;
|
||||
}
|
||||
}
|
||||
const usersByLastUpdate = Object.entries(lastUpdateCache)
|
||||
.sort((a, b) => a[1] - b[1])
|
||||
.map((a) => a[0]);
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const username of usersByLastUpdate) {
|
||||
if (
|
||||
!limitReached &&
|
||||
lastUpdateCache[username] < Date.now() - 24 * 3600 * 1000
|
||||
) {
|
||||
if (!(await fetchImage(username, lastUpdateCache, authorsMap))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
await fs.outputFile(
|
||||
lastUpdateCachePath,
|
||||
JSON.stringify(lastUpdateCache, null, 2),
|
||||
);
|
||||
await fs.outputFile(authorsPath, JSON.stringify(authorsMap, null, 2));
|
||||
}
|
||||
|
||||
module.exports = syncAvatars;
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* 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 Link from '@docusaurus/Link';
|
||||
import type {Props} from '@theme/BlogPostAuthor';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
function ChangelogAuthor({author}: Props): JSX.Element {
|
||||
const {name, url, imageURL} = author;
|
||||
return (
|
||||
<div className="avatar margin-bottom--sm">
|
||||
{imageURL && (
|
||||
<Link className="avatar__photo-link avatar__photo" href={url}>
|
||||
<img
|
||||
className={styles.image}
|
||||
src={imageURL}
|
||||
alt={name}
|
||||
onError={(e) => {
|
||||
// Image returns 404 if the user's handle changes. We display a fallback instead.
|
||||
e.currentTarget.src =
|
||||
'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" fill="none" stroke="%2325c2a0" stroke-width="30" version="1.1"><circle cx="300" cy="230" r="115"/><path stroke-linecap="butt" d="M106.81863443903,481.4 a205,205 1 0,1 386.36273112194,0"/></svg>';
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangelogAuthor;
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* 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} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import type {Props} from '@theme/BlogPostAuthors';
|
||||
import ChangelogAuthor from '@theme/ChangelogAuthor';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
import IconExpand from '@theme/IconExpand';
|
||||
|
||||
// Component responsible for the authors layout
|
||||
export default function BlogPostAuthors({
|
||||
authors,
|
||||
assets,
|
||||
}: Props): JSX.Element | null {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const authorsCount = authors.length;
|
||||
if (authorsCount === 0) {
|
||||
return null;
|
||||
}
|
||||
const filteredAuthors = authors.slice(0, expanded ? authors.length : 10);
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'margin-top--md margin-bottom--sm',
|
||||
styles.imageOnlyAuthorRow,
|
||||
)}>
|
||||
{filteredAuthors.map((author, idx) => (
|
||||
<div className={styles.imageOnlyAuthorCol} key={idx}>
|
||||
<ChangelogAuthor
|
||||
author={{
|
||||
...author,
|
||||
// Handle author images using relative paths
|
||||
imageURL: assets.authorsImageUrls[idx] ?? author.imageURL,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{authors.length > 10 && (
|
||||
<button
|
||||
className={clsx('clean-btn', styles.toggleButton)}
|
||||
type="button"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
aria-label="expand">
|
||||
<IconExpand expanded={expanded} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
||||
.authorCol {
|
||||
max-width: inherit !important;
|
||||
flex-grow: 1 !important;
|
||||
}
|
||||
|
||||
.imageOnlyAuthorRow {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
.imageOnlyAuthorCol {
|
||||
margin-left: 0.3rem;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
|
||||
.imageOnlyAuthorCol [class^='image'] {
|
||||
background-color: var(--ifm-color-emphasis-100);
|
||||
}
|
||||
|
||||
.toggleButton {
|
||||
margin-left: 0.3rem;
|
||||
margin-right: 0.3rem;
|
||||
border-radius: 50%;
|
||||
width: var(--ifm-avatar-photo-size-md);
|
||||
height: var(--ifm-avatar-photo-size-md);
|
||||
background-color: var(--ifm-color-emphasis-100);
|
||||
}
|
||||
|
||||
.toggleButtonIconExpanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
79
website/src/plugins/changelog/theme/ChangelogItem/index.tsx
Normal file
79
website/src/plugins/changelog/theme/ChangelogItem/index.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* 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';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import {MDXProvider} from '@mdx-js/react';
|
||||
import Link from '@docusaurus/Link';
|
||||
import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
|
||||
import {blogPostContainerID} from '@docusaurus/utils-common';
|
||||
import MDXComponents from '@theme/MDXComponents';
|
||||
import type {Props} from '@theme/BlogPostItem';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
import ChangelogAuthors from '@theme/ChangelogAuthors';
|
||||
|
||||
function ChangelogItem(props: Props): JSX.Element {
|
||||
const {withBaseUrl} = useBaseUrlUtils();
|
||||
const {
|
||||
children,
|
||||
frontMatter,
|
||||
assets,
|
||||
metadata,
|
||||
isBlogPostPage = false,
|
||||
} = props;
|
||||
const {date, formattedDate, permalink, title, authors} = metadata;
|
||||
|
||||
const image = assets.image ?? frontMatter.image;
|
||||
|
||||
const TitleHeading = isBlogPostPage ? 'h1' : 'h2';
|
||||
|
||||
return (
|
||||
<article
|
||||
className={!isBlogPostPage ? 'margin-bottom--md' : undefined}
|
||||
itemProp="blogPost"
|
||||
itemScope
|
||||
itemType="http://schema.org/BlogPosting">
|
||||
<header>
|
||||
<TitleHeading
|
||||
className={clsx(
|
||||
isBlogPostPage ? styles.blogPostPageTitle : styles.blogPostTitle,
|
||||
)}
|
||||
itemProp="headline">
|
||||
{isBlogPostPage ? (
|
||||
title
|
||||
) : (
|
||||
<Link itemProp="url" to={permalink}>
|
||||
{title}
|
||||
</Link>
|
||||
)}
|
||||
</TitleHeading>
|
||||
<div className={clsx(styles.blogPostData, 'margin-vert--md')}>
|
||||
<time dateTime={date} itemProp="datePublished">
|
||||
{formattedDate}
|
||||
</time>
|
||||
</div>
|
||||
<ChangelogAuthors authors={authors} assets={assets} />
|
||||
</header>
|
||||
|
||||
{image && (
|
||||
<meta itemProp="image" content={withBaseUrl(image, {absolute: true})} />
|
||||
)}
|
||||
|
||||
<div
|
||||
// This ID is used for the feed generation to locate the main content
|
||||
id={isBlogPostPage ? blogPostContainerID : undefined}
|
||||
className="markdown"
|
||||
itemProp="articleBody">
|
||||
<MDXProvider components={MDXComponents}>{children}</MDXProvider>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangelogItem;
|
|
@ -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.
|
||||
*/
|
||||
|
||||
.blogPostTitle {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.blogPostPageTitle {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.blogPostData {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.blogPostDetailsFull {
|
||||
flex-direction: column;
|
||||
}
|
92
website/src/plugins/changelog/theme/ChangelogList/index.tsx
Normal file
92
website/src/plugins/changelog/theme/ChangelogList/index.tsx
Normal file
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* 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 BlogLayout from '@theme/BlogLayout';
|
||||
import BlogListPaginator from '@theme/BlogListPaginator';
|
||||
import type {Props} from '@theme/BlogListPage';
|
||||
import {ThemeClassNames} from '@docusaurus/theme-common';
|
||||
import Link from '@docusaurus/Link';
|
||||
import ChangelogItem from '@theme/ChangelogItem';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
function ChangelogList(props: Props): JSX.Element {
|
||||
const {metadata, items, sidebar} = props;
|
||||
const {blogDescription, blogTitle} = metadata;
|
||||
|
||||
return (
|
||||
<BlogLayout
|
||||
title={blogTitle}
|
||||
description={blogDescription}
|
||||
wrapperClassName={ThemeClassNames.wrapper.blogPages}
|
||||
pageClassName={ThemeClassNames.page.blogListPage}
|
||||
searchMetadata={{
|
||||
// assign unique search tag to exclude this page from search results!
|
||||
tag: 'blog_posts_list',
|
||||
}}
|
||||
sidebar={sidebar}>
|
||||
<header className="margin-bottom--lg">
|
||||
<h1 style={{fontSize: '3rem'}}>{blogTitle}</h1>
|
||||
<p>
|
||||
Subscribe through{' '}
|
||||
<Link href="pathname:///changelog/rss.xml" className={styles.rss}>
|
||||
<b>RSS feeds</b>
|
||||
<svg
|
||||
style={{
|
||||
fill: '#f26522',
|
||||
position: 'relative',
|
||||
left: 4,
|
||||
top: 1,
|
||||
marginRight: 8,
|
||||
}}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24">
|
||||
<path d="M6.503 20.752c0 1.794-1.456 3.248-3.251 3.248-1.796 0-3.252-1.454-3.252-3.248 0-1.794 1.456-3.248 3.252-3.248 1.795.001 3.251 1.454 3.251 3.248zm-6.503-12.572v4.811c6.05.062 10.96 4.966 11.022 11.009h4.817c-.062-8.71-7.118-15.758-15.839-15.82zm0-3.368c10.58.046 19.152 8.594 19.183 19.188h4.817c-.03-13.231-10.755-23.954-24-24v4.812z" />
|
||||
</svg>
|
||||
</Link>{' '}
|
||||
or follow us on{' '}
|
||||
<Link
|
||||
href="https://twitter.com/docusaurus"
|
||||
className={styles.twitter}>
|
||||
<b>Twitter</b>
|
||||
<svg
|
||||
style={{
|
||||
fill: '#1da1f2',
|
||||
position: 'relative',
|
||||
left: 4,
|
||||
top: 1,
|
||||
marginRight: 8,
|
||||
}}
|
||||
width="16"
|
||||
height="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512">
|
||||
<path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z" />
|
||||
</svg>
|
||||
</Link>{' '}
|
||||
to stay up-to-date with new releases!
|
||||
</p>
|
||||
</header>
|
||||
{items.map(({content: BlogPostContent}) => (
|
||||
<ChangelogItem
|
||||
key={BlogPostContent.metadata.permalink}
|
||||
frontMatter={BlogPostContent.frontMatter}
|
||||
assets={BlogPostContent.assets}
|
||||
metadata={BlogPostContent.metadata}
|
||||
truncated={BlogPostContent.metadata.truncated}>
|
||||
<BlogPostContent />
|
||||
</ChangelogItem>
|
||||
))}
|
||||
<BlogListPaginator metadata={metadata} />
|
||||
</BlogLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangelogList;
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.blogPostTitle {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.blogPostData {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.blogPostDetailsFull {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.rss,
|
||||
.rss:hover {
|
||||
color: #f26522;
|
||||
}
|
||||
|
||||
.twitter,
|
||||
.twitter:hover {
|
||||
color: #1da1f2;
|
||||
}
|
102
website/src/plugins/changelog/theme/ChangelogPage/index.tsx
Normal file
102
website/src/plugins/changelog/theme/ChangelogPage/index.tsx
Normal file
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* 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 Seo from '@theme/Seo';
|
||||
import BlogLayout from '@theme/BlogLayout';
|
||||
import ChangelogItem from '@theme/ChangelogItem';
|
||||
import BlogPostPaginator from '@theme/BlogPostPaginator';
|
||||
import type {Props} from '@theme/BlogPostPage';
|
||||
import {ThemeClassNames} from '@docusaurus/theme-common';
|
||||
import TOC from '@theme/TOC';
|
||||
import Link from '@docusaurus/Link';
|
||||
|
||||
// This page doesn't change anything. It's just swapping BlogPostItem with our
|
||||
// own ChangelogItem. We don't want to apply the swizzled item to the actual blog.
|
||||
function BlogPostPage(props: Props): JSX.Element {
|
||||
const {content: BlogPostContents, sidebar} = props;
|
||||
const {assets, metadata} = BlogPostContents;
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
nextItem,
|
||||
prevItem,
|
||||
date,
|
||||
tags,
|
||||
authors,
|
||||
frontMatter,
|
||||
// @ts-expect-error: we injected this
|
||||
listPageLink,
|
||||
} = metadata;
|
||||
const {
|
||||
hide_table_of_contents: hideTableOfContents,
|
||||
keywords,
|
||||
toc_min_heading_level: tocMinHeadingLevel,
|
||||
toc_max_heading_level: tocMaxHeadingLevel,
|
||||
} = frontMatter;
|
||||
|
||||
const image = assets.image ?? frontMatter.image;
|
||||
|
||||
return (
|
||||
<BlogLayout
|
||||
wrapperClassName={ThemeClassNames.wrapper.blogPages}
|
||||
pageClassName={ThemeClassNames.page.blogPostPage}
|
||||
sidebar={sidebar}
|
||||
toc={
|
||||
!hideTableOfContents &&
|
||||
BlogPostContents.toc &&
|
||||
BlogPostContents.toc.length > 0 ? (
|
||||
<TOC
|
||||
toc={BlogPostContents.toc}
|
||||
minHeadingLevel={tocMinHeadingLevel}
|
||||
maxHeadingLevel={tocMaxHeadingLevel}
|
||||
/>
|
||||
) : undefined
|
||||
}>
|
||||
<Seo
|
||||
title={title}
|
||||
description={description}
|
||||
keywords={keywords}
|
||||
image={image}>
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="article:published_time" content={date} />
|
||||
|
||||
{authors.some((author) => author.url) && (
|
||||
<meta
|
||||
property="article:author"
|
||||
content={authors
|
||||
.map((author) => author.url)
|
||||
.filter(Boolean)
|
||||
.join(',')}
|
||||
/>
|
||||
)}
|
||||
{tags.length > 0 && (
|
||||
<meta
|
||||
property="article:tag"
|
||||
content={tags.map((tag) => tag.label).join(',')}
|
||||
/>
|
||||
)}
|
||||
</Seo>
|
||||
|
||||
<Link to={listPageLink}>← Back to index page</Link>
|
||||
|
||||
<ChangelogItem
|
||||
frontMatter={frontMatter}
|
||||
assets={assets}
|
||||
metadata={metadata}
|
||||
isBlogPostPage>
|
||||
<BlogPostContents />
|
||||
</ChangelogItem>
|
||||
|
||||
{(nextItem || prevItem) && (
|
||||
<BlogPostPaginator nextItem={nextItem} prevItem={prevItem} />
|
||||
)}
|
||||
</BlogLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlogPostPage;
|
37
website/src/plugins/changelog/theme/IconExpand/index.tsx
Normal file
37
website/src/plugins/changelog/theme/IconExpand/index.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* 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 type {Props} from '@theme/IconExpand';
|
||||
|
||||
function IconExpand({expanded, ...props}: Props): JSX.Element {
|
||||
if (expanded) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="currentColor"
|
||||
{...props}>
|
||||
<path d="M783.915092 1009.031953l-271.898251-277.615587-271.930737 277.550617a49.214558 49.214558 0 0 1-70.752018 0 51.780862 51.780862 0 0 1 0-72.246322l307.274261-313.706262a49.279528 49.279528 0 0 1 70.784503 0l307.33923 313.706262a51.975771 51.975771 0 0 1 0 72.311292 49.409467 49.409467 0 0 1-70.816988 0z m-307.306745-608.05155L169.269117 87.274141A51.975771 51.975771 0 0 1 169.269117 14.96285a49.409467 49.409467 0 0 1 70.816987 0l271.930737 277.615586L783.850122 14.96285a49.409467 49.409467 0 0 1 70.816988 0 51.975771 51.975771 0 0 1 0 72.311291l-307.33923 313.706262a49.376982 49.376982 0 0 1-70.719533 0z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="currentColor"
|
||||
{...props}>
|
||||
<path d="M476.612887 1009.12034L169.240699 695.380437a51.981345 51.981345 0 0 1 0-72.319045 49.382277 49.382277 0 0 1 70.824582 0l271.959897 277.645356 271.862433-277.645356a49.382277 49.382277 0 0 1 70.824582 0 51.981345 51.981345 0 0 1 0 72.319045l-307.307212 313.739903a49.447254 49.447254 0 0 1-70.792094 0z m307.274724-608.116755L511.99269 123.455693l-271.959897 277.645357a49.382277 49.382277 0 0 1-70.824582 0 51.981345 51.981345 0 0 1 0-72.319045L476.580399 15.042102a49.382277 49.382277 0 0 1 70.727117 0l307.372188 313.739903a51.981345 51.981345 0 0 1 0 72.319045 49.414766 49.414766 0 0 1-70.824582 0z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default IconExpand;
|
20
website/src/plugins/changelog/theme/types.d.ts
vendored
Normal file
20
website/src/plugins/changelog/theme/types.d.ts
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* 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 '@theme/ChangelogItem';
|
||||
declare module '@theme/ChangelogAuthors';
|
||||
declare module '@theme/ChangelogAuthor';
|
||||
declare module '@theme/IconExpand' {
|
||||
import type {ComponentProps} from 'react';
|
||||
|
||||
export interface Props extends ComponentProps<'svg'> {
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
const IconExpand: (props: Props) => JSX.Element;
|
||||
export default IconExpand;
|
||||
}
|
38
website/src/plugins/featureRequests/FeatureRequestsPage.tsx
Normal file
38
website/src/plugins/featureRequests/FeatureRequestsPage.tsx
Normal file
|
@ -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.
|
||||
*/
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import Layout from '@theme/Layout';
|
||||
|
||||
import cannyScript from './cannyScript';
|
||||
import clsx from 'clsx';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
const BOARD_TOKEN = '054e0e53-d951-b14c-7e74-9eb8f9ed2f91';
|
||||
|
||||
function FeatureRequests({basePath}: {basePath: string}): JSX.Element {
|
||||
useEffect(() => {
|
||||
cannyScript();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const {Canny} = window as any;
|
||||
Canny('render', {
|
||||
boardToken: BOARD_TOKEN,
|
||||
basePath,
|
||||
});
|
||||
}, [basePath]);
|
||||
|
||||
return (
|
||||
<Layout title="Feedback" description="Docusaurus 2 Feature Requests page">
|
||||
<main
|
||||
className={clsx('container', 'margin-vert--lg', styles.main)}
|
||||
data-canny
|
||||
/>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default FeatureRequests;
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* 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 utils from '@docusaurus/utils';
|
||||
|
||||
/**
|
||||
* @param {import('@docusaurus/types').LoadContext} context
|
||||
* @returns {import('@docusaurus/types').Plugin}
|
||||
*/
|
||||
export default function FeatureRequestsPlugin(context) {
|
||||
return {
|
||||
name: 'feature-requests-plugin',
|
||||
async contentLoaded({actions}) {
|
||||
const basePath = utils.normalizeUrl([
|
||||
context.baseUrl,
|
||||
'/feature-requests',
|
||||
]);
|
||||
const paths = await actions.createData(
|
||||
'paths.json',
|
||||
JSON.stringify(basePath),
|
||||
);
|
||||
actions.addRoute({
|
||||
path: basePath,
|
||||
exact: false,
|
||||
component: '@site/src/plugins/featureRequests/FeatureRequestsPage',
|
||||
modules: {
|
||||
basePath: paths,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
31
website/src/plugins/featureRequests/cannyScript.js
Normal file
31
website/src/plugins/featureRequests/cannyScript.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
// Provided by Canny.
|
||||
/* eslint-disable */
|
||||
|
||||
function cannyScript() {
|
||||
!(function (w, d, i, s) {
|
||||
function l() {
|
||||
if (!d.getElementById(i)) {
|
||||
let f = d.getElementsByTagName(s)[0],
|
||||
e = d.createElement(s);
|
||||
(e.type = 'text/javascript'),
|
||||
(e.async = !0),
|
||||
(e.src = 'https://canny.io/sdk.js'),
|
||||
f.parentNode.insertBefore(e, f);
|
||||
}
|
||||
}
|
||||
if (typeof w.Canny !== 'function') {
|
||||
var c = function () {
|
||||
c.q.push(arguments);
|
||||
};
|
||||
(c.q = []),
|
||||
(w.Canny = c),
|
||||
d.readyState === 'complete'
|
||||
? l()
|
||||
: w.attachEvent
|
||||
? w.attachEvent('onload', l)
|
||||
: w.addEventListener('load', l, !1);
|
||||
}
|
||||
})(window, document, 'canny-jssdk', 'script');
|
||||
}
|
||||
|
||||
export default cannyScript;
|
13
website/src/plugins/featureRequests/styles.module.css
Normal file
13
website/src/plugins/featureRequests/styles.module.css
Normal file
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.main {
|
||||
padding: var(--ifm-spacing-horizontal);
|
||||
border-radius: 4px;
|
||||
background: var(--site-color-feedback-background);
|
||||
min-height: 500px;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue