This is an automated email from the ASF dual-hosted git repository.

guoqi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-website.git


The following commit(s) were added to refs/heads/master by this push:
     new 8b75fe12d3d feat: apisix docs version with badge (#1248)
8b75fe12d3d is described below

commit 8b75fe12d3dbf6fbb817c96fa8845c02d3465b00
Author: Young <[email protected]>
AuthorDate: Fri Jul 29 14:54:52 2022 +0800

    feat: apisix docs version with badge (#1248)
---
 .eslintrc.js                                       |  40 ++-----
 config/apisix-versions.js                          |   7 +-
 doc/src/theme/DocSidebar/index.tsx                 |  44 ++++----
 .../NavbarItem/DocsVersionDropdownNavbarItem.tsx   | 123 +++++++++++++++++++++
 doc/src/theme/NavbarItem/index.tsx                 |  64 +++++++++++
 doc/src/theme/NavbarItem/style.module.scss         |  17 +++
 6 files changed, 237 insertions(+), 58 deletions(-)

diff --git a/.eslintrc.js b/.eslintrc.js
index afeaab67102..3212fe5ca0b 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -9,11 +9,7 @@ module.exports = {
     es2021: true,
     node: true,
   },
-  extends: [
-    'airbnb',
-    'plugin:react/recommended',
-    'plugin:@typescript-eslint/recommended',
-  ],
+  extends: ['airbnb', 'plugin:react/recommended', 
'plugin:@typescript-eslint/recommended'],
   parser: '@typescript-eslint/parser',
   parserOptions: {
     ecmaFeatures: {
@@ -36,25 +32,13 @@ module.exports = {
     quotes: [ERROR, 'single', { allowTemplateLiterals: true }],
     'no-unused-vars': OFF,
     '@typescript-eslint/no-unused-vars': [ERROR, { ignoreRestSiblings: true }],
-    '@typescript-eslint/ban-ts-comment': [
-      ERROR,
-      { 'ts-expect-error': 'allow-with-description' },
-    ],
-    '@typescript-eslint/consistent-indexed-object-style': [
-      WARNING,
-      'index-signature',
-    ],
-    '@typescript-eslint/consistent-type-imports': [
-      WARNING,
-      { disallowTypeAnnotations: false },
-    ],
+    '@typescript-eslint/ban-ts-comment': [ERROR, { 'ts-expect-error': 
'allow-with-description' }],
+    '@typescript-eslint/consistent-indexed-object-style': [WARNING, 
'index-signature'],
+    '@typescript-eslint/consistent-type-imports': [WARNING, { 
disallowTypeAnnotations: false }],
     '@typescript-eslint/explicit-module-boundary-types': WARNING,
     '@typescript-eslint/method-signature-style': ERROR,
     '@typescript-eslint/no-empty-function': OFF,
-    '@typescript-eslint/no-empty-interface': [
-      ERROR,
-      { allowSingleExtends: true },
-    ],
+    '@typescript-eslint/no-empty-interface': [ERROR, { allowSingleExtends: 
true }],
     '@typescript-eslint/no-inferrable-types': OFF,
     '@typescript-eslint/no-namespace': [WARNING, { allowDeclarations: true }],
     'no-use-before-define': OFF,
@@ -70,22 +54,14 @@ module.exports = {
     'import/no-unresolved': [
       ERROR,
       {
-        ignore: [
-          '^@theme',
-          '^@docusaurus',
-          '^@generated',
-          '^@site',
-          '^@testing-utils',
-        ],
+        ignore: ['^@theme', '^@docusaurus', '^@generated', '^@site', 
'^@testing-utils'],
       },
     ],
     'no-lonely-if': OFF,
     'no-lone-blocks': OFF,
     'react/jsx-filename-extension': [ERROR, { extensions: ['.jsx', '.tsx'] }],
-    'import/extensions': [
-      ERROR,
-      { tsx: 'never', svg: 'always', json: 'never' },
-    ],
+    'import/extensions': [ERROR, { tsx: 'never', svg: 'always', json: 'never' 
}],
+    'import/no-relative-packages': OFF,
     'react/jsx-props-no-spreading': OFF,
     'react/function-component-definition': [
       ERROR,
diff --git a/config/apisix-versions.js b/config/apisix-versions.js
index 5f7da584299..6d3289247cf 100644
--- a/config/apisix-versions.js
+++ b/config/apisix-versions.js
@@ -5,6 +5,11 @@
  */
 const versions = ['2.12', '2.13', '2.14', '2.15'];
 
+/**
+ * @type {Array<string>} LTS version list
+ */
+const LTSVersions = ['2.13'];
+
 /**
  * @type {Array<{label: string, href: string}>}
  */
@@ -43,4 +48,4 @@ const archivedVersions = [
   },
 ];
 
-module.exports = { versions, archivedVersions };
+module.exports = { versions, LTSVersions, archivedVersions };
diff --git a/doc/src/theme/DocSidebar/index.tsx 
b/doc/src/theme/DocSidebar/index.tsx
index 904df878ed1..fb9f0ba0ee8 100644
--- a/doc/src/theme/DocSidebar/index.tsx
+++ b/doc/src/theme/DocSidebar/index.tsx
@@ -5,10 +5,10 @@
  * LICENSE file in the root directory of this source tree.
  */
 
+/* eslint-disable import/no-extraneous-dependencies, max-len */
 import type { FC } from 'react';
 import React, { useState } from 'react';
 import clsx from 'clsx';
-// eslint-disable-next-line import/no-extraneous-dependencies
 import {
   useThemeConfig,
   useAnnouncementBar,
@@ -23,6 +23,7 @@ import { translate } from '@docusaurus/Translate';
 import { DocSidebarItems } from '@theme/DocSidebarItem';
 import DocsVersionDropdownNavbarItem from 
'@theme/NavbarItem/DocsVersionDropdownNavbarItem';
 import type { Props } from '@theme/DocSidebar';
+// eslint-disable-next-line import/no-unresolved
 import { archivedVersions } from '../../../../config/apisix-versions';
 
 import styles from './styles.module.css';
@@ -38,7 +39,7 @@ function useShowAnnouncementBar() {
   return showAnnouncementBar;
 }
 
-const HideableSidebarButton = ({ onClick }: {onClick: 
React.MouseEventHandler}) => (
+const HideableSidebarButton = ({ onClick }: { onClick: React.MouseEventHandler 
}) => (
   <button
     type="button"
     title={translate({
@@ -51,17 +52,14 @@ const HideableSidebarButton = ({ onClick }: {onClick: 
React.MouseEventHandler})
       message: 'Collapse sidebar',
       description: 'The title attribute for collapse button of doc sidebar',
     })}
-    className={clsx(
-      'button button--secondary button--outline',
-      styles.collapseSidebarButton,
-    )}
+    className={clsx('button button--secondary button--outline', 
styles.collapseSidebarButton)}
     onClick={onClick}
   >
     <IconArrow className={styles.collapseSidebarButtonIcon} />
   </button>
 );
 
-const DocsVersionWrapper = (props: {docsPluginId: string}) => {
+const DocsVersionWrapper = (props: { docsPluginId: string }) => {
   const { docsPluginId } = props;
   return (
     <div className={styles.sidebarVersionSwitch}>
@@ -78,10 +76,10 @@ const DocsVersionWrapper = (props: {docsPluginId: string}) 
=> {
 
 const DocsVersionWrapperMemo = React.memo(DocsVersionWrapper);
 
- interface DocSidebarMobileSecondaryMenuProps extends Props {
-   docsPluginId: string,
-   toggleSidebar: () => void
- }
+interface DocSidebarMobileSecondaryMenuProps extends Props {
+  docsPluginId: string;
+  toggleSidebar: () => void;
+}
 
 const DocSidebarMobileSecondaryMenu: FC<DocSidebarMobileSecondaryMenuProps> = 
({
   toggleSidebar,
@@ -92,25 +90,22 @@ const DocSidebarMobileSecondaryMenu: 
FC<DocSidebarMobileSecondaryMenuProps> = ({
   <>
     <DocsVersionWrapperMemo docsPluginId={docsPluginId} />
     <ul className={clsx(ThemeClassNames.docs.docSidebarMenu, 'menu__list')}>
-      <DocSidebarItems
-        items={sidebar}
-        activePath={path}
-        onItemClick={() => toggleSidebar()}
-      />
+      <DocSidebarItems items={sidebar} activePath={path} onItemClick={() => 
toggleSidebar()} />
     </ul>
   </>
 );
 
 const DocSidebarMobile = (props: Props) => (
-  <MobileSecondaryMenuFiller
-    component={DocSidebarMobileSecondaryMenu}
-    props={props}
-  />
+  <MobileSecondaryMenuFiller component={DocSidebarMobileSecondaryMenu} 
props={props} />
 );
 
 const DocSidebarDesktop = ({
-  path, sidebar, onCollapse, isHidden, docsPluginId,
-}: Props & {docsPluginId: string}) => {
+  path,
+  sidebar,
+  onCollapse,
+  isHidden,
+  docsPluginId,
+}: Props & { docsPluginId: string }) => {
   const showAnnouncementBar = useShowAnnouncementBar();
   const {
     navbar: { hideOnScroll },
@@ -129,8 +124,7 @@ const DocSidebarDesktop = ({
       <DocsVersionWrapperMemo docsPluginId={docsPluginId} />
       <nav
         className={clsx('menu thin-scrollbar', styles.menu, {
-          [styles.menuWithAnnouncementBar]:
-             !isAnnouncementBarClosed && showAnnouncementBar,
+          [styles.menuWithAnnouncementBar]: !isAnnouncementBarClosed && 
showAnnouncementBar,
         })}
       >
         <ul className={clsx(ThemeClassNames.docs.docSidebarMenu, 
'menu__list')}>
@@ -145,7 +139,7 @@ const DocSidebarDesktop = ({
 const DocSidebarMobileMemo = React.memo(DocSidebarMobile);
 const DocSidebarDesktopMemo = React.memo(DocSidebarDesktop);
 
-const DocSidebar: FC<Props & {docsPluginId: string}> = (props) => {
+const DocSidebar: FC<Props & { docsPluginId: string }> = (props) => {
   const windowSize = useWindowSize();
 
   // Desktop sidebar visible on hydration: need SSR rendering
diff --git a/doc/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.tsx 
b/doc/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.tsx
new file mode 100644
index 00000000000..72cc6d7238f
--- /dev/null
+++ b/doc/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.tsx
@@ -0,0 +1,123 @@
+/**
+ * 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, max-len, 
import/no-unresolved */
+import type { FC } from 'react';
+import React from 'react';
+import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
+import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
+import type { GlobalVersion } from '@theme/hooks/useDocs';
+import { useVersions, useLatestVersion, useActiveDocContext } from 
'@theme/hooks/useDocs';
+import type { Props } from '@theme/NavbarItem/DocsVersionDropdownNavbarItem';
+import { useDocsPreferredVersion } from '@docusaurus/theme-common';
+import { translate } from '@docusaurus/Translate';
+import type { GlobalDataVersion } from '@docusaurus/plugin-content-docs-types';
+import clsx from 'clsx';
+import { LTSVersions } from '../../../../config/apisix-versions';
+import style from './style.module.scss';
+
+const getVersionMainDoc = (version: GlobalDataVersion) => 
version.docs.find((doc) => doc.id === version.mainDocId)!;
+
+const badgeObj = {
+  LTS: <div className={clsx(style.badge, style.LTS)}>LTS</div>,
+  Latest: <div className={clsx(style.badge, style.Latest)}>Latest</div>,
+};
+
+interface LabelWithBadgeProps {
+  version: GlobalVersion;
+  isApisx: boolean;
+}
+const LabelWithBadge: FC<LabelWithBadgeProps> = (props) => {
+  const { version, isApisx } = props;
+  return (
+    <div>
+      {version.label}
+      {(() => {
+        if (version.isLast) return badgeObj.Latest;
+        if (isApisx && LTSVersions.includes(version.label)) return 
badgeObj.LTS;
+        return null;
+      })()}
+    </div>
+  );
+};
+
+const DocsVersionDropdownNavbarItem = ({
+  mobile,
+  docsPluginId,
+  dropdownActiveClassDisabled,
+  dropdownItemsBefore,
+  dropdownItemsAfter,
+  ...props
+}: Props): JSX.Element => {
+  const activeDocContext = useActiveDocContext(docsPluginId);
+  const versions = useVersions(docsPluginId);
+  const latestVersion = useLatestVersion(docsPluginId);
+  const isApisix = docsPluginId === 'docs-apisix';
+
+  const { preferredVersion, savePreferredVersionName } = 
useDocsPreferredVersion(docsPluginId);
+
+  function getItems() {
+    const versionLinks = versions.map((version) => {
+      // We try to link to the same doc, in another version
+      // When not possible, fallback to the "main doc" of the version
+      const versionDoc = activeDocContext?.alternateDocVersions[version.name] 
|| getVersionMainDoc(version);
+      return {
+        isNavLink: true,
+        label: <LabelWithBadge version={version} isApisx={isApisix} />,
+        to: versionDoc.path,
+        isActive: () => version === activeDocContext?.activeVersion,
+        onClick: () => {
+          savePreferredVersionName(version.name);
+        },
+      };
+    });
+
+    return [...dropdownItemsBefore, ...versionLinks, ...dropdownItemsAfter];
+  }
+
+  const items = getItems();
+
+  const dropdownVersion = activeDocContext.activeVersion ?? preferredVersion 
?? latestVersion;
+
+  // Mobile dropdown is handled a bit differently
+  const dropdownLabel = mobile && items
+    ? translate({
+      id: 'theme.navbar.mobileVersionsDropdown.label',
+      message: 'Versions',
+      description: 'The label for the navbar versions dropdown on mobile view',
+    })
+    : dropdownVersion.label;
+  const dropdownTo = mobile && items ? undefined : 
getVersionMainDoc(dropdownVersion).path;
+
+  // We don't want to render a version dropdown with 0 or 1 item
+  // If we build the site with a single docs version (onlyIncludeVersions: 
['1.0.0'])
+  // We'd rather render a button instead of a dropdown
+  if (items.length <= 1) {
+    return (
+      <DefaultNavbarItem
+        {...props}
+        mobile={mobile}
+        label={dropdownLabel}
+        to={dropdownTo}
+        isActive={dropdownActiveClassDisabled ? () => false : undefined}
+      />
+    );
+  }
+
+  return (
+    <DropdownNavbarItem
+      {...props}
+      mobile={mobile}
+      label={dropdownLabel}
+      to={dropdownTo}
+      items={items}
+      isActive={dropdownActiveClassDisabled ? () => false : undefined}
+    />
+  );
+};
+
+export default DocsVersionDropdownNavbarItem;
diff --git a/doc/src/theme/NavbarItem/index.tsx 
b/doc/src/theme/NavbarItem/index.tsx
new file mode 100644
index 00000000000..02aa9fa098c
--- /dev/null
+++ b/doc/src/theme/NavbarItem/index.tsx
@@ -0,0 +1,64 @@
+/**
+ * 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 max-len, @typescript-eslint/no-explicit-any */
+import React from 'react';
+import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
+import type {
+  Props as DropdownNavbarItemProps,
+} from '@theme/NavbarItem/DropdownNavbarItem';
+import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
+import LocaleDropdownNavbarItem from 
'@theme/NavbarItem/LocaleDropdownNavbarItem';
+import SearchNavbarItem from '@theme/NavbarItem/SearchNavbarItem';
+import type { Types, Props } from '@theme/NavbarItem';
+
+const NavbarItemComponents: { [key: Exclude<Types, undefined>]: () => (props: 
any) => JSX.Element } = {
+  default: () => DefaultNavbarItem,
+  localeDropdown: () => LocaleDropdownNavbarItem,
+  search: () => SearchNavbarItem,
+  dropdown: () => DropdownNavbarItem,
+
+  // Need to lazy load these items as we don't know for sure the docs plugin 
is loaded
+  // See https://github.com/facebook/docusaurus/issues/3360
+  /* eslint-disable @typescript-eslint/no-var-requires, global-require */
+  docsVersion: () => 
require('@theme/NavbarItem/DocsVersionNavbarItem').default,
+  docsVersionDropdown: () => 
require('@theme/NavbarItem/DocsVersionDropdownNavbarItem').default,
+  doc: () => require('@theme/NavbarItem/DocNavbarItem').default,
+  /* eslint-enable @typescript-eslint/no-var-requires, global-require */
+} as const;
+
+type NavbarItemComponentType = keyof typeof NavbarItemComponents;
+
+const getNavbarItemComponent = (type: NavbarItemComponentType) => {
+  const navbarItemComponentFn = NavbarItemComponents[type];
+  if (!navbarItemComponentFn) {
+    throw new Error(`No NavbarItem component found for type "${type}".`);
+  }
+  return navbarItemComponentFn();
+};
+
+function getComponentType(type: Types, isDropdown: boolean): 
NavbarItemComponentType {
+  // Backward compatibility: navbar item with no type set
+  // but containing dropdown items should use the type "dropdown"
+  if (!type || type === 'default') {
+    return isDropdown ? 'dropdown' : 'default';
+  }
+  return type as NavbarItemComponentType;
+}
+
+export const getInfimaActiveClassName = (mobile?: boolean): string => (mobile 
? 'menu__link--active' : 'navbar__link--active');
+
+const NavbarItem = ({ type, ...props }: Props): JSX.Element => {
+  const componentType = getComponentType(
+    type,
+    (props as DropdownNavbarItemProps).items !== undefined,
+  );
+  const NavbarItemComponent = getNavbarItemComponent(componentType);
+  return <NavbarItemComponent {...props} />;
+};
+
+export default NavbarItem;
diff --git a/doc/src/theme/NavbarItem/style.module.scss 
b/doc/src/theme/NavbarItem/style.module.scss
new file mode 100644
index 00000000000..2344cc56bf4
--- /dev/null
+++ b/doc/src/theme/NavbarItem/style.module.scss
@@ -0,0 +1,17 @@
+.badge {
+  padding: 0 8px;
+  margin-left: 9px;
+  border-radius: 8px;
+  font-size: 0.8rem;
+  text-transform: uppercase;
+
+  &.LTS {
+    color: hsl(198deg 77% 20%);
+    background-color: hsl(198deg 100% 92%);
+  }
+
+  &.Latest {
+    color: hsl(129deg 81% 14%);
+    background-color: hsl(130deg 71% 93%);
+  }
+}

Reply via email to