├── .env.example ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc ├── README-zh_CN.md ├── README.md ├── components ├── Banner.tsx ├── Chrome.tsx ├── DropDown.tsx ├── EditButton.tsx ├── Footer.tsx ├── GitHub.tsx ├── Header.tsx ├── LoadingCircle.tsx ├── LoadingDots.tsx ├── LoadingDots │ ├── index.module.css │ └── index.tsx ├── Modal │ └── index.tsx ├── Popover.tsx ├── ResizablePanel.tsx ├── Result.tsx ├── RuleCard.tsx ├── SaveButton.tsx ├── Twitter.tsx ├── TwitterIcon.tsx └── UserDropdown.tsx ├── hooks ├── useSignInModal.tsx └── useView.ts ├── lint-staged.config.js ├── messages ├── en.json └── zh.json ├── middleware.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── api │ ├── auth │ │ └── [...nextauth].ts │ ├── generate.ts │ └── rules │ │ ├── [id] │ │ ├── index.ts │ │ └── view.ts │ │ ├── count.ts │ │ └── index.ts ├── explore.tsx ├── index.tsx ├── my │ └── rules.tsx └── r │ └── [id].tsx ├── postcss.config.js ├── prisma └── schema.prisma ├── privacy.md ├── public ├── 1-black.png ├── 2-black.png ├── favicon.ico ├── icon.svg ├── screenshot.png ├── vercel.svg └── vercelLogo.png ├── styles ├── globals.css ├── loading-dots.module.css ├── markdown.css └── prism.css ├── tailwind.config.js ├── tsconfig.json ├── turbo.json └── utils ├── OpenAIStream.ts ├── api.ts ├── auth.ts ├── fetchWithTimeout.ts ├── index.ts ├── planetscale.ts ├── prisma.ts ├── session.ts └── upstash.ts /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=sk- 2 | NEXT_PUBLIC_SECRET= 3 | NEXT_NOTICE=false 4 | # OPENAI_MODEL=text-chat-davinci-002-20221122 text-davinci-003 gpt-3.5-turbo gpt-3.5-turbo-0301 5 | OPENAI_MODEL=gpt-3.5-turbo 6 | DATABASE_URL= 7 | NEXTAUTH_URL= 8 | GOOGLE_CLIENT_ID= 9 | GOOGLE_CLIENT_SECRET= 10 | TWITTER_CLIENT_ID= 11 | TWITTER_CLIENT_SECRET= 12 | GITHUB_ID= 13 | GITHUB_SECRET= 14 | NEXTAUTH_SECRET= 15 | UPSTASH_REDIS_REST_URL= 16 | UPSTASH_REDIS_REST_TOKEN= 17 | OPENAI_HOST= 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | pnpm-lock.yaml 28 | 29 | # local env files 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | .env 39 | 40 | # idea 41 | .idea 42 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .snapshots/ 2 | build/ 3 | dist/ 4 | node_modules/ 5 | .next/ 6 | .vercel/ 7 | 8 | .demo/ 9 | .renderer/ 10 | pnpm-lock.yaml 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "useTabs": false, 6 | "tabWidth": 2, 7 | "bracketSpacing": true, 8 | "jsxBracketSameLine": false, 9 | "arrowParens": "always", 10 | "trailingComma": "none" 11 | } 12 | -------------------------------------------------------------------------------- /README-zh_CN.md: -------------------------------------------------------------------------------- 1 | # 🤖 ESLint GPT 2 | 3 | ![](https://user-images.githubusercontent.com/13595509/228750807-aa1e7e2b-e777-4136-96a1-969b4f5bb90f.png) 4 | 5 | ESLint GPT 是一个使用 Vercel 和 Next.js 构建的工具,可以帮助你自动生成 ESLint 规则。它基于 GPT 技术,可以通过输入示例代码,自动生成对应的 ESLint 规则,从而提高开发效率。 6 | 7 | ### 功能亮点 8 | 9 | - **自动生成 ESLint 规则**:通过输入示例代码,自动生成对应的 ESLint 规则。 10 | - **支持多语言**:目前支持中文和英文,方便不同语言的开发者使用。 11 | - **用户友好的界面**:简洁直观的用户界面,易于操作。 12 | - **保存和分享功能**:可以保存生成的规则,并与他人分享。 13 | 14 | ## 快速开始 15 | 16 | 要开始使用 ESLint GPT,请按照以下步骤操作: 17 | 18 | 1. 克隆本仓库:`git clone https://github.com/ycjcl868/eslint-gpt.git` 19 | 2. 安装依赖:`npm install` 20 | 3. 启动服务:`npm run dev` 21 | 4. 打开浏览器,在地址栏中输入:`http://localhost:3000` 22 | 23 | ## 📖 详细说明 24 | 25 | ESLint GPT 基于 GPT 技术,可以通过输入示例代码,自动生成对应的 ESLint 规则。它使用了 Vercel 和 Next.js 进行构建,具有以下特点: 26 | 27 | - 支持输入示例代码,自动生成 ESLint 规则 28 | - 支持保存和分享生成的规则 29 | - 使用 Vercel 和 Next.js 进行构建,具有高效性和可扩展性 30 | 31 | 如果您对 ESLint GPT 有任何问题或建议,请随时在本仓库的 Issue 中提出。 32 | 33 | ## 🤝 贡献 34 | 35 | 如果您想为 ESLint GPT 做出贡献,欢迎提交 Pull Request 或 Issue。我们欢迎任何形式的贡献,包括但不限于代码、文档、测试用例等。 36 | 37 | 请注意,所有提交的代码必须遵循本仓库的代码贡献指南。我们将会对每个提交进行审核,确保代码的质量和可读性。 38 | 39 | ## 📄 许可证 40 | 41 | ESLint GPT 使用 MIT 许可证进行发布。详情请参阅 LICENSE 文件。 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESLint GPT 🤖 2 | 3 | ![](https://user-images.githubusercontent.com/13595509/228750807-aa1e7e2b-e777-4136-96a1-969b4f5bb90f.png) 4 | 5 | ESLint GPT is an innovative project that combines the power of OpenAI's GPT model and Vercel's Edge Functions to generate custom ESLint rules. This project aims to simplify the process of creating and maintaining ESLint rules for developers. 6 | 7 | ## About ESLint GPT 8 | 9 | ESLint GPT is a project that uses OpenAI's GPT model to generate custom ESLint rules. With the help of Vercel Edge Functions, the generated rules can be easily integrated into any project. This project is perfect for developers who want to customize their ESLint rules based on their specific needs. 10 | 11 | ### Key Features 12 | 13 | - **Automatic ESLint Rule Generation**: Automatically generate corresponding ESLint rules by inputting example code. 14 | - **Multi-language Support**: Currently supports both English and Chinese, making it accessible for developers of different languages. 15 | - **User-friendly Interface**: A clean and intuitive user interface that is easy to navigate. 16 | - **Save and Share Functionality**: Save generated rules and share them with others. 17 | 18 | ## Quick Start 19 | 20 | To get started with ESLint GPT, follow these steps: 21 | 22 | 1. Clone the repository: `git clone https://github.com/ycjcl868/eslint-gpt.git` 23 | 2. Install dependencies: `npm install` 24 | 3. Start the service: `npm run dev` 25 | 4. Open your browser and navigate to: `http://localhost:3000` 26 | 27 | ### Login 28 | 29 | ![](https://user-images.githubusercontent.com/13595509/231453712-0ed9df97-05b2-469c-8c06-458ab47f4b4a.png) 30 | 31 | ### Custom OpenAI Config 32 | 33 | ![](https://user-images.githubusercontent.com/13595509/233825257-24ef5470-3434-4a80-9144-d9d86f090c9c.gif) 34 | 35 | ### Save Rule 36 | 37 | ![](https://user-images.githubusercontent.com/13595509/231453228-247ac0aa-5057-4ad0-aea7-c8848f2f9ade.png) 38 | 39 | ### Sharing Rule 40 | 41 | ![](https://user-images.githubusercontent.com/13595509/231453277-d8cd5ce1-e46f-40ca-ae23-ed05e9dbd667.png) 42 | 43 | ### Delete Rule 44 | 45 | TODO 46 | 47 | ## Getting Started 48 | 49 | To get started with ESLint GPT, you will need to have an OpenAI API key and a Vercel account. Once you have these, you can simply clone the project and follow the instructions in the README file. 50 | 51 | ## Benefits of ESLint GPT 52 | 53 | - Simplifies the process of creating custom ESLint rules 54 | - Saves time by automating the rule generation process 55 | - Provides developers with more flexibility in creating their own rules 56 | - Makes it easier to maintain and update ESLint rules 57 | 58 | ## Contributing 59 | 60 | If you are interested in contributing to ESLint GPT, please feel free to submit a pull request. We welcome all contributions and appreciate your help in making this project even better. 61 | 62 | ## License 63 | 64 | ESLint GPT is licensed under the MIT License. See the LICENSE file for more information. 65 | -------------------------------------------------------------------------------- /components/Banner.tsx: -------------------------------------------------------------------------------- 1 | import { Link as LinkIcon, Check, Eye } from 'lucide-react' 2 | import Image from 'next/image' 3 | import { useRouter } from 'next/router' 4 | import { useSession } from 'next-auth/react' 5 | import { toast } from 'react-hot-toast' 6 | import { useEffect, useState, useRef } from 'react' 7 | import { nFormatter } from '@/utils/index' 8 | import SaveButton from '@/components/SaveButton' 9 | import EditButton from '@/components/EditButton' 10 | import { useTranslations } from 'next-intl' 11 | 12 | export default function Banner({ 13 | detail, 14 | onSave, 15 | onEdit 16 | }: { 17 | detail: any 18 | onSave: any 19 | onEdit: any 20 | }) { 21 | const router = useRouter() 22 | const t = useTranslations('Index') 23 | const { data: session } = useSession() 24 | const copyRef = useRef(null) 25 | const [copied, setCopied] = useState(false) 26 | const [id, setId] = useState(router?.query?.id) 27 | 28 | useEffect(() => { 29 | copyRef?.current?.focus() 30 | }, []) 31 | 32 | // @ts-ignore 33 | const isOwner = detail?.creatorId === session?.user?.id 34 | 35 | return ( 36 |
37 |
38 | 43 | EslintGPT logo 50 |

