📝 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",
"private": true,
"packageManager": "yarn@1.22.22",
"workspaces": [
"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 { 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",

View File

@ -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": [

View File

@ -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(() => {

View File

@ -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={

View File

@ -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: "基本信息",

View File

@ -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: "基本信息",

View File

@ -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");

View File

@ -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: "包信息",

View File

@ -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">

View File

@ -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

View File

@ -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();

View File

@ -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 />

View File

@ -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">

View File

@ -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");

View File

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

View File

@ -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
);

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) =>
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}

View File

@ -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;
}
}
}

View File

@ -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)}

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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}
/>

View File

@ -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} />

View File

@ -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>
</>
);
}

View File

@ -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;
}

View File

@ -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)}

View File

@ -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> => ({

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 { 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],
};
}

View File

@ -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 />

View File

@ -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: "适配器商店",

View File

@ -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: "机器人商店",

View File

@ -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: "驱动器商店",

View File

@ -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" />;
}

View File

@ -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: "插件商店",

View File

@ -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"

View File

@ -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)">

View File

@ -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";

4267
yarn.lock

File diff suppressed because it is too large Load Diff