📝 Docs: 升级新版 NonePress 主题 (#2375)

This commit is contained in:
Ju4tCode
2023-09-27 16:00:26 +08:00
committed by GitHub
parent 7754f6da1d
commit 842c6ff4c6
234 changed files with 8759 additions and 5887 deletions

View File

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

View File

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

View File

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

View File

@ -1,3 +1,7 @@
.asciinema-player svg {
display: inline-block;
.ap-player svg {
@apply inline-block;
}
.ap-container {
@apply w-full my-4;
}

View File

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

View File

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

View File

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

View File

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

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

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

View 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>
);
}

View 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];
}
}

View File

@ -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, "&nbsp;"),
}}
@ -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>

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View 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>
);
}

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

View 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>
);
}

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

View 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>
);
}

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

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

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

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

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

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

View 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>
);
}

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

View File

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

View File

@ -1,3 +0,0 @@
.homeCodeBlock {
width: 602px;
}

16
website/src/libs/color.ts Normal file
View 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
View 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(),
};
}

View File

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

View 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,
};
}

View File

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

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

View File

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

View File

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

View File

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

View 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>
);
}

View 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>
);
}

View 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>
);
}

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

View 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>
);
}

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

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

View File

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

View 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>
);
}

View 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>
);
}

View 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>
);
}

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

View File

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

View File

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

View 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
View 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[];

View 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[];

View 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
View File

@ -0,0 +1,4 @@
export type Tag = {
label: string;
color: `#${string}`;
};