📝 Docs: 商店插件可用性筛选 & 更新排序 (#3334)

This commit is contained in:
StarHeart 2025-02-26 23:05:06 +08:00 committed by GitHub
parent db857b11fa
commit 6cff660af0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 2320 additions and 2374 deletions

View File

@ -1,6 +1,7 @@
{ {
"name": "root", "name": "root",
"private": true, "private": true,
"packageManager": "yarn@1.22.22",
"workspaces": [ "workspaces": [
"website" "website"
], ],

View File

@ -3,14 +3,6 @@ import type { Options as ChangelogOptions } from "@nullbot/docusaurus-plugin-cha
import type * as Preset from "@nullbot/docusaurus-preset-nonepress"; import type * as Preset from "@nullbot/docusaurus-preset-nonepress";
import { themes } from "prism-react-renderer"; 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 // color mode config
const colorMode: Preset.ThemeConfig["colorMode"] = { const colorMode: Preset.ThemeConfig["colorMode"] = {
defaultMode: "light", defaultMode: "light",

View File

@ -13,37 +13,38 @@
"docusaurus": "docusaurus", "docusaurus": "docusaurus",
"start": "docusaurus start --host 0.0.0.0 --port 3000", "start": "docusaurus start --host 0.0.0.0 --port 3000",
"build": "docusaurus build", "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", "swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy", "deploy": "docusaurus deploy",
"clear": "docusaurus clear", "clear": "docusaurus clear",
"serve": "docusaurus serve", "serve": "docusaurus serve",
"write-translations": "docusaurus write-translations", "write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids", "write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc" "typecheck": "tsc",
"prettier": "prettier --config ../.prettierrc --write ."
}, },
"dependencies": { "dependencies": {
"@docusaurus/core": "^3.6.2", "@docusaurus/core": "^3.7.0",
"@mdx-js/react": "^3.0.0", "@mdx-js/react": "^3.0.0",
"@nullbot/docusaurus-plugin-changelog": "^3.0.0", "@nullbot/docusaurus-plugin-changelog": "^3.0.0",
"@nullbot/docusaurus-preset-nonepress": "^3.0.0", "@nullbot/docusaurus-preset-nonepress": "^3.0.0",
"@swc/core": "^1.7.26",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"copy-text-to-clipboard": "^3.2.0", "copy-text-to-clipboard": "^3.2.0",
"prism-react-renderer": "^2.3.0", "prism-react-renderer": "^2.3.0",
"raw-loader": "^4.0.2", "react": "^19.0.0",
"react": "^18.0.0",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-dom": "^18.0.0", "react-dom": "^19.0.0",
"react-use-pagination": "^2.0.1", "react-use-pagination": "^2.0.1"
"swc-loader": "^0.2.6"
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/faster": "^3.6.2", "@docusaurus/faster": "^3.7.0",
"@docusaurus/module-type-aliases": "^3.6.2", "@docusaurus/module-type-aliases": "^3.7.0",
"@nullbot/docusaurus-tsconfig": "^3.0.0", "@nullbot/docusaurus-tsconfig": "^3.0.0",
"@types/react-color": "^3.0.10", "@types/react-color": "^3.0.10",
"asciinema-player": "^3.5.0", "asciinema-player": "^3.5.0",
"typescript": "~5.5.2" "typescript": "~5.7.2"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [

View File

@ -25,7 +25,7 @@ export type Props = {
export default function AsciinemaContainer({ export default function AsciinemaContainer({
url, url,
options = {}, options = {},
}: Props): JSX.Element { }: Props): React.ReactNode {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {

View File

@ -1,14 +1,14 @@
import React from "react"; import React from "react";
import "asciinema-player/dist/bundle/asciinema-player.css";
import BrowserOnly from "@docusaurus/BrowserOnly"; import BrowserOnly from "@docusaurus/BrowserOnly";
import "asciinema-player/dist/bundle/asciinema-player.css";
import "./styles.css";
import type { Props } from "./container"; import type { Props } from "./container";
import "./styles.css";
export type { Props } from "./container"; export type { Props } from "./container";
export default function Asciinema(props: Props): JSX.Element { export default function Asciinema(props: Props): React.ReactNode {
return ( return (
<BrowserOnly <BrowserOnly
fallback={ fallback={

View File

@ -2,7 +2,7 @@ import React from "react";
import { Form } from "."; import { Form } from ".";
export default function AdapterForm(): JSX.Element { export default function AdapterForm(): React.ReactNode {
const formItems = [ const formItems = [
{ {
name: "基本信息", name: "基本信息",

View File

@ -2,7 +2,7 @@ import React from "react";
import { Form } from "."; import { Form } from ".";
export default function BotForm(): JSX.Element { export default function BotForm(): React.ReactNode {
const formItems = [ const formItems = [
{ {
name: "基本信息", name: "基本信息",

View File

@ -18,7 +18,7 @@ export type Props = {
export default function TagFormItem({ export default function TagFormItem({
allowTags, allowTags,
onTagUpdate, onTagUpdate,
}: Props): JSX.Element { }: Props): React.ReactNode {
const [tags, setTags] = useState<TagType[]>([]); const [tags, setTags] = useState<TagType[]>([]);
const [label, setLabel] = useState<TagType["label"]>(""); const [label, setLabel] = useState<TagType["label"]>("");
const [color, setColor] = useState<TagType["color"]>("#ea5252"); const [color, setColor] = useState<TagType["color"]>("#ea5252");

View File

@ -2,7 +2,7 @@ import React from "react";
import { Form } from "."; import { Form } from ".";
export default function PluginForm(): JSX.Element { export default function PluginForm(): React.ReactNode {
const formItems = [ const formItems = [
{ {
name: "包信息", name: "包信息",

View File

@ -32,7 +32,7 @@ export function Form({
children, children,
formItems, formItems,
handleSubmit, handleSubmit,
}: Props): JSX.Element { }: Props): React.ReactNode {
const [currentStep, setCurrentStep] = useState<number>(0); const [currentStep, setCurrentStep] = useState<number>(0);
const [result, setResult] = useState<Record<string, string>>({}); const [result, setResult] = useState<Record<string, string>>({});
const [allowTags, setAllowTags] = useState<TagType[]>([]); const [allowTags, setAllowTags] = useState<TagType[]>([]);
@ -125,7 +125,7 @@ export function FormItem({
allowTags: TagType[]; allowTags: TagType[];
result: Record<string, string>; result: Record<string, string>;
setResult: (key: string, value: string) => void; setResult: (key: string, value: string) => void;
}): JSX.Element { }): React.ReactNode {
return ( return (
<> <>
<label className="label"> <label className="label">

View File

@ -16,7 +16,7 @@ export function HomeFeature({
description, description,
annotaion, annotaion,
children, children,
}: Feature): JSX.Element { }: Feature): React.ReactNode {
return ( return (
<div className="flex flex-col items-center justify-center p-4"> <div className="flex flex-col items-center justify-center p-4">
<p className="text-sm text-base-content/70 font-medium tracking-wide uppercase"> <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 ( return (
<div className="grid grid-cols-1 px-4 py-8 md:px-16 mx-auto"> <div className="grid grid-cols-1 px-4 py-8 md:px-16 mx-auto">
<HomeFeature {...props} /> <HomeFeature {...props} />
@ -46,7 +46,7 @@ function HomeFeatureDoubleColumn({
}: { }: {
features: [Feature, Feature]; features: [Feature, Feature];
children?: [React.ReactNode, React.ReactNode]; children?: [React.ReactNode, React.ReactNode];
}): JSX.Element { }): React.ReactNode {
const [children1, children2] = children ?? []; const [children1, children2] = children ?? [];
return ( return (
@ -57,7 +57,7 @@ function HomeFeatureDoubleColumn({
); );
} }
function HomeFeatures(): JSX.Element { function HomeFeatures(): React.ReactNode {
return ( return (
<> <>
<HomeFeatureSingleColumn <HomeFeatureSingleColumn

View File

@ -10,7 +10,7 @@ import copy from "copy-text-to-clipboard";
import IconCopy from "@theme/Icon/Copy"; import IconCopy from "@theme/Icon/Copy";
import IconSuccess from "@theme/Icon/Success"; import IconSuccess from "@theme/Icon/Success";
function HomeHeroInstallButton(): JSX.Element { function HomeHeroInstallButton(): React.ReactNode {
const code = "pipx run nb-cli create"; const code = "pipx run nb-cli create";
const [isCopied, setIsCopied] = useState(false); const [isCopied, setIsCopied] = useState(false);
@ -37,7 +37,7 @@ function HomeHeroInstallButton(): JSX.Element {
); );
} }
function HomeHero(): JSX.Element { function HomeHero(): React.ReactNode {
const { const {
siteConfig: { tagline }, siteConfig: { tagline },
} = useDocusaurusContext(); } = useDocusaurusContext();

View File

@ -1,10 +1,10 @@
import React from "react"; import React from "react";
import "./styles.css";
import HomeFeatures from "./Feature"; import HomeFeatures from "./Feature";
import HomeHero from "./Hero"; import HomeHero from "./Hero";
import "./styles.css";
export default function HomeContent(): JSX.Element { export default function HomeContent(): React.ReactNode {
return ( return (
<div className="home-container"> <div className="home-container">
<HomeHero /> <HomeHero />

View File

@ -6,8 +6,8 @@ import useBaseUrl from "@docusaurus/useBaseUrl";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNonepressThemeConfig } from "@nullbot/docusaurus-theme-nonepress/client"; import { useNonepressThemeConfig } from "@nullbot/docusaurus-theme-nonepress/client";
import "./styles.css";
import ThemedImage from "@theme/ThemedImage"; import ThemedImage from "@theme/ThemedImage";
import "./styles.css";
export type Message = { export type Message = {
msg: string; msg: string;
@ -19,7 +19,7 @@ function MessageBox({
msg, msg,
position = "left", position = "left",
monospace = false, monospace = false,
}: Message): JSX.Element { }: Message): React.ReactNode {
const { const {
navbar: { logo }, navbar: { logo },
} = useNonepressThemeConfig(); } = useNonepressThemeConfig();
@ -63,7 +63,7 @@ export default function Messenger({
msgs = [], msgs = [],
}: { }: {
msgs?: Message[]; msgs?: Message[];
}): JSX.Element { }): React.ReactNode {
return ( return (
<div className="messenger-container"> <div className="messenger-container">
<header className="messenger-title"> <header className="messenger-title">

View File

@ -22,7 +22,7 @@ export default function Modal({
useCustomTitle, useCustomTitle,
backdropExit, backdropExit,
title, title,
}: Props): JSX.Element { }: Props): React.ReactNode {
const [transitionClass, setTransitionClass] = useState<string>(""); const [transitionClass, setTransitionClass] = useState<string>("");
const onFadeIn = () => setTransitionClass("fade-in"); const onFadeIn = () => setTransitionClass("fade-in");

View File

@ -31,7 +31,7 @@ export default function Paginate({
setPage, setPage,
previousEnabled, previousEnabled,
nextEnabled, nextEnabled,
}: Props): JSX.Element { }: Props): React.ReactNode {
// const [containerElement, setContainerElement] = useState<HTMLElement | null>( // const [containerElement, setContainerElement] = useState<HTMLElement | null>(
// null // null
// ); // );

View File

@ -5,10 +5,10 @@ import clsx from "clsx";
import Link from "@docusaurus/Link"; import Link from "@docusaurus/Link";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import "./styles.css";
import Tag from "@/components/Resource/Tag"; import Tag from "@/components/Resource/Tag";
import ValidStatus from "@/components/Resource/ValidStatus"; import ValidStatus from "@/components/Resource/ValidStatus";
import type { Resource } from "@/libs/store"; import type { Resource } from "@/libs/store";
import "./styles.css";
export type Props = { export type Props = {
resource: Resource; resource: Resource;
@ -24,7 +24,7 @@ export default function ResourceCard({
onTagClick, onTagClick,
onAuthorClick, onAuthorClick,
className, className,
}: Props): JSX.Element { }: Props): React.ReactNode {
const isGithub = /^https:\/\/github.com\/[^/]+\/[^/]+/.test( const isGithub = /^https:\/\/github.com\/[^/]+\/[^/]+/.test(
resource.homepage resource.homepage
); );

View File

@ -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) => const fetchPypiProject = (projectName: string) =>
fetch(`https://pypi.org/pypi/${projectName}/json`) fetch(`https://pypi.org/pypi/${projectName}/json`)
.then((response) => response.json()) .then((response) => response.json())
@ -99,8 +108,9 @@ export default function ResourceDetailCard({ resource }: Props) {
const projectLink = getProjectLink(resource) || "无"; const projectLink = getProjectLink(resource) || "无";
const moduleName = getModuleName(resource) || "无"; const moduleName = getModuleName(resource) || "无";
const homepageLink = getHomepageLink(resource) || undefined; const homepageLink = getHomepageLink(resource);
const pypiProjectLink = getPypiProjectLink(resource) || undefined; const pypiProjectLink = getPypiProjectLink(resource);
const updatedTime = getPluginStatusUpdatedTime(resource);
return ( return (
<> <>
@ -183,31 +193,39 @@ export default function ResourceDetailCard({ resource }: Props) {
{(pypiData && pypiData.info.version) || "无"} {(pypiData && pypiData.info.version) || "无"}
</div> </div>
<div className="detail-card-meta-item"> {homepageLink && (
<FontAwesomeIcon fixedWidth icon={["fas", "fingerprint"]} />{" "} <div className="detail-card-meta-item">
<a <FontAwesomeIcon fixedWidth icon={["fas", "fingerprint"]} />{" "}
href={homepageLink} <a
target="_blank" href={homepageLink}
rel="noreferrer" target="_blank"
className={homepageLink && "hover:underline hover:text-primary"} rel="noreferrer"
> className="detail-card-meta-item-link"
{moduleName} >
</a> {moduleName}
</div> </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"> <div className="detail-card-meta-item">
<FontAwesomeIcon fixedWidth icon={["fas", "cubes"]} />{" "} <FontAwesomeIcon fixedWidth icon={["fas", "clock-rotate-left"]} />{" "}
<a {updatedTime}
href={pypiProjectLink}
target="_blank"
rel="noreferrer"
className={
pypiProjectLink && "hover:underline hover:text-primary"
}
>
{projectLink}
</a>
</div> </div>
<div className="detail-card-actions"> <div className="detail-card-actions">
<ValidStatus <ValidStatus
resource={resource} resource={resource}

View File

@ -24,7 +24,7 @@
} }
&-actions { &-actions {
@apply flex items-center gap-x-2 ml-auto; @apply flex items-center gap-x-2 lg:ml-auto;
&-button { &-button {
@apply btn btn-sm; @apply btn btn-sm;
@ -51,11 +51,15 @@
} }
&-right { &-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 { &-meta-item {
@apply text-sm truncate; @apply text-sm truncate;
&-link {
@apply hover:text-primary hover:transition;
}
} }
} }

View File

@ -2,9 +2,9 @@ import React from "react";
import clsx from "clsx"; import clsx from "clsx";
import "./styles.css";
import { pickTextColor } from "@/libs/color"; import { pickTextColor } from "@/libs/color";
import type { Tag } from "@/types/tag"; import type { Tag } from "@/types/tag";
import "./styles.css";
export type Props = Tag & { export type Props = Tag & {
className?: string; className?: string;
@ -16,7 +16,7 @@ export default function ResourceTag({
color, color,
className, className,
onClick, onClick,
}: Props): JSX.Element { }: Props): React.ReactNode {
return ( return (
<span <span
className={clsx("resource-tag", className)} className={clsx("resource-tag", className)}

View File

@ -2,9 +2,9 @@ import React, { useRef } from "react";
import clsx from "clsx"; import clsx from "clsx";
import "./styles.css";
import { translate } from "@docusaurus/Translate"; import { translate } from "@docusaurus/Translate";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import "./styles.css";
export type Props = { export type Props = {
onChange: (value: string) => void; onChange: (value: string) => void;
@ -28,7 +28,7 @@ export default function Searcher({
className, className,
placeholder, placeholder,
disabled = false, disabled = false,
}: Props): JSX.Element { }: Props): React.ReactNode {
const ref = useRef<HTMLInputElement>(null); const ref = useRef<HTMLInputElement>(null);
const handleSubmit = (e: React.FormEvent<HTMLInputElement>) => { const handleSubmit = (e: React.FormEvent<HTMLInputElement>) => {
@ -85,6 +85,10 @@ export default function Searcher({
onClick={() => onTagClick(index)} onClick={() => onTagClick(index)}
> >
{tag} {tag}
<FontAwesomeIcon
className="searcher-action-icon close ml-1"
icon={["fas", "xmark"]}
/>
</div> </div>
))} ))}
<input <input

View File

@ -18,7 +18,7 @@ import { fetchRegistryData, loadFailedTitle } from "@/libs/store";
import { useToolbar } from "@/libs/toolbar"; import { useToolbar } from "@/libs/toolbar";
import type { Adapter } from "@/types/adapter"; 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 [adapters, setAdapters] = useState<Adapter[] | null>(null);
const adapterCount = adapters?.length ?? 0; const adapterCount = adapters?.length ?? 0;
const loading = adapters === null; const loading = adapters === null;

View File

@ -17,7 +17,7 @@ import { fetchRegistryData, loadFailedTitle } from "@/libs/store";
import { useToolbar } from "@/libs/toolbar"; import { useToolbar } from "@/libs/toolbar";
import type { Bot } from "@/types/bot"; 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 [bots, setBots] = useState<Bot[] | null>(null);
const botCount = bots?.length ?? 0; const botCount = bots?.length ?? 0;
const loading = bots === null; const loading = bots === null;

View File

@ -15,7 +15,7 @@ import { useSearchControl } from "@/libs/search";
import { fetchRegistryData, loadFailedTitle } from "@/libs/store"; import { fetchRegistryData, loadFailedTitle } from "@/libs/store";
import type { Driver } from "@/types/driver"; 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 [drivers, setDrivers] = useState<Driver[] | null>(null);
const driverCount = drivers?.length ?? 0; const driverCount = drivers?.length ?? 0;
const loading = drivers === null; const loading = drivers === null;

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from "react"; 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 { usePagination } from "react-use-pagination";
import Admonition from "@theme/Admonition"; import Admonition from "@theme/Admonition";
@ -11,14 +11,18 @@ import Paginate from "@/components/Paginate";
import ResourceCard from "@/components/Resource/Card"; import ResourceCard from "@/components/Resource/Card";
import ResourceDetailCard from "@/components/Resource/DetailCard"; import ResourceDetailCard from "@/components/Resource/DetailCard";
import Searcher from "@/components/Searcher"; 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 { authorFilter, tagFilter } from "@/libs/filter";
import { useSearchControl } from "@/libs/search"; import { useSearchControl } from "@/libs/search";
import { SortMode } from "@/libs/sorter";
import { fetchRegistryData, loadFailedTitle } from "@/libs/store"; import { fetchRegistryData, loadFailedTitle } from "@/libs/store";
import { useToolbar } from "@/libs/toolbar"; import { useToolbar } from "@/libs/toolbar";
import type { Plugin } from "@/types/plugin"; 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 [plugins, setPlugins] = useState<Plugin[] | null>(null);
const pluginCount = plugins?.length ?? 0; const pluginCount = plugins?.length ?? 0;
const loading = plugins === null; const loading = plugins === null;
@ -27,6 +31,38 @@ export default function PluginPage(): JSX.Element {
const [isOpenModal, setIsOpenModal] = useState<boolean>(false); const [isOpenModal, setIsOpenModal] = useState<boolean>(false);
const [isOpenCardModal, setIsOpenCardModal] = useState<boolean>(false); const [isOpenCardModal, setIsOpenCardModal] = useState<boolean>(false);
const [clickedPlugin, setClickedPlugin] = useState<Plugin | null>(null); 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 { const {
filteredResources: filteredPlugins, filteredResources: filteredPlugins,
@ -37,7 +73,7 @@ export default function PluginPage(): JSX.Element {
onSearchBackspace, onSearchBackspace,
onSearchClear, onSearchClear,
onSearchTagClick, onSearchTagClick,
} = useSearchControl<Plugin>(plugins ?? []); } = useSearchControl<Plugin>(getSortedPlugins(plugins ?? []));
const filteredPluginCount = filteredPlugins.length; const filteredPluginCount = filteredPlugins.length;
const { const {
@ -134,6 +170,7 @@ export default function PluginPage(): JSX.Element {
<StoreToolbar <StoreToolbar
className="not-prose" className="not-prose"
filters={filterTools} filters={filterTools}
sorter={sorterTool}
action={actionTool} action={actionTool}
/> />

View File

@ -18,7 +18,7 @@ type Props = {
children: React.ReactNode; children: React.ReactNode;
}; };
function StorePage({ title, children }: Props): JSX.Element { function StorePage({ title, children }: Props): React.ReactNode {
const sidebarItems = useVersionedSidebar( const sidebarItems = useVersionedSidebar(
useDocsVersionCandidates()[0].name, useDocsVersionCandidates()[0].name,
SIDEBAR_ID 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 ( return (
<> <>
<PageMetadata title={title} /> <PageMetadata title={title} />

View File

@ -12,6 +12,13 @@ export type Filter = {
onSubmit: (query: string) => void; onSubmit: (query: string) => void;
}; };
export type Sorter = {
label: string;
icon: IconProp;
active: boolean;
onClick: () => void;
};
export type Action = { export type Action = {
label: string; label: string;
icon: IconProp; icon: IconProp;
@ -20,6 +27,7 @@ export type Action = {
export type Props = { export type Props = {
filters?: Filter[]; filters?: Filter[];
sorter?: Sorter;
action?: Action; action?: Action;
className?: string; className?: string;
}; };
@ -29,7 +37,7 @@ function ToolbarFilter({
icon, icon,
choices, choices,
onSubmit, onSubmit,
}: Filter): JSX.Element { }: Filter): React.ReactNode {
const [query, setQuery] = useState<string>(""); const [query, setQuery] = useState<string>("");
const filteredChoices = choices const filteredChoices = choices
@ -96,33 +104,65 @@ function ToolbarFilter({
export default function StoreToolbar({ export default function StoreToolbar({
filters, filters,
sorter,
action, action,
className, className,
}: Props): JSX.Element | null { }: Props): React.ReactNode | null {
if (!(filters && filters.length > 0) && !action) { if (!(filters && filters.length > 0) && !action) {
return null; return null;
} }
return ( return (
<div className={clsx("store-toolbar", className)}> <>
{filters && filters.length > 0 && ( <div className={clsx("store-toolbar", className)}>
<div className="store-toolbar-filters"> <div className="store-toolbar-filters">
{filters.map((filter, index) => ( {filters?.map((filter, index) => (
<ToolbarFilter key={index} {...filter} /> <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> </div>
)}
{action && ( {action && (
<div className="store-toolbar-action"> <div className="store-toolbar-action">
<button <button
className="btn btn-sm btn-primary no-animation" className="btn btn-sm btn-primary no-animation"
onClick={action.onClick} onClick={action.onClick}
> >
<FontAwesomeIcon icon={action.icon} /> <FontAwesomeIcon icon={action.icon} />
{action.label} {action.label}
</button> </button>
</div> </div>
)} )}
</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>
</>
); );
} }

View File

@ -14,6 +14,18 @@
&-toolbar { &-toolbar {
@apply flex items-center justify-center my-4; @apply flex items-center justify-center my-4;
&-second {
@apply lg:hidden;
}
&-sorter {
@apply max-lg:flex-1;
&-desktop {
@apply max-lg:hidden;
}
}
&-filters { &-filters {
@apply flex grow gap-2; @apply flex grow gap-2;
} }

View File

@ -15,7 +15,7 @@ export default function Tag({
}: TagType & { }: TagType & {
className?: string; className?: string;
onClick?: React.MouseEventHandler<HTMLSpanElement>; onClick?: React.MouseEventHandler<HTMLSpanElement>;
}): JSX.Element { }): React.ReactNode {
return ( return (
<span <span
className={clsx("tag", className)} className={clsx("tag", className)}

View File

@ -3,14 +3,52 @@ import { useCallback, useState } from "react";
import { translate } from "@docusaurus/Translate"; import { translate } from "@docusaurus/Translate";
import type { Resource } from "./store"; import type { Resource } from "./store";
import { ValidStatus } from "./valid";
import { getValidStatus } from "@/components/Resource/ValidStatus";
export type Filter<T extends Resource = Resource> = { export type Filter<T extends Resource = Resource> = {
type: string; type: string;
id: string; id: string;
displayName: string; displayName?: string;
filter: (resource: T) => boolean; 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>( export const tagFilter = <T extends Resource = Resource>(
tag: string tag: string
): Filter<T> => ({ ): Filter<T> => ({
@ -27,6 +65,7 @@ export const tagFilter = <T extends Resource = Resource>(
filter: (resource: Resource): boolean => filter: (resource: Resource): boolean =>
resource.tags.map((tag) => tag.label).includes(tag), resource.tags.map((tag) => tag.label).includes(tag),
}); });
export const officialFilter = <T extends Resource = Resource>( export const officialFilter = <T extends Resource = Resource>(
official: boolean = true official: boolean = true
): Filter<T> => ({ ): Filter<T> => ({
@ -39,6 +78,7 @@ export const officialFilter = <T extends Resource = Resource>(
}).split("|")[Number(official)], }).split("|")[Number(official)],
filter: (resource: Resource): boolean => resource.is_official === official, filter: (resource: Resource): boolean => resource.is_official === official,
}); });
export const authorFilter = <T extends Resource = Resource>( export const authorFilter = <T extends Resource = Resource>(
author: string author: string
): Filter<T> => ({ ): Filter<T> => ({
@ -54,6 +94,7 @@ export const authorFilter = <T extends Resource = Resource>(
), ),
filter: (resource: Resource): boolean => resource.author === author, filter: (resource: Resource): boolean => resource.author === author,
}); });
export const queryFilter = <T extends Resource = Resource>( export const queryFilter = <T extends Resource = Resource>(
query: string query: string
): Filter<T> => ({ ): Filter<T> => ({

View File

@ -0,0 +1,4 @@
export enum SortMode {
Default,
UpdateDesc,
}

View File

@ -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 type { Resource } from "./store";
import { ValidStatus } from "./valid";
import type { Filter as FilterTool } from "@/components/Store/Toolbar"; 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 { return {
filters: [authorFilterTool, tagFilterTool], filters: [authorFilterTool, tagFilterTool, validStatusFilterTool],
}; };
} }

View File

@ -4,7 +4,7 @@ import Layout from "@theme/Layout";
import HomeContent from "@/components/Home"; import HomeContent from "@/components/Home";
export default function Homepage(): JSX.Element { export default function Homepage(): React.ReactNode {
return ( return (
<Layout> <Layout>
<HomeContent /> <HomeContent />

View File

@ -5,7 +5,7 @@ import { translate } from "@docusaurus/Translate";
import AdapterPageContent from "@/components/Store/Content/Adapter"; import AdapterPageContent from "@/components/Store/Content/Adapter";
import StoreLayout from "@/components/Store/Layout"; import StoreLayout from "@/components/Store/Layout";
export default function StoreAdapters(): JSX.Element { export default function StoreAdapters(): React.ReactNode {
const title = translate({ const title = translate({
id: "pages.store.adapter.title", id: "pages.store.adapter.title",
message: "适配器商店", message: "适配器商店",

View File

@ -5,7 +5,7 @@ import { translate } from "@docusaurus/Translate";
import BotPageContent from "@/components/Store/Content/Bot"; import BotPageContent from "@/components/Store/Content/Bot";
import StoreLayout from "@/components/Store/Layout"; import StoreLayout from "@/components/Store/Layout";
export default function StoreBots(): JSX.Element { export default function StoreBots(): React.ReactNode {
const title = translate({ const title = translate({
id: "pages.store.bot.title", id: "pages.store.bot.title",
message: "机器人商店", message: "机器人商店",

View File

@ -5,7 +5,7 @@ import { translate } from "@docusaurus/Translate";
import DriverPageContent from "@/components/Store/Content/Driver"; import DriverPageContent from "@/components/Store/Content/Driver";
import StoreLayout from "@/components/Store/Layout"; import StoreLayout from "@/components/Store/Layout";
export default function StoreDrivers(): JSX.Element { export default function StoreDrivers(): React.ReactNode {
const title = translate({ const title = translate({
id: "pages.store.driver.title", id: "pages.store.driver.title",
message: "驱动器商店", message: "驱动器商店",

View File

@ -2,6 +2,6 @@ import React from "react";
import { Redirect } from "@docusaurus/router"; import { Redirect } from "@docusaurus/router";
export default function Store(): JSX.Element { export default function Store(): React.ReactNode {
return <Redirect to="/store/plugins" />; return <Redirect to="/store/plugins" />;
} }

View File

@ -5,7 +5,7 @@ import { translate } from "@docusaurus/Translate";
import PluginPageContent from "@/components/Store/Content/Plugin"; import PluginPageContent from "@/components/Store/Content/Plugin";
import StoreLayout from "@/components/Store/Layout"; import StoreLayout from "@/components/Store/Layout";
export default function StorePlugins(): JSX.Element { export default function StorePlugins(): React.ReactNode {
const title = translate({ const title = translate({
id: "pages.store.plugin.title", id: "pages.store.plugin.title",
message: "插件商店", message: "插件商店",

View File

@ -2,7 +2,7 @@ import React, { type ComponentProps } from "react";
export interface Props extends Omit<ComponentProps<"svg">, "viewBox"> {} 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 ( return (
<svg <svg
viewBox="0 0 651.29 94.76" viewBox="0 0 651.29 94.76"

View File

@ -2,7 +2,7 @@ import React, { type ComponentProps } from "react";
export interface Props extends Omit<ComponentProps<"svg">, "viewBox"> {} 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 ( return (
<svg viewBox="0 0 256 105" xmlns="http://www.w3.org/2000/svg" {...props}> <svg viewBox="0 0 256 105" xmlns="http://www.w3.org/2000/svg" {...props}>
<g clipPath="url(#clip0_236_25)"> <g clipPath="url(#clip0_236_25)">

View File

@ -9,7 +9,7 @@ import "./styles.css";
export default function TOCContainer({ export default function TOCContainer({
children, children,
...props ...props
}: Props): JSX.Element { }: Props): React.ReactNode {
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const isClient = windowSize !== "ssr"; const isClient = windowSize !== "ssr";

4267
yarn.lock

File diff suppressed because it is too large Load Diff