mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-07-28 00:31:14 +00:00
📝 Docs: 升级新版 NonePress 主题 (#2375)
This commit is contained in:
@ -1,33 +0,0 @@
|
||||
# Website
|
||||
|
||||
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
|
||||
|
||||
### Installation
|
||||
|
||||
```
|
||||
$ yarn
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
```
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||
|
||||
### Deployment
|
||||
|
||||
```
|
||||
$ GIT_USER=<Your GitHub username> USE_SSH=true yarn deploy
|
||||
```
|
||||
|
||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
@ -4,8 +4,8 @@ description: 注册适配器与指定平台交互
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 20
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 20
|
||||
---
|
||||
|
||||
# 使用适配器
|
||||
@ -158,4 +158,4 @@ is_tome: bool = event.is_tome()
|
||||
|
||||
## 更多
|
||||
|
||||
官方支持的适配器和社区贡献的适配器均可在[商店](/store)中查看。如果你想要开发自己的适配器,可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。
|
||||
官方支持的适配器和社区贡献的适配器均可在[商店](/store/adapters)中查看。如果你想要开发自己的适配器,可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。
|
||||
|
@ -4,8 +4,8 @@ description: 通过依赖注入获取上下文信息
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 70
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 70
|
||||
---
|
||||
|
||||
# 依赖注入
|
||||
@ -557,7 +557,7 @@ async def _(x: httpx.AsyncClient = Depends(get_client)):
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
生成器作为依赖时,其中只能进行一次 `yield`,否则将会触发异常。如果对此有疑问并想探究原因,可以参考 [contextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.contextmanager) 和 [asynccontextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.asynccontextmanager) 文档。事实上,NoneBot 内部就使用了这两个装饰器。
|
||||
:::
|
||||
|
||||
|
@ -4,8 +4,8 @@ description: 选择合适的驱动器运行机器人
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 10
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 10
|
||||
---
|
||||
|
||||
# 选择驱动器
|
||||
@ -118,7 +118,7 @@ DRIVER=~fastapi
|
||||
|
||||
##### `fastapi_reload`
|
||||
|
||||
:::warning 警告
|
||||
:::caution 警告
|
||||
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
|
||||
|
||||
```bash
|
||||
@ -200,7 +200,7 @@ DRIVER=~quart
|
||||
|
||||
##### `quart_reload`
|
||||
|
||||
:::warning 警告
|
||||
:::caution 警告
|
||||
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
|
||||
|
||||
```bash
|
||||
@ -252,7 +252,7 @@ nonebot.run(app="bot:app")
|
||||
|
||||
**类型:**HTTP 客户端驱动器
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
本驱动器仅支持 HTTP 请求,不支持 WebSocket 连接请求。
|
||||
:::
|
||||
|
||||
@ -266,7 +266,7 @@ DRIVER=~httpx
|
||||
|
||||
**类型:**WebSocket 客户端驱动器
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
本驱动器仅支持 WebSocket 连接请求,不支持 HTTP 请求。
|
||||
:::
|
||||
|
||||
|
@ -4,8 +4,8 @@ description: 自定义事件响应器存储
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 110
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 110
|
||||
---
|
||||
|
||||
# 事件响应器存储
|
||||
|
@ -4,8 +4,8 @@ description: 事件响应器组成与内置响应规则
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 60
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 60
|
||||
---
|
||||
|
||||
# 事件响应器进阶
|
||||
|
@ -4,8 +4,8 @@ description: 填写与获取插件相关的信息
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 30
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 30
|
||||
---
|
||||
|
||||
# 插件信息
|
||||
@ -14,7 +14,7 @@ NoneBot 是一个插件化的框架,可以通过加载插件来扩展功能。
|
||||
|
||||
## 插件元数据
|
||||
|
||||
在 NoneBot 中,插件 [`Plugin`](../api/plugin/plugin.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常,只有插件开发者才需要关心这些信息,而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此,我们可以为插件添加插件元数据 `PluginMetadata`,它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层,可以直接通过源码查看,或者通过 NoneBot 插件系统获取收集到的信息,通过其他方式发送给机器人用户等。
|
||||
在 NoneBot 中,插件 [`Plugin`](../api/plugin/model.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常,只有插件开发者才需要关心这些信息,而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此,我们可以为插件添加插件元数据 `PluginMetadata`,它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层,可以直接通过源码查看,或者通过 NoneBot 插件系统获取收集到的信息,通过其他方式发送给机器人用户等。
|
||||
|
||||
现在,假设我们有一个插件 `example`, 它的模块结构如下:
|
||||
|
||||
|
@ -4,8 +4,8 @@ description: 编写与加载嵌套插件
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 40
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 40
|
||||
---
|
||||
|
||||
# 嵌套插件
|
||||
|
@ -4,8 +4,8 @@ description: 使用其他插件提供的功能
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 50
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 50
|
||||
---
|
||||
|
||||
# 跨插件访问
|
||||
|
@ -4,8 +4,8 @@ description: 添加服务端路由规则
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 100
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 100
|
||||
---
|
||||
|
||||
# 添加路由
|
||||
@ -21,10 +21,11 @@ NoneBot 中,我们可以通过两种途径向 ASGI 驱动器添加路由规则
|
||||
|
||||
在向驱动器添加路由规则时,我们需要注意驱动器是否为服务端类型,我们可以通过以下方式判断:
|
||||
|
||||
```python {3}
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
from nonebot.drivers import ASGIMixin
|
||||
|
||||
# highlight-next-line
|
||||
can_use = isinstance(get_driver(), ASGIMixin)
|
||||
```
|
||||
|
||||
|
@ -4,8 +4,8 @@ description: 在特定的生命周期中执行代码
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 90
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 90
|
||||
---
|
||||
|
||||
# 钩子函数
|
||||
|
@ -4,8 +4,8 @@ description: 控制会话响应对象
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 80
|
||||
category: advanced
|
||||
- category: advanced
|
||||
weight: 80
|
||||
---
|
||||
|
||||
# 会话更新
|
||||
@ -56,4 +56,4 @@ async def _(matcher: Matcher) -> Permission:
|
||||
|
||||
请注意,此处为全大写字母的 `USER` 权限,它可以匹配多个会话 ID。通过这种方式,我们可以实现多用户同时参与的会话。
|
||||
|
||||
我们已经了解了如何控制会话的更新,相信你已经能够实现更复杂的会话功能了,例如多人小游戏等等。欢迎将你的作品分享到[插件商店](/store)。
|
||||
我们已经了解了如何控制会话的更新,相信你已经能够实现更复杂的会话功能了,例如多人小游戏等等。欢迎将你的作品分享到[插件商店](/store/plugins)。
|
||||
|
@ -4,14 +4,13 @@ description: 使用平台接口,完成更多功能
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 50
|
||||
category: appendices
|
||||
- category: appendices
|
||||
weight: 50
|
||||
---
|
||||
|
||||
# 使用平台接口
|
||||
|
||||
import Messenger from "@site/src/components/Messenger";
|
||||
import MarkdownText from "!!raw-loader!./assets/console-markdown.txt";
|
||||
import Messenger from "@/components/Messenger";
|
||||
|
||||
在 NoneBot 中,除了使用事件响应器操作发送文本消息外,我们还可以直接通过使用协议适配器提供的方法来使用平台特定的接口,完成发送特殊消息、获取信息等其他平台提供的功能。同时,在部分无法使用事件响应器的情况中,例如[定时任务](../best-practice/scheduler.md),我们也可以使用平台接口来完成需要的功能。
|
||||
|
||||
@ -19,7 +18,7 @@ import MarkdownText from "!!raw-loader!./assets/console-markdown.txt";
|
||||
|
||||
在之前的章节中,我们介绍了如何向用户发送文本消息以及[如何处理平台消息](../tutorial/message.md),现在我们来向用户发送平台特殊消息。
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
在以下的示例中,我们将使用 `Console` 协议适配器来演示如何发送平台消息。在实际使用中,你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。
|
||||
:::
|
||||
|
||||
@ -49,7 +48,11 @@ async def got_location(location: str = ArgPlainText()):
|
||||
{ position: "right", msg: "/天气" },
|
||||
{ position: "left", msg: "❓请输入地名" },
|
||||
{ position: "right", msg: "北京" },
|
||||
{ position: "left", msg: MarkdownText },
|
||||
{
|
||||
position: "left",
|
||||
monospace: true,
|
||||
msg: "┏━━━━━━━━━━━━━━━━┓\n┃ 北京 ┃\n┗━━━━━━━━━━━━━━━━┛\n• 今天\n⛅ 多云 20℃~24℃",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@ -100,7 +103,7 @@ result = await bot.get_user_info(user_id=12345678)
|
||||
result = await bot.call_api("get_user_info", user_id=12345678)
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
实际可以使用的 API 以及参数取决于平台提供的接口以及协议适配器的实现,请参考协议适配器以及平台文档。
|
||||
:::
|
||||
|
||||
|
@ -1,6 +0,0 @@
|
||||
┏━━━━━━━━━━━━━━━━┓
|
||||
┃ 北京 ┃
|
||||
┗━━━━━━━━━━━━━━━━┛
|
||||
|
||||
• 今天
|
||||
⛅ 多云 20℃~24℃
|
@ -4,8 +4,8 @@ description: 读取用户配置来控制插件行为
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 10
|
||||
category: appendices
|
||||
- category: appendices
|
||||
weight: 10
|
||||
---
|
||||
|
||||
# 配置
|
||||
@ -62,7 +62,7 @@ export CUSTOM_CONFIG="config in environment variables"
|
||||
|
||||
那最终 NoneBot 所读取的内容为环境变量中的内容,即 `config in environment variables`。
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
NoneBot 不会自发读取未被定义的配置项的环境变量,如果需要读取某一环境变量需要在 dotenv 配置文件中进行声明。
|
||||
:::
|
||||
|
||||
|
@ -4,8 +4,8 @@ description: 记录与控制日志
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 70
|
||||
category: appendices
|
||||
- category: appendices
|
||||
weight: 70
|
||||
---
|
||||
|
||||
# 日志
|
||||
|
@ -4,8 +4,8 @@ description: 根据事件类型进行不同的处理
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 80
|
||||
category: appendices
|
||||
- category: appendices
|
||||
weight: 80
|
||||
---
|
||||
|
||||
# 事件类型与重载
|
||||
@ -28,7 +28,7 @@ async def got_location(event: MessageEvent, location: str = ArgPlainText()):
|
||||
|
||||
在上面的代码中,我们获取了 `Console` 协议适配器的消息事件提供的发送时间 `time` 属性。
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
如果**基类**就能满足你的需求,那么就**不要修改**事件参数类型注解,这样可以使你的代码更加**通用**,可以在更多平台上运行。如何根据不同平台事件类型进行不同的处理,我们将在[重载](#重载)一节中介绍。
|
||||
:::
|
||||
|
||||
@ -63,7 +63,7 @@ async def handle_onebot(bot: OneBot):
|
||||
await bot.send_group_message(group_id=123123, message="OneBot")
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
重载机制对所有的参数类型注解都有效,因此,依赖注入也可以使用这个特性来对不同的返回值进行处理。
|
||||
|
||||
但 Bot、Event 和 Matcher 三者的参数类型注解具有最高检查优先级,如果三者任一类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。
|
||||
|
@ -4,8 +4,8 @@ description: 控制事件响应器的权限
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 60
|
||||
category: appendices
|
||||
- category: appendices
|
||||
weight: 60
|
||||
---
|
||||
|
||||
# 权限控制
|
||||
|
@ -4,8 +4,8 @@ description: 自定义响应规则
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 20
|
||||
category: appendices
|
||||
- category: appendices
|
||||
weight: 20
|
||||
---
|
||||
|
||||
# 响应规则
|
||||
|
@ -4,8 +4,8 @@ description: 更灵活的会话控制
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 30
|
||||
category: appendices
|
||||
- category: appendices
|
||||
weight: 30
|
||||
---
|
||||
|
||||
# 会话控制
|
||||
@ -322,7 +322,7 @@ async def _(matcher: Matcher):
|
||||
matcher.stop_propagation()
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
`stop_propagation` 操作是实例方法,需要先通过依赖注入获取事件响应器实例再进行调用。
|
||||
:::
|
||||
|
||||
|
@ -4,8 +4,8 @@ description: 会话状态信息
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 40
|
||||
category: appendices
|
||||
- category: appendices
|
||||
weight: 40
|
||||
---
|
||||
|
||||
# 会话状态
|
||||
|
@ -180,7 +180,7 @@ docker compose build
|
||||
|
||||
将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下,并将文件中高亮行中的仓库名称替换为你的仓库名称:
|
||||
|
||||
```yaml title=.github/workflows/build.yml {34}
|
||||
```yaml title=.github/workflows/build.yml
|
||||
name: Docker Hub Release
|
||||
|
||||
on:
|
||||
@ -213,6 +213,7 @@ jobs:
|
||||
id: metadata
|
||||
with:
|
||||
images: |
|
||||
# highlight-next-line
|
||||
{organization}/{repository}
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
|
@ -27,7 +27,7 @@ nb plugin install nonebot-plugin-sentry
|
||||
|
||||
### 配置插件
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
错误跟踪通常在生产环境中使用,因此开发环境中 `sentry_dsn` 留空即会停用插件。
|
||||
:::
|
||||
|
||||
|
@ -58,7 +58,7 @@ scheduler.add_job(
|
||||
)
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
由于 APScheduler 的定时任务并不是**由事件响应器所触发的事件**,因此其任务函数无法同[事件处理函数](../tutorial/handler.mdx#事件处理函数)一样通过[依赖注入](../tutorial/event-data.mdx#认识依赖注入)获取上下文信息,也无法通过事件响应器对象的方法进行任何操作,因此我们需要使用[调用平台 API](../appendices/api-calling.mdx#调用平台-api)的方式来获取信息或收发消息。
|
||||
|
||||
相对于事件处理依赖而言,编写定时任务更像是编写普通的函数,需要我们自行获取信息以及发送信息,请**不要**将事件处理依赖的特殊语法用于定时任务!
|
||||
|
@ -189,7 +189,7 @@ async def _(bot: Bot):
|
||||
|
||||
然后我们对该插件进行测试:
|
||||
|
||||
```python {19,20,23,24} title=tests/test_example.py
|
||||
```python title=tests/test_example.py
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
@ -210,12 +210,16 @@ async def test_example(app: App):
|
||||
from awesome_bot.plugins.example import foo
|
||||
|
||||
async with app.test_matcher(foo) as ctx:
|
||||
# highlight-start
|
||||
adapter = nonebot.get_adapter(Adapter)
|
||||
bot = ctx.create_bot(base=Bot, adapter=adapter)
|
||||
# highlight-end
|
||||
event = make_event("/foo")
|
||||
ctx.receive_event(bot, event)
|
||||
# highlight-start
|
||||
ctx.should_call_send(event, "message", result=None, bot=bot)
|
||||
ctx.should_call_api("bell", {}, result=None, adapter=adapter)
|
||||
# highlight-end
|
||||
```
|
||||
|
||||
请注意,对于在依赖注入中使用了非基类对象的情况,我们需要在 `create_bot` 方法中指定 `base` 和 `adapter` 参数,确保不会因为重载功能而出现非预期情况。
|
||||
|
@ -576,6 +576,6 @@ class Message(BaseMessage[MessageSegment]):
|
||||
|
||||
## 后续工作
|
||||
|
||||
在完成适配器代码的编写后,如果想要将适配器发布到 NoneBot 商店,我们需要将适配器发布到 PyPI 中,然后前往[商店](/store)页面,切换到适配器页签,点击**发布适配器**按钮,填写适配器相关信息并提交。
|
||||
在完成适配器代码的编写后,如果想要将适配器发布到 NoneBot 商店,我们需要将适配器发布到 PyPI 中,然后前往[商店](/store/adapters)页面,切换到适配器页签,点击**发布适配器**按钮,填写适配器相关信息并提交。
|
||||
|
||||
另外建议编写适配器文档或者一些插件开发示例,以便其他开发者使用我们的适配器。
|
||||
|
@ -62,7 +62,7 @@ NoneBot 插件使用下述命名规范:
|
||||
|
||||
依赖填写的基本原则:程序直接导入了什么第三方库,就添加什么第三方包依赖;能用哪些第三方库的特性,就根据使用的特性锁定第三方包版本。
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
|
||||
1. 插件需要添加 `nonebot2` 为依赖以避免“幽灵依赖”;
|
||||
2. 插件需要将使用的适配器加入依赖列表,如:使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖;
|
||||
@ -102,7 +102,7 @@ __plugin_meta__ = PluginMetadata(
|
||||
)
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
`__plugin_meta__` 变量**必须**处于插件最外层(如 `__init__.py` 中),否则无法正常识别。
|
||||
|
||||
一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。
|
||||
@ -183,7 +183,7 @@ twine upload dist/* # 只发布先前的构建
|
||||
|
||||
### 提交申请
|
||||
|
||||
完成在 PyPI 的插件发布流程后,前往[商店](/store)页面,切换到插件页签,点击 **发布插件** 按钮。
|
||||
完成在 PyPI 的插件发布流程后,前往[商店](/store/plugins)页面,切换到插件页签,点击 **发布插件** 按钮。
|
||||
|
||||
在弹出的插件信息提交表单内,填入您所要发布的相应插件信息。请注意,如果插件需要必要配置项才能正常导入,请在“插件配置项”中填写必要的内容(请勿填写密钥等敏感信息)。
|
||||
|
||||
@ -199,4 +199,4 @@ twine upload dist/* # 只发布先前的构建
|
||||
|
||||
之后,NoneBot 的维护者和一些插件开发者会初步检查插件代码,帮助减少该插件的问题。
|
||||
|
||||
完成这些步骤后,您的插件将会被自动合并到[商店](/store),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。
|
||||
完成这些步骤后,您的插件将会被自动合并到[商店](/store/plugins),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。
|
||||
|
@ -4,8 +4,8 @@ description: 尝试使用 NoneBot
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 10
|
||||
category: tutorial
|
||||
- category: tutorial
|
||||
weight: 10
|
||||
---
|
||||
|
||||
import Asciinema from "@site/src/components/Asciinema";
|
||||
@ -13,7 +13,7 @@ import Messenger from "@site/src/components/Messenger";
|
||||
|
||||
# 快速上手
|
||||
|
||||
:::warning 前提条件
|
||||
:::caution 前提条件
|
||||
|
||||
- 请确保你的 Python 版本 >= 3.8
|
||||
- **我们强烈建议使用虚拟环境进行开发**,如果没有使用虚拟环境,请确保已经卸载可能存在的 NoneBot v1!!!
|
||||
|
@ -4,15 +4,15 @@ description: 创建一个 NoneBot 项目
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 20
|
||||
category: tutorial
|
||||
- category: tutorial
|
||||
weight: 20
|
||||
---
|
||||
|
||||
# 手动创建项目
|
||||
|
||||
在[快速上手](./quick-start.mdx)中,我们已经介绍了如何安装和使用 `nb-cli` 创建一个项目。在本章节中,我们将简要介绍如何在不使用 `nb-cli` 的方式创建一个机器人项目的**最小实例**并启动。如果你想要了解 NoneBot 的启动流程,也可以阅读本章节。
|
||||
在[快速上手](../quick-start.mdx)中,我们已经介绍了如何安装和使用 `nb-cli` 创建一个项目。在本章节中,我们将简要介绍如何在不使用 `nb-cli` 的方式创建一个机器人项目的**最小实例**并启动。如果你想要了解 NoneBot 的启动流程,也可以阅读本章节。
|
||||
|
||||
:::warning
|
||||
:::caution 警告
|
||||
我们十分不推荐直接创建机器人项目,请优先考虑使用 nb-cli 进行项目创建。
|
||||
:::
|
||||
|
||||
@ -44,7 +44,7 @@ options:
|
||||
pip install 'nonebot2[fastapi]'
|
||||
```
|
||||
|
||||
驱动器包名可以在 [驱动器商店](/store) 中找到。
|
||||
驱动器包名可以在 [驱动器商店](/store/drivers) 中找到。
|
||||
|
||||
3. 安装适配器
|
||||
|
||||
@ -52,7 +52,7 @@ options:
|
||||
pip install nonebot-adapter-console
|
||||
```
|
||||
|
||||
适配器包名可以在 [适配器商店](/store) 中找到。
|
||||
适配器包名可以在 [适配器商店](/store/adapters) 中找到。
|
||||
|
||||
## 创建配置文件
|
||||
|
||||
|
@ -4,8 +4,8 @@ description: 创建并加载自定义插件
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 50
|
||||
category: tutorial
|
||||
- category: tutorial
|
||||
weight: 50
|
||||
---
|
||||
|
||||
# 插件编写准备
|
||||
@ -41,7 +41,7 @@ options:
|
||||
|
||||
## 创建插件
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
如果在之前的[快速上手](../quick-start.mdx)章节中已经使用 `bootstrap` 模板创建了项目,那么你需要做出如下修改:
|
||||
|
||||
1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins`
|
||||
@ -63,7 +63,7 @@ options:
|
||||
|
||||
:::
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
如果在之前的[创建项目](./application.md)章节中手动创建了相关文件,那么你需要做出如下修改:
|
||||
|
||||
1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins`
|
||||
@ -144,7 +144,7 @@ nonebot.load_plugin("path.to.your.plugin") # 加载第三方插件
|
||||
nonebot.load_plugin(Path("./path/to/your/plugin.py")) # 加载项目插件
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
请注意,本地插件的路径应该为相对机器人 **入口文件(通常为 bot.py)** 可导入的,例如在项目 `plugins` 目录下。
|
||||
:::
|
||||
|
||||
@ -156,7 +156,7 @@ nonebot.load_plugin(Path("./path/to/your/plugin.py")) # 加载项目插件
|
||||
nonebot.load_plugins("src/plugins", "path/to/your/plugins")
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
请注意,插件目录应该为相对机器人 **入口文件(通常为 bot.py)** 可导入的,例如在项目 `plugins` 目录下。
|
||||
:::
|
||||
|
||||
|
@ -4,8 +4,8 @@ description: 通过依赖注入获取所需事件信息
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 80
|
||||
category: tutorial
|
||||
- category: tutorial
|
||||
weight: 80
|
||||
---
|
||||
|
||||
# 获取事件信息
|
||||
|
@ -4,8 +4,8 @@ description: NoneBot 机器人构成及基本使用
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 30
|
||||
category: tutorial
|
||||
- category: tutorial
|
||||
weight: 30
|
||||
---
|
||||
|
||||
# 机器人的构成
|
||||
|
@ -4,8 +4,8 @@ description: 处理接收到的特定事件
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 70
|
||||
category: tutorial
|
||||
- category: tutorial
|
||||
weight: 70
|
||||
---
|
||||
|
||||
# 事件处理
|
||||
|
@ -4,8 +4,8 @@ description: 响应接收到的特定事件
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 60
|
||||
category: tutorial
|
||||
- category: tutorial
|
||||
weight: 60
|
||||
---
|
||||
|
||||
# 事件响应器
|
||||
|
@ -4,8 +4,8 @@ description: 处理消息序列与消息段
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 90
|
||||
category: tutorial
|
||||
- category: tutorial
|
||||
weight: 90
|
||||
---
|
||||
|
||||
# 处理消息
|
||||
@ -26,7 +26,7 @@ options:
|
||||
|
||||
顾名思义,消息段 `MessageSegment` 是一段消息。由于消息序列的本质是由若干消息段所组成的序列,消息段可以被认为是构成消息序列的最小单位。简单来说,消息序列类似于一个自然段,而消息段则是组成自然段的一句话。同时,作为特殊消息载体的存在,绝大多数的平台都有着**独特的消息类型**,这些独特的内容均需要由对应的**协议适配器**所提供,以适应不同平台中的消息模式。**这也意味着,你需要导入对应的协议适配器中的消息序列和消息段后才能使用其特殊的工厂方法。**
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
消息段的类型是由协议适配器提供的,因此你需要参考协议适配器的文档并导入对应的消息段后才能使用其特殊的消息类型。
|
||||
|
||||
在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中,我们导入的为 `nonebot.adapters.Message` 抽象基类,因此我们无法使用平台特有的消息类型。仅能使用 `str` 作为纯文本消息回复。
|
||||
@ -34,7 +34,7 @@ options:
|
||||
|
||||
## 使用消息序列
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
在以下的示例中,为了更好的理解多种类型的消息组成方式,我们将使用 `Console` 协议适配器来演示消息序列的使用方法。在实际使用中,你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。
|
||||
:::
|
||||
|
||||
@ -297,7 +297,7 @@ msg == Message(
|
||||
|
||||
如果 `Message.template` 构建消息模板,那么消息模板将采用消息序列形式的格式化,此时的消息将会是平台特定的:
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
使用 `Message.template` 构建消息模板时,应注意消息序列为平台适配器提供的类型,不能使用 `nonebot.adapters.Message` 基类作为模板构建。使用基类构建模板与使用 `str` 构建模板的效果是一样的,因此请使用上述的 `MessageTemplate` 类直接构建模板。:
|
||||
:::
|
||||
|
||||
@ -337,7 +337,7 @@ Message(
|
||||
)
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
只有消息序列中的文本类型消息段才能被格式化,其他类型的消息段将会原样添加。
|
||||
:::
|
||||
|
||||
|
@ -4,8 +4,8 @@ description: 从商店安装适配器和插件
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 40
|
||||
category: tutorial
|
||||
- category: tutorial
|
||||
weight: 40
|
||||
---
|
||||
|
||||
# 获取商店内容
|
||||
@ -15,10 +15,12 @@ import TabItem from "@theme/TabItem";
|
||||
import Asciinema from "@site/src/components/Asciinema";
|
||||
|
||||
:::tip 提示
|
||||
|
||||
如果你暂时没有获取商店内容的需求,可以跳过本章节。
|
||||
|
||||
:::
|
||||
|
||||
NoneBot 提供了一个[商店](/store),商店内容均由社区开发者贡献。你可以在商店中查找你需要的适配器和插件等,进行安装或者参考其文档等。
|
||||
NoneBot 提供了一个[商店](/store/plugins),商店内容均由社区开发者贡献。你可以在商店中查找你需要的适配器和插件等,进行安装或者参考其文档等。
|
||||
|
||||
商店中每个内容的卡片都包含了其名称和简介等信息,点击**卡片右上角**链接图标即可跳转到其主页。
|
||||
|
||||
|
@ -1,26 +1,226 @@
|
||||
// @ts-check
|
||||
// Note: type annotations allow type checking and IDEs autocompletion
|
||||
|
||||
// color mode config
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["colorMode"]} */
|
||||
const colorMode = {
|
||||
defaultMode: "light",
|
||||
respectPrefersColorScheme: true,
|
||||
};
|
||||
|
||||
// navbar config
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["navbar"]} */
|
||||
const navbar = {
|
||||
title: "NoneBot",
|
||||
logo: {
|
||||
alt: "NoneBot",
|
||||
src: "logo.png",
|
||||
href: "/",
|
||||
target: "_self",
|
||||
height: 32,
|
||||
width: 32,
|
||||
},
|
||||
hideOnScroll: false,
|
||||
items: [
|
||||
{
|
||||
label: "指南",
|
||||
type: "docsMenu",
|
||||
category: "tutorial",
|
||||
},
|
||||
{
|
||||
label: "深入",
|
||||
type: "docsMenu",
|
||||
category: "appendices",
|
||||
},
|
||||
{
|
||||
label: "进阶",
|
||||
type: "docsMenu",
|
||||
category: "advanced",
|
||||
},
|
||||
{
|
||||
label: "API",
|
||||
type: "doc",
|
||||
docId: "api/index",
|
||||
},
|
||||
{
|
||||
label: "更多",
|
||||
type: "dropdown",
|
||||
to: "/store/plugins",
|
||||
items: [
|
||||
{
|
||||
label: "最佳实践",
|
||||
type: "doc",
|
||||
docId: "best-practice/scheduler",
|
||||
},
|
||||
{
|
||||
label: "开发者",
|
||||
type: "doc",
|
||||
docId: "developer/plugin-publishing",
|
||||
},
|
||||
{ label: "社区", type: "doc", docId: "community/contact" },
|
||||
{ label: "开源之夏", type: "doc", docId: "ospp/2023" },
|
||||
{ label: "商店", to: "/store/plugins" },
|
||||
{ label: "更新日志", to: "/changelog" },
|
||||
{ label: "论坛", href: "https://discussions.nonebot.dev" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// footer config
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["footer"]} */
|
||||
const footer = {
|
||||
style: "light",
|
||||
logo: {
|
||||
alt: "NoneBot",
|
||||
src: "logo.png",
|
||||
href: "/",
|
||||
target: "_self",
|
||||
height: 32,
|
||||
width: 32,
|
||||
},
|
||||
copyright: `Copyright © ${new Date().getFullYear()} NoneBot. All rights reserved.`,
|
||||
links: [
|
||||
{
|
||||
title: "Learn",
|
||||
items: [
|
||||
{ label: "Introduction", to: "/docs/" },
|
||||
{ label: "QuickStart", to: "/docs/quick-start" },
|
||||
{ label: "Changelog", to: "/changelog" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "NoneBot Team",
|
||||
items: [
|
||||
{
|
||||
label: "Homepage",
|
||||
href: "https://nonebot.dev",
|
||||
},
|
||||
{
|
||||
label: "NoneBot V1",
|
||||
href: "https://v1.nonebot.dev",
|
||||
},
|
||||
{ label: "NoneBot CLI", href: "https://cli.nonebot.dev" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Related",
|
||||
items: [
|
||||
{ label: "OneBot", href: "https://onebot.dev/" },
|
||||
{ label: "go-cqhttp", href: "https://docs.go-cqhttp.org/" },
|
||||
{ label: "Mirai", href: "https://mirai.mamoe.net/" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// prism config
|
||||
/** @type {import('prism-react-renderer').PrismTheme} */
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line import/order
|
||||
const lightCodeTheme = require("prism-react-renderer/themes/github");
|
||||
/** @type {import('prism-react-renderer').PrismTheme} */
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line import/order
|
||||
const darkCodeTheme = require("prism-react-renderer/themes/dracula");
|
||||
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["prism"]} */
|
||||
const prism = {
|
||||
theme: lightCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
additionalLanguages: ["docker", "ini"],
|
||||
};
|
||||
|
||||
// algolia config
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["algolia"]} */
|
||||
const algolia = {
|
||||
appId: "X0X5UACHZQ",
|
||||
apiKey: "ac03e1ac2bd0812e2ea38c0cc1ea38c5",
|
||||
indexName: "nonebot",
|
||||
contextualSearch: true,
|
||||
};
|
||||
|
||||
// nonepress config
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["nonepress"]} */
|
||||
const nonepress = {
|
||||
tailwindConfig: require("./tailwind.config"),
|
||||
navbar: {
|
||||
docsVersionDropdown: {
|
||||
dropdownItemsAfter: [
|
||||
{
|
||||
label: "1.x",
|
||||
href: "https://v1.nonebot.dev/",
|
||||
},
|
||||
],
|
||||
},
|
||||
socialLinks: [
|
||||
{
|
||||
icon: ["fab", "github"],
|
||||
href: "https://github.com/nonebot/nonebot2",
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
socialLinks: [
|
||||
{
|
||||
icon: ["fab", "github"],
|
||||
href: "https://github.com/nonebot/nonebot2",
|
||||
},
|
||||
{
|
||||
icon: ["fab", "qq"],
|
||||
href: "https://jq.qq.com/?_wv=1027&k=5OFifDh",
|
||||
},
|
||||
{
|
||||
icon: ["fab", "telegram"],
|
||||
href: "https://t.me/botuniverse",
|
||||
},
|
||||
{
|
||||
icon: ["fab", "discord"],
|
||||
href: "https://discord.gg/VKtE6Gdc4h",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// theme config
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig} */
|
||||
const themeConfig = {
|
||||
colorMode,
|
||||
navbar,
|
||||
footer,
|
||||
prism,
|
||||
algolia,
|
||||
nonepress,
|
||||
};
|
||||
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
const siteConfig = {
|
||||
title: "NoneBot",
|
||||
tagline: "跨平台 Python 异步机器人框架",
|
||||
url: "https://nonebot.dev",
|
||||
baseUrl: process.env.BASE_URL || "/",
|
||||
onBrokenLinks: "throw",
|
||||
onBrokenMarkdownLinks: "warn",
|
||||
favicon: "icons/favicon.ico",
|
||||
|
||||
// Set the production url of your site here
|
||||
url: "https://nonebot.dev",
|
||||
// Set the /<baseUrl>/ pathname under which your site is served
|
||||
// For GitHub pages deployment, it is often '/<projectName>/'
|
||||
baseUrl: process.env.BASE_URL || "/",
|
||||
|
||||
// GitHub pages deployment config.
|
||||
// If you aren't using GitHub pages, you don't need these.
|
||||
organizationName: "nonebot", // Usually your GitHub org/user name.
|
||||
projectName: "nonebot2", // Usually your repo name.
|
||||
|
||||
onBrokenLinks: "throw",
|
||||
onBrokenMarkdownLinks: "warn",
|
||||
|
||||
// Even if you don't use internalization, you can use this field to set useful
|
||||
// metadata like html lang. For example, if your site is Chinese, you may want
|
||||
// to replace "en" with "zh-Hans".
|
||||
i18n: {
|
||||
defaultLocale: "zh-Hans",
|
||||
locales: ["zh-Hans"],
|
||||
localeConfigs: {
|
||||
"zh-Hans": { label: "简体中文" },
|
||||
},
|
||||
},
|
||||
|
||||
scripts: [
|
||||
@ -34,8 +234,8 @@ const config = {
|
||||
|
||||
presets: [
|
||||
[
|
||||
"docusaurus-preset-nonepress",
|
||||
/** @type {import('docusaurus-preset-nonepress').Options} */
|
||||
"@nullbot/docusaurus-preset-nonepress",
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').Options} */
|
||||
({
|
||||
docs: {
|
||||
sidebarPath: require.resolve("./sidebars.js"),
|
||||
@ -49,160 +249,42 @@ const config = {
|
||||
// "**/*.test.{js,jsx,ts,tsx}",
|
||||
// "**/__tests__/**",
|
||||
// ],
|
||||
// async sidebarItemsGenerator({
|
||||
// isCategoryIndex: defaultCategoryIndexMatcher,
|
||||
// defaultSidebarItemsGenerator,
|
||||
// ...args
|
||||
// }) {
|
||||
// return defaultSidebarItemsGenerator({
|
||||
// ...args,
|
||||
// isCategoryIndex(doc) {
|
||||
// // disable category index convention for generated API docs
|
||||
// if (
|
||||
// doc.directories.length > 0 &&
|
||||
// doc.directories.at(-1) === "api"
|
||||
// ) {
|
||||
// return false;
|
||||
// }
|
||||
// return defaultCategoryIndexMatcher(doc);
|
||||
// },
|
||||
// });
|
||||
// },
|
||||
},
|
||||
// theme: {
|
||||
// customCss: require.resolve("./src/css/custom.css"),
|
||||
// },
|
||||
sitemap: {
|
||||
changefreq: "daily",
|
||||
priority: 0.5,
|
||||
},
|
||||
gtag: {
|
||||
trackingID: "G-MRS1GMZG0F",
|
||||
},
|
||||
}),
|
||||
],
|
||||
],
|
||||
plugins: [require("./src/plugins/webpack-plugin.cjs")],
|
||||
|
||||
themeConfig:
|
||||
/** @type {import('docusaurus-preset-nonepress').ThemeConfig} */
|
||||
({
|
||||
colorMode: {
|
||||
defaultMode: "light",
|
||||
},
|
||||
logo: {
|
||||
alt: "",
|
||||
src: "logo.png",
|
||||
href: "/",
|
||||
target: "_self",
|
||||
},
|
||||
navbar: {
|
||||
hideOnScroll: true,
|
||||
items: [
|
||||
{
|
||||
label: "指南",
|
||||
type: "docsMenu",
|
||||
category: "tutorial",
|
||||
},
|
||||
{
|
||||
label: "深入",
|
||||
type: "docsMenu",
|
||||
category: "appendices",
|
||||
},
|
||||
{
|
||||
label: "进阶",
|
||||
type: "docsMenu",
|
||||
category: "advanced",
|
||||
},
|
||||
{
|
||||
label: "API",
|
||||
type: "docLink",
|
||||
docId: "api/index",
|
||||
},
|
||||
{
|
||||
label: "更多",
|
||||
type: "dropdown",
|
||||
to: "/store",
|
||||
items: [
|
||||
{
|
||||
label: "最佳实践",
|
||||
type: "docLink",
|
||||
docId: "best-practice/scheduler",
|
||||
},
|
||||
{
|
||||
label: "开发者",
|
||||
type: "docLink",
|
||||
docId: "developer/plugin-publishing",
|
||||
},
|
||||
{ label: "社区", type: "docLink", docId: "community/contact" },
|
||||
{ label: "开源之夏", type: "docLink", docId: "ospp/2023" },
|
||||
{ label: "商店", to: "/store" },
|
||||
{ label: "更新日志", to: "/changelog" },
|
||||
{ label: "论坛", href: "https://discussions.nonebot.dev" },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: ["fab", "github"],
|
||||
href: "https://github.com/nonebot/nonebot2",
|
||||
},
|
||||
],
|
||||
docsVersionItemAfter: [
|
||||
{
|
||||
label: "2.0.0a16",
|
||||
href: "https://61d3d9dbcadf413fd3238e89--nonebot2.netlify.app/",
|
||||
},
|
||||
{
|
||||
label: "1.x",
|
||||
href: "https://v1.nonebot.dev/",
|
||||
},
|
||||
],
|
||||
},
|
||||
hideableSidebar: true,
|
||||
footer: {
|
||||
copyright: `Copyright © ${new Date().getFullYear()} NoneBot. All rights reserved.`,
|
||||
iconLinks: [
|
||||
{
|
||||
icon: ["fab", "github"],
|
||||
href: "https://github.com/nonebot/nonebot2",
|
||||
description: "GitHub",
|
||||
},
|
||||
{
|
||||
icon: ["fab", "qq"],
|
||||
href: "https://jq.qq.com/?_wv=1027&k=5OFifDh",
|
||||
},
|
||||
{
|
||||
icon: ["fab", "telegram"],
|
||||
href: "https://t.me/botuniverse",
|
||||
},
|
||||
{
|
||||
icon: ["fab", "discord"],
|
||||
href: "https://discord.gg/VKtE6Gdc4h",
|
||||
},
|
||||
],
|
||||
links: [
|
||||
{
|
||||
title: "Learn",
|
||||
icon: ["fas", "book"],
|
||||
items: [
|
||||
{ label: "Introduction", to: "/docs/" },
|
||||
// { label: "QuickStart", to: "/docs/quick-start" },
|
||||
{ label: "Changelog", to: "/changelog" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "NoneBot Team",
|
||||
icon: ["fas", "user-friends"],
|
||||
items: [
|
||||
{
|
||||
label: "Homepage",
|
||||
href: "https://nonebot.dev",
|
||||
},
|
||||
{
|
||||
label: "NoneBot V1",
|
||||
href: "https://docs.nonebot.dev",
|
||||
},
|
||||
{ label: "NoneBot CLI", href: "https://cli.nonebot.dev" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Related",
|
||||
icon: ["fas", "external-link-alt"],
|
||||
items: [
|
||||
{ label: "OneBot", href: "https://onebot.dev/" },
|
||||
{ label: "go-cqhttp", href: "https://docs.go-cqhttp.org/" },
|
||||
{ label: "Mirai", href: "https://mirai.mamoe.net/" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
prism: {
|
||||
theme: lightCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
additionalLanguages: ["docker", "ini"],
|
||||
},
|
||||
algolia: {
|
||||
appId: "X0X5UACHZQ",
|
||||
apiKey: "ac03e1ac2bd0812e2ea38c0cc1ea38c5",
|
||||
indexName: "nonebot",
|
||||
contextualSearch: true,
|
||||
},
|
||||
tailwindConfig: require("./tailwind.config"),
|
||||
customCss: [require.resolve("./src/css/custom.css")],
|
||||
}),
|
||||
themeConfig,
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
module.exports = siteConfig;
|
||||
|
@ -11,7 +11,7 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"start": "docusaurus start --host 0.0.0.0 --port 3000",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
@ -22,27 +22,23 @@
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "2.0.0-beta.9",
|
||||
"@mdx-js/react": "^1.6.21",
|
||||
"@svgr/webpack": "^5.5.0",
|
||||
"clsx": "^1.1.1",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"docusaurus-preset-nonepress": "canary",
|
||||
"file-loader": "^6.2.0",
|
||||
"prism-react-renderer": "^1.2.1",
|
||||
"@docusaurus/core": "^2.4.1",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@nullbot/docusaurus-preset-nonepress": "^2.1.2",
|
||||
"clsx": "^1.2.1",
|
||||
"copy-text-to-clipboard": "^3.0.1",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^17.0.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-use-pagination": "^2.0.1",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"url-loader": "^4.1.1"
|
||||
"react-use-pagination": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "2.0.0-beta.9",
|
||||
"@tsconfig/docusaurus": "^1.0.4",
|
||||
"asciinema-player": "^3.0.0-rc.1",
|
||||
"typescript": "^4.3.5"
|
||||
"@docusaurus/module-type-aliases": "^2.4.1",
|
||||
"@tsconfig/docusaurus": "^1.0.5",
|
||||
"asciinema-player": "^3.5.0",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@ -55,5 +51,8 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14"
|
||||
}
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ const sidebars = {
|
||||
{
|
||||
type: "category",
|
||||
label: "开源之夏",
|
||||
collapsible: false,
|
||||
collapsible: true,
|
||||
items: [
|
||||
{
|
||||
type: "autogenerated",
|
||||
@ -102,8 +102,28 @@ const sidebars = {
|
||||
items: [
|
||||
{
|
||||
type: "link",
|
||||
label: "商店",
|
||||
href: "/store",
|
||||
label: "插件商店",
|
||||
href: "/store/plugins",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
label: "适配器商店",
|
||||
href: "/store/adapters",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
label: "驱动器商店",
|
||||
href: "/store/drivers",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
label: "机器人商店",
|
||||
href: "/store/bots",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
label: "Awesome NoneBot",
|
||||
href: "https://awesome.nonebot.dev",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
|
@ -1,260 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { ChromePicker } from "react-color";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import adapters from "../../static/adapters.json";
|
||||
import { Tag, useFilteredObjs } from "../libs/store";
|
||||
import Card from "./Card";
|
||||
import Modal from "./Modal";
|
||||
import ModalAction from "./ModalAction";
|
||||
import ModalContent from "./ModalContent";
|
||||
import ModalTitle from "./ModalTitle";
|
||||
import Paginate from "./Paginate";
|
||||
import TagComponent from "./Tag";
|
||||
|
||||
export default function Adapter(): JSX.Element {
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const {
|
||||
filter,
|
||||
setFilter,
|
||||
filteredObjs: filteredAdapters,
|
||||
} = useFilteredObjs(adapters);
|
||||
|
||||
const props = usePagination({
|
||||
totalItems: filteredAdapters.length,
|
||||
initialPageSize: 10,
|
||||
});
|
||||
const { startIndex, endIndex } = props;
|
||||
const currentAdapters = filteredAdapters.slice(startIndex, endIndex + 1);
|
||||
|
||||
const [form, setForm] = useState<{
|
||||
name: string;
|
||||
desc: string;
|
||||
projectLink: string;
|
||||
moduleName: string;
|
||||
homepage: string;
|
||||
}>({ name: "", desc: "", projectLink: "", moduleName: "", homepage: "" });
|
||||
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
const [label, setLabel] = useState<string>("");
|
||||
const [color, setColor] = useState<string>("#ea5252");
|
||||
|
||||
const urlEncode = (str: string) =>
|
||||
encodeURIComponent(str).replace(/%2B/gi, "+");
|
||||
|
||||
const onSubmit = () => {
|
||||
setModalOpen(false);
|
||||
const queries: { key: string; value: string }[] = [
|
||||
{ key: "template", value: "adapter_publish.yml" },
|
||||
{ key: "title", value: form.name && `Adapter: ${form.name}` },
|
||||
{ key: "labels", value: "Adapter" },
|
||||
{ key: "name", value: form.name },
|
||||
{ key: "description", value: form.desc },
|
||||
{ key: "pypi", value: form.projectLink },
|
||||
{ key: "module", value: form.moduleName },
|
||||
{ key: "homepage", value: form.homepage },
|
||||
{ key: "tags", value: JSON.stringify(tags) },
|
||||
];
|
||||
const urlQueries = queries
|
||||
.filter((query) => !!query.value)
|
||||
.map((query) => `${query.key}=${urlEncode(query.value)}`)
|
||||
.join("&");
|
||||
window.open(`https://github.com/nonebot/nonebot2/issues/new?${urlQueries}`);
|
||||
};
|
||||
const onChange = (event) => {
|
||||
const target = event.target;
|
||||
const value = target.type === "checkbox" ? target.checked : target.value;
|
||||
const name = target.name;
|
||||
|
||||
setForm({
|
||||
...form,
|
||||
[name]: value,
|
||||
});
|
||||
event.preventDefault();
|
||||
};
|
||||
const onChangeLabel = (event) => {
|
||||
setLabel(event.target.value);
|
||||
};
|
||||
const onChangeColor = (color) => {
|
||||
setColor(color.hex);
|
||||
};
|
||||
const validateTag = () => {
|
||||
return label.length >= 1 && label.length <= 10;
|
||||
};
|
||||
const newTag = () => {
|
||||
if (tags.length >= 3) {
|
||||
return;
|
||||
}
|
||||
if (validateTag()) {
|
||||
const tag = { label, color };
|
||||
setTags([...tags, tag]);
|
||||
}
|
||||
};
|
||||
const delTag = (index: number) => {
|
||||
setTags(tags.filter((_, i) => i !== index));
|
||||
};
|
||||
const insertTagType = (text: string) => {
|
||||
setLabel(text + label);
|
||||
ref.current.value = text + label;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4 px-4">
|
||||
<input
|
||||
className="w-full px-4 py-2 border rounded-full bg-light-nonepress-100 dark:bg-dark-nonepress-100"
|
||||
value={filter}
|
||||
placeholder="搜索适配器"
|
||||
onChange={(event) => setFilter(event.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="w-full rounded-lg bg-hero text-white"
|
||||
onClick={() => setModalOpen(true)}
|
||||
>
|
||||
发布适配器
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 p-4">
|
||||
<Paginate {...props} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-4">
|
||||
{currentAdapters.map((adapter, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
{...adapter}
|
||||
action={`nb adapter install ${adapter.project_link}`}
|
||||
actionDisabled={!adapter.project_link}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 p-4">
|
||||
<Paginate {...props} />
|
||||
</div>
|
||||
<Modal active={modalOpen} setActive={setModalOpen}>
|
||||
<ModalTitle title={"适配器信息"} />
|
||||
<ModalContent>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="grid grid-cols-1 gap-4 p-4">
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">适配器名称:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
maxLength={20}
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">适配器介绍:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="desc"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">PyPI 项目名:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="projectLink"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">import 包名:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="moduleName"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">仓库/主页:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="homepage"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<div className="px-4">
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">标签:</span>
|
||||
{tags.map((tag, index) => (
|
||||
<TagComponent
|
||||
key={index}
|
||||
{...tag}
|
||||
className="cursor-pointer"
|
||||
onClick={() => delTag(index)}
|
||||
/>
|
||||
))}
|
||||
</label>
|
||||
</div>
|
||||
<div className="px-4 pt-4">
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChangeLabel}
|
||||
/>
|
||||
<ChromePicker
|
||||
className="mt-2"
|
||||
color={color}
|
||||
disableAlpha={true}
|
||||
onChangeComplete={onChangeColor}
|
||||
/>
|
||||
<div className="flex flex-wrap mt-2 items-center">
|
||||
<span className="mr-2">Type:</span>
|
||||
<button
|
||||
className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={() => insertTagType("a:")}
|
||||
>
|
||||
Adapter
|
||||
</button>
|
||||
<button
|
||||
className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={() => insertTagType("t:")}
|
||||
>
|
||||
Topic
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex mt-2">
|
||||
<TagComponent label={label} color={color} />
|
||||
<button
|
||||
className={clsx(
|
||||
"px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]",
|
||||
{ "pointer-events-none opacity-60": !validateTag() }
|
||||
)}
|
||||
onClick={newTag}
|
||||
>
|
||||
添加标签
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalAction>
|
||||
<button
|
||||
className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={() => setModalOpen(false)}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
<button
|
||||
className="ml-2 px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
发布
|
||||
</button>
|
||||
</ModalAction>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import * as AsciinemaPlayer from "asciinema-player";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
import * as AsciinemaPlayer from "asciinema-player";
|
||||
|
||||
export type AsciinemaOptions = {
|
||||
cols: number;
|
||||
rows: number;
|
||||
@ -16,7 +17,7 @@ export type AsciinemaOptions = {
|
||||
fontSize: string;
|
||||
};
|
||||
|
||||
export type AsciinemaProps = {
|
||||
export type Props = {
|
||||
url: string;
|
||||
options?: Partial<AsciinemaOptions>;
|
||||
};
|
||||
@ -24,12 +25,12 @@ export type AsciinemaProps = {
|
||||
export default function AsciinemaContainer({
|
||||
url,
|
||||
options = {},
|
||||
}: AsciinemaProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
AsciinemaPlayer.create(url, ref.current, options);
|
||||
}, []);
|
||||
}, [url, options]);
|
||||
|
||||
return <div ref={ref} className="not-prose w-full max-w-full my-4"></div>;
|
||||
return <div ref={ref} className="not-prose ap-container"></div>;
|
||||
}
|
||||
|
@ -1,15 +1,24 @@
|
||||
import "asciinema-player/dist/bundle/asciinema-player.css";
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import "asciinema-player/dist/bundle/asciinema-player.css";
|
||||
import BrowserOnly from "@docusaurus/BrowserOnly";
|
||||
|
||||
export default function Asciinema(props): JSX.Element {
|
||||
import "./styles.css";
|
||||
import type { Props } from "./container";
|
||||
|
||||
export type { Props } from "./container";
|
||||
|
||||
export default function Asciinema(props: Props): JSX.Element {
|
||||
return (
|
||||
<BrowserOnly fallback={<div></div>}>
|
||||
<BrowserOnly
|
||||
fallback={
|
||||
<a href={props.url} title="Asciinema video player">
|
||||
Asciinema cast
|
||||
</a>
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const AsciinemaContainer = require("./container.tsx").default;
|
||||
return <AsciinemaContainer {...props} />;
|
||||
}}
|
||||
|
@ -1,3 +1,7 @@
|
||||
.asciinema-player svg {
|
||||
display: inline-block;
|
||||
.ap-player svg {
|
||||
@apply inline-block;
|
||||
}
|
||||
|
||||
.ap-container {
|
||||
@apply w-full my-4;
|
||||
}
|
||||
|
@ -1,233 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { ChromePicker } from "react-color";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import bots from "../../static/bots.json";
|
||||
import { Tag, useFilteredObjs } from "../libs/store";
|
||||
import Card from "./Card";
|
||||
import Modal from "./Modal";
|
||||
import ModalAction from "./ModalAction";
|
||||
import ModalContent from "./ModalContent";
|
||||
import ModalTitle from "./ModalTitle";
|
||||
import Paginate from "./Paginate";
|
||||
import TagComponent from "./Tag";
|
||||
|
||||
export default function Bot(): JSX.Element {
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const {
|
||||
filter,
|
||||
setFilter,
|
||||
filteredObjs: filteredBots,
|
||||
} = useFilteredObjs(bots);
|
||||
|
||||
const props = usePagination({
|
||||
totalItems: filteredBots.length,
|
||||
initialPageSize: 10,
|
||||
});
|
||||
const { startIndex, endIndex } = props;
|
||||
const currentBots = filteredBots.slice(startIndex, endIndex + 1);
|
||||
|
||||
const [form, setForm] = useState<{
|
||||
name: string;
|
||||
desc: string;
|
||||
homepage: string;
|
||||
}>({ name: "", desc: "", homepage: "" });
|
||||
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
const [label, setLabel] = useState<string>("");
|
||||
const [color, setColor] = useState<string>("#ea5252");
|
||||
|
||||
const urlEncode = (str: string) =>
|
||||
encodeURIComponent(str).replace(/%2B/gi, "+");
|
||||
|
||||
const onSubmit = () => {
|
||||
setModalOpen(false);
|
||||
const queries: { key: string; value: string }[] = [
|
||||
{ key: "template", value: "bot_publish.yml" },
|
||||
{ key: "title", value: form.name && `Bot: ${form.name}` },
|
||||
{ key: "labels", value: "Bot" },
|
||||
{ key: "name", value: form.name },
|
||||
{ key: "description", value: form.desc },
|
||||
{ key: "homepage", value: form.homepage },
|
||||
{ key: "tags", value: JSON.stringify(tags) },
|
||||
];
|
||||
const urlQueries = queries
|
||||
.filter((query) => !!query.value)
|
||||
.map((query) => `${query.key}=${urlEncode(query.value)}`)
|
||||
.join("&");
|
||||
window.open(`https://github.com/nonebot/nonebot2/issues/new?${urlQueries}`);
|
||||
};
|
||||
const onChange = (event) => {
|
||||
const target = event.target;
|
||||
const value = target.type === "checkbox" ? target.checked : target.value;
|
||||
const name = target.name;
|
||||
|
||||
setForm({
|
||||
...form,
|
||||
[name]: value,
|
||||
});
|
||||
event.preventDefault();
|
||||
};
|
||||
const onChangeLabel = (event) => {
|
||||
setLabel(event.target.value);
|
||||
};
|
||||
const onChangeColor = (color) => {
|
||||
setColor(color.hex);
|
||||
};
|
||||
const validateTag = () => {
|
||||
return label.length >= 1 && label.length <= 10;
|
||||
};
|
||||
const newTag = () => {
|
||||
if (tags.length >= 3) {
|
||||
return;
|
||||
}
|
||||
if (validateTag()) {
|
||||
const tag = { label, color };
|
||||
setTags([...tags, tag]);
|
||||
}
|
||||
};
|
||||
const delTag = (index: number) => {
|
||||
setTags(tags.filter((_, i) => i !== index));
|
||||
};
|
||||
const insertTagType = (text: string) => {
|
||||
setLabel(text + label);
|
||||
ref.current.value = text + label;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4 px-4">
|
||||
<input
|
||||
className="w-full px-4 py-2 border rounded-full bg-light-nonepress-100 dark:bg-dark-nonepress-100"
|
||||
value={filter}
|
||||
placeholder="搜索机器人"
|
||||
onChange={(event) => setFilter(event.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="w-full rounded-lg bg-hero text-white"
|
||||
onClick={() => setModalOpen(true)}
|
||||
>
|
||||
发布机器人
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 p-4">
|
||||
<Paginate {...props} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-4">
|
||||
{currentBots.map((bot, index) => (
|
||||
<Card key={index} {...bot} />
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 p-4">
|
||||
<Paginate {...props} />
|
||||
</div>
|
||||
<Modal active={modalOpen} setActive={setModalOpen}>
|
||||
<ModalTitle title={"机器人信息"} />
|
||||
<ModalContent>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="grid grid-cols-1 gap-4 p-4">
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">机器人名称:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
maxLength={20}
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">机器人介绍:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="desc"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">仓库/主页:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="homepage"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<div className="px-4">
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">标签:</span>
|
||||
{tags.map((tag, index) => (
|
||||
<TagComponent
|
||||
key={index}
|
||||
{...tag}
|
||||
className="cursor-pointer"
|
||||
onClick={() => delTag(index)}
|
||||
/>
|
||||
))}
|
||||
</label>
|
||||
</div>
|
||||
<div className="px-4 pt-4">
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChangeLabel}
|
||||
/>
|
||||
<ChromePicker
|
||||
className="mt-2"
|
||||
color={color}
|
||||
disableAlpha={true}
|
||||
onChangeComplete={onChangeColor}
|
||||
/>
|
||||
<div className="flex flex-wrap mt-2 items-center">
|
||||
<span className="mr-2">Type:</span>
|
||||
<button
|
||||
className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={() => insertTagType("a:")}
|
||||
>
|
||||
Adapter
|
||||
</button>
|
||||
<button
|
||||
className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={() => insertTagType("t:")}
|
||||
>
|
||||
Topic
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex mt-2">
|
||||
<TagComponent label={label} color={color} />
|
||||
<button
|
||||
className={clsx(
|
||||
"px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]",
|
||||
{ "pointer-events-none opacity-60": !validateTag() }
|
||||
)}
|
||||
onClick={newTag}
|
||||
>
|
||||
添加标签
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalAction>
|
||||
<button
|
||||
className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={() => setModalOpen(false)}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
<button
|
||||
className="ml-2 px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
发布
|
||||
</button>
|
||||
</ModalAction>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import copy from "copy-to-clipboard";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import Link from "@docusaurus/Link";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import type { Obj } from "../../libs/store";
|
||||
import Tag from "../Tag";
|
||||
|
||||
export default function Card({
|
||||
module_name,
|
||||
project_link,
|
||||
name,
|
||||
desc,
|
||||
author,
|
||||
homepage,
|
||||
tags,
|
||||
is_official,
|
||||
action,
|
||||
actionDisabled = false,
|
||||
actionLabel = "点击复制安装命令",
|
||||
}: Obj & {
|
||||
action?: string;
|
||||
actionLabel?: string;
|
||||
actionDisabled?: boolean;
|
||||
}): JSX.Element {
|
||||
const isGithub = /^https:\/\/github.com\/[^/]+\/[^/]+/.test(homepage);
|
||||
const [copied, setCopied] = useState<boolean>(false);
|
||||
|
||||
const copyAction = () => {
|
||||
copy(action);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="block max-w-full px-4 border-2 rounded-lg outline-none no-underline bg-light-nonepress-100 dark:bg-dark-nonepress-100 border-light-nonepress-200 dark:border-dark-nonepress-200 shadow-md shadow-light-nonepress-300 dark:shadow-dark-nonepress-300">
|
||||
<div className="flex justify-between pt-4 text-lg font-medium">
|
||||
<span>
|
||||
{name}
|
||||
{is_official && (
|
||||
<FontAwesomeIcon
|
||||
icon={["fas", "check-circle"]}
|
||||
className="text-green-600 ml-2"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
{homepage && (
|
||||
<Link
|
||||
href={homepage}
|
||||
className="text-black dark:text-white opacity-60 hover:text-hero dark:hover:text-white hover:opacity-100"
|
||||
>
|
||||
{isGithub ? (
|
||||
<FontAwesomeIcon icon={["fab", "github"]} />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={["fas", "link"]} />
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
{tags && (
|
||||
<div className="pt-2 pb-4">
|
||||
{tags.map((tag, index) => (
|
||||
<Tag key={index} {...tag} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* FIXME: full height */}
|
||||
{desc && (
|
||||
<div className="pb-4 text-sm font-normal opacity-60">{desc}</div>
|
||||
)}
|
||||
{project_link && (
|
||||
<div className="my-2 text-sm font-normal opacity-60 font-mono">
|
||||
<FontAwesomeIcon icon={["fas", "cube"]} className="mr-2" />
|
||||
{project_link}
|
||||
</div>
|
||||
)}
|
||||
{module_name && (
|
||||
<div className="my-2 text-sm font-normal opacity-60 font-mono">
|
||||
<FontAwesomeIcon icon={["fas", "fingerprint"]} className="mr-2" />
|
||||
{module_name}
|
||||
</div>
|
||||
)}
|
||||
{/* TODO: add user avatar */}
|
||||
{/* link: https://github.com/<username>.png */}
|
||||
{author && (
|
||||
<div className="my-2 text-sm font-normal opacity-60 font-mono">
|
||||
<FontAwesomeIcon icon={["fas", "user"]} className="mr-2" />
|
||||
{author}
|
||||
</div>
|
||||
)}
|
||||
{action && actionLabel && (
|
||||
<button
|
||||
className={clsx(
|
||||
"my-2 text-sm py-2 w-full rounded select-none bg-light-nonepress-200 dark:bg-dark-nonepress-200 active:bg-light-nonepress-300 active:dark:bg-dark-nonepress-300",
|
||||
{ "opacity-60 pointer-events-none": actionDisabled }
|
||||
)}
|
||||
onClick={copyAction}
|
||||
>
|
||||
<span className="flex grow items-center justify-center">
|
||||
{copied ? "复制成功" : actionLabel}
|
||||
<FontAwesomeIcon
|
||||
icon={["fas", copied ? "check-circle" : "copy"]}
|
||||
className="ml-2"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
import React from "react";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import drivers from "../../static/drivers.json";
|
||||
import { useFilteredObjs } from "../libs/store";
|
||||
import Card from "./Card";
|
||||
import Paginate from "./Paginate";
|
||||
|
||||
export default function Driver(): JSX.Element {
|
||||
const {
|
||||
filter,
|
||||
setFilter,
|
||||
filteredObjs: filteredDrivers,
|
||||
} = useFilteredObjs(drivers);
|
||||
|
||||
const props = usePagination({
|
||||
totalItems: filteredDrivers.length,
|
||||
initialPageSize: 10,
|
||||
});
|
||||
const { startIndex, endIndex } = props;
|
||||
const currentDrivers = filteredDrivers.slice(startIndex, endIndex + 1);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4 px-4">
|
||||
<input
|
||||
className="w-full px-4 py-2 border rounded-full bg-light-nonepress-100 dark:bg-dark-nonepress-100"
|
||||
value={filter}
|
||||
placeholder="搜索驱动器"
|
||||
onChange={(event) => setFilter(event.target.value)}
|
||||
/>
|
||||
<button className="w-full rounded-lg bg-hero text-white opacity-60">
|
||||
发布驱动器
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 p-4">
|
||||
<Paginate {...props} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-4">
|
||||
{currentDrivers.map((driver, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
{...driver}
|
||||
action={`nb driver install ${driver.project_link}`}
|
||||
actionDisabled={!driver.project_link}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 p-4">
|
||||
<Paginate {...props} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
import React, { PropsWithChildren } from "react";
|
||||
|
||||
import Link from "@docusaurus/Link";
|
||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import Logo from "@theme/Logo";
|
||||
|
||||
export function Hero(): JSX.Element {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap p-16 mx-auto max-w-7xl h-screen relative px-4 sm:p-24">
|
||||
<div className="flex-grow self-center text-center">
|
||||
<Logo imageClassName="max-h-48" />
|
||||
<h1 className="text-5xl tracking-tight font-light sm:text-5xl md:text-5xl">
|
||||
<span className="text-hero">N</span>one
|
||||
<span className="text-hero">B</span>ot
|
||||
</h1>
|
||||
<p className="my-3 max-w-md mx-auto text-sm font-medium tracking-wide uppercase opacity-70 md:mt-5 md:max-w-3xl">
|
||||
{siteConfig.tagline}
|
||||
</p>
|
||||
<div className="mt-8">
|
||||
<Link
|
||||
to="/docs/"
|
||||
className="inline-block bg-hero text-white font-bold rounded-lg px-6 py-3"
|
||||
>
|
||||
开始使用 <FontAwesomeIcon icon={["fas", "chevron-right"]} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute flex-grow flex items-center justify-between bottom-0 right-0 w-full">
|
||||
<div className="mx-auto self-start animate-bounce">
|
||||
<FontAwesomeIcon
|
||||
className="text-4xl text-hero"
|
||||
icon={["fas", "angle-down"]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type Feature = {
|
||||
readonly title: string;
|
||||
readonly tagline?: string;
|
||||
readonly description?: string;
|
||||
readonly annotaion?: string;
|
||||
};
|
||||
|
||||
export function HeroFeature(props: PropsWithChildren<Feature>): JSX.Element {
|
||||
const { title, tagline, description, annotaion, children } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mt-3 mb-3 max-w-md mx-auto text-sm font-medium tracking-wide uppercase opacity-70 md:mt-5 md:max-w-3xl">
|
||||
{tagline}
|
||||
</p>
|
||||
<h1 className="font-mono font-light text-4xl tracking-tight sm:text-5xl md:text-5xl text-hero">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="mt-10 mb-6">{description}</p>
|
||||
{children}
|
||||
<p className="text-sm italic opacity-70">{annotaion}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeroFeatureSingle(
|
||||
props: PropsWithChildren<Feature>
|
||||
): JSX.Element {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto py-16 px-4 text-center md:px-16">
|
||||
<HeroFeature {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeroFeatureDouble(
|
||||
props: PropsWithChildren<{ features: [Feature, Feature] }>
|
||||
): JSX.Element {
|
||||
const {
|
||||
features: [feature1, feature2],
|
||||
children,
|
||||
} = props;
|
||||
|
||||
let children1, children2;
|
||||
if (Array.isArray(children) && children.length === 2) {
|
||||
[children1, children2] = children;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto py-16 px-4 md:grid md:grid-cols-2 md:gap-6 md:px-16">
|
||||
<div className="pb-16 text-center md:pb-0">
|
||||
<HeroFeature {...feature1} children={children1} />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<HeroFeature {...feature2} children={children2} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
174
website/src/components/Home/Feature.tsx
Normal file
174
website/src/components/Home/Feature.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
import React from "react";
|
||||
|
||||
import CodeBlock from "@theme/CodeBlock";
|
||||
|
||||
export type Feature = {
|
||||
title: string;
|
||||
tagline?: string;
|
||||
description?: string;
|
||||
annotaion?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function HomeFeature({
|
||||
title,
|
||||
tagline,
|
||||
description,
|
||||
annotaion,
|
||||
children,
|
||||
}: Feature): JSX.Element {
|
||||
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">
|
||||
{tagline}
|
||||
</p>
|
||||
<h1 className="mt-3 font-mono font-light text-4xl tracking-tight sm:text-5xl md:text-5xl text-primary">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="mt-10 mb-6">{description}</p>
|
||||
{children}
|
||||
<p className="text-sm italic text-base-content/70">{annotaion}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HomeFeatureSingleColumn(props: Feature): JSX.Element {
|
||||
return (
|
||||
<div className="grid mx-auto px-4 py-8 md:px-16">
|
||||
<HomeFeature {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HomeFeatureDoubleColumn({
|
||||
features: [feature1, feature2],
|
||||
children,
|
||||
}: {
|
||||
features: [Feature, Feature];
|
||||
children?: [React.ReactNode, React.ReactNode];
|
||||
}): JSX.Element {
|
||||
const [children1, children2] = children ?? [];
|
||||
|
||||
return (
|
||||
<div className="grid gap-x-6 gap-y-8 grid-cols-1 lg:grid-cols-2 max-w-7xl mx-auto px-4 py-8 md:px-16">
|
||||
<HomeFeature {...feature1}>{children1}</HomeFeature>
|
||||
<HomeFeature {...feature2}>{children2}</HomeFeature>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HomeFeatures(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<HomeFeatureSingleColumn
|
||||
title="开箱即用"
|
||||
tagline="out of box"
|
||||
description="使用 NB-CLI 快速构建属于你的机器人"
|
||||
>
|
||||
<CodeBlock
|
||||
title="Installation"
|
||||
language="bash"
|
||||
className="home-codeblock"
|
||||
>
|
||||
{[
|
||||
"$ pipx install nb-cli",
|
||||
"$ nb",
|
||||
// "d8b db .d88b. d8b db d88888b d8888b. .d88b. d888888b",
|
||||
// "888o 88 .8P Y8. 888o 88 88' 88 `8D .8P Y8. `~~88~~'",
|
||||
// "88V8o 88 88 88 88V8o 88 88ooooo 88oooY' 88 88 88",
|
||||
// "88 V8o88 88 88 88 V8o88 88~~~~~ 88~~~b. 88 88 88",
|
||||
// "88 V888 `8b d8' 88 V888 88. 88 8D `8b d8' 88",
|
||||
// "VP V8P `Y88P' VP V8P Y88888P Y8888P' `Y88P' YP",
|
||||
"[?] What do you want to do?",
|
||||
"❯ Create a NoneBot project.",
|
||||
" Run the bot in current folder.",
|
||||
" Manage bot driver.",
|
||||
" Manage bot adapters.",
|
||||
" Manage bot plugins.",
|
||||
" ...",
|
||||
].join("\n")}
|
||||
</CodeBlock>
|
||||
</HomeFeatureSingleColumn>
|
||||
<HomeFeatureDoubleColumn
|
||||
features={[
|
||||
{
|
||||
title: "插件系统",
|
||||
tagline: "plugin system",
|
||||
description: "插件化开发,模块化管理",
|
||||
},
|
||||
{
|
||||
title: "跨平台支持",
|
||||
tagline: "cross-platform support",
|
||||
description: "支持多种平台,以及多样的事件响应方式",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CodeBlock title="" language="python" className="home-codeblock">
|
||||
{[
|
||||
"import nonebot",
|
||||
"# 加载一个插件",
|
||||
'nonebot.load_plugin("path.to.your.plugin")',
|
||||
"# 从文件夹加载插件",
|
||||
'nonebot.load_plugins("plugins")',
|
||||
"# 从配置文件加载多个插件",
|
||||
'nonebot.load_from_json("plugins.json")',
|
||||
'nonebot.load_from_toml("pyproject.toml")',
|
||||
].join("\n")}
|
||||
</CodeBlock>
|
||||
<CodeBlock title="" language="python" className="home-codeblock">
|
||||
{[
|
||||
"import nonebot",
|
||||
"# OneBot",
|
||||
"from nonebot.adapters.onebot.v11 import Adapter as OneBotAdapter",
|
||||
"# QQ 频道",
|
||||
"from nonebot.adapters.qqguild import Adapter as QQGuildAdapter",
|
||||
"driver = nonebot.get_driver()",
|
||||
"driver.register_adapter(OneBotAdapter)",
|
||||
"driver.register_adapter(QQGuildAdapter)",
|
||||
].join("\n")}
|
||||
</CodeBlock>
|
||||
</HomeFeatureDoubleColumn>
|
||||
<HomeFeatureDoubleColumn
|
||||
features={[
|
||||
{
|
||||
title: "异步开发",
|
||||
tagline: "asynchronous first",
|
||||
description: "异步优先式开发,提高运行效率",
|
||||
},
|
||||
{
|
||||
title: "依赖注入",
|
||||
tagline: "builtin dependency injection system",
|
||||
description: "简单清晰的依赖注入系统,内置依赖函数减少用户代码",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CodeBlock title="" language="python" className="home-codeblock">
|
||||
{[
|
||||
"from nonebot import on_message",
|
||||
"# 注册一个消息响应器",
|
||||
"matcher = on_message()",
|
||||
"# 注册一个消息处理器",
|
||||
"# 并重复收到的消息",
|
||||
"@matcher.handle()",
|
||||
"async def handler(event: Event) -> None:",
|
||||
" await matcher.send(event.get_message())",
|
||||
].join("\n")}
|
||||
</CodeBlock>
|
||||
<CodeBlock title="" language="python" className="home-codeblock">
|
||||
{[
|
||||
"from nonebot import on_command",
|
||||
"# 注册一个命令响应器",
|
||||
'matcher = on_command("help", alias={"帮助"})',
|
||||
"# 注册一个命令处理器",
|
||||
"# 通过依赖注入获得命令名以及参数",
|
||||
"@matcher.handle()",
|
||||
"async def handler(cmd = Command(), arg = CommandArg()) -> None:",
|
||||
" await matcher.finish()",
|
||||
].join("\n")}
|
||||
</CodeBlock>
|
||||
</HomeFeatureDoubleColumn>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(HomeFeatures);
|
69
website/src/components/Home/Hero.tsx
Normal file
69
website/src/components/Home/Hero.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import Link from "@docusaurus/Link";
|
||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNonepressThemeConfig } from "@nullbot/docusaurus-theme-nonepress/client";
|
||||
// @ts-expect-error: we need to make package have type: module
|
||||
import copy from "copy-text-to-clipboard";
|
||||
|
||||
import IconCopy from "@theme/Icon/Copy";
|
||||
import IconSuccess from "@theme/Icon/Success";
|
||||
|
||||
function HomeHeroInstallButton(): JSX.Element {
|
||||
const code = "pipx run nb-cli create";
|
||||
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const copyTimeout = useRef<number | undefined>(undefined);
|
||||
|
||||
const handleCopyCode = useCallback(() => {
|
||||
copy(code);
|
||||
setIsCopied(true);
|
||||
copyTimeout.current = window.setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 1500);
|
||||
}, [code]);
|
||||
|
||||
useEffect(() => () => window.clearTimeout(copyTimeout.current), []);
|
||||
|
||||
return (
|
||||
<button
|
||||
className="btn no-animation home-hero-copy"
|
||||
onClick={handleCopyCode}
|
||||
>
|
||||
<code>$ {code}</code>
|
||||
{isCopied ? <IconSuccess className="text-success" /> : <IconCopy />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function HomeHero(): JSX.Element {
|
||||
const {
|
||||
siteConfig: { tagline },
|
||||
} = useDocusaurusContext();
|
||||
const {
|
||||
navbar: { logo },
|
||||
} = useNonepressThemeConfig();
|
||||
|
||||
return (
|
||||
<div className="home-hero">
|
||||
<img src={logo!.src} alt={logo!.alt} className="home-hero-logo" />
|
||||
<h1 className="home-hero-title">
|
||||
<span className="text-primary">None</span>
|
||||
Bot
|
||||
</h1>
|
||||
<p className="home-hero-tagline">{tagline}</p>
|
||||
<div className="home-hero-actions">
|
||||
<Link to="/docs/" className="btn btn-primary">
|
||||
开始使用 <FontAwesomeIcon icon={["fas", "chevron-right"]} />
|
||||
</Link>
|
||||
<HomeHeroInstallButton />
|
||||
</div>
|
||||
<div className="home-hero-next">
|
||||
<FontAwesomeIcon icon={["fas", "angle-down"]} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(HomeHero);
|
14
website/src/components/Home/index.tsx
Normal file
14
website/src/components/Home/index.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
|
||||
import "./styles.css";
|
||||
import HomeFeatures from "./Feature";
|
||||
import HomeHero from "./Hero";
|
||||
|
||||
export default function HomeContent(): JSX.Element {
|
||||
return (
|
||||
<div className="home-container">
|
||||
<HomeHero />
|
||||
<HomeFeatures />
|
||||
</div>
|
||||
);
|
||||
}
|
41
website/src/components/Home/styles.css
Normal file
41
website/src/components/Home/styles.css
Normal file
@ -0,0 +1,41 @@
|
||||
.home {
|
||||
&-container {
|
||||
@apply -mt-16;
|
||||
}
|
||||
|
||||
&-hero {
|
||||
@apply relative flex flex-col items-center justify-center gap-4 h-screen;
|
||||
|
||||
&-logo {
|
||||
@apply h-48 w-auto;
|
||||
}
|
||||
|
||||
&-title {
|
||||
@apply text-5xl font-normal tracking-tight;
|
||||
}
|
||||
|
||||
&-tagline {
|
||||
@apply text-sm font-medium uppercase tracking-wide text-base-content/70;
|
||||
}
|
||||
|
||||
&-actions {
|
||||
@apply flex flex-col sm:flex-row gap-4;
|
||||
}
|
||||
|
||||
&-copy {
|
||||
@apply font-normal normal-case text-base-content/70;
|
||||
}
|
||||
|
||||
&-next {
|
||||
@apply absolute bottom-4;
|
||||
|
||||
& svg {
|
||||
@apply animate-bounce text-primary text-4xl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-codeblock {
|
||||
@apply inline-block !max-w-[600px];
|
||||
}
|
||||
}
|
@ -1,40 +1,56 @@
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import Logo from "@theme/Logo";
|
||||
import clsx from "clsx";
|
||||
|
||||
import styles from "./styles.module.css";
|
||||
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";
|
||||
|
||||
export type Message = {
|
||||
position?: "left" | "right";
|
||||
msg: string;
|
||||
position?: "left" | "right";
|
||||
monospace?: boolean;
|
||||
};
|
||||
|
||||
function MessageBox({
|
||||
msg,
|
||||
isRight,
|
||||
}: {
|
||||
msg: string;
|
||||
isRight: boolean;
|
||||
}): JSX.Element {
|
||||
position = "left",
|
||||
monospace = false,
|
||||
}: Message): JSX.Element {
|
||||
const {
|
||||
navbar: { logo },
|
||||
} = useNonepressThemeConfig();
|
||||
const sources = {
|
||||
light: useBaseUrl(logo!.src),
|
||||
dark: useBaseUrl(logo!.srcDark || logo!.src),
|
||||
};
|
||||
|
||||
const isRight = position === "right";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.message, {
|
||||
[styles.messageRight]: isRight,
|
||||
})}
|
||||
>
|
||||
{isRight ? (
|
||||
<div className={clsx("bg-cyan-600 text-base", styles.messageAvatar)}>
|
||||
<FontAwesomeIcon icon={["fas", "user"]} />
|
||||
<div className={clsx("chat", isRight ? "chat-end" : "chat-start")}>
|
||||
<div className="chat-image avatar">
|
||||
<div
|
||||
className={clsx(
|
||||
"messenger-chat-avatar",
|
||||
isRight && "messenger-chat-avatar-user"
|
||||
)}
|
||||
>
|
||||
{isRight ? (
|
||||
<FontAwesomeIcon icon={["fas", "user"]} />
|
||||
) : (
|
||||
<ThemedImage sources={sources} />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className={clsx("transparent", styles.messageAvatar)}>
|
||||
<Logo imageClassName="h-full w-full" disabled />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(styles.messageBox, { "order-first": isRight })}
|
||||
className={clsx(
|
||||
"chat-bubble messenger-chat-bubble",
|
||||
monospace && "font-mono"
|
||||
)}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: msg.replace(/\n/g, "<br/>").replace(/ /g, " "),
|
||||
}}
|
||||
@ -48,62 +64,55 @@ export default function Messenger({
|
||||
}: {
|
||||
msgs?: Message[];
|
||||
}): JSX.Element {
|
||||
const isRight = (msg: Message): boolean => msg.position === "right";
|
||||
|
||||
return (
|
||||
<div className="block w-full max-w-full my-4 rounded shadow-md outline-none no-underline bg-light-nonepress-100 dark:bg-dark-nonepress-100">
|
||||
<header className="flex items-center h-12 px-4 bg-blue-500 text-white rounded-t-[inherit]">
|
||||
<div className="text-left text-base grow">
|
||||
<div className="messenger-container">
|
||||
<header className="messenger-title">
|
||||
<div className="messenger-title-back">
|
||||
<FontAwesomeIcon icon={["fas", "chevron-left"]} />
|
||||
</div>
|
||||
<div className="flex-initial grow-0">
|
||||
<span className="text-xl font-bold">NoneBot</span>
|
||||
<div className="messenger-title-name">
|
||||
<span>NoneBot</span>
|
||||
</div>
|
||||
<div className="text-right text-base grow">
|
||||
<FontAwesomeIcon icon={["fas", "user"]} />
|
||||
<div className="messenger-title-more">
|
||||
<FontAwesomeIcon icon={["fas", "bars"]} />
|
||||
</div>
|
||||
</header>
|
||||
<div className="p-3 min-h-[150px]">
|
||||
<div className="messenger-chat">
|
||||
{msgs.map((msg, i) => (
|
||||
<MessageBox msg={msg.msg} isRight={isRight(msg)} key={i} />
|
||||
<MessageBox {...msg} key={i} />
|
||||
))}
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex-1 p-1 max-w-full">
|
||||
<div className="messenger-footer">
|
||||
<div className="messenger-footer-action">
|
||||
<div className="messenger-footer-action-input">
|
||||
<input
|
||||
className="w-full rounded bg-light dark:bg-dark focus:outline-none focus:ring focus:border-blue-500"
|
||||
className="input input-xs input-bordered input-info w-full"
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-initial grow-0 w-fit">
|
||||
<button
|
||||
className={clsx(
|
||||
"h-7 px-3 rounded-full bg-blue-500 text-white",
|
||||
styles.messageSendButton
|
||||
)}
|
||||
>
|
||||
<span>发送</span>
|
||||
<div className="messenger-footer-action-send">
|
||||
<button className="btn btn-xs btn-info no-animation text-white">
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row items-center text-center text-base text-gray-600">
|
||||
<div className="p-1 shrink-0 grow-0 basis-1/6">
|
||||
<div className="messenger-footer-tools">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={["fas", "microphone"]} />
|
||||
</div>
|
||||
<div className="p-1 shrink-0 grow-0 basis-1/6">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={["fas", "image"]} />
|
||||
</div>
|
||||
<div className="p-1 shrink-0 grow-0 basis-1/6">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={["fas", "camera"]} />
|
||||
</div>
|
||||
<div className="p-1 shrink-0 grow-0 basis-1/6">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={["fas", "wallet"]} />
|
||||
</div>
|
||||
<div className="p-1 shrink-0 grow-0 basis-1/6">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={["fas", "smile-wink"]} />
|
||||
</div>
|
||||
<div className="p-1 shrink-0 grow-0 basis-1/6">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={["fas", "plus-circle"]} />
|
||||
</div>
|
||||
</div>
|
||||
|
66
website/src/components/Messenger/styles.css
Normal file
66
website/src/components/Messenger/styles.css
Normal file
@ -0,0 +1,66 @@
|
||||
.messenger {
|
||||
&-container {
|
||||
@apply block w-full my-4 overflow-hidden;
|
||||
@apply rounded-lg outline-none bg-base-200;
|
||||
@apply transition-[background-color] duration-500;
|
||||
}
|
||||
|
||||
&-title {
|
||||
@apply flex items-center h-12 px-4 bg-info text-white;
|
||||
|
||||
&-back {
|
||||
@apply text-left text-base grow;
|
||||
}
|
||||
|
||||
&-name {
|
||||
@apply flex-initial grow-0 text-xl font-bold;
|
||||
}
|
||||
|
||||
&-more {
|
||||
@apply text-right text-base grow;
|
||||
}
|
||||
}
|
||||
|
||||
&-chat {
|
||||
@apply p-4 min-h-[150px];
|
||||
|
||||
&-avatar {
|
||||
@apply !flex items-center justify-center;
|
||||
@apply w-10 rounded-full;
|
||||
|
||||
&-user {
|
||||
@apply bg-info text-white;
|
||||
}
|
||||
}
|
||||
|
||||
&-bubble {
|
||||
@apply bg-base-100 text-base-content [word-break:break-word];
|
||||
@apply transition-[color,background-color] duration-500;
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
@apply px-4;
|
||||
|
||||
&-action {
|
||||
@apply flex items-center gap-2;
|
||||
|
||||
&-input {
|
||||
@apply flex-1;
|
||||
|
||||
& > input {
|
||||
@apply transition-[color,background-color] duration-500;
|
||||
}
|
||||
}
|
||||
|
||||
&-send {
|
||||
@apply flex-initial w-fit;
|
||||
}
|
||||
}
|
||||
|
||||
&-tools {
|
||||
@apply grid grid-cols-6 items-center py-1;
|
||||
@apply text-center text-base text-base-content/60;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
.message {
|
||||
@apply flex flex-row flex-wrap justify-start;
|
||||
}
|
||||
|
||||
.messageRight {
|
||||
@apply !justify-end;
|
||||
}
|
||||
|
||||
.message .messageAvatar {
|
||||
@apply relative inline-flex items-center justify-center text-center align-middle h-9 w-9 rounded-full;
|
||||
}
|
||||
|
||||
.message .messageBox {
|
||||
@apply relative w-fit max-w-[55%] px-2 py-[0.375rem] mx-3 my-2 rounded-lg bg-light;
|
||||
}
|
||||
:global(.dark) .message .messageBox {
|
||||
@apply !bg-dark;
|
||||
}
|
||||
|
||||
.message .messageBox::after {
|
||||
content: "";
|
||||
border-bottom: 7px solid;
|
||||
@apply absolute top-0 right-full w-2 h-3 text-light rounded-bl-lg;
|
||||
}
|
||||
:global(.dark) .message .messageBox::after {
|
||||
@apply !text-dark;
|
||||
}
|
||||
.message.messageRight .messageBox::after {
|
||||
@apply !left-full !right-auto !rounded-bl-[0] !rounded-br-lg;
|
||||
}
|
||||
|
||||
.messageSendButton {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
export default function Modal({
|
||||
active,
|
||||
setActive,
|
||||
children,
|
||||
}: {
|
||||
active: boolean;
|
||||
setActive: (active: boolean) => void;
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{/* overlay */}
|
||||
<div
|
||||
className={clsx(
|
||||
"fixed top-0 bottom-0 left-0 right-0 flex items-center justify-center transition z-[200] pointer-events-auto",
|
||||
{ "!hidden": !active }
|
||||
)}
|
||||
onClick={() => setActive(false)}
|
||||
>
|
||||
<div className="absolute top-0 bottom-0 left-0 right-0 h-full w-full bg-gray-800 opacity-[.46]"></div>
|
||||
</div>
|
||||
{/* modal */}
|
||||
<div
|
||||
className={clsx(
|
||||
"fixed top-0 left-0 flex items-center justify-center h-full w-full z-[201] pointer-events-none",
|
||||
{ "!hidden": !active }
|
||||
)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="w-full max-w-[600px] max-h-[90%] overflow-y-auto rounded shadow-lg m-6 z-[inherit] pointer-events-auto thin-scrollbar">
|
||||
<div className="bg-light-nonepress-100 dark:bg-dark-nonepress-100">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export default function ModalAction({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
return <div className="px-4 py-2 flex justify-end">{children}</div>;
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export default function ModalContent({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
return <div className="px-6 pb-5 w-full">{children}</div>;
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export default function ModalTitle({ title }: { title: string }): JSX.Element {
|
||||
return (
|
||||
<div className="px-6 pt-4 pb-2 font-medium text-xl">
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,39 +1,55 @@
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
import clsx from "clsx";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import type { usePagination } from "react-use-pagination";
|
||||
|
||||
import { useContentWidth } from "../../libs/width";
|
||||
import styles from "./styles.module.css";
|
||||
import "./styles.css";
|
||||
|
||||
const MAX_LENGTH = 7;
|
||||
|
||||
export type Props = Pick<
|
||||
ReturnType<typeof usePagination>,
|
||||
| "totalPages"
|
||||
| "currentPage"
|
||||
| "setNextPage"
|
||||
| "setPreviousPage"
|
||||
| "setPage"
|
||||
| "previousEnabled"
|
||||
| "nextEnabled"
|
||||
> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function Paginate({
|
||||
className,
|
||||
totalPages,
|
||||
currentPage,
|
||||
setPreviousPage,
|
||||
setNextPage,
|
||||
setPage,
|
||||
currentPage,
|
||||
previousEnabled,
|
||||
nextEnabled,
|
||||
}: ReturnType<typeof usePagination>): JSX.Element {
|
||||
const [containerElement, setContainerElement] = useState<HTMLElement | null>(
|
||||
null
|
||||
);
|
||||
}: Props): JSX.Element {
|
||||
// const [containerElement, setContainerElement] = useState<HTMLElement | null>(
|
||||
// null
|
||||
// );
|
||||
|
||||
const ref = useCallback(
|
||||
(element: HTMLElement | null) => {
|
||||
setContainerElement(element);
|
||||
},
|
||||
[setContainerElement]
|
||||
);
|
||||
// const ref = useCallback(
|
||||
// (element: HTMLElement | null) => {
|
||||
// setContainerElement(element);
|
||||
// },
|
||||
// [setContainerElement]
|
||||
// );
|
||||
|
||||
const maxWidth = useContentWidth(
|
||||
containerElement?.parentElement ?? undefined
|
||||
);
|
||||
const maxLength = Math.min(
|
||||
(maxWidth && Math.floor(maxWidth / 50) - 2) || totalPages,
|
||||
totalPages
|
||||
);
|
||||
// const maxWidth = useContentWidth(
|
||||
// containerElement?.parentElement ?? undefined
|
||||
// );
|
||||
// const maxLength = Math.min(
|
||||
// (maxWidth && Math.floor(maxWidth / 50) - 2) || totalPages,
|
||||
// totalPages
|
||||
// );
|
||||
|
||||
const range = useCallback((start: number, end: number) => {
|
||||
const result = [];
|
||||
@ -47,12 +63,12 @@ export default function Paginate({
|
||||
const pages: (React.ReactNode | number)[] = [];
|
||||
const ellipsis = <FontAwesomeIcon icon="ellipsis-h" />;
|
||||
|
||||
const even = maxLength % 2 === 0 ? 1 : 0;
|
||||
const left = Math.floor(maxLength / 2);
|
||||
const even = MAX_LENGTH % 2 === 0 ? 1 : 0;
|
||||
const left = Math.floor(MAX_LENGTH / 2);
|
||||
const right = totalPages - left + even + 1;
|
||||
currentPage = currentPage + 1;
|
||||
|
||||
if (totalPages <= maxLength) {
|
||||
if (totalPages <= MAX_LENGTH) {
|
||||
pages.push(...range(1, totalPages));
|
||||
} else if (currentPage > left && currentPage < right) {
|
||||
const firstItem = 1;
|
||||
@ -74,34 +90,44 @@ export default function Paginate({
|
||||
}
|
||||
|
||||
return (
|
||||
<nav role="navigation" aria-label="Pagination Navigation" ref={ref}>
|
||||
<ul className={styles.container}>
|
||||
<li
|
||||
className={clsx(styles.li, { [styles.disabled]: !previousEnabled })}
|
||||
>
|
||||
<button className={styles.button} onClick={setPreviousPage}>
|
||||
<FontAwesomeIcon icon="chevron-left" />
|
||||
</button>
|
||||
</li>
|
||||
<nav
|
||||
className={clsx("paginate-container", className)}
|
||||
role="navigation"
|
||||
aria-label="Pagination Navigation"
|
||||
>
|
||||
<button
|
||||
className="paginate-button"
|
||||
onClick={setPreviousPage}
|
||||
disabled={!previousEnabled}
|
||||
>
|
||||
<FontAwesomeIcon icon={["fas", "chevron-left"]} />
|
||||
</button>
|
||||
<ul className="paginate-pager">
|
||||
{pages.map((page, index) => (
|
||||
<li className={styles.li} key={index}>
|
||||
<button
|
||||
className={clsx(styles.button, {
|
||||
[styles.active]: page === currentPage,
|
||||
"pointer-events-none": typeof page !== "number",
|
||||
})}
|
||||
onClick={() => typeof page === "number" && setPage(page - 1)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
<li
|
||||
key={index}
|
||||
className={clsx(
|
||||
"paginate-button",
|
||||
typeof page !== "number" && "ellipsis",
|
||||
currentPage === page && "active"
|
||||
)}
|
||||
onClick={() =>
|
||||
typeof page === "number" &&
|
||||
currentPage !== page &&
|
||||
setPage(page - 1)
|
||||
}
|
||||
>
|
||||
{page}
|
||||
</li>
|
||||
))}
|
||||
<li className={clsx(styles.li, { [styles.disabled]: !nextEnabled })}>
|
||||
<button className={styles.button} onClick={setNextPage}>
|
||||
<FontAwesomeIcon icon="chevron-right" />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
className="paginate-button"
|
||||
onClick={setNextPage}
|
||||
disabled={!nextEnabled}
|
||||
>
|
||||
<FontAwesomeIcon icon={["fas", "chevron-right"]} />
|
||||
</button>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
30
website/src/components/Paginate/styles.css
Normal file
30
website/src/components/Paginate/styles.css
Normal file
@ -0,0 +1,30 @@
|
||||
.paginate {
|
||||
&-container {
|
||||
@apply flex items-center justify-center gap-2;
|
||||
}
|
||||
|
||||
&-button {
|
||||
@apply flex items-center justify-center cursor-pointer select-none;
|
||||
@apply w-8 h-8 text-sm leading-8 text-center bg-base-200 text-base-content;
|
||||
|
||||
&.ellipsis {
|
||||
@apply !text-base-content cursor-default;
|
||||
}
|
||||
|
||||
&.active {
|
||||
@apply bg-primary !text-primary-content cursor-default;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply text-primary;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@apply !text-base-content/80 cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&-pager {
|
||||
@apply flex items-center justify-center gap-2;
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
.container {
|
||||
@apply w-full max-w-full inline-flex justify-center items-center m-0 pl-0 list-none select-none;
|
||||
}
|
||||
|
||||
.li {
|
||||
@apply flex items-center;
|
||||
}
|
||||
|
||||
.button {
|
||||
height: 34px;
|
||||
width: auto;
|
||||
min-width: 34px;
|
||||
@apply m-1 px-1 border-2 rounded shadow-lg text-center;
|
||||
@apply border-light-nonepress-200 shadow-light-nonepress-300;
|
||||
@apply text-black bg-light-nonepress-100;
|
||||
}
|
||||
|
||||
:global(.dark) .button {
|
||||
@apply border-dark-nonepress-200 shadow-dark-nonepress-300;
|
||||
@apply text-white bg-dark-nonepress-100;
|
||||
}
|
||||
|
||||
.button.active {
|
||||
@apply bg-hero text-white border-hero;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
@apply opacity-60 pointer-events-none;
|
||||
}
|
@ -1,211 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { ChromePicker } from "react-color";
|
||||
import { usePagination } from "react-use-pagination";
|
||||
|
||||
import plugins from "../../static/plugins.json";
|
||||
import { Tag, useFilteredObjs } from "../libs/store";
|
||||
import Card from "./Card";
|
||||
import Modal from "./Modal";
|
||||
import ModalAction from "./ModalAction";
|
||||
import ModalContent from "./ModalContent";
|
||||
import ModalTitle from "./ModalTitle";
|
||||
import Paginate from "./Paginate";
|
||||
import TagComponent from "./Tag";
|
||||
|
||||
export default function Plugin(): JSX.Element {
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const {
|
||||
filter,
|
||||
setFilter,
|
||||
filteredObjs: filteredPlugins,
|
||||
} = useFilteredObjs(plugins);
|
||||
|
||||
const props = usePagination({
|
||||
totalItems: filteredPlugins.length,
|
||||
initialPageSize: 10,
|
||||
});
|
||||
const { startIndex, endIndex } = props;
|
||||
const currentPlugins = filteredPlugins.slice(startIndex, endIndex + 1);
|
||||
|
||||
const [form, setForm] = useState<{
|
||||
projectLink: string;
|
||||
moduleName: string;
|
||||
}>({ projectLink: "", moduleName: "" });
|
||||
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
const [label, setLabel] = useState<string>("");
|
||||
const [color, setColor] = useState<string>("#ea5252");
|
||||
|
||||
const urlEncode = (str: string) =>
|
||||
encodeURIComponent(str).replace(/%2B/gi, "+");
|
||||
|
||||
const onSubmit = () => {
|
||||
setModalOpen(false);
|
||||
const queries: { key: string; value: string }[] = [
|
||||
{ key: "template", value: "plugin_publish.yml" },
|
||||
{
|
||||
key: "title",
|
||||
value: form.projectLink && `Plugin: ${form.projectLink}`,
|
||||
},
|
||||
{ key: "labels", value: "Plugin" },
|
||||
{ key: "pypi", value: form.projectLink },
|
||||
{ key: "module", value: form.moduleName },
|
||||
{ key: "tags", value: JSON.stringify(tags) },
|
||||
];
|
||||
const urlQueries = queries
|
||||
.filter((query) => !!query.value)
|
||||
.map((query) => `${query.key}=${urlEncode(query.value)}`)
|
||||
.join("&");
|
||||
window.open(`https://github.com/nonebot/nonebot2/issues/new?${urlQueries}`);
|
||||
};
|
||||
const onChange = (event) => {
|
||||
const target = event.target;
|
||||
const value = target.type === "checkbox" ? target.checked : target.value;
|
||||
const name = target.name;
|
||||
|
||||
setForm({
|
||||
...form,
|
||||
[name]: value,
|
||||
});
|
||||
event.preventDefault();
|
||||
};
|
||||
const onChangeLabel = (event) => {
|
||||
setLabel(event.target.value);
|
||||
};
|
||||
const onChangeColor = (color) => {
|
||||
setColor(color.hex);
|
||||
};
|
||||
const validateTag = () => {
|
||||
return label.length >= 1 && label.length <= 10;
|
||||
};
|
||||
const newTag = () => {
|
||||
if (tags.length >= 3) {
|
||||
return;
|
||||
}
|
||||
if (validateTag()) {
|
||||
const tag = { label, color };
|
||||
setTags([...tags, tag]);
|
||||
}
|
||||
};
|
||||
const delTag = (index: number) => {
|
||||
setTags(tags.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4 px-4">
|
||||
<input
|
||||
className="w-full px-4 py-2 border rounded-full bg-light-nonepress-100 dark:bg-dark-nonepress-100"
|
||||
value={filter}
|
||||
placeholder="搜索插件"
|
||||
onChange={(event) => setFilter(event.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="w-full rounded-lg bg-hero text-white"
|
||||
onClick={() => setModalOpen(true)}
|
||||
>
|
||||
发布插件
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 p-4">
|
||||
<Paginate {...props} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-4">
|
||||
{currentPlugins.map((plugin, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
{...plugin}
|
||||
action={`nb plugin install ${plugin.project_link}`}
|
||||
actionDisabled={!plugin.project_link}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 p-4">
|
||||
<Paginate {...props} />
|
||||
</div>
|
||||
<Modal active={modalOpen} setActive={setModalOpen}>
|
||||
<ModalTitle title={"插件信息"} />
|
||||
<ModalContent>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="grid grid-cols-1 gap-4 p-4">
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">PyPI 项目名:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="projectLink"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">import 包名:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="moduleName"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<div className="px-4">
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">标签:</span>
|
||||
{tags.map((tag, index) => (
|
||||
<TagComponent
|
||||
key={index}
|
||||
{...tag}
|
||||
className="cursor-pointer"
|
||||
onClick={() => delTag(index)}
|
||||
/>
|
||||
))}
|
||||
</label>
|
||||
</div>
|
||||
<div className="px-4 pt-4">
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChangeLabel}
|
||||
/>
|
||||
<ChromePicker
|
||||
className="mt-2"
|
||||
color={color}
|
||||
disableAlpha={true}
|
||||
onChangeComplete={onChangeColor}
|
||||
/>
|
||||
|
||||
<div className="flex mt-2">
|
||||
<TagComponent label={label} color={color} />
|
||||
<button
|
||||
className={clsx(
|
||||
"px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]",
|
||||
{ "pointer-events-none opacity-60": !validateTag() }
|
||||
)}
|
||||
onClick={newTag}
|
||||
>
|
||||
添加标签
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalAction>
|
||||
<button
|
||||
className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={() => setModalOpen(false)}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
<button
|
||||
className="ml-2 px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
发布
|
||||
</button>
|
||||
</ModalAction>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
118
website/src/components/Resource/Card/index.tsx
Normal file
118
website/src/components/Resource/Card/index.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import React from "react";
|
||||
|
||||
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 type { Resource } from "@/libs/store";
|
||||
|
||||
export type Props = {
|
||||
resource: Resource;
|
||||
onClick?: () => void;
|
||||
onTagClick: (tag: string) => void;
|
||||
onAuthorClick: () => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function ResourceCard({
|
||||
resource,
|
||||
onClick,
|
||||
onTagClick,
|
||||
onAuthorClick,
|
||||
className,
|
||||
}: Props): JSX.Element {
|
||||
const isGithub = /^https:\/\/github.com\/[^/]+\/[^/]+/.test(
|
||||
resource.homepage
|
||||
);
|
||||
|
||||
const isPlugin = resource.resourceType === "plugin";
|
||||
const registryLink =
|
||||
isPlugin &&
|
||||
`https://registry.nonebot.dev/plugin/${resource.project_link}:${resource.module_name}`;
|
||||
|
||||
const authorLink = `https://github.com/${resource.author}`;
|
||||
const authorAvatar = `${authorLink}.png?size=80`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"resource-card-container",
|
||||
onClick && "resource-card-container-clickable",
|
||||
className
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="resource-card-header">
|
||||
<div className="resource-card-header-title">
|
||||
{resource.name}
|
||||
{resource.is_official && (
|
||||
<FontAwesomeIcon
|
||||
className="resource-card-header-check"
|
||||
icon={["fas", "circle-check"]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="resource-card-header-expand">
|
||||
<FontAwesomeIcon icon={["fas", "expand"]} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="resource-card-desc">{resource.desc}</div>
|
||||
<div className="resource-card-footer">
|
||||
<div className="resource-card-footer-tags">
|
||||
{resource.tags.map((tag, index) => (
|
||||
<Tag
|
||||
className="resource-card-footer-tag"
|
||||
key={index}
|
||||
{...tag}
|
||||
onClick={() => onTagClick(tag.label)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="divider resource-card-footer-divider"></div>
|
||||
<div className="resource-card-footer-info">
|
||||
<div className="resource-card-footer-group">
|
||||
<Link href={resource.homepage}>
|
||||
{isGithub ? (
|
||||
<FontAwesomeIcon
|
||||
className="resource-card-footer-icon"
|
||||
icon={["fab", "github"]}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
className="resource-card-footer-icon"
|
||||
icon={["fas", "link"]}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
{isPlugin && (
|
||||
<Link href={registryLink as string}>
|
||||
<FontAwesomeIcon
|
||||
className="resource-card-footer-icon"
|
||||
icon={["fas", "cube"]}
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="resource-card-footer-group">
|
||||
<div className="avatar">
|
||||
<div className="resource-card-footer-avatar">
|
||||
<Link href={authorLink}>
|
||||
<img src={authorAvatar} key={resource.author} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className="resource-card-footer-author"
|
||||
onClick={onAuthorClick}
|
||||
>
|
||||
{resource.author}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
75
website/src/components/Resource/Card/styles.css
Normal file
75
website/src/components/Resource/Card/styles.css
Normal file
@ -0,0 +1,75 @@
|
||||
.resource-card {
|
||||
&-container {
|
||||
@apply flex flex-col gap-y-2 w-full min-h-[12rem] p-4;
|
||||
@apply transition-colors duration-500 bg-base-200;
|
||||
@apply border-2 border-base-200 rounded-lg;
|
||||
|
||||
&-clickable {
|
||||
@apply cursor-pointer hover:border-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&-header {
|
||||
@apply flex items-center w-full text-lg font-medium;
|
||||
|
||||
&-title {
|
||||
@apply grow text-left truncate;
|
||||
}
|
||||
|
||||
&-check {
|
||||
@apply ml-2 text-success w-5 h-5 fill-current;
|
||||
}
|
||||
|
||||
&-expand {
|
||||
@apply flex-none fill-current;
|
||||
}
|
||||
}
|
||||
|
||||
&-desc {
|
||||
@apply flex-1 w-full text-sm text-ellipsis break-words;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
@apply flex flex-col w-full cursor-default;
|
||||
|
||||
&-tags {
|
||||
@apply flex flex-wrap gap-1;
|
||||
}
|
||||
|
||||
&-tag {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
&-divider {
|
||||
@apply m-0;
|
||||
}
|
||||
|
||||
&-info {
|
||||
@apply flex items-center justify-between w-full;
|
||||
}
|
||||
|
||||
&-group {
|
||||
@apply flex items-center justify-center gap-x-1 leading-none;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
@apply w-5 h-5 fill-current opacity-80;
|
||||
|
||||
&:hover {
|
||||
@apply opacity-100;
|
||||
}
|
||||
}
|
||||
|
||||
&-avatar {
|
||||
@apply w-5 h-5 rounded-full transition-shadow;
|
||||
|
||||
&:hover {
|
||||
@apply ring-1 ring-primary ring-offset-base-100 ring-offset-1;
|
||||
}
|
||||
}
|
||||
|
||||
&-author {
|
||||
@apply text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
}
|
32
website/src/components/Resource/Tag/index.tsx
Normal file
32
website/src/components/Resource/Tag/index.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
|
||||
import clsx from "clsx";
|
||||
|
||||
import "./styles.css";
|
||||
import { pickTextColor } from "@/libs/color";
|
||||
import type { Tag } from "@/types/tag";
|
||||
|
||||
export type Props = Tag & {
|
||||
className?: string;
|
||||
onClick?: React.MouseEventHandler<HTMLSpanElement>;
|
||||
};
|
||||
|
||||
export default function ResourceTag({
|
||||
label,
|
||||
color,
|
||||
className,
|
||||
onClick,
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<span
|
||||
className={clsx("resource-tag", className)}
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
color: pickTextColor(color, "#fff", "#000"),
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
4
website/src/components/Resource/Tag/styles.css
Normal file
4
website/src/components/Resource/Tag/styles.css
Normal file
@ -0,0 +1,4 @@
|
||||
.resource-tag {
|
||||
@apply inline-flex items-center justify-center;
|
||||
@apply text-xs font-mono w-fit h-5 px-2 rounded;
|
||||
}
|
118
website/src/components/Searcher/index.tsx
Normal file
118
website/src/components/Searcher/index.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import React, { useRef } from "react";
|
||||
|
||||
import clsx from "clsx";
|
||||
|
||||
import "./styles.css";
|
||||
import { translate } from "@docusaurus/Translate";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export type Props = {
|
||||
onChange: (value: string) => void;
|
||||
onSubmit: (value: string) => void;
|
||||
onBackspace: () => void;
|
||||
onClear: () => void;
|
||||
onTagClick: (index: number) => void;
|
||||
tags?: string[];
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export default function Searcher({
|
||||
onChange,
|
||||
onSubmit,
|
||||
onBackspace,
|
||||
onClear,
|
||||
onTagClick,
|
||||
tags = [],
|
||||
className,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
}: Props): JSX.Element {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
onSubmit(e.currentTarget.value);
|
||||
e.currentTarget.value = "";
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleEscape = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
e.currentTarget.value = "";
|
||||
};
|
||||
|
||||
const handleBackspace = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.currentTarget.value === "") {
|
||||
onBackspace();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
switch (e.key) {
|
||||
case "Enter": {
|
||||
handleSubmit(e);
|
||||
break;
|
||||
}
|
||||
case "Escape": {
|
||||
handleEscape(e);
|
||||
break;
|
||||
}
|
||||
case "Backspace": {
|
||||
handleBackspace(e);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
if (ref.current) {
|
||||
ref.current.value = "";
|
||||
ref.current.focus();
|
||||
}
|
||||
onClear();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx("searcher-box", className)}>
|
||||
<div className="searcher-container">
|
||||
{tags.map((tag, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="badge badge-primary searcher-tag"
|
||||
onClick={() => onTagClick(index)}
|
||||
>
|
||||
{tag}
|
||||
</div>
|
||||
))}
|
||||
<input
|
||||
ref={ref}
|
||||
className="searcher-input"
|
||||
placeholder={
|
||||
placeholder ??
|
||||
translate({
|
||||
id: "theme.searcher.input.placeholder",
|
||||
description: "Search input placeholder",
|
||||
message: "搜索",
|
||||
})
|
||||
}
|
||||
onChange={(e) => onChange(e.currentTarget.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<div className="searcher-action" onClick={handleClear}>
|
||||
<FontAwesomeIcon
|
||||
className="searcher-action-icon search"
|
||||
icon={["fas", "magnifying-glass"]}
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
className="searcher-action-icon close"
|
||||
icon={["fas", "xmark"]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
37
website/src/components/Searcher/styles.css
Normal file
37
website/src/components/Searcher/styles.css
Normal file
@ -0,0 +1,37 @@
|
||||
.searcher {
|
||||
&-box {
|
||||
@apply flex items-center w-full rounded-3xl bg-base-200;
|
||||
@apply transition-[color,background-color] duration-500;
|
||||
}
|
||||
|
||||
&-container {
|
||||
@apply flex-1 flex items-center flex-wrap gap-x-1 gap-y-2;
|
||||
@apply pl-5 py-3;
|
||||
}
|
||||
|
||||
&-tag {
|
||||
@apply flex-initial shrink-0;
|
||||
@apply font-medium cursor-pointer select-none;
|
||||
}
|
||||
|
||||
&-input {
|
||||
@apply flex-1 text-sm min-w-[10rem];
|
||||
@apply bg-transparent border-none outline-none;
|
||||
}
|
||||
|
||||
&-action {
|
||||
@apply flex-initial shrink-0 flex items-center justify-center cursor-pointer w-12 h-10;
|
||||
|
||||
&-icon {
|
||||
@apply h-4 opacity-50;
|
||||
}
|
||||
|
||||
&:hover &-icon.search {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
&:not(:hover) &-icon.close {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
}
|
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;
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import { Tag as TagType } from "../../libs/store";
|
||||
|
||||
function pickTextColor(bgColor, lightColor, darkColor) {
|
||||
var color = bgColor.charAt(0) === "#" ? bgColor.substring(1, 7) : bgColor;
|
||||
var r = parseInt(color.substring(0, 2), 16); // hexToR
|
||||
var g = parseInt(color.substring(2, 4), 16); // hexToG
|
||||
var b = parseInt(color.substring(4, 6), 16); // hexToB
|
||||
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? darkColor : lightColor;
|
||||
}
|
||||
|
||||
export default function Tag({
|
||||
label,
|
||||
color,
|
||||
className,
|
||||
onClick,
|
||||
}: TagType & {
|
||||
className?: string;
|
||||
onClick?: React.MouseEventHandler<HTMLSpanElement>;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
"font-mono inline-flex px-3 rounded-full items-center align-middle mr-2",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
color: pickTextColor(color, "#fff", "#000"),
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
.homeCodeBlock {
|
||||
width: 602px;
|
||||
}
|
16
website/src/libs/color.ts
Normal file
16
website/src/libs/color.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Choose fg color by bg color
|
||||
* @see https://www.npmjs.com/package/colord
|
||||
* @see https://www.w3.org/TR/AERT/#color-contrast
|
||||
*/
|
||||
export function pickTextColor(
|
||||
bgColor: string,
|
||||
lightColor: string,
|
||||
darkColor: string
|
||||
) {
|
||||
const color = bgColor.charAt(0) === "#" ? bgColor.substring(1, 7) : bgColor;
|
||||
const r = parseInt(color.substring(0, 2), 16); // hexToR
|
||||
const g = parseInt(color.substring(2, 4), 16); // hexToG
|
||||
const b = parseInt(color.substring(4, 6), 16); // hexToB
|
||||
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? darkColor : lightColor;
|
||||
}
|
130
website/src/libs/filter.ts
Normal file
130
website/src/libs/filter.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { translate } from "@docusaurus/Translate";
|
||||
|
||||
import type { Resource } from "./store";
|
||||
|
||||
export type Filter<T extends Resource = Resource> = {
|
||||
type: string;
|
||||
id: string;
|
||||
displayName: string;
|
||||
filter: (resource: T) => boolean;
|
||||
};
|
||||
|
||||
export const tagFilter = <T extends Resource = Resource>(
|
||||
tag: string
|
||||
): Filter<T> => ({
|
||||
type: "tag",
|
||||
id: `tag-${tag}`,
|
||||
displayName: translate(
|
||||
{
|
||||
id: "pages.store.filter.tagDisplayName",
|
||||
description: "The display name of tag filter",
|
||||
message: "标签: {tag}",
|
||||
},
|
||||
{ tag }
|
||||
),
|
||||
filter: (resource: Resource): boolean =>
|
||||
resource.tags.map((tag) => tag.label).includes(tag),
|
||||
});
|
||||
export const officialFilter = <T extends Resource = Resource>(
|
||||
official: boolean = true
|
||||
): Filter<T> => ({
|
||||
type: "official",
|
||||
id: `official-${official}`,
|
||||
displayName: translate({
|
||||
id: "pages.store.filter.officialDisplayName",
|
||||
description: "The display name of official filter",
|
||||
message: "非官方|官方",
|
||||
}).split("|")[Number(official)],
|
||||
filter: (resource: Resource): boolean => resource.is_official === official,
|
||||
});
|
||||
export const authorFilter = <T extends Resource = Resource>(
|
||||
author: string
|
||||
): Filter<T> => ({
|
||||
type: "author",
|
||||
id: `author-${author}`,
|
||||
displayName: translate(
|
||||
{
|
||||
id: "pages.store.filter.authorDisplayName",
|
||||
description: "The display name of author filter",
|
||||
message: "作者: {author}",
|
||||
},
|
||||
{ author }
|
||||
),
|
||||
filter: (resource: Resource): boolean => resource.author === author,
|
||||
});
|
||||
export const queryFilter = <T extends Resource = Resource>(
|
||||
query: string
|
||||
): Filter<T> => ({
|
||||
type: "query",
|
||||
id: `query-${query}`,
|
||||
displayName: query,
|
||||
filter: (resource: Resource): boolean => {
|
||||
if (!query) return true;
|
||||
const queryLower = query.toLowerCase();
|
||||
const pluginMatch =
|
||||
resource.resourceType === "plugin" &&
|
||||
(resource.module_name?.toLowerCase().includes(queryLower) ||
|
||||
resource.project_link?.toLowerCase().includes(queryLower));
|
||||
const commonMatch =
|
||||
resource.name.toLowerCase().includes(queryLower) ||
|
||||
resource.desc.toLowerCase().includes(queryLower) ||
|
||||
resource.author.toLowerCase().includes(queryLower) ||
|
||||
resource.tags.filter((t) => t.label.toLowerCase().includes(queryLower))
|
||||
.length > 0;
|
||||
return pluginMatch || commonMatch;
|
||||
},
|
||||
});
|
||||
|
||||
export function filterResources<T extends Resource>(
|
||||
resources: T[],
|
||||
filters: Filter<T>[]
|
||||
): T[] {
|
||||
return resources.filter((resource) =>
|
||||
filters.every((filter) => filter.filter(resource))
|
||||
);
|
||||
}
|
||||
|
||||
type useFilteredResourcesReturn<T extends Resource> = {
|
||||
filters: Filter<T>[];
|
||||
addFilter: (filter: Filter<T>) => void;
|
||||
removeFilter: (filter: Filter<T> | string) => void;
|
||||
filteredResources: T[];
|
||||
};
|
||||
|
||||
export function useFilteredResources<T extends Resource>(
|
||||
resources: T[]
|
||||
): useFilteredResourcesReturn<T> {
|
||||
const [filters, setFilters] = useState<Filter<T>[]>([]);
|
||||
|
||||
const addFilter = useCallback(
|
||||
(filter: Filter<T>) => {
|
||||
if (filters.some((f) => f.id === filter.id)) return;
|
||||
setFilters((filters) => [...filters, filter]);
|
||||
},
|
||||
[filters, setFilters]
|
||||
);
|
||||
const removeFilter = useCallback(
|
||||
(filter: Filter<T> | string) => {
|
||||
setFilters((filters) =>
|
||||
filters.filter((f) =>
|
||||
typeof filter === "string" ? f.id !== filter : f !== filter
|
||||
)
|
||||
);
|
||||
},
|
||||
[setFilters]
|
||||
);
|
||||
|
||||
const filteredResources = useCallback(
|
||||
() => filterResources(resources, filters),
|
||||
[resources, filters]
|
||||
);
|
||||
|
||||
return {
|
||||
filters,
|
||||
addFilter,
|
||||
removeFilter,
|
||||
filteredResources: filteredResources(),
|
||||
};
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
import { useLayoutEffect, useRef } from "react";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
|
||||
export function useResizeNotifier(
|
||||
element: HTMLElement | undefined,
|
||||
callback: () => void
|
||||
) {
|
||||
const callBackRef = useRef(callback);
|
||||
useLayoutEffect(() => {
|
||||
callBackRef.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!element) return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(
|
||||
withResizeLoopDetection(() => {
|
||||
callBackRef.current!();
|
||||
})
|
||||
);
|
||||
|
||||
resizeObserver.observe(element);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [element]);
|
||||
}
|
||||
|
||||
function withResizeLoopDetection(callback: () => void) {
|
||||
return (entries: ResizeObserverEntry[], resizeObserver: ResizeObserver) => {
|
||||
const elements = entries.map((entry) => entry.target);
|
||||
|
||||
const rectsBefore = elements.map((element) =>
|
||||
element.getBoundingClientRect()
|
||||
);
|
||||
|
||||
callback();
|
||||
|
||||
const rectsAfter = elements.map((element) =>
|
||||
element.getBoundingClientRect()
|
||||
);
|
||||
|
||||
const changedElements = elements.filter(
|
||||
(_, i) => !areRectSizesEqual(rectsBefore[i], rectsAfter[i])
|
||||
);
|
||||
|
||||
changedElements.forEach((element) =>
|
||||
unobserveUntilNextFrame(element, resizeObserver)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function unobserveUntilNextFrame(
|
||||
element: Element,
|
||||
resizeObserver: ResizeObserver
|
||||
) {
|
||||
resizeObserver.unobserve(element);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
resizeObserver.observe(element);
|
||||
});
|
||||
}
|
||||
|
||||
function areRectSizesEqual(rect1: DOMRect, rect2: DOMRect) {
|
||||
return rect1.width === rect2.width && rect1.height === rect2.height;
|
||||
}
|
88
website/src/libs/search.ts
Normal file
88
website/src/libs/search.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { type Filter, useFilteredResources, queryFilter } from "./filter";
|
||||
import type { Resource } from "./store";
|
||||
|
||||
type useSearchControlReturn<T extends Resource> = {
|
||||
filteredResources: T[];
|
||||
searcherTags: string[];
|
||||
addFilter: (filter: Filter<T>) => void;
|
||||
onSearchQueryChange: (query: string) => void;
|
||||
onSearchQuerySubmit: () => void;
|
||||
onSearchBackspace: () => void;
|
||||
onSearchClear: () => void;
|
||||
onSearchTagClick: (index: number) => void;
|
||||
};
|
||||
|
||||
export function useSearchControl<T extends Resource>(
|
||||
resources: T[]
|
||||
): useSearchControlReturn<T> {
|
||||
const [currentFilter, setCurrentFilter] = useState<Filter<T> | null>(null);
|
||||
|
||||
const { filters, addFilter, removeFilter, filteredResources } =
|
||||
useFilteredResources(resources);
|
||||
|
||||
// display tags in searcher (except current filter)
|
||||
const [searcherFilters, setSearcherFilters] = useState<Filter<T>[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setSearcherFilters(
|
||||
filters.filter((f) => !(currentFilter && f === currentFilter))
|
||||
);
|
||||
}, [filters, currentFilter]);
|
||||
|
||||
const onSearchQueryChange = useCallback(
|
||||
(newQuery: string) => {
|
||||
// remove current filter if query is empty
|
||||
if (newQuery === "") {
|
||||
currentFilter && removeFilter(currentFilter);
|
||||
setCurrentFilter(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const newFilter = queryFilter<T>(newQuery);
|
||||
// do nothing if filter is not changed
|
||||
if (currentFilter?.id === newFilter.id) return;
|
||||
|
||||
// remove old currentFilter
|
||||
currentFilter && removeFilter(currentFilter);
|
||||
// add new filter
|
||||
setCurrentFilter(newFilter);
|
||||
addFilter(newFilter);
|
||||
},
|
||||
[currentFilter, setCurrentFilter, addFilter, removeFilter]
|
||||
);
|
||||
|
||||
const onSearchQuerySubmit = useCallback(() => {
|
||||
// set current filter to null to make filter permanent
|
||||
setCurrentFilter(null);
|
||||
}, [setCurrentFilter]);
|
||||
|
||||
const onSearchBackspace = useCallback(() => {
|
||||
// remove last filter
|
||||
removeFilter(searcherFilters[searcherFilters.length - 1]);
|
||||
}, [removeFilter, searcherFilters]);
|
||||
|
||||
const onSearchClear = useCallback(() => {
|
||||
// remove all filters
|
||||
searcherFilters.forEach((filter) => removeFilter(filter));
|
||||
}, [removeFilter, searcherFilters]);
|
||||
|
||||
const onSearchTagClick = useCallback(
|
||||
(index: number) => {
|
||||
removeFilter(searcherFilters[index]);
|
||||
},
|
||||
[removeFilter, searcherFilters]
|
||||
);
|
||||
|
||||
return {
|
||||
filteredResources,
|
||||
searcherTags: searcherFilters.map((filter) => filter.displayName),
|
||||
addFilter,
|
||||
onSearchQueryChange,
|
||||
onSearchQuerySubmit,
|
||||
onSearchBackspace,
|
||||
onSearchClear,
|
||||
onSearchTagClick,
|
||||
};
|
||||
}
|
@ -1,48 +1,47 @@
|
||||
import { useState } from "react";
|
||||
import { translate } from "@docusaurus/Translate";
|
||||
|
||||
export type Tag = {
|
||||
label: string;
|
||||
color: string;
|
||||
import type { Adapter, AdaptersResponse } from "@/types/adapter";
|
||||
import type { Bot, BotsResponse } from "@/types/bot";
|
||||
import type { Driver, DriversResponse } from "@/types/driver";
|
||||
import type { Plugin, PluginsResponse } from "@/types/plugin";
|
||||
|
||||
type RegistryDataResponseTypes = {
|
||||
adapter: AdaptersResponse;
|
||||
bot: BotsResponse;
|
||||
driver: DriversResponse;
|
||||
plugin: PluginsResponse;
|
||||
};
|
||||
type RegistryDataType = keyof RegistryDataResponseTypes;
|
||||
|
||||
type ResourceTypes = {
|
||||
adapter: Adapter;
|
||||
bot: Bot;
|
||||
driver: Driver;
|
||||
plugin: Plugin;
|
||||
};
|
||||
|
||||
export type Obj = {
|
||||
module_name?: string;
|
||||
project_link?: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
author: string;
|
||||
homepage: string;
|
||||
tags: Tag[];
|
||||
is_official: boolean;
|
||||
};
|
||||
export type Resource = Adapter | Bot | Driver | Plugin;
|
||||
|
||||
export function filterObjs(filter: string, objs: Obj[]): Obj[] {
|
||||
let filterLower = filter.toLowerCase();
|
||||
return objs.filter((o) => {
|
||||
return (
|
||||
o.module_name?.toLowerCase().includes(filterLower) ||
|
||||
o.project_link?.toLowerCase().includes(filterLower) ||
|
||||
o.name.toLowerCase().includes(filterLower) ||
|
||||
o.desc.toLowerCase().includes(filterLower) ||
|
||||
o.author.toLowerCase().includes(filterLower) ||
|
||||
o.tags.filter((t) => t.label.toLowerCase().includes(filterLower)).length >
|
||||
0
|
||||
);
|
||||
export async function fetchRegistryData<T extends RegistryDataType>(
|
||||
dataType: T
|
||||
): Promise<ResourceTypes[T][]> {
|
||||
const resp = await fetch(
|
||||
`https://registry.nonebot.dev/${dataType}s.json`
|
||||
).catch((e) => {
|
||||
throw new Error(`Failed to fetch ${dataType}s: ${e}`);
|
||||
});
|
||||
if (!resp.ok)
|
||||
throw new Error(
|
||||
`Failed to fetch ${dataType}s: ${resp.status} ${resp.statusText}`
|
||||
);
|
||||
const data = (await resp.json()) as RegistryDataResponseTypes[T];
|
||||
return data.map(
|
||||
(resource) => ({ ...resource, resourceType: dataType }) as ResourceTypes[T]
|
||||
);
|
||||
}
|
||||
|
||||
type useFilteredObjsReturn = {
|
||||
filter: string;
|
||||
setFilter: (filter: string) => void;
|
||||
filteredObjs: Obj[];
|
||||
};
|
||||
|
||||
export function useFilteredObjs(objs: Obj[]): useFilteredObjsReturn {
|
||||
const [filter, setFilter] = useState<string>("");
|
||||
const filteredObjs = filterObjs(filter, objs);
|
||||
return {
|
||||
filter,
|
||||
setFilter,
|
||||
filteredObjs,
|
||||
};
|
||||
}
|
||||
export const loadFailedTitle = translate({
|
||||
id: "pages.store.loadFailed.title",
|
||||
message: "加载失败",
|
||||
description: "Title to display when loading content failed",
|
||||
});
|
||||
|
44
website/src/libs/toolbar.ts
Normal file
44
website/src/libs/toolbar.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { authorFilter, tagFilter, type Filter } from "./filter";
|
||||
import type { Resource } from "./store";
|
||||
|
||||
import type { Filter as FilterTool } from "@/components/Store/Toolbar";
|
||||
|
||||
type Props<T extends Resource = Resource> = {
|
||||
resources: T[];
|
||||
addFilter: (filter: Filter<T>) => void;
|
||||
};
|
||||
|
||||
type useToolbarReturns = {
|
||||
filters: FilterTool[];
|
||||
};
|
||||
|
||||
export function useToolbar<T extends Resource = Resource>({
|
||||
resources,
|
||||
addFilter,
|
||||
}: Props<T>): useToolbarReturns {
|
||||
const authorFilterTool: FilterTool = {
|
||||
label: "作者",
|
||||
icon: ["fas", "user"],
|
||||
choices: Array.from(new Set(resources.map((resource) => resource.author))),
|
||||
onSubmit: (author: string) => {
|
||||
addFilter(authorFilter(author));
|
||||
},
|
||||
};
|
||||
|
||||
const tagFilterTool: FilterTool = {
|
||||
label: "标签",
|
||||
icon: ["fas", "tag"],
|
||||
choices: Array.from(
|
||||
new Set(
|
||||
resources.flatMap((resource) => resource.tags.map((tag) => tag.label))
|
||||
)
|
||||
),
|
||||
onSubmit: (tag: string) => {
|
||||
addFilter(tagFilter(tag));
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
filters: [authorFilterTool, tagFilterTool],
|
||||
};
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
|
||||
import { useResizeNotifier } from "./resize";
|
||||
|
||||
export function getElementWidth(element: HTMLElement) {
|
||||
const style = getComputedStyle(element);
|
||||
|
||||
return (
|
||||
styleMetricToInt(style.marginLeft) +
|
||||
getWidth(element) +
|
||||
styleMetricToInt(style.marginRight)
|
||||
);
|
||||
}
|
||||
|
||||
export function getContentWidth(element: HTMLElement) {
|
||||
const style = getComputedStyle(element);
|
||||
|
||||
return (
|
||||
element.getBoundingClientRect().width -
|
||||
styleMetricToInt(style.borderLeftWidth) -
|
||||
styleMetricToInt(style.paddingLeft) -
|
||||
styleMetricToInt(style.paddingRight) -
|
||||
styleMetricToInt(style.borderRightWidth)
|
||||
);
|
||||
}
|
||||
|
||||
export function getNonContentWidth(element: HTMLElement) {
|
||||
const style = getComputedStyle(element);
|
||||
|
||||
return (
|
||||
styleMetricToInt(style.marginLeft) +
|
||||
styleMetricToInt(style.borderLeftWidth) +
|
||||
styleMetricToInt(style.paddingLeft) +
|
||||
styleMetricToInt(style.paddingRight) +
|
||||
styleMetricToInt(style.borderRightWidth) +
|
||||
styleMetricToInt(style.marginRight)
|
||||
);
|
||||
}
|
||||
|
||||
export function getWidth(element: HTMLElement) {
|
||||
return element.getBoundingClientRect().width;
|
||||
}
|
||||
|
||||
function styleMetricToInt(styleAttribute: string | null) {
|
||||
return styleAttribute ? parseInt(styleAttribute) : 0;
|
||||
}
|
||||
|
||||
export function useContentWidth(element: HTMLElement | undefined) {
|
||||
const [width, setWidth] = useState<number>();
|
||||
|
||||
function syncWidth() {
|
||||
const newWidth = element ? getContentWidth(element) : undefined;
|
||||
|
||||
if (width !== newWidth) {
|
||||
setWidth(newWidth);
|
||||
}
|
||||
}
|
||||
|
||||
useResizeNotifier(element, syncWidth);
|
||||
|
||||
useLayoutEffect(syncWidth);
|
||||
|
||||
return width;
|
||||
}
|
@ -1,165 +1,13 @@
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import CodeBlock from "@theme/CodeBlock";
|
||||
import Layout from "@theme/Layout";
|
||||
|
||||
import { Hero, HeroFeature } from "../components/Hero";
|
||||
import type { Feature } from "../components/Hero";
|
||||
import styles from "../css/index.module.css";
|
||||
|
||||
export default function Home() {
|
||||
const firstFeature: Feature = {
|
||||
title: "开箱即用",
|
||||
tagline: "out of box",
|
||||
description: "使用 NB-CLI 快速构建属于你的机器人",
|
||||
} as const;
|
||||
const secondFeatures = [
|
||||
{
|
||||
title: "插件系统",
|
||||
tagline: "plugin system",
|
||||
description: "插件化开发,模块化管理",
|
||||
},
|
||||
{
|
||||
title: "跨平台支持",
|
||||
tagline: "cross-platform support",
|
||||
description: "支持多种平台,以及多样的事件响应方式",
|
||||
},
|
||||
] as const;
|
||||
const thirdFeatures = [
|
||||
{
|
||||
title: "异步开发",
|
||||
tagline: "asynchronous first",
|
||||
description: "异步优先式开发,提高运行效率",
|
||||
},
|
||||
{
|
||||
title: "依赖注入",
|
||||
tagline: "builtin dependency injection system",
|
||||
description: "简单清晰的依赖注入系统,内置依赖函数减少用户代码",
|
||||
},
|
||||
];
|
||||
import HomeContent from "@/components/Home";
|
||||
|
||||
export default function Homepage(): JSX.Element {
|
||||
return (
|
||||
<Layout>
|
||||
<Hero />
|
||||
<div className="max-w-7xl mx-auto py-16 px-4 text-center md:px-16">
|
||||
<HeroFeature {...firstFeature}>
|
||||
<CodeBlock
|
||||
title="Installation"
|
||||
className={clsx("inline-block language-bash", styles.homeCodeBlock)}
|
||||
>
|
||||
{[
|
||||
"$ pip install nb-cli",
|
||||
"$ nb",
|
||||
// "d8b db .d88b. d8b db d88888b d8888b. .d88b. d888888b",
|
||||
// "888o 88 .8P Y8. 888o 88 88' 88 `8D .8P Y8. `~~88~~'",
|
||||
// "88V8o 88 88 88 88V8o 88 88ooooo 88oooY' 88 88 88",
|
||||
// "88 V8o88 88 88 88 V8o88 88~~~~~ 88~~~b. 88 88 88",
|
||||
// "88 V888 `8b d8' 88 V888 88. 88 8D `8b d8' 88",
|
||||
// "VP V8P `Y88P' VP V8P Y88888P Y8888P' `Y88P' YP",
|
||||
"[?] What do you want to do?",
|
||||
"❯ Create a New Project",
|
||||
" Run the Bot in Current Folder",
|
||||
" Driver ->",
|
||||
" Adapter ->",
|
||||
" Plugin ->",
|
||||
" ...",
|
||||
].join("\n")}
|
||||
</CodeBlock>
|
||||
</HeroFeature>
|
||||
</div>
|
||||
<div className="max-w-7xl mx-auto py-16 px-4 md:grid md:grid-cols-2 md:gap-6 md:px-16">
|
||||
<div className="pb-16 text-center md:pb-0">
|
||||
<HeroFeature {...secondFeatures[0]}>
|
||||
<CodeBlock
|
||||
title
|
||||
className={clsx(
|
||||
"inline-block language-python",
|
||||
styles.homeCodeBlock
|
||||
)}
|
||||
>
|
||||
{[
|
||||
"import nonebot",
|
||||
"# 加载一个插件",
|
||||
'nonebot.load_plugin("path.to.your.plugin")',
|
||||
"# 从文件夹加载插件",
|
||||
'nonebot.load_plugins("plugins")',
|
||||
"# 从配置文件加载多个插件",
|
||||
'nonebot.load_from_json("plugins.json")',
|
||||
'nonebot.load_from_toml("pyproject.toml")',
|
||||
].join("\n")}
|
||||
</CodeBlock>
|
||||
</HeroFeature>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<HeroFeature {...secondFeatures[1]}>
|
||||
<CodeBlock
|
||||
title
|
||||
className={clsx(
|
||||
"inline-block language-python",
|
||||
styles.homeCodeBlock
|
||||
)}
|
||||
>
|
||||
{[
|
||||
"import nonebot",
|
||||
"# OneBot",
|
||||
"from nonebot.adapters.onebot.v11 import Adapter as OneBotAdapter",
|
||||
"# 钉钉",
|
||||
"from nonebot.adapters.ding import Adapter as DingAdapter",
|
||||
"driver = nonebot.get_driver()",
|
||||
"driver.register_adapter(OneBotAdapter)",
|
||||
"driver.register_adapter(DingAdapter)",
|
||||
].join("\n")}
|
||||
</CodeBlock>
|
||||
</HeroFeature>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-7xl mx-auto py-16 px-4 md:grid md:grid-cols-2 md:gap-6 md:px-16">
|
||||
<div className="pb-16 text-center md:pb-0">
|
||||
<HeroFeature {...thirdFeatures[0]}>
|
||||
<CodeBlock
|
||||
title
|
||||
className={clsx(
|
||||
"inline-block language-python",
|
||||
styles.homeCodeBlock
|
||||
)}
|
||||
>
|
||||
{[
|
||||
"from nonebot import on_message",
|
||||
"# 注册一个消息响应器",
|
||||
"matcher = on_message()",
|
||||
"# 注册一个消息处理器",
|
||||
"# 并重复收到的消息",
|
||||
"@matcher.handle()",
|
||||
"async def handler(event: Event) -> None:",
|
||||
" await matcher.send(event.get_message())",
|
||||
].join("\n")}
|
||||
</CodeBlock>
|
||||
</HeroFeature>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<HeroFeature {...thirdFeatures[1]}>
|
||||
<CodeBlock
|
||||
title
|
||||
className={clsx(
|
||||
"inline-block language-python",
|
||||
styles.homeCodeBlock
|
||||
)}
|
||||
>
|
||||
{[
|
||||
"from nonebot import on_command",
|
||||
"# 注册一个命令响应器",
|
||||
'matcher = on_command("help", alias={"帮助"})',
|
||||
"# 注册一个命令处理器",
|
||||
"# 通过依赖注入获得命令名以及参数",
|
||||
"@matcher.handle()",
|
||||
"async def handler(cmd = Command(), arg = CommandArg()) -> None:",
|
||||
" await matcher.finish()",
|
||||
].join("\n")}
|
||||
</CodeBlock>
|
||||
</HeroFeature>
|
||||
</div>
|
||||
</div>
|
||||
<HomeContent />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
@ -1,35 +0,0 @@
|
||||
---
|
||||
description: NoneBot Store
|
||||
hide_table_of_contents: true
|
||||
---
|
||||
|
||||
import Tabs from "@theme/Tabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
|
||||
import Driver from "../components/Driver";
|
||||
import Adapter from "../components/Adapter";
|
||||
import Plugin from "../components/Plugin";
|
||||
import Bot from "../components/Bot";
|
||||
|
||||
# 商店
|
||||
|
||||
:::warning 警告
|
||||
商店未带有绿色官方标记的任何适配器、插件均由社区贡献,官方无法对其质量、安全性、可用性负责。
|
||||
:::
|
||||
|
||||
<div className="w-full border rounded shadow">
|
||||
<Tabs defaultValue="plugin" className="justify-center font-light">
|
||||
<TabItem value="driver" label="驱动器">
|
||||
<Driver />
|
||||
</TabItem>
|
||||
<TabItem value="adapter" label="适配器">
|
||||
<Adapter />
|
||||
</TabItem>
|
||||
<TabItem value="plugin" label="插件">
|
||||
<Plugin />
|
||||
</TabItem>
|
||||
<TabItem value="bot" label="机器人">
|
||||
<Bot />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</div>
|
20
website/src/pages/store/adapters.tsx
Normal file
20
website/src/pages/store/adapters.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
|
||||
import { translate } from "@docusaurus/Translate";
|
||||
|
||||
import AdapterPageContent from "@/components/Store/Content/Adapter";
|
||||
import StoreLayout from "@/components/Store/Layout";
|
||||
|
||||
export default function StoreAdapters(): JSX.Element {
|
||||
const title = translate({
|
||||
id: "pages.store.adapter.title",
|
||||
message: "适配器商店",
|
||||
description: "Title for the adapter store page",
|
||||
});
|
||||
|
||||
return (
|
||||
<StoreLayout title={title}>
|
||||
<AdapterPageContent />
|
||||
</StoreLayout>
|
||||
);
|
||||
}
|
20
website/src/pages/store/bots.tsx
Normal file
20
website/src/pages/store/bots.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
|
||||
import { translate } from "@docusaurus/Translate";
|
||||
|
||||
import BotPageContent from "@/components/Store/Content/Bot";
|
||||
import StoreLayout from "@/components/Store/Layout";
|
||||
|
||||
export default function StoreBots(): JSX.Element {
|
||||
const title = translate({
|
||||
id: "pages.store.bot.title",
|
||||
message: "机器人商店",
|
||||
description: "Title for the bot store page",
|
||||
});
|
||||
|
||||
return (
|
||||
<StoreLayout title={title}>
|
||||
<BotPageContent />
|
||||
</StoreLayout>
|
||||
);
|
||||
}
|
20
website/src/pages/store/drivers.tsx
Normal file
20
website/src/pages/store/drivers.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
|
||||
import { translate } from "@docusaurus/Translate";
|
||||
|
||||
import DriverPageContent from "@/components/Store/Content/Driver";
|
||||
import StoreLayout from "@/components/Store/Layout";
|
||||
|
||||
export default function StoreDrivers(): JSX.Element {
|
||||
const title = translate({
|
||||
id: "pages.store.driver.title",
|
||||
message: "驱动器商店",
|
||||
description: "Title for the driver store page",
|
||||
});
|
||||
|
||||
return (
|
||||
<StoreLayout title={title}>
|
||||
<DriverPageContent />
|
||||
</StoreLayout>
|
||||
);
|
||||
}
|
7
website/src/pages/store/index.tsx
Normal file
7
website/src/pages/store/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import { Redirect } from "@docusaurus/router";
|
||||
|
||||
export default function Store(): JSX.Element {
|
||||
return <Redirect to="/store/plugins" />;
|
||||
}
|
20
website/src/pages/store/plugins.tsx
Normal file
20
website/src/pages/store/plugins.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
|
||||
import { translate } from "@docusaurus/Translate";
|
||||
|
||||
import PluginPageContent from "@/components/Store/Content/Plugin";
|
||||
import StoreLayout from "@/components/Store/Layout";
|
||||
|
||||
export default function StorePlugins(): JSX.Element {
|
||||
const title = translate({
|
||||
id: "pages.store.plugin.title",
|
||||
message: "插件商店",
|
||||
description: "Title for the plugin store page",
|
||||
});
|
||||
|
||||
return (
|
||||
<StoreLayout title={title}>
|
||||
<PluginPageContent />
|
||||
</StoreLayout>
|
||||
);
|
||||
}
|
23
website/src/plugins/webpack-plugin.cjs
Normal file
23
website/src/plugins/webpack-plugin.cjs
Normal file
@ -0,0 +1,23 @@
|
||||
// @ts-check
|
||||
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* @returns {import('@docusaurus/types').Plugin}
|
||||
*/
|
||||
function webpackPlugin() {
|
||||
return {
|
||||
name: "webpack-plugin",
|
||||
configureWebpack() {
|
||||
return {
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "../"),
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = webpackPlugin;
|
45
website/src/theme/Footer/Copyright/index.tsx
Normal file
45
website/src/theme/Footer/Copyright/index.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
|
||||
import Link from "@docusaurus/Link";
|
||||
import Translate, { translate } from "@docusaurus/Translate";
|
||||
|
||||
import type { Props } from "@theme/Footer/Copyright";
|
||||
import IconCloudflare from "@theme/Icon/Cloudflare";
|
||||
import IconNetlify from "@theme/Icon/Netlify";
|
||||
import OriginCopyright from "@theme-original/Footer/Copyright";
|
||||
|
||||
export default function FooterCopyright(props: Props) {
|
||||
return (
|
||||
<>
|
||||
<OriginCopyright {...props} />
|
||||
<div className="footer-support">
|
||||
<Translate
|
||||
id="theme.FooterCopyright.deployBy"
|
||||
description="The deploy by message."
|
||||
>
|
||||
Deployed by
|
||||
</Translate>
|
||||
<Link
|
||||
to="https://www.netlify.com/"
|
||||
title={translate({
|
||||
id: "theme.FooterCopyright.netlifyLinkTitle",
|
||||
message: "Go to the Netlify website",
|
||||
description: "The title attribute for the Netlify logo link",
|
||||
})}
|
||||
>
|
||||
<IconNetlify className="footer-support-icon" />
|
||||
</Link>
|
||||
<Link
|
||||
to="https://www.cloudflare.com/"
|
||||
title={translate({
|
||||
id: "theme.FooterCopyright.cloudflareLinkTitle",
|
||||
message: "Go to the Cloudflare website",
|
||||
description: "The title attribute for the Cloudflare logo link",
|
||||
})}
|
||||
>
|
||||
<IconCloudflare className="footer-support-icon" />
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import Link from "@docusaurus/Link";
|
||||
import OriginCopyright from "@theme-original/FooterCopyright";
|
||||
|
||||
export default function FooterCopyright(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<OriginCopyright />
|
||||
<div>
|
||||
<p className="flex flex-col text-base opacity-60 md:items-center md:justify-center xl:text-center">
|
||||
{/* <Link to="https://www.netlify.com">
|
||||
<img
|
||||
src="https://www.netlify.com/img/global/badges/netlify-color-accent.svg"
|
||||
alt="Deploys by Netlify"
|
||||
/>
|
||||
</Link> */}
|
||||
<span className="flex items-center justify-start md:justify-center">
|
||||
Deployed by
|
||||
<Link to="https://www.netlify.com" className="ml-1 opacity-100">
|
||||
<svg
|
||||
height="1rem"
|
||||
viewBox="0 0 256 105"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_236_25)">
|
||||
<path
|
||||
d="M58.4704 103.765V77.4144L59.0165 76.8683H65.6043L66.1504 77.4144V103.765L65.6043 104.311H59.0165L58.4704 103.765Z"
|
||||
fill="#05BDBA"
|
||||
/>
|
||||
<path
|
||||
d="M58.4704 26.8971V0.546133L59.0165 0H65.6043L66.1504 0.546133V26.8971L65.6043 27.4432H59.0165L58.4704 26.8971Z"
|
||||
fill="#05BDBA"
|
||||
/>
|
||||
<path
|
||||
d="M35.7973 85.2395H34.8928L30.3616 80.7083V79.8037L38.8523 71.3045L43.648 71.3131L44.288 71.9445V76.7403L35.7973 85.2395Z"
|
||||
fill="#05BDBA"
|
||||
/>
|
||||
<path
|
||||
d="M30.3616 24.7467V23.8336L34.8928 19.3109H35.7973L44.288 27.8016V32.5888L43.648 33.2373H38.8523L30.3616 24.7467Z"
|
||||
fill="#05BDBA"
|
||||
/>
|
||||
<path
|
||||
d="M0.546133 48.3072H37.8795L38.4256 48.8533V55.4496L37.8795 55.9958H0.546133L0 55.4496V48.8533L0.546133 48.3072Z"
|
||||
fill="#05BDBA"
|
||||
/>
|
||||
<path
|
||||
d="M255.445 48.3157L255.991 48.8619V55.4496L255.445 55.9957H217.566L217.02 55.4496L219.759 48.8619L220.305 48.3157H255.445Z"
|
||||
fill="#05BDBA"
|
||||
/>
|
||||
<path
|
||||
d="M74.6667 65.8859H68.0789L67.5328 65.3397V49.92C67.5328 47.1723 66.4576 45.0475 63.1467 44.9792C61.44 44.9365 59.4944 44.9792 57.4123 45.0645L57.0965 45.3803V65.3312L56.5504 65.8773H49.9627L49.4165 65.3312V38.9803L49.9627 38.4341H64.7851C70.5451 38.4341 75.2128 43.1019 75.2128 48.8619V65.3312L74.6667 65.8773V65.8859Z"
|
||||
fill="#014847"
|
||||
/>
|
||||
<path
|
||||
d="M106.573 54.3488L106.027 54.8949H88.9941L88.448 55.4411C88.448 56.5419 89.5488 59.8357 93.9435 59.8357C95.5904 59.8357 97.2373 59.2896 97.792 58.1888L98.3381 57.6427H104.926L105.472 58.1888C104.926 61.4827 102.178 66.432 93.9349 66.432C84.5995 66.432 80.2048 59.8443 80.2048 52.1472C80.2048 44.4501 84.5995 37.8624 93.3888 37.8624C102.178 37.8624 106.573 44.4501 106.573 52.1472V54.3488ZM98.3296 48.8533C98.3296 48.3072 97.7835 44.4587 93.3888 44.4587C88.9941 44.4587 88.448 48.3072 88.448 48.8533L88.9941 49.3995H97.7835L98.3296 48.8533Z"
|
||||
fill="#014847"
|
||||
/>
|
||||
<path
|
||||
d="M121.95 57.6427C121.95 58.7435 122.496 59.2896 123.597 59.2896H128.538L129.084 59.8358V65.3312L128.538 65.8773H123.597C118.656 65.8773 114.261 63.6758 114.261 57.6342V45.5509L113.715 45.0048H109.867L109.321 44.4587V38.9632L109.867 38.4171H113.715L114.261 37.8709V32.9301L114.807 32.384H121.395L121.941 32.9301V37.8709L122.487 38.4171H128.529L129.075 38.9632V44.4587L128.529 45.0048H122.487L121.941 45.5509V57.6342L121.95 57.6427Z"
|
||||
fill="#014847"
|
||||
/>
|
||||
<path
|
||||
d="M142.276 65.8859H135.689L135.142 65.3397V27.9808L135.689 27.4347H142.276L142.822 27.9808V65.3312L142.276 65.8773V65.8859Z"
|
||||
fill="#014847"
|
||||
/>
|
||||
<path
|
||||
d="M157.107 34.0224H150.519L149.973 33.4763V27.9808L150.519 27.4347H157.107L157.653 27.9808V33.4763L157.107 34.0224ZM157.107 65.8859H150.519L149.973 65.3397V38.9717L150.519 38.4256H157.107L157.653 38.9717V65.3397L157.107 65.8859Z"
|
||||
fill="#014847"
|
||||
/>
|
||||
<path
|
||||
d="M182.929 27.9808V33.4763L182.383 34.0224H177.442C176.341 34.0224 175.795 34.5685 175.795 35.6693V37.8709L176.341 38.4171H181.837L182.383 38.9632V44.4587L181.837 45.0048H176.341L175.795 45.5509V65.3227L175.249 65.8688H168.661L168.115 65.3227V45.5509L167.569 45.0048H163.721L163.174 44.4587V38.9632L163.721 38.4171H167.569L168.115 37.8709V35.6693C168.115 29.6277 172.51 27.4261 177.451 27.4261H182.391L182.938 27.9723L182.929 27.9808Z"
|
||||
fill="#014847"
|
||||
/>
|
||||
<path
|
||||
d="M203.247 66.432C201.045 71.9275 198.852 75.2213 191.164 75.2213H188.416L187.87 74.6752V69.1797L188.416 68.6336H191.164C193.911 68.6336 194.458 68.0875 195.012 66.4405V65.8944L186.223 44.4672V38.9717L186.769 38.4256H191.71L192.256 38.9717L198.844 57.6512H199.39L205.978 38.9717L206.524 38.4256H211.465L212.011 38.9717V44.4672L203.221 66.4405L203.247 66.432Z"
|
||||
fill="#014847"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_236_25">
|
||||
<rect width="256" height="104.311" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</Link>
|
||||
<Link to="https://www.cloudflare.com/" className="ml-1 opacity-100">
|
||||
<svg
|
||||
height="1rem"
|
||||
viewBox="0 0 651.29 94.76"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill="#f78100"
|
||||
d="M143.05,93.42l1.07-3.71c1.27-4.41.8-8.48-1.34-11.48-2-2.76-5.26-4.38-9.25-4.57L58,72.7a1.47,1.47,0,0,1-1.35-2,2,2,0,0,1,1.75-1.34l76.26-1c9-.41,18.84-7.75,22.27-16.71l4.34-11.36a2.68,2.68,0,0,0,.18-1,3.31,3.31,0,0,0-.06-.54,49.67,49.67,0,0,0-95.49-5.14,22.35,22.35,0,0,0-35,23.42A31.73,31.73,0,0,0,.34,93.45a1.47,1.47,0,0,0,1.45,1.27l139.49,0h0A1.83,1.83,0,0,0,143.05,93.42Z"
|
||||
/>
|
||||
<path
|
||||
fill="#fcad32"
|
||||
d="M168.22,41.15q-1,0-2.1.06a.88.88,0,0,0-.32.07,1.17,1.17,0,0,0-.76.8l-3,10.26c-1.28,4.41-.81,8.48,1.34,11.48a11.65,11.65,0,0,0,9.24,4.57l16.11,1a1.44,1.44,0,0,1,1.14.62,1.5,1.5,0,0,1,.17,1.37,2,2,0,0,1-1.75,1.34l-16.73,1c-9.09.42-18.88,7.75-22.31,16.7l-1.21,3.16a.9.9,0,0,0,.79,1.22h57.63A1.55,1.55,0,0,0,208,93.63a41.34,41.34,0,0,0-39.76-52.48Z"
|
||||
/>
|
||||
<polygon points="273.03 59.66 282.56 59.66 282.56 85.72 299.23 85.72 299.23 94.07 273.03 94.07 273.03 59.66" />
|
||||
<path d="M309.11,77v-.09c0-9.88,8-17.9,18.58-17.9s18.48,7.92,18.48,17.8v.1c0,9.88-8,17.89-18.58,17.89S309.11,86.85,309.11,77m27.33,0v-.09c0-5-3.59-9.29-8.85-9.29s-8.7,4.22-8.7,9.19v.1c0,5,3.59,9.29,8.8,9.29s8.75-4.23,8.75-9.2" />
|
||||
<path d="M357.84,79V59.66h9.69V78.78c0,5,2.5,7.33,6.34,7.33s6.34-2.26,6.34-7.08V59.66h9.68V78.73c0,11.11-6.34,16-16.12,16s-15.93-5-15.93-15.73" />
|
||||
<path d="M404.49,59.66h13.27c12.29,0,19.42,7.08,19.42,17v.1c0,9.93-7.23,17.3-19.61,17.3H404.49Zm13.42,26c5.7,0,9.49-3.15,9.49-8.71v-.09c0-5.51-3.79-8.71-9.49-8.71H414V85.62Z" />
|
||||
<polygon points="451.04 59.66 478.56 59.66 478.56 68.02 460.58 68.02 460.58 73.87 476.85 73.87 476.85 81.78 460.58 81.78 460.58 94.07 451.04 94.07 451.04 59.66" />
|
||||
<polygon points="491.84 59.66 501.37 59.66 501.37 85.72 518.04 85.72 518.04 94.07 491.84 94.07 491.84 59.66" />
|
||||
<path d="M543,59.42h9.19L566.8,94.07H556.58l-2.51-6.14H540.79l-2.45,6.14h-10Zm8.35,21.08-3.83-9.78L543.6,80.5Z" />
|
||||
<path d="M579.08,59.66h16.27c5.27,0,8.9,1.38,11.21,3.74a10.64,10.64,0,0,1,3.05,8v.1a10.88,10.88,0,0,1-7.08,10.57l8.21,12h-11L592.8,83.65h-4.18V94.07h-9.54Zm15.83,16.52c3.25,0,5.12-1.58,5.12-4.08V72c0-2.71-2-4.08-5.17-4.08h-6.24v8.26Z" />
|
||||
<polygon points="623.37 59.66 651.05 59.66 651.05 67.77 632.81 67.77 632.81 72.98 649.33 72.98 649.33 80.5 632.81 80.5 632.81 85.96 651.29 85.96 651.29 94.07 623.37 94.07 623.37 59.66" />
|
||||
<path d="M252.15,81a8.44,8.44,0,0,1-7.88,5.16c-5.22,0-8.8-4.33-8.8-9.29v-.1c0-5,3.49-9.2,8.7-9.2a8.64,8.64,0,0,1,8.18,5.71h10C260.79,65.09,253.6,59,244.27,59c-10.62,0-18.58,8-18.58,17.9V77c0,9.88,7.86,17.8,18.48,17.8,9.08,0,16.18-5.88,18.05-13.76Z" />
|
||||
</svg>
|
||||
</Link>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
32
website/src/theme/Icon/Cloudflare.tsx
Normal file
32
website/src/theme/Icon/Cloudflare.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React, { type ComponentProps } from "react";
|
||||
|
||||
export interface Props extends Omit<ComponentProps<"svg">, "viewBox"> {}
|
||||
|
||||
export default function IconCloudflare(props: Props): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 651.29 94.76"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="#f78100"
|
||||
d="M143.05,93.42l1.07-3.71c1.27-4.41.8-8.48-1.34-11.48-2-2.76-5.26-4.38-9.25-4.57L58,72.7a1.47,1.47,0,0,1-1.35-2,2,2,0,0,1,1.75-1.34l76.26-1c9-.41,18.84-7.75,22.27-16.71l4.34-11.36a2.68,2.68,0,0,0,.18-1,3.31,3.31,0,0,0-.06-.54,49.67,49.67,0,0,0-95.49-5.14,22.35,22.35,0,0,0-35,23.42A31.73,31.73,0,0,0,.34,93.45a1.47,1.47,0,0,0,1.45,1.27l139.49,0h0A1.83,1.83,0,0,0,143.05,93.42Z"
|
||||
/>
|
||||
<path
|
||||
fill="#fcad32"
|
||||
d="M168.22,41.15q-1,0-2.1.06a.88.88,0,0,0-.32.07,1.17,1.17,0,0,0-.76.8l-3,10.26c-1.28,4.41-.81,8.48,1.34,11.48a11.65,11.65,0,0,0,9.24,4.57l16.11,1a1.44,1.44,0,0,1,1.14.62,1.5,1.5,0,0,1,.17,1.37,2,2,0,0,1-1.75,1.34l-16.73,1c-9.09.42-18.88,7.75-22.31,16.7l-1.21,3.16a.9.9,0,0,0,.79,1.22h57.63A1.55,1.55,0,0,0,208,93.63a41.34,41.34,0,0,0-39.76-52.48Z"
|
||||
/>
|
||||
<polygon points="273.03 59.66 282.56 59.66 282.56 85.72 299.23 85.72 299.23 94.07 273.03 94.07 273.03 59.66" />
|
||||
<path d="M309.11,77v-.09c0-9.88,8-17.9,18.58-17.9s18.48,7.92,18.48,17.8v.1c0,9.88-8,17.89-18.58,17.89S309.11,86.85,309.11,77m27.33,0v-.09c0-5-3.59-9.29-8.85-9.29s-8.7,4.22-8.7,9.19v.1c0,5,3.59,9.29,8.8,9.29s8.75-4.23,8.75-9.2" />
|
||||
<path d="M357.84,79V59.66h9.69V78.78c0,5,2.5,7.33,6.34,7.33s6.34-2.26,6.34-7.08V59.66h9.68V78.73c0,11.11-6.34,16-16.12,16s-15.93-5-15.93-15.73" />
|
||||
<path d="M404.49,59.66h13.27c12.29,0,19.42,7.08,19.42,17v.1c0,9.93-7.23,17.3-19.61,17.3H404.49Zm13.42,26c5.7,0,9.49-3.15,9.49-8.71v-.09c0-5.51-3.79-8.71-9.49-8.71H414V85.62Z" />
|
||||
<polygon points="451.04 59.66 478.56 59.66 478.56 68.02 460.58 68.02 460.58 73.87 476.85 73.87 476.85 81.78 460.58 81.78 460.58 94.07 451.04 94.07 451.04 59.66" />
|
||||
<polygon points="491.84 59.66 501.37 59.66 501.37 85.72 518.04 85.72 518.04 94.07 491.84 94.07 491.84 59.66" />
|
||||
<path d="M543,59.42h9.19L566.8,94.07H556.58l-2.51-6.14H540.79l-2.45,6.14h-10Zm8.35,21.08-3.83-9.78L543.6,80.5Z" />
|
||||
<path d="M579.08,59.66h16.27c5.27,0,8.9,1.38,11.21,3.74a10.64,10.64,0,0,1,3.05,8v.1a10.88,10.88,0,0,1-7.08,10.57l8.21,12h-11L592.8,83.65h-4.18V94.07h-9.54Zm15.83,16.52c3.25,0,5.12-1.58,5.12-4.08V72c0-2.71-2-4.08-5.17-4.08h-6.24v8.26Z" />
|
||||
<polygon points="623.37 59.66 651.05 59.66 651.05 67.77 632.81 67.77 632.81 72.98 649.33 72.98 649.33 80.5 632.81 80.5 632.81 85.96 651.29 85.96 651.29 94.07 623.37 94.07 623.37 59.66" />
|
||||
<path d="M252.15,81a8.44,8.44,0,0,1-7.88,5.16c-5.22,0-8.8-4.33-8.8-9.29v-.1c0-5,3.49-9.2,8.7-9.2a8.64,8.64,0,0,1,8.18,5.71h10C260.79,65.09,253.6,59,244.27,59c-10.62,0-18.58,8-18.58,17.9V77c0,9.88,7.86,17.8,18.48,17.8,9.08,0,16.18-5.88,18.05-13.76Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
69
website/src/theme/Icon/Netlify.tsx
Normal file
69
website/src/theme/Icon/Netlify.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { type ComponentProps } from "react";
|
||||
|
||||
export interface Props extends Omit<ComponentProps<"svg">, "viewBox"> {}
|
||||
|
||||
export default function IconNetlify(props: Props): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="0 0 256 105" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<g clipPath="url(#clip0_236_25)">
|
||||
<path
|
||||
d="M58.4704 103.765V77.4144L59.0165 76.8683H65.6043L66.1504 77.4144V103.765L65.6043 104.311H59.0165L58.4704 103.765Z"
|
||||
fill="#05BDBA"
|
||||
/>
|
||||
<path
|
||||
d="M58.4704 26.8971V0.546133L59.0165 0H65.6043L66.1504 0.546133V26.8971L65.6043 27.4432H59.0165L58.4704 26.8971Z"
|
||||
fill="#05BDBA"
|
||||
/>
|
||||
<path
|
||||
d="M35.7973 85.2395H34.8928L30.3616 80.7083V79.8037L38.8523 71.3045L43.648 71.3131L44.288 71.9445V76.7403L35.7973 85.2395Z"
|
||||
fill="#05BDBA"
|
||||
/>
|
||||
<path
|
||||
d="M30.3616 24.7467V23.8336L34.8928 19.3109H35.7973L44.288 27.8016V32.5888L43.648 33.2373H38.8523L30.3616 24.7467Z"
|
||||
fill="#05BDBA"
|
||||
/>
|
||||
<path
|
||||
d="M0.546133 48.3072H37.8795L38.4256 48.8533V55.4496L37.8795 55.9958H0.546133L0 55.4496V48.8533L0.546133 48.3072Z"
|
||||
fill="#05BDBA"
|
||||
/>
|
||||
<path
|
||||
d="M255.445 48.3157L255.991 48.8619V55.4496L255.445 55.9957H217.566L217.02 55.4496L219.759 48.8619L220.305 48.3157H255.445Z"
|
||||
fill="#05BDBA"
|
||||
/>
|
||||
<path
|
||||
d="M74.6667 65.8859H68.0789L67.5328 65.3397V49.92C67.5328 47.1723 66.4576 45.0475 63.1467 44.9792C61.44 44.9365 59.4944 44.9792 57.4123 45.0645L57.0965 45.3803V65.3312L56.5504 65.8773H49.9627L49.4165 65.3312V38.9803L49.9627 38.4341H64.7851C70.5451 38.4341 75.2128 43.1019 75.2128 48.8619V65.3312L74.6667 65.8773V65.8859Z"
|
||||
fill="#014847"
|
||||
/>
|
||||
<path
|
||||
d="M106.573 54.3488L106.027 54.8949H88.9941L88.448 55.4411C88.448 56.5419 89.5488 59.8357 93.9435 59.8357C95.5904 59.8357 97.2373 59.2896 97.792 58.1888L98.3381 57.6427H104.926L105.472 58.1888C104.926 61.4827 102.178 66.432 93.9349 66.432C84.5995 66.432 80.2048 59.8443 80.2048 52.1472C80.2048 44.4501 84.5995 37.8624 93.3888 37.8624C102.178 37.8624 106.573 44.4501 106.573 52.1472V54.3488ZM98.3296 48.8533C98.3296 48.3072 97.7835 44.4587 93.3888 44.4587C88.9941 44.4587 88.448 48.3072 88.448 48.8533L88.9941 49.3995H97.7835L98.3296 48.8533Z"
|
||||
fill="#014847"
|
||||
/>
|
||||
<path
|
||||
d="M121.95 57.6427C121.95 58.7435 122.496 59.2896 123.597 59.2896H128.538L129.084 59.8358V65.3312L128.538 65.8773H123.597C118.656 65.8773 114.261 63.6758 114.261 57.6342V45.5509L113.715 45.0048H109.867L109.321 44.4587V38.9632L109.867 38.4171H113.715L114.261 37.8709V32.9301L114.807 32.384H121.395L121.941 32.9301V37.8709L122.487 38.4171H128.529L129.075 38.9632V44.4587L128.529 45.0048H122.487L121.941 45.5509V57.6342L121.95 57.6427Z"
|
||||
fill="#014847"
|
||||
/>
|
||||
<path
|
||||
d="M142.276 65.8859H135.689L135.142 65.3397V27.9808L135.689 27.4347H142.276L142.822 27.9808V65.3312L142.276 65.8773V65.8859Z"
|
||||
fill="#014847"
|
||||
/>
|
||||
<path
|
||||
d="M157.107 34.0224H150.519L149.973 33.4763V27.9808L150.519 27.4347H157.107L157.653 27.9808V33.4763L157.107 34.0224ZM157.107 65.8859H150.519L149.973 65.3397V38.9717L150.519 38.4256H157.107L157.653 38.9717V65.3397L157.107 65.8859Z"
|
||||
fill="#014847"
|
||||
/>
|
||||
<path
|
||||
d="M182.929 27.9808V33.4763L182.383 34.0224H177.442C176.341 34.0224 175.795 34.5685 175.795 35.6693V37.8709L176.341 38.4171H181.837L182.383 38.9632V44.4587L181.837 45.0048H176.341L175.795 45.5509V65.3227L175.249 65.8688H168.661L168.115 65.3227V45.5509L167.569 45.0048H163.721L163.174 44.4587V38.9632L163.721 38.4171H167.569L168.115 37.8709V35.6693C168.115 29.6277 172.51 27.4261 177.451 27.4261H182.391L182.938 27.9723L182.929 27.9808Z"
|
||||
fill="#014847"
|
||||
/>
|
||||
<path
|
||||
d="M203.247 66.432C201.045 71.9275 198.852 75.2213 191.164 75.2213H188.416L187.87 74.6752V69.1797L188.416 68.6336H191.164C193.911 68.6336 194.458 68.0875 195.012 66.4405V65.8944L186.223 44.4672V38.9717L186.769 38.4256H191.71L192.256 38.9717L198.844 57.6512H199.39L205.978 38.9717L206.524 38.4256H211.465L212.011 38.9717V44.4672L203.221 66.4405L203.247 66.432Z"
|
||||
fill="#014847"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_236_25">
|
||||
<rect width="256" height="104.311" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
19
website/src/theme/TOC/Container/index.tsx
Normal file
19
website/src/theme/TOC/Container/index.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
import "./styles.css";
|
||||
import type { Props } from "@theme/TOC/Container";
|
||||
import OriginTOCContainer from "@theme-original/TOC/Container";
|
||||
|
||||
export default function TOCContainer({
|
||||
children,
|
||||
...props
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<OriginTOCContainer {...props}>
|
||||
{children}
|
||||
<div className="toc-ads-container">
|
||||
<div className="wwads-cn wwads-vertical toc-ads" data-id="281"></div>
|
||||
</div>
|
||||
</OriginTOCContainer>
|
||||
);
|
||||
}
|
12
website/src/theme/TOC/Container/styles.css
Normal file
12
website/src/theme/TOC/Container/styles.css
Normal file
@ -0,0 +1,12 @@
|
||||
.toc-ads {
|
||||
@apply max-w-full !m-0 !bg-transparent;
|
||||
|
||||
& .wwads-text {
|
||||
@apply !text-base-content;
|
||||
@apply transition-[color] duration-500;
|
||||
}
|
||||
|
||||
&-container {
|
||||
@apply shrink-0 w-full max-w-full px-4;
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import TOCItems from "@theme/TOCItems";
|
||||
import styles from "./styles.module.css";
|
||||
import type { TOCProps } from "@theme/TOC";
|
||||
|
||||
const LINK_CLASS_NAME = styles["toc-link"];
|
||||
const LINK_ACTIVE_CLASS_NAME = styles["toc-link-active"];
|
||||
|
||||
export default function TOC({ className, ...props }: TOCProps): JSX.Element {
|
||||
return (
|
||||
<div className={clsx(styles.toc, "thin-scrollbar", className)}>
|
||||
<TOCItems
|
||||
{...props}
|
||||
linkClassName={LINK_CLASS_NAME}
|
||||
linkActiveClassName={LINK_ACTIVE_CLASS_NAME}
|
||||
/>
|
||||
<div className={styles.tocAdsContainer}>
|
||||
<div
|
||||
className={clsx("wwads-cn wwads-horizontal", styles.tocAds)}
|
||||
data-id="281"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user