diff --git a/website/package.json b/website/package.json index bc92fd51..3294ee42 100644 --- a/website/package.json +++ b/website/package.json @@ -37,6 +37,7 @@ "devDependencies": { "@docusaurus/module-type-aliases": "^2.4.1", "@tsconfig/docusaurus": "^1.0.5", + "@types/react-color": "^3.0.10", "asciinema-player": "^3.5.0", "typescript": "^4.7.4" }, diff --git a/website/src/components/Form/Adapter.tsx b/website/src/components/Form/Adapter.tsx new file mode 100644 index 00000000..364f3b77 --- /dev/null +++ b/website/src/components/Form/Adapter.tsx @@ -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) => { + 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 ( +
+ ); +} diff --git a/website/src/components/Form/Bot.tsx b/website/src/components/Form/Bot.tsx new file mode 100644 index 00000000..4ea5299a --- /dev/null +++ b/website/src/components/Form/Bot.tsx @@ -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) => { + 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 ; +} diff --git a/website/src/components/Form/Items/Tag/index.tsx b/website/src/components/Form/Items/Tag/index.tsx new file mode 100644 index 00000000..97ec0a0b --- /dev/null +++ b/website/src/components/Form/Items/Tag/index.tsx @@ -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([]); + const [label, setLabel] = useState(""); + const [color, setColor] = useState("#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 ( + <> + +
+ 标签名称 +
+ setLabel(e.target.value)} + /> + {slicedTags.length > 0 && ( + + )} +
+
+
+ 标签颜色 + +
+ + ); +} diff --git a/website/src/components/Form/Items/Tag/styles.css b/website/src/components/Form/Items/Tag/styles.css new file mode 100644 index 00000000..a6b66cee --- /dev/null +++ b/website/src/components/Form/Items/Tag/styles.css @@ -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; + } +} diff --git a/website/src/components/Form/Plugin.tsx b/website/src/components/Form/Plugin.tsx new file mode 100644 index 00000000..a149ea14 --- /dev/null +++ b/website/src/components/Form/Plugin.tsx @@ -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) => { + 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 ( + + ); +} diff --git a/website/src/components/Form/index.tsx b/website/src/components/Form/index.tsx new file mode 100644 index 00000000..bd844275 --- /dev/null +++ b/website/src/components/Form/index.tsx @@ -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) => void; +}; + +export function Form({ + type, + children, + formItems, + handleSubmit, +}: Props): JSX.Element { + const [currentStep, setCurrentStep] = useState(0); + const [result, setResult] = useState>({}); + const [allowTags, setAllowTags] = useState([]); + + // 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 ( + <> +
    + {formItems.map((item, index) => ( +
  • + {item.name} +
  • + ))} +
+
+ {children || + formItems[currentStep].items.map((item) => ( + + ))} +
+
+ + +
+ + ); +} + +export function FormItem({ + type, + name, + labelText, + allowTags, + result, + setResult, +}: FormItemData & { + allowTags: TagType[]; + result: Record; + setResult: (key: string, value: string) => void; +}): JSX.Element { + return ( + <> + + {type === "text" && ( + setResult(name, e.target.value)} + placeholder="请输入" + className={clsx("form-input", { + "form-input-error": !result[name], + })} + /> + )} + {type === "text" && !result[name] && ( + + )} + {type === "tag" && ( + setResult(name, JSON.stringify(tags))} + /> + )} + + ); +} diff --git a/website/src/components/Form/styles.css b/website/src/components/Form/styles.css new file mode 100644 index 00000000..b85fc99f --- /dev/null +++ b/website/src/components/Form/styles.css @@ -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; + } +} diff --git a/website/src/components/Modal/index.tsx b/website/src/components/Modal/index.tsx new file mode 100644 index 00000000..97db5858 --- /dev/null +++ b/website/src/components/Modal/index.tsx @@ -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(""); + + const onFadeIn = () => setTransitionClass("fade-in"); + const onFadeOut = () => setTransitionClass("fade-out"); + const onTransitionEnd = () => + transitionClass === "fade-out" && setOpenModal(false); + + useEffect(onFadeIn, []); + + return ( +
+
backdropExit && onFadeOut()} + /> +
+
+
+ {!useCustomTitle && ( +
+ {title} +
+ +
+
+ )} + {children} +
+
+
+
+ ); +} diff --git a/website/src/components/Modal/styles.css b/website/src/components/Modal/styles.css new file mode 100644 index 00000000..e941029e --- /dev/null +++ b/website/src/components/Modal/styles.css @@ -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; + } + } +} diff --git a/website/src/components/Resource/DetailCard/index.tsx b/website/src/components/Resource/DetailCard/index.tsx new file mode 100644 index 00000000..c0cbfeed --- /dev/null +++ b/website/src/components/Resource/DetailCard/index.tsx @@ -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(null); + const [copied, setCopied] = useState(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[] = []; + 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 ( + <> +
+ +
+ {resource.name} + {resource.author} +
+ +
+
+
+ {resource.desc} +
+ {resource.tags.map((tag, index) => ( + + ))} +
+
+
+
+
+ + {" "} + {(pypiData && pypiData.info.requires_python) || "无"} + +
+
+ {" "} + {(pypiData && + pypiData.releases[pypiData.info.version] && + `${ + pypiData.releases[pypiData.info.version].reduce( + (acc, curr) => acc + curr.size, + 0 + ) / 1000 + }K`) || + "无"} +
+
+ + {" "} + {(pypiData && pypiData.info.license) || "无"} + +
+
+ {" "} + {(pypiData && pypiData.info.version) || "无"} +
+ +
+ {" "} + {moduleName} +
+ +
+ {" "} + {projectLink} +
+ +
+
+ + ); +} diff --git a/website/src/components/Resource/DetailCard/styles.css b/website/src/components/Resource/DetailCard/styles.css new file mode 100644 index 00000000..47e66e26 --- /dev/null +++ b/website/src/components/Resource/DetailCard/styles.css @@ -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; + } +} diff --git a/website/src/components/Resource/DetailCard/types.ts b/website/src/components/Resource/DetailCard/types.ts new file mode 100644 index 00000000..c0fec8f6 --- /dev/null +++ b/website/src/components/Resource/DetailCard/types.ts @@ -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[]; +}; diff --git a/website/src/components/Store/Content/Adapter.tsx b/website/src/components/Store/Content/Adapter.tsx index 5d1c938b..0cb4caa5 100644 --- a/website/src/components/Store/Content/Adapter.tsx +++ b/website/src/components/Store/Content/Adapter.tsx @@ -5,8 +5,11 @@ import { usePagination } from "react-use-pagination"; import Admonition from "@theme/Admonition"; +import AdapterForm from "@/components/Form/Adapter"; +import Modal from "@/components/Modal"; import Paginate from "@/components/Paginate"; import ResourceCard from "@/components/Resource/Card"; +import ResourceDetailCard from "@/components/Resource/DetailCard"; import Searcher from "@/components/Searcher"; import StoreToolbar, { type Action } from "@/components/Store/Toolbar"; import { authorFilter, tagFilter } from "@/libs/filter"; @@ -21,6 +24,9 @@ export default function AdapterPage(): JSX.Element { const loading = adapters === null; const [error, setError] = useState(null); + const [isOpenModal, setIsOpenModal] = useState(false); + const [isOpenCardModal, setIsOpenCardModal] = useState(false); + const [clickedAdapter, setClickedAdapter] = useState(null); const { filteredResources: filteredAdapters, @@ -69,16 +75,13 @@ export default function AdapterPage(): JSX.Element { label: "发布适配器", icon: ["fas", "plus"], onClick: () => { - // TODO: open adapter release modal - window.open( - "https://github.com/nonebot/nonebot2/issues/new?template=adapter_publish.yml&title=Adapter%3A+%7Bname%7D&labels=Adapter" - ); + setIsOpenModal(true); }, }; const onCardClick = useCallback((adapter: Adapter) => { - // TODO: open adapter modal - console.log(adapter, "clicked"); + setClickedAdapter(adapter); + setIsOpenCardModal(true); }, []); const onCardTagClick = useCallback( @@ -170,6 +173,25 @@ export default function AdapterPage(): JSX.Element { nextEnabled={nextEnabled} previousEnabled={previousEnabled} /> + {isOpenModal && ( + + + + )} + {isOpenCardModal && ( + + {clickedAdapter && } + + )} ); } diff --git a/website/src/components/Store/Content/Bot.tsx b/website/src/components/Store/Content/Bot.tsx index a238861e..299e8087 100644 --- a/website/src/components/Store/Content/Bot.tsx +++ b/website/src/components/Store/Content/Bot.tsx @@ -5,6 +5,8 @@ import { usePagination } from "react-use-pagination"; import Admonition from "@theme/Admonition"; +import BotForm from "@/components/Form/Bot"; +import Modal from "@/components/Modal"; import Paginate from "@/components/Paginate"; import ResourceCard from "@/components/Resource/Card"; import Searcher from "@/components/Searcher"; @@ -21,6 +23,7 @@ export default function PluginPage(): JSX.Element { const loading = bots === null; const [error, setError] = useState(null); + const [isOpenModal, setIsOpenModal] = useState(false); const { filteredResources: filteredBots, @@ -69,10 +72,7 @@ export default function PluginPage(): JSX.Element { label: "发布机器人", icon: ["fas", "plus"], onClick: () => { - // TODO: open bot release modal - window.open( - "https://github.com/nonebot/nonebot2/issues/new?template=bot_publish.yml&title=Bot%3A+%7Bname%7D&labels=Bot" - ); + setIsOpenModal(true); }, }; @@ -164,6 +164,15 @@ export default function PluginPage(): JSX.Element { nextEnabled={nextEnabled} previousEnabled={previousEnabled} /> + {isOpenModal && ( + + + + )} ); } diff --git a/website/src/components/Store/Content/Driver.tsx b/website/src/components/Store/Content/Driver.tsx index 6b32bf1b..501c7f4e 100644 --- a/website/src/components/Store/Content/Driver.tsx +++ b/website/src/components/Store/Content/Driver.tsx @@ -5,8 +5,10 @@ import { usePagination } from "react-use-pagination"; import Admonition from "@theme/Admonition"; +import Modal from "@/components/Modal"; import Paginate from "@/components/Paginate"; import ResourceCard from "@/components/Resource/Card"; +import ResourceDetailCard from "@/components/Resource/DetailCard"; import Searcher from "@/components/Searcher"; import { authorFilter, tagFilter } from "@/libs/filter"; import { useSearchControl } from "@/libs/search"; @@ -19,6 +21,8 @@ export default function DriverPage(): JSX.Element { const loading = drivers === null; const [error, setError] = useState(null); + const [isOpenCardModal, setIsOpenCardModal] = useState(false); + const [clickedDriver, setClickedDriver] = useState(null); const { filteredResources: filteredDrivers, @@ -59,8 +63,8 @@ export default function DriverPage(): JSX.Element { }, []); const onCardClick = useCallback((driver: Driver) => { - // TODO: open driver modal - console.log(driver, "clicked"); + setClickedDriver(driver); + setIsOpenCardModal(true); }, []); const onCardTagClick = useCallback( @@ -146,6 +150,17 @@ export default function DriverPage(): JSX.Element { nextEnabled={nextEnabled} previousEnabled={previousEnabled} /> + {isOpenCardModal && ( + + {clickedDriver && } + + )} ); } diff --git a/website/src/components/Store/Content/Plugin.tsx b/website/src/components/Store/Content/Plugin.tsx index 607dd2f7..0cda530f 100644 --- a/website/src/components/Store/Content/Plugin.tsx +++ b/website/src/components/Store/Content/Plugin.tsx @@ -5,8 +5,11 @@ import { usePagination } from "react-use-pagination"; import Admonition from "@theme/Admonition"; +import PluginForm from "@/components/Form/Plugin"; +import Modal from "@/components/Modal"; import Paginate from "@/components/Paginate"; import ResourceCard from "@/components/Resource/Card"; +import ResourceDetailCard from "@/components/Resource/DetailCard"; import Searcher from "@/components/Searcher"; import StoreToolbar, { type Action } from "@/components/Store/Toolbar"; import { authorFilter, tagFilter } from "@/libs/filter"; @@ -21,6 +24,9 @@ export default function PluginPage(): JSX.Element { const loading = plugins === null; const [error, setError] = useState(null); + const [isOpenModal, setIsOpenModal] = useState(false); + const [isOpenCardModal, setIsOpenCardModal] = useState(false); + const [clickedPlugin, setClickedPlugin] = useState(null); const { filteredResources: filteredPlugins, @@ -69,16 +75,13 @@ export default function PluginPage(): JSX.Element { label: "发布插件", icon: ["fas", "plus"], onClick: () => { - // TODO: open plugin release modal - window.open( - "https://github.com/nonebot/nonebot2/issues/new?template=plugin_publish.yml&title=Plugin%3A+%7Bname%7D&labels=Plugin" - ); + setIsOpenModal(true); }, }; const onCardClick = useCallback((plugin: Plugin) => { - // TODO: open plugin modal - console.log(plugin, "clicked"); + setClickedPlugin(plugin); + setIsOpenCardModal(true); }, []); const onCardTagClick = useCallback( @@ -167,6 +170,26 @@ export default function PluginPage(): JSX.Element { nextEnabled={nextEnabled} previousEnabled={previousEnabled} /> + {isOpenModal && ( + + + + )} + {isOpenCardModal && ( + + {clickedPlugin && } + + )} ); } diff --git a/website/src/components/Tag/index.tsx b/website/src/components/Tag/index.tsx new file mode 100644 index 00000000..740773b2 --- /dev/null +++ b/website/src/components/Tag/index.tsx @@ -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; +}): JSX.Element { + return ( + + {label} + + ); +} diff --git a/website/src/components/Tag/styles.css b/website/src/components/Tag/styles.css new file mode 100644 index 00000000..908714c1 --- /dev/null +++ b/website/src/components/Tag/styles.css @@ -0,0 +1,3 @@ +.tag { + @apply font-mono inline-flex px-3 rounded-full items-center align-middle; +} diff --git a/yarn.lock b/yarn.lock index 43ccda1b..29746924 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2307,6 +2307,14 @@ resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz#38bd1733ae299620771bd414837ade2e57757498" 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": version "5.0.8" resolved "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.8.tgz#dd00654de4d79927570a4a8807c4a728feed59f3" @@ -2342,6 +2350,13 @@ "@types/scheduler" "*" 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": version "0.12.0" resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"