mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-09-10 14:07:04 +00:00
📝 Docs: 升级新版 NonePress 主题 (#2375)
This commit is contained in:
175
website/src/components/Store/Content/Adapter.tsx
Normal file
175
website/src/components/Store/Content/Adapter.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import Translate from "@docusaurus/Translate";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import Admonition from "@theme/Admonition";
|
||||
|
||||
import Paginate from "@/components/Paginate";
|
||||
import ResourceCard from "@/components/Resource/Card";
|
||||
import Searcher from "@/components/Searcher";
|
||||
import StoreToolbar, { type Action } from "@/components/Store/Toolbar";
|
||||
import { authorFilter, tagFilter } from "@/libs/filter";
|
||||
import { useSearchControl } from "@/libs/search";
|
||||
import { fetchRegistryData, loadFailedTitle } from "@/libs/store";
|
||||
import { useToolbar } from "@/libs/toolbar";
|
||||
import type { Adapter } from "@/types/adapter";
|
||||
|
||||
export default function AdapterPage(): JSX.Element {
|
||||
const [adapters, setAdapters] = useState<Adapter[] | null>(null);
|
||||
const adapterCount = adapters?.length ?? 0;
|
||||
const loading = adapters === null;
|
||||
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const {
|
||||
filteredResources: filteredAdapters,
|
||||
searcherTags,
|
||||
addFilter,
|
||||
onSearchQueryChange,
|
||||
onSearchQuerySubmit,
|
||||
onSearchBackspace,
|
||||
onSearchClear,
|
||||
onSearchTagClick,
|
||||
} = useSearchControl<Adapter>(adapters ?? []);
|
||||
const filteredAdapterCount = filteredAdapters.length;
|
||||
|
||||
const {
|
||||
startIndex,
|
||||
endIndex,
|
||||
totalPages,
|
||||
currentPage,
|
||||
setNextPage,
|
||||
setPreviousPage,
|
||||
setPage,
|
||||
previousEnabled,
|
||||
nextEnabled,
|
||||
} = usePagination({
|
||||
totalItems: filteredAdapters.length,
|
||||
initialPageSize: 12,
|
||||
});
|
||||
const currentAdapters = filteredAdapters.slice(startIndex, endIndex + 1);
|
||||
|
||||
// load adapters asynchronously
|
||||
useEffect(() => {
|
||||
fetchRegistryData("adapter")
|
||||
.then(setAdapters)
|
||||
.catch((e) => {
|
||||
setError(e);
|
||||
console.error(e);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { filters: filterTools } = useToolbar({
|
||||
resources: adapters ?? [],
|
||||
addFilter,
|
||||
});
|
||||
|
||||
const actionTool: Action = {
|
||||
label: "发布适配器",
|
||||
icon: ["fas", "plus"],
|
||||
onClick: () => {
|
||||
// TODO: open adapter release modal
|
||||
window.open(
|
||||
"https://github.com/nonebot/nonebot2/issues/new?template=adapter_publish.yml"
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const onCardClick = useCallback((adapter: Adapter) => {
|
||||
// TODO: open adapter modal
|
||||
console.log(adapter, "clicked");
|
||||
}, []);
|
||||
|
||||
const onCardTagClick = useCallback(
|
||||
(tag: string) => {
|
||||
addFilter(tagFilter(tag));
|
||||
},
|
||||
[addFilter]
|
||||
);
|
||||
|
||||
const onCardAuthorClick = useCallback(
|
||||
(author: string) => {
|
||||
addFilter(authorFilter(author));
|
||||
},
|
||||
[addFilter]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="store-description">
|
||||
{adapterCount === filteredAdapterCount ? (
|
||||
<Translate
|
||||
id="pages.store.adapter.info"
|
||||
description="Adapters info of the adapter store page"
|
||||
values={{ adapterCount }}
|
||||
>
|
||||
{"当前共有 {adapterCount} 个适配器"}
|
||||
</Translate>
|
||||
) : (
|
||||
<Translate
|
||||
id="pages.store.adapter.searchInfo"
|
||||
description="Adapters search info of the adapter store page"
|
||||
values={{
|
||||
adapterCount,
|
||||
filteredAdapterCount,
|
||||
}}
|
||||
>
|
||||
{"当前共有 {filteredAdapterCount} / {adapterCount} 个插件"}
|
||||
</Translate>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<Searcher
|
||||
className="store-searcher not-prose"
|
||||
onChange={onSearchQueryChange}
|
||||
onSubmit={onSearchQuerySubmit}
|
||||
onBackspace={onSearchBackspace}
|
||||
onClear={onSearchClear}
|
||||
onTagClick={onSearchTagClick}
|
||||
tags={searcherTags}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
<StoreToolbar
|
||||
className="not-prose"
|
||||
filters={filterTools}
|
||||
action={actionTool}
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
<Admonition type="caution" title={loadFailedTitle}>
|
||||
{error.message}
|
||||
</Admonition>
|
||||
) : loading ? (
|
||||
<p className="store-loading-container">
|
||||
<span className="loading loading-dots loading-lg store-loading"></span>
|
||||
</p>
|
||||
) : (
|
||||
<div className="store-container">
|
||||
{currentAdapters.map((adapter, index) => (
|
||||
<ResourceCard
|
||||
key={index}
|
||||
className="not-prose"
|
||||
resource={adapter}
|
||||
onClick={() => onCardClick(adapter)}
|
||||
onTagClick={onCardTagClick}
|
||||
onAuthorClick={() => onCardAuthorClick(adapter.author)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Paginate
|
||||
className="not-prose"
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
setNextPage={setNextPage}
|
||||
setPreviousPage={setPreviousPage}
|
||||
setPage={setPage}
|
||||
nextEnabled={nextEnabled}
|
||||
previousEnabled={previousEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
169
website/src/components/Store/Content/Bot.tsx
Normal file
169
website/src/components/Store/Content/Bot.tsx
Normal file
@ -0,0 +1,169 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import Translate from "@docusaurus/Translate";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import Admonition from "@theme/Admonition";
|
||||
|
||||
import Paginate from "@/components/Paginate";
|
||||
import ResourceCard from "@/components/Resource/Card";
|
||||
import Searcher from "@/components/Searcher";
|
||||
import StoreToolbar, { type Action } from "@/components/Store/Toolbar";
|
||||
import { authorFilter, tagFilter } from "@/libs/filter";
|
||||
import { useSearchControl } from "@/libs/search";
|
||||
import { fetchRegistryData, loadFailedTitle } from "@/libs/store";
|
||||
import { useToolbar } from "@/libs/toolbar";
|
||||
import type { Bot } from "@/types/bot";
|
||||
|
||||
export default function PluginPage(): JSX.Element {
|
||||
const [bots, setBots] = useState<Bot[] | null>(null);
|
||||
const botCount = bots?.length ?? 0;
|
||||
const loading = bots === null;
|
||||
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const {
|
||||
filteredResources: filteredBots,
|
||||
searcherTags,
|
||||
addFilter,
|
||||
onSearchQueryChange,
|
||||
onSearchQuerySubmit,
|
||||
onSearchBackspace,
|
||||
onSearchClear,
|
||||
onSearchTagClick,
|
||||
} = useSearchControl<Bot>(bots ?? []);
|
||||
const filteredBotCount = filteredBots.length;
|
||||
|
||||
const {
|
||||
startIndex,
|
||||
endIndex,
|
||||
totalPages,
|
||||
currentPage,
|
||||
setNextPage,
|
||||
setPreviousPage,
|
||||
setPage,
|
||||
previousEnabled,
|
||||
nextEnabled,
|
||||
} = usePagination({
|
||||
totalItems: filteredBots.length,
|
||||
initialPageSize: 12,
|
||||
});
|
||||
const currentBots = filteredBots.slice(startIndex, endIndex + 1);
|
||||
|
||||
// load bots asynchronously
|
||||
useEffect(() => {
|
||||
fetchRegistryData("bot")
|
||||
.then(setBots)
|
||||
.catch((e) => {
|
||||
setError(e);
|
||||
console.error(e);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { filters: filterTools } = useToolbar({
|
||||
resources: bots ?? [],
|
||||
addFilter,
|
||||
});
|
||||
|
||||
const actionTool: Action = {
|
||||
label: "发布机器人",
|
||||
icon: ["fas", "plus"],
|
||||
onClick: () => {
|
||||
// TODO: open bot release modal
|
||||
window.open(
|
||||
"https://github.com/nonebot/nonebot2/issues/new?template=bot_publish.yml"
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const onCardTagClick = useCallback(
|
||||
(tag: string) => {
|
||||
addFilter(tagFilter(tag));
|
||||
},
|
||||
[addFilter]
|
||||
);
|
||||
|
||||
const onAuthorClick = useCallback(
|
||||
(author: string) => {
|
||||
addFilter(authorFilter(author));
|
||||
},
|
||||
[addFilter]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="store-description">
|
||||
{botCount === filteredBotCount ? (
|
||||
<Translate
|
||||
id="pages.store.bot.info"
|
||||
description="Bots info of the bot store page"
|
||||
values={{ botCount }}
|
||||
>
|
||||
{"当前共有 {botCount} 个机器人"}
|
||||
</Translate>
|
||||
) : (
|
||||
<Translate
|
||||
id="pages.store.bot.searchInfo"
|
||||
description="Bots search info of the bot store page"
|
||||
values={{
|
||||
botCount,
|
||||
filteredBotCount,
|
||||
}}
|
||||
>
|
||||
{"当前共有 {filteredBotCount} / {botCount} 个机器人"}
|
||||
</Translate>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<Searcher
|
||||
className="store-searcher not-prose"
|
||||
onChange={onSearchQueryChange}
|
||||
onSubmit={onSearchQuerySubmit}
|
||||
onBackspace={onSearchBackspace}
|
||||
onClear={onSearchClear}
|
||||
onTagClick={onSearchTagClick}
|
||||
tags={searcherTags}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
<StoreToolbar
|
||||
className="not-prose"
|
||||
filters={filterTools}
|
||||
action={actionTool}
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
<Admonition type="caution" title={loadFailedTitle}>
|
||||
{error.message}
|
||||
</Admonition>
|
||||
) : loading ? (
|
||||
<p className="store-loading-container">
|
||||
<span className="loading loading-dots loading-lg store-loading"></span>
|
||||
</p>
|
||||
) : (
|
||||
<div className="store-container">
|
||||
{currentBots.map((bot, index) => (
|
||||
<ResourceCard
|
||||
key={index}
|
||||
className="not-prose"
|
||||
resource={bot}
|
||||
onTagClick={onCardTagClick}
|
||||
onAuthorClick={() => onAuthorClick(bot.author)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Paginate
|
||||
className="not-prose"
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
setNextPage={setNextPage}
|
||||
setPreviousPage={setPreviousPage}
|
||||
setPage={setPage}
|
||||
nextEnabled={nextEnabled}
|
||||
previousEnabled={previousEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
151
website/src/components/Store/Content/Driver.tsx
Normal file
151
website/src/components/Store/Content/Driver.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import Translate from "@docusaurus/Translate";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import Admonition from "@theme/Admonition";
|
||||
|
||||
import Paginate from "@/components/Paginate";
|
||||
import ResourceCard from "@/components/Resource/Card";
|
||||
import Searcher from "@/components/Searcher";
|
||||
import { authorFilter, tagFilter } from "@/libs/filter";
|
||||
import { useSearchControl } from "@/libs/search";
|
||||
import { fetchRegistryData, loadFailedTitle } from "@/libs/store";
|
||||
import type { Driver } from "@/types/driver";
|
||||
|
||||
export default function DriverPage(): JSX.Element {
|
||||
const [drivers, setDrivers] = useState<Driver[] | null>(null);
|
||||
const driverCount = drivers?.length ?? 0;
|
||||
const loading = drivers === null;
|
||||
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const {
|
||||
filteredResources: filteredDrivers,
|
||||
searcherTags,
|
||||
addFilter,
|
||||
onSearchQueryChange,
|
||||
onSearchQuerySubmit,
|
||||
onSearchBackspace,
|
||||
onSearchClear,
|
||||
onSearchTagClick,
|
||||
} = useSearchControl<Driver>(drivers ?? []);
|
||||
const filteredDriverCount = filteredDrivers.length;
|
||||
|
||||
const {
|
||||
startIndex,
|
||||
endIndex,
|
||||
totalPages,
|
||||
currentPage,
|
||||
setNextPage,
|
||||
setPreviousPage,
|
||||
setPage,
|
||||
previousEnabled,
|
||||
nextEnabled,
|
||||
} = usePagination({
|
||||
totalItems: filteredDrivers.length,
|
||||
initialPageSize: 12,
|
||||
});
|
||||
const currentDrivers = filteredDrivers.slice(startIndex, endIndex + 1);
|
||||
|
||||
// load drivers asynchronously
|
||||
useEffect(() => {
|
||||
fetchRegistryData("driver")
|
||||
.then(setDrivers)
|
||||
.catch((e) => {
|
||||
setError(e);
|
||||
console.error(e);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onCardClick = useCallback((driver: Driver) => {
|
||||
// TODO: open driver modal
|
||||
console.log(driver, "clicked");
|
||||
}, []);
|
||||
|
||||
const onCardTagClick = useCallback(
|
||||
(tag: string) => {
|
||||
addFilter(tagFilter(tag));
|
||||
},
|
||||
[addFilter]
|
||||
);
|
||||
|
||||
const onAuthorClick = useCallback(
|
||||
(author: string) => {
|
||||
addFilter(authorFilter(author));
|
||||
},
|
||||
[addFilter]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="store-description">
|
||||
{driverCount === filteredDriverCount ? (
|
||||
<Translate
|
||||
id="pages.store.driver.info"
|
||||
description="Drivers info of the driver store page"
|
||||
values={{ driverCount }}
|
||||
>
|
||||
{"当前共有 {driverCount} 个插件"}
|
||||
</Translate>
|
||||
) : (
|
||||
<Translate
|
||||
id="pages.store.driver.searchInfo"
|
||||
description="Drivers search info of the driver store page"
|
||||
values={{
|
||||
driverCount,
|
||||
filteredDriverCount,
|
||||
}}
|
||||
>
|
||||
{"当前共有 {filteredDriverCount} / {driverCount} 个插件"}
|
||||
</Translate>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<Searcher
|
||||
className="store-searcher not-prose"
|
||||
onChange={onSearchQueryChange}
|
||||
onSubmit={onSearchQuerySubmit}
|
||||
onBackspace={onSearchBackspace}
|
||||
onClear={onSearchClear}
|
||||
onTagClick={onSearchTagClick}
|
||||
tags={searcherTags}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
<Admonition type="caution" title={loadFailedTitle}>
|
||||
{error.message}
|
||||
</Admonition>
|
||||
) : loading ? (
|
||||
<p className="store-loading-container">
|
||||
<span className="loading loading-dots loading-lg store-loading"></span>
|
||||
</p>
|
||||
) : (
|
||||
<div className="store-container">
|
||||
{currentDrivers.map((driver, index) => (
|
||||
<ResourceCard
|
||||
key={index}
|
||||
className="not-prose"
|
||||
resource={driver}
|
||||
onClick={() => onCardClick(driver)}
|
||||
onTagClick={onCardTagClick}
|
||||
onAuthorClick={() => onAuthorClick(driver.author)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Paginate
|
||||
className="not-prose"
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
setNextPage={setNextPage}
|
||||
setPreviousPage={setPreviousPage}
|
||||
setPage={setPage}
|
||||
nextEnabled={nextEnabled}
|
||||
previousEnabled={previousEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
172
website/src/components/Store/Content/Plugin.tsx
Normal file
172
website/src/components/Store/Content/Plugin.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import Translate from "@docusaurus/Translate";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import Admonition from "@theme/Admonition";
|
||||
|
||||
import Paginate from "@/components/Paginate";
|
||||
import ResourceCard from "@/components/Resource/Card";
|
||||
import Searcher from "@/components/Searcher";
|
||||
import StoreToolbar, { type Action } from "@/components/Store/Toolbar";
|
||||
import { authorFilter, tagFilter } from "@/libs/filter";
|
||||
import { useSearchControl } from "@/libs/search";
|
||||
import { fetchRegistryData, loadFailedTitle } from "@/libs/store";
|
||||
import { useToolbar } from "@/libs/toolbar";
|
||||
import type { Plugin } from "@/types/plugin";
|
||||
|
||||
export default function PluginPage(): JSX.Element {
|
||||
const [plugins, setPlugins] = useState<Plugin[] | null>(null);
|
||||
const pluginCount = plugins?.length ?? 0;
|
||||
const loading = plugins === null;
|
||||
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const {
|
||||
filteredResources: filteredPlugins,
|
||||
searcherTags,
|
||||
addFilter,
|
||||
onSearchQueryChange,
|
||||
onSearchQuerySubmit,
|
||||
onSearchBackspace,
|
||||
onSearchClear,
|
||||
onSearchTagClick,
|
||||
} = useSearchControl<Plugin>(plugins ?? []);
|
||||
const filteredPluginCount = filteredPlugins.length;
|
||||
|
||||
const {
|
||||
startIndex,
|
||||
endIndex,
|
||||
totalPages,
|
||||
currentPage,
|
||||
setNextPage,
|
||||
setPreviousPage,
|
||||
setPage,
|
||||
previousEnabled,
|
||||
nextEnabled,
|
||||
} = usePagination({
|
||||
totalItems: filteredPlugins.length,
|
||||
initialPageSize: 12,
|
||||
});
|
||||
const currentPlugins = filteredPlugins.slice(startIndex, endIndex + 1);
|
||||
|
||||
// load plugins asynchronously
|
||||
useEffect(() => {
|
||||
fetchRegistryData("plugin")
|
||||
.then(setPlugins)
|
||||
.catch((e) => {
|
||||
setError(e);
|
||||
console.error(e);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { filters: filterTools } = useToolbar({
|
||||
resources: plugins ?? [],
|
||||
addFilter,
|
||||
});
|
||||
|
||||
const actionTool: Action = {
|
||||
label: "发布插件",
|
||||
icon: ["fas", "plus"],
|
||||
onClick: () => {
|
||||
// TODO: open plugin release modal
|
||||
window.open(
|
||||
"https://github.com/nonebot/nonebot2/issues/new?template=plugin_publish.yml"
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const onCardClick = useCallback((plugin: Plugin) => {
|
||||
// TODO: open plugin modal
|
||||
console.log(plugin, "clicked");
|
||||
}, []);
|
||||
|
||||
const onCardTagClick = useCallback(
|
||||
(tag: string) => {
|
||||
addFilter(tagFilter(tag));
|
||||
},
|
||||
[addFilter]
|
||||
);
|
||||
|
||||
const onCardAuthorClick = useCallback(
|
||||
(author: string) => {
|
||||
addFilter(authorFilter(author));
|
||||
},
|
||||
[addFilter]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="store-description">
|
||||
{pluginCount === filteredPluginCount ? (
|
||||
<Translate
|
||||
id="pages.store.plugin.info"
|
||||
description="Plugins info of the plugin store page"
|
||||
values={{ pluginCount }}
|
||||
>
|
||||
{"当前共有 {pluginCount} 个插件"}
|
||||
</Translate>
|
||||
) : (
|
||||
<Translate
|
||||
id="pages.store.plugin.searchInfo"
|
||||
description="Plugins search info of the plugin store page"
|
||||
values={{ pluginCount, filteredPluginCount: filteredPluginCount }}
|
||||
>
|
||||
{"当前共有 {filteredPluginCount} / {pluginCount} 个插件"}
|
||||
</Translate>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<Searcher
|
||||
className="store-searcher not-prose"
|
||||
onChange={onSearchQueryChange}
|
||||
onSubmit={onSearchQuerySubmit}
|
||||
onBackspace={onSearchBackspace}
|
||||
onClear={onSearchClear}
|
||||
onTagClick={onSearchTagClick}
|
||||
tags={searcherTags}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
<StoreToolbar
|
||||
className="not-prose"
|
||||
filters={filterTools}
|
||||
action={actionTool}
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
<Admonition type="caution" title={loadFailedTitle}>
|
||||
{error.message}
|
||||
</Admonition>
|
||||
) : loading ? (
|
||||
<p className="store-loading-container">
|
||||
<span className="loading loading-dots loading-lg store-loading"></span>
|
||||
</p>
|
||||
) : (
|
||||
<div className="store-container">
|
||||
{currentPlugins.map((plugin, index) => (
|
||||
<ResourceCard
|
||||
key={index}
|
||||
className="not-prose"
|
||||
resource={plugin}
|
||||
onClick={() => onCardClick(plugin)}
|
||||
onTagClick={onCardTagClick}
|
||||
onAuthorClick={() => onCardAuthorClick(plugin.author)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Paginate
|
||||
className="not-prose"
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
setNextPage={setNextPage}
|
||||
setPreviousPage={setPreviousPage}
|
||||
setPage={setPage}
|
||||
nextEnabled={nextEnabled}
|
||||
previousEnabled={previousEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
50
website/src/components/Store/Layout.tsx
Normal file
50
website/src/components/Store/Layout.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
|
||||
import { PageMetadata } from "@docusaurus/theme-common";
|
||||
import { useDocsVersionCandidates } from "@docusaurus/theme-common/internal";
|
||||
import { useVersionedSidebar } from "@nullbot/docusaurus-plugin-getsidebar/client";
|
||||
import { SidebarContentFiller } from "@nullbot/docusaurus-theme-nonepress/contexts";
|
||||
|
||||
import BackToTopButton from "@theme/BackToTopButton";
|
||||
import Layout from "@theme/Layout";
|
||||
import Page from "@theme/Page";
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
const SIDEBAR_ID = "ecosystem";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
function StorePage({ title, children }: Props): JSX.Element {
|
||||
const sidebarItems = useVersionedSidebar(
|
||||
useDocsVersionCandidates()[0].name,
|
||||
SIDEBAR_ID
|
||||
)!;
|
||||
|
||||
return (
|
||||
<Page hideTableOfContents reduceContentWidth={false}>
|
||||
<SidebarContentFiller items={sidebarItems} />
|
||||
<article className="prose max-w-full">
|
||||
<h1 className="store-title">{title}</h1>
|
||||
{children}
|
||||
</article>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StoreLayout({ title, ...props }: Props): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<PageMetadata title={title} />
|
||||
|
||||
<Layout>
|
||||
<BackToTopButton />
|
||||
|
||||
<StorePage title={title} {...props} />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
128
website/src/components/Store/Toolbar.tsx
Normal file
128
website/src/components/Store/Toolbar.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import clsx from "clsx";
|
||||
|
||||
import type { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export type Filter = {
|
||||
label: string;
|
||||
icon: IconProp;
|
||||
choices?: string[];
|
||||
onSubmit: (query: string) => void;
|
||||
};
|
||||
|
||||
export type Action = {
|
||||
label: string;
|
||||
icon: IconProp;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
filters?: Filter[];
|
||||
action?: Action;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function ToolbarFilter({
|
||||
label,
|
||||
icon,
|
||||
choices,
|
||||
onSubmit,
|
||||
}: Filter): JSX.Element {
|
||||
const [query, setQuery] = useState<string>("");
|
||||
|
||||
const filteredChoices = choices
|
||||
?.filter((choice) => choice.toLowerCase().includes(query.toLowerCase()))
|
||||
?.slice(0, 5);
|
||||
|
||||
const handleQuerySubmit = () => {
|
||||
if (filteredChoices && filteredChoices.length > 0) {
|
||||
onSubmit(filteredChoices[0]);
|
||||
} else if (choices === null) {
|
||||
onSubmit(query);
|
||||
}
|
||||
};
|
||||
|
||||
const onQueryKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
handleQuerySubmit();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const onChoiceKeyDown = (e: React.KeyboardEvent<HTMLLIElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
onSubmit(e.currentTarget.innerText);
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dropdown">
|
||||
<label
|
||||
className="btn btn-sm btn-outline btn-primary no-animation"
|
||||
tabIndex={0}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} />
|
||||
{label}
|
||||
</label>
|
||||
<div className="dropdown-content store-toolbar-dropdown">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={onQueryKeyDown}
|
||||
className="input input-sm input-bordered w-full"
|
||||
/>
|
||||
{filteredChoices && (
|
||||
<ul className="menu menu-sm">
|
||||
{filteredChoices.map((choice, index) => (
|
||||
<li
|
||||
key={index}
|
||||
onClick={() => onSubmit(choice)}
|
||||
onKeyDown={onChoiceKeyDown}
|
||||
>
|
||||
<a tabIndex={0}>{choice}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StoreToolbar({
|
||||
filters,
|
||||
action,
|
||||
className,
|
||||
}: Props): JSX.Element | null {
|
||||
if (!(filters && filters.length > 0) && !action) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx("store-toolbar", className)}>
|
||||
{filters && filters.length > 0 && (
|
||||
<div className="store-toolbar-filters">
|
||||
{filters.map((filter, index) => (
|
||||
<ToolbarFilter key={index} {...filter} />
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
}
|
38
website/src/components/Store/styles.css
Normal file
38
website/src/components/Store/styles.css
Normal file
@ -0,0 +1,38 @@
|
||||
.store {
|
||||
&-title {
|
||||
@apply text-center;
|
||||
}
|
||||
|
||||
&-description {
|
||||
@apply text-center;
|
||||
}
|
||||
|
||||
&-searcher {
|
||||
@apply max-w-2xl mx-auto my-4;
|
||||
}
|
||||
|
||||
&-toolbar {
|
||||
@apply flex items-center justify-center my-4;
|
||||
|
||||
&-filters {
|
||||
@apply flex grow gap-2;
|
||||
}
|
||||
|
||||
&-dropdown {
|
||||
@apply w-36 z-10 m-0 p-2;
|
||||
@apply rounded-md bg-base-100 shadow-lg border border-base-200;
|
||||
}
|
||||
}
|
||||
|
||||
&-loading {
|
||||
@apply text-primary;
|
||||
|
||||
&-container {
|
||||
@apply text-center;
|
||||
}
|
||||
}
|
||||
|
||||
&-container {
|
||||
@apply grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-6 mt-4 mb-8;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user