mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-06-05 03:45:46 +00:00
📝 Docs: 商店插件可用性筛选 & 更新排序 (#3334)
This commit is contained in:
parent
db857b11fa
commit
6cff660af0
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "root",
|
||||
"private": true,
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"workspaces": [
|
||||
"website"
|
||||
],
|
||||
|
@ -3,14 +3,6 @@ import type { Options as ChangelogOptions } from "@nullbot/docusaurus-plugin-cha
|
||||
import type * as Preset from "@nullbot/docusaurus-preset-nonepress";
|
||||
import { themes } from "prism-react-renderer";
|
||||
|
||||
// By default, we use Docusaurus Faster
|
||||
// DOCUSAURUS_SLOWER=true is useful for benchmarking faster against slower
|
||||
// hyperfine --prepare 'yarn clear:website' --runs 3 'DOCUSAURUS_SLOWER=true yarn build:website:fast' 'yarn build:website:fast'
|
||||
const isSlower = process.env.DOCUSAURUS_SLOWER === "true";
|
||||
if (isSlower) {
|
||||
console.log("🐢 Using slower Docusaurus build");
|
||||
}
|
||||
|
||||
// color mode config
|
||||
const colorMode: Preset.ThemeConfig["colorMode"] = {
|
||||
defaultMode: "light",
|
||||
|
@ -13,37 +13,38 @@
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start --host 0.0.0.0 --port 3000",
|
||||
"build": "docusaurus build",
|
||||
"build:fast": "cross-env BUILD_FAST=true yarn build",
|
||||
"build:fast:rsdoctor": "cross-env BUILD_FAST=true RSDOCTOR=true yarn build",
|
||||
"build:fast:profile": "cross-env BUILD_FAST=true node --cpu-prof --cpu-prof-dir .cpu-prof ./node_modules/.bin/docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"typecheck": "tsc"
|
||||
"typecheck": "tsc",
|
||||
"prettier": "prettier --config ../.prettierrc --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^3.6.2",
|
||||
"@docusaurus/core": "^3.7.0",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@nullbot/docusaurus-plugin-changelog": "^3.0.0",
|
||||
"@nullbot/docusaurus-preset-nonepress": "^3.0.0",
|
||||
"@swc/core": "^1.7.26",
|
||||
"clsx": "^2.0.0",
|
||||
"copy-text-to-clipboard": "^3.2.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^18.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-use-pagination": "^2.0.1",
|
||||
"swc-loader": "^0.2.6"
|
||||
"react-dom": "^19.0.0",
|
||||
"react-use-pagination": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/faster": "^3.6.2",
|
||||
"@docusaurus/module-type-aliases": "^3.6.2",
|
||||
"@docusaurus/faster": "^3.7.0",
|
||||
"@docusaurus/module-type-aliases": "^3.7.0",
|
||||
"@nullbot/docusaurus-tsconfig": "^3.0.0",
|
||||
"@types/react-color": "^3.0.10",
|
||||
"asciinema-player": "^3.5.0",
|
||||
"typescript": "~5.5.2"
|
||||
"typescript": "~5.7.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
@ -25,7 +25,7 @@ export type Props = {
|
||||
export default function AsciinemaContainer({
|
||||
url,
|
||||
options = {},
|
||||
}: Props): JSX.Element {
|
||||
}: Props): React.ReactNode {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -1,14 +1,14 @@
|
||||
import React from "react";
|
||||
|
||||
import "asciinema-player/dist/bundle/asciinema-player.css";
|
||||
import BrowserOnly from "@docusaurus/BrowserOnly";
|
||||
import "asciinema-player/dist/bundle/asciinema-player.css";
|
||||
|
||||
import "./styles.css";
|
||||
import type { Props } from "./container";
|
||||
import "./styles.css";
|
||||
|
||||
export type { Props } from "./container";
|
||||
|
||||
export default function Asciinema(props: Props): JSX.Element {
|
||||
export default function Asciinema(props: Props): React.ReactNode {
|
||||
return (
|
||||
<BrowserOnly
|
||||
fallback={
|
||||
|
@ -2,7 +2,7 @@ import React from "react";
|
||||
|
||||
import { Form } from ".";
|
||||
|
||||
export default function AdapterForm(): JSX.Element {
|
||||
export default function AdapterForm(): React.ReactNode {
|
||||
const formItems = [
|
||||
{
|
||||
name: "基本信息",
|
||||
|
@ -2,7 +2,7 @@ import React from "react";
|
||||
|
||||
import { Form } from ".";
|
||||
|
||||
export default function BotForm(): JSX.Element {
|
||||
export default function BotForm(): React.ReactNode {
|
||||
const formItems = [
|
||||
{
|
||||
name: "基本信息",
|
||||
|
@ -18,7 +18,7 @@ export type Props = {
|
||||
export default function TagFormItem({
|
||||
allowTags,
|
||||
onTagUpdate,
|
||||
}: Props): JSX.Element {
|
||||
}: Props): React.ReactNode {
|
||||
const [tags, setTags] = useState<TagType[]>([]);
|
||||
const [label, setLabel] = useState<TagType["label"]>("");
|
||||
const [color, setColor] = useState<TagType["color"]>("#ea5252");
|
||||
|
@ -2,7 +2,7 @@ import React from "react";
|
||||
|
||||
import { Form } from ".";
|
||||
|
||||
export default function PluginForm(): JSX.Element {
|
||||
export default function PluginForm(): React.ReactNode {
|
||||
const formItems = [
|
||||
{
|
||||
name: "包信息",
|
||||
|
@ -32,7 +32,7 @@ export function Form({
|
||||
children,
|
||||
formItems,
|
||||
handleSubmit,
|
||||
}: Props): JSX.Element {
|
||||
}: Props): React.ReactNode {
|
||||
const [currentStep, setCurrentStep] = useState<number>(0);
|
||||
const [result, setResult] = useState<Record<string, string>>({});
|
||||
const [allowTags, setAllowTags] = useState<TagType[]>([]);
|
||||
@ -125,7 +125,7 @@ export function FormItem({
|
||||
allowTags: TagType[];
|
||||
result: Record<string, string>;
|
||||
setResult: (key: string, value: string) => void;
|
||||
}): JSX.Element {
|
||||
}): React.ReactNode {
|
||||
return (
|
||||
<>
|
||||
<label className="label">
|
||||
|
@ -16,7 +16,7 @@ export function HomeFeature({
|
||||
description,
|
||||
annotaion,
|
||||
children,
|
||||
}: Feature): JSX.Element {
|
||||
}: Feature): React.ReactNode {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-4">
|
||||
<p className="text-sm text-base-content/70 font-medium tracking-wide uppercase">
|
||||
@ -32,7 +32,7 @@ export function HomeFeature({
|
||||
);
|
||||
}
|
||||
|
||||
function HomeFeatureSingleColumn(props: Feature): JSX.Element {
|
||||
function HomeFeatureSingleColumn(props: Feature): React.ReactNode {
|
||||
return (
|
||||
<div className="grid grid-cols-1 px-4 py-8 md:px-16 mx-auto">
|
||||
<HomeFeature {...props} />
|
||||
@ -46,7 +46,7 @@ function HomeFeatureDoubleColumn({
|
||||
}: {
|
||||
features: [Feature, Feature];
|
||||
children?: [React.ReactNode, React.ReactNode];
|
||||
}): JSX.Element {
|
||||
}): React.ReactNode {
|
||||
const [children1, children2] = children ?? [];
|
||||
|
||||
return (
|
||||
@ -57,7 +57,7 @@ function HomeFeatureDoubleColumn({
|
||||
);
|
||||
}
|
||||
|
||||
function HomeFeatures(): JSX.Element {
|
||||
function HomeFeatures(): React.ReactNode {
|
||||
return (
|
||||
<>
|
||||
<HomeFeatureSingleColumn
|
||||
|
@ -10,7 +10,7 @@ import copy from "copy-text-to-clipboard";
|
||||
import IconCopy from "@theme/Icon/Copy";
|
||||
import IconSuccess from "@theme/Icon/Success";
|
||||
|
||||
function HomeHeroInstallButton(): JSX.Element {
|
||||
function HomeHeroInstallButton(): React.ReactNode {
|
||||
const code = "pipx run nb-cli create";
|
||||
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
@ -37,7 +37,7 @@ function HomeHeroInstallButton(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
function HomeHero(): JSX.Element {
|
||||
function HomeHero(): React.ReactNode {
|
||||
const {
|
||||
siteConfig: { tagline },
|
||||
} = useDocusaurusContext();
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
|
||||
import "./styles.css";
|
||||
import HomeFeatures from "./Feature";
|
||||
import HomeHero from "./Hero";
|
||||
import "./styles.css";
|
||||
|
||||
export default function HomeContent(): JSX.Element {
|
||||
export default function HomeContent(): React.ReactNode {
|
||||
return (
|
||||
<div className="home-container">
|
||||
<HomeHero />
|
||||
|
@ -6,8 +6,8 @@ import useBaseUrl from "@docusaurus/useBaseUrl";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNonepressThemeConfig } from "@nullbot/docusaurus-theme-nonepress/client";
|
||||
|
||||
import "./styles.css";
|
||||
import ThemedImage from "@theme/ThemedImage";
|
||||
import "./styles.css";
|
||||
|
||||
export type Message = {
|
||||
msg: string;
|
||||
@ -19,7 +19,7 @@ function MessageBox({
|
||||
msg,
|
||||
position = "left",
|
||||
monospace = false,
|
||||
}: Message): JSX.Element {
|
||||
}: Message): React.ReactNode {
|
||||
const {
|
||||
navbar: { logo },
|
||||
} = useNonepressThemeConfig();
|
||||
@ -63,7 +63,7 @@ export default function Messenger({
|
||||
msgs = [],
|
||||
}: {
|
||||
msgs?: Message[];
|
||||
}): JSX.Element {
|
||||
}): React.ReactNode {
|
||||
return (
|
||||
<div className="messenger-container">
|
||||
<header className="messenger-title">
|
||||
|
@ -22,7 +22,7 @@ export default function Modal({
|
||||
useCustomTitle,
|
||||
backdropExit,
|
||||
title,
|
||||
}: Props): JSX.Element {
|
||||
}: Props): React.ReactNode {
|
||||
const [transitionClass, setTransitionClass] = useState<string>("");
|
||||
|
||||
const onFadeIn = () => setTransitionClass("fade-in");
|
||||
|
@ -31,7 +31,7 @@ export default function Paginate({
|
||||
setPage,
|
||||
previousEnabled,
|
||||
nextEnabled,
|
||||
}: Props): JSX.Element {
|
||||
}: Props): React.ReactNode {
|
||||
// const [containerElement, setContainerElement] = useState<HTMLElement | null>(
|
||||
// null
|
||||
// );
|
||||
|
@ -5,10 +5,10 @@ import clsx from "clsx";
|
||||
import Link from "@docusaurus/Link";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import "./styles.css";
|
||||
import Tag from "@/components/Resource/Tag";
|
||||
import ValidStatus from "@/components/Resource/ValidStatus";
|
||||
import type { Resource } from "@/libs/store";
|
||||
import "./styles.css";
|
||||
|
||||
export type Props = {
|
||||
resource: Resource;
|
||||
@ -24,7 +24,7 @@ export default function ResourceCard({
|
||||
onTagClick,
|
||||
onAuthorClick,
|
||||
className,
|
||||
}: Props): JSX.Element {
|
||||
}: Props): React.ReactNode {
|
||||
const isGithub = /^https:\/\/github.com\/[^/]+\/[^/]+/.test(
|
||||
resource.homepage
|
||||
);
|
||||
|
@ -72,6 +72,15 @@ export default function ResourceDetailCard({ resource }: Props) {
|
||||
}
|
||||
};
|
||||
|
||||
const getPluginStatusUpdatedTime = (resource: Resource) => {
|
||||
switch (resource.resourceType) {
|
||||
case "plugin":
|
||||
return new Date(resource.time).toLocaleString();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPypiProject = (projectName: string) =>
|
||||
fetch(`https://pypi.org/pypi/${projectName}/json`)
|
||||
.then((response) => response.json())
|
||||
@ -99,8 +108,9 @@ export default function ResourceDetailCard({ resource }: Props) {
|
||||
|
||||
const projectLink = getProjectLink(resource) || "无";
|
||||
const moduleName = getModuleName(resource) || "无";
|
||||
const homepageLink = getHomepageLink(resource) || undefined;
|
||||
const pypiProjectLink = getPypiProjectLink(resource) || undefined;
|
||||
const homepageLink = getHomepageLink(resource);
|
||||
const pypiProjectLink = getPypiProjectLink(resource);
|
||||
const updatedTime = getPluginStatusUpdatedTime(resource);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -183,31 +193,39 @@ export default function ResourceDetailCard({ resource }: Props) {
|
||||
{(pypiData && pypiData.info.version) || "无"}
|
||||
</div>
|
||||
|
||||
<div className="detail-card-meta-item">
|
||||
<FontAwesomeIcon fixedWidth icon={["fas", "fingerprint"]} />{" "}
|
||||
<a
|
||||
href={homepageLink}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={homepageLink && "hover:underline hover:text-primary"}
|
||||
>
|
||||
{moduleName}
|
||||
</a>
|
||||
</div>
|
||||
{homepageLink && (
|
||||
<div className="detail-card-meta-item">
|
||||
<FontAwesomeIcon fixedWidth icon={["fas", "fingerprint"]} />{" "}
|
||||
<a
|
||||
href={homepageLink}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="detail-card-meta-item-link"
|
||||
>
|
||||
{moduleName}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pypiProjectLink && (
|
||||
<div className="detail-card-meta-item">
|
||||
<FontAwesomeIcon fixedWidth icon={["fas", "cubes"]} />{" "}
|
||||
<a
|
||||
href={pypiProjectLink}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="detail-card-meta-item-link"
|
||||
>
|
||||
{projectLink}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="detail-card-meta-item">
|
||||
<FontAwesomeIcon fixedWidth icon={["fas", "cubes"]} />{" "}
|
||||
<a
|
||||
href={pypiProjectLink}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={
|
||||
pypiProjectLink && "hover:underline hover:text-primary"
|
||||
}
|
||||
>
|
||||
{projectLink}
|
||||
</a>
|
||||
<FontAwesomeIcon fixedWidth icon={["fas", "clock-rotate-left"]} />{" "}
|
||||
{updatedTime}
|
||||
</div>
|
||||
|
||||
<div className="detail-card-actions">
|
||||
<ValidStatus
|
||||
resource={resource}
|
||||
|
@ -24,7 +24,7 @@
|
||||
}
|
||||
|
||||
&-actions {
|
||||
@apply flex items-center gap-x-2 ml-auto;
|
||||
@apply flex items-center gap-x-2 lg:ml-auto;
|
||||
|
||||
&-button {
|
||||
@apply btn btn-sm;
|
||||
@ -51,11 +51,15 @@
|
||||
}
|
||||
|
||||
&-right {
|
||||
@apply flex flex-col justify-start gap-y-2 lg:basis-1/4 max-w-[45%];
|
||||
@apply flex flex-col justify-start gap-y-2 lg:basis-1/4 lg:max-w-[45%];
|
||||
}
|
||||
}
|
||||
|
||||
&-meta-item {
|
||||
@apply text-sm truncate;
|
||||
|
||||
&-link {
|
||||
@apply hover:text-primary hover:transition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,9 @@ import React from "react";
|
||||
|
||||
import clsx from "clsx";
|
||||
|
||||
import "./styles.css";
|
||||
import { pickTextColor } from "@/libs/color";
|
||||
import type { Tag } from "@/types/tag";
|
||||
import "./styles.css";
|
||||
|
||||
export type Props = Tag & {
|
||||
className?: string;
|
||||
@ -16,7 +16,7 @@ export default function ResourceTag({
|
||||
color,
|
||||
className,
|
||||
onClick,
|
||||
}: Props): JSX.Element {
|
||||
}: Props): React.ReactNode {
|
||||
return (
|
||||
<span
|
||||
className={clsx("resource-tag", className)}
|
||||
|
@ -2,9 +2,9 @@ import React, { useRef } from "react";
|
||||
|
||||
import clsx from "clsx";
|
||||
|
||||
import "./styles.css";
|
||||
import { translate } from "@docusaurus/Translate";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import "./styles.css";
|
||||
|
||||
export type Props = {
|
||||
onChange: (value: string) => void;
|
||||
@ -28,7 +28,7 @@ export default function Searcher({
|
||||
className,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
}: Props): JSX.Element {
|
||||
}: Props): React.ReactNode {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
@ -85,6 +85,10 @@ export default function Searcher({
|
||||
onClick={() => onTagClick(index)}
|
||||
>
|
||||
{tag}
|
||||
<FontAwesomeIcon
|
||||
className="searcher-action-icon close ml-1"
|
||||
icon={["fas", "xmark"]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<input
|
||||
|
@ -18,7 +18,7 @@ import { fetchRegistryData, loadFailedTitle } from "@/libs/store";
|
||||
import { useToolbar } from "@/libs/toolbar";
|
||||
import type { Adapter } from "@/types/adapter";
|
||||
|
||||
export default function AdapterPage(): JSX.Element {
|
||||
export default function AdapterPage(): React.ReactNode {
|
||||
const [adapters, setAdapters] = useState<Adapter[] | null>(null);
|
||||
const adapterCount = adapters?.length ?? 0;
|
||||
const loading = adapters === null;
|
||||
|
@ -17,7 +17,7 @@ import { fetchRegistryData, loadFailedTitle } from "@/libs/store";
|
||||
import { useToolbar } from "@/libs/toolbar";
|
||||
import type { Bot } from "@/types/bot";
|
||||
|
||||
export default function PluginPage(): JSX.Element {
|
||||
export default function PluginPage(): React.ReactNode {
|
||||
const [bots, setBots] = useState<Bot[] | null>(null);
|
||||
const botCount = bots?.length ?? 0;
|
||||
const loading = bots === null;
|
||||
|
@ -15,7 +15,7 @@ import { useSearchControl } from "@/libs/search";
|
||||
import { fetchRegistryData, loadFailedTitle } from "@/libs/store";
|
||||
import type { Driver } from "@/types/driver";
|
||||
|
||||
export default function DriverPage(): JSX.Element {
|
||||
export default function DriverPage(): React.ReactNode {
|
||||
const [drivers, setDrivers] = useState<Driver[] | null>(null);
|
||||
const driverCount = drivers?.length ?? 0;
|
||||
const loading = drivers === null;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import Translate from "@docusaurus/Translate";
|
||||
import Translate, { translate } from "@docusaurus/Translate";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import Admonition from "@theme/Admonition";
|
||||
@ -11,14 +11,18 @@ import Paginate from "@/components/Paginate";
|
||||
import ResourceCard from "@/components/Resource/Card";
|
||||
import ResourceDetailCard from "@/components/Resource/DetailCard";
|
||||
import Searcher from "@/components/Searcher";
|
||||
import StoreToolbar, { type Action } from "@/components/Store/Toolbar";
|
||||
import StoreToolbar, {
|
||||
type Action,
|
||||
type Sorter,
|
||||
} from "@/components/Store/Toolbar";
|
||||
import { authorFilter, tagFilter } from "@/libs/filter";
|
||||
import { useSearchControl } from "@/libs/search";
|
||||
import { SortMode } from "@/libs/sorter";
|
||||
import { fetchRegistryData, loadFailedTitle } from "@/libs/store";
|
||||
import { useToolbar } from "@/libs/toolbar";
|
||||
import type { Plugin } from "@/types/plugin";
|
||||
|
||||
export default function PluginPage(): JSX.Element {
|
||||
export default function PluginPage(): React.ReactNode {
|
||||
const [plugins, setPlugins] = useState<Plugin[] | null>(null);
|
||||
const pluginCount = plugins?.length ?? 0;
|
||||
const loading = plugins === null;
|
||||
@ -27,6 +31,38 @@ export default function PluginPage(): JSX.Element {
|
||||
const [isOpenModal, setIsOpenModal] = useState<boolean>(false);
|
||||
const [isOpenCardModal, setIsOpenCardModal] = useState<boolean>(false);
|
||||
const [clickedPlugin, setClickedPlugin] = useState<Plugin | null>(null);
|
||||
const [sortMode, setSortMode] = useState<SortMode>(SortMode.Default);
|
||||
|
||||
const sorterTool: Sorter = {
|
||||
label:
|
||||
sortMode === SortMode.Default
|
||||
? translate({
|
||||
id: "pages.store.sorter.label.default",
|
||||
description: "The label of default sorter",
|
||||
message: "默认顺序",
|
||||
})
|
||||
: translate({
|
||||
id: "pages.store.sorter.label.updateDesc",
|
||||
description: "The label of updateDesc sorter",
|
||||
message: "更新时间倒序",
|
||||
}),
|
||||
icon: ["fas", "sort-amount-down"],
|
||||
active: sortMode === SortMode.UpdateDesc,
|
||||
onClick: () => {
|
||||
setSortMode(
|
||||
sortMode === SortMode.Default ? SortMode.UpdateDesc : SortMode.Default
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const getSortedPlugins = (plugins: Plugin[]): Plugin[] => {
|
||||
if (sortMode === SortMode.UpdateDesc) {
|
||||
return [...plugins].sort(
|
||||
(a, b) => new Date(b.time).getTime() - new Date(a.time).getTime()
|
||||
);
|
||||
}
|
||||
return plugins;
|
||||
};
|
||||
|
||||
const {
|
||||
filteredResources: filteredPlugins,
|
||||
@ -37,7 +73,7 @@ export default function PluginPage(): JSX.Element {
|
||||
onSearchBackspace,
|
||||
onSearchClear,
|
||||
onSearchTagClick,
|
||||
} = useSearchControl<Plugin>(plugins ?? []);
|
||||
} = useSearchControl<Plugin>(getSortedPlugins(plugins ?? []));
|
||||
const filteredPluginCount = filteredPlugins.length;
|
||||
|
||||
const {
|
||||
@ -134,6 +170,7 @@ export default function PluginPage(): JSX.Element {
|
||||
<StoreToolbar
|
||||
className="not-prose"
|
||||
filters={filterTools}
|
||||
sorter={sorterTool}
|
||||
action={actionTool}
|
||||
/>
|
||||
|
||||
|
@ -18,7 +18,7 @@ type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
function StorePage({ title, children }: Props): JSX.Element {
|
||||
function StorePage({ title, children }: Props): React.ReactNode {
|
||||
const sidebarItems = useVersionedSidebar(
|
||||
useDocsVersionCandidates()[0].name,
|
||||
SIDEBAR_ID
|
||||
@ -35,7 +35,10 @@ function StorePage({ title, children }: Props): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
export default function StoreLayout({ title, ...props }: Props): JSX.Element {
|
||||
export default function StoreLayout({
|
||||
title,
|
||||
...props
|
||||
}: Props): React.ReactNode {
|
||||
return (
|
||||
<>
|
||||
<PageMetadata title={title} />
|
||||
|
@ -12,6 +12,13 @@ export type Filter = {
|
||||
onSubmit: (query: string) => void;
|
||||
};
|
||||
|
||||
export type Sorter = {
|
||||
label: string;
|
||||
icon: IconProp;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export type Action = {
|
||||
label: string;
|
||||
icon: IconProp;
|
||||
@ -20,6 +27,7 @@ export type Action = {
|
||||
|
||||
export type Props = {
|
||||
filters?: Filter[];
|
||||
sorter?: Sorter;
|
||||
action?: Action;
|
||||
className?: string;
|
||||
};
|
||||
@ -29,7 +37,7 @@ function ToolbarFilter({
|
||||
icon,
|
||||
choices,
|
||||
onSubmit,
|
||||
}: Filter): JSX.Element {
|
||||
}: Filter): React.ReactNode {
|
||||
const [query, setQuery] = useState<string>("");
|
||||
|
||||
const filteredChoices = choices
|
||||
@ -96,33 +104,65 @@ function ToolbarFilter({
|
||||
|
||||
export default function StoreToolbar({
|
||||
filters,
|
||||
sorter,
|
||||
action,
|
||||
className,
|
||||
}: Props): JSX.Element | null {
|
||||
}: Props): React.ReactNode | null {
|
||||
if (!(filters && filters.length > 0) && !action) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx("store-toolbar", className)}>
|
||||
{filters && filters.length > 0 && (
|
||||
<>
|
||||
<div className={clsx("store-toolbar", className)}>
|
||||
<div className="store-toolbar-filters">
|
||||
{filters.map((filter, index) => (
|
||||
{filters?.map((filter, index) => (
|
||||
<ToolbarFilter key={index} {...filter} />
|
||||
))}
|
||||
{sorter && (
|
||||
<div className="store-toolbar-sorter store-toolbar-sorter-desktop">
|
||||
<button
|
||||
className={clsx(
|
||||
"btn btn-sm btn-primary no-animation mr-2",
|
||||
!sorter.active && "btn-outline"
|
||||
)}
|
||||
onClick={sorter.onClick}
|
||||
>
|
||||
<FontAwesomeIcon icon={sorter.icon} />
|
||||
{sorter.label}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{action && (
|
||||
<div className="store-toolbar-action">
|
||||
<button
|
||||
className="btn btn-sm btn-primary no-animation"
|
||||
onClick={action.onClick}
|
||||
>
|
||||
<FontAwesomeIcon icon={action.icon} />
|
||||
{action.label}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{action && (
|
||||
<div className="store-toolbar-action">
|
||||
<button
|
||||
className="btn btn-sm btn-primary no-animation"
|
||||
onClick={action.onClick}
|
||||
>
|
||||
<FontAwesomeIcon icon={action.icon} />
|
||||
{action.label}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={clsx("store-toolbar store-toolbar-second", className)}>
|
||||
{sorter && (
|
||||
<div className="store-toolbar-sorter">
|
||||
<button
|
||||
className={clsx(
|
||||
"btn btn-sm btn-primary no-animation mr-2",
|
||||
!sorter.active && "btn-outline"
|
||||
)}
|
||||
onClick={sorter.onClick}
|
||||
>
|
||||
<FontAwesomeIcon icon={sorter.icon} />
|
||||
{sorter.label}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -14,6 +14,18 @@
|
||||
&-toolbar {
|
||||
@apply flex items-center justify-center my-4;
|
||||
|
||||
&-second {
|
||||
@apply lg:hidden;
|
||||
}
|
||||
|
||||
&-sorter {
|
||||
@apply max-lg:flex-1;
|
||||
|
||||
&-desktop {
|
||||
@apply max-lg:hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&-filters {
|
||||
@apply flex grow gap-2;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ export default function Tag({
|
||||
}: TagType & {
|
||||
className?: string;
|
||||
onClick?: React.MouseEventHandler<HTMLSpanElement>;
|
||||
}): JSX.Element {
|
||||
}): React.ReactNode {
|
||||
return (
|
||||
<span
|
||||
className={clsx("tag", className)}
|
||||
|
@ -3,14 +3,52 @@ import { useCallback, useState } from "react";
|
||||
import { translate } from "@docusaurus/Translate";
|
||||
|
||||
import type { Resource } from "./store";
|
||||
import { ValidStatus } from "./valid";
|
||||
|
||||
import { getValidStatus } from "@/components/Resource/ValidStatus";
|
||||
|
||||
export type Filter<T extends Resource = Resource> = {
|
||||
type: string;
|
||||
id: string;
|
||||
displayName: string;
|
||||
displayName?: string;
|
||||
filter: (resource: T) => boolean;
|
||||
};
|
||||
|
||||
const validStatusDisplayName = {
|
||||
[ValidStatus.VALID]: translate({
|
||||
id: "pages.store.filter.validateStatusDisplayName.valid",
|
||||
description: "The display name of validateStatus filter",
|
||||
message: "状态: 通过",
|
||||
}),
|
||||
[ValidStatus.INVALID]: translate({
|
||||
id: "pages.store.filter.validateStatusDisplayName.invalid",
|
||||
description: "The display name of validateStatus filter",
|
||||
message: "状态: 未通过",
|
||||
}),
|
||||
[ValidStatus.SKIP]: translate({
|
||||
id: "pages.store.filter.validateStatusDisplayName.skip",
|
||||
description: "The display name of validateStatus filter",
|
||||
message: "状态: 跳过",
|
||||
}),
|
||||
[ValidStatus.MISSING]: translate({
|
||||
id: "pages.store.filter.validateStatusDisplayName.missing",
|
||||
description: "The display name of validateStatus filter",
|
||||
message: "状态: 缺失",
|
||||
}),
|
||||
};
|
||||
|
||||
export const validStatusFilter = <T extends Resource = Resource>(
|
||||
validStatus: ValidStatus
|
||||
): Filter<T> => ({
|
||||
type: "validStatus",
|
||||
id: `validStatus-${validStatus}`,
|
||||
displayName: validStatusDisplayName[validStatus],
|
||||
filter: (resource: Resource): boolean =>
|
||||
resource.resourceType === "plugin"
|
||||
? getValidStatus(resource) === validStatus
|
||||
: true,
|
||||
});
|
||||
|
||||
export const tagFilter = <T extends Resource = Resource>(
|
||||
tag: string
|
||||
): Filter<T> => ({
|
||||
@ -27,6 +65,7 @@ export const tagFilter = <T extends Resource = Resource>(
|
||||
filter: (resource: Resource): boolean =>
|
||||
resource.tags.map((tag) => tag.label).includes(tag),
|
||||
});
|
||||
|
||||
export const officialFilter = <T extends Resource = Resource>(
|
||||
official: boolean = true
|
||||
): Filter<T> => ({
|
||||
@ -39,6 +78,7 @@ export const officialFilter = <T extends Resource = Resource>(
|
||||
}).split("|")[Number(official)],
|
||||
filter: (resource: Resource): boolean => resource.is_official === official,
|
||||
});
|
||||
|
||||
export const authorFilter = <T extends Resource = Resource>(
|
||||
author: string
|
||||
): Filter<T> => ({
|
||||
@ -54,6 +94,7 @@ export const authorFilter = <T extends Resource = Resource>(
|
||||
),
|
||||
filter: (resource: Resource): boolean => resource.author === author,
|
||||
});
|
||||
|
||||
export const queryFilter = <T extends Resource = Resource>(
|
||||
query: string
|
||||
): Filter<T> => ({
|
||||
|
4
website/src/libs/sorter.ts
Normal file
4
website/src/libs/sorter.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum SortMode {
|
||||
Default,
|
||||
UpdateDesc,
|
||||
}
|
@ -1,5 +1,13 @@
|
||||
import { authorFilter, tagFilter, type Filter } from "./filter";
|
||||
import { translate } from "@docusaurus/Translate";
|
||||
|
||||
import {
|
||||
authorFilter,
|
||||
tagFilter,
|
||||
validStatusFilter,
|
||||
type Filter,
|
||||
} from "./filter";
|
||||
import type { Resource } from "./store";
|
||||
import { ValidStatus } from "./valid";
|
||||
|
||||
import type { Filter as FilterTool } from "@/components/Store/Toolbar";
|
||||
|
||||
@ -38,7 +46,41 @@ export function useToolbar<T extends Resource = Resource>({
|
||||
},
|
||||
};
|
||||
|
||||
const validateStatusFilterMapping: Record<string, ValidStatus> = {
|
||||
[translate({
|
||||
id: "pages.store.filter.validateStatusDisplayName.valid",
|
||||
description: "The display name of validateStatus filter",
|
||||
message: "通过",
|
||||
})]: ValidStatus.VALID,
|
||||
[translate({
|
||||
id: "pages.store.filter.validateStatusDisplayName.invalid",
|
||||
description: "The display name of validateStatus filter",
|
||||
message: "未通过",
|
||||
})]: ValidStatus.INVALID,
|
||||
[translate({
|
||||
id: "pages.store.filter.validateStatusDisplayName.skip",
|
||||
description: "The display name of validateStatus filter",
|
||||
message: "跳过",
|
||||
})]: ValidStatus.SKIP,
|
||||
[translate({
|
||||
id: "pages.store.filter.validateStatusDisplayName.missing",
|
||||
description: "The display name of validateStatus filter",
|
||||
message: "缺失",
|
||||
})]: ValidStatus.MISSING,
|
||||
};
|
||||
|
||||
const validStatusFilterTool: FilterTool = {
|
||||
label: "状态",
|
||||
icon: ["fas", "plug"],
|
||||
choices: Object.keys(validateStatusFilterMapping),
|
||||
onSubmit: (type: string) => {
|
||||
const validStatus = validateStatusFilterMapping[type];
|
||||
if (!validStatus) return;
|
||||
addFilter(validStatusFilter(validStatus));
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
filters: [authorFilterTool, tagFilterTool],
|
||||
filters: [authorFilterTool, tagFilterTool, validStatusFilterTool],
|
||||
};
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import Layout from "@theme/Layout";
|
||||
|
||||
import HomeContent from "@/components/Home";
|
||||
|
||||
export default function Homepage(): JSX.Element {
|
||||
export default function Homepage(): React.ReactNode {
|
||||
return (
|
||||
<Layout>
|
||||
<HomeContent />
|
||||
|
@ -5,7 +5,7 @@ import { translate } from "@docusaurus/Translate";
|
||||
import AdapterPageContent from "@/components/Store/Content/Adapter";
|
||||
import StoreLayout from "@/components/Store/Layout";
|
||||
|
||||
export default function StoreAdapters(): JSX.Element {
|
||||
export default function StoreAdapters(): React.ReactNode {
|
||||
const title = translate({
|
||||
id: "pages.store.adapter.title",
|
||||
message: "适配器商店",
|
||||
|
@ -5,7 +5,7 @@ import { translate } from "@docusaurus/Translate";
|
||||
import BotPageContent from "@/components/Store/Content/Bot";
|
||||
import StoreLayout from "@/components/Store/Layout";
|
||||
|
||||
export default function StoreBots(): JSX.Element {
|
||||
export default function StoreBots(): React.ReactNode {
|
||||
const title = translate({
|
||||
id: "pages.store.bot.title",
|
||||
message: "机器人商店",
|
||||
|
@ -5,7 +5,7 @@ import { translate } from "@docusaurus/Translate";
|
||||
import DriverPageContent from "@/components/Store/Content/Driver";
|
||||
import StoreLayout from "@/components/Store/Layout";
|
||||
|
||||
export default function StoreDrivers(): JSX.Element {
|
||||
export default function StoreDrivers(): React.ReactNode {
|
||||
const title = translate({
|
||||
id: "pages.store.driver.title",
|
||||
message: "驱动器商店",
|
||||
|
@ -2,6 +2,6 @@ import React from "react";
|
||||
|
||||
import { Redirect } from "@docusaurus/router";
|
||||
|
||||
export default function Store(): JSX.Element {
|
||||
export default function Store(): React.ReactNode {
|
||||
return <Redirect to="/store/plugins" />;
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { translate } from "@docusaurus/Translate";
|
||||
import PluginPageContent from "@/components/Store/Content/Plugin";
|
||||
import StoreLayout from "@/components/Store/Layout";
|
||||
|
||||
export default function StorePlugins(): JSX.Element {
|
||||
export default function StorePlugins(): React.ReactNode {
|
||||
const title = translate({
|
||||
id: "pages.store.plugin.title",
|
||||
message: "插件商店",
|
||||
|
@ -2,7 +2,7 @@ import React, { type ComponentProps } from "react";
|
||||
|
||||
export interface Props extends Omit<ComponentProps<"svg">, "viewBox"> {}
|
||||
|
||||
export default function IconCloudflare(props: Props): JSX.Element {
|
||||
export default function IconCloudflare(props: Props): React.ReactNode {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 651.29 94.76"
|
||||
|
@ -2,7 +2,7 @@ import React, { type ComponentProps } from "react";
|
||||
|
||||
export interface Props extends Omit<ComponentProps<"svg">, "viewBox"> {}
|
||||
|
||||
export default function IconNetlify(props: Props): JSX.Element {
|
||||
export default function IconNetlify(props: Props): React.ReactNode {
|
||||
return (
|
||||
<svg viewBox="0 0 256 105" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<g clipPath="url(#clip0_236_25)">
|
||||
|
@ -9,7 +9,7 @@ import "./styles.css";
|
||||
export default function TOCContainer({
|
||||
children,
|
||||
...props
|
||||
}: Props): JSX.Element {
|
||||
}: Props): React.ReactNode {
|
||||
const windowSize = useWindowSize();
|
||||
const isClient = windowSize !== "ssr";
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user