From 188010c209b42eb48d58c9b1df8d53f4630616b3 Mon Sep 17 00:00:00 2001 From: Slowlife Date: Sat, 25 Feb 2023 19:26:18 +0700 Subject: [PATCH] Add button to copy code in markdown (#1845) Resolves #1814 ![image](https://user-images.githubusercontent.com/54318514/221346522-07b94f93-533f-4370-a8ec-3e5230c96418.png) ![image](https://user-images.githubusercontent.com/54318514/221346515-f1ba9a43-1143-4d4f-82db-18a28a2c5778.png) Not sure if using Toast is a bit too much but I think it fits the style --- .../components/Messages/RenderedCodeblock.tsx | 71 +++++++++++++++++++ .../components/Messages/RenderedMarkdown.tsx | 25 +++---- 2 files changed, 81 insertions(+), 15 deletions(-) create mode 100644 website/src/components/Messages/RenderedCodeblock.tsx diff --git a/website/src/components/Messages/RenderedCodeblock.tsx b/website/src/components/Messages/RenderedCodeblock.tsx new file mode 100644 index 0000000000..8ec91189aa --- /dev/null +++ b/website/src/components/Messages/RenderedCodeblock.tsx @@ -0,0 +1,71 @@ +import { Button, Flex, useToast } from "@chakra-ui/react"; +import { Copy, Check } from "lucide-react"; +import { useState, MouseEvent } from "react"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; + +export const RenderedCodeblock = ({ node, inline, className, children, style, ...props }) => { + const match = /language-(\w+)/.exec(className || ""); + const lang = match ? match[1] : ""; + + const [isCopied, setIsCopied] = useState(false); + const toast = useToast(); + + const handleCopyClick = async (event: MouseEvent) => { + event.stopPropagation(); + toast.closeAll(); + + try { + await navigator.clipboard.writeText(String(children)); + + toast({ + title: "Copied to clipboard", + status: "info", + duration: 2000, + isClosable: true, + }); + + setIsCopied(true); + setTimeout(() => setIsCopied(false), 2000); + } catch { + toast({ + title: "Failed to copy", + status: "error", + duration: 2000, + isClosable: true, + }); + } + }; + + return !inline ? ( + + + {String(children).replace(/\n$/, "")} + + + + ) : ( + + {children} + + ); +}; diff --git a/website/src/components/Messages/RenderedMarkdown.tsx b/website/src/components/Messages/RenderedMarkdown.tsx index 93364b12ed..06c59abf87 100644 --- a/website/src/components/Messages/RenderedMarkdown.tsx +++ b/website/src/components/Messages/RenderedMarkdown.tsx @@ -17,9 +17,9 @@ import { useTranslation } from "next-i18next"; import { memo, MouseEvent, useMemo, useState } from "react"; import ReactMarkdown from "react-markdown"; import type { ReactMarkdownOptions } from "react-markdown/lib/react-markdown"; -import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; -import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; import remarkGfm from "remark-gfm"; +import { RenderedCodeblock } from "src/components/Messages/RenderedCodeblock"; + interface RenderedMarkdownProps { markdown: string; } @@ -27,6 +27,7 @@ interface RenderedMarkdownProps { const sx: SystemStyleObject = { overflowX: "auto", pre: { + width: "100%", bg: "transparent", }, code: { @@ -36,6 +37,12 @@ const sx: SystemStyleObject = { bg: "gray.300", p: 0.5, borderRadius: "2px", + _rtl: { + marginLeft: "3.2rem", + }, + _ltr: { + marginRight: "3.2rem", + }, _dark: { bg: "gray.700", }, @@ -87,19 +94,7 @@ const RenderedMarkdown = ({ markdown }: RenderedMarkdownProps) => { const components: ReactMarkdownOptions["components"] = useMemo(() => { return { // eslint-disable-next-line @typescript-eslint/no-unused-vars - code({ node, inline, className, children, style, ...props }) { - const match = /language-(\w+)/.exec(className || ""); - const lang = match ? match[1] : ""; - return !inline ? ( - - {String(children).replace(/\n$/, "")} - - ) : ( - - {children} - - ); - }, + code: RenderedCodeblock, a({ href, ...props }) { if (!href) { return props.children;