mirror of
https://github.com/nonebot/nonebot2.git
synced 2024-11-27 18:45:05 +08:00
📝 Docs: 添加商店表单支持 (#2460)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
This commit is contained in:
parent
af9327de14
commit
ef882927f3
@ -37,6 +37,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/module-type-aliases": "^2.4.1",
|
"@docusaurus/module-type-aliases": "^2.4.1",
|
||||||
"@tsconfig/docusaurus": "^1.0.5",
|
"@tsconfig/docusaurus": "^1.0.5",
|
||||||
|
"@types/react-color": "^3.0.10",
|
||||||
"asciinema-player": "^3.5.0",
|
"asciinema-player": "^3.5.0",
|
||||||
"typescript": "^4.7.4"
|
"typescript": "^4.7.4"
|
||||||
},
|
},
|
||||||
|
51
website/src/components/Form/Adapter.tsx
Normal file
51
website/src/components/Form/Adapter.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { Form } from ".";
|
||||||
|
|
||||||
|
export default function AdapterForm(): JSX.Element {
|
||||||
|
const formItems = [
|
||||||
|
{
|
||||||
|
name: "基本信息",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
name: "name",
|
||||||
|
labelText: "适配器名称",
|
||||||
|
},
|
||||||
|
{ type: "text", name: "description", labelText: "适配器描述" },
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
name: "homepage",
|
||||||
|
labelText: "适配器项目仓库/主页链接",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "包信息",
|
||||||
|
items: [
|
||||||
|
{ type: "text", name: "pypi", labelText: "PyPI 项目名" },
|
||||||
|
{ type: "text", name: "module", labelText: "适配器 import 包名" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "其他信息",
|
||||||
|
items: [{ type: "tag", name: "tags", labelText: "标签" }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const handleSubmit = (result: Record<string, string>) => {
|
||||||
|
window.open(
|
||||||
|
`https://github.com/nonebot/nonebot2/issues/new?${new URLSearchParams({
|
||||||
|
assignees: "",
|
||||||
|
labels: "Adapter",
|
||||||
|
projects: "",
|
||||||
|
template: "adapter_publish.yml",
|
||||||
|
title: `Adapter: ${result.name}`,
|
||||||
|
...result,
|
||||||
|
})}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form type="adapter" formItems={formItems} handleSubmit={handleSubmit} />
|
||||||
|
);
|
||||||
|
}
|
43
website/src/components/Form/Bot.tsx
Normal file
43
website/src/components/Form/Bot.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { Form } from ".";
|
||||||
|
|
||||||
|
export default function BotForm(): JSX.Element {
|
||||||
|
const formItems = [
|
||||||
|
{
|
||||||
|
name: "基本信息",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
name: "name",
|
||||||
|
labelText: "机器人名称",
|
||||||
|
},
|
||||||
|
{ type: "text", name: "description", labelText: "机器人描述" },
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
name: "homepage",
|
||||||
|
labelText: "机器人项目仓库/主页链接",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "其他信息",
|
||||||
|
items: [{ type: "tag", name: "tags", labelText: "标签" }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSubmit = (result: Record<string, string>) => {
|
||||||
|
window.open(
|
||||||
|
`https://github.com/nonebot/nonebot2/issues/new?${new URLSearchParams({
|
||||||
|
assignees: "",
|
||||||
|
labels: "Bot",
|
||||||
|
projects: "",
|
||||||
|
template: "bot_publish.yml",
|
||||||
|
title: `Bot: ${result.name}`,
|
||||||
|
...result,
|
||||||
|
})}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Form type="bot" formItems={formItems} handleSubmit={handleSubmit} />;
|
||||||
|
}
|
110
website/src/components/Form/Items/Tag/index.tsx
Normal file
110
website/src/components/Form/Items/Tag/index.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { ChromePicker, type ColorResult } from "react-color";
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
import TagComponent from "@/components/Tag";
|
||||||
|
import { Tag as TagType } from "@/types/tag";
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
allowTags: TagType[];
|
||||||
|
onTagUpdate: (tags: TagType[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TagFormItem({
|
||||||
|
allowTags,
|
||||||
|
onTagUpdate,
|
||||||
|
}: Props): JSX.Element {
|
||||||
|
const [tags, setTags] = useState<TagType[]>([]);
|
||||||
|
const [label, setLabel] = useState<TagType["label"]>("");
|
||||||
|
const [color, setColor] = useState<TagType["color"]>("#ea5252");
|
||||||
|
const slicedTags = Array.from(
|
||||||
|
new Set(
|
||||||
|
allowTags
|
||||||
|
.filter((tag) => tag.label.toLocaleLowerCase().includes(label))
|
||||||
|
.map((e) => e.label)
|
||||||
|
)
|
||||||
|
).slice(0, 5);
|
||||||
|
|
||||||
|
const validateTag = () => {
|
||||||
|
return label.length >= 1 && label.length <= 10;
|
||||||
|
};
|
||||||
|
const newTag = () => {
|
||||||
|
if (tags.length >= 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (validateTag()) {
|
||||||
|
const tag: TagType = { label, color };
|
||||||
|
setTags([...tags, tag]);
|
||||||
|
onTagUpdate(tags);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const delTag = (index: number) => {
|
||||||
|
setTags(tags.filter((_, i) => i !== index));
|
||||||
|
onTagUpdate(tags);
|
||||||
|
};
|
||||||
|
const onChangeColor = (color: ColorResult) => {
|
||||||
|
setColor(color.hex as TagType["color"]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label className="flex flex-wrap gap-x-1 gap-y-1">
|
||||||
|
{tags.map((tag, index) => (
|
||||||
|
<TagComponent
|
||||||
|
key={index}
|
||||||
|
{...tag}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => delTag(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{tags.length < 3 && (
|
||||||
|
<span
|
||||||
|
className={clsx("add-btn", { "add-btn-disabled": !validateTag() })}
|
||||||
|
onClick={() => newTag()}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon className="pr-1" icon={["fas", "plus"]} />
|
||||||
|
新建标签
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<div className="form-item-container">
|
||||||
|
<span className="form-item-title">标签名称</span>
|
||||||
|
<div className="dropdown dropdown-bottom w-full">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={label}
|
||||||
|
className="form-item form-item-input"
|
||||||
|
placeholder="请输入"
|
||||||
|
onChange={(e) => setLabel(e.target.value)}
|
||||||
|
/>
|
||||||
|
{slicedTags.length > 0 && (
|
||||||
|
<ul
|
||||||
|
tabIndex={0}
|
||||||
|
className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box w-52"
|
||||||
|
>
|
||||||
|
{slicedTags.map((tag) => (
|
||||||
|
<li key={tag}>
|
||||||
|
<a onClick={() => setLabel(tag)}>{tag}</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-item-container">
|
||||||
|
<span className="form-item-title">标签颜色</span>
|
||||||
|
<ChromePicker
|
||||||
|
className="my-4 fix-input-color"
|
||||||
|
color={color}
|
||||||
|
disableAlpha={true}
|
||||||
|
onChangeComplete={onChangeColor}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
35
website/src/components/Form/Items/Tag/styles.css
Normal file
35
website/src/components/Form/Items/Tag/styles.css
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
.add-btn {
|
||||||
|
@apply px-2 select-none cursor-pointer min-w-[64px] rounded-full hover:bg-opacity-[.08];
|
||||||
|
@apply flex justify-center items-center border-dashed border-2;
|
||||||
|
|
||||||
|
&-disabled {
|
||||||
|
@apply pointer-events-none opacity-60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
@apply basis-3/4;
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
@apply basis-1/4 label-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-input {
|
||||||
|
@apply input input-sm input-bordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-select {
|
||||||
|
@apply select select-sm select-bordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-container {
|
||||||
|
@apply flex items-center mt-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fix-input-color {
|
||||||
|
@apply !text-base-content !bg-base-100;
|
||||||
|
input {
|
||||||
|
@apply !text-base-content !bg-base-100;
|
||||||
|
}
|
||||||
|
}
|
35
website/src/components/Form/Plugin.tsx
Normal file
35
website/src/components/Form/Plugin.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { Form } from ".";
|
||||||
|
|
||||||
|
export default function PluginForm(): JSX.Element {
|
||||||
|
const formItems = [
|
||||||
|
{
|
||||||
|
name: "包信息",
|
||||||
|
items: [
|
||||||
|
{ type: "text", name: "pypi", labelText: "PyPI 项目名" },
|
||||||
|
{ type: "text", name: "module", labelText: "插件 import 包名" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "其他信息",
|
||||||
|
items: [{ type: "tag", name: "tags", labelText: "标签" }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const handleSubmit = (result: Record<string, string>) => {
|
||||||
|
window.open(
|
||||||
|
`https://github.com/nonebot/nonebot2/issues/new?${new URLSearchParams({
|
||||||
|
assignees: "",
|
||||||
|
labels: "Plugin",
|
||||||
|
projects: "",
|
||||||
|
template: "plugin_publish.yml",
|
||||||
|
title: `Plugin: ${result.pypi}`,
|
||||||
|
...result,
|
||||||
|
})}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form type="plugin" formItems={formItems} handleSubmit={handleSubmit} />
|
||||||
|
);
|
||||||
|
}
|
159
website/src/components/Form/index.tsx
Normal file
159
website/src/components/Form/index.tsx
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
import TagFormItem from "./Items/Tag";
|
||||||
|
|
||||||
|
import { fetchRegistryData, Resource } from "@/libs/store";
|
||||||
|
import { Tag as TagType } from "@/types/tag";
|
||||||
|
|
||||||
|
export type FormItemData = {
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
labelText: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FormItemGroup = {
|
||||||
|
name: string;
|
||||||
|
items: FormItemData[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
type: Resource["resourceType"];
|
||||||
|
formItems: FormItemGroup[];
|
||||||
|
handleSubmit: (result: Record<string, string>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Form({
|
||||||
|
type,
|
||||||
|
children,
|
||||||
|
formItems,
|
||||||
|
handleSubmit,
|
||||||
|
}: Props): JSX.Element {
|
||||||
|
const [currentStep, setCurrentStep] = useState<number>(0);
|
||||||
|
const [result, setResult] = useState<Record<string, string>>({});
|
||||||
|
const [allowTags, setAllowTags] = useState<TagType[]>([]);
|
||||||
|
|
||||||
|
// load tags asynchronously
|
||||||
|
useEffect(() => {
|
||||||
|
fetchRegistryData(type)
|
||||||
|
.then((data) =>
|
||||||
|
setAllowTags(
|
||||||
|
data
|
||||||
|
.filter((item) => item.tags.length > 0)
|
||||||
|
.map((ele) => ele.tags)
|
||||||
|
.flat()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
}, [type]);
|
||||||
|
|
||||||
|
const setFormValue = (key: string, value: string) => {
|
||||||
|
setResult({ ...result, [key]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextStep = () => {
|
||||||
|
const currentStepNames = formItems[currentStep].items.map(
|
||||||
|
(item) => item.name
|
||||||
|
);
|
||||||
|
if (currentStepNames.every((name) => result[name]))
|
||||||
|
setCurrentStep(currentStep + 1);
|
||||||
|
else return;
|
||||||
|
};
|
||||||
|
const onPrev = () => currentStep > 0 && setCurrentStep(currentStep - 1);
|
||||||
|
const onNext = () =>
|
||||||
|
currentStep < formItems.length - 1
|
||||||
|
? handleNextStep()
|
||||||
|
: handleSubmit(result);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ul className="steps">
|
||||||
|
{formItems.map((item, index) => (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className={clsx("step", currentStep === index && "step-primary")}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="form-control w-full min-h-[300px]">
|
||||||
|
{children ||
|
||||||
|
formItems[currentStep].items.map((item) => (
|
||||||
|
<FormItem
|
||||||
|
key={item.name}
|
||||||
|
type={item.type}
|
||||||
|
name={item.name}
|
||||||
|
labelText={item.labelText}
|
||||||
|
allowTags={allowTags}
|
||||||
|
result={result}
|
||||||
|
setResult={setFormValue}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<button
|
||||||
|
className={clsx("form-btn form-btn-prev", {
|
||||||
|
"form-btn-hidden": currentStep === 0,
|
||||||
|
})}
|
||||||
|
onClick={onPrev}
|
||||||
|
>
|
||||||
|
上一步
|
||||||
|
</button>
|
||||||
|
<button className="form-btn form-btn-next" onClick={onNext}>
|
||||||
|
{currentStep === formItems.length - 1 ? "提交" : "下一步"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormItem({
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
labelText,
|
||||||
|
allowTags,
|
||||||
|
result,
|
||||||
|
setResult,
|
||||||
|
}: FormItemData & {
|
||||||
|
allowTags: TagType[];
|
||||||
|
result: Record<string, string>;
|
||||||
|
setResult: (key: string, value: string) => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">{labelText}</span>
|
||||||
|
</label>
|
||||||
|
{type === "text" && (
|
||||||
|
<input
|
||||||
|
value={result[name] || ""}
|
||||||
|
type="text"
|
||||||
|
name={name}
|
||||||
|
onChange={(e) => setResult(name, e.target.value)}
|
||||||
|
placeholder="请输入"
|
||||||
|
className={clsx("form-input", {
|
||||||
|
"form-input-error": !result[name],
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{type === "text" && !result[name] && (
|
||||||
|
<label className="label">
|
||||||
|
<span className="form-label form-label-error">请输入{labelText}</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{type === "tag" && (
|
||||||
|
<TagFormItem
|
||||||
|
allowTags={allowTags}
|
||||||
|
onTagUpdate={(tags) => setResult(name, JSON.stringify(tags))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
31
website/src/components/Form/styles.css
Normal file
31
website/src/components/Form/styles.css
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
.form-btn {
|
||||||
|
@apply btn btn-sm btn-primary no-animation;
|
||||||
|
|
||||||
|
&-prev {
|
||||||
|
@apply mr-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-next {
|
||||||
|
@apply ml-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-hidden {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
@apply input input-bordered w-full;
|
||||||
|
|
||||||
|
&-error {
|
||||||
|
@apply input-error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
@apply text-xs;
|
||||||
|
|
||||||
|
&-error {
|
||||||
|
@apply text-error;
|
||||||
|
}
|
||||||
|
}
|
61
website/src/components/Modal/index.tsx
Normal file
61
website/src/components/Modal/index.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import IconClose from "@theme/Icon/Close";
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
title: string;
|
||||||
|
useCustomTitle?: boolean;
|
||||||
|
backdropExit?: boolean;
|
||||||
|
setOpenModal: (isOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Modal({
|
||||||
|
setOpenModal,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
useCustomTitle,
|
||||||
|
backdropExit,
|
||||||
|
title,
|
||||||
|
}: Props): JSX.Element {
|
||||||
|
const [transitionClass, setTransitionClass] = useState<string>("");
|
||||||
|
|
||||||
|
const onFadeIn = () => setTransitionClass("fade-in");
|
||||||
|
const onFadeOut = () => setTransitionClass("fade-out");
|
||||||
|
const onTransitionEnd = () =>
|
||||||
|
transitionClass === "fade-out" && setOpenModal(false);
|
||||||
|
|
||||||
|
useEffect(onFadeIn, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx("nb-modal-root", className)}>
|
||||||
|
<div
|
||||||
|
className={clsx("nb-modal-backdrop", transitionClass)}
|
||||||
|
onTransitionEnd={onTransitionEnd}
|
||||||
|
onClick={() => backdropExit && onFadeOut()}
|
||||||
|
/>
|
||||||
|
<div className={clsx("nb-modal-container", transitionClass)}>
|
||||||
|
<div className="card bg-base-100 shadow-xl">
|
||||||
|
<div className="card-body">
|
||||||
|
{!useCustomTitle && (
|
||||||
|
<div className="nb-modal-title">
|
||||||
|
{title}
|
||||||
|
<div className="card-actions ml-auto">
|
||||||
|
<button className="btn btn-square btn-sm" onClick={onFadeOut}>
|
||||||
|
<IconClose />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
36
website/src/components/Modal/styles.css
Normal file
36
website/src/components/Modal/styles.css
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
.nb-modal {
|
||||||
|
&-title {
|
||||||
|
@apply flex items-center font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-root {
|
||||||
|
@apply fixed z-[1300] inset-0 flex items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-container {
|
||||||
|
@apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 min-w-[400px] lg:min-w-[600px];
|
||||||
|
@apply p-8 opacity-0;
|
||||||
|
@apply transition-opacity duration-[225ms] ease-in-out delay-0;
|
||||||
|
|
||||||
|
&.fade-in {
|
||||||
|
@apply opacity-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fade-out {
|
||||||
|
@apply opacity-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-backdrop {
|
||||||
|
@apply fixed flex right-0 bottom-0 top-0 left-0 bg-transparent opacity-0;
|
||||||
|
@apply transition-all duration-[225ms] ease-in-out delay-0 -z-[1];
|
||||||
|
|
||||||
|
&.fade-in {
|
||||||
|
@apply opacity-100 bg-black/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fade-out {
|
||||||
|
@apply opacity-0 bg-transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
157
website/src/components/Resource/DetailCard/index.tsx
Normal file
157
website/src/components/Resource/DetailCard/index.tsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
// @ts-expect-error: we need to make package have type: module
|
||||||
|
import copy from "copy-text-to-clipboard";
|
||||||
|
|
||||||
|
import { PyPIData } from "./types";
|
||||||
|
|
||||||
|
import Tag from "@/components/Resource/Tag";
|
||||||
|
import type { Resource } from "@/libs/store";
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
resource: Resource;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ResourceDetailCard({ resource }: Props) {
|
||||||
|
const [pypiData, setPypiData] = useState<PyPIData | null>(null);
|
||||||
|
const [copied, setCopied] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const authorLink = `https://github.com/${resource.author}`;
|
||||||
|
const authorAvatar = `${authorLink}.png?size=100`;
|
||||||
|
|
||||||
|
const getProjectLink = (resource: Resource) => {
|
||||||
|
switch (resource.resourceType) {
|
||||||
|
case "plugin":
|
||||||
|
case "adapter":
|
||||||
|
case "driver":
|
||||||
|
return resource.project_link;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getModuleName = (resource: Resource) => {
|
||||||
|
switch (resource.resourceType) {
|
||||||
|
case "plugin":
|
||||||
|
case "adapter":
|
||||||
|
return resource.module_name;
|
||||||
|
case "driver":
|
||||||
|
return resource.module_name.replace(/~/, "nonebot.drivers.");
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchPypiProject = (projectName: string) =>
|
||||||
|
fetch(`https://pypi.org/pypi/${projectName}/json`)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => setPypiData(data));
|
||||||
|
|
||||||
|
const copyCommand = (resource: Resource) => {
|
||||||
|
const projectLink = getProjectLink(resource);
|
||||||
|
if (projectLink) {
|
||||||
|
copy(`nb ${resource.resourceType} install ${projectLink}`);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchingTasks: Promise<void>[] = [];
|
||||||
|
if (resource.resourceType === "bot" || resource.resourceType === "driver")
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (resource.project_link)
|
||||||
|
fetchingTasks.push(fetchPypiProject(resource.project_link));
|
||||||
|
|
||||||
|
Promise.all(fetchingTasks);
|
||||||
|
}, [resource]);
|
||||||
|
|
||||||
|
const projectLink = getProjectLink(resource) || "无";
|
||||||
|
const moduleName = getModuleName(resource) || "无";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="detail-card-header">
|
||||||
|
<img
|
||||||
|
src={authorAvatar}
|
||||||
|
className="detail-card-avatar"
|
||||||
|
decoding="async"
|
||||||
|
></img>
|
||||||
|
<div className="detail-card-title">
|
||||||
|
<span className="detail-card-title-main">{resource.name}</span>
|
||||||
|
<span className="detail-card-title-sub">{resource.author}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="detail-card-copy-button detail-card-copy-button-desktop"
|
||||||
|
onClick={() => copyCommand(resource)}
|
||||||
|
>
|
||||||
|
{copied ? "复制成功" : "复制安装命令"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="detail-card-body">
|
||||||
|
<div className="detail-card-body-left">
|
||||||
|
<span className="h-full">{resource.desc}</span>
|
||||||
|
<div className="resource-card-footer-tags mb-4">
|
||||||
|
{resource.tags.map((tag, index) => (
|
||||||
|
<Tag className="align-bottom" key={index} {...tag} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="detail-card-body-divider" />
|
||||||
|
<div className="detail-card-body-right">
|
||||||
|
<div className="detail-card-meta-item">
|
||||||
|
<span>
|
||||||
|
<FontAwesomeIcon fixedWidth icon={["fab", "python"]} />{" "}
|
||||||
|
{(pypiData && pypiData.info.requires_python) || "无"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="detail-card-meta-item">
|
||||||
|
<FontAwesomeIcon fixedWidth icon={["fas", "file-zipper"]} />{" "}
|
||||||
|
{(pypiData &&
|
||||||
|
pypiData.releases[pypiData.info.version] &&
|
||||||
|
`${
|
||||||
|
pypiData.releases[pypiData.info.version].reduce(
|
||||||
|
(acc, curr) => acc + curr.size,
|
||||||
|
0
|
||||||
|
) / 1000
|
||||||
|
}K`) ||
|
||||||
|
"无"}
|
||||||
|
</div>
|
||||||
|
<div className="detail-card-meta-item">
|
||||||
|
<span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
className="fa-fw"
|
||||||
|
icon={["fas", "scale-balanced"]}
|
||||||
|
/>{" "}
|
||||||
|
{(pypiData && pypiData.info.license) || "无"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="detail-card-meta-item">
|
||||||
|
<FontAwesomeIcon fixedWidth icon={["fas", "tag"]} />{" "}
|
||||||
|
{(pypiData && pypiData.info.version) || "无"}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="detail-card-meta-item">
|
||||||
|
<FontAwesomeIcon fixedWidth icon={["fas", "fingerprint"]} />{" "}
|
||||||
|
<span>{moduleName}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="detail-card-meta-item">
|
||||||
|
<FontAwesomeIcon fixedWidth icon={["fas", "cubes"]} />{" "}
|
||||||
|
<span>{projectLink}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="detail-card-copy-button detail-card-copy-button-mobile w-full"
|
||||||
|
onClick={() => copyCommand(resource)}
|
||||||
|
>
|
||||||
|
{copied ? "复制成功" : "复制安装命令"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
53
website/src/components/Resource/DetailCard/styles.css
Normal file
53
website/src/components/Resource/DetailCard/styles.css
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
.detail-card {
|
||||||
|
&-header {
|
||||||
|
@apply flex items-center align-middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-avatar {
|
||||||
|
@apply mr-3 w-12 h-12 rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
@apply inline-flex flex-col h-12 justify-start;
|
||||||
|
|
||||||
|
&-main {
|
||||||
|
@apply font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-sub {
|
||||||
|
@apply text-sm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-copy-button {
|
||||||
|
@apply ml-auto btn btn-sm;
|
||||||
|
|
||||||
|
&-mobile {
|
||||||
|
@apply lg:hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-desktop {
|
||||||
|
@apply max-lg:hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-body {
|
||||||
|
@apply flex flex-col w-full lg:flex-row;
|
||||||
|
|
||||||
|
&-left {
|
||||||
|
@apply flex flex-col min-h-[150px] lg:basis-3/4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-divider {
|
||||||
|
@apply divider lg:divider-horizontal;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-right {
|
||||||
|
@apply flex flex-col justify-start gap-y-2 lg:basis-1/4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-meta-item {
|
||||||
|
@apply text-sm whitespace-nowrap;
|
||||||
|
}
|
||||||
|
}
|
64
website/src/components/Resource/DetailCard/types.ts
Normal file
64
website/src/components/Resource/DetailCard/types.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
export type Downloads = {
|
||||||
|
last_day: number;
|
||||||
|
last_month: number;
|
||||||
|
last_week: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Info = {
|
||||||
|
author: string;
|
||||||
|
author_email: string;
|
||||||
|
bugtrack_url: null;
|
||||||
|
classifiers: string[];
|
||||||
|
description: string;
|
||||||
|
description_content_type: string;
|
||||||
|
docs_url: null;
|
||||||
|
download_url: string;
|
||||||
|
downloads: Downloads;
|
||||||
|
home_page: string;
|
||||||
|
keywords: string;
|
||||||
|
license: string;
|
||||||
|
maintainer: string;
|
||||||
|
maintainer_email: string;
|
||||||
|
name: string;
|
||||||
|
package_url: string;
|
||||||
|
platform: null;
|
||||||
|
project_url: string;
|
||||||
|
release_url: string;
|
||||||
|
requires_dist: string[];
|
||||||
|
requires_python: string;
|
||||||
|
summary: string;
|
||||||
|
version: string;
|
||||||
|
yanked: boolean;
|
||||||
|
yanked_reason: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Digests {
|
||||||
|
blake2b_256: string;
|
||||||
|
md5: string;
|
||||||
|
sha256: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Releases = {
|
||||||
|
comment_text: string;
|
||||||
|
digests: Digests;
|
||||||
|
downloads: number;
|
||||||
|
filename: string;
|
||||||
|
has_sig: boolean;
|
||||||
|
md5_digest: string;
|
||||||
|
packagetype: string;
|
||||||
|
python_version: string;
|
||||||
|
requires_python: string;
|
||||||
|
size: number;
|
||||||
|
upload_time: Date;
|
||||||
|
upload_time_iso_8601: Date;
|
||||||
|
url: string;
|
||||||
|
yanked: boolean;
|
||||||
|
yanked_reason: null;
|
||||||
|
};
|
||||||
|
export type PyPIData = {
|
||||||
|
info: Info;
|
||||||
|
last_serial: number;
|
||||||
|
releases: { [key: string]: Releases[] };
|
||||||
|
urls: URL[];
|
||||||
|
vulnerabilities: unknown[];
|
||||||
|
};
|
@ -5,8 +5,11 @@ import { usePagination } from "react-use-pagination";
|
|||||||
|
|
||||||
import Admonition from "@theme/Admonition";
|
import Admonition from "@theme/Admonition";
|
||||||
|
|
||||||
|
import AdapterForm from "@/components/Form/Adapter";
|
||||||
|
import Modal from "@/components/Modal";
|
||||||
import Paginate from "@/components/Paginate";
|
import Paginate from "@/components/Paginate";
|
||||||
import ResourceCard from "@/components/Resource/Card";
|
import ResourceCard from "@/components/Resource/Card";
|
||||||
|
import ResourceDetailCard from "@/components/Resource/DetailCard";
|
||||||
import Searcher from "@/components/Searcher";
|
import Searcher from "@/components/Searcher";
|
||||||
import StoreToolbar, { type Action } from "@/components/Store/Toolbar";
|
import StoreToolbar, { type Action } from "@/components/Store/Toolbar";
|
||||||
import { authorFilter, tagFilter } from "@/libs/filter";
|
import { authorFilter, tagFilter } from "@/libs/filter";
|
||||||
@ -21,6 +24,9 @@ export default function AdapterPage(): JSX.Element {
|
|||||||
const loading = adapters === null;
|
const loading = adapters === null;
|
||||||
|
|
||||||
const [error, setError] = useState<Error | null>(null);
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const [isOpenModal, setIsOpenModal] = useState<boolean>(false);
|
||||||
|
const [isOpenCardModal, setIsOpenCardModal] = useState<boolean>(false);
|
||||||
|
const [clickedAdapter, setClickedAdapter] = useState<Adapter | null>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
filteredResources: filteredAdapters,
|
filteredResources: filteredAdapters,
|
||||||
@ -69,16 +75,13 @@ export default function AdapterPage(): JSX.Element {
|
|||||||
label: "发布适配器",
|
label: "发布适配器",
|
||||||
icon: ["fas", "plus"],
|
icon: ["fas", "plus"],
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
// TODO: open adapter release modal
|
setIsOpenModal(true);
|
||||||
window.open(
|
|
||||||
"https://github.com/nonebot/nonebot2/issues/new?template=adapter_publish.yml&title=Adapter%3A+%7Bname%7D&labels=Adapter"
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCardClick = useCallback((adapter: Adapter) => {
|
const onCardClick = useCallback((adapter: Adapter) => {
|
||||||
// TODO: open adapter modal
|
setClickedAdapter(adapter);
|
||||||
console.log(adapter, "clicked");
|
setIsOpenCardModal(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onCardTagClick = useCallback(
|
const onCardTagClick = useCallback(
|
||||||
@ -170,6 +173,25 @@ export default function AdapterPage(): JSX.Element {
|
|||||||
nextEnabled={nextEnabled}
|
nextEnabled={nextEnabled}
|
||||||
previousEnabled={previousEnabled}
|
previousEnabled={previousEnabled}
|
||||||
/>
|
/>
|
||||||
|
{isOpenModal && (
|
||||||
|
<Modal
|
||||||
|
className="not-prose"
|
||||||
|
title="发布适配器"
|
||||||
|
setOpenModal={setIsOpenModal}
|
||||||
|
>
|
||||||
|
<AdapterForm />
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{isOpenCardModal && (
|
||||||
|
<Modal
|
||||||
|
className="not-prose"
|
||||||
|
title="适配器详情"
|
||||||
|
backdropExit
|
||||||
|
setOpenModal={setIsOpenCardModal}
|
||||||
|
>
|
||||||
|
{clickedAdapter && <ResourceDetailCard resource={clickedAdapter} />}
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ import { usePagination } from "react-use-pagination";
|
|||||||
|
|
||||||
import Admonition from "@theme/Admonition";
|
import Admonition from "@theme/Admonition";
|
||||||
|
|
||||||
|
import BotForm from "@/components/Form/Bot";
|
||||||
|
import Modal from "@/components/Modal";
|
||||||
import Paginate from "@/components/Paginate";
|
import Paginate from "@/components/Paginate";
|
||||||
import ResourceCard from "@/components/Resource/Card";
|
import ResourceCard from "@/components/Resource/Card";
|
||||||
import Searcher from "@/components/Searcher";
|
import Searcher from "@/components/Searcher";
|
||||||
@ -21,6 +23,7 @@ export default function PluginPage(): JSX.Element {
|
|||||||
const loading = bots === null;
|
const loading = bots === null;
|
||||||
|
|
||||||
const [error, setError] = useState<Error | null>(null);
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const [isOpenModal, setIsOpenModal] = useState<boolean>(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
filteredResources: filteredBots,
|
filteredResources: filteredBots,
|
||||||
@ -69,10 +72,7 @@ export default function PluginPage(): JSX.Element {
|
|||||||
label: "发布机器人",
|
label: "发布机器人",
|
||||||
icon: ["fas", "plus"],
|
icon: ["fas", "plus"],
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
// TODO: open bot release modal
|
setIsOpenModal(true);
|
||||||
window.open(
|
|
||||||
"https://github.com/nonebot/nonebot2/issues/new?template=bot_publish.yml&title=Bot%3A+%7Bname%7D&labels=Bot"
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -164,6 +164,15 @@ export default function PluginPage(): JSX.Element {
|
|||||||
nextEnabled={nextEnabled}
|
nextEnabled={nextEnabled}
|
||||||
previousEnabled={previousEnabled}
|
previousEnabled={previousEnabled}
|
||||||
/>
|
/>
|
||||||
|
{isOpenModal && (
|
||||||
|
<Modal
|
||||||
|
className="not-prose"
|
||||||
|
title="发布机器人"
|
||||||
|
setOpenModal={setIsOpenModal}
|
||||||
|
>
|
||||||
|
<BotForm />
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,10 @@ import { usePagination } from "react-use-pagination";
|
|||||||
|
|
||||||
import Admonition from "@theme/Admonition";
|
import Admonition from "@theme/Admonition";
|
||||||
|
|
||||||
|
import Modal from "@/components/Modal";
|
||||||
import Paginate from "@/components/Paginate";
|
import Paginate from "@/components/Paginate";
|
||||||
import ResourceCard from "@/components/Resource/Card";
|
import ResourceCard from "@/components/Resource/Card";
|
||||||
|
import ResourceDetailCard from "@/components/Resource/DetailCard";
|
||||||
import Searcher from "@/components/Searcher";
|
import Searcher from "@/components/Searcher";
|
||||||
import { authorFilter, tagFilter } from "@/libs/filter";
|
import { authorFilter, tagFilter } from "@/libs/filter";
|
||||||
import { useSearchControl } from "@/libs/search";
|
import { useSearchControl } from "@/libs/search";
|
||||||
@ -19,6 +21,8 @@ export default function DriverPage(): JSX.Element {
|
|||||||
const loading = drivers === null;
|
const loading = drivers === null;
|
||||||
|
|
||||||
const [error, setError] = useState<Error | null>(null);
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const [isOpenCardModal, setIsOpenCardModal] = useState<boolean>(false);
|
||||||
|
const [clickedDriver, setClickedDriver] = useState<Driver | null>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
filteredResources: filteredDrivers,
|
filteredResources: filteredDrivers,
|
||||||
@ -59,8 +63,8 @@ export default function DriverPage(): JSX.Element {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onCardClick = useCallback((driver: Driver) => {
|
const onCardClick = useCallback((driver: Driver) => {
|
||||||
// TODO: open driver modal
|
setClickedDriver(driver);
|
||||||
console.log(driver, "clicked");
|
setIsOpenCardModal(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onCardTagClick = useCallback(
|
const onCardTagClick = useCallback(
|
||||||
@ -146,6 +150,17 @@ export default function DriverPage(): JSX.Element {
|
|||||||
nextEnabled={nextEnabled}
|
nextEnabled={nextEnabled}
|
||||||
previousEnabled={previousEnabled}
|
previousEnabled={previousEnabled}
|
||||||
/>
|
/>
|
||||||
|
{isOpenCardModal && (
|
||||||
|
<Modal
|
||||||
|
className="not-prose"
|
||||||
|
useCustomTitle
|
||||||
|
backdropExit
|
||||||
|
title="驱动器详情"
|
||||||
|
setOpenModal={setIsOpenCardModal}
|
||||||
|
>
|
||||||
|
{clickedDriver && <ResourceDetailCard resource={clickedDriver} />}
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,11 @@ import { usePagination } from "react-use-pagination";
|
|||||||
|
|
||||||
import Admonition from "@theme/Admonition";
|
import Admonition from "@theme/Admonition";
|
||||||
|
|
||||||
|
import PluginForm from "@/components/Form/Plugin";
|
||||||
|
import Modal from "@/components/Modal";
|
||||||
import Paginate from "@/components/Paginate";
|
import Paginate from "@/components/Paginate";
|
||||||
import ResourceCard from "@/components/Resource/Card";
|
import ResourceCard from "@/components/Resource/Card";
|
||||||
|
import ResourceDetailCard from "@/components/Resource/DetailCard";
|
||||||
import Searcher from "@/components/Searcher";
|
import Searcher from "@/components/Searcher";
|
||||||
import StoreToolbar, { type Action } from "@/components/Store/Toolbar";
|
import StoreToolbar, { type Action } from "@/components/Store/Toolbar";
|
||||||
import { authorFilter, tagFilter } from "@/libs/filter";
|
import { authorFilter, tagFilter } from "@/libs/filter";
|
||||||
@ -21,6 +24,9 @@ export default function PluginPage(): JSX.Element {
|
|||||||
const loading = plugins === null;
|
const loading = plugins === null;
|
||||||
|
|
||||||
const [error, setError] = useState<Error | null>(null);
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const [isOpenModal, setIsOpenModal] = useState<boolean>(false);
|
||||||
|
const [isOpenCardModal, setIsOpenCardModal] = useState<boolean>(false);
|
||||||
|
const [clickedPlugin, setClickedPlugin] = useState<Plugin | null>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
filteredResources: filteredPlugins,
|
filteredResources: filteredPlugins,
|
||||||
@ -69,16 +75,13 @@ export default function PluginPage(): JSX.Element {
|
|||||||
label: "发布插件",
|
label: "发布插件",
|
||||||
icon: ["fas", "plus"],
|
icon: ["fas", "plus"],
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
// TODO: open plugin release modal
|
setIsOpenModal(true);
|
||||||
window.open(
|
|
||||||
"https://github.com/nonebot/nonebot2/issues/new?template=plugin_publish.yml&title=Plugin%3A+%7Bname%7D&labels=Plugin"
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCardClick = useCallback((plugin: Plugin) => {
|
const onCardClick = useCallback((plugin: Plugin) => {
|
||||||
// TODO: open plugin modal
|
setClickedPlugin(plugin);
|
||||||
console.log(plugin, "clicked");
|
setIsOpenCardModal(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onCardTagClick = useCallback(
|
const onCardTagClick = useCallback(
|
||||||
@ -167,6 +170,26 @@ export default function PluginPage(): JSX.Element {
|
|||||||
nextEnabled={nextEnabled}
|
nextEnabled={nextEnabled}
|
||||||
previousEnabled={previousEnabled}
|
previousEnabled={previousEnabled}
|
||||||
/>
|
/>
|
||||||
|
{isOpenModal && (
|
||||||
|
<Modal
|
||||||
|
className="not-prose"
|
||||||
|
title="发布插件"
|
||||||
|
setOpenModal={setIsOpenModal}
|
||||||
|
>
|
||||||
|
<PluginForm />
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{isOpenCardModal && (
|
||||||
|
<Modal
|
||||||
|
className="not-prose"
|
||||||
|
useCustomTitle
|
||||||
|
backdropExit
|
||||||
|
title="插件详情"
|
||||||
|
setOpenModal={setIsOpenCardModal}
|
||||||
|
>
|
||||||
|
{clickedPlugin && <ResourceDetailCard resource={clickedPlugin} />}
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
31
website/src/components/Tag/index.tsx
Normal file
31
website/src/components/Tag/index.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
import { pickTextColor } from "@/libs/color";
|
||||||
|
import { Tag as TagType } from "@/types/tag";
|
||||||
|
|
||||||
|
export default function Tag({
|
||||||
|
label,
|
||||||
|
color,
|
||||||
|
className,
|
||||||
|
onClick,
|
||||||
|
}: TagType & {
|
||||||
|
className?: string;
|
||||||
|
onClick?: React.MouseEventHandler<HTMLSpanElement>;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={clsx("tag", className)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
color: pickTextColor(color, "#fff", "#000"),
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
3
website/src/components/Tag/styles.css
Normal file
3
website/src/components/Tag/styles.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.tag {
|
||||||
|
@apply font-mono inline-flex px-3 rounded-full items-center align-middle;
|
||||||
|
}
|
15
yarn.lock
15
yarn.lock
@ -2307,6 +2307,14 @@
|
|||||||
resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz#38bd1733ae299620771bd414837ade2e57757498"
|
resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz#38bd1733ae299620771bd414837ade2e57757498"
|
||||||
integrity sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==
|
integrity sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==
|
||||||
|
|
||||||
|
"@types/react-color@^3.0.10":
|
||||||
|
version "3.0.10"
|
||||||
|
resolved "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.10.tgz#f869ab3a46938fb97a5c2ee568bc6469f82b14dd"
|
||||||
|
integrity sha512-6K5BAn3zyd8lW8UbckIAVeXGxR82Za9jyGD2DBEynsa7fKaguLDVtjfypzs7fgEV7bULgs7uhds8A8v1wABTvQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
"@types/reactcss" "*"
|
||||||
|
|
||||||
"@types/react-router-config@*", "@types/react-router-config@^5.0.6":
|
"@types/react-router-config@*", "@types/react-router-config@^5.0.6":
|
||||||
version "5.0.8"
|
version "5.0.8"
|
||||||
resolved "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.8.tgz#dd00654de4d79927570a4a8807c4a728feed59f3"
|
resolved "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.8.tgz#dd00654de4d79927570a4a8807c4a728feed59f3"
|
||||||
@ -2342,6 +2350,13 @@
|
|||||||
"@types/scheduler" "*"
|
"@types/scheduler" "*"
|
||||||
csstype "^3.0.2"
|
csstype "^3.0.2"
|
||||||
|
|
||||||
|
"@types/reactcss@*":
|
||||||
|
version "1.2.9"
|
||||||
|
resolved "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.9.tgz#e3ddff5ee18c02e62cdef3588dd408203b275610"
|
||||||
|
integrity sha512-dN2TtynLIZaZZ4gQNK6WM0Nff8GWYCXKl1Kvsp59WgROtx03ixCwuC1UWdesgt2O1P5Qk+0+SIfsy3eiwblMEA==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/retry@0.12.0":
|
"@types/retry@0.12.0":
|
||||||
version "0.12.0"
|
version "0.12.0"
|
||||||
resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
|
resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
|
||||||
|
Loading…
Reference in New Issue
Block a user