mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-07-29 17:27:41 +00:00
📝 Docs: 升级新版 NonePress 主题 (#2375)
This commit is contained in:
@ -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>
|
||||
);
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
.toc {
|
||||
max-height: calc(100vh - 7rem);
|
||||
|
||||
@apply sticky top-28 overflow-y-auto;
|
||||
}
|
||||
|
||||
.toc-link {
|
||||
@apply text-light-text;
|
||||
}
|
||||
|
||||
:global .dark :local .toc-link {
|
||||
@apply text-dark-text;
|
||||
}
|
||||
|
||||
.toc-link-active {
|
||||
@apply text-light-text-active;
|
||||
}
|
||||
|
||||
:global .dark :local .toc-link-active {
|
||||
@apply text-dark-text-active;
|
||||
}
|
||||
|
||||
.tocAdsContainer {
|
||||
@apply sticky bottom-0 w-full max-w-full mt-2;
|
||||
}
|
||||
|
||||
.tocAds {
|
||||
@apply max-w-full !bg-light;
|
||||
}
|
||||
|
||||
:global .dark :local .tocAds {
|
||||
@apply !bg-dark;
|
||||
}
|
||||
|
||||
.tocAds :global .wwads-text {
|
||||
@apply text-light-text;
|
||||
}
|
||||
|
||||
:global .dark :local .tocAds :global .wwads-text {
|
||||
@apply text-dark-text;
|
||||
}
|
16
website/src/types/adapter.ts
Normal file
16
website/src/types/adapter.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { Tag } from "./tag";
|
||||
|
||||
type BaseAdapter = {
|
||||
module_name: string;
|
||||
project_link: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
author: string;
|
||||
homepage: string;
|
||||
tags: Tag[];
|
||||
is_official: boolean;
|
||||
};
|
||||
|
||||
export type Adapter = { resourceType: "adapter" } & BaseAdapter;
|
||||
|
||||
export type AdaptersResponse = BaseAdapter[];
|
14
website/src/types/bot.ts
Normal file
14
website/src/types/bot.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { Tag } from "./tag";
|
||||
|
||||
type BaseBot = {
|
||||
name: string;
|
||||
desc: string;
|
||||
author: string;
|
||||
homepage: string;
|
||||
tags: Tag[];
|
||||
is_official: boolean;
|
||||
};
|
||||
|
||||
export type Bot = { resourceType: "bot" } & BaseBot;
|
||||
|
||||
export type BotsResponse = BaseBot[];
|
16
website/src/types/driver.ts
Normal file
16
website/src/types/driver.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { Tag } from "./tag";
|
||||
|
||||
type BaseDriver = {
|
||||
module_name: string;
|
||||
project_link: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
author: string;
|
||||
homepage: string;
|
||||
tags: Tag[];
|
||||
is_official: boolean;
|
||||
};
|
||||
|
||||
export type Driver = { resourceType: "driver" } & BaseDriver;
|
||||
|
||||
export type DriversResponse = BaseDriver[];
|
22
website/src/types/plugin.ts
Normal file
22
website/src/types/plugin.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Tag } from "./tag";
|
||||
|
||||
type BasePlugin = {
|
||||
author: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
homepage: string;
|
||||
is_official: boolean;
|
||||
module_name: string;
|
||||
project_link: string;
|
||||
skip_test: boolean;
|
||||
supported_adapters: string[] | null;
|
||||
tags: Array<Tag>;
|
||||
time: string;
|
||||
type: string;
|
||||
valid: boolean;
|
||||
version: string;
|
||||
};
|
||||
|
||||
export type Plugin = { resourceType: "plugin" } & BasePlugin;
|
||||
|
||||
export type PluginsResponse = BasePlugin[];
|
4
website/src/types/tag.ts
Normal file
4
website/src/types/tag.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type Tag = {
|
||||
label: string;
|
||||
color: `#${string}`;
|
||||
};
|
Reference in New Issue
Block a user