EslintGPT

51 |
52 |
53 |
54 | {!detail && } 55 | {isOwner && } 56 | {id && ( 57 | 79 | )} 80 | {detail && ( 81 |
82 | 83 |

84 | {nFormatter(detail?.views)} 85 |

86 |
87 | )} 88 |
89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /components/Chrome.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Chrome = (props) => ( 4 | 11 | 16 | 21 | 26 | 31 | 36 | 37 | ) 38 | export default Chrome 39 | -------------------------------------------------------------------------------- /components/DropDown.tsx: -------------------------------------------------------------------------------- 1 | import { Menu, Transition } from '@headlessui/react' 2 | import { 3 | CheckIcon, 4 | ChevronDownIcon, 5 | ChevronUpIcon 6 | } from '@heroicons/react/20/solid' 7 | import { Fragment } from 'react' 8 | import { useTranslations } from 'next-intl' 9 | 10 | function classNames(...classes: string[]) { 11 | return classes.filter(Boolean).join(' ') 12 | } 13 | 14 | export type FormType = 'paragraphForm' | 'outlineForm' 15 | 16 | interface DropDownProps { 17 | form: FormType 18 | setForm: (form: FormType) => void 19 | } 20 | 21 | let forms: FormType[] = ['paragraphForm', 'outlineForm'] 22 | 23 | export default function DropDown({ form, setForm }: DropDownProps) { 24 | const t = useTranslations('Index') 25 | return ( 26 | 27 |
28 | 29 | {t(form)} 30 | 39 |
40 | 41 | 50 | 54 |
55 | {forms.map((formItem) => ( 56 | 57 | {({ active }) => ( 58 | 71 | )} 72 | 73 | ))} 74 |
75 |
76 |
77 |
78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /components/EditButton.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import LoadingCircle from '@/components/LoadingCircle' 3 | import { useSignInModal } from '@/hooks/useSignInModal' 4 | import { useSession } from 'next-auth/react' 5 | import { Edit } from 'lucide-react' 6 | import { useRef } from 'react' 7 | import { useTranslations } from 'next-intl' 8 | 9 | export default function EditButton({ onEdit }: { onEdit: any }) { 10 | const t = useTranslations('Index') 11 | const { data: session } = useSession() 12 | const { SignInModal, setShowSignInModal } = useSignInModal() 13 | 14 | const buttonRef = useRef() 15 | 16 | const [submitting, setSubmitting] = useState(false) 17 | 18 | const handleEdit = async () => { 19 | if (!session?.user) { 20 | setShowSignInModal(true) 21 | } else { 22 | try { 23 | setSubmitting(true) 24 | await onEdit?.() 25 | } finally { 26 | setSubmitting(false) 27 | } 28 | } 29 | } 30 | 31 | return ( 32 | <> 33 | 34 | 48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | export default function Footer() { 4 | return ( 5 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /components/GitHub.tsx: -------------------------------------------------------------------------------- 1 | export default function Github({ className }: { className?: string }) { 2 | return ( 3 | 11 | 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import Link from 'next/link' 3 | import { useSession } from 'next-auth/react' 4 | import { useRouter } from 'next/router' 5 | import { useTranslations } from 'next-intl' 6 | import cls from 'classnames' 7 | import { AnimatePresence, motion } from 'framer-motion' 8 | import UserDropdown from './UserDropdown' 9 | import { Button } from '@geist-ui/core' 10 | 11 | const FADE_IN_ANIMATION_SETTINGS = { 12 | initial: { opacity: 0 }, 13 | animate: { opacity: 1 }, 14 | exit: { opacity: 0 }, 15 | transition: { duration: 0.2 } 16 | } 17 | 18 | const LinkTab = ({ children, hover = true }) => { 19 | const { data: session } = useSession() 20 | return ( 21 |
28 | {children} 29 |
30 | ) 31 | } 32 | 33 | export default function Header(props) { 34 | const { onSignInClick } = props 35 | const { data: session, status } = useSession() 36 | const t = useTranslations('Index') 37 | const { locale, locales, route } = useRouter() 38 | const otherLocale = locales?.find((cur) => cur !== locale) 39 | 40 | return ( 41 |
42 | 43 | header text 50 |

51 | {t('title')} 52 |

53 | 54 | 55 |
56 | 57 | 58 | {t('guide')} 59 | 60 | 61 | 62 | {t('explore')} 63 | 64 | {otherLocale && ( 65 | 66 | 67 | 70 | 71 | 72 | )} 73 |
74 | 75 | {!session && status !== 'loading' ? ( 76 | 81 | Sign In 82 | 83 | ) : ( 84 | 85 | )} 86 | 87 |
88 |
89 |
90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /components/LoadingCircle.tsx: -------------------------------------------------------------------------------- 1 | export default function LoadingCircle({ dimensions }: { dimensions?: string }) { 2 | return ( 3 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /components/LoadingDots.tsx: -------------------------------------------------------------------------------- 1 | import styles from '../styles/loading-dots.module.css' 2 | 3 | const LoadingDots = ({ 4 | color = '#000', 5 | style = 'small' 6 | }: { 7 | color: string 8 | style?: string 9 | }) => { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | } 18 | 19 | export default LoadingDots 20 | -------------------------------------------------------------------------------- /components/LoadingDots/index.module.css: -------------------------------------------------------------------------------- 1 | .loading { 2 | display: inline-flex; 3 | align-items: center; 4 | } 5 | 6 | .loading .spacer { 7 | margin-right: 2px; 8 | } 9 | 10 | .loading span { 11 | animation-name: blink; 12 | animation-duration: 1.4s; 13 | animation-iteration-count: infinite; 14 | animation-fill-mode: both; 15 | width: 5px; 16 | height: 5px; 17 | border-radius: 50%; 18 | display: inline-block; 19 | margin: 0 1px; 20 | } 21 | 22 | .loading span:nth-of-type(2) { 23 | animation-delay: 0.2s; 24 | } 25 | 26 | .loading span:nth-of-type(3) { 27 | animation-delay: 0.4s; 28 | } 29 | 30 | @keyframes blink { 31 | 0% { 32 | opacity: 0.2; 33 | } 34 | 20% { 35 | opacity: 1; 36 | } 37 | 100% { 38 | opacity: 0.2; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /components/LoadingDots/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from './index.module.css' 2 | 3 | const LoadingDots = ({ color = '#000' }: { color?: string }) => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ) 11 | } 12 | 13 | export default LoadingDots 14 | -------------------------------------------------------------------------------- /components/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router' 2 | import { Dispatch, SetStateAction, useCallback, useEffect, useRef } from 'react' 3 | import FocusTrap from 'focus-trap-react' 4 | import { AnimatePresence, motion, useAnimation } from 'framer-motion' 5 | 6 | export default function Modal({ 7 | children, 8 | showModal, 9 | setShowModal, 10 | bgColor = 'bg-white', 11 | closeWithX 12 | }: { 13 | children: React.ReactNode 14 | showModal: boolean 15 | setShowModal: Dispatch> 16 | bgColor?: string 17 | closeWithX?: boolean 18 | }) { 19 | const router = useRouter() 20 | const { key } = router.query 21 | const mobileModalRef = useRef(null) 22 | const desktopModalRef = useRef(null) 23 | 24 | const closeModal = useCallback( 25 | (closeWithX?: boolean) => { 26 | if (closeWithX) { 27 | return 28 | } else if (key) { 29 | router.push('/') 30 | } else { 31 | setShowModal(false) 32 | } 33 | }, 34 | [key, router, setShowModal] 35 | ) 36 | 37 | const onKeyDown = useCallback((e: KeyboardEvent) => { 38 | if (e.key === 'Escape' && !closeWithX) { 39 | setShowModal(false) 40 | } 41 | // eslint-disable-next-line react-hooks/exhaustive-deps 42 | }, []) 43 | 44 | useEffect(() => { 45 | document.addEventListener('keydown', onKeyDown) 46 | return () => document.removeEventListener('keydown', onKeyDown) 47 | }, [onKeyDown]) 48 | 49 | const controls = useAnimation() 50 | const transitionProps = { type: 'spring', stiffness: 500, damping: 30 } 51 | useEffect(() => { 52 | controls.start({ 53 | y: 0, 54 | transition: transitionProps 55 | }) 56 | // eslint-disable-next-line react-hooks/exhaustive-deps 57 | }, []) 58 | 59 | async function handleDragEnd(_: any, info: any) { 60 | const offset = info.offset.y 61 | const velocity = info.velocity.y 62 | // @ts-ignore 63 | const height = mobileModalRef.current.getBoundingClientRect().height 64 | if (offset > height / 2 || velocity > 800) { 65 | await controls.start({ y: '100%', transition: transitionProps }) 66 | closeModal() 67 | } else { 68 | controls.start({ y: 0, transition: transitionProps }) 69 | } 70 | } 71 | 72 | return ( 73 | 74 | {showModal && ( 75 | 76 |
77 | 91 |
94 |
95 |
96 |
97 | {children} 98 | 99 | { 107 | if (desktopModalRef.current === e.target) { 108 | closeModal(closeWithX) 109 | } 110 | }} 111 | > 112 | {children} 113 | 114 | closeModal(closeWithX)} 121 | /> 122 |
123 | 124 | )} 125 | 126 | ) 127 | } 128 | -------------------------------------------------------------------------------- /components/Popover.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useRef } from 'react' 2 | import * as PopoverPrimitive from '@radix-ui/react-popover' 3 | import { AnimatePresence, motion, useAnimation } from 'framer-motion' 4 | 5 | export default function Popover({ 6 | children, 7 | content, 8 | align = 'center', 9 | openPopover, 10 | setOpenPopover 11 | }: { 12 | children: ReactNode 13 | content: ReactNode | string 14 | align?: 'center' | 'start' | 'end' 15 | openPopover: boolean 16 | setOpenPopover: (open: boolean) => void 17 | }) { 18 | const mobileTooltipRef = useRef(null) 19 | const controls = useAnimation() 20 | const transitionProps = { type: 'spring', stiffness: 500, damping: 30 } 21 | 22 | async function handleDragEnd(_: any, info: any) { 23 | const offset = info.offset.y 24 | const velocity = info.velocity.y 25 | const height = mobileTooltipRef.current?.getBoundingClientRect().height || 0 26 | if (offset > height / 2 || velocity > 800) { 27 | await controls.start({ y: '100%', transition: transitionProps }) 28 | setOpenPopover(false) 29 | } else { 30 | controls.start({ y: 0, transition: transitionProps }) 31 | } 32 | } 33 | return ( 34 | <> 35 |
{children}
36 | 37 | {openPopover && ( 38 | <> 39 | 56 |
59 |
60 |
61 |
62 |
63 | {content} 64 |
65 | 66 | setOpenPopover(false)} 73 | /> 74 | 75 | )} 76 | 77 | 78 | 79 | {children} 80 | 81 | 86 | {content} 87 | 88 | 89 | 90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /components/ResizablePanel.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion' 2 | import useMeasure from 'react-use-measure' 3 | 4 | export default function ResizablePanel({ 5 | children 6 | }: { 7 | children: React.ReactNode 8 | }) { 9 | let [ref, { height }] = useMeasure() 10 | 11 | return ( 12 | 18 |
19 | {children} 20 |
21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /components/Result.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence, motion } from 'framer-motion' 2 | import { marked } from 'marked' 3 | import prism from 'prismjs' 4 | import ResizablePanel from './ResizablePanel' 5 | import { useSession } from 'next-auth/react' 6 | import { useState } from 'react' 7 | import { Button, ButtonGroup } from '@geist-ui/core' 8 | import { useTranslations } from 'next-intl' 9 | 10 | interface ResultProps { 11 | value: string 12 | detail?: any 13 | loading: boolean 14 | disable?: boolean 15 | onChange?: (v: string) => void 16 | } 17 | 18 | const Result: React.FC = ({ 19 | detail, 20 | value, 21 | loading, 22 | disable, 23 | onChange 24 | }) => { 25 | const { data: session } = useSession() 26 | const t = useTranslations('Index') 27 | const [editable, setEditable] = useState(false) 28 | if (!value) { 29 | return <> 30 | } 31 | // @ts-ignore 32 | const isOwner = detail?.creatorId === session?.user?.id 33 | 34 | return ( 35 | 36 | 37 | 38 |
39 |
44 | {isOwner && !disable && !loading && ( 45 |
46 | 47 | 59 | 72 | 73 |
74 | )} 75 | {editable ? ( 76 |