From be1bb388924f4422058099dcb0debdd1c857d36a Mon Sep 17 00:00:00 2001 From: kamtschatka Date: Sun, 9 Jun 2024 15:30:56 +0200 Subject: feature(web): Add syntax highlighting to code blocks and a quick copy button. Fixes #195 (#197) * Any plans to support copy to clipboard (markdown code) for notes? #195 added a button to copy the markdown and added code highlighting * Any plans to support copy to clipboard (markdown code) for notes? #195 Changed the copy-button to a generic one added a safeguard and a message to the copy button if copying is not possible * Some code cleanups --------- Co-authored-by: kamtschatka Co-authored-by: MohamedBassem --- .../components/dashboard/bookmarks/TextCard.tsx | 8 +-- .../dashboard/preview/TextContentSection.tsx | 6 +-- .../components/dashboard/settings/AddApiKey.tsx | 22 +++----- apps/web/components/ui/copy-button.tsx | 37 ++++++++++++++ apps/web/components/ui/markdown-component.tsx | 58 ++++++++++++++++++++++ apps/web/package.json | 2 + 6 files changed, 107 insertions(+), 26 deletions(-) create mode 100644 apps/web/components/ui/copy-button.tsx create mode 100644 apps/web/components/ui/markdown-component.tsx (limited to 'apps') diff --git a/apps/web/components/dashboard/bookmarks/TextCard.tsx b/apps/web/components/dashboard/bookmarks/TextCard.tsx index 74b3e8e5..b18efc3d 100644 --- a/apps/web/components/dashboard/bookmarks/TextCard.tsx +++ b/apps/web/components/dashboard/bookmarks/TextCard.tsx @@ -1,10 +1,10 @@ "use client"; import { useState } from "react"; +import { MarkdownComponent } from "@/components/ui/markdown-component"; import { api } from "@/lib/trpc"; import { bookmarkLayoutSwitch } from "@/lib/userLocalSettings/bookmarksLayout"; import { cn } from "@/lib/utils"; -import Markdown from "react-markdown"; import type { ZBookmark } from "@hoarder/shared/types/bookmarks"; import { isBookmarkStillTagging } from "@hoarder/shared-react/utils/bookmarkUtils"; @@ -52,11 +52,7 @@ export default function TextCard({ /> - {bookmarkedText.text} - - } + content={{bookmarkedText.text}} footer={null} wrapTags={true} bookmark={bookmark} diff --git a/apps/web/components/dashboard/preview/TextContentSection.tsx b/apps/web/components/dashboard/preview/TextContentSection.tsx index eba7d28b..2df1e964 100644 --- a/apps/web/components/dashboard/preview/TextContentSection.tsx +++ b/apps/web/components/dashboard/preview/TextContentSection.tsx @@ -1,5 +1,5 @@ +import { MarkdownComponent } from "@/components/ui/markdown-component"; import { ScrollArea } from "@radix-ui/react-scroll-area"; -import Markdown from "react-markdown"; import type { ZBookmark } from "@hoarder/shared/types/bookmarks"; @@ -9,9 +9,7 @@ export function TextContentSection({ bookmark }: { bookmark: ZBookmark }) { } return ( - - {bookmark.content.text} - + {bookmark.content.text} ); } diff --git a/apps/web/components/dashboard/settings/AddApiKey.tsx b/apps/web/components/dashboard/settings/AddApiKey.tsx index 15a78d56..34fd2df7 100644 --- a/apps/web/components/dashboard/settings/AddApiKey.tsx +++ b/apps/web/components/dashboard/settings/AddApiKey.tsx @@ -5,6 +5,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { ActionButton } from "@/components/ui/action-button"; import { Button } from "@/components/ui/button"; +import CopyBtn from "@/components/ui/copy-button"; import { Dialog, DialogClose, @@ -28,19 +29,10 @@ import { Input } from "@/components/ui/input"; import { toast } from "@/components/ui/use-toast"; import { api } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Check, Copy } from "lucide-react"; import { useForm } from "react-hook-form"; import { z } from "zod"; function ApiKeySuccess({ apiKey }: { apiKey: string }) { - const [isCopied, setCopied] = useState(false); - - const onCopy = () => { - navigator.clipboard.writeText(apiKey); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - return (
@@ -49,13 +41,11 @@ function ApiKeySuccess({ apiKey }: { apiKey: string }) {
- + { + return apiKey; + }} + />
); diff --git a/apps/web/components/ui/copy-button.tsx b/apps/web/components/ui/copy-button.tsx new file mode 100644 index 00000000..a51ce902 --- /dev/null +++ b/apps/web/components/ui/copy-button.tsx @@ -0,0 +1,37 @@ +import React, { useEffect } from "react"; +import { Check, Copy } from "lucide-react"; + +export default function CopyBtn({ + className, + getStringToCopy, +}: { + className?: string; + getStringToCopy: () => string; +}) { + const [copyOk, setCopyOk] = React.useState(false); + const [disabled, setDisabled] = React.useState(false); + useEffect(() => { + if (!navigator || !navigator.clipboard) { + setDisabled(true); + } + }); + + const handleClick = async () => { + await navigator.clipboard.writeText(getStringToCopy()); + setCopyOk(true); + setTimeout(() => { + setCopyOk(false); + }, 2000); + }; + + return ( + + ); +} diff --git a/apps/web/components/ui/markdown-component.tsx b/apps/web/components/ui/markdown-component.tsx new file mode 100644 index 00000000..f92cb3a3 --- /dev/null +++ b/apps/web/components/ui/markdown-component.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import CopyBtn from "@/components/ui/copy-button"; +import { cn } from "@/lib/utils"; +import Markdown from "react-markdown"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { dracula } from "react-syntax-highlighter/dist/cjs/styles/prism"; + +function PreWithCopyBtn({ className, ...props }: React.ComponentProps<"pre">) { + const ref = React.useRef(null); + return ( + + { + return ref.current?.textContent ?? ""; + }} + /> +
+    
+  );
+}
+
+export function MarkdownComponent({
+  children: markdown,
+}: {
+  children: string;
+}) {
+  return (
+    ;
+        },
+        code({ className, children, ...props }) {
+          const match = /language-(\w+)/.exec(className ?? "");
+          return match ? (
+            // @ts-expect-error -- Refs are not compatible for some reason
+            
+              {String(children).replace(/\n$/, "")}
+            
+          ) : (
+            
+              {children}
+            
+          );
+        },
+      }}
+    >
+      {markdown}
+    
+  );
+}
diff --git a/apps/web/package.json b/apps/web/package.json
index ebec0278..91743602 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -62,6 +62,7 @@
     "react-markdown": "^9.0.1",
     "react-masonry-css": "^1.0.16",
     "react-select": "^5.8.0",
+    "react-syntax-highlighter": "^15.5.0",
     "sharp": "^0.33.3",
     "superjson": "^2.2.1",
     "tailwind-merge": "^2.2.1",
@@ -76,6 +77,7 @@
     "@types/emoji-mart": "^3.0.14",
     "@types/react": "^18.2.55",
     "@types/react-dom": "^18.2.19",
+    "@types/react-syntax-highlighter": "^15.5.13",
     "autoprefixer": "^10.4.17",
     "postcss": "^8.4.35",
     "tailwindcss": "^3.4.1",
-- 
cgit v1.2.3-70-g09d2