mirror of
				https://github.com/nonebot/nonebot2.git
				synced 2025-10-31 15:06:42 +00:00 
			
		
		
		
	📝 Docs: 升级新版 NonePress 主题 (#2375)
This commit is contained in:
		| @@ -1,260 +0,0 @@ | ||||
| import clsx from "clsx"; | ||||
| import React, { useRef, useState } from "react"; | ||||
| import { ChromePicker } from "react-color"; | ||||
| import { usePagination } from "react-use-pagination"; | ||||
|  | ||||
| import adapters from "../../static/adapters.json"; | ||||
| import { Tag, useFilteredObjs } from "../libs/store"; | ||||
| import Card from "./Card"; | ||||
| import Modal from "./Modal"; | ||||
| import ModalAction from "./ModalAction"; | ||||
| import ModalContent from "./ModalContent"; | ||||
| import ModalTitle from "./ModalTitle"; | ||||
| import Paginate from "./Paginate"; | ||||
| import TagComponent from "./Tag"; | ||||
|  | ||||
| export default function Adapter(): JSX.Element { | ||||
|   const [modalOpen, setModalOpen] = useState<boolean>(false); | ||||
|   const { | ||||
|     filter, | ||||
|     setFilter, | ||||
|     filteredObjs: filteredAdapters, | ||||
|   } = useFilteredObjs(adapters); | ||||
|  | ||||
|   const props = usePagination({ | ||||
|     totalItems: filteredAdapters.length, | ||||
|     initialPageSize: 10, | ||||
|   }); | ||||
|   const { startIndex, endIndex } = props; | ||||
|   const currentAdapters = filteredAdapters.slice(startIndex, endIndex + 1); | ||||
|  | ||||
|   const [form, setForm] = useState<{ | ||||
|     name: string; | ||||
|     desc: string; | ||||
|     projectLink: string; | ||||
|     moduleName: string; | ||||
|     homepage: string; | ||||
|   }>({ name: "", desc: "", projectLink: "", moduleName: "", homepage: "" }); | ||||
|  | ||||
|   const ref = useRef<HTMLInputElement>(null); | ||||
|   const [tags, setTags] = useState<Tag[]>([]); | ||||
|   const [label, setLabel] = useState<string>(""); | ||||
|   const [color, setColor] = useState<string>("#ea5252"); | ||||
|  | ||||
|   const urlEncode = (str: string) => | ||||
|     encodeURIComponent(str).replace(/%2B/gi, "+"); | ||||
|  | ||||
|   const onSubmit = () => { | ||||
|     setModalOpen(false); | ||||
|     const queries: { key: string; value: string }[] = [ | ||||
|       { key: "template", value: "adapter_publish.yml" }, | ||||
|       { key: "title", value: form.name && `Adapter: ${form.name}` }, | ||||
|       { key: "labels", value: "Adapter" }, | ||||
|       { key: "name", value: form.name }, | ||||
|       { key: "description", value: form.desc }, | ||||
|       { key: "pypi", value: form.projectLink }, | ||||
|       { key: "module", value: form.moduleName }, | ||||
|       { key: "homepage", value: form.homepage }, | ||||
|       { key: "tags", value: JSON.stringify(tags) }, | ||||
|     ]; | ||||
|     const urlQueries = queries | ||||
|       .filter((query) => !!query.value) | ||||
|       .map((query) => `${query.key}=${urlEncode(query.value)}`) | ||||
|       .join("&"); | ||||
|     window.open(`https://github.com/nonebot/nonebot2/issues/new?${urlQueries}`); | ||||
|   }; | ||||
|   const onChange = (event) => { | ||||
|     const target = event.target; | ||||
|     const value = target.type === "checkbox" ? target.checked : target.value; | ||||
|     const name = target.name; | ||||
|  | ||||
|     setForm({ | ||||
|       ...form, | ||||
|       [name]: value, | ||||
|     }); | ||||
|     event.preventDefault(); | ||||
|   }; | ||||
|   const onChangeLabel = (event) => { | ||||
|     setLabel(event.target.value); | ||||
|   }; | ||||
|   const onChangeColor = (color) => { | ||||
|     setColor(color.hex); | ||||
|   }; | ||||
|   const validateTag = () => { | ||||
|     return label.length >= 1 && label.length <= 10; | ||||
|   }; | ||||
|   const newTag = () => { | ||||
|     if (tags.length >= 3) { | ||||
|       return; | ||||
|     } | ||||
|     if (validateTag()) { | ||||
|       const tag = { label, color }; | ||||
|       setTags([...tags, tag]); | ||||
|     } | ||||
|   }; | ||||
|   const delTag = (index: number) => { | ||||
|     setTags(tags.filter((_, i) => i !== index)); | ||||
|   }; | ||||
|   const insertTagType = (text: string) => { | ||||
|     setLabel(text + label); | ||||
|     ref.current.value = text + label; | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4 px-4"> | ||||
|         <input | ||||
|           className="w-full px-4 py-2 border rounded-full bg-light-nonepress-100 dark:bg-dark-nonepress-100" | ||||
|           value={filter} | ||||
|           placeholder="搜索适配器" | ||||
|           onChange={(event) => setFilter(event.target.value)} | ||||
|         /> | ||||
|         <button | ||||
|           className="w-full rounded-lg bg-hero text-white" | ||||
|           onClick={() => setModalOpen(true)} | ||||
|         > | ||||
|           发布适配器 | ||||
|         </button> | ||||
|       </div> | ||||
|       <div className="grid grid-cols-1 p-4"> | ||||
|         <Paginate {...props} /> | ||||
|       </div> | ||||
|       <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-4"> | ||||
|         {currentAdapters.map((adapter, index) => ( | ||||
|           <Card | ||||
|             key={index} | ||||
|             {...adapter} | ||||
|             action={`nb adapter install ${adapter.project_link}`} | ||||
|             actionDisabled={!adapter.project_link} | ||||
|           /> | ||||
|         ))} | ||||
|       </div> | ||||
|       <div className="grid grid-cols-1 p-4"> | ||||
|         <Paginate {...props} /> | ||||
|       </div> | ||||
|       <Modal active={modalOpen} setActive={setModalOpen}> | ||||
|         <ModalTitle title={"适配器信息"} /> | ||||
|         <ModalContent> | ||||
|           <form onSubmit={onSubmit}> | ||||
|             <div className="grid grid-cols-1 gap-4 p-4"> | ||||
|               <label className="flex flex-wrap"> | ||||
|                 <span className="mr-2">适配器名称:</span> | ||||
|                 <input | ||||
|                   type="text" | ||||
|                   name="name" | ||||
|                   maxLength={20} | ||||
|                   className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200" | ||||
|                   onChange={onChange} | ||||
|                 /> | ||||
|               </label> | ||||
|               <label className="flex flex-wrap"> | ||||
|                 <span className="mr-2">适配器介绍:</span> | ||||
|                 <input | ||||
|                   type="text" | ||||
|                   name="desc" | ||||
|                   className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200" | ||||
|                   onChange={onChange} | ||||
|                 /> | ||||
|               </label> | ||||
|               <label className="flex flex-wrap"> | ||||
|                 <span className="mr-2">PyPI 项目名:</span> | ||||
|                 <input | ||||
|                   type="text" | ||||
|                   name="projectLink" | ||||
|                   className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200" | ||||
|                   onChange={onChange} | ||||
|                 /> | ||||
|               </label> | ||||
|               <label className="flex flex-wrap"> | ||||
|                 <span className="mr-2">import 包名:</span> | ||||
|                 <input | ||||
|                   type="text" | ||||
|                   name="moduleName" | ||||
|                   className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200" | ||||
|                   onChange={onChange} | ||||
|                 /> | ||||
|               </label> | ||||
|               <label className="flex flex-wrap"> | ||||
|                 <span className="mr-2">仓库/主页:</span> | ||||
|                 <input | ||||
|                   type="text" | ||||
|                   name="homepage" | ||||
|                   className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200" | ||||
|                   onChange={onChange} | ||||
|                 /> | ||||
|               </label> | ||||
|             </div> | ||||
|           </form> | ||||
|           <div className="px-4"> | ||||
|             <label className="flex flex-wrap"> | ||||
|               <span className="mr-2">标签:</span> | ||||
|               {tags.map((tag, index) => ( | ||||
|                 <TagComponent | ||||
|                   key={index} | ||||
|                   {...tag} | ||||
|                   className="cursor-pointer" | ||||
|                   onClick={() => delTag(index)} | ||||
|                 /> | ||||
|               ))} | ||||
|             </label> | ||||
|           </div> | ||||
|           <div className="px-4 pt-4"> | ||||
|             <input | ||||
|               ref={ref} | ||||
|               type="text" | ||||
|               className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200" | ||||
|               onChange={onChangeLabel} | ||||
|             /> | ||||
|             <ChromePicker | ||||
|               className="mt-2" | ||||
|               color={color} | ||||
|               disableAlpha={true} | ||||
|               onChangeComplete={onChangeColor} | ||||
|             /> | ||||
|             <div className="flex flex-wrap mt-2 items-center"> | ||||
|               <span className="mr-2">Type:</span> | ||||
|               <button | ||||
|                 className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]" | ||||
|                 onClick={() => insertTagType("a:")} | ||||
|               > | ||||
|                 Adapter | ||||
|               </button> | ||||
|               <button | ||||
|                 className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]" | ||||
|                 onClick={() => insertTagType("t:")} | ||||
|               > | ||||
|                 Topic | ||||
|               </button> | ||||
|             </div> | ||||
|             <div className="flex mt-2"> | ||||
|               <TagComponent label={label} color={color} /> | ||||
|               <button | ||||
|                 className={clsx( | ||||
|                   "px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]", | ||||
|                   { "pointer-events-none opacity-60": !validateTag() } | ||||
|                 )} | ||||
|                 onClick={newTag} | ||||
|               > | ||||
|                 添加标签 | ||||
|               </button> | ||||
|             </div> | ||||
|           </div> | ||||
|         </ModalContent> | ||||
|         <ModalAction> | ||||
|           <button | ||||
|             className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]" | ||||
|             onClick={() => setModalOpen(false)} | ||||
|           > | ||||
|             关闭 | ||||
|           </button> | ||||
|           <button | ||||
|             className="ml-2 px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]" | ||||
|             onClick={onSubmit} | ||||
|           > | ||||
|             发布 | ||||
|           </button> | ||||
|         </ModalAction> | ||||
|       </Modal> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @@ -1,6 +1,7 @@ | ||||
| import * as AsciinemaPlayer from "asciinema-player"; | ||||
| import React, { useEffect, useRef } from "react"; | ||||
|  | ||||
| import * as AsciinemaPlayer from "asciinema-player"; | ||||
|  | ||||
| export type AsciinemaOptions = { | ||||
|   cols: number; | ||||
|   rows: number; | ||||
| @@ -16,7 +17,7 @@ export type AsciinemaOptions = { | ||||
|   fontSize: string; | ||||
| }; | ||||
|  | ||||
| export type AsciinemaProps = { | ||||
| export type Props = { | ||||
|   url: string; | ||||
|   options?: Partial<AsciinemaOptions>; | ||||
| }; | ||||
| @@ -24,12 +25,12 @@ export type AsciinemaProps = { | ||||
| export default function AsciinemaContainer({ | ||||
|   url, | ||||
|   options = {}, | ||||
| }: AsciinemaProps): JSX.Element { | ||||
| }: Props): JSX.Element { | ||||
|   const ref = useRef<HTMLDivElement>(null); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     AsciinemaPlayer.create(url, ref.current, options); | ||||
|   }, []); | ||||
|   }, [url, options]); | ||||
|  | ||||
|   return <div ref={ref} className="not-prose w-full max-w-full my-4"></div>; | ||||
|   return <div ref={ref} className="not-prose ap-container"></div>; | ||||
| } | ||||
|   | ||||
| @@ -1,15 +1,24 @@ | ||||
| import "asciinema-player/dist/bundle/asciinema-player.css"; | ||||
|  | ||||
| import "./styles.css"; | ||||
|  | ||||
| import React from "react"; | ||||
|  | ||||
| import "asciinema-player/dist/bundle/asciinema-player.css"; | ||||
| import BrowserOnly from "@docusaurus/BrowserOnly"; | ||||
|  | ||||
| export default function Asciinema(props): JSX.Element { | ||||
| import "./styles.css"; | ||||
| import type { Props } from "./container"; | ||||
|  | ||||
| export type { Props } from "./container"; | ||||
|  | ||||
| export default function Asciinema(props: Props): JSX.Element { | ||||
|   return ( | ||||
|     <BrowserOnly fallback={<div></div>}> | ||||
|     <BrowserOnly | ||||
|       fallback={ | ||||
|         <a href={props.url} title="Asciinema video player"> | ||||
|           Asciinema cast | ||||
|         </a> | ||||
|       } | ||||
|     > | ||||
|       {() => { | ||||
|         // eslint-disable-next-line @typescript-eslint/no-var-requires | ||||
|         const AsciinemaContainer = require("./container.tsx").default; | ||||
|         return <AsciinemaContainer {...props} />; | ||||
|       }} | ||||
|   | ||||
| @@ -1,3 +1,7 @@ | ||||
| .asciinema-player svg { | ||||
|   display: inline-block; | ||||
| .ap-player svg { | ||||
|   @apply inline-block; | ||||
| } | ||||
|  | ||||
| .ap-container { | ||||
|   @apply w-full my-4; | ||||
| } | ||||
|   | ||||
| @@ -1,233 +0,0 @@ | ||||
| import clsx from "clsx"; | ||||
| import React, { useRef, useState } from "react"; | ||||
| import { ChromePicker } from "react-color"; | ||||
| import { usePagination } from "react-use-pagination"; | ||||
|  | ||||
| import bots from "../../static/bots.json"; | ||||
| import { Tag, useFilteredObjs } from "../libs/store"; | ||||
| import Card from "./Card"; | ||||
| import Modal from "./Modal"; | ||||
| import ModalAction from "./ModalAction"; | ||||
| import ModalContent from "./ModalContent"; | ||||
| import ModalTitle from "./ModalTitle"; | ||||
| import Paginate from "./Paginate"; | ||||
| import TagComponent from "./Tag"; | ||||
|  | ||||
| export default function Bot(): JSX.Element { | ||||
|   const [modalOpen, setModalOpen] = useState<boolean>(false); | ||||
|   const { | ||||
|     filter, | ||||
|     setFilter, | ||||
|     filteredObjs: filteredBots, | ||||
|   } = useFilteredObjs(bots); | ||||
|  | ||||
|   const props = usePagination({ | ||||
|     totalItems: filteredBots.length, | ||||
|     initialPageSize: 10, | ||||
|   }); | ||||
|   const { startIndex, endIndex } = props; | ||||
|   const currentBots = filteredBots.slice(startIndex, endIndex + 1); | ||||
|  | ||||
|   const [form, setForm] = useState<{ | ||||
|     name: string; | ||||
|     desc: string; | ||||
|     homepage: string; | ||||
|   }>({ name: "", desc: "", homepage: "" }); | ||||
|  | ||||
|   const ref = useRef<HTMLInputElement>(null); | ||||
|   const [tags, setTags] = useState<Tag[]>([]); | ||||
|   const [label, setLabel] = useState<string>(""); | ||||
|   const [color, setColor] = useState<string>("#ea5252"); | ||||
|  | ||||
|   const urlEncode = (str: string) => | ||||
|     encodeURIComponent(str).replace(/%2B/gi, "+"); | ||||
|  | ||||
|   const onSubmit = () => { | ||||
|     setModalOpen(false); | ||||
|     const queries: { key: string; value: string }[] = [ | ||||
|       { key: "template", value: "bot_publish.yml" }, | ||||
|       { key: "title", value: form.name && `Bot: ${form.name}` }, | ||||
|       { key: "labels", value: "Bot" }, | ||||
|       { key: "name", value: form.name }, | ||||
|       { key: "description", value: form.desc }, | ||||
|       { key: "homepage", value: form.homepage }, | ||||
|       { key: "tags", value: JSON.stringify(tags) }, | ||||
|     ]; | ||||
|     const urlQueries = queries | ||||
|       .filter((query) => !!query.value) | ||||
|       .map((query) => `${query.key}=${urlEncode(query.value)}`) | ||||
|       .join("&"); | ||||
|     window.open(`https://github.com/nonebot/nonebot2/issues/new?${urlQueries}`); | ||||
|   }; | ||||
|   const onChange = (event) => { | ||||
|     const target = event.target; | ||||
|     const value = target.type === "checkbox" ? target.checked : target.value; | ||||
|     const name = target.name; | ||||
|  | ||||
|     setForm({ | ||||
|       ...form, | ||||
|       [name]: value, | ||||
|     }); | ||||
|     event.preventDefault(); | ||||
|   }; | ||||
|   const onChangeLabel = (event) => { | ||||
|     setLabel(event.target.value); | ||||
|   }; | ||||
|   const onChangeColor = (color) => { | ||||
|     setColor(color.hex); | ||||
|   }; | ||||
|   const validateTag = () => { | ||||
|     return label.length >= 1 && label.length <= 10; | ||||
|   }; | ||||
|   const newTag = () => { | ||||
|     if (tags.length >= 3) { | ||||
|       return; | ||||
|     } | ||||
|     if (validateTag()) { | ||||
|       const tag = { label, color }; | ||||
|       setTags([...tags, tag]); | ||||
|     } | ||||
|   }; | ||||
|   const delTag = (index: number) => { | ||||
|     setTags(tags.filter((_, i) => i !== index)); | ||||
|   }; | ||||
|   const insertTagType = (text: string) => { | ||||
|     setLabel(text + label); | ||||
|     ref.current.value = text + label; | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4 px-4"> | ||||
|         <input | ||||
|           className="w-full px-4 py-2 border rounded-full bg-light-nonepress-100 dark:bg-dark-nonepress-100" | ||||
|           value={filter} | ||||
|           placeholder="搜索机器人" | ||||
|           onChange={(event) => setFilter(event.target.value)} | ||||
|         /> | ||||
|         <button | ||||
|           className="w-full rounded-lg bg-hero text-white" | ||||
|           onClick={() => setModalOpen(true)} | ||||
|         > | ||||
|           发布机器人 | ||||
|         </button> | ||||
|       </div> | ||||
|       <div className="grid grid-cols-1 p-4"> | ||||
|         <Paginate {...props} /> | ||||
|       </div> | ||||
|       <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-4"> | ||||
|         {currentBots.map((bot, index) => ( | ||||
|           <Card key={index} {...bot} /> | ||||
|         ))} | ||||
|       </div> | ||||
|       <div className="grid grid-cols-1 p-4"> | ||||
|         <Paginate {...props} /> | ||||
|       </div> | ||||
|       <Modal active={modalOpen} setActive={setModalOpen}> | ||||
|         <ModalTitle title={"机器人信息"} /> | ||||
|         <ModalContent> | ||||
|           <form onSubmit={onSubmit}> | ||||
|             <div className="grid grid-cols-1 gap-4 p-4"> | ||||
|               <label className="flex flex-wrap"> | ||||
|                 <span className="mr-2">机器人名称:</span> | ||||
|                 <input | ||||
|                   type="text" | ||||
|                   name="name" | ||||
|                   maxLength={20} | ||||
|                   className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200" | ||||
|                   onChange={onChange} | ||||
|                 /> | ||||
|               </label> | ||||
|               <label className="flex flex-wrap"> | ||||
|                 <span className="mr-2">机器人介绍:</span> | ||||
|                 <input | ||||
|                   type="text" | ||||
|                   name="desc" | ||||
|                   className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200" | ||||
|                   onChange={onChange} | ||||
|                 /> | ||||
|               </label> | ||||
|               <label className="flex flex-wrap"> | ||||
|                 <span className="mr-2">仓库/主页:</span> | ||||
|                 <input | ||||
|                   type="text" | ||||
|                   name="homepage" | ||||
|                   className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200" | ||||
|                   onChange={onChange} | ||||
|                 /> | ||||
|               </label> | ||||
|             </div> | ||||
|           </form> | ||||
|           <div className="px-4"> | ||||
|             <label className="flex flex-wrap"> | ||||
|               <span className="mr-2">标签:</span> | ||||
|               {tags.map((tag, index) => ( | ||||
|                 <TagComponent | ||||
|                   key={index} | ||||
|                   {...tag} | ||||
|                   className="cursor-pointer" | ||||
|                   onClick={() => delTag(index)} | ||||
|                 /> | ||||
|               ))} | ||||
|             </label> | ||||
|           </div> | ||||
|           <div className="px-4 pt-4"> | ||||
|             <input | ||||
|               ref={ref} | ||||
|               type="text" | ||||
|               className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200" | ||||
|               onChange={onChangeLabel} | ||||
|             /> | ||||
|             <ChromePicker | ||||
|               className="mt-2" | ||||
|               color={color} | ||||
|               disableAlpha={true} | ||||
|               onChangeComplete={onChangeColor} | ||||
|             /> | ||||
|             <div className="flex flex-wrap mt-2 items-center"> | ||||
|               <span className="mr-2">Type:</span> | ||||
|               <button | ||||
|                 className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]" | ||||
|                 onClick={() => insertTagType("a:")} | ||||
|               > | ||||
|                 Adapter | ||||
|               </button> | ||||
|               <button | ||||
|                 className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]" | ||||
|                 onClick={() => insertTagType("t:")} | ||||
|               > | ||||
|                 Topic | ||||
|               </button> | ||||
|             </div> | ||||
|             <div className="flex mt-2"> | ||||
|               <TagComponent label={label} color={color} /> | ||||
|               <button | ||||
|                 className={clsx( | ||||
|                   "px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]", | ||||
|                   { "pointer-events-none opacity-60": !validateTag() } | ||||
|                 )} | ||||
|                 onClick={newTag} | ||||
|               > | ||||
|                 添加标签 | ||||
|               </button> | ||||
|             </div> | ||||
|           </div> | ||||
|         </ModalContent> | ||||
|         <ModalAction> | ||||
|           <button | ||||
|             className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]" | ||||
|             onClick={() => setModalOpen(false)} | ||||
|           > | ||||
|             关闭 | ||||
|           </button> | ||||
|           <button | ||||
|             className="ml-2 px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]" | ||||
|             onClick={onSubmit} | ||||
|           > | ||||
|             发布 | ||||
|           </button> | ||||
|         </ModalAction> | ||||
|       </Modal> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @@ -1,112 +0,0 @@ | ||||
| import clsx from "clsx"; | ||||
| import copy from "copy-to-clipboard"; | ||||
| import React, { useState } from "react"; | ||||
|  | ||||
| import Link from "@docusaurus/Link"; | ||||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||||
|  | ||||
| import type { Obj } from "../../libs/store"; | ||||
| import Tag from "../Tag"; | ||||
|  | ||||
| export default function Card({ | ||||
|   module_name, | ||||
|   project_link, | ||||
|   name, | ||||
|   desc, | ||||
|   author, | ||||
|   homepage, | ||||
|   tags, | ||||
|   is_official, | ||||
|   action, | ||||
|   actionDisabled = false, | ||||
|   actionLabel = "点击复制安装命令", | ||||
| }: Obj & { | ||||
|   action?: string; | ||||
|   actionLabel?: string; | ||||
|   actionDisabled?: boolean; | ||||
| }): JSX.Element { | ||||
|   const isGithub = /^https:\/\/github.com\/[^/]+\/[^/]+/.test(homepage); | ||||
|   const [copied, setCopied] = useState<boolean>(false); | ||||
|  | ||||
|   const copyAction = () => { | ||||
|     copy(action); | ||||
|     setCopied(true); | ||||
|     setTimeout(() => setCopied(false), 2000); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <div className="block max-w-full px-4 border-2 rounded-lg outline-none no-underline bg-light-nonepress-100 dark:bg-dark-nonepress-100 border-light-nonepress-200 dark:border-dark-nonepress-200 shadow-md shadow-light-nonepress-300 dark:shadow-dark-nonepress-300"> | ||||
|       <div className="flex justify-between pt-4 text-lg font-medium"> | ||||
|         <span> | ||||
|           {name} | ||||
|           {is_official && ( | ||||
|             <FontAwesomeIcon | ||||
|               icon={["fas", "check-circle"]} | ||||
|               className="text-green-600 ml-2" | ||||
|             /> | ||||
|           )} | ||||
|         </span> | ||||
|         {homepage && ( | ||||
|           <Link | ||||
|             href={homepage} | ||||
|             className="text-black dark:text-white opacity-60 hover:text-hero dark:hover:text-white hover:opacity-100" | ||||
|           > | ||||
|             {isGithub ? ( | ||||
|               <FontAwesomeIcon icon={["fab", "github"]} /> | ||||
|             ) : ( | ||||
|               <FontAwesomeIcon icon={["fas", "link"]} /> | ||||
|             )} | ||||
|           </Link> | ||||
|         )} | ||||
|       </div> | ||||
|       {tags && ( | ||||
|         <div className="pt-2 pb-4"> | ||||
|           {tags.map((tag, index) => ( | ||||
|             <Tag key={index} {...tag} /> | ||||
|           ))} | ||||
|         </div> | ||||
|       )} | ||||
|       {/* FIXME: full height */} | ||||
|       {desc && ( | ||||
|         <div className="pb-4 text-sm font-normal opacity-60">{desc}</div> | ||||
|       )} | ||||
|       {project_link && ( | ||||
|         <div className="my-2 text-sm font-normal opacity-60 font-mono"> | ||||
|           <FontAwesomeIcon icon={["fas", "cube"]} className="mr-2" /> | ||||
|           {project_link} | ||||
|         </div> | ||||
|       )} | ||||
|       {module_name && ( | ||||
|         <div className="my-2 text-sm font-normal opacity-60 font-mono"> | ||||
|           <FontAwesomeIcon icon={["fas", "fingerprint"]} className="mr-2" /> | ||||
|           {module_name} | ||||
|         </div> | ||||
|       )} | ||||
|       {/* TODO: add user avatar */} | ||||
|       {/* link: https://github.com/<username>.png */} | ||||
|       {author && ( | ||||
|         <div className="my-2 text-sm font-normal opacity-60 font-mono"> | ||||
|           <FontAwesomeIcon icon={["fas", "user"]} className="mr-2" /> | ||||
|           {author} | ||||
|         </div> | ||||
|       )} | ||||
|       {action && actionLabel && ( | ||||
|         <button | ||||
|           className={clsx( | ||||
|             "my-2 text-sm py-2 w-full rounded select-none bg-light-nonepress-200 dark:bg-dark-nonepress-200 active:bg-light-nonepress-300 active:dark:bg-dark-nonepress-300", | ||||
|             { "opacity-60 pointer-events-none": actionDisabled } | ||||
|           )} | ||||
|           onClick={copyAction} | ||||
|         > | ||||
|           <span className="flex grow items-center justify-center"> | ||||
|             {copied ? "复制成功" : actionLabel} | ||||
|             <FontAwesomeIcon | ||||
|               icon={["fas", copied ? "check-circle" : "copy"]} | ||||
|               className="ml-2" | ||||
|             /> | ||||
|           </span> | ||||
|         </button> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| @@ -1,54 +0,0 @@ | ||||
| import React from "react"; | ||||
| import { usePagination } from "react-use-pagination"; | ||||
|  | ||||
| import drivers from "../../static/drivers.json"; | ||||
| import { useFilteredObjs } from "../libs/store"; | ||||
| import Card from "./Card"; | ||||
| import Paginate from "./Paginate"; | ||||
|  | ||||
| export default function Driver(): JSX.Element { | ||||
|   const { | ||||
|     filter, | ||||
|     setFilter, | ||||
|     filteredObjs: filteredDrivers, | ||||
|   } = useFilteredObjs(drivers); | ||||
|  | ||||
|   const props = usePagination({ | ||||
|     totalItems: filteredDrivers.length, | ||||
|     initialPageSize: 10, | ||||
|   }); | ||||
|   const { startIndex, endIndex } = props; | ||||
|   const currentDrivers = filteredDrivers.slice(startIndex, endIndex + 1); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4 px-4"> | ||||
|         <input | ||||
|           className="w-full px-4 py-2 border rounded-full bg-light-nonepress-100 dark:bg-dark-nonepress-100" | ||||
|           value={filter} | ||||
|           placeholder="搜索驱动器" | ||||
|           onChange={(event) => setFilter(event.target.value)} | ||||
|         /> | ||||
|         <button className="w-full rounded-lg bg-hero text-white opacity-60"> | ||||
|           发布驱动器 | ||||
|         </button> | ||||
|       </div> | ||||
|       <div className="grid grid-cols-1 p-4"> | ||||
|         <Paginate {...props} /> | ||||
|       </div> | ||||
|       <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-4"> | ||||
|         {currentDrivers.map((driver, index) => ( | ||||
|           <Card | ||||
|             key={index} | ||||
|             {...driver} | ||||
|             action={`nb driver install ${driver.project_link}`} | ||||
|             actionDisabled={!driver.project_link} | ||||
|           /> | ||||
|         ))} | ||||
|       </div> | ||||
|       <div className="grid grid-cols-1 p-4"> | ||||
|         <Paginate {...props} /> | ||||
|       </div> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @@ -1,101 +0,0 @@ | ||||
| import React, { PropsWithChildren } from "react"; | ||||
|  | ||||
| import Link from "@docusaurus/Link"; | ||||
| import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; | ||||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||||
| import Logo from "@theme/Logo"; | ||||
|  | ||||
| export function Hero(): JSX.Element { | ||||
|   const { siteConfig } = useDocusaurusContext(); | ||||
|  | ||||
|   return ( | ||||
|     <div className="flex flex-wrap p-16 mx-auto max-w-7xl h-screen relative px-4 sm:p-24"> | ||||
|       <div className="flex-grow self-center text-center"> | ||||
|         <Logo imageClassName="max-h-48" /> | ||||
|         <h1 className="text-5xl tracking-tight font-light sm:text-5xl md:text-5xl"> | ||||
|           <span className="text-hero">N</span>one | ||||
|           <span className="text-hero">B</span>ot | ||||
|         </h1> | ||||
|         <p className="my-3 max-w-md mx-auto text-sm font-medium tracking-wide uppercase opacity-70 md:mt-5 md:max-w-3xl"> | ||||
|           {siteConfig.tagline} | ||||
|         </p> | ||||
|         <div className="mt-8"> | ||||
|           <Link | ||||
|             to="/docs/" | ||||
|             className="inline-block bg-hero text-white font-bold rounded-lg px-6 py-3" | ||||
|           > | ||||
|             开始使用 <FontAwesomeIcon icon={["fas", "chevron-right"]} /> | ||||
|           </Link> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div className="absolute flex-grow flex items-center justify-between bottom-0 right-0 w-full"> | ||||
|         <div className="mx-auto self-start animate-bounce"> | ||||
|           <FontAwesomeIcon | ||||
|             className="text-4xl text-hero" | ||||
|             icon={["fas", "angle-down"]} | ||||
|           /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export type Feature = { | ||||
|   readonly title: string; | ||||
|   readonly tagline?: string; | ||||
|   readonly description?: string; | ||||
|   readonly annotaion?: string; | ||||
| }; | ||||
|  | ||||
| export function HeroFeature(props: PropsWithChildren<Feature>): JSX.Element { | ||||
|   const { title, tagline, description, annotaion, children } = props; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <p className="mt-3 mb-3 max-w-md mx-auto text-sm font-medium tracking-wide uppercase opacity-70 md:mt-5 md:max-w-3xl"> | ||||
|         {tagline} | ||||
|       </p> | ||||
|       <h1 className="font-mono font-light text-4xl tracking-tight sm:text-5xl md:text-5xl text-hero"> | ||||
|         {title} | ||||
|       </h1> | ||||
|       <p className="mt-10 mb-6">{description}</p> | ||||
|       {children} | ||||
|       <p className="text-sm italic opacity-70">{annotaion}</p> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export function HeroFeatureSingle( | ||||
|   props: PropsWithChildren<Feature> | ||||
| ): JSX.Element { | ||||
|   return ( | ||||
|     <div className="max-w-7xl mx-auto py-16 px-4 text-center md:px-16"> | ||||
|       <HeroFeature {...props} /> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export function HeroFeatureDouble( | ||||
|   props: PropsWithChildren<{ features: [Feature, Feature] }> | ||||
| ): JSX.Element { | ||||
|   const { | ||||
|     features: [feature1, feature2], | ||||
|     children, | ||||
|   } = props; | ||||
|  | ||||
|   let children1, children2; | ||||
|   if (Array.isArray(children) && children.length === 2) { | ||||
|     [children1, children2] = children; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div className="max-w-7xl mx-auto py-16 px-4 md:grid md:grid-cols-2 md:gap-6 md:px-16"> | ||||
|       <div className="pb-16 text-center md:pb-0"> | ||||
|         <HeroFeature {...feature1} children={children1} /> | ||||
|       </div> | ||||
|       <div className="text-center"> | ||||
|         <HeroFeature {...feature2} children={children2} /> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										174
									
								
								website/src/components/Home/Feature.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								website/src/components/Home/Feature.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,174 @@ | ||||
| import React from "react"; | ||||
|  | ||||
| import CodeBlock from "@theme/CodeBlock"; | ||||
|  | ||||
| export type Feature = { | ||||
|   title: string; | ||||
|   tagline?: string; | ||||
|   description?: string; | ||||
|   annotaion?: string; | ||||
|   children?: React.ReactNode; | ||||
| }; | ||||
|  | ||||
| export function HomeFeature({ | ||||
|   title, | ||||
|   tagline, | ||||
|   description, | ||||
|   annotaion, | ||||
|   children, | ||||
| }: Feature): JSX.Element { | ||||
|   return ( | ||||
|     <div className="flex flex-col items-center justify-center p-4"> | ||||
|       <p className="text-sm text-base-content/70 font-medium tracking-wide uppercase"> | ||||
|         {tagline} | ||||
|       </p> | ||||
|       <h1 className="mt-3 font-mono font-light text-4xl tracking-tight sm:text-5xl md:text-5xl text-primary"> | ||||
|         {title} | ||||
|       </h1> | ||||
|       <p className="mt-10 mb-6">{description}</p> | ||||
|       {children} | ||||
|       <p className="text-sm italic text-base-content/70">{annotaion}</p> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function HomeFeatureSingleColumn(props: Feature): JSX.Element { | ||||
|   return ( | ||||
|     <div className="grid mx-auto px-4 py-8 md:px-16"> | ||||
|       <HomeFeature {...props} /> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function HomeFeatureDoubleColumn({ | ||||
|   features: [feature1, feature2], | ||||
|   children, | ||||
| }: { | ||||
|   features: [Feature, Feature]; | ||||
|   children?: [React.ReactNode, React.ReactNode]; | ||||
| }): JSX.Element { | ||||
|   const [children1, children2] = children ?? []; | ||||
|  | ||||
|   return ( | ||||
|     <div className="grid gap-x-6 gap-y-8 grid-cols-1 lg:grid-cols-2 max-w-7xl mx-auto px-4 py-8 md:px-16"> | ||||
|       <HomeFeature {...feature1}>{children1}</HomeFeature> | ||||
|       <HomeFeature {...feature2}>{children2}</HomeFeature> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function HomeFeatures(): JSX.Element { | ||||
|   return ( | ||||
|     <> | ||||
|       <HomeFeatureSingleColumn | ||||
|         title="开箱即用" | ||||
|         tagline="out of box" | ||||
|         description="使用 NB-CLI 快速构建属于你的机器人" | ||||
|       > | ||||
|         <CodeBlock | ||||
|           title="Installation" | ||||
|           language="bash" | ||||
|           className="home-codeblock" | ||||
|         > | ||||
|           {[ | ||||
|             "$ pipx install nb-cli", | ||||
|             "$ nb", | ||||
|             // "d8b   db  .d88b.  d8b   db d88888b d8888b.  .d88b.  d888888b", | ||||
|             // "888o  88 .8P  Y8. 888o  88 88'     88  `8D .8P  Y8. `~~88~~'", | ||||
|             // "88V8o 88 88    88 88V8o 88 88ooooo 88oooY' 88    88    88", | ||||
|             // "88 V8o88 88    88 88 V8o88 88~~~~~ 88~~~b. 88    88    88", | ||||
|             // "88  V888 `8b  d8' 88  V888 88.     88   8D `8b  d8'    88", | ||||
|             // "VP   V8P  `Y88P'  VP   V8P Y88888P Y8888P'  `Y88P'     YP", | ||||
|             "[?] What do you want to do?", | ||||
|             "❯ Create a NoneBot project.", | ||||
|             "  Run the bot in current folder.", | ||||
|             "  Manage bot driver.", | ||||
|             "  Manage bot adapters.", | ||||
|             "  Manage bot plugins.", | ||||
|             "  ...", | ||||
|           ].join("\n")} | ||||
|         </CodeBlock> | ||||
|       </HomeFeatureSingleColumn> | ||||
|       <HomeFeatureDoubleColumn | ||||
|         features={[ | ||||
|           { | ||||
|             title: "插件系统", | ||||
|             tagline: "plugin system", | ||||
|             description: "插件化开发,模块化管理", | ||||
|           }, | ||||
|           { | ||||
|             title: "跨平台支持", | ||||
|             tagline: "cross-platform support", | ||||
|             description: "支持多种平台,以及多样的事件响应方式", | ||||
|           }, | ||||
|         ]} | ||||
|       > | ||||
|         <CodeBlock title="" language="python" className="home-codeblock"> | ||||
|           {[ | ||||
|             "import nonebot", | ||||
|             "# 加载一个插件", | ||||
|             'nonebot.load_plugin("path.to.your.plugin")', | ||||
|             "# 从文件夹加载插件", | ||||
|             'nonebot.load_plugins("plugins")', | ||||
|             "# 从配置文件加载多个插件", | ||||
|             'nonebot.load_from_json("plugins.json")', | ||||
|             'nonebot.load_from_toml("pyproject.toml")', | ||||
|           ].join("\n")} | ||||
|         </CodeBlock> | ||||
|         <CodeBlock title="" language="python" className="home-codeblock"> | ||||
|           {[ | ||||
|             "import nonebot", | ||||
|             "# OneBot", | ||||
|             "from nonebot.adapters.onebot.v11 import Adapter as OneBotAdapter", | ||||
|             "# QQ 频道", | ||||
|             "from nonebot.adapters.qqguild import Adapter as QQGuildAdapter", | ||||
|             "driver = nonebot.get_driver()", | ||||
|             "driver.register_adapter(OneBotAdapter)", | ||||
|             "driver.register_adapter(QQGuildAdapter)", | ||||
|           ].join("\n")} | ||||
|         </CodeBlock> | ||||
|       </HomeFeatureDoubleColumn> | ||||
|       <HomeFeatureDoubleColumn | ||||
|         features={[ | ||||
|           { | ||||
|             title: "异步开发", | ||||
|             tagline: "asynchronous first", | ||||
|             description: "异步优先式开发,提高运行效率", | ||||
|           }, | ||||
|           { | ||||
|             title: "依赖注入", | ||||
|             tagline: "builtin dependency injection system", | ||||
|             description: "简单清晰的依赖注入系统,内置依赖函数减少用户代码", | ||||
|           }, | ||||
|         ]} | ||||
|       > | ||||
|         <CodeBlock title="" language="python" className="home-codeblock"> | ||||
|           {[ | ||||
|             "from nonebot import on_message", | ||||
|             "# 注册一个消息响应器", | ||||
|             "matcher = on_message()", | ||||
|             "# 注册一个消息处理器", | ||||
|             "# 并重复收到的消息", | ||||
|             "@matcher.handle()", | ||||
|             "async def handler(event: Event) -> None:", | ||||
|             "    await matcher.send(event.get_message())", | ||||
|           ].join("\n")} | ||||
|         </CodeBlock> | ||||
|         <CodeBlock title="" language="python" className="home-codeblock"> | ||||
|           {[ | ||||
|             "from nonebot import on_command", | ||||
|             "# 注册一个命令响应器", | ||||
|             'matcher = on_command("help", alias={"帮助"})', | ||||
|             "# 注册一个命令处理器", | ||||
|             "# 通过依赖注入获得命令名以及参数", | ||||
|             "@matcher.handle()", | ||||
|             "async def handler(cmd = Command(), arg = CommandArg()) -> None:", | ||||
|             "    await matcher.finish()", | ||||
|           ].join("\n")} | ||||
|         </CodeBlock> | ||||
|       </HomeFeatureDoubleColumn> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export default React.memo(HomeFeatures); | ||||
							
								
								
									
										69
									
								
								website/src/components/Home/Hero.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								website/src/components/Home/Hero.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| import React, { useCallback, useEffect, useRef, useState } from "react"; | ||||
|  | ||||
| import Link from "@docusaurus/Link"; | ||||
| import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; | ||||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||||
| import { useNonepressThemeConfig } from "@nullbot/docusaurus-theme-nonepress/client"; | ||||
| // @ts-expect-error: we need to make package have type: module | ||||
| import copy from "copy-text-to-clipboard"; | ||||
|  | ||||
| import IconCopy from "@theme/Icon/Copy"; | ||||
| import IconSuccess from "@theme/Icon/Success"; | ||||
|  | ||||
| function HomeHeroInstallButton(): JSX.Element { | ||||
|   const code = "pipx run nb-cli create"; | ||||
|  | ||||
|   const [isCopied, setIsCopied] = useState(false); | ||||
|   const copyTimeout = useRef<number | undefined>(undefined); | ||||
|  | ||||
|   const handleCopyCode = useCallback(() => { | ||||
|     copy(code); | ||||
|     setIsCopied(true); | ||||
|     copyTimeout.current = window.setTimeout(() => { | ||||
|       setIsCopied(false); | ||||
|     }, 1500); | ||||
|   }, [code]); | ||||
|  | ||||
|   useEffect(() => () => window.clearTimeout(copyTimeout.current), []); | ||||
|  | ||||
|   return ( | ||||
|     <button | ||||
|       className="btn no-animation home-hero-copy" | ||||
|       onClick={handleCopyCode} | ||||
|     > | ||||
|       <code>$ {code}</code> | ||||
|       {isCopied ? <IconSuccess className="text-success" /> : <IconCopy />} | ||||
|     </button> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function HomeHero(): JSX.Element { | ||||
|   const { | ||||
|     siteConfig: { tagline }, | ||||
|   } = useDocusaurusContext(); | ||||
|   const { | ||||
|     navbar: { logo }, | ||||
|   } = useNonepressThemeConfig(); | ||||
|  | ||||
|   return ( | ||||
|     <div className="home-hero"> | ||||
|       <img src={logo!.src} alt={logo!.alt} className="home-hero-logo" /> | ||||
|       <h1 className="home-hero-title"> | ||||
|         <span className="text-primary">None</span> | ||||
|         Bot | ||||
|       </h1> | ||||
|       <p className="home-hero-tagline">{tagline}</p> | ||||
|       <div className="home-hero-actions"> | ||||
|         <Link to="/docs/" className="btn btn-primary"> | ||||
|           开始使用 <FontAwesomeIcon icon={["fas", "chevron-right"]} /> | ||||
|         </Link> | ||||
|         <HomeHeroInstallButton /> | ||||
|       </div> | ||||
|       <div className="home-hero-next"> | ||||
|         <FontAwesomeIcon icon={["fas", "angle-down"]} /> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export default React.memo(HomeHero); | ||||
							
								
								
									
										14
									
								
								website/src/components/Home/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								website/src/components/Home/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import React from "react"; | ||||
|  | ||||
| import "./styles.css"; | ||||
| import HomeFeatures from "./Feature"; | ||||
| import HomeHero from "./Hero"; | ||||
|  | ||||
| export default function HomeContent(): JSX.Element { | ||||
|   return ( | ||||
|     <div className="home-container"> | ||||
|       <HomeHero /> | ||||
|       <HomeFeatures /> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										41
									
								
								website/src/components/Home/styles.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								website/src/components/Home/styles.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| .home { | ||||
|   &-container { | ||||
|     @apply -mt-16; | ||||
|   } | ||||
|  | ||||
|   &-hero { | ||||
|     @apply relative flex flex-col items-center justify-center gap-4 h-screen; | ||||
|  | ||||
|     &-logo { | ||||
|       @apply h-48 w-auto; | ||||
|     } | ||||
|  | ||||
|     &-title { | ||||
|       @apply text-5xl font-normal tracking-tight; | ||||
|     } | ||||
|  | ||||
|     &-tagline { | ||||
|       @apply text-sm font-medium uppercase tracking-wide text-base-content/70; | ||||
|     } | ||||
|  | ||||
|     &-actions { | ||||
|       @apply flex flex-col sm:flex-row gap-4; | ||||
|     } | ||||
|  | ||||
|     &-copy { | ||||
|       @apply font-normal normal-case text-base-content/70; | ||||
|     } | ||||
|  | ||||
|     &-next { | ||||
|       @apply absolute bottom-4; | ||||
|  | ||||
|       & svg { | ||||
|         @apply animate-bounce text-primary text-4xl; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &-codeblock { | ||||
|     @apply inline-block !max-w-[600px]; | ||||
|   } | ||||
| } | ||||
| @@ -1,40 +1,56 @@ | ||||
| import clsx from "clsx"; | ||||
| import React from "react"; | ||||
|  | ||||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||||
| import Logo from "@theme/Logo"; | ||||
| import clsx from "clsx"; | ||||
|  | ||||
| import styles from "./styles.module.css"; | ||||
| import useBaseUrl from "@docusaurus/useBaseUrl"; | ||||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||||
| import { useNonepressThemeConfig } from "@nullbot/docusaurus-theme-nonepress/client"; | ||||
|  | ||||
| import "./styles.css"; | ||||
| import ThemedImage from "@theme/ThemedImage"; | ||||
|  | ||||
| export type Message = { | ||||
|   position?: "left" | "right"; | ||||
|   msg: string; | ||||
|   position?: "left" | "right"; | ||||
|   monospace?: boolean; | ||||
| }; | ||||
|  | ||||
| function MessageBox({ | ||||
|   msg, | ||||
|   isRight, | ||||
| }: { | ||||
|   msg: string; | ||||
|   isRight: boolean; | ||||
| }): JSX.Element { | ||||
|   position = "left", | ||||
|   monospace = false, | ||||
| }: Message): JSX.Element { | ||||
|   const { | ||||
|     navbar: { logo }, | ||||
|   } = useNonepressThemeConfig(); | ||||
|   const sources = { | ||||
|     light: useBaseUrl(logo!.src), | ||||
|     dark: useBaseUrl(logo!.srcDark || logo!.src), | ||||
|   }; | ||||
|  | ||||
|   const isRight = position === "right"; | ||||
|  | ||||
|   return ( | ||||
|     <div | ||||
|       className={clsx(styles.message, { | ||||
|         [styles.messageRight]: isRight, | ||||
|       })} | ||||
|     > | ||||
|       {isRight ? ( | ||||
|         <div className={clsx("bg-cyan-600 text-base", styles.messageAvatar)}> | ||||
|           <FontAwesomeIcon icon={["fas", "user"]} /> | ||||
|     <div className={clsx("chat", isRight ? "chat-end" : "chat-start")}> | ||||
|       <div className="chat-image avatar"> | ||||
|         <div | ||||
|           className={clsx( | ||||
|             "messenger-chat-avatar", | ||||
|             isRight && "messenger-chat-avatar-user" | ||||
|           )} | ||||
|         > | ||||
|           {isRight ? ( | ||||
|             <FontAwesomeIcon icon={["fas", "user"]} /> | ||||
|           ) : ( | ||||
|             <ThemedImage sources={sources} /> | ||||
|           )} | ||||
|         </div> | ||||
|       ) : ( | ||||
|         <div className={clsx("transparent", styles.messageAvatar)}> | ||||
|           <Logo imageClassName="h-full w-full" disabled /> | ||||
|         </div> | ||||
|       )} | ||||
|       </div> | ||||
|       <div | ||||
|         className={clsx(styles.messageBox, { "order-first": isRight })} | ||||
|         className={clsx( | ||||
|           "chat-bubble messenger-chat-bubble", | ||||
|           monospace && "font-mono" | ||||
|         )} | ||||
|         dangerouslySetInnerHTML={{ | ||||
|           __html: msg.replace(/\n/g, "<br/>").replace(/ /g, " "), | ||||
|         }} | ||||
| @@ -48,62 +64,55 @@ export default function Messenger({ | ||||
| }: { | ||||
|   msgs?: Message[]; | ||||
| }): JSX.Element { | ||||
|   const isRight = (msg: Message): boolean => msg.position === "right"; | ||||
|  | ||||
|   return ( | ||||
|     <div className="block w-full max-w-full my-4 rounded shadow-md outline-none no-underline bg-light-nonepress-100 dark:bg-dark-nonepress-100"> | ||||
|       <header className="flex items-center h-12 px-4 bg-blue-500 text-white rounded-t-[inherit]"> | ||||
|         <div className="text-left text-base grow"> | ||||
|     <div className="messenger-container"> | ||||
|       <header className="messenger-title"> | ||||
|         <div className="messenger-title-back"> | ||||
|           <FontAwesomeIcon icon={["fas", "chevron-left"]} /> | ||||
|         </div> | ||||
|         <div className="flex-initial grow-0"> | ||||
|           <span className="text-xl font-bold">NoneBot</span> | ||||
|         <div className="messenger-title-name"> | ||||
|           <span>NoneBot</span> | ||||
|         </div> | ||||
|         <div className="text-right text-base grow"> | ||||
|           <FontAwesomeIcon icon={["fas", "user"]} /> | ||||
|         <div className="messenger-title-more"> | ||||
|           <FontAwesomeIcon icon={["fas", "bars"]} /> | ||||
|         </div> | ||||
|       </header> | ||||
|       <div className="p-3 min-h-[150px]"> | ||||
|       <div className="messenger-chat"> | ||||
|         {msgs.map((msg, i) => ( | ||||
|           <MessageBox msg={msg.msg} isRight={isRight(msg)} key={i} /> | ||||
|           <MessageBox {...msg} key={i} /> | ||||
|         ))} | ||||
|       </div> | ||||
|       <div className="px-3"> | ||||
|         <div className="flex flex-row items-center"> | ||||
|           <div className="flex-1 p-1 max-w-full"> | ||||
|       <div className="messenger-footer"> | ||||
|         <div className="messenger-footer-action"> | ||||
|           <div className="messenger-footer-action-input"> | ||||
|             <input | ||||
|               className="w-full rounded bg-light dark:bg-dark focus:outline-none focus:ring focus:border-blue-500" | ||||
|               className="input input-xs input-bordered input-info w-full" | ||||
|               readOnly | ||||
|             /> | ||||
|           </div> | ||||
|           <div className="flex-initial grow-0 w-fit"> | ||||
|             <button | ||||
|               className={clsx( | ||||
|                 "h-7 px-3 rounded-full bg-blue-500 text-white", | ||||
|                 styles.messageSendButton | ||||
|               )} | ||||
|             > | ||||
|               <span>发送</span> | ||||
|           <div className="messenger-footer-action-send"> | ||||
|             <button className="btn btn-xs btn-info no-animation text-white"> | ||||
|               发送 | ||||
|             </button> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div className="flex flex-row items-center text-center text-base text-gray-600"> | ||||
|           <div className="p-1 shrink-0 grow-0 basis-1/6"> | ||||
|         <div className="messenger-footer-tools"> | ||||
|           <div> | ||||
|             <FontAwesomeIcon icon={["fas", "microphone"]} /> | ||||
|           </div> | ||||
|           <div className="p-1 shrink-0 grow-0 basis-1/6"> | ||||
|           <div> | ||||
|             <FontAwesomeIcon icon={["fas", "image"]} /> | ||||
|           </div> | ||||
|           <div className="p-1 shrink-0 grow-0 basis-1/6"> | ||||
|           <div> | ||||
|             <FontAwesomeIcon icon={["fas", "camera"]} /> | ||||
|           </div> | ||||
|           <div className="p-1 shrink-0 grow-0 basis-1/6"> | ||||
|           <div> | ||||
|             <FontAwesomeIcon icon={["fas", "wallet"]} /> | ||||
|           </div> | ||||
|           <div className="p-1 shrink-0 grow-0 basis-1/6"> | ||||
|           <div> | ||||
|             <FontAwesomeIcon icon={["fas", "smile-wink"]} /> | ||||
|           </div> | ||||
|           <div className="p-1 shrink-0 grow-0 basis-1/6"> | ||||
|           <div> | ||||
|             <FontAwesomeIcon icon={["fas", "plus-circle"]} /> | ||||
|           </div> | ||||
|         </div> | ||||
|   | ||||
							
								
								
									
										66
									
								
								website/src/components/Messenger/styles.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								website/src/components/Messenger/styles.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| .messenger { | ||||
|   &-container { | ||||
|     @apply block w-full my-4 overflow-hidden; | ||||
|     @apply rounded-lg outline-none bg-base-200; | ||||
|     @apply transition-[background-color] duration-500; | ||||
|   } | ||||
|  | ||||
|   &-title { | ||||
|     @apply flex items-center h-12 px-4 bg-info text-white; | ||||
|  | ||||
|     &-back { | ||||
|       @apply text-left text-base grow; | ||||
|     } | ||||
|  | ||||
|     &-name { | ||||
|       @apply flex-initial grow-0 text-xl font-bold; | ||||
|     } | ||||
|  | ||||
|     &-more { | ||||
|       @apply text-right text-base grow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &-chat { | ||||
|     @apply p-4 min-h-[150px]; | ||||
|  | ||||
|     &-avatar { | ||||
|       @apply !flex items-center justify-center; | ||||
|       @apply w-10 rounded-full; | ||||
|  | ||||
|       &-user { | ||||
|         @apply bg-info text-white; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     &-bubble { | ||||
|       @apply bg-base-100 text-base-content [word-break:break-word]; | ||||
|       @apply transition-[color,background-color] duration-500; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &-footer { | ||||
|     @apply px-4; | ||||
|  | ||||
|     &-action { | ||||
|       @apply flex items-center gap-2; | ||||
|  | ||||
|       &-input { | ||||
|         @apply flex-1; | ||||
|  | ||||
|         & > input { | ||||
|           @apply transition-[color,background-color] duration-500; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       &-send { | ||||
|         @apply flex-initial w-fit; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     &-tools { | ||||
|       @apply grid grid-cols-6 items-center py-1; | ||||
|       @apply text-center text-base text-base-content/60; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,40 +0,0 @@ | ||||
| .message { | ||||
|   @apply flex flex-row flex-wrap justify-start; | ||||
| } | ||||
|  | ||||
| .messageRight { | ||||
|   @apply !justify-end; | ||||
| } | ||||
|  | ||||
| .message .messageAvatar { | ||||
|   @apply relative inline-flex items-center justify-center text-center align-middle h-9 w-9 rounded-full; | ||||
| } | ||||
|  | ||||
| .message .messageBox { | ||||
|   @apply relative w-fit max-w-[55%] px-2 py-[0.375rem] mx-3 my-2 rounded-lg bg-light; | ||||
| } | ||||
| :global(.dark) .message .messageBox { | ||||
|   @apply !bg-dark; | ||||
| } | ||||
|  | ||||
| .message .messageBox::after { | ||||
|   content: ""; | ||||
|   border-bottom: 7px solid; | ||||
|   @apply absolute top-0 right-full w-2 h-3 text-light rounded-bl-lg; | ||||
| } | ||||
| :global(.dark) .message .messageBox::after { | ||||
|   @apply !text-dark; | ||||
| } | ||||
| .message.messageRight .messageBox::after { | ||||
|   @apply !left-full !right-auto !rounded-bl-[0] !rounded-br-lg; | ||||
| } | ||||
|  | ||||
| .messageSendButton { | ||||
|   -webkit-tap-highlight-color: transparent; | ||||
|   -webkit-touch-callout: none; | ||||
|   -webkit-user-select: none; | ||||
|   -khtml-user-select: none; | ||||
|   -moz-user-select: none; | ||||
|   -ms-user-select: none; | ||||
|   user-select: none; | ||||
| } | ||||
| @@ -1,41 +0,0 @@ | ||||
| import clsx from "clsx"; | ||||
| import React from "react"; | ||||
|  | ||||
| export default function Modal({ | ||||
|   active, | ||||
|   setActive, | ||||
|   children, | ||||
| }: { | ||||
|   active: boolean; | ||||
|   setActive: (active: boolean) => void; | ||||
|   children: React.ReactNode; | ||||
| }): JSX.Element { | ||||
|   return ( | ||||
|     <> | ||||
|       {/* overlay */} | ||||
|       <div | ||||
|         className={clsx( | ||||
|           "fixed top-0 bottom-0 left-0 right-0 flex items-center justify-center transition z-[200] pointer-events-auto", | ||||
|           { "!hidden": !active } | ||||
|         )} | ||||
|         onClick={() => setActive(false)} | ||||
|       > | ||||
|         <div className="absolute top-0 bottom-0 left-0 right-0 h-full w-full bg-gray-800 opacity-[.46]"></div> | ||||
|       </div> | ||||
|       {/* modal */} | ||||
|       <div | ||||
|         className={clsx( | ||||
|           "fixed top-0 left-0 flex items-center justify-center h-full w-full z-[201] pointer-events-none", | ||||
|           { "!hidden": !active } | ||||
|         )} | ||||
|         tabIndex={0} | ||||
|       > | ||||
|         <div className="w-full max-w-[600px] max-h-[90%] overflow-y-auto rounded shadow-lg m-6 z-[inherit] pointer-events-auto thin-scrollbar"> | ||||
|           <div className="bg-light-nonepress-100 dark:bg-dark-nonepress-100"> | ||||
|             {children} | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| import React from "react"; | ||||
|  | ||||
| export default function ModalAction({ | ||||
|   children, | ||||
| }: { | ||||
|   children: React.ReactNode; | ||||
| }): JSX.Element { | ||||
|   return <div className="px-4 py-2 flex justify-end">{children}</div>; | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| import React from "react"; | ||||
|  | ||||
| export default function ModalContent({ | ||||
|   children, | ||||
| }: { | ||||
|   children: React.ReactNode; | ||||
| }): JSX.Element { | ||||
|   return <div className="px-6 pb-5 w-full">{children}</div>; | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| import React from "react"; | ||||
|  | ||||
| export default function ModalTitle({ title }: { title: string }): JSX.Element { | ||||
|   return ( | ||||
|     <div className="px-6 pt-4 pb-2 font-medium text-xl"> | ||||
|       <span>{title}</span> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| @@ -1,39 +1,55 @@ | ||||
| import React, { useCallback } from "react"; | ||||
|  | ||||
| import clsx from "clsx"; | ||||
| import React, { useCallback, useState } from "react"; | ||||
| import { usePagination } from "react-use-pagination"; | ||||
|  | ||||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||||
| import type { usePagination } from "react-use-pagination"; | ||||
|  | ||||
| import { useContentWidth } from "../../libs/width"; | ||||
| import styles from "./styles.module.css"; | ||||
| import "./styles.css"; | ||||
|  | ||||
| const MAX_LENGTH = 7; | ||||
|  | ||||
| export type Props = Pick< | ||||
|   ReturnType<typeof usePagination>, | ||||
|   | "totalPages" | ||||
|   | "currentPage" | ||||
|   | "setNextPage" | ||||
|   | "setPreviousPage" | ||||
|   | "setPage" | ||||
|   | "previousEnabled" | ||||
|   | "nextEnabled" | ||||
| > & { | ||||
|   className?: string; | ||||
| }; | ||||
|  | ||||
| export default function Paginate({ | ||||
|   className, | ||||
|   totalPages, | ||||
|   currentPage, | ||||
|   setPreviousPage, | ||||
|   setNextPage, | ||||
|   setPage, | ||||
|   currentPage, | ||||
|   previousEnabled, | ||||
|   nextEnabled, | ||||
| }: ReturnType<typeof usePagination>): JSX.Element { | ||||
|   const [containerElement, setContainerElement] = useState<HTMLElement | null>( | ||||
|     null | ||||
|   ); | ||||
| }: Props): JSX.Element { | ||||
|   // const [containerElement, setContainerElement] = useState<HTMLElement | null>( | ||||
|   //   null | ||||
|   // ); | ||||
|  | ||||
|   const ref = useCallback( | ||||
|     (element: HTMLElement | null) => { | ||||
|       setContainerElement(element); | ||||
|     }, | ||||
|     [setContainerElement] | ||||
|   ); | ||||
|   // const ref = useCallback( | ||||
|   //   (element: HTMLElement | null) => { | ||||
|   //     setContainerElement(element); | ||||
|   //   }, | ||||
|   //   [setContainerElement] | ||||
|   // ); | ||||
|  | ||||
|   const maxWidth = useContentWidth( | ||||
|     containerElement?.parentElement ?? undefined | ||||
|   ); | ||||
|   const maxLength = Math.min( | ||||
|     (maxWidth && Math.floor(maxWidth / 50) - 2) || totalPages, | ||||
|     totalPages | ||||
|   ); | ||||
|   // const maxWidth = useContentWidth( | ||||
|   //   containerElement?.parentElement ?? undefined | ||||
|   // ); | ||||
|   // const maxLength = Math.min( | ||||
|   //   (maxWidth && Math.floor(maxWidth / 50) - 2) || totalPages, | ||||
|   //   totalPages | ||||
|   // ); | ||||
|  | ||||
|   const range = useCallback((start: number, end: number) => { | ||||
|     const result = []; | ||||
| @@ -47,12 +63,12 @@ export default function Paginate({ | ||||
|   const pages: (React.ReactNode | number)[] = []; | ||||
|   const ellipsis = <FontAwesomeIcon icon="ellipsis-h" />; | ||||
|  | ||||
|   const even = maxLength % 2 === 0 ? 1 : 0; | ||||
|   const left = Math.floor(maxLength / 2); | ||||
|   const even = MAX_LENGTH % 2 === 0 ? 1 : 0; | ||||
|   const left = Math.floor(MAX_LENGTH / 2); | ||||
|   const right = totalPages - left + even + 1; | ||||
|   currentPage = currentPage + 1; | ||||
|  | ||||
|   if (totalPages <= maxLength) { | ||||
|   if (totalPages <= MAX_LENGTH) { | ||||
|     pages.push(...range(1, totalPages)); | ||||
|   } else if (currentPage > left && currentPage < right) { | ||||
|     const firstItem = 1; | ||||
| @@ -74,34 +90,44 @@ export default function Paginate({ | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <nav role="navigation" aria-label="Pagination Navigation" ref={ref}> | ||||
|       <ul className={styles.container}> | ||||
|         <li | ||||
|           className={clsx(styles.li, { [styles.disabled]: !previousEnabled })} | ||||
|         > | ||||
|           <button className={styles.button} onClick={setPreviousPage}> | ||||
|             <FontAwesomeIcon icon="chevron-left" /> | ||||
|           </button> | ||||
|         </li> | ||||
|     <nav | ||||
|       className={clsx("paginate-container", className)} | ||||
|       role="navigation" | ||||
|       aria-label="Pagination Navigation" | ||||
|     > | ||||
|       <button | ||||
|         className="paginate-button" | ||||
|         onClick={setPreviousPage} | ||||
|         disabled={!previousEnabled} | ||||
|       > | ||||
|         <FontAwesomeIcon icon={["fas", "chevron-left"]} /> | ||||
|       </button> | ||||
|       <ul className="paginate-pager"> | ||||
|         {pages.map((page, index) => ( | ||||
|           <li className={styles.li} key={index}> | ||||
|             <button | ||||
|               className={clsx(styles.button, { | ||||
|                 [styles.active]: page === currentPage, | ||||
|                 "pointer-events-none": typeof page !== "number", | ||||
|               })} | ||||
|               onClick={() => typeof page === "number" && setPage(page - 1)} | ||||
|             > | ||||
|               {page} | ||||
|             </button> | ||||
|           <li | ||||
|             key={index} | ||||
|             className={clsx( | ||||
|               "paginate-button", | ||||
|               typeof page !== "number" && "ellipsis", | ||||
|               currentPage === page && "active" | ||||
|             )} | ||||
|             onClick={() => | ||||
|               typeof page === "number" && | ||||
|               currentPage !== page && | ||||
|               setPage(page - 1) | ||||
|             } | ||||
|           > | ||||
|             {page} | ||||
|           </li> | ||||
|         ))} | ||||
|         <li className={clsx(styles.li, { [styles.disabled]: !nextEnabled })}> | ||||
|           <button className={styles.button} onClick={setNextPage}> | ||||
|             <FontAwesomeIcon icon="chevron-right" /> | ||||
|           </button> | ||||
|         </li> | ||||
|       </ul> | ||||
|       <button | ||||
|         className="paginate-button" | ||||
|         onClick={setNextPage} | ||||
|         disabled={!nextEnabled} | ||||
|       > | ||||
|         <FontAwesomeIcon icon={["fas", "chevron-right"]} /> | ||||
|       </button> | ||||
|     </nav> | ||||
|   ); | ||||
| } | ||||
|   | ||||
							
								
								
									
										30
									
								
								website/src/components/Paginate/styles.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								website/src/components/Paginate/styles.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| .paginate { | ||||
|   &-container { | ||||
|     @apply flex items-center justify-center gap-2; | ||||
|   } | ||||
|  | ||||
|   &-button { | ||||
|     @apply flex items-center justify-center cursor-pointer select-none; | ||||
|     @apply w-8 h-8 text-sm leading-8 text-center bg-base-200 text-base-content; | ||||
|  | ||||
|     &.ellipsis { | ||||
|       @apply !text-base-content cursor-default; | ||||
|     } | ||||
|  | ||||
|     &.active { | ||||
|       @apply bg-primary !text-primary-content cursor-default; | ||||
|     } | ||||
|  | ||||
|     &:hover { | ||||
|       @apply text-primary; | ||||
|     } | ||||
|  | ||||
|     &:disabled { | ||||
|       @apply !text-base-content/80 cursor-not-allowed; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &-pager { | ||||
|     @apply flex items-center justify-center gap-2; | ||||
|   } | ||||
| } | ||||
| @@ -1,29 +0,0 @@ | ||||
| .container { | ||||
|   @apply w-full max-w-full inline-flex justify-center items-center m-0 pl-0 list-none select-none; | ||||
| } | ||||
|  | ||||
| .li { | ||||
|   @apply flex items-center; | ||||
| } | ||||
|  | ||||
| .button { | ||||
|   height: 34px; | ||||
|   width: auto; | ||||
|   min-width: 34px; | ||||
|   @apply m-1 px-1 border-2 rounded shadow-lg text-center; | ||||
|   @apply border-light-nonepress-200 shadow-light-nonepress-300; | ||||
|   @apply text-black bg-light-nonepress-100; | ||||
| } | ||||
|  | ||||
| :global(.dark) .button { | ||||
|   @apply border-dark-nonepress-200 shadow-dark-nonepress-300; | ||||
|   @apply text-white bg-dark-nonepress-100; | ||||
| } | ||||
|  | ||||
| .button.active { | ||||
|   @apply bg-hero text-white border-hero; | ||||
| } | ||||
|  | ||||
| .disabled { | ||||
|   @apply opacity-60 pointer-events-none; | ||||
| } | ||||
| @@ -1,211 +0,0 @@ | ||||
| import clsx from "clsx"; | ||||
| import React, { useRef, useState } from "react"; | ||||
| import { ChromePicker } from "react-color"; | ||||
| import { usePagination } from "react-use-pagination"; | ||||
|  | ||||
| import plugins from "../../static/plugins.json"; | ||||
| import { Tag, useFilteredObjs } from "../libs/store"; | ||||
| import Card from "./Card"; | ||||
| import Modal from "./Modal"; | ||||
| import ModalAction from "./ModalAction"; | ||||
| import ModalContent from "./ModalContent"; | ||||
| import ModalTitle from "./ModalTitle"; | ||||
| import Paginate from "./Paginate"; | ||||
| import TagComponent from "./Tag"; | ||||
|  | ||||
| export default function Plugin(): JSX.Element { | ||||
|   const [modalOpen, setModalOpen] = useState<boolean>(false); | ||||
|   const { | ||||
|     filter, | ||||
|     setFilter, | ||||
|     filteredObjs: filteredPlugins, | ||||
|   } = useFilteredObjs(plugins); | ||||
|  | ||||
|   const props = usePagination({ | ||||
|     totalItems: filteredPlugins.length, | ||||
|     initialPageSize: 10, | ||||
|   }); | ||||
|   const { startIndex, endIndex } = props; | ||||
|   const currentPlugins = filteredPlugins.slice(startIndex, endIndex + 1); | ||||
|  | ||||
|   const [form, setForm] = useState<{ | ||||
|     projectLink: string; | ||||
|     moduleName: string; | ||||
|   }>({ projectLink: "", moduleName: "" }); | ||||
|  | ||||
|   const ref = useRef<HTMLInputElement>(null); | ||||
|   const [tags, setTags] = useState<Tag[]>([]); | ||||
|   const [label, setLabel] = useState<string>(""); | ||||
|   const [color, setColor] = useState<string>("#ea5252"); | ||||
|  | ||||
|   const urlEncode = (str: string) => | ||||
|     encodeURIComponent(str).replace(/%2B/gi, "+"); | ||||
|  | ||||
|   const onSubmit = () => { | ||||
|     setModalOpen(false); | ||||
|     const queries: { key: string; value: string }[] = [ | ||||
|       { key: "template", value: "plugin_publish.yml" }, | ||||
|       { | ||||
|         key: "title", | ||||
|         value: form.projectLink && `Plugin: ${form.projectLink}`, | ||||
|       }, | ||||
|       { key: "labels", value: "Plugin" }, | ||||
|       { key: "pypi", value: form.projectLink }, | ||||
|       { key: "module", value: form.moduleName }, | ||||
|       { key: "tags", value: JSON.stringify(tags) }, | ||||
|     ]; | ||||
|     const urlQueries = queries | ||||
|       .filter((query) => !!query.value) | ||||
|       .map((query) => `${query.key}=${urlEncode(query.value)}`) | ||||
|       .join("&"); | ||||
|     window.open(`https://github.com/nonebot/nonebot2/issues/new?${urlQueries}`); | ||||
|   }; | ||||
|   const onChange = (event) => { | ||||
|     const target = event.target; | ||||
|     const value = target.type === "checkbox" ? target.checked : target.value; | ||||
|     const name = target.name; | ||||
|  | ||||
|     setForm({ | ||||
|       ...form, | ||||
|       [name]: value, | ||||
|     }); | ||||
|     event.preventDefault(); | ||||
|   }; | ||||
|   const onChangeLabel = (event) => { | ||||
|     setLabel(event.target.value); | ||||
|   }; | ||||
|   const onChangeColor = (color) => { | ||||
|     setColor(color.hex); | ||||
|   }; | ||||
|   const validateTag = () => { | ||||
|     return label.length >= 1 && label.length <= 10; | ||||
|   }; | ||||
|   const newTag = () => { | ||||
|     if (tags.length >= 3) { | ||||
|       return; | ||||
|     } | ||||
|     if (validateTag()) { | ||||
|       const tag = { label, color }; | ||||
|       setTags([...tags, tag]); | ||||
|     } | ||||
|   }; | ||||
|   const delTag = (index: number) => { | ||||
|     setTags(tags.filter((_, i) => i !== index)); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4 px-4"> | ||||
|         <input | ||||
|           className="w-full px-4 py-2 border rounded-full bg-light-nonepress-100 dark:bg-dark-nonepress-100" | ||||
|           value={filter} | ||||
|           placeholder="搜索插件" | ||||
|           onChange={(event) => setFilter(event.target.value)} | ||||
|         /> | ||||
|         <button | ||||
|           className="w-full rounded-lg bg-hero text-white" | ||||
|           onClick={() => setModalOpen(true)} | ||||
|         > | ||||
|           发布插件 | ||||
|         </button> | ||||
|       </div> | ||||
|       <div className="grid grid-cols-1 p-4"> | ||||
|         <Paginate {...props} /> | ||||
|       </div> | ||||
|       <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-4"> | ||||
|         {currentPlugins.map((plugin, index) => ( | ||||
|           <Card | ||||
|             key={index} | ||||
|             {...plugin} | ||||
|             action={`nb plugin install ${plugin.project_link}`} | ||||
|             actionDisabled={!plugin.project_link} | ||||
|           /> | ||||
|         ))} | ||||
|       </div> | ||||
|       <div className="grid grid-cols-1 p-4"> | ||||
|         <Paginate {...props} /> | ||||
|       </div> | ||||
|       <Modal active={modalOpen} setActive={setModalOpen}> | ||||
|         <ModalTitle title={"插件信息"} /> | ||||
|         <ModalContent> | ||||
|           <form onSubmit={onSubmit}> | ||||
|             <div className="grid grid-cols-1 gap-4 p-4"> | ||||
|               <label className="flex flex-wrap"> | ||||
|                 <span className="mr-2">PyPI 项目名:</span> | ||||
|                 <input | ||||
|                   type="text" | ||||
|                   name="projectLink" | ||||
|                   className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200" | ||||
|                   onChange={onChange} | ||||
|                 /> | ||||
|               </label> | ||||
|               <label className="flex flex-wrap"> | ||||
|                 <span className="mr-2">import 包名:</span> | ||||
|                 <input | ||||
|                   type="text" | ||||
|                   name="moduleName" | ||||
|                   className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200" | ||||
|                   onChange={onChange} | ||||
|                 /> | ||||
|               </label> | ||||
|             </div> | ||||
|           </form> | ||||
|           <div className="px-4"> | ||||
|             <label className="flex flex-wrap"> | ||||
|               <span className="mr-2">标签:</span> | ||||
|               {tags.map((tag, index) => ( | ||||
|                 <TagComponent | ||||
|                   key={index} | ||||
|                   {...tag} | ||||
|                   className="cursor-pointer" | ||||
|                   onClick={() => delTag(index)} | ||||
|                 /> | ||||
|               ))} | ||||
|             </label> | ||||
|           </div> | ||||
|           <div className="px-4 pt-4"> | ||||
|             <input | ||||
|               ref={ref} | ||||
|               type="text" | ||||
|               className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200" | ||||
|               onChange={onChangeLabel} | ||||
|             /> | ||||
|             <ChromePicker | ||||
|               className="mt-2" | ||||
|               color={color} | ||||
|               disableAlpha={true} | ||||
|               onChangeComplete={onChangeColor} | ||||
|             /> | ||||
|  | ||||
|             <div className="flex mt-2"> | ||||
|               <TagComponent label={label} color={color} /> | ||||
|               <button | ||||
|                 className={clsx( | ||||
|                   "px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]", | ||||
|                   { "pointer-events-none opacity-60": !validateTag() } | ||||
|                 )} | ||||
|                 onClick={newTag} | ||||
|               > | ||||
|                 添加标签 | ||||
|               </button> | ||||
|             </div> | ||||
|           </div> | ||||
|         </ModalContent> | ||||
|         <ModalAction> | ||||
|           <button | ||||
|             className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]" | ||||
|             onClick={() => setModalOpen(false)} | ||||
|           > | ||||
|             关闭 | ||||
|           </button> | ||||
|           <button | ||||
|             className="ml-2 px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]" | ||||
|             onClick={onSubmit} | ||||
|           > | ||||
|             发布 | ||||
|           </button> | ||||
|         </ModalAction> | ||||
|       </Modal> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										118
									
								
								website/src/components/Resource/Card/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								website/src/components/Resource/Card/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| import React from "react"; | ||||
|  | ||||
| import clsx from "clsx"; | ||||
|  | ||||
| import Link from "@docusaurus/Link"; | ||||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||||
|  | ||||
| import "./styles.css"; | ||||
| import Tag from "@/components/Resource/Tag"; | ||||
| import type { Resource } from "@/libs/store"; | ||||
|  | ||||
| export type Props = { | ||||
|   resource: Resource; | ||||
|   onClick?: () => void; | ||||
|   onTagClick: (tag: string) => void; | ||||
|   onAuthorClick: () => void; | ||||
|   className?: string; | ||||
| }; | ||||
|  | ||||
| export default function ResourceCard({ | ||||
|   resource, | ||||
|   onClick, | ||||
|   onTagClick, | ||||
|   onAuthorClick, | ||||
|   className, | ||||
| }: Props): JSX.Element { | ||||
|   const isGithub = /^https:\/\/github.com\/[^/]+\/[^/]+/.test( | ||||
|     resource.homepage | ||||
|   ); | ||||
|  | ||||
|   const isPlugin = resource.resourceType === "plugin"; | ||||
|   const registryLink = | ||||
|     isPlugin && | ||||
|     `https://registry.nonebot.dev/plugin/${resource.project_link}:${resource.module_name}`; | ||||
|  | ||||
|   const authorLink = `https://github.com/${resource.author}`; | ||||
|   const authorAvatar = `${authorLink}.png?size=80`; | ||||
|  | ||||
|   return ( | ||||
|     <div | ||||
|       className={clsx( | ||||
|         "resource-card-container", | ||||
|         onClick && "resource-card-container-clickable", | ||||
|         className | ||||
|       )} | ||||
|       onClick={onClick} | ||||
|     > | ||||
|       <div className="resource-card-header"> | ||||
|         <div className="resource-card-header-title"> | ||||
|           {resource.name} | ||||
|           {resource.is_official && ( | ||||
|             <FontAwesomeIcon | ||||
|               className="resource-card-header-check" | ||||
|               icon={["fas", "circle-check"]} | ||||
|             /> | ||||
|           )} | ||||
|         </div> | ||||
|         <div className="resource-card-header-expand"> | ||||
|           <FontAwesomeIcon icon={["fas", "expand"]} /> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div className="resource-card-desc">{resource.desc}</div> | ||||
|       <div className="resource-card-footer"> | ||||
|         <div className="resource-card-footer-tags"> | ||||
|           {resource.tags.map((tag, index) => ( | ||||
|             <Tag | ||||
|               className="resource-card-footer-tag" | ||||
|               key={index} | ||||
|               {...tag} | ||||
|               onClick={() => onTagClick(tag.label)} | ||||
|             /> | ||||
|           ))} | ||||
|         </div> | ||||
|         <div className="divider resource-card-footer-divider"></div> | ||||
|         <div className="resource-card-footer-info"> | ||||
|           <div className="resource-card-footer-group"> | ||||
|             <Link href={resource.homepage}> | ||||
|               {isGithub ? ( | ||||
|                 <FontAwesomeIcon | ||||
|                   className="resource-card-footer-icon" | ||||
|                   icon={["fab", "github"]} | ||||
|                 /> | ||||
|               ) : ( | ||||
|                 <FontAwesomeIcon | ||||
|                   className="resource-card-footer-icon" | ||||
|                   icon={["fas", "link"]} | ||||
|                 /> | ||||
|               )} | ||||
|             </Link> | ||||
|             {isPlugin && ( | ||||
|               <Link href={registryLink as string}> | ||||
|                 <FontAwesomeIcon | ||||
|                   className="resource-card-footer-icon" | ||||
|                   icon={["fas", "cube"]} | ||||
|                 /> | ||||
|               </Link> | ||||
|             )} | ||||
|           </div> | ||||
|           <div className="resource-card-footer-group"> | ||||
|             <div className="avatar"> | ||||
|               <div className="resource-card-footer-avatar"> | ||||
|                 <Link href={authorLink}> | ||||
|                   <img src={authorAvatar} key={resource.author} /> | ||||
|                 </Link> | ||||
|               </div> | ||||
|             </div> | ||||
|             <span | ||||
|               className="resource-card-footer-author" | ||||
|               onClick={onAuthorClick} | ||||
|             > | ||||
|               {resource.author} | ||||
|             </span> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										75
									
								
								website/src/components/Resource/Card/styles.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								website/src/components/Resource/Card/styles.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| .resource-card { | ||||
|   &-container { | ||||
|     @apply flex flex-col gap-y-2 w-full min-h-[12rem] p-4; | ||||
|     @apply transition-colors duration-500 bg-base-200; | ||||
|     @apply border-2 border-base-200 rounded-lg; | ||||
|  | ||||
|     &-clickable { | ||||
|       @apply cursor-pointer hover:border-primary; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &-header { | ||||
|     @apply flex items-center w-full text-lg font-medium; | ||||
|  | ||||
|     &-title { | ||||
|       @apply grow text-left truncate; | ||||
|     } | ||||
|  | ||||
|     &-check { | ||||
|       @apply ml-2 text-success w-5 h-5 fill-current; | ||||
|     } | ||||
|  | ||||
|     &-expand { | ||||
|       @apply flex-none fill-current; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &-desc { | ||||
|     @apply flex-1 w-full text-sm text-ellipsis break-words; | ||||
|   } | ||||
|  | ||||
|   &-footer { | ||||
|     @apply flex flex-col w-full cursor-default; | ||||
|  | ||||
|     &-tags { | ||||
|       @apply flex flex-wrap gap-1; | ||||
|     } | ||||
|  | ||||
|     &-tag { | ||||
|       @apply cursor-pointer; | ||||
|     } | ||||
|  | ||||
|     &-divider { | ||||
|       @apply m-0; | ||||
|     } | ||||
|  | ||||
|     &-info { | ||||
|       @apply flex items-center justify-between w-full; | ||||
|     } | ||||
|  | ||||
|     &-group { | ||||
|       @apply flex items-center justify-center gap-x-1 leading-none; | ||||
|     } | ||||
|  | ||||
|     &-icon { | ||||
|       @apply w-5 h-5 fill-current opacity-80; | ||||
|  | ||||
|       &:hover { | ||||
|         @apply opacity-100; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     &-avatar { | ||||
|       @apply w-5 h-5 rounded-full transition-shadow; | ||||
|  | ||||
|       &:hover { | ||||
|         @apply ring-1 ring-primary ring-offset-base-100 ring-offset-1; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     &-author { | ||||
|       @apply text-sm cursor-pointer; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										32
									
								
								website/src/components/Resource/Tag/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								website/src/components/Resource/Tag/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| import React from "react"; | ||||
|  | ||||
| import clsx from "clsx"; | ||||
|  | ||||
| import "./styles.css"; | ||||
| import { pickTextColor } from "@/libs/color"; | ||||
| import type { Tag } from "@/types/tag"; | ||||
|  | ||||
| export type Props = Tag & { | ||||
|   className?: string; | ||||
|   onClick?: React.MouseEventHandler<HTMLSpanElement>; | ||||
| }; | ||||
|  | ||||
| export default function ResourceTag({ | ||||
|   label, | ||||
|   color, | ||||
|   className, | ||||
|   onClick, | ||||
| }: Props): JSX.Element { | ||||
|   return ( | ||||
|     <span | ||||
|       className={clsx("resource-tag", className)} | ||||
|       style={{ | ||||
|         backgroundColor: color, | ||||
|         color: pickTextColor(color, "#fff", "#000"), | ||||
|       }} | ||||
|       onClick={onClick} | ||||
|     > | ||||
|       {label} | ||||
|     </span> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										4
									
								
								website/src/components/Resource/Tag/styles.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								website/src/components/Resource/Tag/styles.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| .resource-tag { | ||||
|   @apply inline-flex items-center justify-center; | ||||
|   @apply text-xs font-mono w-fit h-5 px-2 rounded; | ||||
| } | ||||
							
								
								
									
										118
									
								
								website/src/components/Searcher/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								website/src/components/Searcher/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| import React, { useRef } from "react"; | ||||
|  | ||||
| import clsx from "clsx"; | ||||
|  | ||||
| import "./styles.css"; | ||||
| import { translate } from "@docusaurus/Translate"; | ||||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||||
|  | ||||
| export type Props = { | ||||
|   onChange: (value: string) => void; | ||||
|   onSubmit: (value: string) => void; | ||||
|   onBackspace: () => void; | ||||
|   onClear: () => void; | ||||
|   onTagClick: (index: number) => void; | ||||
|   tags?: string[]; | ||||
|   className?: string; | ||||
|   placeholder?: string; | ||||
|   disabled?: boolean; | ||||
| }; | ||||
|  | ||||
| export default function Searcher({ | ||||
|   onChange, | ||||
|   onSubmit, | ||||
|   onBackspace, | ||||
|   onClear, | ||||
|   onTagClick, | ||||
|   tags = [], | ||||
|   className, | ||||
|   placeholder, | ||||
|   disabled = false, | ||||
| }: Props): JSX.Element { | ||||
|   const ref = useRef<HTMLInputElement>(null); | ||||
|  | ||||
|   const handleSubmit = (e: React.FormEvent<HTMLInputElement>) => { | ||||
|     onSubmit(e.currentTarget.value); | ||||
|     e.currentTarget.value = ""; | ||||
|     e.preventDefault(); | ||||
|   }; | ||||
|  | ||||
|   const handleEscape = (e: React.KeyboardEvent<HTMLInputElement>) => { | ||||
|     e.currentTarget.value = ""; | ||||
|   }; | ||||
|  | ||||
|   const handleBackspace = (e: React.KeyboardEvent<HTMLInputElement>) => { | ||||
|     if (e.currentTarget.value === "") { | ||||
|       onBackspace(); | ||||
|       e.preventDefault(); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { | ||||
|     switch (e.key) { | ||||
|       case "Enter": { | ||||
|         handleSubmit(e); | ||||
|         break; | ||||
|       } | ||||
|       case "Escape": { | ||||
|         handleEscape(e); | ||||
|         break; | ||||
|       } | ||||
|       case "Backspace": { | ||||
|         handleBackspace(e); | ||||
|         break; | ||||
|       } | ||||
|       default: | ||||
|         break; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleClear = () => { | ||||
|     if (ref.current) { | ||||
|       ref.current.value = ""; | ||||
|       ref.current.focus(); | ||||
|     } | ||||
|     onClear(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <div className={clsx("searcher-box", className)}> | ||||
|       <div className="searcher-container"> | ||||
|         {tags.map((tag, index) => ( | ||||
|           <div | ||||
|             key={index} | ||||
|             className="badge badge-primary searcher-tag" | ||||
|             onClick={() => onTagClick(index)} | ||||
|           > | ||||
|             {tag} | ||||
|           </div> | ||||
|         ))} | ||||
|         <input | ||||
|           ref={ref} | ||||
|           className="searcher-input" | ||||
|           placeholder={ | ||||
|             placeholder ?? | ||||
|             translate({ | ||||
|               id: "theme.searcher.input.placeholder", | ||||
|               description: "Search input placeholder", | ||||
|               message: "搜索", | ||||
|             }) | ||||
|           } | ||||
|           onChange={(e) => onChange(e.currentTarget.value)} | ||||
|           onKeyDown={handleKeyDown} | ||||
|           disabled={disabled} | ||||
|         /> | ||||
|       </div> | ||||
|       <div className="searcher-action" onClick={handleClear}> | ||||
|         <FontAwesomeIcon | ||||
|           className="searcher-action-icon search" | ||||
|           icon={["fas", "magnifying-glass"]} | ||||
|         /> | ||||
|         <FontAwesomeIcon | ||||
|           className="searcher-action-icon close" | ||||
|           icon={["fas", "xmark"]} | ||||
|         /> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										37
									
								
								website/src/components/Searcher/styles.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								website/src/components/Searcher/styles.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| .searcher { | ||||
|   &-box { | ||||
|     @apply flex items-center w-full rounded-3xl bg-base-200; | ||||
|     @apply transition-[color,background-color] duration-500; | ||||
|   } | ||||
|  | ||||
|   &-container { | ||||
|     @apply flex-1 flex items-center flex-wrap gap-x-1 gap-y-2; | ||||
|     @apply pl-5 py-3; | ||||
|   } | ||||
|  | ||||
|   &-tag { | ||||
|     @apply flex-initial shrink-0; | ||||
|     @apply font-medium cursor-pointer select-none; | ||||
|   } | ||||
|  | ||||
|   &-input { | ||||
|     @apply flex-1 text-sm min-w-[10rem]; | ||||
|     @apply bg-transparent border-none outline-none; | ||||
|   } | ||||
|  | ||||
|   &-action { | ||||
|     @apply flex-initial shrink-0 flex items-center justify-center cursor-pointer w-12 h-10; | ||||
|  | ||||
|     &-icon { | ||||
|       @apply h-4 opacity-50; | ||||
|     } | ||||
|  | ||||
|     &:hover &-icon.search { | ||||
|       @apply hidden; | ||||
|     } | ||||
|  | ||||
|     &:not(:hover) &-icon.close { | ||||
|       @apply hidden; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										175
									
								
								website/src/components/Store/Content/Adapter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								website/src/components/Store/Content/Adapter.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,175 @@ | ||||
| import React, { useCallback, useEffect, useState } from "react"; | ||||
|  | ||||
| import Translate from "@docusaurus/Translate"; | ||||
| import { usePagination } from "react-use-pagination"; | ||||
|  | ||||
| import Admonition from "@theme/Admonition"; | ||||
|  | ||||
| import Paginate from "@/components/Paginate"; | ||||
| import ResourceCard from "@/components/Resource/Card"; | ||||
| import Searcher from "@/components/Searcher"; | ||||
| import StoreToolbar, { type Action } from "@/components/Store/Toolbar"; | ||||
| import { authorFilter, tagFilter } from "@/libs/filter"; | ||||
| import { useSearchControl } from "@/libs/search"; | ||||
| import { fetchRegistryData, loadFailedTitle } from "@/libs/store"; | ||||
| import { useToolbar } from "@/libs/toolbar"; | ||||
| import type { Adapter } from "@/types/adapter"; | ||||
|  | ||||
| export default function AdapterPage(): JSX.Element { | ||||
|   const [adapters, setAdapters] = useState<Adapter[] | null>(null); | ||||
|   const adapterCount = adapters?.length ?? 0; | ||||
|   const loading = adapters === null; | ||||
|  | ||||
|   const [error, setError] = useState<Error | null>(null); | ||||
|  | ||||
|   const { | ||||
|     filteredResources: filteredAdapters, | ||||
|     searcherTags, | ||||
|     addFilter, | ||||
|     onSearchQueryChange, | ||||
|     onSearchQuerySubmit, | ||||
|     onSearchBackspace, | ||||
|     onSearchClear, | ||||
|     onSearchTagClick, | ||||
|   } = useSearchControl<Adapter>(adapters ?? []); | ||||
|   const filteredAdapterCount = filteredAdapters.length; | ||||
|  | ||||
|   const { | ||||
|     startIndex, | ||||
|     endIndex, | ||||
|     totalPages, | ||||
|     currentPage, | ||||
|     setNextPage, | ||||
|     setPreviousPage, | ||||
|     setPage, | ||||
|     previousEnabled, | ||||
|     nextEnabled, | ||||
|   } = usePagination({ | ||||
|     totalItems: filteredAdapters.length, | ||||
|     initialPageSize: 12, | ||||
|   }); | ||||
|   const currentAdapters = filteredAdapters.slice(startIndex, endIndex + 1); | ||||
|  | ||||
|   // load adapters asynchronously | ||||
|   useEffect(() => { | ||||
|     fetchRegistryData("adapter") | ||||
|       .then(setAdapters) | ||||
|       .catch((e) => { | ||||
|         setError(e); | ||||
|         console.error(e); | ||||
|       }); | ||||
|   }, []); | ||||
|  | ||||
|   const { filters: filterTools } = useToolbar({ | ||||
|     resources: adapters ?? [], | ||||
|     addFilter, | ||||
|   }); | ||||
|  | ||||
|   const actionTool: Action = { | ||||
|     label: "发布适配器", | ||||
|     icon: ["fas", "plus"], | ||||
|     onClick: () => { | ||||
|       // TODO: open adapter release modal | ||||
|       window.open( | ||||
|         "https://github.com/nonebot/nonebot2/issues/new?template=adapter_publish.yml" | ||||
|       ); | ||||
|     }, | ||||
|   }; | ||||
|  | ||||
|   const onCardClick = useCallback((adapter: Adapter) => { | ||||
|     // TODO: open adapter modal | ||||
|     console.log(adapter, "clicked"); | ||||
|   }, []); | ||||
|  | ||||
|   const onCardTagClick = useCallback( | ||||
|     (tag: string) => { | ||||
|       addFilter(tagFilter(tag)); | ||||
|     }, | ||||
|     [addFilter] | ||||
|   ); | ||||
|  | ||||
|   const onCardAuthorClick = useCallback( | ||||
|     (author: string) => { | ||||
|       addFilter(authorFilter(author)); | ||||
|     }, | ||||
|     [addFilter] | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <p className="store-description"> | ||||
|         {adapterCount === filteredAdapterCount ? ( | ||||
|           <Translate | ||||
|             id="pages.store.adapter.info" | ||||
|             description="Adapters info of the adapter store page" | ||||
|             values={{ adapterCount }} | ||||
|           > | ||||
|             {"当前共有 {adapterCount} 个适配器"} | ||||
|           </Translate> | ||||
|         ) : ( | ||||
|           <Translate | ||||
|             id="pages.store.adapter.searchInfo" | ||||
|             description="Adapters search info of the adapter store page" | ||||
|             values={{ | ||||
|               adapterCount, | ||||
|               filteredAdapterCount, | ||||
|             }} | ||||
|           > | ||||
|             {"当前共有 {filteredAdapterCount} / {adapterCount} 个插件"} | ||||
|           </Translate> | ||||
|         )} | ||||
|       </p> | ||||
|  | ||||
|       <Searcher | ||||
|         className="store-searcher not-prose" | ||||
|         onChange={onSearchQueryChange} | ||||
|         onSubmit={onSearchQuerySubmit} | ||||
|         onBackspace={onSearchBackspace} | ||||
|         onClear={onSearchClear} | ||||
|         onTagClick={onSearchTagClick} | ||||
|         tags={searcherTags} | ||||
|         disabled={loading} | ||||
|       /> | ||||
|  | ||||
|       <StoreToolbar | ||||
|         className="not-prose" | ||||
|         filters={filterTools} | ||||
|         action={actionTool} | ||||
|       /> | ||||
|  | ||||
|       {error ? ( | ||||
|         <Admonition type="caution" title={loadFailedTitle}> | ||||
|           {error.message} | ||||
|         </Admonition> | ||||
|       ) : loading ? ( | ||||
|         <p className="store-loading-container"> | ||||
|           <span className="loading loading-dots loading-lg store-loading"></span> | ||||
|         </p> | ||||
|       ) : ( | ||||
|         <div className="store-container"> | ||||
|           {currentAdapters.map((adapter, index) => ( | ||||
|             <ResourceCard | ||||
|               key={index} | ||||
|               className="not-prose" | ||||
|               resource={adapter} | ||||
|               onClick={() => onCardClick(adapter)} | ||||
|               onTagClick={onCardTagClick} | ||||
|               onAuthorClick={() => onCardAuthorClick(adapter.author)} | ||||
|             /> | ||||
|           ))} | ||||
|         </div> | ||||
|       )} | ||||
|  | ||||
|       <Paginate | ||||
|         className="not-prose" | ||||
|         totalPages={totalPages} | ||||
|         currentPage={currentPage} | ||||
|         setNextPage={setNextPage} | ||||
|         setPreviousPage={setPreviousPage} | ||||
|         setPage={setPage} | ||||
|         nextEnabled={nextEnabled} | ||||
|         previousEnabled={previousEnabled} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										169
									
								
								website/src/components/Store/Content/Bot.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								website/src/components/Store/Content/Bot.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,169 @@ | ||||
| import React, { useCallback, useEffect, useState } from "react"; | ||||
|  | ||||
| import Translate from "@docusaurus/Translate"; | ||||
| import { usePagination } from "react-use-pagination"; | ||||
|  | ||||
| import Admonition from "@theme/Admonition"; | ||||
|  | ||||
| import Paginate from "@/components/Paginate"; | ||||
| import ResourceCard from "@/components/Resource/Card"; | ||||
| import Searcher from "@/components/Searcher"; | ||||
| import StoreToolbar, { type Action } from "@/components/Store/Toolbar"; | ||||
| import { authorFilter, tagFilter } from "@/libs/filter"; | ||||
| import { useSearchControl } from "@/libs/search"; | ||||
| import { fetchRegistryData, loadFailedTitle } from "@/libs/store"; | ||||
| import { useToolbar } from "@/libs/toolbar"; | ||||
| import type { Bot } from "@/types/bot"; | ||||
|  | ||||
| export default function PluginPage(): JSX.Element { | ||||
|   const [bots, setBots] = useState<Bot[] | null>(null); | ||||
|   const botCount = bots?.length ?? 0; | ||||
|   const loading = bots === null; | ||||
|  | ||||
|   const [error, setError] = useState<Error | null>(null); | ||||
|  | ||||
|   const { | ||||
|     filteredResources: filteredBots, | ||||
|     searcherTags, | ||||
|     addFilter, | ||||
|     onSearchQueryChange, | ||||
|     onSearchQuerySubmit, | ||||
|     onSearchBackspace, | ||||
|     onSearchClear, | ||||
|     onSearchTagClick, | ||||
|   } = useSearchControl<Bot>(bots ?? []); | ||||
|   const filteredBotCount = filteredBots.length; | ||||
|  | ||||
|   const { | ||||
|     startIndex, | ||||
|     endIndex, | ||||
|     totalPages, | ||||
|     currentPage, | ||||
|     setNextPage, | ||||
|     setPreviousPage, | ||||
|     setPage, | ||||
|     previousEnabled, | ||||
|     nextEnabled, | ||||
|   } = usePagination({ | ||||
|     totalItems: filteredBots.length, | ||||
|     initialPageSize: 12, | ||||
|   }); | ||||
|   const currentBots = filteredBots.slice(startIndex, endIndex + 1); | ||||
|  | ||||
|   // load bots asynchronously | ||||
|   useEffect(() => { | ||||
|     fetchRegistryData("bot") | ||||
|       .then(setBots) | ||||
|       .catch((e) => { | ||||
|         setError(e); | ||||
|         console.error(e); | ||||
|       }); | ||||
|   }, []); | ||||
|  | ||||
|   const { filters: filterTools } = useToolbar({ | ||||
|     resources: bots ?? [], | ||||
|     addFilter, | ||||
|   }); | ||||
|  | ||||
|   const actionTool: Action = { | ||||
|     label: "发布机器人", | ||||
|     icon: ["fas", "plus"], | ||||
|     onClick: () => { | ||||
|       // TODO: open bot release modal | ||||
|       window.open( | ||||
|         "https://github.com/nonebot/nonebot2/issues/new?template=bot_publish.yml" | ||||
|       ); | ||||
|     }, | ||||
|   }; | ||||
|  | ||||
|   const onCardTagClick = useCallback( | ||||
|     (tag: string) => { | ||||
|       addFilter(tagFilter(tag)); | ||||
|     }, | ||||
|     [addFilter] | ||||
|   ); | ||||
|  | ||||
|   const onAuthorClick = useCallback( | ||||
|     (author: string) => { | ||||
|       addFilter(authorFilter(author)); | ||||
|     }, | ||||
|     [addFilter] | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <p className="store-description"> | ||||
|         {botCount === filteredBotCount ? ( | ||||
|           <Translate | ||||
|             id="pages.store.bot.info" | ||||
|             description="Bots info of the bot store page" | ||||
|             values={{ botCount }} | ||||
|           > | ||||
|             {"当前共有 {botCount} 个机器人"} | ||||
|           </Translate> | ||||
|         ) : ( | ||||
|           <Translate | ||||
|             id="pages.store.bot.searchInfo" | ||||
|             description="Bots search info of the bot store page" | ||||
|             values={{ | ||||
|               botCount, | ||||
|               filteredBotCount, | ||||
|             }} | ||||
|           > | ||||
|             {"当前共有 {filteredBotCount} / {botCount} 个机器人"} | ||||
|           </Translate> | ||||
|         )} | ||||
|       </p> | ||||
|  | ||||
|       <Searcher | ||||
|         className="store-searcher not-prose" | ||||
|         onChange={onSearchQueryChange} | ||||
|         onSubmit={onSearchQuerySubmit} | ||||
|         onBackspace={onSearchBackspace} | ||||
|         onClear={onSearchClear} | ||||
|         onTagClick={onSearchTagClick} | ||||
|         tags={searcherTags} | ||||
|         disabled={loading} | ||||
|       /> | ||||
|  | ||||
|       <StoreToolbar | ||||
|         className="not-prose" | ||||
|         filters={filterTools} | ||||
|         action={actionTool} | ||||
|       /> | ||||
|  | ||||
|       {error ? ( | ||||
|         <Admonition type="caution" title={loadFailedTitle}> | ||||
|           {error.message} | ||||
|         </Admonition> | ||||
|       ) : loading ? ( | ||||
|         <p className="store-loading-container"> | ||||
|           <span className="loading loading-dots loading-lg store-loading"></span> | ||||
|         </p> | ||||
|       ) : ( | ||||
|         <div className="store-container"> | ||||
|           {currentBots.map((bot, index) => ( | ||||
|             <ResourceCard | ||||
|               key={index} | ||||
|               className="not-prose" | ||||
|               resource={bot} | ||||
|               onTagClick={onCardTagClick} | ||||
|               onAuthorClick={() => onAuthorClick(bot.author)} | ||||
|             /> | ||||
|           ))} | ||||
|         </div> | ||||
|       )} | ||||
|  | ||||
|       <Paginate | ||||
|         className="not-prose" | ||||
|         totalPages={totalPages} | ||||
|         currentPage={currentPage} | ||||
|         setNextPage={setNextPage} | ||||
|         setPreviousPage={setPreviousPage} | ||||
|         setPage={setPage} | ||||
|         nextEnabled={nextEnabled} | ||||
|         previousEnabled={previousEnabled} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										151
									
								
								website/src/components/Store/Content/Driver.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								website/src/components/Store/Content/Driver.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | ||||
| import React, { useCallback, useEffect, useState } from "react"; | ||||
|  | ||||
| import Translate from "@docusaurus/Translate"; | ||||
| import { usePagination } from "react-use-pagination"; | ||||
|  | ||||
| import Admonition from "@theme/Admonition"; | ||||
|  | ||||
| import Paginate from "@/components/Paginate"; | ||||
| import ResourceCard from "@/components/Resource/Card"; | ||||
| import Searcher from "@/components/Searcher"; | ||||
| import { authorFilter, tagFilter } from "@/libs/filter"; | ||||
| import { useSearchControl } from "@/libs/search"; | ||||
| import { fetchRegistryData, loadFailedTitle } from "@/libs/store"; | ||||
| import type { Driver } from "@/types/driver"; | ||||
|  | ||||
| export default function DriverPage(): JSX.Element { | ||||
|   const [drivers, setDrivers] = useState<Driver[] | null>(null); | ||||
|   const driverCount = drivers?.length ?? 0; | ||||
|   const loading = drivers === null; | ||||
|  | ||||
|   const [error, setError] = useState<Error | null>(null); | ||||
|  | ||||
|   const { | ||||
|     filteredResources: filteredDrivers, | ||||
|     searcherTags, | ||||
|     addFilter, | ||||
|     onSearchQueryChange, | ||||
|     onSearchQuerySubmit, | ||||
|     onSearchBackspace, | ||||
|     onSearchClear, | ||||
|     onSearchTagClick, | ||||
|   } = useSearchControl<Driver>(drivers ?? []); | ||||
|   const filteredDriverCount = filteredDrivers.length; | ||||
|  | ||||
|   const { | ||||
|     startIndex, | ||||
|     endIndex, | ||||
|     totalPages, | ||||
|     currentPage, | ||||
|     setNextPage, | ||||
|     setPreviousPage, | ||||
|     setPage, | ||||
|     previousEnabled, | ||||
|     nextEnabled, | ||||
|   } = usePagination({ | ||||
|     totalItems: filteredDrivers.length, | ||||
|     initialPageSize: 12, | ||||
|   }); | ||||
|   const currentDrivers = filteredDrivers.slice(startIndex, endIndex + 1); | ||||
|  | ||||
|   // load drivers asynchronously | ||||
|   useEffect(() => { | ||||
|     fetchRegistryData("driver") | ||||
|       .then(setDrivers) | ||||
|       .catch((e) => { | ||||
|         setError(e); | ||||
|         console.error(e); | ||||
|       }); | ||||
|   }, []); | ||||
|  | ||||
|   const onCardClick = useCallback((driver: Driver) => { | ||||
|     // TODO: open driver modal | ||||
|     console.log(driver, "clicked"); | ||||
|   }, []); | ||||
|  | ||||
|   const onCardTagClick = useCallback( | ||||
|     (tag: string) => { | ||||
|       addFilter(tagFilter(tag)); | ||||
|     }, | ||||
|     [addFilter] | ||||
|   ); | ||||
|  | ||||
|   const onAuthorClick = useCallback( | ||||
|     (author: string) => { | ||||
|       addFilter(authorFilter(author)); | ||||
|     }, | ||||
|     [addFilter] | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <p className="store-description"> | ||||
|         {driverCount === filteredDriverCount ? ( | ||||
|           <Translate | ||||
|             id="pages.store.driver.info" | ||||
|             description="Drivers info of the driver store page" | ||||
|             values={{ driverCount }} | ||||
|           > | ||||
|             {"当前共有 {driverCount} 个插件"} | ||||
|           </Translate> | ||||
|         ) : ( | ||||
|           <Translate | ||||
|             id="pages.store.driver.searchInfo" | ||||
|             description="Drivers search info of the driver store page" | ||||
|             values={{ | ||||
|               driverCount, | ||||
|               filteredDriverCount, | ||||
|             }} | ||||
|           > | ||||
|             {"当前共有 {filteredDriverCount} / {driverCount} 个插件"} | ||||
|           </Translate> | ||||
|         )} | ||||
|       </p> | ||||
|  | ||||
|       <Searcher | ||||
|         className="store-searcher not-prose" | ||||
|         onChange={onSearchQueryChange} | ||||
|         onSubmit={onSearchQuerySubmit} | ||||
|         onBackspace={onSearchBackspace} | ||||
|         onClear={onSearchClear} | ||||
|         onTagClick={onSearchTagClick} | ||||
|         tags={searcherTags} | ||||
|         disabled={loading} | ||||
|       /> | ||||
|  | ||||
|       {error ? ( | ||||
|         <Admonition type="caution" title={loadFailedTitle}> | ||||
|           {error.message} | ||||
|         </Admonition> | ||||
|       ) : loading ? ( | ||||
|         <p className="store-loading-container"> | ||||
|           <span className="loading loading-dots loading-lg store-loading"></span> | ||||
|         </p> | ||||
|       ) : ( | ||||
|         <div className="store-container"> | ||||
|           {currentDrivers.map((driver, index) => ( | ||||
|             <ResourceCard | ||||
|               key={index} | ||||
|               className="not-prose" | ||||
|               resource={driver} | ||||
|               onClick={() => onCardClick(driver)} | ||||
|               onTagClick={onCardTagClick} | ||||
|               onAuthorClick={() => onAuthorClick(driver.author)} | ||||
|             /> | ||||
|           ))} | ||||
|         </div> | ||||
|       )} | ||||
|  | ||||
|       <Paginate | ||||
|         className="not-prose" | ||||
|         totalPages={totalPages} | ||||
|         currentPage={currentPage} | ||||
|         setNextPage={setNextPage} | ||||
|         setPreviousPage={setPreviousPage} | ||||
|         setPage={setPage} | ||||
|         nextEnabled={nextEnabled} | ||||
|         previousEnabled={previousEnabled} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										172
									
								
								website/src/components/Store/Content/Plugin.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								website/src/components/Store/Content/Plugin.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | ||||
| import React, { useCallback, useEffect, useState } from "react"; | ||||
|  | ||||
| import Translate from "@docusaurus/Translate"; | ||||
| import { usePagination } from "react-use-pagination"; | ||||
|  | ||||
| import Admonition from "@theme/Admonition"; | ||||
|  | ||||
| import Paginate from "@/components/Paginate"; | ||||
| import ResourceCard from "@/components/Resource/Card"; | ||||
| import Searcher from "@/components/Searcher"; | ||||
| import StoreToolbar, { type Action } from "@/components/Store/Toolbar"; | ||||
| import { authorFilter, tagFilter } from "@/libs/filter"; | ||||
| import { useSearchControl } from "@/libs/search"; | ||||
| import { fetchRegistryData, loadFailedTitle } from "@/libs/store"; | ||||
| import { useToolbar } from "@/libs/toolbar"; | ||||
| import type { Plugin } from "@/types/plugin"; | ||||
|  | ||||
| export default function PluginPage(): JSX.Element { | ||||
|   const [plugins, setPlugins] = useState<Plugin[] | null>(null); | ||||
|   const pluginCount = plugins?.length ?? 0; | ||||
|   const loading = plugins === null; | ||||
|  | ||||
|   const [error, setError] = useState<Error | null>(null); | ||||
|  | ||||
|   const { | ||||
|     filteredResources: filteredPlugins, | ||||
|     searcherTags, | ||||
|     addFilter, | ||||
|     onSearchQueryChange, | ||||
|     onSearchQuerySubmit, | ||||
|     onSearchBackspace, | ||||
|     onSearchClear, | ||||
|     onSearchTagClick, | ||||
|   } = useSearchControl<Plugin>(plugins ?? []); | ||||
|   const filteredPluginCount = filteredPlugins.length; | ||||
|  | ||||
|   const { | ||||
|     startIndex, | ||||
|     endIndex, | ||||
|     totalPages, | ||||
|     currentPage, | ||||
|     setNextPage, | ||||
|     setPreviousPage, | ||||
|     setPage, | ||||
|     previousEnabled, | ||||
|     nextEnabled, | ||||
|   } = usePagination({ | ||||
|     totalItems: filteredPlugins.length, | ||||
|     initialPageSize: 12, | ||||
|   }); | ||||
|   const currentPlugins = filteredPlugins.slice(startIndex, endIndex + 1); | ||||
|  | ||||
|   // load plugins asynchronously | ||||
|   useEffect(() => { | ||||
|     fetchRegistryData("plugin") | ||||
|       .then(setPlugins) | ||||
|       .catch((e) => { | ||||
|         setError(e); | ||||
|         console.error(e); | ||||
|       }); | ||||
|   }, []); | ||||
|  | ||||
|   const { filters: filterTools } = useToolbar({ | ||||
|     resources: plugins ?? [], | ||||
|     addFilter, | ||||
|   }); | ||||
|  | ||||
|   const actionTool: Action = { | ||||
|     label: "发布插件", | ||||
|     icon: ["fas", "plus"], | ||||
|     onClick: () => { | ||||
|       // TODO: open plugin release modal | ||||
|       window.open( | ||||
|         "https://github.com/nonebot/nonebot2/issues/new?template=plugin_publish.yml" | ||||
|       ); | ||||
|     }, | ||||
|   }; | ||||
|  | ||||
|   const onCardClick = useCallback((plugin: Plugin) => { | ||||
|     // TODO: open plugin modal | ||||
|     console.log(plugin, "clicked"); | ||||
|   }, []); | ||||
|  | ||||
|   const onCardTagClick = useCallback( | ||||
|     (tag: string) => { | ||||
|       addFilter(tagFilter(tag)); | ||||
|     }, | ||||
|     [addFilter] | ||||
|   ); | ||||
|  | ||||
|   const onCardAuthorClick = useCallback( | ||||
|     (author: string) => { | ||||
|       addFilter(authorFilter(author)); | ||||
|     }, | ||||
|     [addFilter] | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <p className="store-description"> | ||||
|         {pluginCount === filteredPluginCount ? ( | ||||
|           <Translate | ||||
|             id="pages.store.plugin.info" | ||||
|             description="Plugins info of the plugin store page" | ||||
|             values={{ pluginCount }} | ||||
|           > | ||||
|             {"当前共有 {pluginCount} 个插件"} | ||||
|           </Translate> | ||||
|         ) : ( | ||||
|           <Translate | ||||
|             id="pages.store.plugin.searchInfo" | ||||
|             description="Plugins search info of the plugin store page" | ||||
|             values={{ pluginCount, filteredPluginCount: filteredPluginCount }} | ||||
|           > | ||||
|             {"当前共有 {filteredPluginCount} / {pluginCount} 个插件"} | ||||
|           </Translate> | ||||
|         )} | ||||
|       </p> | ||||
|  | ||||
|       <Searcher | ||||
|         className="store-searcher not-prose" | ||||
|         onChange={onSearchQueryChange} | ||||
|         onSubmit={onSearchQuerySubmit} | ||||
|         onBackspace={onSearchBackspace} | ||||
|         onClear={onSearchClear} | ||||
|         onTagClick={onSearchTagClick} | ||||
|         tags={searcherTags} | ||||
|         disabled={loading} | ||||
|       /> | ||||
|  | ||||
|       <StoreToolbar | ||||
|         className="not-prose" | ||||
|         filters={filterTools} | ||||
|         action={actionTool} | ||||
|       /> | ||||
|  | ||||
|       {error ? ( | ||||
|         <Admonition type="caution" title={loadFailedTitle}> | ||||
|           {error.message} | ||||
|         </Admonition> | ||||
|       ) : loading ? ( | ||||
|         <p className="store-loading-container"> | ||||
|           <span className="loading loading-dots loading-lg store-loading"></span> | ||||
|         </p> | ||||
|       ) : ( | ||||
|         <div className="store-container"> | ||||
|           {currentPlugins.map((plugin, index) => ( | ||||
|             <ResourceCard | ||||
|               key={index} | ||||
|               className="not-prose" | ||||
|               resource={plugin} | ||||
|               onClick={() => onCardClick(plugin)} | ||||
|               onTagClick={onCardTagClick} | ||||
|               onAuthorClick={() => onCardAuthorClick(plugin.author)} | ||||
|             /> | ||||
|           ))} | ||||
|         </div> | ||||
|       )} | ||||
|  | ||||
|       <Paginate | ||||
|         className="not-prose" | ||||
|         totalPages={totalPages} | ||||
|         currentPage={currentPage} | ||||
|         setNextPage={setNextPage} | ||||
|         setPreviousPage={setPreviousPage} | ||||
|         setPage={setPage} | ||||
|         nextEnabled={nextEnabled} | ||||
|         previousEnabled={previousEnabled} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										50
									
								
								website/src/components/Store/Layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								website/src/components/Store/Layout.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| import React from "react"; | ||||
|  | ||||
| import { PageMetadata } from "@docusaurus/theme-common"; | ||||
| import { useDocsVersionCandidates } from "@docusaurus/theme-common/internal"; | ||||
| import { useVersionedSidebar } from "@nullbot/docusaurus-plugin-getsidebar/client"; | ||||
| import { SidebarContentFiller } from "@nullbot/docusaurus-theme-nonepress/contexts"; | ||||
|  | ||||
| import BackToTopButton from "@theme/BackToTopButton"; | ||||
| import Layout from "@theme/Layout"; | ||||
| import Page from "@theme/Page"; | ||||
|  | ||||
| import "./styles.css"; | ||||
|  | ||||
| const SIDEBAR_ID = "ecosystem"; | ||||
|  | ||||
| type Props = { | ||||
|   title: string; | ||||
|   children: React.ReactNode; | ||||
| }; | ||||
|  | ||||
| function StorePage({ title, children }: Props): JSX.Element { | ||||
|   const sidebarItems = useVersionedSidebar( | ||||
|     useDocsVersionCandidates()[0].name, | ||||
|     SIDEBAR_ID | ||||
|   )!; | ||||
|  | ||||
|   return ( | ||||
|     <Page hideTableOfContents reduceContentWidth={false}> | ||||
|       <SidebarContentFiller items={sidebarItems} /> | ||||
|       <article className="prose max-w-full"> | ||||
|         <h1 className="store-title">{title}</h1> | ||||
|         {children} | ||||
|       </article> | ||||
|     </Page> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export default function StoreLayout({ title, ...props }: Props): JSX.Element { | ||||
|   return ( | ||||
|     <> | ||||
|       <PageMetadata title={title} /> | ||||
|  | ||||
|       <Layout> | ||||
|         <BackToTopButton /> | ||||
|  | ||||
|         <StorePage title={title} {...props} /> | ||||
|       </Layout> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										128
									
								
								website/src/components/Store/Toolbar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								website/src/components/Store/Toolbar.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | ||||
| import React, { useState } from "react"; | ||||
|  | ||||
| import clsx from "clsx"; | ||||
|  | ||||
| import type { IconProp } from "@fortawesome/fontawesome-svg-core"; | ||||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||||
|  | ||||
| export type Filter = { | ||||
|   label: string; | ||||
|   icon: IconProp; | ||||
|   choices?: string[]; | ||||
|   onSubmit: (query: string) => void; | ||||
| }; | ||||
|  | ||||
| export type Action = { | ||||
|   label: string; | ||||
|   icon: IconProp; | ||||
|   onClick: () => void; | ||||
| }; | ||||
|  | ||||
| export type Props = { | ||||
|   filters?: Filter[]; | ||||
|   action?: Action; | ||||
|   className?: string; | ||||
| }; | ||||
|  | ||||
| function ToolbarFilter({ | ||||
|   label, | ||||
|   icon, | ||||
|   choices, | ||||
|   onSubmit, | ||||
| }: Filter): JSX.Element { | ||||
|   const [query, setQuery] = useState<string>(""); | ||||
|  | ||||
|   const filteredChoices = choices | ||||
|     ?.filter((choice) => choice.toLowerCase().includes(query.toLowerCase())) | ||||
|     ?.slice(0, 5); | ||||
|  | ||||
|   const handleQuerySubmit = () => { | ||||
|     if (filteredChoices && filteredChoices.length > 0) { | ||||
|       onSubmit(filteredChoices[0]); | ||||
|     } else if (choices === null) { | ||||
|       onSubmit(query); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const onQueryKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { | ||||
|     if (e.key === "Enter") { | ||||
|       handleQuerySubmit(); | ||||
|       e.preventDefault(); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const onChoiceKeyDown = (e: React.KeyboardEvent<HTMLLIElement>) => { | ||||
|     if (e.key === "Enter") { | ||||
|       onSubmit(e.currentTarget.innerText); | ||||
|       e.preventDefault(); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <div className="dropdown"> | ||||
|       <label | ||||
|         className="btn btn-sm btn-outline btn-primary no-animation" | ||||
|         tabIndex={0} | ||||
|       > | ||||
|         <FontAwesomeIcon icon={icon} /> | ||||
|         {label} | ||||
|       </label> | ||||
|       <div className="dropdown-content store-toolbar-dropdown"> | ||||
|         <input | ||||
|           type="text" | ||||
|           placeholder="搜索" | ||||
|           value={query} | ||||
|           onChange={(e) => setQuery(e.target.value)} | ||||
|           onKeyDown={onQueryKeyDown} | ||||
|           className="input input-sm input-bordered w-full" | ||||
|         /> | ||||
|         {filteredChoices && ( | ||||
|           <ul className="menu menu-sm"> | ||||
|             {filteredChoices.map((choice, index) => ( | ||||
|               <li | ||||
|                 key={index} | ||||
|                 onClick={() => onSubmit(choice)} | ||||
|                 onKeyDown={onChoiceKeyDown} | ||||
|               > | ||||
|                 <a tabIndex={0}>{choice}</a> | ||||
|               </li> | ||||
|             ))} | ||||
|           </ul> | ||||
|         )} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export default function StoreToolbar({ | ||||
|   filters, | ||||
|   action, | ||||
|   className, | ||||
| }: Props): JSX.Element | null { | ||||
|   if (!(filters && filters.length > 0) && !action) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div className={clsx("store-toolbar", className)}> | ||||
|       {filters && filters.length > 0 && ( | ||||
|         <div className="store-toolbar-filters"> | ||||
|           {filters.map((filter, index) => ( | ||||
|             <ToolbarFilter key={index} {...filter} /> | ||||
|           ))} | ||||
|         </div> | ||||
|       )} | ||||
|       {action && ( | ||||
|         <div className="store-toolbar-action"> | ||||
|           <button | ||||
|             className="btn btn-sm btn-primary no-animation" | ||||
|             onClick={action.onClick} | ||||
|           > | ||||
|             <FontAwesomeIcon icon={action.icon} /> | ||||
|             {action.label} | ||||
|           </button> | ||||
|         </div> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										38
									
								
								website/src/components/Store/styles.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								website/src/components/Store/styles.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| .store { | ||||
|   &-title { | ||||
|     @apply text-center; | ||||
|   } | ||||
|  | ||||
|   &-description { | ||||
|     @apply text-center; | ||||
|   } | ||||
|  | ||||
|   &-searcher { | ||||
|     @apply max-w-2xl mx-auto my-4; | ||||
|   } | ||||
|  | ||||
|   &-toolbar { | ||||
|     @apply flex items-center justify-center my-4; | ||||
|  | ||||
|     &-filters { | ||||
|       @apply flex grow gap-2; | ||||
|     } | ||||
|  | ||||
|     &-dropdown { | ||||
|       @apply w-36 z-10 m-0 p-2; | ||||
|       @apply rounded-md bg-base-100 shadow-lg border border-base-200; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &-loading { | ||||
|     @apply text-primary; | ||||
|  | ||||
|     &-container { | ||||
|       @apply text-center; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &-container { | ||||
|     @apply grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-6 mt-4 mb-8; | ||||
|   } | ||||
| } | ||||
| @@ -1,38 +0,0 @@ | ||||
| import clsx from "clsx"; | ||||
| import React from "react"; | ||||
|  | ||||
| import { Tag as TagType } from "../../libs/store"; | ||||
|  | ||||
| function pickTextColor(bgColor, lightColor, darkColor) { | ||||
|   var color = bgColor.charAt(0) === "#" ? bgColor.substring(1, 7) : bgColor; | ||||
|   var r = parseInt(color.substring(0, 2), 16); // hexToR | ||||
|   var g = parseInt(color.substring(2, 4), 16); // hexToG | ||||
|   var b = parseInt(color.substring(4, 6), 16); // hexToB | ||||
|   return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? darkColor : lightColor; | ||||
| } | ||||
|  | ||||
| export default function Tag({ | ||||
|   label, | ||||
|   color, | ||||
|   className, | ||||
|   onClick, | ||||
| }: TagType & { | ||||
|   className?: string; | ||||
|   onClick?: React.MouseEventHandler<HTMLSpanElement>; | ||||
| }): JSX.Element { | ||||
|   return ( | ||||
|     <span | ||||
|       className={clsx( | ||||
|         "font-mono inline-flex px-3 rounded-full items-center align-middle mr-2", | ||||
|         className | ||||
|       )} | ||||
|       style={{ | ||||
|         backgroundColor: color, | ||||
|         color: pickTextColor(color, "#fff", "#000"), | ||||
|       }} | ||||
|       onClick={onClick} | ||||
|     > | ||||
|       {label} | ||||
|     </span> | ||||
|   ); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user